C++ Primer笔记
ch2 变量和基本类型
声明
extern int i;
extern int i = 3.14;//定义
左值引用(绑定零一变量初始值,别名)
不能定义引用的引用;引用必须被初始化;类型严格匹配;不能绑定字面值/计算结果;无法二次绑定
int i=4;
int &r=i;
指针
本身是对象,允许赋值和拷贝;无需定义时赋初值;类型严格匹配
int *ip1, *ip2;
int ival = 42;
int *p = &ival;
*p = 0;
cout << *p;
int *p2 = p;
int &r2 = *p;
指针值:1.指向一个对象;2.指向紧令对象的下一个位置;3.nullptr;4.无效指针
不能把int变量直接赋值给指针
有时候无法清楚赋值语句改变了指针的值还是指针所指对象的值:赋值永远改变等号左边的对象
指针比较:(==)相等时意味着地址值相等(都为空/指向同一对象/指向同一对象的下一地址)
**表示指向指针的指针
int ival = 1024;
int *pi = &ival;
int **ppi = π//ppi指向pi的地址
int *pi2 = pi; //pi2和pi指向同一对象
指向指针的引用
int i = 42;
int *p;
int *&r = p;//r是p的引用
r = &i;//r引用了p,此处让p指向i
*r = 0;
理解r的类型是什么,最简单的是从右向左读r的定义,&r的&表明r是一个引用,*说明r引用的是一个指针,int指出r引用的是一个int指针
const
必须初始化;
默认状态下,const对象仅在文件内有效,共享必须添加extern
//文件间共享const对象
extern const int bufSize = fcn();//定义初始化常量,且能被其他文件访问
extern const int bufSize;
const引用
初始化常量引用时允许用任意表达式
int i = 42;
const int &r1=i;
const int &r2=42;
const int &r3=r1*2;
double dval = 3.14;
const int &ri = dval;//浮点数生成临时常量再绑定
对const的引用可能会引用一个并非const的对象
指向常量的指针:不能通过该指针改变对象的值。
double dval = 3.14;
const double *cptr = &dval;
const指针(必须初始化,值不能改变)
//从右向左确定含义
int errNumb = 0;
int *const curErr = &errNumb;//curErr一直指向errNumb
const double pi = 3.14;
const double *const pip = π//pip是一个指向常量对象的常量指针
- 顶层const:指针本身是常量
- 底层const:指针所指对象是常量
int i = 0;
int *const pi = &i;//顶层const
const int ci = 42;//顶层const
const int *p2 = &ci;//底层const
const int *const p3 = p2;//靠右是顶层,靠左是底层
const int &r = ci;//用于声明的都是底层const
//拷贝时,顶层const不受影响;底层const必须相同
i = ci;
p2 = p3;
int *p = p3;//wrong,p3有底层,p没有
p2 = p3;//都是底层
p2 = &i;//int*能转换为const int*
int &r = ci;//wrong,int&不能绑定到int常量上
const int &r2 = i;//const int&可以绑定到int
常量表达式:constexpr
ch3 字符串、向量和数组
头文件不应包含using声明
string
定义和初始化
string s1 = "hiya";//默认初始化使用= string s2("hiya");//直接初始化 string s3(10,'c');//直接初始化
getline(cin, line),使用endl结束当前行并刷新缓冲区
empty和size操作
size返回值:string::size_type。返回的是一个无符号整数,不要使用int了比较(长度/大小写敏感)
赋值/对象相加/字面值与string相加(保证每个加法运算对象之一是string)
处理字符:range for语句
decltype(s.size()) punct_cnt = 0;//使用s.size()返回值类型声明punct_cnt for(auto &c : s){...}//加引用改变字符
下标(随机访问)
vector(容器/类模板)
定义和初始化
vector<T> v1//默认初始化 vector<T> v2(v1); vector<T> v2 = v1; vector<T> v4(n);//值初始化 vector<T> v5{a,b,c,...};//列表初始化 vector<T> v6={a,b,c,...};
操作
v.push_back()//添加 v.empty() v.size()//vector<int>::size_type v1 = v2;//拷贝 v1 = {a,b,c}//列表拷贝 v1 == v2;//数量相同且元素值相同
不能用下标添加元素
迭代器
*iter//返回iter所指元素的引用
iter->data//解引用获取data的成员,等价于(*iter).data
++iter
--iter
iter1 == iter2
auto e = v.end();//因为end返回的迭代器不指示某个元素,不能对其递增或解引用的操作
迭代器运算
数组
//初始化
const unsigned sz = 3;
int a[] = {0,1,2};
int a[5] = {0,1,2};
string a[] = {"hi","bye"}
int a2[]=a;//wrong,不允许拷贝赋值
a2 = a;
int *ptrs[10];//ptrs含有10个整形指针
int &refs[10];//wrong
int (*Parray)[10] = &arr;//Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr;arrRef引用一个含哟10个整数的数组
//数组的名称由内向外读
int * (&array)[10] = ptrs://array是数组的引用,该数组包含10个指针
访问数组元素(size_t,无符号)
指针和数组
string nums[] = {"one", "two", "three"};
string *p = nums;//等价于p2 = &nums[0]
auto ia(nums);//ia是一个string指针,指向nums的第一个元素
指针也是迭代器
int ia[] = {0,1,2};
int *beg = begin(ia);
int *last = end(ia);
尾后指针不能解引用和递增操作
C风格字符串
strlen(p)
strcmp(p1, p2)
strcat(p1, p2)//p2附加到p1之后
strcpy(p1, p2)//p2拷贝给p1
混用string和C风格字符串
char *str = s;//wrong
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));
vector<int> subVec(int_arr+1, int_arr+4);
//尽量使用vector和迭代器;尽量使用string
多维数组
int ia[2][3] = {
{1,2,3},
{2,3,4}
};
//下标引用
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1];//row绑定到ia的第二个4元素数组上
size_t cnt = 0;
for(auto &row : ia)//不加引用则无法编译
for(auto &col : row){
col = cnt++;
}
//指针和多维数组
int ia[3][4];
int (*p)[4] = ia;
p = &ia[2];//p指向ia的尾元素
ch4 表达式
重载运算符(运算对象的类型和返回值的类型)
- (对象被用作)左值:用的是对象的身份(内存地址)
- 赋值运算符运算左值对象,返回左值
- 取地址符作用于左值,返回指向该运算的指针(右值)
- 解引用/下标/迭代器/string和vector的下标运算符,结果为左值
- 内置类型和迭代器的递增递减运算符,作用于左值,返回左值
- (对象被用作)右值:用的是对象的值(内容)
处理复合表达式
- 拿不准的时候最好用括号来限制
- 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象(*++iter例外)
溢出和其他算术运算符异常(“环绕”)
//(-m)/n和m/(-n)等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)
//除法运算中,两个运算对象的符号相同则商为正,否则为负
//取余运算中,m和n是整数且n非0,则(m/n)*n+m%n的结果与m相同
-21 % -8 = -5 -21 / -8 = 2
21 % -5 = 1 21 / -5 = -4
进行比较运算时除非比较的对象是布尔类型,否则不要使用true和false作为运算对象。
赋值运算满足右结合律
除非必须,否则不使用递增递减运算符的后置版本(相对复杂的迭代器类型,额外工作消耗巨大)
一条语句中混用解引用和递增运算符(*iter++|后置递增运算符返回初始的未加1的值)
while(beg != s.end() && !isspace(*beg))
*beg = toupper(*beg++);//wrong
*beg = toupper(*beg);
*(beg+1) = toupper(*beg);
条件运算符(?)右结合律,优先级比<<低
sizeof(所得的值是size_t类型)
//sizeof运算能够返回整个数组的大小
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr[sz];
类型转换
- 隐式转换
- 比int小的整数型首先提升为较大的整数类型(算术转换/数组->指针/指针转换)
- 条件中,非布尔转换为布尔
- 初始化过程中,初始值转换成变量类型;赋值语句中,右侧运算对象转换成左侧类型
- 算术运算/关系运算的运算对象多种类型,需要转换成同一种类型
- 函数调用时
- 显示转换
static_cast:最广泛,除了底层const
const_cast:之恶能改变运算对象的底层const,常量对象转换为非常量对象
const char *pc; char *p = const_cast<char*>(pc); char *q = static_cast<char*>(pc);//wrong static_cast<string>(pc); const_cast<string>(pc);//wrong
reinterpret_cast:为运算对象的位模式提供较低层次上的重新解释。
dynamic_cast:支持运算时类型识别
尽量避免强制类型转换
ch5 语句
悬垂else:else就近匹配
switch语句内部的变量定义
case true:
string file_name;//wrong,隐式初始化
int ival = 0;//wrong,显式初始化
int jval;//true
break;
case false:
jval = next_num();
if(file_name.empty()){}
范围for:不能通过范围for增加vector对象元素,因为预存end(),一旦再序列中添加/删除元素,end函数的值就可能变得无效了。
跳转语句
- break:作用范围仅限于最近循环/switch
- continue:中断迭代,继续执行循环
- goto:无条件跳转到同一函数内的另一条语句
不要在程序中使用goto,会使得程序难理解难修改
try-catch语句块
throw表达式
Sales_item item1, item2; cin >> item1 >> item2; if(item1.isbn()!=item2.isbn()) throw runtime_error("Data must refer to same ISBN!"); //抛出runtime_error异常,用string对象初始化 cout << item1+item2<<endl;
try-catch()
while(cin >> item1 >> item2){ try{ // }catch (runtime_error err){ cout << err.what() << endl; } }
函数在寻找处理代码的过程中退出:编写异常安全代码十分困难
标准异常
- exception:定义最通用的异常类,报告异常的发生
- stdexcept:定义几种常见的异常类
- new定义bad_alloc异常类型
- type_info定义bad_cast异常类型
ch6 函数
函数:返回类型,函数名字,0个或多个形参组成的列表,函数体
局部对象
- 名字的作用域是程序文本的一部分,名字在其中可见;
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
- static局部静态对象,第一次经过对象定义语句后初始化,直到程序结束才终止
变量和函数在头文件中声明(不包含函数体),在源文件中定义
分离式编译:多文件组成的程序是如何编译并执行的
参数传递
传值参数(不影响初始值)
//指针形参,执行指针拷贝时,拷贝的时指针的值;拷贝后,两个指针不同指向同一对象 void reset(int *ip) { *ip = 0;//改变指针ip所指对象的值 ip = 0;//改变ip的局部拷贝,实参未被改变 } int i = 0; reset(&i); //建议使用引用类型的形参替代指针
传引用
void reset(int &i){ i = 0; } int j = 42; reset(j); //使用引用避免拷贝,若无须修改引用形参的值,声明为常量引用
const形参和实参
const int ci = 42;//wrong,const顶层 int i = ci;//实参初始化形参和拷贝时,忽略顶层const int *const p = &i;//wrong,const顶层 *p = 0; void fcn(const int i)//不能向i写值 void fcn(int i)//wrong,重复定义
指针/引用形参与const
int i = 42; const int *cp = &i;//cp don't change i const int &r = i;//r don't change i const int &r2 = 42; int *p = cp;//wrong,type don't match int &r3 = r;//wrong,type don't match int &r4 = 42;//wrong,字面值不能初始化非常量引用 const int ci = i; string::size_type ctr = 0; reset(&i); reset(&ci);//wrong,不能用指向const int对象指针初始化int* reset(i); reset(ci);//wrong,普通引用不能绑定在const对象上 reset(42);//wrong,普通引用不能绑定在字面值 rest(ctr);//wrong,type don't match find_char("hello world!", 'o', ctr);
尽量使用常量引用
数组形参print(const int*) print(const int[]) print(const int[10]) int i = 0, j[2]={0,1}; print(&i); print(j);//j转换为int*指向j[0]
管理指针形参三种常用的技术
标记指定数组长度
void print(const char *cp){ if(cp) while(*cp) cout << *cp++ << endl; }
标准库规范
void print(const int *beg, const int *end) { while(beg != end) cout << *beg++ << endl; }
显示传递数组大小形参
void print(const int ia[], size_t size) { for(size_t i = 0; i < size; ++i) cout << ia[i] << endl; }
数组引用形参
void print(int (&arr)[10]){} f(int &arrp[10])//wrong,将arr声明成了引用的数组 f(int (&arr)[10])//true,arr是包含10个整数的数组引用 void print(int (*matrix)[10], int rowSize){} void print(int matrix[][10], int rowSize){}//两者等价
含有可变形参的函数:initializer_list形参(用于与C函数交互的接口)
void error_msg(initializer_list<string> il) { for(auto beg = il.begin();beg!=il.end();++beg) cout << *beg << endl; }
return 语句:不要返回局部对象的引用或指针
引用返回左值
char &get_val(string &str, string::size_type ix) return str[ix]; int main() { string s("a value"); get_val(s,0) = 'A'; cout << s << endl; }
列表初始化返回值
返回数组指针
//声明一个返回数组指针的函数 int arr[10];//10个整数的数组 int *p1[10];//10个整数指针 int (*p2)[10] = &arr;//p2指向arr int (*func(int i))[10];//func的调用进行解引用后得到一个10的数组 //尾置返回类型 auto func(int i)->int (*)[10]; //使用decltype decltype(odd) *arrPtr(int i) { return (i % 2) ? &odd : &even; }
函数重载(函数名字相同形参列表不同)
重载和const形参:顶层const不影响传入函数的对象,一个拥有顶层const的形参无法与一个没有顶层const的形参区分开来
最好是重载那些确实非常相似的操作
const_cast和重载
string &shorterString(string &s1, string &s2) { auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); }
特殊用途语言特性
默认实参
typedef string::size_type sz; string screen(sz ht = 24, sz wid = 80, char backgrnd = ' '); //通常,应该在头文件函数声明中指定默认实参
内联函数和constexpr函数(定义在头文件中)
//避免函数调用的开销,在编译时展开 //能用于常量表达式的函数 constexpr size_t scale(size_t cnt) { return new_sz() * cnt; } int arr[scale(2)]; int i =2; int a2[scale(i)];//wrong,scale(i)不是常量表达
调试帮助
assert预处理宏assert(word.size() > threshold)
NDEBUG预处理变量
void print(const int ia[], size_t size) { #ifndef NDEBUG cerr << __func__<< "" << endl; #endif } __FILE__存放文件名 __LINE__存放当前行号 __TIME__存放文件编译时间 __DATE__存放文件编译日期
函数匹配
函数指针:指向是函数而非对象。函数的类型由返回类型和形参类型共同决定bool lengthCompare(const string &, const string &); bool (*pf)(const string &, const string &);//not initialize pf = lengthCompare; bool b1 = pf("hello world", "goodbye"); bool b2 = (*pf)("hello world", "goodbye");//equal bool b3 = lengthCompare("hello world", "goodbye");//equal //重载函数的指针 void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff; //函数指针形参 void useBigger(const string &s1, const string &s2, bool pf(const string&, const string&)); useBigger(s1, s2, lengthCompare); //函数 typedef bool Func(const string&, const string&); typedef decltype(lengthCompare) Func2;//equal //指向函数的指针 typedef bool (*FuncP)(const string&, const string&); typedef decltype(lengthComapre) *FuncP;//equal //返回指向函数的指针 //auto和decltype用于函数指针
声明getFcn唯一需要注意的地方是,牢记当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。显示加上*表明需要返回指针,而非函数本身.
ch7 类
定义在类内部的函数时隐式的inline函数
成员函数通过一个名为this常量指针的额外的隐式参数来访问调用它的那个对象。
const放在成员函数参数表后,表明this时一个指向常量的指针。
//定义read和print函数
istream &read(istream &is, Sailes_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
osteam &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price() << endl;
return os;
}
构造函数
- 默认构造函数:无需任何实参
- 构造函数初始值列表
- 拷贝/赋值/析构
访问控制和封装(public|private;class|struct)
- 确保用户代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码
友元函数(friend函数声明|类定义开始/结束位置集中声明友元)
重载成员函数
可变数据成员:mutable
//返回*this的成员函数
inline Screen &set(char);//若返回类型不是引用,则返回值时*this的副本
myScreen.display(cout).set('*');//wrong,display返回常量引用*this,无法set一个常量对象
//基于const的重载
Screen &display(std::ostream &os){return *this;}
const Screen &display(std::ostream &os) const {return *this;}
myScreen.set('&').display(cout);//调用非常量版本
blank.display(cout);//常量版本
建议:对于公共代码使用私有功能属性
类之间的友元关系:friend声明,友元类的成员函数可以访问此类非公有成员
建议:使用构造函数初始值
委托构造函数
class Sales_data{
public:
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price){}
//委托delegate
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);}
};
隐式类类型转换:通过将构造函数声明为explicit加以阻止,只能用于直接初始化,且对一个实参的构造函数有效
类的静态成员(static关键字):存在于任何对象之外,成员函数不包含this指针
double r = Account::rate();
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2>rate();//通过对象/指针访问
静态成员的类内初始化,使用constexpr,但通常情况下依然在类外定义该成员。
ch8 IO库
IO类
- iostream定义了用于读写流的基本类型
- fstream定义了读写命名文件的类型
- sstream定义了读写内存string对象的类型
标准库能使我们忽略不同类型的流之间的差异:继承机制实现的
IO对象无拷贝或赋值:不能将形参或返回类型设置为流类型,通常以引用方式传递返回流,不能使const的
缓冲刷新的原因:
- 程序正常结束,main函数的return操作的一部分,缓冲刷新被执行
- 缓冲区满时,刷新
- 使用endl显示刷新
- 每个书除操作之后,用unitbuf设置流的内部状态,清空缓冲区。默认情况下,cerr时设置unitbuf的
- 输出流关联到另一个流时。如cin和cerr都关联到cout,读写cin和cerr会导致cout的缓冲区被刷新
使用flush和ends刷新缓冲区(输出一个空字符)
警告:如果程序崩溃,书除缓冲区不会被刷新
文件输入输出
ifstream in(ifile);
ofstream out;
out.open(ifile+".copy");
in.close();
in.open(ifile+"2");
当一个fstream对象被销毁时,close会自动被调用,自动构造和析构
在每次打开文件时,都要设置文件模式,可能时显示地设置,也可能是隐式地设置。当程序未指定模式,使用默认值。
string流:某些工作对整行文本进行处理,而其他工作是处理行内地某个单词,用istringstream
string line, word;
vector<PersonInfo> people;
while(getline(cin,line)){
PersonInfo info;
istringstream record(line);
record >> info.name;
while(record >> word)
info.phones.push_back(word);
people.push_back(info);
}
ostringstream:逐步构造输出,最后一起打印
ch9 顺序容器
- vector:可变大小数组,支持快速随机访问,在尾部之外地位置插入或删除可能很慢
- deque:双端队列,支持快速随机访问,在头尾位置插入/删除速度很快
- list:双向链表,只支持双向顺序访问,list任何位置插入/删除速度很快
- forward_list:单向链表,只支持单项顺序访问,链表任何位置进行插入/删除速度很快
- array:固定数组大小,支持快速随机访问,不能添加/删除元素
- string:与vector相似地容器,专门保存字符,随机访问很快,尾部插入/删除快
选择哪种顺序容器
- 通常选择vector
- 程序中有很多小的元素,且空间额外开销很重要,不要使用list和forward_list
- 程序要求随机访问元素,使用vector和deque
- 要求中间插入/删除元素,使用list和forward_list
- 要求头尾插入/删除元素,中间不插入/删除,使用deque
- 只在读取输入时在中间插入元素,则
- 首先,确定是否真的需要在容器中间插入元素,借助vector和sort
- 必须中间插入元素,输入阶段使用list,结束后拷贝到vector
容器操作:P330
迭代器范围由一对迭代器表示,begin和end,[begin,end),带r的版本返回反向迭代器,以c为开头的版本返回const迭代器
容器定义和初始化
C c;//默认构造函数
C c1(c2);//拷贝
C c1 = c2;
C c{a,b,c};//列表初始化
C c={a,b,c};
C c(b,e)//c初始化为b和e指定范围内元素的拷贝。
C seq(n);//seq包含n个元素,构造函数时explicit的
C seq(n,t);//seq包含n个初始值为t的元素
容器赋值运算
c1 = c2;//c1替换为c2的拷贝
c = {a,b,c};//c1元素替换为c2的拷贝
swap(c1,c2);
c1.swap(c2);//swap比从c2到c1拷贝快得多
seq.assign(b,e)//seq元素替换为b,e表示范围内的元素
seq.assign(il)
seq.assign(n,t)
//assign可以将char*值赋予list
list<string> name;
vector<const char*> oldstyle;
names = oldstyle;//wrong,类型不匹配
names.assign(oldstylee.cbegin(),oldstyle.cend());
关键概念:容器元素时拷贝
push_front:list,forward_list,deque支持,常数级
vector,deque,list,string支持insert,比较耗时
slist.insert(iter, "Hello!");
svec.insert(svec.end(),10,"Anna");
slist.insert(slist.begin(),v.end()-2,v.end());
slist.insert(slist.end(),{"these","words"});
slist.insert(slist.begin(),slist.begin(),slist.end());//wrong,迭代器表示拷贝范围,不能指向与目的位置相同的容器
iter = lst.insert(iter,word);//等同于push_front()
emplace_frontinsert,emplace_back~push_back,用于容器中直接构造元素
at和下标操作只适用于string、vector、deque、array
back不适用于forward_list
如果容器为空,front和back时未定义的
c.pop_back()//删除c中尾元素,c为空,行为未定义,返回void
c.pop_front()//删除c中首元素,c为空,行为未定义,返回void
c.erase(p)
c.erase(b,e)//返回一个指向最后一个被删元素之后元素的迭代器,若e为尾后迭代器,返回尾后迭代器
c.clear()
//删除deque除首尾之外的任何元素都会使任何迭代器、指针、引用失效
forward_list特殊版本的添加/删除操作
vector是如何增长的
- size:容器已经保存的元素数量
- capacity:不分配新的内存空间最多可以保存多少元素
不同的分配策略遵循一个原则:确保用push_back添加元素的操作由高效率
额外的string操作
string s(cp,n)//cp指向的数组前n个字符
string s(s2,pos2)//s2从pos2开始的字符的拷贝
string s(s2,pos2,len2)//s2从pos2开始len2字符的拷贝
string s2 = s.substr(0,5)
string s2 = s.substr(6)
s.append(args);//args追加到s,返回s的引用
s.replace(range,args);//将range字符替换成args
//搜索操作
string name("AnnaBelle");
auto pos1=name.find("Anna");
string numbers("0123456789"),name("r2d2");
auto pos = name.find_first_of(numbers);
string dept("03714p3");
auto pos = dept.find_first_not_of(numbers);
//compare函数
数值转换
int i = 42;
string s = to_string(i);
double d = stod(s);
容器适配器:stack,queue,priority_queue
本质上,一个适配器是一种机制。每个适配器都在其底层顺序接口容器上定义了一个新的接口。
ch10 泛型算法
算法:实现了一些经典算法的公共接口
泛型:它们可以用于不同类型的元素和多种容器类型(包括标准库vector和list以及内置的数组类型)
算法遍历由两个迭代器指定的一个元素范围进行操作
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作
关键:算法不会执行容器的操作,只会运行于迭代器之上,算法永远不会改变底层容器的大小
只读算法
find //求和 int sum = accumulate(vec.cbegin(), vec.cend(), 0); string sum = accumulate(v.cbegin(), v.cend(), string(""));//去掉string会导致编译错误,不能直接传递字面值 //比较两个序列 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());//假定第二个序列至少与第一个序列一样长
写容器元素算法
fill(vec.begin(), vec.end(), 0);//每个元素重置为0,写操作安全 //算法不检查写操作 fill_n(vec.begin(), vec.size(), 0); vector<intj> vec; fill_n(vec.begin(), 10, 0);//结果未定义wrong
迭代器参数:两个序列的元素可以来自不同的容器。
插入迭代器:back_insertervector<int> vec; fill_n(back_inserter(vec), 10, 0);
copy算法
int a1[] = {0,1,2,3,4}; int a2[sizeof(a1)/sizeof(*a1)]; auto ret = copy(begin(a1), end(a1), a2); replace(ilst.begin(), ilst.end(), 0, 42); replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0 ,42);//ivec包含ilst的一份拷贝
重排容器元素算法
void elimDupe(vector<string> &words){ sort(words.begin(), words.end()); auto end_unique = unique(words.begin(), words.end())//返回指向不重复区域之后一个位置的迭代器 words.erase(end_unique, words.end()); }
定制操作
向算法传递函数
bool isShorter(const string &s1, const string &s2){ return s1.size() < s2.size(); } sort(words.begin(), words.end(), isShorter); elimDups(words); stable_sort(words.begin(), words.end(), isShorter); for(const auto &s:words)//无须拷贝字符串 cout << s << " " ; cout << endl;
lambda表达式(一个返回类型,一个参数列表,一个函数体)
void biggies(vector<string> &words, vector<string>::size_type sz) { elimDups(words); stable_sort(words.begin(), words.end(), isShorter); } //lambda [capture list](parameter list) -> return type { function body } auto f = []{return 42; } [](const string &s1, const string &s2){ return s1.size() < s2.size(); } [sz](const string &a){ return a.size() >= sz; } auto wc = find_if(words.begin(), words.end(), [sz](const string &a) {return a.size() >= sz;}) for_each(wc, words.end(), [](const string &s){cout << s << " ";}); cout << endl;
lambda捕获和返回
- 值捕获
- 引用捕获
- 隐式捕获
建议:尽量保持lambda的变量捕获简单化
可变lambda:加上mutable
指定lambda返回类型:transform
参数绑定bind
auto check6 = bind(check_size(), _1, 6); sort(words.begin(), words.end(), bind(isShorter, _2, _1));//重排参数顺序 for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));//绑定引用参数
再探迭代器
- insert iterator:插入元素,it = t, *it, ++it, it++
back_inserter:创建一个使用push_back的迭代器
front_inserter:使用push_front的迭代器 - iostream迭代器
istream_iterator
ostream_iterator - 反向迭代器
迭代器类别
- 输入迭代器:==,!=;++;*;->
- 输出迭代器:++;*
- 前向迭代器:只能沿一个方向移动,replace|forward_list
- 双向迭代器:--,reverse
- 随机访问迭代器:<,<=,>,>=;+,+=,-=;-;下标运算符
特定容器算法
lst.merge(lst2)
lst.merge(lst2, comp)
list.remove(val)
lst.remove_if(pred)
list.reverse()
lst.sort()
lst.sort(comp)
lst.unique()
lst.unique(pred)
lst.splice(args)
//链表特有的操作会改变容器,如remove,unique,merge,splice
ch11 关联容器
关联容器中的元素是按关键字保存和访问的
主要是map和set
map,set,multimap,multiset,unordered_map,unordered_set,unordered_multimap,unordered_multiset
pair标准库类型
有序容器:使用比较运算符组织元素
关联容器操作
//map的value_type是一个pair,可以改变值,但不能改变关键字成员的值
//set的迭代器是const的
auto map_it = word_count.cbegin();
while(map_it != word_count.cend())
{
cout << map_it->first << map_it->second << endl;
++map_it;
}
//添加元素
set.insert(ivec.cbegin(), ivec.cend());
set.insert({1,2,3});
word_count.insert({word,1});
word_count.insert(make_pair(word,1));
word_count.insert(pair<string,size_t>(word,1));
word_count.insert(map<string,size_t>::value_type(word,1));
//insert操作
c.insert(v);
c.emplace(args)
c.insert(b,e)
c.insert(il)
c.insert(p,v)
c.emplace(p,args)
//multimap
multimap<string,string> authors;
authors.insrt({"Barth,John","Factor"});
//删除元素
c.erase(k)
c.erase(p)
c.erase(b,e)
//访问元素
c.find(k)
c.count(k)
c.lower_bound(k)
c.upper_bound(k)
c.equal_range(k)
string search_item("Alain de Botton");
auto entries = authors.count(search_item);
auto iter = authors.find(search_item);
while(entries)
{
cout << iter->second << endl;
++iter;
entries--;
}
for(auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item);
beg != end; ++beg)
{
cout << beg->second << endl;
}
//equal_range
for(auto pos = authors.equal_range(search_item); pos.first != pos.end; ++pos.first)
{
cout << pos.first->second << endl;
}
对map使用find代替下标操作
无序容器:使用哈希函数和关键字类型的==运算符,存储上组织为一组桶,哈希函数将元素映射到桶
size_t hasher(const Sales_data &sd)
{
return hash<string>()(sd.isbn());
}
bool eqOp(const Sales_data &lhs, cont Sales_data &rhs){
return lhs.isbn() == rhs.isbn();
}
ch12 动态内存
静态内存:局部static对象、类static数据成员、定义在函数之外的变量。
栈内存:保存定义在函数内的非static对象
内存池(堆):存储动态分配的对象,且显式销毁
shared_ptr类(模板)
默认初始化的智能指针中保存着一个空指针if(p1 && p1->empty()) *p1 = "hi"; //shared_ptr和unique_ptr都支持 shared_ptr<T> sp unique_ptr<T> sp p *p p->mem p.get()//p中保存的指针 swap(p,q)//交换指针 p.swap(q) //shared_ptr独有 make_shared<T>(args) shared_ptr<T>p(q) p = q p.unique() p.use_count()//用于调试
make_shared
最为安全 auto p6 = make_shared<vector拷贝赋值
shared_ptr都有一个引用计数。auto r = make_shared<int>(42); r = q;//递增q指向对象的引用计数,递减r的引用计数,r原来指向的对象自动销毁
自动释放相关联的内存
shared_ptr<Foo> factory(T arg) { rdturn make_shared<Foo>(arg); } shared_ptr<Foo> use_factory(T arg) { shared_ptr<Foo> p = factory(arg); return p;//返回p时,引用计数递增 }
使用了动态生存期的资源的类
程序不知道自己需要使用多少对象(容器类)
程序不知道所需对象的准确类型(ch15)
程序需要在多个对象中共享数据
Blob<string> b1; { Blob<string> b2 = {"a","aa","aaa"}; b1 = b2;//b1和b2共享底层数据,b2被销毁,但b2中的元素不能销毁 }
StrBlob
直接管理内存
int *p1 = new int;//分配失败,抛出std::bad_alloc string *ps = new string(10,'9'); auto p1 = new auto(obj); const int *pci = new const int(1025); delete p; int i, *pi1 = &i, *pi2 = nullptr; double *pd = new double(33), *pd2 = pd; delete i;//false delete pi1;//undefined delete pd;//true delete pd2;//undifined delete pi2;//true
由内置指针管理的动态内存在显式释放前都会存在
new和delete管理内存存在三个问题:- 忘记delete
- 使用已经释放掉的内存
- 同一块内存释放两次
shared_ptr和new结合使用
//接受指针参数的智能指针构造函数是explicit shared_ptr<int> p1 = new int(1025);//wrong shared_ptr<int> p2(new int(1025));true shared_ptr<int> clone(int p){ return shared_ptr<int>(new int (p)); } //定义和改变shared_ptr shared_ptr<T> p(q) shared_ptr<T> p(u) shared_ptr<T> p(q,d) shared_ptr<T> p(p2,d) p.rerset() p.reset(q) p.reset(q,d)
不要混合使用智能指针和普通指针
不要使用get初始化另一个智能指针或为智能指针赋值shared_ptr<int> p(new int(42)); int *q = p.get(); { shared_ptr<int>(q); } imt foo = *p;//未定义,p指向的内存已释放
reset更新引用计数,经常与unique一起使用
if(!p.unique())
p.reset(new string(*p));
*p += newVal;智能指针与异常
智能指针陷阱:- 不使用相同的内置指针值初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不适用get()初始化或reset另一个智能指针
- 使用get()返回的指针,最后一个对应的智能指针销毁后,指针变为无效
- 使用智能指针,传递一个删除器
unique_ptr
unique_ptr不支持拷贝和赋值//unique_ptr支持的操作 unique_ptr<T> u1 unique_ptr<T,D> u2 unique_ptr<T,D> u(d) u = nullptr u.release() u.reset() u.reset(q) u.reset(nullptr)
拷贝或赋值一个将要被销毁的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<objT, delT> p(new objT, fcn); void f(destination &d) { connection c = connect(&d); unique_ptr<connection, decltype(end_connection)*> p(&c,end_connection); //使用连接,且f退出时,connection会被关闭 }
weak_ptr
不控制所指向对象生存期的智能指针,指向一个shared_ptr管理的对象,不会改变shared_ptr的引用计数。weak_ptr<T> w weak_ptr<T> w(sp) w = p w.reset() w.use_count() w.expired() w.lock() auto p = make_shared<int>(42); weak_ptr<int> wp(p); if(shared_ptr<int> np = wp.lock()) //...
大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更不容易出线内存管理错误且可能有更好的性能。
new和数组
int *p = new int(get_size());
allocator类
allocator<T> a a.allocate(n) a.deallocate(p,n) a.construct(p,args) a.destroy(p) uninitialized_copy(b,e,b2) uninitialized_copy_n(b,n,b2) uninitialized_fill(b,e,t) uninitialized_fill_n(b,n,t)
ch13 拷贝控制
拷贝构造函数
class Foo { public: Foo(); Foo(const Foo&); }
合成~:
类类型成员:使用其拷贝构造函数
内置类型:直接拷贝
对于数组,如果不是类类型,则逐元素拷贝;否则按照元素拷贝构造函数拷贝
class Sales_data{
public:
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
}
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}
//拷贝初始化
string dots(10, '.');//直接初始化
string s(dots);//直接初始化
string s2 = dots;//拷贝初始化
string nullbook = "9-99-99999-9";//拷贝初始化
string nines = string(100, '9');//拷贝初始化
拷贝发生在:
=定义变量
将一个对象从实参传递给非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
花括号初始化一个数组或一个struct
当传递一个实参或从函数返回一个值时,不能隐式使用一个explicit构造函数。vector
编译器可以绕过拷贝构造函数
拷贝赋值运算符
重载运算符:本质上是函数,由operator后接运算符符号。class Foo{ public: Foo& operator=(const Foo&); }
合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs){ bookNo = rhs.bookNo; units_sold = rhs.units_sold; revenue = rhs.revenue; return *this; }
析构函数
析构函数不接受参数,不能重载
析构函数被调用:- 变量离开作用域被销毁
- 当以额对象被销毁时,其成员被销毁
- 容器被销毁时,其元素被销毁
- 动态分配的对象,指向它的指针用delete被销毁
- 临时对象,创建它的表达式结束时被销毁
当指向一个对象的引用或指针离开作用域时,析构函数不会执行
析构函数体不直接销毁成员,而是在析构函数体之后隐含的析构阶段被销毁
三/五原则
如果一个类需要析构函数,那么必然需要拷贝构造和拷贝赋值函数class HasPtr{ public: HasPtr(const std::string &s = std::string()): ps(new std::string(s)),i(0){} ~HasPtr(){delete ps;} //wrong,HasPtr需要一个拷贝构造和拷贝赋值 } //赋值 HasPtr f(HasPtr hp) { HasPtr ret = hp; return ret; } //hp和ret都被销毁,导致ps被析构两次 HasPtr p("some values"); f(p);//f结束时,p.ps被析构 HasPtr q(p);//q和p都指向无效内存
拷贝构造和拷贝赋值是共生的
使用=defalut
我们只能对具有合成版本的成员函数使用=defalut(默认构造,拷贝控制成员)阻止拷贝
=delete通知编译器,我们不希望定义这些成员。我们可以对任何函数指定,主要用途是禁止拷贝控制成员。
析构函数不能=delete合成拷贝控制成员可能是删除的
- 类的某个成员的析构函数是删除的或private
- 拷贝构造函数是删除的或private
- 拷贝赋值运算符是删除的或private
- 类有一个引用成员,没有类内初始化器,或有一个const成员,没有类内初始化器,则默认构造函数是删除的
类的行为像一个值,意味着它应该有自己的状态,副本与原对象完全独立。如标准库容器和string类
类的行为像一个指针,共享状态。副本和原对象使用相同的底层数据。如shared_ptr
IO和unique_ptr不允许拷贝和赋值,既不是指针也不是值
类值
class HasPtr{ public: HasPtr(const std::string &s = std::string()): ps(new std::string(s)),i(0){} HasPtr(const HasPtr &p): ps(new std::string(*p.ps)), i(0){} HasPtr& operator=(const HasPtr &); ~HasPtr(){delete ps;} private: std::string *ps; int i; } HasPtr& HasPtr::operator=(const HasPtr &rhs){ auto newp = new string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; } //note //如果一个对象赋予它自身,赋值运算符必须正常工作 //大多数赋值运算符组合了析构函数和拷贝构造函数的工作
类指针
class HasPtr{ public: HasPtr(const std::string &s = std::string()): ps(new std::string(s)),i(0),use(new std::size_t(1)){} HasPtr(const HasPtr &p): ps(new std::string(*p.ps)), i(p.i), use(p.use{++*use;} HasPtr& operator=(const HasPtr &); ~HasPtr(); private: std::string *ps; int i; } 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; }
swap
拷贝控制示例
动态内存管理类
对象移动
//右值引用
int i = 42;
int &r = i;//true
int &&rr = i;//wrong
int &r2 = i*42;//wrong
const int &r3 = i*42;//true
int &&rr2 = i*42;//true
右值引用:所引用的对象即将销毁;对象没有其他用户
显示将左值转换成右值引用:int &&rr3 = std::move(rr1);
StrVec::StrVec(StrVec &&s) noexcept//承诺不抛出异常
: elements(s.elements), first_free(s.first_free),cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}
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;
}
ch14 重载运算与类型转换
基本概念
一元运算符有一个,二元运算符有两个参数。operator()含有默认实参- 赋值=、下标[]、调用()和成员访问箭头->必须是成员
- 复合赋值运算符一般来说是成员
- 递增、递减和解引用运算符,通常应该是成员
- 对称性的运算符,如算术、相等、关系、位运算等,则为普通的非成员函数
输入和输出运算符
ostream &operator<<(ostream &os, const Sales_data &item) { os << item.isbn(); return os; } //输入输出运算符必须是非成员函数 istream &operator>>(istream &is, 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; }
算术和关系运算符
定义成非成员函数相等运算符
bool operator==(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue; } bool operator!=(const Sales_data &lhs, const Sales_data &rhs) { return !(lhs == rhs); }
- 如果一个类含有判断两个对象是否相等的操作,则它应该把函数定义成operator==而非一个普通的命名函数
- 若定义了operator==,则应该能够判定一组给定对象中是否含有重复数据
- 同时应该定义!=
- ==和!=应该把工作托管给其中一个
关系运算符
定义operator<比较有用- 定义顺序关系,使得与关联容器中对关键字的要求一致
- 类同时有保持一致
如果存在唯一一种逻辑可靠的<定义,则应该考虑定义<,若同时包含产生的结果一致才定义
赋值运算符
复合赋值运算符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]; } private: std::string *elements; }
递增/递减运算符
//前置 class StrBlobPtr{ public: StrBlobPtr& operator++(); StrBlobPtr& operator--(); }; StrBlobPtr& StrBlobPtr::operator++() { check(curr,"increment past end of StrBlobPtr"); ++curr; return *this; } StrBlobPtr& StrBlobPtr::operator--() { --curr; check(curr,"increment past begin of StrBlobPtr"); return *this; } //后置 class StrBlobPtr{ public: StrBlobPtr& operator++(int); StrBlobPtr& operator--(int); }; StrBlobPtr& StrBlobPtr::operator++(int) { StrBlobPtr ret = *this; ++*this; return ret; } StrBlobPtr& StrBlobPtr::operator--() { StrBlobPtr ret = *this; --*this; return ret; }
成员访问运算符
class StrBlobPtr{ public: std::string& operator*()const { auto p = check(curr, "dereference past end"); return (*p)[curr]; } std::string* operator->()const { return & this->operator*(); } }
函数调用运算符
重载、类型转换与运算符
ch15 面向对象程序设计
OOP(object-oriented programming)核心思想是数据抽象、继承和动态绑定。
- 抽象:接口和实现分离
- 继承:定义相似的类型并对相似关系建模
- 动态绑定:一定程度上忽略类型的却别,以统一的方式使用他们的对象
使用基类的引用/指针调用一个虚函数将发生动态绑定
OOP的核心思想是多态。引用/指针的静态类型和动态类型不同正是C++支持多态的根本所在
基类与派生类
class Quote{
public:
Quote() = default;
Quote(const std::string &book, double sales_price):
bookNo(book),price(sales_price){}
std::string isbn()const {return bookNo;}
virtual double net_price(std::size_t n)const
{
return n * price;
}
virtual ~Quote() = default;
private:
std::string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote : public Quote{
public:
Bulk_quote() = default;
Bulk_quote(const string&, double, std::size_t, double);
double net_price(std::size_t)const override;
private:
std::size_t min_qty = 0;
double discount = 0.0;
}
//派生类到基类的隐式转换
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk;
Quote &r = bulk;
Bulk_quote(const std::string book, double p,std::size_t qty, double disc):
Quote(book,p),min_qty(qty),discount(disc){}//首先初始化基类,然后按照声明顺序初始化
继承与静态成员:只存在该成员唯一定义
防止继承发生:final
- 虚函数
对虚函数的调用直到运行时才被解析。
通过作用域运算符可以强行调用基类虚函数 - 抽象基类
含有纯虚函数=0的类是抽象基类。不能直接创建一个抽象基类对象。 - 访问控制和继承
protected:- 和私有成员类似,受保护的成员对于类的用户是不可访问的
- 与公有成员类似,受保护的成员对于派生类的成员和友元是可访问的
- 继承中的类作用域
派生类成员隐藏同名基类成员
p->mem()- 首先确定p的静态类型。
- p的静态类型对应的类内赵mem,如果找不到,则在直接基类内查找直至继承链的顶端,还是找不到编译器报错。
- 找到了mem,进行类型检查
- 若mem是虚函数,通过引用/指针调用,则在运行时确定到底运行虚函数的哪个版本
- 反之,若是通过对象或不是虚函数,则产生一个常规函数调用
- 构造函数与拷贝控制
虚析构函数 = default,阻止合成移动操作
合成拷贝控制与继承
派生类的拷贝控制成员
继承的构造函数 - 容器与继承
ch16 模板与泛型编程
定义模板
template <typename T> int compare(const T &v1, const T &v2) { if(v1 < v2) return -1; if(v2 < v1) return 1; return 0; } template <typename T> T foo(T* p) { T tmp = *p; return tmp; } template <typename T, class U> calc(const T&, const U&);
非类型模板参数
inline和constexpr的函数模板template <typename T> int compare(const T &v1, const T &v2) { if(less<T>()(v1,v2)) return -1; if(less<T>()(v1,v2)) return 1; return 0; }
类模板
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化
类模板作用域内,可以直接使用模板名而不必指定模板实参
类模板与友元:一对一(BlobPtr
类模板的static:每个实例都有唯一一个static对象
模板参数当我们希望通知编译器一个名字表示类型时,必须使用typename,而不能使用class
默认模板实参
template <typename T, typename F = less<T>> int compare(const T &v1, cosnt T &v2,F f = F()) { if(f(v1,v2)) return -1; if(f(v2,v1)) return 1; return 0; }
成员模板(不能是虚函数)
template <typename T> class Blob{ template <typename It> Blob(It b, It e); }; template <typename T> template <typename T> Blob<T>::Blob(It b, It e):data(std::make_shared_ptr<std::vector<T>>(b,e)){}
控制实例化
多文件中实例化相同模板的开销非常严重,可以通过显式实例化避免开销。
extern template class Blob<string>; template int compare(const int&, const int&);
在一个类模板的实例化定义中,所用类型必须能够用于模板的所有成员函数
效率和灵活性运行时绑定删除器:shared_ptr
编译时绑定删除器:unique_ptr
模板实参推断(函数实参->模板实参)
调用中应用于函数模板const转换,将一个非const对象的引用传递给const引用形参
数组或函数指针转换
显式实参
auto val3 = sum<long,long>(i,lng); template <typename T1,typename T2,typename T3> T3 alternative_sum(T2,T1); auto val2 = alternative_sum(long long,int,int)(i,lng);
尾置返回类型与类型转换
//允许我们在参数列表之后声明返回类型 template <typename It> auto fcn(It beg, It end)->decltype(*beg) { return *beg; }
std::move实现
转发:如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的const属性和左值/右值属性将得到保持
使用std::forward保持类型
重载与模板
正确定义一组重载的函数模板需要对类型间的关系和模板函数允许的有限的实参类型转换有深刻的理解。可变参数模板
模板参数包:0个/多个模板参数
//Args是模板参数包,rest是函数参数包 template <typename T, typename... Args> void foo(const T &t, const Args&... rest); //sizeof...运算符返回包中元素
函数参数包:0个/多个函数参数
包扩展
转发参数包
模板特例化(本质是实例化一个模板)
template<> int compare(const char* const &p1, const char* const &p2) return strcmp(p1,p2); namespace std{ template<> struct hash<Sales_data> { typedef size_t result_type; typedef Sales_data argument_type; size_t operator()(const Sales_data& s)const; }; size_t hash<Sales_data>::operator()(const Sales_data &s)const { return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue); } }
部分特例化类模板,而不能部分特例化函数模板
特例化成员而不是类