2、变量和基本类型
- 1.算术类型
- C++规定sizeof(int)>=sizeof(short),sizeof(long)>=sizeof(int),sizeof(long long)>sizeof(long),long long是C++11中新定义的
- double比float扩展了精度值,计算代价相差无几甚至更快,long double基本没必要的且带来的耗时也不容忽视
- 需要注意的是,char、signed char、unsigned char三者是不一样的,虽然char会表现为后两者之一,但具体是由编译器决定的
- C++没有约定带符号的类型如何表示但约定了正值和负值的量应该平衡
-
无法预知和依赖于实现环境的行为:无法预知的行为源于编译器无须(有时是不能)检测的错误,程序也应尽量避免依赖于实现环境的行为,如把int的尺寸看做是确定不变的,看下表。
原文:https://sourceforge.net/p/predef/wiki/DataModels/Data type LP32 ILP32 LP64 LLP64 ILP64 SILP64 char 8 8 8 8 8 8 short 16 16 16 16 16 64 int 16 32 32 32 64 64 long 32 32 64 32 64 64 long long 64 64 64 64 pointer 32 32 64 64 64 64 - 1.2.字面值常量
- 如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则实际是一个整体。
- 转义序列
\a
,报警响铃符,\b
,退格符,\f
,进纸符,\v
,纵向制表符- 泛化转义序列:
\x
后紧跟1个或多个十六进制数,或\
后紧跟1/2/3
个8进制数
- 指定字面值类型:前缀(
u/U/L/u8
)、后缀(uU lL(long/long Double) fF llLL
)
- 2.变量
C++的每个变量都有类型,决定了所占空间大小和布局方式和能参与的运算
- 变量的定义(下面4种初始化方式都是可以的)
int a1 = 0; int a2 = {0}; int a3{0}; //列表初始化,存在丢失风险时将报错 int a4(0);
- 变量声明和定义的关系
- 声明规定类型和名字,定义还申请空间、赋值
- 声明:由一个基本类型(base type)和一个声明符(declarator)列表组成
- 声明规定类型和名字,定义还申请空间、赋值
- 2、复合类型
- 2.1 引用:为对象起+ 90另外一个名字
int ival = 1024; int &refVal = ival;
- 2.2 指针,实现了对其他对象的间接引用
- 空指针
- :char *p1 = nullptr,*p2 = 0,*p3 = NULL;都可生成空指针
- nullptr是一种特殊类型的字面值
- NULL是预处理变量,和0一样
- void *指针:可存放任意对象的地址
- 空指针
- 2.3 复合类型申明
变量定义:基本数据类型+一组声明符(类型修饰符)
int i=0,*p=&i,&r=i; //一条定义语句,一个基本数据类型可定义出不同类型的变量- 指向指针的引用:引用不是对象,所以不能定义指向引用的指针,但指针是对象
(从右向左读)
int p,&r=p;//r是对指针p的引用
- 指向指针的引用:引用不是对象,所以不能定义指向引用的指针,但指针是对象
- 2.4 const 限定符
默认情况下,const对象仅在文件内有效,C++允许:对于connst变量不管是声明还是定义都加上extern关键字
(gcc编译会报错)
- const的引用:
const int &r1 = ci;//不能修改绑定的对象
- 引用的类型必须与其所引用的对象一致,
except
①初始化常量引用时允许用任意表达式做初始值 ②允许常量引用绑定非常量对象、字面值、表达式 - 常量表达式:值不变,编译过程中能得到计算结果
const int a = 1;//常量表达式
int b = 1;//非常量表达式
const int c = getSize();//非常量表达式(运行时才能获取值) - 可用
constexpr
修饰常量表达式- 指针必须被声明成nullptr或0
- 函数体内定义的变量不存放在固定地址,不能用它修饰
const int *p = nullptr; //指向整型常量的指针
constexpr int *q = nullptr; //指向整数的常量指针,等效于 int *const q
- const的引用:
- 2.5 处理类型
- 类型别名
typedef char * pstring; const pstring p1 = 0; const char *p2 = 0;//注意这个类型和上面的不一样
- auto类型说明符
通过初始值推断变量的类型,会忽略顶层const,保留底层const
- decltype类型指示符
选择并返回操作数的数据类型(包括顶层const和引用在内)
- 类型别名
- 2.6 自定义数据结构
- 2.1 引用:为对象起+ 90另外一个名字
3、字符串、向量和数组
3.1 命名空间的using声明
-
形式:using namespace::name;
3.2 标准库类型string
-
表示可变长的字符序列
- size()的返回值值类型是string::size_type,一个无符号的整数
- string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符两侧对象至少一个时string,如:
string s6 = s1 + ", " + “world”;//正确
string s7 = “hello” + “,” +s1; //错误 <cctype>
中定义了函数isalnum()、isdigit()…- 对string对象的每个元素做某个操作,如
for (auto &c : s) //基于范围的for语句 c = toupper(c); //改成大写
- 使用下标方式迭代:
for(decltype(s.size()) index=0;index!=s.size() &&!isspace(s[index]); ++index) s[index] = toupper(s[index]);
- string 对象上的操作
- 1.初始化对象的方式
- 2.所能执行的操作:函数名调用操作、<<、+等运算符操作
string s;cin>>s1>>s2;
: 会自动忽略开头的空白(\n \t)while(getline(cin,s));
:遇到换行符结束但不会存到s中,返回流参数
3.2 标准库类型vector
表示对象的集合,也叫容器,是一个类模板
#include <vector>
using std::vector
- 编译器根据模板创建类或函数的过程称为实例化
- 初始化如:
vector<T> v5{a,b,c,d,...} vector<T> v5={a,b,c,d,..} //注意这里不是圆括号
3.4 迭代器
- 有效的迭代器或者指向某个元素、或者指向容器中尾元素的下一位置。
- 有迭代器的类型同时拥有返回迭代器的成员,比如都有begin/cbegin()和end/cend()成员
- *iter:返回迭代器所指元素的引用
- iter->mem:解引用该元素,(*iter).mem
- …
- 迭代器类型:
vector<int>::iterator it; // it能读写vector<int>的元素 string::iterator it; // it能读写string对象中的元素 vector<int>::const_iterator it; // it能读不能写vector<int>的元素 string::const_iterator it; // it能读不能写string对象的元素
谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器中添加元素
- 迭代器的运算:iter±n表向前后移动n个元素,iter1-iter2表之间的距离(
difference_type
), - 例:
auto beg = text.begin(),end = text.end(); auto min = text.begin() + (end - beg)/2; while(mid != end && *mid != sought){ if(sought < *mid) end = mid; else beg = mid+1; mid = beg + (end-beg)/2; }
3.5 数组
- 复杂的数组声明
- int *ptrs[10]; //ptrs是含有10个整型指针的数组
- int &refs[10] = /* ? */ //错误,不存在引用的数组
- int (*pArray)[10] = &arr; //指向含有10个整数的数组,从内向外理解
- int (&arrRef)[10] = arr; //引用一个含10个整数的数组
- int *(&array)[10] = ptrs; //array是数组的引用,该数组含有10个指针
- 数组下标为定义在
<cstddef>
中的size_t
类型 - 数组与指针:
int ia[]={0,1,2}; auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素 decltype(ia) ia3 = {0,1,2};//ia3是数组 int *beg=begin(ia); //指向ia的首元素 int *last=end(ia); //指向ia尾元素的下一位置的指针
- 与旧代码的接口:
- 允许使用空字符结束的字符数组来初始化string对象或为string对象赋值
- const char *str=s.c_str();//不能保证返回的数组一直有效
- 数组初始化vector对象:
int int_arr[]={0,1,2,3,4,5}; vector<int> ivec(begin(int_arr),end(int_arr));
4.表达式
4.11 类型转换
- 隐式类型转换
- 表达式中,比int小的会提升为较大的整数类型
- 条件中,非布尔会转换成布尔类型
- 初始化过程中,初始值转换成变量类型
- 算术转换:
- 运算对象会转换成最宽的类型
- 整数值会被转换成浮点类型
- 整型提升
- char、uchar、short、ushort会转int,不能存则转unsigned int
- 较大的char(wchar_t、char16_t、char32_t)会转成int、uint、ulong、llong、ullong中最小的能容纳原类型值得一个
- 无符号类型得运算对象
- 先进行整型提升
- 无符号>=带符号(类型大小),对象转为无符号
- 无符号<带符号(类型大小),如 unsigned int a,long b,若a<=b,a转b,否则转a
- 显示转换:
- 形式:
cast-name<type>(expression)
- type是目标
- expression是要转换的值
- case-name是
①static_case、②dynamic_case、③const_cast、④reinterpret_cast
中的一种- ①.任何具有明确定义的类型转换
double s=static_case<double>(j)/i;
- ②.只能改变运算对象的底层const
const char *pc; char *p=const_cast<char*>(pc);//正确但通过p写行为未定义
- ③为运算对象的位模式提供较低层次上的重新解释
int *ip; char *pc=reinterpret_cast<char*>(ip);//很危险 string str(pc);//可能产生错误
- ①.任何具有明确定义的类型转换
- 形式:
4.12 运算符的优先级表
- p148页
5.语句
5.6 TRY语句块和异常处理
异常:存在运行时的反常行为,超出了函数的正常功能范围,如失去数据库连接、遇到意外输入。—设计系统最难的一部分
- throw表达式:用来表示遇到了无法处理的问题,我们说throw引发了异常
- try语句块:用它处理异常,以try语句块开始,多个catch子句结束。我们称为异常处理代码。
- 一套异常类:用于在throw和相关catch子句之间传递异常的具体信息
- 例:
#include <stdexcept> while(cin>>item1>>item2) { try{ if(item1.isbn() != item2.isbn()) throw runtime_error("Date must refer to same ISBN"); } catch (runtime_error err) { // 提醒用户两个ISBN必须一致,询问是否重新输入 cout<<err.what() <<"\nTry Again? Enter y or n"<<endl; char c; cin>>c; if(!cin || c == 'n') break; } }
- 标准异常:
- <exception>定义了最通用的异常类,只报告异常发生
- <stdexcept>定义了常用的异常类
- <new>定义了bad_alloc异常类型
- <type_info>定义了bad_cast异常类型
6.函数
- 组成:return value、function name、parameter、<body>
- 名字有作用域,对象有生命周期
6.2.6 含有可变形参的函数
-
initializer_list 形参
- 与vector不一样的是,initializer_list对象中的元素永远是常量值,无发改变
void error_msg(ErrCode e,initializer_list<string> il){ cout<<e.mesg()<<": "; for(auto beg=il.begin();beg!=il.end();++beg) cout<<*beg<<" "; cout<<endl; } //调用: error_msg(ErrCode(42),{"test1","test2"});
- 与vector不一样的是,initializer_list对象中的元素永远是常量值,无发改变
-
省略符形参
- 只能出现在形参列表的最后一个位置
- 你的C编译文档会教你怎么使用varargs
-
值是怎么返回的
- 返回的值用于初始化调用点的一个临时变量,该临时变量就是函数调用的结果
- 不要返回局部对象的引用或指针
-
列表初始化返回值
- C++ 11规定函数可以返回花括号包围的值的列表,如:
vector<string> process(){ return {"aa","bb"}; }
- C++ 11规定函数可以返回花括号包围的值的列表,如:
-
主函数main的返回值
- 没有语句隐式返回0
- <cstdlib>中定了两个预处理变量:
EXIT_FAILURE
、EXIT_SUCCESS
-
返回数组指针的函数:数组维度必须跟在函数名后,形参列表跟在函数名后且先于数组维度
Type (*function(parameter_list))[dimension] int (*func(int i))[10];
-
使用尾置返回类型
尾置返回类型跟在形参列表后,
->
开头,原来的位置替换位auto,如:auto func(int i) -> int(*)[10];
-
使用decltype,如:
int odd[]={1,3,5}; int even[]={0,2,4}; decltype(odd) *arrPtr(int i){ return (i%2)?&odd:&even; }
6.4 函数重载
同一作用域内几个函数名字相同但行参列表(类型或数量)不同,称为
重载函数
- 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开
- 如果形参是指针或引用,可通过区分其指向的是常量还是非常量实现函数重载,如:
Record lookup(Account&); //用做Account的引用 Record lookup(const Account&); //新函数,作用于常量的引用
7、类
7.1 抽象数据类型
struct Sales_data {
//构造函数
Sales_data() = default;
Sales_data(const std::string &s):bookNo(s){}
Sales_data(const std::string &s,unsigned n,double p):
bookNo(s),units_sold(n),revenue(p*n){}
Sales_data(std::istream &);
//新成员:关于Sales_data对象的操作
std::string isbn() const {return bookNo;}
Sales_data &combine(const Sales_data&);
double avg_price() const;
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);
std::istream &read(std::istream,Sales_data&);
- 因为this总是指向"这个"对象,所以this是一个常量指针(不允许改变this中保存的地址)
- const成员函数
- isbn()函数后面的const修饰隐式的this指针
- 默认情况下,this指向类类型非常量版本的常量指针,这样的函数称为常量成员函数
常量对象,以及常量对象的引用或指针都只能调用常量成员函数
- 类作用域和成员函数
- 类本身是一个作用域
- 在类的外部定义成员函数
- 定义与申明需要匹配,必须包含所属类名,如:
double Sales_data::avg_prise() const { if(units_sold) return revenue/units_sold; else return 0; }
- 定义与申明需要匹配,必须包含所属类名,如:
- 定义一个返回this对象的函数
// 内置运算符将左侧对象作为当成左值返回,故这里保持一致
Sales_data &Sales_data::combine(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;//返回调用该函数的对象
}
//调用:
total.combine(trans); //解引用得到total的引用
7.1.3 定义类相关的非成员函数
属于类接口的组成部分但不属于类本身
// IO类属于不能被拷贝的类,只能用引用方式
istream &read(istream &is,Sales_data &item){
double price=0;
is>>item.bookNo>>item.units_sold>>price;
item.revenue=price*item.units_sold;
return is;
}
7.1.4 构造函数
控制对象的初始化过程,没有返回类型,
不能被声明成const,因为创建const对象时,知道构造函数完成初始化过程,对象才取得常量属性
-
合成的默认构造函数
- 不提供任何构造函数则隐式定义,无需实参
- 如果类内存在初始值则用它初始化成员,否则默认初始化
-
=default
- 定义一个默认构造函数,作用等同于默认构造函数
- 在类内表该构造函数是内联的,类外不是
-
初始话列表
- 冒号和花括号之间的代码称为构造函数初始化列表
- 被初始值列表忽略的成员使用默认构造函数相同的方式初始化
- 有些编译器不支持类内初始化
-
在类外部定义构造函数
Sales_data::Sales_data(std::istream &is){ read(is,*this);//从is中读取一条交易信息存入this中 }
- 该构造函数初始值列表是空的,在构造函数体内初始化成员
7.1.5 拷贝、赋值和析构
- 如果类包含vector或string成员,则其拷贝、赋值或销毁的合成版本能正常工作,管理动态内存通常不能依赖于合成版本
7.2 访问控制与封装
- public:说明符之后的成员在整个程序内可被访问
- private:说明符之后的成员可以内类的成员函数访问
- class和struct的唯一区别是默认访问权限不同
class Sales_data { public: //添加访问说明符 //构造函数 Sales_data() = default; Sales_data(const std::string &s):bookNo(s){} Sales_data(const std::string &s,unsigned n,double p): bookNo(s),units_sold(n),revenue(p*n){} Sales_data(std::istream &); //新成员:关于Sales_data对象的操作 std::string isbn() const {return bookNo;} Sales_data &combine(const Sales_data&); private: double avg_price() const {return units_sold?revenue/units_sold:0;} //数据成员 std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; // Sales_data的非成员接口函数 Sales_data add(const Sales_data&,const Sales_data&); std::ostream &print(std::ostream&,const Sales_data&); std::istream &read(std::istream,Sales_data&);
7.2.1 友元
类可以允许其他类或函数访问它的非公有成员,方法是令其他类或函数成为它的友元
- 友元的声明仅仅指定了访问权限,如果希望类用户能调用,必须再友元声明之外再声明一次(不过很多编译器未强制限定)
7.3 类的其他特性
- 除了定义数据成员,类还能自定义某种类型在类中的别名(存在访问限制)
#ifndef _SCREEN_H
#define _SCREEN_H
#include <string>
class Screen{
public:
// 必须先定义后使用
typedef std::string::size_type pos;
Screen() = default;//已有构造函数,不加不会自动生成默认构造函数
Screen(pos ht,pos wd,char c):height(ht),width(wd),
contents(ht *wd,c){}
char get() const
{return contents[cursor];} //去读光标处字符,隐式内联
inline char get(pos ht,pos wd) const; //显示内联
Screen &move(pos r,pos c); // 能在之后被设置为内联
// 类型别名,与上等价
// using pos = std::string size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
inline
Screen &Screen::move(pos r,pos c){
pos row = r*width;
cursor = row + c;
return *this;
}
char Screen::get(pos r,pos c)const //在类内声明成inline
{
pos row = r*width; //计算行的位置
return contents[row + c];//返回给定列字符
}
#endif
- 虽然无需在声明和定义的地方同时声明inline,不过最好只在类外定义的地方说明,这样容易理解
- 成员函数也能被重载
- 可变数据成员:
- mutable修饰的成员可在const成员函数中被修改
class Window_mgr{ std::vector<Screen> screens{Screen(24,80,' ')}; };
- 提供类内初始值时必须使用
符号=
或花括号
7.3.2 返回*this的成员函数
- 如:myScreen.set(’#’).display(count);
7.3.3 类类型
- 使用方法:
Sales_data item1; class Sales_data item1; //等价声明,从C语言继承而来
- 类的声明:
class Screen; //前向声明,只声明不定义,不完全类型,可已指向这种类型的指针或引用,也可以声明以不完全类型为参数或者返回类型的函数
- 只有当类全部完成后才算被定义,所以类成员不能时类自己
- 一旦一个名字出现后,它就被认为出现过了(尚未定义),因此允许包含指向自身类型的引用或指针
7.3.4 友元再探
- 友元函数定义在类内部,隐式内联
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类的所有成员(包括私有成员)
- 令成员函数作为友元:
calss Screen{ // Window_mgr::clear 需要在之前被声明 friend void Window_mgr::clear(ScreenIndex); }
- 定义Window_mgr类,申明clear()但不能定义,使用screen前先声明
- 定义Screen,包括对clear的友元声明
- 定义clear
- 函数重载和友元
- 需要对函数中的每一个分别声明
- 友元声明和作用域
- 类和成员函数的声明不是必须出现在友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式假定该名字在当前作用域中是可见的,然而友元本身不一定真的声明在当前作用域中,
甚至在类内部定义该函数,也必须在类外提供相应的声明
,换句话说,仅仅用声明友元函数的类的成员调用该友元函数,它也必须是被声明过的:struct X { friend void f(){/*友元函数可以在类内部定义*/} X(){f();} //错误,f还没被声明 void g(); void h(); } void X::g(){return f();}//错误,f还没被声明 void f(); void X::h(){return f();}//正确,现在f的声明在作用域中了
- 重要的是理解友元函数声明的作用是影响访问权限,它本身并非普通意义上的声明
有的编译器并不强制执行上述关于友元的限定
- 类和成员函数的声明不是必须出现在友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式假定该名字在当前作用域中是可见的,然而友元本身不一定真的声明在当前作用域中,
7.4 类的作用域
- 每个类都会定义它自己的作用域,作用域之外,普通数据和成员只能通过对象、引用或者指针使用成员访问运算符。
Screen::pos ht=24; //使用Screen定义的pos类型
7.4.1 名字查找与类作用域
- 类的定义分成两步:
- 编译成员声明
- 直到类全部可见才编译函数体(所以能使用类中定义的任何名字)
7.5 构造函数再探
- 构造函数的初始值有时必不可少(比如const或引用)
- 成员初始化的顺序:
- 与在类定义中出现的顺序一致,与初始化列表中出现的顺序没有关系
- 默认实参和构造函数
7.5.2 委托构造函数
- 一个委托构造函数使用它所属类的其他构造函数执行自己的初始化过程
class Sales_data {
public:
//非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s,unsigned cnt,double price):
bookNo(s),units_sold(cnt),revenue(cnt*price){}
//其余构造函数全都委托给另一个构造函数
Sales_data():Sales_data("",0,0){}
Sales_data(std::string s):Sales_data(s,0,0){}
Sales_data(std::istream &is):Sales_data()
{read(is,*this);} //委托默认构造函数
}
7.5.3 默认构造函数的作用
- 默认初始化一下情况会发生:
- 不适用任何初始值定义非静态变量或数组
- 类本身含有类类型成员且使用合成默认构造函数
- 类类型成员没有在构造函数初始化列表显示初始化时
7.5.4 隐式类型转换
- 转换构造函数:构造函数只接受一个实参,则实际上定义了转换为此类型的隐式转换机制
- 只允许一步类类型转换
item.combine("9-999-999"); //错误 item.combine(string("9-999-999")); //正确
- 抑制构造函数定义的隐式转换
class Sales_data{ explicit Sales_data(std::istream&); }
- explicit 关键字只对一个实参的构造函数有效
- 只能在类内部声明构造函数时使用,类外不能重复定义
- explicit构造函数只能用于直接初始化
- 为转换显示使用构造函数
item.combine(Sales_data(nukk_book));//正确 item.combine(static_cast<Sales_data>(cin));//正确
7.5.5 聚合类
用户可以直接访问其成员,且具有特殊的初始化语法形式,类似C语言中的struct
- 所有成员是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类、virtual函数
struct Data{ int ival; string s; } Data val={0,"Anna"};
7.5.6 字面值常量类
- 字面值类型的类可能含有constexpr函数成员,成员必须符合constexpr函数的所有要求,他们是隐式const的
- 数据成员都是字面值类型的聚合类是字面值常量类,不是聚合类但满足以下要求也是字面值常量类:
- 成员都是字面值类型
- 至少含有有一个constexpr构造函数
- 若成员有类内初始值,则必须是常量表达式、或成员使用自己的constsxpr构造函数初始化
- 类必须使用析构函数的默认定义
- constexpr构造函数:一般来说应该是空的
class Debug{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b){} //必须初始化所有数据成员
...
}
7.6 类的静态成员
有时候类需要一些成员与类本身相关,而非与类各个对象保持联系
class Account{
public:
void calculate(){amount += amount*interestRate;}//成员函数可以直接访问
static double rate(){return interestRate;}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
}
//在外部定义静态成员时不能重复使用static关键字
void Account::rate(double newRate){
interestRate=newRate;
}
//定义并初始化静态成员
double Account::interestRate=initRate();
- 类的静态成员存在于任何对象之外,对象中不包含任何于静态成员有关的数据
- 静态成员函数也不与任何对象绑定一起,不包含this指针,不能声明为const
- 使用静态成员:
Account c1; double r=Account::rate(); //使用作用域运算符访问静态成员 r=c1.rate(); //通过对象访问
- 静态函数的类内初始化
class Account{ private: static constexpr int period=30; double daily_tbl[period]; } //即使在类内初始化了,类外部也该定义一下 constexpr int Account::period;
- 静态数据成员可以是不完全类型
- 静态数据成员可以作为默认实参
8.IO库
8.1 IO 类
- 头文件:iostream、fstream、sstream
- 关系:ifstream 和 istringstream 都继承自 istream、所以标准库的流特性都可以无差别的适用于普通流、文件流、string流、char、宽字符流版本
- IO对象无拷贝或赋值
- 不能将形参或返回值设置为流类型,通常以引用的方式传递和返回流。读写会改变状态,所以不能是const
- 条件状态
- strm::iostate :机器相关类型,提供表达条件状态的完整功能
- strm::badbit :指出流已经崩溃
- strm::failbit :指出一个IO操作失败了
- strm::eofbit :指出流到达了文件结束
- strm::goodbit:指出流处于错误状态,此值保证为0
- eof(),fail(),bad(),good(),clear(),clear(flags),setstate(flags),rdstate()
8.1.3 管理输出缓冲区
- 缓冲区刷新:
- 程序正常结束
- 缓冲区满
- 操纵符
endl/flush/ends
显式刷新 - 操纵符unitbuf设置流内部状态,清空缓冲区
- 读写被关联的流时,关联到的流缓冲区被刷新,如读cin或写cerr会导致cout缓冲区被刷新
cout<<unitbuf;//输出操作后立即刷新缓冲区 cout<<nounitbuf;//回到正常缓冲方式
waring:程序崩溃,输出缓冲区不会被刷新
- 关联输入输出流
- 当输入流被关联到输出流时,任何试图读取数据的操作会先刷新关联的输出流
cin>>ival;//导致cout的缓存区被刷新
- 当输入流被关联到输出流时,任何试图读取数据的操作会先刷新关联的输出流
tie
-
不带参数返回指向输出流的指针,未关联输出流则返回空指针,第二个版本接收一个指向ostream的指针,将自己关联到此ostream。
-
每个流最多关联到一个流,但多个流可以同时关联到一个ostream
- 带参数 cin.tie(cout);
- 示例,标准库已关联
- ostream *oldTie=cin.tie(nullptr);
- cin不再与其他流关联,oldTie指向当前关联到cin的流
- cin.tie(&cerr);
- cin关联到cerr
- cin.tie(&oldTie);
- 重建cin和cout之间的正常关联
- 带参数 cin.tie(cout);
-
8.2 文件输入输出
ifstream、ofstream、fstream对于读、写、读写
- 除了继承自iostream类型的行为之外,新增了下面的新成员
- fstream fstrm; //创建一个未绑定的流
- fstream fstrm(s); //创建流并打开名为s的文件
- fstream fstrm(s,mode); //指定mode打开文件
- fstrm.open(s); //将流与fstrm绑定
- fstrm.close(); //关闭绑定文件
- fstrm.is_open(); //是否关联文件成功且未关闭
8.2.1 使用文件流
- 提供文件名时open()会被自动调用,旧版本只允许C风格数组,
对一个已经打开的文件流调用open会失败
- 用fstream代替iostream:在要求使用基类对象的地方,可以用继承类型的对象来代替
8.2.3 文件模式
- in/out/app/ate/trunc/binary:读/写/追加/打开后定位到文件尾/截断/二进制方式IO
- out模式打开会丢弃已有数据,同时指定app可避免
ofstream out2("file2",ofstream::out|ofstream::app);
- out模式打开会丢弃已有数据,同时指定app可避免
8.3 string流
- istringstream:从string读取数据
- ostringstream:向string写入数据
- stringstream:读写
9.顺序容器
不依赖元素的值,而与加入时的位置相对应
9.1 顺序容器概述
新标准库容器性能几乎肯定与最精心设计过的同类数据结构一样好(通常更好)
- vector:可变大小数组,在尾部之外插入或删除可能很慢
- deque:双端队列,头尾插入很快
- list:双向链表
- forward_list:单向链表
- array:固定大小数组
- string:保存字符,与vector类似
- 选用原则:
- 除非有很好的理由,否则选vector
- …
9.2 容器库概览
- 迭代器:公用接口的实现方式是相同的
- 范围:[begin(),end())
- 容器类型成员:size_type、iterator、const_iterator
- 标准库array具有固定大小
array<int,42> a; array<int,42> copy = a; array<string,10>
- assign:从不同但相容的类型赋值
list<string> names; vector<const char *> oldstyle; //names = oldstyle; //错误,容器类型不匹配 names.assign(oldstyle.cbegin(),oldstyle.cend());
- swap:交换两个相同类型容器的内容
vector<string> svec1(10); vector<string> svec1(24); swap(svec1,svec2);//常数时间内完成,迭代器、引用、指针都不会失效,但已属于不同的容器
10、泛型算法
?:标准库容器提供的操作集合很小,是因为提供了一组算法,独立与任何特定的容器,所以叫泛型算法
- 泛型算法不依赖与容器,但算法依赖与元素类型的操作
- 算法永远不会执行容器的操作:只运行于迭代器上,执行迭代器的操作(不会改变容器的大小)
10.1 概述
- 大多定义在
<algorithm>
、<numberic>
int ia[] = {1,2,3,3,4,5,6,7}; int *result = find(begin(ia),end(ia),4);
- 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作
- 如 find()依赖于
==
操作符
- 如 find()依赖于
关键概念
:算法永远不会执行容器操作- 只运行于迭代器之上:永远不会改变底层容器的大小
- 可能移动元素、改变元素的值
- 只运行于迭代器之上:永远不会改变底层容器的大小
10.2 初识泛型算法
- 标准库提供的超过一百个算法有一定的结构
- 需要了解算法是否读取、改变、重排元素
10.2.1 只读算法
- 如find()、 accumulate()、equal()
- 算法和元素类型
string sum=accumulate(v.cbegin(),v.cend(),string(""));
- 操作两个序列的算法
// v2至少跟v1一样长 equal(v1.cbegin(),v1.cend(),v2.cbegin());
10.2.2 写容器算法
- 须确保原序列大小不小于要求算法写入的元素数目
- 用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长
fill(v.begin(),v.begin()+v.size()/2,10);//子序列设置为10
- 算法不检查写操作
// fill_n() 假定写入的元素时安全的 fill_n(v.begin(),v.size(),0);
- back_insert()
- 定义在
<iterator>
- 接受一个指向容器的引用,返回绑定的插入迭代器,向该容器赋值时会调用 push_back()
vector<int> vec; auto it = back_inserter(vec); *it = 42; fill_n(back_inserter(vec),10,0);
- 定义在
- 拷贝算法
auto ret = copy(begin(a1),end(a1),a2); // a1拷贝到a2,返回end(a2) replace(li.begin(),li.end(),0,42); replace_copy(li.cbegin(),li.cend(),back_inserter(ivec),0,42)
10.2.3 重排容器元素的算法
- 消除重复单词
void elimDups(vector<string> &words) { sord(words.begin(),words.end()); auto endUnique = unique(words.begin(),words.end()); words.erase(endUnique,words.end()); }
10.3 定制操作
10.3.1 向算法传递函数
- 谓词
- 一元谓词、二元谓词
- 元素类型必须能转化为谓词的参数类型
- 排序算法
bool is_shorter(std::string const& lhs, std::string const& rhs){ return lhs.size() < rhs.size(); } std::stable_sort(v.begin(), v.end(), is_shorter);
10.3.2 lambda 表达式
- lambda 简介
- 一种可调用对象(其他还有函数、函数指针、重载了函数调用运算符的类)
- 参数列表和返回类型可忽略
[捕获列表](参数列表) -> 返回类型 { 函数体 }
- 若 lambda 包含任何单一 return 语句之外的内容,且未指定返回类型,则返回 void
- 向 lambda 传递参数
- 不能有默认参数
- 使用捕获列表
auto wc=find_if(words.begin(),words.end(), [sz](const string &a) // 捕获局部变量 sz {return a.size() >= sz; }); for_each(wc,words.end(),[](const string &s){cout<<s<<" ";});
- lambda 可直接使用局部static变量和所在函数之外声明的名字
10.3.3 lambda 的捕获和返回
- 定义一个 lambda 时, 编译器生成了一个新类类型(和一个类型的对象):
- 参数就是次编译器生成的类类型的未命名对象
- 包含一个对应 lambda 所捕获的变量的数据成员
- 值捕获
- 被捕获的变量时在创建时拷贝
- 引用捕获
- 需确保在引用时是存在的
[&os,c](const string &s){os<<s<<c;}; // ostream 不能拷贝,只能捕获引用
- lambda 作为函数返回时,不能包含引用捕获
- 需确保在引用时是存在的
建议
:可能的话,应尽量避免捕获指针或引用- 隐式捕获:
- 还可根据 lambda 体中的代码来推断我们要用哪些变量
- 捕获列表写一个
=
(值)、&
(引用),可混用[&,c](const string &s){os<<s<<c;}; // 显示捕获必须与隐式捕获方式不同
- 可变 lambda
- 想修改按值捕获的变量,可使用关键字 mutable
auto f = [v] () mutable { return ++v; };
- 引用捕获的变量是否能修改取决于是否是 const 属性
- 指定 lambda 返回类型
- 需使用尾置返回类型
transfrom(v1.begin(),vi.end(),v1.begin(), [](int i)->int{return i>0?i:-i;});
- 需使用尾置返回类型
10.3.4 参数绑定
- 标准库的 bind() 函数
- 定义在
<functional>
中auto newCallable = bind(callable, arg_list);
可看作函数适配器
:接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表
- 定义在
- 绑定 check_size() 的 sz 参数
auto check_size = [](string &l,int sz){ return l.size()>sz; } auto wc = find_if(w.engin(),w.end(), bind(check_size,_l,sz))
- bind()生成一个可调用对象:第二个参数绑定到 sz 的值
- 使用 placeholders 名字
- 对 bing() 调用代码假定之前已恰当使用了 using 声明
using std::placeholders::_1; using namespace std::placeholders; //这种形式也可以
- 对 bing() 调用代码假定之前已恰当使用了 using 声明
- bind() 的参数
- 可用来修正参数的值
auto g=bind(f,a,b,_2,c_1);
- g 将参数传递给 f 的第三个和第五个参数(第一个绑定到 _1)
- 第124分别绑定到a,b,c
- 可用来修正参数的值
- 用 bing() 重排参数顺序
sort(w.begin().w.end(),bind(isShorter, _2, _1));
- bind() 引用参数
- 如果希望绑定一个参数又不拷贝,可用系统 ref() 函数
ostream &print(ostream &os,const string s,char c); for_each(w.begin(),w.end(), bind(print,ref(os),_1,' '));
10.4 再探迭代器
<iteartor>
包含额外的迭代器- 插入迭代器:绑定到容器,用来插入元素
- 流迭代器:用来遍历 IO 流
- 反向迭代器:处理 forword_list 都有
- 移动迭代器
10.4.1 插入迭代器
it=t
:将调用 push_back()等方法- 有三种类型
- back_inserter:创建一个使用 push_back() 的迭代器
- front_inserter
- inserter:第二个参数指向给定容器的迭代器
list<int> l1={1,2,3,4},l2,l3; copy(l1.begin(),l1.end(),front_inserter(l2)); // l2:4,3,2,1 copy(l1.begin(),l1.end(),inserter(l3.begin())); // l2:1,2,3,4
10.4.2 iostream 迭代器
- 虽不是容器,但还是有可用的迭代器
- istream_iterator
- ostream_iterator
- istream_iterator 操作
- 要指定将要读写的对象类型(定义了输入运算符)
istream_iterator<int> in_iter(cin); // 从 cin 读取 int istream_iterator<int> int_eof; // 空迭代器,作为尾迭代器 while( in_iter != eof) vec.push_back(*in_iter++);
- 高级一点的写法:
istream_iterator<int> in_iter(cin), eof; vector<int> vec(in_iter,eof);
- 要指定将要读写的对象类型(定义了输入运算符)
- 使用算法操作流迭代器
istream_iterator<int> in_iter(cin), eof; cout<<accumulate(in,eof,0)<<endl;
- istream_iterator 允许使用的懒惰求值
- istream_iterator 绑定的流直到使用迭代器时才从流中取数据
- 在第一次解引用之前,从流中读取数据操作已经完成
- istream_iterator 绑定的流直到使用迭代器时才从流中取数据
- ostream_iterator 操作
- 可对任何具有输出运算符(<<)的类型定义 ostream_iterator
- 不允许空或尾后尾置的 ostream_iterator
ostream_iterator<T> out(os); ostream_iterator<T> out(os,d); out=val *out, ++out,out++ // 运算符存在但不做任何事情
- 可对任何具有输出运算符(<<)的类型定义 ostream_iterator
- 使用流迭代器处理类类型
10.4.3 反向迭代器
- 例:
for(auto r_iter = vec.crbegin(); t_iter != vec.crend();++r_iter) cout<<*r_iter<<endl; sort(vec.rbegin(),vec.rend());
- 反向迭代器需要递减运算符
- forward_list 和 流迭代器不支持
- 反向迭代器和其他迭代器之间的关系
// 打印最后一个逗号 auto rcomma = find(line.crbegin(),line.crend(),','); cout<<string(rcomma.base(),line.cend())<<endl;
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jkbF5fC3-1615377801069)(./picture/反向迭代器.png)]
10.5 泛型算法结构
- 可分为5个迭代器类别
- 输入迭代器
- 输出迭代器
- 前向迭代器
- 双向迭代器
- 随机访问迭代器
10.5.1 五类迭代器
- C++ 标准指明了泛型和数值型算法的每个迭代器参数的最小类别
- 如 find()至少需要输入迭代器
- replace()至少需要前向迭代器
- 输入迭代器(需支持)
- 相等和不相等运算符
- 递增运算符
- 读取元素的解引用运算符
- 箭头运算符
- 输出迭代器(需支持)
- 递增运算符
- 读取元素的解引用运算符
- 前向迭代器
- 只能在一个方向移动
- 支持输入/出迭代器的所有操作
- forward_list 是前向的
- 双向迭代器
- 支持所有前向迭代器的操作
- 还支持递减运算符
- 随机访问迭代器
- 支持所有双向迭代器的操作
- 支持关系运算符(
<、>、<=、>=
) - 迭代器和一个整数的加减运算
- 用于两个迭代器的减法运算
- 下标运算符
10.5.2 算法形参模板
- 大多具有四种形式
alg(beg,end,otherArgs)
alg(beg,end,dest,otherArgs)
alg(beg,end,beg2,otherArgs)
alg(beg,end,beg2,end2,otherArgs)
- 接受单个目标迭代器的算法
- dest 参数是一个表示算法可写入目的位置的迭代器
- 通常将 dest 绑定到插入迭代器
- 接受两个目标迭代器的算法
- 假定 (beg2,end2)表示的范围至少跟(beg,end)一样大
10.5.3 算法命名规范
- 一些算法使用重载形式传递一个谓词
unique(beg,end); unique(beg,end,comp);
- _if 版本的算法
find(beg,end,val); find_if(beg,end,val,comp);
- 区分拷贝元素的版本和不拷贝元素的版本
reverse(beg,end); reverse_copy(beg,end,dest);
remove_if(v1.begin(),v1.end(),[](int i){return i%2;}); remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),[](int i){return i%2;});
10.6 特定容器算法
- list 和 forward_list 提供了成员函数形式的 sort、merge、remove、reverse
- sqlice() 成员
- 链表特有的操作会改变容器
11、关联容器
简介:关联容器是按照关键字来保存和访问的,顺序容器是按照位置来访问的
- 标准库提供了8个关联容器:
- map
- set
- multimap
- multiset
- unordered_map
- unordered_set
- unordered_multimap
- unordered_multiset
11.1 使用关联容器
- map类型常被称为
关联数组
(下标不必是整数),通过关键字而不是位置来查找值map<string, size_t> word_count; string word; while (cin >> word) ++word_count[word]; for (const auto &w : word_count) cout << w.first << " occurs " << w.second << "times" << endl;
- set 是关键字的简单集合
- 都可进行列表初始化,如:
set<string> ex = {"the","or","and"};
- 都可进行列表初始化,如:
11.2 关联容器概述
- 不支持顺序容器的位置相关操作,如 push_front()、push_back()
- 不支持构造函数或插入操作这种接受一个元素值和一个数量值的操作
- 关联容器的迭代器都是双向的
11.2.1 定义关联容器
- map 必须通知指明关键字类型和值类型
- set 只需指明关键字类型
- 每个关联容器都定义了一个默认构造函数:创建指定类型的空容器
- 可将关联容器初始化为另一个同类型容器的拷贝,或从一个值范围来初始化关联容器
map<string,string> authors = { {"J","Joyce"}, {"A","Austen"} }
- 初始化 multimap 或 multiset
11.2.2 关键字类型的要求
必须定义元素比较方法(
<
小于 )
- 有序容器的关键字类型
严格弱序
- 使用关键字类型的比较函数
// bookstore 中多条记录可以有相同的 ISBN
// bookstore 中的元素以ISBN的顺序进行排列
multiset<Sales_data,decltype(compareIsbn) *> bookstore(compareIsbn);
11.2.3 pair类型
- 定义在
<utility>
中,保存两个数据成员,类似容器- 数据成员是public的,名字分别是 first 和 second
- 创建 pair 对象
pair<string, int>
process(vector<string> &v) {
if(!v.empty())
return {v.back(),v.back().size()}; // 列表初始化
return pair<string,int>{}; // 隐式构造返回值
reutrn make_pair(v.back(),v.back().size());
}
11.3 关联容器操作
- 关联容器定义的类型
- key_type:关键字类型
- mapped_type:每个关键字关联的类型
- value_type:对于map,为
pair<const key_type, mapped_type>
- 关联容器迭代器
- 都支持 begin()和end()操作
- 关联容器和算法
- 通常不对关联容器使用泛型算法,可用于只读取元素的算法
- 使用关联容器定义的专用find成员会比用泛型find快得多
- 真要使用算法,可当作一个源序列或目的位置
11.3.2 添加元素
- insert()成员添加一个元素或一个元素范围
- 可接受一个初始化列表或一对迭代器或一个元素
- 向map添加元素
w.insert({word,1}); w.insert(make_pair(word,1)); w.insert(pair<string, size_t>(word,i)); w.insert(map<string, size_t>::value_type {word,i});
- 检测 insert() 的返回值
- 不包含重复关键字的容器返回一个pair,first是迭代器,second是bool(插入成功还是已存在)
auto ret = w.insert({word,1}); if(!ret.second) ++ret.first->second
- 不包含重复关键字的容器返回一个pair,first是迭代器,second是bool(插入成功还是已存在)
- 展开递增语句
pair<map<string, size_t>::iterator,bool> ret = word_count.insert(make_pair(word,i));
- 向 multiset 或 multimap 添加元素
multimap<string, string> au; au.insert("A","1"); au.insert("A","2"); // 总是返回新元素的迭代器
11.3.3 删除元素
- erase() 可接受一个或一对迭代器来删除一个或一个元素范围,删除成功返回 void
- 关联容器还接受一个 key_type 参数,返回实际删除的数量
11.3.4 map的下标操作
- map 和 unordered_map 提供了下标运算符和对于的at函数
- multimap 和 unordered_multimap 没有下标运算
- 关键字不存在时会创建并插入,同时进行值初始化
- 所以只能对非 const 的map进行下标操作
c[k]; // 不存在则创建 c.at(k); // 不存在抛异常
- 所以只能对非 const 的map进行下标操作
- 使用下标操作的返回值
- 下标运算返回 mapped_type 对象, 解引用返回 value_type 对象
- 下标运算返回一个左值
- 只想判断下标在不在 map 中,切记不能用下标运算
11.3.5 访问元素
- 在关联容器中查找元素的操作
- lower_bound()和upper_bound()不适用于无序容器
- 下标和at()只适用于 非const 的 map/unordered_map
- find(k):第一个关键字为k的元素
- count(k)
- c.lower_bound():第一个不小于k的元素
- c.upper_bound():第一个大于k的元素
- c.equal_rand(k):关键字等于k的元素范围
- 对 map 使用 find 代替下标操作
- 想确定关键字是否存在的情况
- 在 multimap 或 multiset 中查找元素
- 关键字相邻的元素会相邻存储
string item("me"); auto endi = auth.count(item); auto iter = auth.find(item); while(endi) { cout<<iter->second<<end; ++iter; --endi; }
- 关键字相邻的元素会相邻存储
- 一种不同的,面向迭代器的解决方法
for(auto beg = auth.lower_bound(item),end = auth.upper_bound(item); beg != end; ++beg) { cout<<beg->second<<end; }
- equal_range() 函数
for(auto pos = auth.equal_range(item); pos.first != pos.second; ++ pos.first) { cout<<pos->first->second<<end; }
11.3.6 一个单词转换的map
11.4 无序容器
- 若关键字固有就是无序的,或性能可通过哈希技术解决,就可用无序容器
- 管理桶
- 桶接口:
- c.bucket_count() :桶数目
- c.max_bucket_count() :能容纳的最多的桶的数量
- c.bucket_size(n) :第n个桶中元素个数
- c.bucket(k) :关键字k在哪个桶中
- 桶迭代:
- local_iterator
- const_local_iterator
- c.begin(n)、c.end()
- c.cbegin()、c.cend()
- 哈希策略:
- c.load_factor()
- c.max_load_factor()
- c.rehash(n)
- c.reserve(n)
- 桶接口:
- 无序容器对关键字类型的要求
- 自定义的关键字必须提供自己的哈希模板
size_t hasher(const sales_data &sd) { return hash<string> ()(sd.isbn()); } bool eqOp(const sales_data &lhs, const sales_data &rhs) { return lhs.isbn() == rhs.isbn(); } using SD_multiset = unordered_multimap<sales_data,decltype(hasher)*,decltype (eqOp)*>; SD_multiset bookstore(42, hasher, eqOp);
- 自定义的关键字必须提供自己的哈希模板
12、动态内存
12.1 动态内存与智能指针
- 新的标准库提供了两种
智能指针
类型来管理动态对象(自动释放所指对象),在头文件<memory>中- shared_ptr:允许多个指针指向同一个对象
- week_ptr:伴随类, 弱引用, 指向shared_ptr所管理的对象
- unique_ptr:独占所指向的对象
- shared_ptr:允许多个指针指向同一个对象
12.1.1 shared_ptr 类
shared_ptr<string > p1;
:空智能指针- make_shared() 函数
- 类似顺序容器的 emplace 成员, 用其参数来构造给定类型的对象,
- shared_ptr 的拷贝和赋值
- shared_ptr 会记录有多少个指向相同的对象,即
引用计数
- 引用计数为0时会自动释放所管理的对象
- shared_ptr 会记录有多少个指向相同的对象,即
- shared_ptr 自动销毁所管理的对象
- 通过析构函数完成
- 还会自动释放相关联的内存
- shared_ptr 在无用之后仍然保留的一种可能情况是:你将 shared_ptr 放在一个容器中,然后重排了容器,你应确保用 erase 删除那些不再需要的 shared_ptr 元素
- 使用动态生存期的资源的类
- 原因:
- 1.不知道需要使用多少对象
- 2.不知道所需对象的准确类型
- 3.需要在多个对象间共享数据
- 4.个人认为:所需的空间太大,使用栈空间会导致效率低下
- 原因:
12.1.2 直接管理内存
- 自己直接管理内存的类不用依赖类对象拷贝、赋值和销毁操作的任何默认定义,
- 堆空间分配的内存是无名的,因此
new
返回一个对象指针- 内置类型的值是未定义的,类对象使用构造函数初始化
- 如果使用括号包围的初始化器,就能用auto来推断要分配对象的类型, 如:auto p1=new auto(obj);
- 用 new 分配 const 对象是合法的
- 内存耗尽:
- 默认抛出类型为
bad_alloc
的异常int *p1 = new int; // 失败时抛 std::bad_alloc 异常 int *p2 = new (nothrow) int; // 分配失败返回空指针
- 默认抛出类型为
- 释放动态内存
- 传递给 delete 的指针必须指向动态分配的内存
- 释放一块非 new 分配的内存或释放多次,行为是为定义的
- const 对象是可以被销毁的
12.1.3 shared_ptr 和 new 结合使用
- 可用 new 返回的指针来初始化智能指针
shared_ptr<double> p1(new int(43));
- 不要混用智能指针和普通指针(参考练习12.11)
- 将一个 shared_ptr 绑定到普通指针时,就将内存管理的责任交给了 shared_ptr,一旦这样做,就不该再用内置指针来访问指向的内存了
- 不要用 get() 初始化另一个智能指针或为智能指针赋值
get()会返回一个内置指针,但绑定到另一个智能指针上是错误的
, 是否能由此推断引用计数不是通过哈希实现或根本不使用计数方式而是其他数据结构实现- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZC3h58vA-1615377825106)(./picture/12.1.3.png)]
- 其他操作
- 重新赋值:p.reset(new int(1024));
- 会更新引用计数,常与 unique 一起使用
- 重新赋值:p.reset(new int(1024));
12.1.4 智能指针和异常
- 智能指针和哑类:某些为C/C++两种语言设计的类没有良好的析构函数
- 使用自己的释放函数,如
void end_connection(connection *p){disconnection(*p);} shared_ptr<connection> p(&c, end_connection);
- 智能指针陷阱:
- 1.不使用相同的内置指针初始化(或reset)多个智能指针
- 2.不 delete get() 返回的指针
- 3.不使用 get() 初始化或 reset 另一个指针
- 4.如果用了get(),牢记最后一个智能指针销毁后,你的指针就无效了
- 5.如果智能指针不是
new
分配的,记得传递一个删除器
12.1.5 unique_ptr
- 某个时刻只能有一个 unique_ptr 指向一个给定对象
- 类似的,只能采用直接初始化
- 不支持拷贝或赋值
- 可用 release() 或 reset() 转移所有权:
/// 从 p1 转到 p2 unique_ptr<string> p1(p1.release()); unique_ptr<string> p3(new string("Text")); /// 从 p3 转到 p2 p2.reset(p3.release());
- 传递 unique_ptr 参数和返回 unique_ptr
- 可拷贝一个将被销毁的 unique_ptr (编译器会执行一种特殊的拷贝)
unique_ptr<int> clone(int p) { return unique_ptr<int> (new int(p)); } unique_ptr<int> clone(int p) { unique_ptr<int> ret(new int(p)); return ret; }
- 可拷贝一个将被销毁的 unique_ptr (编译器会执行一种特殊的拷贝)
- 向 unique_ptr 传递删除器
- 与 shared_ptr 采用不同的管理方式,在尖括号中提供删除器类型
void f(destination &d, ...) { connection c = connect(&d); /// 打开连接 /// 将连接交给 unique_ptr 来管理并使用自己的释放函数 unique_ptr<connection, decltype(end_connection)*> p (&c, end_connection); /// 退出时会自动被关闭 }
- 与 shared_ptr 采用不同的管理方式,在尖括号中提供删除器类型
12.1.6 weak_ptr
- 不控制所指向对象生存期的智能指针, 绑定到 shared_ptr 不会改变引用计数
- 创建时需要用 shared_ptr 来初始化
- look() 检查对象是否还存在,返回共享对象的 shared_ptr
if(shared_ptr<int> np = wp.lock()) { /// 使用 np 访问共享对象是安全的 }
- 可做伴随指针使用
12.2 动态数组
- C++语言和标准提供了两种一次分配一个对象数组的方法
- new
- allocator
12.2.1 new 和数组
- 类型名后跟方括号,返回的是第一个元素类型的指针:
int *pia = new int[get_size()]; typedef int arrT[42]; int *p = new arrT; //分配一个42个int的数组
- 初始化动态分配对象的数组
int pia = new int[10](); // 10个值初始化为0的int int pia = new int[10]{0,1,2,3...} // 新标准能用初始化器的花括号列表
- 不能在花括号中给出初始化器意味着不能用auto分配数组
- 分配一个空数组是合法的
- 返回空指针
- 可与本身相加减,但不能解引用
- 释放动态数组:在指针前加一个空方括号
delete p; delete [] pa; // pa 必须指向一个动态分配的数组或为空
忽略方括号或单一对象前加方括号行为是未定义的(编译器很可能不会给警告)
- 智能指针和动态数组
- 编译器提供了一个可管理 new 分配的数组的 unique_ptr
unique_ptr<int[]> up(new int[10]); up.release(); // 自动用 delete[] 销毁指针
- shared_ptr 不直接支持管理动态数组(需要提供自己的删除器)
shared_ptr<int> sp(new int[10], [](int *p){delete[] p;}); for(size_t i=0;i<10;i++) *(sp.get() + i) = i;
- 编译器提供了一个可管理 new 分配的数组的 unique_ptr
12.2.2 allocator 类
- 简介:允许分配大块内存时,按需构造对象(内存分配和对象构造分离),没有默认构造函数的类也能动态分配数组
- allocator 类
- 提供一种类型感知的内存分配方法, 分配的内存时原始的,未构造的
allocator<string> alloc; auto const p = alloc.allocator(n); // 分配n个未初始化的string
- 提供一种类型感知的内存分配方法, 分配的内存时原始的,未构造的
- 主要有下面几个方法:allocate(n)、deallocate(p,n)、construct(p,args)、destroy§
- allocator 分配未构造的内存块
alloc.construct(p++,10,'c'); // *p 初始化为 'cccccccccc' alloc.destroy(--p); // 对指向对象进行析构 alloc.deallocate(p,n); // 将系统归还给系统
- 还未构造对象的情况下就使用原始内存时错误的
- 拷贝和填充未初始化内存的算法
auto p = alloc.allocate(vi.size() * 2); auto q = uninitialized_copy(vi.begin(), vi.end(), p); // 返回最后一个构造的元素之后的位置 uninitialized_fill_n(q, vi.size(), 42);
12.3 使用标准库:文本查询程序
在给定文件中查找单词:出现次数及所在行
13.拷贝控制
- 一个类通过五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
- 第一个参数是自身的引用,且任何额外参数都有默认值
class Foo { Foo(const Foo&); // 拷贝构造函数 };
- 在几种情况下会被隐式使用,所以通常不应该是 explicit
- 合成拷贝构造函数
- 即使定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数
- 编译器从给定的对象中依次将每个非static成员拷贝到正在创建的对象中
- 类使用拷贝构造
- 内置的直接拷贝
sales_data::sales_data(const sales_data &orig): bookNo(orig.bookNo), units_sold(orig.units_sold), revenue(orig.revenue){}
- 拷贝初始化
string dots(10,'.'); //直接初始化,普通函数匹配 string dots = string(10,'.'); //拷贝初始化,将右侧的运算对象拷贝到正在创建的对象中
- 使用等号
- 对象做实参,传递给非引用类型的形参
- 非引用类型函数返回一个对象
- 花括号初始化数组元素或聚合类中的成员
- 参数和返回值
- 函数具有非引用类型的返回类型时,返回值用来初始化调用方的结果
自己的参数必须时引用类型
- 函数具有非引用类型的返回类型时,返回值用来初始化调用方的结果
- 拷贝初始化的限制
- 初始值要求通过一个 explicit 的构造函数进行类型转换时:
class CxString { public: int _size; char *_pstr; explicit CxString(int size) { // 使用关键字explicit的类声明, 显示转换 _size = size; } CxString(const char *p) { // 省略... } } CxString string2 = 10; // 这样是不行的, 因为explicit关键字取消了隐式转换 CxString string5 = "bbb"; // 这样是OK的, 调用的是CxString(const char *p) string3 = string1; // 这样是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载
- 编译器可以绕过拷贝构造函数
- 编译器可以(但不是必须)绕过
string book("11.11"); // 虽然绕过,但拷贝构造函数必须在且能访问
- 编译器可以(但不是必须)绕过
13.1.2 拷贝赋值运算符
- 未定义编译器会合成
- 重载赋值运算符
- 本质上时一个函数
- 名字是
operator=
- 有返回类型和参数列表
- 名字是
- 若运算符是成员函数
- 左侧运算对象就绑定到隐式的 this 指针
- 二元运算符的右侧作为显示参数传递
- 编译器通常要求保存在容器中的类型具有赋值运算符,且返回值是左侧运算对象的引用
- 本质上时一个函数
- 合成拷贝赋值运算符
- 常用来禁止对象的赋值
- 或将右侧每个非static对象成员赋予左侧,返回引用
13.1.3 析构函数
- 成员函数,名字是波浪号接函数名,无返回值与参数
- 不能被重载,唯一
- 析构函数的工作
- 按初始化顺序逆序销毁成员
- 销毁发生什么依赖于成员类型
- 什么时候调用析构函数(对象被销毁)
- 离开作用域
- 对象被销毁时,成员被销毁
- 容器被销毁、成员被销毁
- delete 时
- 临时对象:创建的完整表达式结束时
- 合成析构函数
- 未定义时编译器会自动定义
- 本身不直接销毁成员:成员销毁步骤之外的另一部分进行的
13.1.4 三/五法则
需要析构函数的类也需要拷贝和赋值操作
需要拷贝操作的类也需要赋值操作,反之亦然
13.1.5 使用 =default
- 显示要求编译器生成合成版本
sales_data { sales_data() = default; sales_data(const sales_data &) = default; sales_data & operator=(const sales_data &); ~sales_data() = default; }; sales_data &sales_data::operator=(const sales_data &) = default;
13.1.5 阻止拷贝
- 比如 iostream 类阻止了拷贝
- 定义删除的函数
- 虽然声明了,但不能以任何方式使用它
=delete
必须出现在函数第一次声明的时候,可对任何函数使用
struct NoCopy { NoCopy() = default; NoCopy(const NoCopy&) = delete; // 阻止拷贝 NoCopy &operator(const NoCopy&) = delete; // 阻止赋值 ~NoCopy() = default; // 阻止使用合成析构 };
- 析构函数不能是删除的成员
- 成员不能被销毁
- 合成的拷贝构造函数可能是删除的
- 有成员的
析构/拷贝构造、拷贝赋值运算符
是删除或不可访问的,或引用成员或const成员没有类内初始化器
,则合成析构/拷贝构造
时删除的
- 有成员的
- private 拷贝控制
- 将 拷贝构造函数或拷贝赋值运算符声明为 private 可阻止拷贝
- 友元函数仍能拷贝对象,所以还是最好用
=delete
- 友元函数仍能拷贝对象,所以还是最好用
- 将 拷贝构造函数或拷贝赋值运算符声明为 private 可阻止拷贝
13.2 拷贝控制和资源管理
- 需先确定拷贝的语义:
- 行为像值:有自己的状态,与副本完全独立
- 行为像指针:共享状态,使用相同底层数据
13.2.1 行为像值的类
- 类值拷贝赋值运算符
- 通常组合了析构、构造函数的操作
- 似析构:会销毁左侧运算对象的资源
- 似拷贝构造:会从右侧运算对象拷贝数据
- 确保将对象赋予自身也正确
- 异常安全
- 通常组合了析构、构造函数的操作
HasPtr &HasPtr::operator=(const HasPtr &rhs) {
auto newp = new string(*rhs.ps);
delete ps; // 释放旧内存
ps = newp; // 从右侧运算对象拷贝数据到本对象
i = rhs.i;
return *this; // 返回本对象
}
13.2.2 定义行为像指针的类
- 最好的办法是使用
shared_ptr
管理类中的资源- 直接管理的话可用
引用计数
- 直接管理的话可用
- 引用计数
- 工作方式
- 记录共享状态的对象个数
- 拷贝构造函数不分配新的计数器
- 析构递减计数器,变0则释放
- 拷贝赋值运算符递增右侧计数器,递减左侧计数器
- 不能直接作为数据成员
- 工作方式
- 定义一个使用引用计数器的类
class HasPtr { public: HasPtr(const string &s = string()): ps(new string(s)),i(0),use(new size_t(1)){} HasPtr &operator=(const HasPtr&); ~HasPtr(); private: string *ps; int i; size_t *use; }
- 类指针的拷贝成员"篡改"引用计数
HasPtr::~HasPtr(){ if(--*use == 0) { delete ps; delete use; } }
HasPtr &HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; if(--*use == 0){ delete ps; delete use; } ps=rhs.ps; i=rhs.i; use = rhs.use; return *this; }
13.3 交换操作
- 交换元素时常用 swap
- 一次拷贝两次赋值(需避免不必要的内存分配)
- 希望交换指针而非分配新副本
- 一次拷贝两次赋值(需避免不必要的内存分配)
- 编写自己的
swap
- 典型实现
calss HasPtr { friend void swap(HasPtr&, HasPtr&); } inline swap(HasPtr &lhs,HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i,rhs.i); }
- 典型实现
- swap() 应调用 swap(),而不是 std::swap()
- 如果内置成员有自己的swap
void swap(Foo &lhs, Foo &rhs) { using std::swap; swap(lhs.h,rhs.h); // 如果 HasPtr定义了swap,会优先用 HasPtr的(匹配优先级高) }
- 如果内置成员有自己的swap
- 在赋值运算符中使用 swap
- 拷贝并交换技术(自动异常安全、且正确处理自然赋值)
// 注意 rhs 按值传递, 即 HasPtr的拷贝构造函数将右侧运算对象的string 拷贝到 rhs HasPtr &HasPtr::operator=(HasPtr rhs) { // 交换左侧和局部变量 swap(*this,rhs); // rhs 现在指向本对象曾经使用过的内存 return *this; // rhs 被销毁 }
- 拷贝并交换技术(自动异常安全、且正确处理自然赋值)
13.4 拷贝控制示例
- 待回头细看
13.5 动态内存管理类
- 待回头细看
13.6 对象移动
- 某些情况下,移动而非拷贝会大幅提升性能
13.6.1 右值引用
- 必须绑定到右值的引用
- 通过
&&
来获得右值引用 - 只能绑定到一个要销毁的对象
一般而言,左值表达式表示对象身份,右值表达式表示对象的值
- 可以绑定到一个表达式,但不能绑定到一个左值上(与左值引用相反)
int i=42; const int &r3 = i*42; int &&rr2 = i*42;
左值表达式:返回
左值引用
的函数,赋值、下标、解引用和前置递增/递减运算符,可绑定到左值引用
右值:返回非引用类型的函数,算数、关系、位运算符及后置递增/递减运算符,都生成右值,可绑定到const的左值引用
- 通过
- 左值持久,右值短暂
- 右值要么是字面值常量,要么是表达式求值过程中创建的临时对象
- 所引用的对象将要被销毁
- 该对象没有其他用户
- 右值要么是字面值常量,要么是表达式求值过程中创建的临时对象
- 变量是左值
- 变量:只有运算对象,没有运算符的表达式,都是左值
- 所以不能将一个右值引用直接绑定到一个变量上
- 标准库 move 函数
- 定义在
<utility>
- 获得绑定到左值上的右值引用
int &rr3=std::move(rr1);
- 调用 move 即承诺不再使用 rr1
- 可销毁
移后源
,也可赋新值,但不能使用对象的值
- 可销毁
- move 不提供 using 声明
- 调用 move 即承诺不再使用 rr1
- 定义在
13.6.2 移动构造函数和移动赋值运算符
- 移动构造函数的第一个参数是该类的一个
右值引用
- 需确保移后源销毁后是无害的
strVec::strVec(strVec &&s) noexcept :elements(s.elements),first_free(s.first_free),cap(s.cap) { // 使s:运行析构是安全的 s.element = s.first_free = s.cap = nullptr; }
- 移动操作、标准库容器异常
- 移动操作’窃取’资源,如果不抛异常,必须标记为’noexcept’, 应通知标准库避免额外的操作
noexcept
需同时加在头文件和源文件- 注意:为了满足发生异常时,旧容器保持不变的要求,我们必须在明确知道元素类型不会抛异常的情况下才能用移动构造函数,否则应使用拷贝构造函数,
- 移动赋值运算符
- 同样,不抛异常就应该标记为
noexcept
, 同时必须正确处理自赋值strVec &strVec:operator=(const strVec &&rhs) noexcept { if(this != &rhs) { free(); elements= rhs.elements; first_free = rhs.first_free; cap = rhs.cap; rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }
- 同样,不抛异常就应该标记为
- 移后源对象必须可析构
- 移动操作对移后源留下的值没有要求,所以应该不依赖移后源对象的值
- 合成的移动操作
- 没错,编译器会自动为某些类合成
- 当类没有定义任何自己版本的拷贝控制成员,且类的非static数据成员读能移动
- 如果数据成员是内置类型或类成员没有移动操作
struct X { int i; std::string s; // string 有自己的移动操作 } struct hasX { X men; // X 有合成的移动操作 }; X x,x2 = std::move(x); // 使用合成移动构造 hasX hx, hx2 = std::move(hx); // 使用合成移动构造
- 移动操作永远不会定义为删除函数,除非使用
=defaule
且有的成员不能移动- 有类成员定义了拷贝构造但未定义移动构造,或编译器不能为其合成移动构造函数
- 有类成员的移动构造函数或移动赋值运算符定义为删除或不可访问的
- 类的析构函数被定义为删除的
- 有类成员是 const 或引用
- 当类没有定义任何自己版本的拷贝控制成员,且类的非static数据成员读能移动
- 注意:若定义了自己的移动构造/赋值运算符,则合成拷贝构造和拷贝赋值运算符会被定义为删除的
- 没错,编译器会自动为某些类合成
- 移动右值,拷贝左值
- 如果类既有移动构造,也有拷贝构造,那使用普通的函数匹配规则,移动构造只适用于非static右值的情形
strVec v1,v2; v1=v2; strVec getVec(istream &);//getVec返回一个右值 v2 = getVec(cin); // getVec(cin)是一个右值,使用移动赋值
- 如果类既有移动构造,也有拷贝构造,那使用普通的函数匹配规则,移动构造只适用于非static右值的情形
- 如果没有移动构造函数,右值也会被拷贝
- 因为可将
Foo&&
转换为const Foo&
- 几乎肯定是安全的
- 因为可将
- 拷贝并交换赋值运算符和移动操作
class HasPtr { public: HasPtr(HasPtr &&p) noexcept:ps(p.ps),i(p.i){p.ps=0;} HasPtr operator=(HasPtr rhs) {swap(*this,rhs);return *this;} }
- 注意赋值运算赋是一个非引用参数,依赖于移动拷贝构造或拷贝构造
- 三五法则:
- 五个拷贝控制成员应该看成一个整体,定义了任何一个就应该定义全部五个
- Message 类的移动操作
void Message::move_Folders(Message *m) { folders = std::move(m_>folders); // 使用set的移动赋值运算符 for(auto f:folders) { f->remMsg(m); // 删除旧Message f->addMsg(this);// 将本Message添加到Folder中 } m->folders.clear(); // 确保销毁m是无害的 }
- 添加元素可能会抛
bad_alloc()
异常,所以函数未标记为noexcept
Message::Message(Message &&m):contents(std::move(m.contents)) { move_Folders(&m); }
Message &Message::operator=(Message &&rhs) { if(this != &rhs) { remove_from_Folders(); contents = std::move(rhs.contents); move_Floders(&rhs); } return *this; }
- 添加元素可能会抛
- 移动迭代器
- 通过改变给定迭代器的解引用运算符来适配迭代器。
- 移动迭代器的解引用运算符生成一个右值引用
- 标准库提供了
make_move_iterator()
返回移动迭代器
void strVec::reallocate() { // 分配两倍规模的空间 auto newcapacity = size() ? 2*size():1; auto first = alloc.allocate(newcapacity); // 移动元素 auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first); free(); elements = first; first_free = last; cap = elements + newcapacity; }
再次强调
:只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。
头发有限,暂时先跳过 13.6.2 节的练习,头发多了记得补上啊!!!
13.6.3 右值引用和成员函数
- 通常定义一个接受
const X&
和X&&
参数的版本class strVec { public: void push_back(const std::string&); void push_back(std::string&&); } void strVec::push_back(const string &s) { chk_n_alloc(); alloc.consturct(first_free++,s); } void strVec::push_back(string &&s) { chk_n_alloc(); alloc.construct(first_free++,std::move(s)); } vec.push_back("done");
- 右值和左值引用成员函数
- 允许向右值赋值
string s1="aaa",s2="bbb"; s1+s2="wow";
- 阻止这种用法的方法是使用
引用限定符
(&或&&)Foo &operator=(const Foo &) &; Foo anotherMen() const &; // 同时拥有两个限定符
- 阻止这种用法的方法是使用
- 允许向右值赋值
- 重载和引用函数
class Foo { public: Foo sorted() && { sort(data.begin(),data.end()); return *this; } Foo sorted() const & { Foo ret(*this); // 拷贝一个副本 sort(ret.data.begin(),ret.data.end()); return ret; } private: vector<int> data; }
- 相同函数相同变量名的函数要么都加引用限定符,要么都不加
14.重载运算与类型转换
14.1 基本概念
- 重载运算符是一种特殊名字的函数,由关键字 operator和要定义的运算符共同组成
- 一元运算符一个参数
- 二元两个:左侧传递给第一个,右侧第二个
- 若是成员函数,则第一个(左侧)绑定到this指针上
- 比运算符函数少一个参数
- 或者是类成员,或者至少包含一个类型参数
- 四个符号(+、-、*、&)既是一元运算符也是二元运算符
- 直接调用一个重载的运算符函数
data1 + data2; operator+(data1 + data2); data1 += data2; data1.operator+=(data2);
- 某些运算符不应该被重载
- 一些运算符无法保留求值顺序:
逻辑与、逻辑或、逗号运算符、&&、||
- 一些有内置的特殊含义,可能导致类用户无法适应
逗号、取地址运算符
- 一些运算符无法保留求值顺序:
- 使用与内置类型一致的含义
- 逻辑上与运算符有关的操作才重载,否则应定义成函数
- IO 操作的移位运算符与内置类型的IO保持一致
- 相等性逻辑应定义
operator==
,通常应同时包含operator!=
- 单序比较操作应定义
opeartor<
,同时应该包含其他关系操作 - 返回类型应与内置版本的返回类型兼容:
- 逻辑、关系运算返回 bool
- 算术运算返回一个类类型的值
- 赋值、复合赋值运算符(
+=
)应返回左侧运算对象的一个引用
- 逻辑上与运算符有关的操作才重载,否则应定义成函数
- 选择作为成员或非成员
- 赋值(
=
)、下标([]
)、调用(()
)、成员访问(->
)运算符必须是成员 - 复合赋值一般应是成员,但非必须
- 改变运算状态或与类型密切相关的运算符(递增、递减、解引用)通常应是成员
- 对称性或可能转换任意一端的运算对象(算术、相等性、关系、位运算)通常应是普通的非成员函数
- 想提供含有类对象的混合类型表达式、运算对象必须定义成非成员函数
- 运算对象定义成成员函数时,左侧运算对象必须是所属类的一个对象
string u = "hi" + s; // 是成员函数则出错
- 赋值(
14.2 输入和输出运算符
14.2.1 重载输出运算符<<
- 形式
- 通常第一个形参是ostream对象的引用
- 第二个形参一般来说是一个常量引用
- 一般返回 ostream 形参
- sales_data 的输出运算符
ostream *operator<<(ostream &os, const sales_data &item) {
os<<item.isbn()<<" "<<item.units_sold<<" "<<item.revenue<<" "<<item.avg_price();
return os;
}
- 输出运算符尽量减少格式化操作
- 不应打印换行符
- 输入输出运算符必须是非成员函数
- 否则左侧运算对象是类的对象
14.2.2 重载输入运算符 >>
- 通常形式
- 第一个形参是运算符将要读取的流的引用
- 第二个形参是将要读入的对象的引用
- 返回给定流的引用
- sales_data 的输入运算符
istream *operator>>(ostream &is, const sales_data &item) { double price; is>>item.bookNo>>item.units_sold>>price; if(is) item.revenue = item.units_sold*price; else item = sales_data(); return is; }
- 输入时错误
- 可能存在下列错误情况:
- 欲读取数字,但输入不是数字,则后续操作都将失败
- 读取操作达到文件尾或者遇到输入流错误也会失败
- 可能存在下列错误情况:
- 标示错误
- 通常只设置 failbit,设置 eofbit 标示文件耗尽,badbit 标示流被破坏
14.3 算术和关系运算符
14.3.1 相等运算符
- 设计准则:
- 相等操作应定义成
operator==
而不是其他函数名 operator==
应该能判断对象是否有重复数据- 应具有传递性
- 若定义了
operator==
,也应定义operator!=
- 上述应该一个的工作托福给另一个
- 相等操作应定义成
14.3.2 关系运算符
- 关系运算符通常应该
- 定义顺序
- 含有 ==运算符的,若不等,则其一应<另一个
14.4 赋值运算符
- 除了拷贝赋值与移动赋值,还有第三种赋值
class StrtVec { public: StrtVec &operator=(std::initializer_list<std::string> li) { auto data = alloc_n_copy(li.begin(),li.end()); free(); elements = data.first; first_free = cap = data.second; return *this; } }
- 不检查自赋值是因为确保了li与this所指的不是一个对象
- 复合赋值运算符
- 不非得是类成员,但建议是
sales_data &sales_data::operator+=(const sales_data &rhs) { units_sold += rhs.units_sold; revenue +=rhs.revenue; return *this; }
- 不非得是类成员,但建议是
- 下标运算符
- 通常同时定义常量和非常量版本
class strVec { public: std::string &operator[](std::size_t n) {return elements[n];} const std::string &operator[](std::size_t n) const {return elements[n];} }
14.6 递增、递减运算符
-
建议设定为成员函数
-
定义前置递增/递减运算符
class strBlobPtr { public: strBlobPtr &operator++() { check(curr,"increment past end of strBlobPtr"); ++curr; return *this; } strBlobPtr &operator--() { --curr; check(curr,"decrement past begin of strBlobPtr"); return *this; } }
-
区分前置和后置运算符
- 为了区分前置,使用一个int形参,通常不使用
class strBlobPtr { public: strBlobPtr &operator++(int) { strBlobPtr ret = *this; ++*this; return ret; } strBlobPtr &operator--() { strBlobPtr ret = *this; --*this; return ret; } }
- 为了区分前置,使用一个int形参,通常不使用
-
显示调用后置运算
strBlobPtr p(a1); p.operator++(0); // 后置版本 p.operator++(); // 前置版本
14.7 成员访问运算符
class strBlobPtr {
public:
std::string &operator*() const {
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
std::string *operator->() const {
return &this->operator*();
}
};
strBlobPtr a1 = {"hi","bye","now"};
strBlobPtr p(a1);
*p = "okey"; // 首元素赋值
(*p).size() == p->size(); // 永远相等
- 注意这里加了 const 限定符
- 对箭头运算符返回值的限定
- 箭头获取成员这一事实永远不变(可变从不同成员获取)
- 下面三个方式等价
point->mem (*point).mem point.operator()->mem
- 必须返回类的指针或自定义了箭头运算符的某个类的对象
14.8 函数调用运算符
- 即重载函数调用运算符(函数对象)
operator()
- 含有状态的函数对象类
class printString { public: printString(ostream &o = cout, char c=' '):os(o),sep(c){} void operator()(const string &s)const{os<<s<<sep;} private: ostream &os; char sep; }; printString printer,errors(cerr,'\n'); printer(s); errors(s);
- 常结合 for_each 一起使用
for_each(vs.begin(),vs.end(),printString(cerr,'\n'));
- 常结合 for_each 一起使用
14.8.1 lambda 是函数对象
[](const string &a, const string &b){return a.size() < b.size();}
类似于:class shortString { public: bool operator()(const string &s,const string &s2) const {return s1.size() < s2.size();} };
- 表示 lambda 及相应捕获行为的类
- 含捕获行为即含相应的数据成员
14.8.2 标准库定义的函数对象
- 包含在头文件
<functional>
plus<T>、equal_to<T>、logical_and<T>、minus<T>、、、
- 在算法中使用标准库函数对象
- 如想要降序:
sort(v.begin(),v.end(),greater<string>())
- 如想要按照地址来排序
vector<string *> nameType; sort(nameType.begin(),nameType.end(),less<string *>());
- 如想要降序:
14.8.3 可调用对象与 function
- 两个不同的类型可能共享一种调用形式(一个函数类型)
- 不同类型可能具有相同的调用形式
- 可用函数表来存储指向这些可调用对象的"指针"
map<string,int(*)(int,int)> binops; binops.insert({"+",add});
- 可用函数表来存储指向这些可调用对象的"指针"
- 标准库 function 类型
map<string, function<int(int,int)>> binops = { {"+",add},//函数指针 {"-",std::minus<int>()},//标准库函数对象 {"/",divide()},//用户定义的函数对象 {"*",[](int i,int j){return i*j;}},//lambda表达式 {"%",mod} //命了名的lambda }; binops["+"](10,5);
- 重载的函数与 function
- 可通过函数指针解决同名函数问题
- 也可通过 lambda 来消除二义性
14.9 重载、类型转换与运算符
- 转换构造函数和类型转换运算符共同定义了
类类型转换
14.9.1 类型转换运算符
- 特殊成员函数:负责将类类型转换成其他类型
operator type const;
- type 要能作为函数的返回类型
- 定义含有类型转换运算符的类
class samllInt { public: smallInt(int i=0):val(i) { if(i<0||i>255)throw std::out_of_range("bad smallInt value"); } operator int() const {return val;} private: std::size_t val; }; smallInt si = 4;//将4隐式转换成smallInt si+=3;// 将si隐式转换成int,再执行整数的加法
- 注意必须是成员函数、不能有返回类型、参数列表为空
- 类型转换运算符可能产生意外结果
- 书上给的例子不妥,总之不要使用让人意外的类型转换
- 显示的类型转换运算符
class samllInt { public: explicit operator int() const {return val;} }; samllInt si=4; si+=3;//错误 static_cast<int>(si) + 3;//正确
- 有一个例外是作为条件时,编译器会将类型转换自动应用于它
- 转换为 bool
- 无论我们在什么时候在条件中使用流对象,都会使用为IO类型定义的 operator bool:
while(std::cin>>value)
- 无论我们在什么时候在条件中使用流对象,都会使用为IO类型定义的 operator bool:
14.9.1 避免有二意性的类型转换
- 有两种情况可能产生二义性
- A类有接受B类的转换构造,B也有A的
- A定义了多个转换规则,涉及的转换类型本身可通过其他类型装换联系在一起
- 实参匹配和相同类型转换
struct B; struct A { A()=default; A(const B&); }; struct B { operator A() const; }; A f(const A&); B b; A a = f(b);//二义性错误
- 可改成显示调用:
A a1=f(b.operator A());//使用显示类型转换 A a2=f(A(b));//使用构造
- 可改成显示调用:
- 二义性与转换目标为内置类型的多重类型转换
struct A { A(int=0); A(double);//最好不要创建两个转换源都是算术类型的类型转换 operator int()const; operator double()const; }; void f2(long double); A a; f2(a);//二义性错误 long lg; A a2(lg);//二义性错误
提示:
除了显示向bool类型的转换外,我们应该尽量避免定义类型转换函数并尽可能限制那些 “显然正确” 的非显示构造函数
- 重载函数与转换构造函数
struct C { C(int); }; struct D { D(int); }; void mainip(const C&); void mainip(const &D); mainip(10);//二义性错误
- 可显示调用构造函数解决
mainip(C(10));
- 可显示调用构造函数解决
- 重载函数与用户定义的类型转换
struct C { C(int); }; struct E { E(double); }; void mainip(const C&); void mainip(const E&); mainip(10);//二义性错误,可以是mainip(C(10))或mainip(E(double(19)))
14.9.3 函数匹配与重载运算符
class smallInt {
std::size_t val;
friend smallInt opeartor+(const smallInt&,const smallInt&);
public:
smallInt(int=0);
operator int()const{return val;}
};
smallInt s1,s2;
smallInt s3=s1+s2;
int i=s3+0; // 二义性错误
15.面向对象程序设计
15.1 OOP:概述
- 面向对象程序设计(object-oriented programming) 核心思想是
数据抽象、继承、动态绑定
- 继承:
- 通过继承联系在一起的类构成了一种层次关系,通常层次关系的根部有一个
基类
,继承得到的类叫派生类
- 希望派生类各自定义适合自身的版本,可声明成
虚函数
class Quote { virtual double net_price(std::size_t n)const; };
- 派生类必须通过
类派生列表
明确指出它是从哪个/些基类继承而来class Bulk_quote:public Quote { public: double net_price(std::size_t) const override; };
- 可在函数前加
virtual
关键字但不必要 - verrride 显示注明将使用哪个成员函数改写基类虚函数
- 可在函数前加
- 派生类必须通过
- 通过继承联系在一起的类构成了一种层次关系,通常层次关系的根部有一个
动态绑定
- 可用一段代码分别处理 Quote 和 Bulk_quote
double print_total(std::ostream &os, const Quote &item, size_t n) { double ret = item.net_price(n); os << "ISBN:" << item.isbn() << "# sold: " << n << " total due: " << ret << std::endl; return ret; } print_total(cout, basic, 20); print_total(cout, bulk, 20);
- 实际传入 print_total 的对象类型将决定到底执行 net_price 的哪个版本
- 这种在运行时由实参决定函数版本的行为,就叫动态绑定,又叫
运行时绑定(run-time binding)
- 使用基类的引用(或指针)调用一个虚函数时将发生
- 这种在运行时由实参决定函数版本的行为,就叫动态绑定,又叫
- 实际传入 print_total 的对象类型将决定到底执行 net_price 的哪个版本
- 可用一段代码分别处理 Quote 和 Bulk_quote
15.2 定义基类和派生类
15.2.1 定义基类
- 作为例子,先定义一个 quote 的基类:
class Quote { std::string bookNo; protected: double price = 0.0; public: Quote() = default; Quote(const std::string &b, double p): bookNo(b), price(p){} std::string isbn() const { return bookNo; } virtual double net_price(std::size_t n) const { return n * price; } virtual ~Quote() = default; };
- 跟节点通常会定义一个虚析构函数,即使不执行任何实际操作
- 成员函数与继承
- 派生类需要对虚函数提供自己的新定义来
覆盖(override)
从基类继承的旧定义- 任何构造函数之外的非静态函数都可以是虚函数
virtual
只能出现在类内部的声明- 派生类中虚函数也是隐式的虚函数
- 非虚函数的解析发生在编译时而非运行时
- 派生类需要对虚函数提供自己的新定义来
- 访问控制与继承
- 派生类可以继承基类成员,但不一定有权限访问基类成员
- 与使用基类的代码一样,能访问共有成员,不能访问私有成员
protected(受保护的)
允许派生类访问不允许其他代码访问
- 派生类可以继承基类成员,但不一定有权限访问基类成员
15.2.2 定义派生类
- 通过使用派生列表指出从哪个基类继承而来
- 冒号后面、逗号分隔、基类前可加三种说明符
class Bulk_quote : public Quote { std::size_t min_qty = 0; double discount = 0.0; public: Bulk_quote() = default; Bulk_quote(const std::string& b, double p, std::size_t q, double disc) :Quote(b, p), min_qty(q), discount(disc){} double net_price(std::size_t n) const override; };
访问说明符
的作用是控制派生类从基类继承而来的成员是否对派生类用户可见
- 如果派生是共有的:
- 基类的成员也是派生类的组成部分
- 公有派生类型可绑定到基类的引用或指针上
- 冒号后面、逗号分隔、基类前可加三种说明符
- 派生类中的虚函数
- 如果派生类没有覆盖基类中某个虚函数,则虚函数类似普通成员
- 显示指定覆盖操作的方法是加上关键字
override
- 派生类对象及派生类向基类的类型转换
- 一个派生类包含多个部分
- 自己定义的(非静态)成员子对象
- 继承的基类对应的子对象
- C++标准没有明确规定派生类对象的内存分布
- 派生类到基类的类型转换(隐式的):
- 派生类对象或引用用在需要基类引用的地方
- 派生类对象指针用在需要基类指针的地方
- 一个派生类包含多个部分
- 派生类构造函数
- 必须使用基类构造函数初始化基类部分,
每个类控制自己成员的初始化过程
- 首先初始化基类部分,然后按照顺序依次初始化派生类成员
- 必须使用基类构造函数初始化基类部分,
- 派生类使用基类成员
- 可访问基类的共有成员和受保护成员
doble bulk_quote::net_price(size_t cnt) const { if(cnt >= min_qty) return cnt*(1-discount)*price; else return cnt*price; }
- 可访问基类的共有成员和受保护成员
- 继承与静态成员
- 不管怎样静态成员都只存在唯一实例
- 遵循访问控制规则,私有成员则派生类无权访问
- 派生类的声明
class bulk_quote; // 正确,派生类列表不能出现
- 被用作基类的类
- 必须已经定义(非仅仅声明)
- 隐含意思:不能继承本身
- 派生类也可以是基类
直接基类
含基类成员、间接基类
继承直接基类的成员
- 必须已经定义(非仅仅声明)
- 防止继承的发生
- 使用关键字
final
class Noderived final { /**/ }; // Noderived 不能作为基类
- 使用关键字
15.2.3 类型转换与继承
- 静态类型与动态类型
- 静态类型的表达式在编译时总是已知的
- 动态类型是变量或表达式表示的内存中的对象类型,运行时才知道
- 如前面将的 print_total() 函数的 item 参数的静态类型是Quote,动态类型取决于绑定的参数
- 不存在从基类向派生类的隐式类型转换
- 因为基类对象可能是派生类对象的一部分,也可能不是
- …在对象之间不存在类型转换
- 用派生类对象为基类对象初始化或赋值时,只有基类部分会被拷贝、移动或赋值,派生部分会被忽略
15.3 虚函数
- 不管虚函数是否被用到,都必须为虚函数提供定义,因为编译器也无法确定到底会用哪个虚函数
- 对虚函数的调用可能在运行时才被解析
- 只有通过指针会引用调用虚函数时才会发生
关键概念:
C++的多态性- OOP的核心是多态性:具有继承关系的多个类型被称为多态类型,指针或引用的动态类型不一致是C++支持多态性的根本性所在
- 派生类中的虚函数
- 覆盖函数声明时可在开头加 virtual 声明(不必须)
- 形参必须完全一致
- 派生类的返回类型必须与基类相匹配
- 返回类型是本身的指针或引用时无效
- final 和 override 说明符
- override 可以更加明确说明派生类中的虚函数
- 也可把函数指定为 final 拒绝覆盖操作
- 虚函数与默认实参
- 若使用了默认实参,实参值由调用的静态类型决定
- 回避虚函数的机制
// 强行调用基类中定义的函数版本而不管baseP的动态类型 doble undiscounted = baseP->Quote::net_price(42);
15.4 抽象基类
- 纯虚函数
- 无须定义,在声明的分号前书写
=0
就行class Disc_quote : public Quote { public: Disc_quote(); Disc_quote(const std::string &b, double p, std::size_t q, double d) : Quote(b, p), quantity(q), discount(d){} virtual double net_price(std::size_t n) const override = 0; protected: std::size_t quantity; double discount; };
- 可以定义函数体,但是必须写在类外
- 无须定义,在声明的分号前书写
- 含有纯虚函数的类是
抽象基类
- 不能直接创建抽象基类的对象
- 从
抽象基类
派生的类必须给出纯虚函数的定义,否则仍是抽象基类
- 派生类构造函数只初始化它的直接基类
class Bulk_quote : public Disc_quote { public: Bulk_quote() = default; Bulk_quote(const std::string& b, double p, std::size_t q, double disc) :Disc_quote(b, p, q, disc){} double net_price(std::size_t n) const override; };