blueyi's notes

Follow Excellence,Success will chase you!

0%

C++学习笔记之基础

关于本文

主要是看《C++ primer》时的总结,由于以前学习过C++,所以仅仅记录了自己忘记的以及一些之前没有弄明白的东西。本文举例全部通过gcc 5.1.2验证,使用g++编译进可以通过指定-std=c++11-std=c++0x来启用对C++11的支持。eg: g++ -o hello hello.cpp -std=c++11 -Wall-Wall是开启警告。
参考手册:
1.cplusplus Reference,包含了C++和C的标准库,并带有示例: http://www.cplusplus.com
2.cppreference配全上面使用,该站点有中文版本:http://www.cppreference.com
其他资料:
1.C++中指针和引用的区别:http://stackoverflow.com/questions/57483/what-are-the-differences-between-a-pointer-variable-and-a-reference-variable-in
2.const int*const int * constint const *的区别:http://stackoverflow.com/questions/1143262/what-is-the-difference-between-const-int-const-int-const-and-int-const
3.什么是C++11中的rvaluelvaluexvalueglvalueprvaluehttp://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues

4.使迭代器失效的规则解释:http://stackoverflow.com/questions/6438086/iterator-invalidation-rules

基础知识

1.注释与”交替出现时,谁在前面先匹配谁:
std::cout << /* "*/" /* "/*" */;输出为/*,因为出现/*时后面的第一个*/会自动与其匹配,然后"出现时它会与后面的第一个"进行匹配。注释会被编译器直接忽略掉。

2.以下输出结果为2^32 - 32

1
2
unsigned a = 10, b = 42;
std::cout << a - b << std::endl;

3.C++支持的4种变量初始化的方法:

1
2
3
4
5
int a = 1;
int a(1);
//以下仅在C++11中支持,好处之一是当变量无法容纳初始值时不会进行强制类型转换
int a = {1};
int a{1};

1.通常标准库的头文件使用尖括号<>,非标准库的头文件使用双引号””。

2.函数体外的变量会被自动初始化为0,函数体内的变量不会被自动初始化。

3.extern 可用于声明该变量在其他地方有定义(extern int a;),或者可以在函数外部定义全局变量(extern int a = 10;)。

4.定义const变量(常量)必须在定义时初始化。如果const变量不是用常量表达式初始化,不应该将其在头文件中定义,该const变量应该在一个源文件中定义并初始化,并在头文件中为它添加extern声明,以使其能被多个文件共享。

7.头文件应该用于声明而不是定义,extern int ival = 10;double cash;都属于定义。但头文件中可以定义类、值在编译时就已经知道的const对象和inline函数。

8.通过定义预处理器变量来避免重复包含头文件。eg:

1
2
3
4
5
6
7
8
9
#ifndef HELLO_H   //测试是否定义该预处理器变量,若没有定义,则后面的所有定义都执行,直到#endif
#define HELLO_h
#include <string>
struct Sales_item {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif

9.引用的定义方式为int &a = b;表示 a是b的别名。

string

10.string类型与字符串字面值类型不同。string有以下几种初始化方式:

1
2
3
4
string str; str = "";
string str(s1);str = s1;
string str("hello");str = "hello";
string str(4,'l');str = "llll";

11.string类型的输入操作符会读取并忽略有效字符前的的所有空白字符,如空格、换行符、制表符。读取字符直到再次遇到空白字符,读取终止,且该空白字符仍在输入流中。

12.getline(cin, line) 一次读取一行,且不忽略开头的空白字符,遇到换行符终止并丢弃换行符(此时换行符不在输入流中),返回一个istream引用。如果这个返回值作为if或者while等的bool逻辑判断,则读取成功返回true,失败返回false,所以istream& getline(istream& in, string & str, char delim = '\n')函数可以使用while循环进行判断。

13.string类的s.size()/length()返回s中英文字符个数或者字节数,返回值为string::size_type类型,该类型与unsigned类型具有相同的含义,不能将其赋给int类型。s.empty()测试s是否为空。

14.string对象使用+操作符与字符串字面值进行连接时左右操作数必须至少有一个是string对象,注意str1+str2返回的是一个string对象。所以这个是非法的string str = "Hello" + "world" + s1;,而这两种是合法的string str = s1 + "Hello" + "world";string str = "Hello" +s1 + "world";。另外注意string str = "Hello";是一个赋值初始化的过程,构造初始化应为string str("Hello");

15.cctype标准库头文件中测试函数返回的是int类型值,测试失败返回0,测试为真返回非0;其中的tolower()和toupper()返回也为int,即对应字符的ASCII码,其中还包含了isalnum(),isalpha()等。

16.C++通过cstring头文件提供对C语言风格的字符串操作。如strlen(s), strcmp(s1, s2), strcat(s1, s2), strcpy(s1, s2), strncat(s1, s2, n), strncpy(s1, s2,n)。其中strn类的函数更安全,它能够控制复制字符的个数。strlen返回的是字符个数,不包括null结束符。处理C风格字符串时一定要记得字符串结束符null。尽量避免使用C风格字符串而是使用标准库类型string,其安全性和效率更高。可以使用C风格字符串初始化string,但不能使用string直接初始化C风格字符串,但string类提供的c_str()函数可以返回一个const类型的C风格字符串。

17.C++11中可以使用范围for,即range for来遍历给定序列中的每个元素,并对该元素进行操作,for (declaration : expression){}
注意默认情况下范围for使用的是非引用的方式访问序列中的元素,所以此时无法修改序列中的元素,如果需要修改序列中的元素,则必须使用引用。
eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string str("Hello world!!!");
//punct_cnt的类型将与str.size()类型一致,都是string:size_type
decltype(str.size()) punct_cnt = 0;
//使用auto来让编译器自动判断所需类型
for (auto c : str) {
if (ispunct(c))
++punct_cnt;
}
//输出标点符号的个数
cout << punct_cnt << endl;

//使用引用c访问每个字符并将其改成大写
for (auto &c : str) {
c = toupper(c);
}
cout << str << endl;

18.注意char *字义的字符串与string并不一样,char *字义的字符串是常量字符串,无法修改它们的值,如char *str = "Hello world",此时str是只读的。

C++11下的字符串数组

1
2
3
char *str = "abc"; //C++11下会出现警告,因为这相当于将一个可变的指针,绑定到一个字符面值常量  
const char *str1 = "abc"; //正确
char str2[] = "abc"; //声明一个数组,当然没有问题

vector

16.vector是同一种类型的对象的集合,是一个类模板,可以定义任意多种数据类型,定义方法为vector<type> variable_list;,意为vector类型的var。它与数组极为相似。vector能容纳绝大多数类型的对应作为其元素,但引用不是对象,所以引用不能作为其原素,vector的元素本身也可以是vector,eg: vector<vector<string>> vstr;//需要C++11如果编译器不支持,需要在后面尖括号中加一个空格,如vector<vector<string> > vstr;

17.所有模板的实例化方法都是在模板名字后面跟一对尖括号,在括号中放入实例化信息。

17.初始化vector对象的方法:

1
2
3
4
5
6
7
vector<T>  v1;  //vector保存类型为T的对象,默认构造函数v1为空。
vector<T> v2(v1); //v2是v1的一个副本。
vector<T> v2 = v1; //等价于v2(v1);
vector<T> v3(n, i); //v3包含n个值为i的元素。eg:`vector<int> vec(10, 2);` 10元素,每个元素都被初始化为2。
vector<T> v4(n); //v4含有类型T默认的值初始化的元素的n个副本。
vector<T> v5{a,b,c...}; //v5包含初始值个数的元素,每个元素被赋予相应的初始值,需要C++11的支持
vector<T> v5 = {a,b,c...}; //等价于v5{a,b,c...}

18.vector初始化时应尽量使用()指定vector中的元素数量,而使用{}指定初始值,eg:

1
2
3
4
5
6
vector<int> v1(10, 12); //10个元素,每个元素值都是12
vector<int> v2(10); //10个元素,每个元素值都是0
vector<int> v3; //0个元素
vector<string> v4(10, "hi"); //10个元素,每个元素值都是“hi”
vector<string> v5{10, "hi"}; //10个元素,每个元素值都是“hi”
vector<string> v6{10}; //10个元素,每个元素值都是“”

17.vector对象支持的操作:

1
2
3
4
5
v.empty();  //v为空返回true
v.size(); //v中元素个数
v.push_back(t); //将t插入到v的末尾
v[n]; //返回v中位置为n的元素
v1 = v2; //把v1中的元素替换为v2中元素的副本。

==, !=, <, <=, >, >=保持其操作符惯有的含义。

18.vector可以动态的增加元素。对vector中各元素的访问与对string中各字符的访问方法一样使用下标,为vector对象增加一个元素需要使用v.push_back(t);。v.size()的返回类型与string也类似为vector::size_type。

19.C++由于可以使用内联函数,所以在每次的循环测试中都直接使用v.size()不会增加太多运行代价。

20.string和vector对象在没有定义确定元素时都无法通过下标直接增加值,string只能使用+操作符,vector只能使用v.push_back(t)。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 0;
string str1 = "Hello world!";
string str2;
for(string::size_type ix = 0; ix != str1.size(); ++ix)
{
str2[i++] = str1[ix]; //Error,str2元素个数为0;
str2 += str1[ix]; //OK
}
vector<int> num;
for (vector<int>::size_type ix = 0; ix < 10;++ix)
{
num[ix] = ix; //Error,num[ix]处的元素并不存在,下标只能访问已经存在的元素
num.push_back(ix); //OK
}

迭代器iterator

21.标准库为每一种容器类型都定义了一种迭代器iterator类型。迭代器比下标访问元素更通用化。它与指针非常相似,C++中的容器有:
顺序性容器:
vector: 从后面快速的插入与删除,直接访问任何元素
deque: 从前面或后面快速的插入与删除,直接访问任何元素
list: 双链表,从任何地方快速插入与删除
关联容器:
set: 快速查找,不允许重复值
multiset: 快速查找,允许重复值
map: 一对多映射,基于关键字快速查找,不允许重复值
multimap: 一对多映射,基于关键字快速查找,允许重复值
容器适配器:
stack: 后进先出
queue: 先进先出
priority_queue: 最高优先级元素总是第一个出列
关于容器的两个讨论:http://www.tuicool.com/articles/aiMnAzhttp://www.cnblogs.com/answeryi/archive/2011/12/16/2289811.html
注意虽然string不是迭代器,但string支持迭代器

22.迭代器类型的定义方式为vector::iterator iter;(以vector为例),其定义了一个vector::iterator类型的迭代器变量。

23.每一种容器都定义一对命名为begin和end的函数,用于返回迭代器。如果容器中有元素则begin返回的迭代器指向第一个元素。end函数返回的是容器的最后一个元素的下一个,它指向的是一个不存在的元素。如果容器为空,则begin与end相同。
eg:vector<int>::iterator iter = ivec.begin();//iter初始化为由为begin的vector操作返回的值。如果ivec不为空,则iter指向元素为ivec[0],iter就像指向容器中元素的指针。如果iter指向ivec的第一个元素,则*iter与ivec[0]指向就是同一个元素,iter支持++,–操作符。

24.迭代器可以使用==或者!=进行比较,如果两个迭代器指向同一个元素则相等。

25.iter->mem 表示解引用iter并获取该元素的名为mem的成员,等价于(iter).mem,例如对于string的vector中元素支持的empty()函数,(.ter).empty() <=> ter->empty()

26.由于所有标准库容器的迭代器都定义了==和!=操作符,所以可以养成使用!=和迭代器而不是<和下标的习惯。

25.每一种迭代器的const_iterator类型只能用于读取它所指向元素的值,不能写,相当于常量指针。begin和end返回值是不是常量由对象是不是常量决定。eg:

1
2
3
4
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); //it1的类型是vector<int>::iterator
auto it2 = cv.begin(); //it1的类型是vector<int>::const_iterator

26.C++11中增加了cbegin()和cend()它们始终返回const_iterator类型,不管对象是不是const。

26.迭代器举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
#include<vector>
#include<iterator>
int main(void)
{
using namespace std;
vector<int> num(10); //声明含有10个int元素的vector对象。
for (vector<int>::iterator ite = num.begin(); ite != num.end(); ++ite) //使用迭代器将num容器中的元素全部置为1
{
*ite = 1;
}
for (vector<int>::iterator ite = num.begin(); ite != num.end(); ++ite)
cout << *ite << " ";
cout << endl;
return 0;
}

27.任何改变vector长度的操作都会使已存在的迭代器失效,例如在调用push_back之后,就不能再信任之前指向这个vector的迭代器的值了。所以凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

28.vector迭代器支持与指针类似的与常量相加、相减以及指向同一个vector的两个迭代器相减的操作,但不能相加,就像两个指针相加的未定义的一样,同时支持两个迭代器的比较,比较的是它们的位置。

29.string和vector都定义了带符号的difference_type类型,该类型是一个带符号的整型数,可正可负。两个迭代器相减的结果就是difference_type类型。

29.bitset是一种用于处理二进制位的类模板,定义时必须指定其长度值。初始化方式为:

1
2
3
4
5
6
7
bitset<n> b;  //b有n位,每位都为0
bitset<n> b(u); //b是unsigned long 型u的一个副本,用u从低位向高位对b赋值,u不够时高位填充0,多余时舍掉
bitset<32> bitvec1(0xffff); //bitvec为0-15为1,16-31为0,当然这里可以放10进制,也会被自动转为2进制。
bitset<n> b(s); //b是string对象s中含有的位串的副本 ,其中string直接表示为位模式。
string strval("1100"); //注意这里只能有0和1组成。
bitset<32> bitvec2(strval); //bitvec2第2和3位置1,其余位都是0.注意此处不能直接放一个字符串字面值
bitset<n> b(s, pos, n); //b是s中从位置pos开始的n个位的副本

30.bitset对象有多种操作能够方便的处理位,参考bitset的reference。也可以使用下标操作符读写某个位。

31.bitset对象返回的数值类型为cstddef头文件中的size_t类型,该类型为与机器相关的unsigned类型。eg:size_t sz = bitvec.size();

32.函数体外定义的数组会被自动初始化为0,函数体内定义的数组不会自动初始化。数组元素为类类型时,不管在哪里定义都会自动调用默认构造函数进行初始化,所以string定义的变量不管在哪里都默认为空字符串,如果没有默认构造函数则必须显示初始化。数组大小必须要在编译时已经,非const的变量只有在运行时才赋值,所以不能使用它作为数组长度。在没有明显性能影响的情况下,能使用vector的地方尽量不要使用数组。使用字符串初始化的字符数组会在数组结尾自动添加’\0’作为结束符。注意数组不能像vector那样直接赋值以及使用另一个数组对其初始化。数组下标类型为size_t

指针

33.指针进行初始化或赋值只能使用以下四种类型的值:1.0值常量表达式,只能是编译时可以获得的0值整型const对象或字面值常量0。2.类型匹配的对象的地址。3.另一对象之后的下一地址。4.同类型的另一个有效指针。eg:

1
2
3
4
5
6
7
int val;
int zero = 0;
const int c_ival = 0;
int *pi = ival; //ERROR,pi只能指向地址,eg:int *pi = &ival;
pi = zero; //ERROR,zero为int型变量,只有在运行时才能获取其值
pi = c_ival; //OK,const的变量在编译时已经确定其值。
pi = 0; //OK,0值字面值常量 ,也可以使用cstdlib头文件中的NULL,int *pi = NULL <=> 0值

34.空指针的三种定义方法:

1
2
3
4
int *p1 = nullptr;  //C++11才支持,等价于int *p1 = 0;
int *p2 = 0;
//首选include cstdlib头文件
int *p3 = NULL;

34.void * 指针。

35.引用与指针的区别:

1
2
3
4
5
int iva1 = 1024, iva2 = 2048;
int *p1 = &iva1, *p2 = &iva2;
int &r1= ival, &r2= iva2;
p1 = p2; //p1指向P2
r1= r2; //将iva2的值赋给iva1

36.两个指针相减返回的数据类型为ptrdiff_t,其也在cstddef中定义,size_t是unsigned类型,而ptrdiff_t则是signed类型。

数组与指针

1.数组名是数组元素的首地址,即&a[0] == a; 使用下标访问数组时,实际上是使用下标访问指针。eg:

1
2
3
4
int ia[] = {0,1,2,3};
int i = ia[2]; //等价于i = *(ia + 2);
int *p = &ia[2];
int j = p[-2]; //等价于j = *(p-2) <=> j = *(&ia[2]-2) = 0;

2.在数组与指针混合应用时应采用从数组的名字开始由内向外的方式进行解释

1
2
3
4
5
int *ptr[10];  //ptr是含有10个指向int型数据的指针的数组  
int &ref[10]; //错误,引用不是对象,所以不存在存放引用的数组
int (*parray)[10] = &arr; //parray是一个指向含有10个整数数组的指针
int (&refarr)[10] = arr; //refarr是一个引用含有10整数数组的引用
int *(&refparr)[10] = ptr; //refparr是一个引用含有10个int指针的数组的引用

38.指针相当于数组的迭代器。可以定义指向数组最后一个元素的下一个位置的指针,该指针只能用于比较,不能访问。eg:

39.以下定义数组的方法是非法的,因为数组长度必须是常量表达式。

1
2
unsigned buff_size = 1024;
int arr[buff_size];

33.使用指针访问数组,C++11在头文件iterator中定义了两个函数begin()和end()用于返回指向数组的首元素和尾元素的下一个位置的指针。用法为将数组作为其参数传递。

1
2
3
4
5
6
7
8
9
10
//非C++11的方式
const size_t arr_sz = 5;
int int_arr[arr_sz] = {0, 1, 2, 3, 4, 5};
for (int *pbegin = int_arr, *pend=int_arr+arr_sz; pbegin != pend; ++pbegin)
cout << *pbegin << ' '; //单引号还是双引号在些处一样,单引号表示字符,双引号表示一个字符串,以`\0`结束。

//C++11的方式
int *pbeg = begin(int_arr), *pend = end(int_arr);
while (pbeg != pend)
cout << pbeg++ << ' ';

39.const和指针:
指向const对象的指针:const double *cptr;不需要对其初始化,因为const修饰的是*cptr,它是一个解引用变量,而cptr是一个指向const对象的指针,其本身不是const变量,只是它指向的对象是个const的变量,不能通过它去修改所指向的值,但可以将其指向其他变量。当然此处的cptr可以指向一个非const变量。

const指针:double *const cptr = &a定义了一个const指针cptr,定义时必须初始化。
因为const修饰的是cptr而不是*cptr,所以不能修改cptr的指向,cptr只能指向变量a,但可以通过cptr修改a的值,例如:*cptr = 3.14;

1.指向const对象的const指针:const double *const cptr = &pi;定义时必须初始化,且pi必须是const对象。既不能修改cptr的指向,也不能通过cptr修改其所指向的值。

2.typedef与指针一起使用时的一个例子能更好的理解const的修饰作用:

1
2
3
typedef string *pstring;
const pstring cstr;
//cstr的类型为一个指向string类型的const指针。因为pstring是一个指针,它相当于由typedef定义的string *类型,typedef是别名,编译时不会简单的替换,而const修饰的是个指针,所以cstr是个const指针。而define是在预编译时进行简单替换。

39.C++11中新增加了通过using来定义类型的别名,eg: using SI = Sales_item;这样SI就是Sales_item的别名,其作用就是把等号左侧的名字规定成等号右侧类型的别名。

40.当使用非const对象初始化const对象的引用时,系统会自动将非const对象转换为const对象。但不能使用const对象对非const对象的引用初始化。eg:

1
2
3
4
int a = 1;
const int *p = &a; //OK
const int b = 2;
int *p2 = &b; //Error

41.C++11支持constexpr变量,该类型的变量由编译器来验证变量的值是否为一个常量表达式,声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化,但初始始化时也可以使用constexpr函数作为初始值。

42.C++11引入了类型指示符decltype,它的作用是选择并返回操作数的数据类型。eg: decltype(f()) sum = x;其中sum的类型就是函数f的返回类型。
decltype((variable))的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

42.动态数组:C++中的new和delete实现了C语言中malloc和free相同的功能。

定义动态数组时只需要指定类型和数组长度,不必为数组对象命名,可以直接初始化为默认值,或者不进行初始化,new表达式返回指向新分配数组的第一个元素指针:

1
2
size_t n = get_size();
int *pia = new int[n]();//初始化为int.

使用之后需要使用delete语句显式释放分配的动态空间:delete [] pia;
eg:

1
2
3
4
5
const char *err = "Error: afunction declaration must specify a function return type!";
int dimension = strlen(err) + 1;
char *errMsg = new char[dimension];
strncpy(errMsg, err, dimesion);
delete [] errMsg; //一定要记得创建时就把释放定后,以免忘记释放

43.可以使用数组直接初始化vector对象,但必须给出数组的两个位置指针,如果需要用整个数组初始化,则第二个指针必须是指向数组最后一个元素的下一位置的地址。eg:

1
2
3
4
const size_t arr_size = 5;
int int_arr[arr_size] = {0, 1, 2, 3, 4};
vector<int> ivec(int_arr, int_arr + arr_size);//使用整个数组。
vector<int> ivec(begin(int_arr), end(int_arr));//使用整个数组。需要C++11支持

多维数组与指针

1.C++中的多维数组指的是数组的数组,不存在真正意义上的多维数组。例如int a[3][4]意思是含有3个int型元素的数组a中,每个元素都是含有4个int型整数的数组。

1
2
3
4
5
6
7
int a[] = {0, 1, 2, 3, 4};
int b = 4;
int *ip[4]; //指向int类型的指针数组,也就是这个数组里面含有4个指针,每个指针都指向int型数据。
ip[1] = &b; //将b的地址存放在ip[1]。
int (*ap)[4]; //指向4个int型元素的数组指针。 相当于指向地址的指针。
ap = &a; //让ap指向数组首地址的的地址。
cout << (*ap)[1]; //输出数组a的第2个值,等价于*(*ap + 1) <=> a[1] <=> *(a + 1)

2.多维数组中的引用定义:

1
2
int ia[2][3] = {0};
int (&row)[3] = ia[1]; //定义一个引用row,将它绑定到ia的第二个含有3个元素的数组上

3.多维数组中使用range for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int ia[2][3];
size_t cnt = 0;
//此处必须使用引用,一方面是为了修改遍历的值,另一方面防止使用auto时row被自动转换成指针,因为ia是数组
for (auto &row : ia) {
for (auto &col : row) {
col = cnt;
++cnt;
}
}
//外层循环依然要使用引用,防止使用auto时row被自动转换成指针,因为ia是数组。为了避免修改数组,所以使用const
for (const auto &row : ia) {
//内层循环col即指向row中的每一个元素,因为C++中的多维数组就是数组的数组,row中存放的就是int类型
for (auto col : row) {
cout << col << " ";
}
cout << endl;
}

4.C++11中使用using简化多维数组中指针的使用

1
2
3
4
5
6
7
8
9
10
using int_arr = int[3];  //声明int[3]的别名
typedef int int_arr[4]; //等价的typedef声明
//prow指向外层数组
for (int_arr *prow = ia; prow != ia + 2; ++prow) {
//*prow即内层数组,pcol指向内层数组的第一个元素,也就是每一行的开头
for (int *pcol = *prow; pcol != *prow + 3; ++pcol) {
cout << *pcol << " ";
}
cout << endl;
}

45.通过指针遍历字符串:

1
2
char *cp = "Hello world";
while(cp && *cp); //表示当cp为非空指针并且cp指向的字符不为空字符null('\0')时执行循环体。

46.C++为在点操作符中使用的解引用操作定义了一个同义词:箭头操作符(->)。eg:(*p).foo; <=> p->foo;

47.一个帮助理解指针、引用、auto、for range的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
using namespace std;
int main()
{
int ia[3][4];
for (size_t row = 0; row < 3; ++row) {
for (size_t col = 0; col < 4; ++col) {
ia[row][col] = col + row * 4;
}
}
//使用C++11中的using来简化定义指向数组的指针
using int_arr = int[4];
//for (const int_arr &row : ia) { //类型别名控制外部循环
//使用引用输出
cout << "Reference" << endl;
for (const int (&row)[4] : ia) {
for (const int col : row) {
cout << col << " ";
}
}
cout << endl << "Subscript" << endl;
//使用下标输出
for (size_t row = 0; row < 3; ++row) {
for (size_t col = 0; col < 4; ++col) {
cout << ia[row][col] << " ";
}
}
cout << endl << "Pointer" << endl;
//使用指针输出
for (const int_arr *row = ia; row != ia + 3; ++row) { //使用类型别名,row是一个指向含有4个int型元素的指针,ia中含有3个int *型的元素,每个元素又含有4个int的整型
// for (const int (*row)[4] = ia; row != ia + 3; ++row) {
for (const int *col = *row; col != *row + 4; ++col) {
cout << *col << " ";
}
}
cout << endl << "auto" << endl;
//使用auto
for (const auto &row : ia) {
for (auto col : row) {
cout << col << " ";
}
}
cout << endl;
return 0;
}

表达式和语句

sizeof

47.sizeof操作符返回的是编译时常量

48.动态创建的对象初始化时如果不进行直接初始化,对于内置类型或者没有默认构造函数的类型不会自动进行自动初始化。而有默认构造函数的类都会自动调用默认构造函数进行初始化。

1
2
int *pi = new int;    //不会自动初始化
int *pi = new int(); //自动初始化为0

48.i != j < k; is equivalent to i != (j < k);

49.sizeof运算符作用的结果,部分的依赖于它的作用的类型:

  • 对char或者类型为char的表达式执行sizeof运算,结果为1
  • 对引用类型执行sizeof运算得到被引用对象所占空间大小
  • 对指针执行sizeof运算返回指针本身所占空间的大小
  • 对解引用执行sizeof运算,返回指针所指向的对象所占的空间大小,指针不需要有效
  • 对数组执行sizeof运算返回整个数组所占空间的大小,相当对使用sizeof对数组中的每个元素运算,并将运算结果求和,注意sizeof作用于数组时,不会将数组转换为指针。
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。也就是说通常对一个vector或string对象执行sizeof运算得不到其占用的总空间大小。例如在我的64位win7操作系统上,vector<int> ivec(1000); cout << sizeof ivec;输出为24。
  • 对一个没有声明任何成员变量与成员函数的类名执行sizeof,返回结果为1。因为为了区分该类的不同对象,编译器会在编译时插入一个char,所以结果为1。

强制类型转换

49.C++风格强制类型转换操作符:static_cast、dynamic_cast、const_cast、reinterpret_cast。使用方式为:

1
2
3
4
5
cast-name<type>(expression)。
const_cast<char *>(pc_str);//转换掉表达式的const性质。
double d = 2.0;
static_cast<char>(d); //将d强制转换为char类型,所有编译器隐式执行的转换都可以通过static_cast显式完成。
static_cast<type *>(pointer); //可以强制将存放在void *中的指针值强制转换为原来的指针类型。

应该尽量避免使用强制类型转换。
dynamic_cast支持运行时类型识别

50.static_cast可用于找回存在于void*指针中的值:

1
2
void *p = &d;
double *dp = static_cast<double*>(p);

50.switch求解的表达式可以非常复杂,也可以直接定义并初始化一个变量。eg:switch(int ival = get_response())

51.C++中switch语句中的case之句可以放在一行。eg: case 'a': case 'e' : case 'i' : case 'o':

52.C++中pair类型的用法:pair类型包含在std命名空间下,是一种结构模板类型,它可以同时存储两个类型不同的值。是std::tuple类型的一种只含有两个元素的特例。它含有两个属性,分别是first表示第一个元素值,second表示第二个元素值。使用方法如下:

1
2
3
4
5
6
pair<string, double> book1("The old man and The sea", 99.99);
pair<string, double> book2;
book2.first = "Harry porter";
book2.second = 88.88;
cout << book1.first << ": " << book1.second << endl;
cout << book2.first << ": " << book2.second << endl;

异常

51.C++的异常:throw表达式抛出异常,try…catch…处理异常。
标准异常(4个):

  • exception头文件定义了最常见的异常类,它的类名是exception。这个类只通知异常的产生,但不会提供更多的信息。
  • stdexcept头文件定义了几种常见的异常类。
  • new头文件定义了bad_alloc异常类型,提供因无法分配内存而由new抛出的异常。
  • type_info头文件定义了bad_cast异常类型。

52.runtime_error异常抛出时必须初始化,示例:

1
2
3
if (item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to same ISBN");
cout << item1 + item2 << endl;

53.try语句块示例:
runtime_error类的成员函数what()返回初始化对象时所用的string对象副本,throw语句应在try块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//输入两个整数,输出其相除的结果
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
for (int num1, num2; cout << "Enter two number: " << endl, cin >> num1 >> num2;) {
try {
if (num2 == 0) {
throw runtime_error("Zero is error!");
}
cout << num1 << " / " << num2 << " = " << static_cast<double>(num1) / num2 << endl;
} catch (runtime_error err) {
cout << err.what() << endl << "Enter \"y\" to continue or \"n\" to end" << endl;
char ch;
cin >> ch;
if (!cin || ch != 'y')
break;
}
}
return 0;
}

函数

1.函数在调用前必须声明,定义可以在调用之后,跟C语言一样。

2.计算阶乘的函数

1
2
3
4
5
int fact(int i)
{
//注意这里对于0的处理非常合适啊
return i > 1 ? i * fact(i - 1) : 1;
}

3.含有函数声明的头文件应该被包含到定义函数的源文件中。

52.使用C++中的引用形参比直接使用C类型的指针进行实参访问更安全和自然。引用相当于原变量的一个别名,所以可以直接修改原变量的值。eg:

1
2
3
4
5
6
void swap(int &v1, int &v2)
{
int tmp = v1;
v1 = v2;
v2 = tmp;
}

53.能用const引用的地方,多用const引用,利用const引用可以避免函数传递时的参数复制。即对于不需要修改变量的函数形参使用引用时应该增加const修饰。

54.const修饰的string引用参数可以直接接受字面值字符串。

1
2
3
4
int find1(const string &str);  
find1("Hello"); //OK
int find2(string &str);
find2("Hello"); //ERROR,type error

55.可以通过给函数传递数组首指针begin(arr)和尾指针end(arr)的方式来限定数组的大小,并遍历数组。

56.int &arr[10]是引用的数组,而int (&arr)[10]却是数组的引用。即声明一个引用,该引用绑定到某个数组。

57.向函数传递多维数组时,第二维的维度不能省略。int *matrix[10]定义10个指针构成的数组。int (*matrix)[10]定义指向含有10个整数的数组的指针。

58.C++的main函数传递参数时,定义方法为int main(int argc, char *argv[])或者int main(int argc, char **argv)其中第二个数组中存放的是C风格字符串的指针,所以后一种形式中的argv指向char*。当实参传递给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次为命令行提供的实参,最后一个指针之后的元素值保存为0。eg:prog -d -o ofile data0中argc为5,argv元素分别为”prog”,”-d”,”-o”,”ofile”,”data0”,0。

59.C++11可以使用initializer_list类型的形参来实现多个不同数量相同类型的参数传递。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。

60.没有返回值的return语句只能用在返回类型是void的函数中,可以用于提前结束程序的运行。返回void的函数,函数在执行到最后一句时会隐式地执行return语句。

61.C++11中可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。eg:

1
2
3
4
5
6
7
8
9
vector<string> process()
{
if (expected.empty())
return {};
else if (expected == actual)
return {"functionx", "okay"};
else
return {"abc", "edu", actual};
}

47.主函数main如果结尾没有返回值,当程序达到了main函数结尾处而且没有return语句,编译器将隐式地插入一条的返回0的return语句。为了使返回值与机器无关,cstdlib头文件中定义了两个预处理变量表达程序执行成功或者失败。eg:

1
2
3
4
5
6
7
int main()
{
if (true)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}

59.声明一个返回数组指针的函数,形式为: Type (*function(parameter_list))[dimesion]。其中Type是元素的类型,dimension表示数组的大小。eg:

1
int (*func(int i))[10];
  • func(int i) 表示调用func函数时需要一个int类型的实参。
  • (*func(int i)) 意味着我们可以对函数调用的结果执行解引用操作。
  • (*func(int i))[10] 表示解引用func的调用将得到一个大小是10的数组。
  • int (*func(int i))[10] 表示数组中的元素是int类型。

60.C++11可以通过尾置返回类型(trailing return type)来简化上述声明。声明方式为auto func(int i) -> int(*)[10];

61.编写一个函数声明,该函数返回数组的引用并且该数组包含10个string对象:

1
2
3
4
5
6
string (&func(string (&strarr)[10]))[10];
using ArrT = string[10];
ArrT func1(ArrT& arr);
auto func2(ArrT& arr) -> string(&)[10];
string arrS[10];
decltype(arrS)& func3(ArrT& arr);

62.C++中一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。通常应尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

55.标准库都在命名空间std下,例如vector,string,iterator

56.函数中不应该传递vector等容器类形参,可以通过向函数传递对应容器对象的迭代器来访问容器元素。

57.内联函数会在调用点处内联地展开,内联函数通常应该是比较短小的函数,最好定义在头文件中,方便修改和调用。eg

1
2
3
4
5
6
inline const string &shorterString(const string &s1, const string &s2)
{
return s1.size() < s2.size() ? s1:s2 << endl;;
}
cout << shorterString(s1, s2) << endl; //该调用在编译时将展开为
cout << (s1.size() < s2.size() ? s1: s2) << endl;

63.C++11中新增加了constexpr函数(constexpr function),是指能用于常量表达式的函数,定义constexpr函数的方法与其他函数类似,但要求函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句,eg:

1
2
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //foo是一个常量表达式,将可以做为数组大小。

为了能在编译过程中随时展开,constexpr函数被隐式指定为内联函数。

63.和其他函数一样,内联函数和constexpr函数可以在程序中定义多次,所以为了保证定义完全一致,内联函数和constexpr函数通常定义在头文件中。

C++调试帮助头文件cassert

64.C++中帮助调式的两个关键字assert(预处理宏)和NDEBUG(预处理变量):

  • assert定义在头文件cassert中,用法为assert(expr);,该语句会首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行,如果表达式为真(即非0),assert什么也不做。 assert宏常用于检查“不能发生”的条件。例如一个对输入文本操作的程序可能要求所有给定单词的长度都大于某个阈值,因此,可以这样定义assert(word.size() > threshold);

  • NDEBUG的作用是限制assert的行为,即assert的行为依赖于NDEBUG的预处理变量的状态,如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG,此时assert将执行运行时检查。可以使用一个#define 语句定义NDEBUG,从而关闭调试状态,如#define NDEBUG,很多编译器也提供一个命令行选项用于定义预处理变量:

    1
    CC -D NDEBUG main.c

    该命令等价于在main.c文件的开始写一行#define NDEBUG

65.除了将NDEBUG用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,则执行#ifndef和#endif之间的代码,如果定义了NDEBUG,这些代码将被忽略掉。eg:

1
2
3
4
5
6
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
cerr << __func__ << ": array size is " << size << endl; //__func__是编译器定义的一个局部静态变量,用于存放函数的名字
#endif
}

67.编译器定义的可用于调试的名字有:

  • __func__ 存放当前调试的函数的名字,它是const char的一个静态数组。
  • __FILE__ 存放文件名的字符串字面值。
  • __LINE__ 存放当前行号的整型字面值。
  • __TIME__ 存放文件编译时间的字符串字面值。
  • __DATE__ 存放文件编译日期的字符串字面值。

68.调试举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <cassert>
#define NDEBUG
using namespace std;
void print(vector<int> &vec)
{
#ifndef NDEBUG
cout << "vector size: " << vec.size() << endl;
#endif
if (!vec.empty()) {
int temp = vec.back();
vec.pop_back();
print(vec);
cout << temp << endl;
}
}
int main()
{
vector<int> veci;
for (vector<int>::size_type i = 0; i < 10; ++i) {
veci.push_back(i + 1);
}
print(veci);
return 0;
}

70.实参赋给形参时有可能会出现类型转换。

64.出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称其为重载函数。局部声明的函数将屏蔽全局声明的函数而不是重载,重载函数必须作用域相同。调用重载函数时尽量避免使用强制类型转换,如果一定要使用强制类型转换,则说明重载函数设计不合理。

函数指针

65.指向函数的指针定义: bool (pf) (const string &, const string &); //定义了一个指向含有两个const string &类型形参,返回bool类型的函数指针pf。使用typedef可以简化定义,typedef bool (tdpf) (const string &, const string &);

66.函数指针只能通过同类型的函数或函数指针或0值常量表达式进行初始化或赋值。函数指针初始化时,直接引用函数名等效于在函数名上应用取地址操作符:tdpf pf1 = getnum; <=> tdpf pf1 = &getnum;

67.通过函数指针调用函数时不需要对其解引用。eg: pf(“Hello”, “world”);

68.当指向函数的指针作为形参时可能使用如下两种方式编写:

1
2
void useBigger(const string &, const string &, bool (const string &, const string &));
void useBigger(const string &, const string &, bool (*)(const string &, const string &)); //就像普通类型的定义类似,不需要指名函数名。

69.返回指向函数的指针,可能从函数名开始由里向外分析。eg:

1
2
3
int  (*ff(int))(int *, int);  //它表示ff声明为一个函数,它带有一个int型的形参,并返回`int (*)(int *, int);`,它是一个指向函数的指针,该函数返回int型并带有两个分别为`int *`和`int`的形参。其等价的typedef定义为:
typedef int (*PF)(int *, int);
PF ff(int);

70.函数无法返回函数,只能返回指向函数的指针。而当形参为指向函数的指针时,具有函数类型的对应的实参会被自动转换为指向相应函数类型的指针。eg:

1
2
3
4
typedef int func(int *, int);  //func为函数类型
void f1(func); //OK,f1含有一个函数类型的形参
func f2(int); //ERROR,f2返回的是个函数
func *f3(int); //OK,f3返回类型为指向函数的指针。

71.C++允许使用函数指针指向重载的函数。定义该指针时其类型必须与重载函数精确匹配。

1.类的声明和定义可以在一起,即声明一个类类型时,同时定义它的成员。也可以暂时只声明而不定义。eg: class Sales_data;。这种声明被称作前面声明(forward declaration),在它声明之后定义之前是一个不完全类型(incomplete type)。不完全类型只能在非常有限的情况下使用:可以定义指向这种类型的指针或引用,也可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数。

58.类的所有成员必须在定义类的花括号中声明,成员函数的定义可以放在类的定义内定义,也可以放在类的定义外定义。放在类的定义内定义的成员函数会被编译器隐式地当作内联(inline)函数。当成员函数在类外进行定义时,记得命名空间,在类外定义时也可以使用inline进行修饰,通常应该加上inline以便于阅读。

1
double Sales_item::avg_price() const {}

59.每个类成员函数都隐含的包含一个形参this,在调用成员函数时,形参this初始化为调用函数的对象的地址。在成员函数中,不必显示的使用this指针来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀的引用,都被假定为通过指针this实现的引用,此规则只是针对对象的成员函数的调用,在定义成员函数时必须使用this。eg:

1
2
3
4
inline void Sales_item::add_price(double price)
{
this->price = price; //其中this->price为类成员属性price,必须使用this->price
}

60.如果在成员函数名后面,花括号前面加上修鉓符const,则this将成为指向调用对象的const的指针,该成员函数称为常量成员函数。可以有效保护对象不被成员函数修改。eg:

1
2
3
//该函数将无法修改this.isbn及this所指对象的常量成员。
bool same_isbn(const Sales_item &rhs) const
{return isbn == rhs.isbn;} //等价于{this->isbn == rhs.isbn;}

61.函数可以直接返回整个对象,或者返回一个对象的引用,也可以返回this对象。 eg:

1
2
3
4
5
Sales_item& Sales_item::combine(const Sales_item &rhs)
{
//combine this and rhs
return *this; //
}

62.IO类属于不能被拷贝的类型,所以函数在传递IO类时需要以引用的方式传递。

构造函数

61.构造函数与类同名,且没有返回类型。每个类可以有多个构造函数,每个构造函数的形参表必须不同。构造函数应该放在public部分,用于初始化对象。

62.当类中没有构造函数时,编译器会自动生成默认构造函数。但不能依赖于默认构造函数,通常应该为类定义默认构造函数,原因为:

  • 当我们定义了其他构造函数时,编译器将不会再生成默认构造函数,除非再定义一个默认构造函数,否则类将没有默认构造函数。
  • 如果类包含内置类型或者复合类型的成员,只有当这些成员都有类内初始值时,这个类才能合适地使用默认构造函数。
  • 有些情况下编译器也无法为某些类合成默认构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数时。

63.C++11中允许使用 = default的方式来要求编译器生成默认构造函数。eg: Sales_data() = default;

62.构造函数的定义:”类名(形参表):构造函数的初始化列表 {函数体}”eg:

1
Sales_data(): units_sold(0), revenue(0.0) {}  //成员名后面的圆括号中为成员初值

构造函数也可以只在类内声明,然后在类外面进行定义,在类外面定义时,必须通过类名指定该构造函数属于哪个类。
构造函数的初始化列表可以为空,函数体也可以为空。

63.类的定义为 class 类名 {}; 记得后面要有个分号。

63.类内的变量的初始值不能使用圆括号初始化。

5.struct和class的唯一区别是默认访问级别,struct默认访问级别为public,class默认访问级别是private。

6.类中public部分定义的成员在程序的任何部分都可以访问,通常在public部分放置操作,以便在程序的其他部分可以执行这些操作。类中private部分定义的成员只能被作为类的组成部分的代码(以及该类的友元)访问。通常在private部分放置数据,以对对象的内部数据进行隐藏。

友元

7.类可以通过友元来允许其他类或者函数访问它的非公有成员,方法是在类内部使用关键字friend声明相应的函数。eg:

1
friend std::istream &read(std::istream&, Sales_data&);

友元的声明只能出现在类定义的内部,但在类内出现的位置不限,友元不是类的成员,所以也不受它所在区域访问控制级别的约束。一般,最好在类定义开始或结束前的位置集中声明友元。

8.由于友元的声明仅仅指定了访问的权限,并非一个通常意义上的函数声明。所以如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。

9.可以通过在类中定义某种类型的别名来隐藏类的具体实现,在类中定义的类型名字同样存在访问限制,可以是public或者private中的一种。eg:

1
2
3
4
5
6
7
8
class Screen {
public:
typedef std::string::size_type pos; //等价于using pos = std::string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
}

10.类的成员函数也可以被重载。

11.可以通过关键字mutable将类的数据成员声明为可变数据成员(mutable data member),可变数据成员永远不会是const,即使它是const对象的成员,const成员函数依然可以改变它的值。eg:

1
2
3
4
5
6
7
8
9
10
class Screen {
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const
{
++access_ctr;
}

8.类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式。

9.成员函数返回*this时,如果该函数需要修改对象的成员,应让其返回对象的引用,这样即可以避免只能改变对象的临时副本,又能避免数据拷贝。

10.一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

11.即使两个类的成员列表完全一致,它们也是不同的类型。

12.默认初始化类的方式:

1
2
3
以下两个声明等价
Sales_data item1;
class Sales_data item1; //C语言中通常使用这种方式

13.类可以把其他的类定义成友元,也可以把其他类的成员函数定义成友元,友元函数能定义在类的内部,这样的函数是隐式内联的。友元关系不存在传递性,每个类负责控制自己的友元类或友元函数。eg:

1
2
3
4
5
6
7
8
9
10
11
//A是B的友元类,所以A的成员函数将可以访问B中包括非公有成员在内的所有成员。  
class A {
void do(B&) {}
}
class B {
friend class A;
//指定类A中的某个成员函数成为B的友元。
free void A::do(B&);
}

A::do(B& b) {return *this}

要想令某个成员函数作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系,必须按照如下方式设计程序:

  • 首先定义A类,其中声明do函数,但此时不能定义,在do使用B的成员之前必须先声明B。
  • 接下来定义B,包括对于do的友元声明。
  • 最后定义do函数的实际内容,此时它才可以使用B的成员。

14.类和非成员函数的声明不是必须在它们的友元声明之前,此时隐匿地假定该名字在当前作用域是可见的。即使在类的内部定义友元时定义该函数,也必须在类的外部提供相应的声明从而使得函数可见。eg:

1
2
3
4
5
6
7
8
9
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的声明在作用域中了

14.一旦遇到类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体,也就是此时可以直接使用类的成员,而无需再次指明类名。但类名之前的成员必须通过::来指明其所属类(例如成员函数的返回类型),包括在类中定义的类型别名。

15.编译器在处理类的定义时分两步:首先编译成员的声明,然后直到类全部可见后才编译函数体。所以定义在类内的成员函数中可以使用在其后定义的成员。而声明中使用的名字,包括返回类型及参数列表则必须在使用前确保可见。

16.通常内层作用域中可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过,然后在类中,如果成员使用外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。

17.三种情况下类成员必须在默认构造函数中显示初始化:成员是const或者是引用,或者当成员属于某种类类型且该类型没有定义默认构造函数时。

18.由于构造函数初始化列表的顺序与成员声明的顺序一致,所有最好令构造函数初始值的顺序与成员声明的顺序保持一致。且如果有可能,应尽量避免使用某些成员初始化其他成员,这样就可以不必考虑成员的初始化顺序。

19.如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

20.C++11中新增加了委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些职责委托给其他构造函数。委托构造函数的声明也有一个成员初始值的列表和一个函数体,其成员初始值列表只有一个唯一的入口,就是类名本身,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中的另一个构造函数匹配。eg:

1
2
3
4
5
6
7
8
9
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); }
};

21.如果构造函数只接受一个实参,则它实际上定义了将该实参转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数,即这种构造函数的参数可以初始隐式的转换为该类的类类型。例如当类A中的某个构造函数只含有一个string类型的实参作为其参数列表时,当调用函数中的参数需要使用A时,则可以使用string直接替代,此时编译器会用给定的string自动创建一个A对象,并将这个(临时)的A对象传递给调用函数,这里的调用函数的参数应该是一个常量引用。编译器只会执行一步类型转换,也就是说当你使用一个字符串字面值直接给这个调用函数时,将无法编译通过,因为它不能被自动转换为string然后再转换为A对象,必须强制将其先转换为string。

22.可以通过将构造函数声明为explicit以阻止构造函数的隐式转换,这样构造函数A将不能再成为转换构造函数。eg:

1
2
3
4
class A {
public:
explicit A(const std::string &s): bookNo(s) {}
}

当构造函数被声明为explicit时,它将只能以直接初始化的形式使用(即圆括号),而不能使用赋值初始化(即通过=号进行的拷贝初始化)

13.类类型当然也支持显式的强制类型转换,例如:

1
2
3
4
5
6
7
8
9
class A {
std::string bookNo;
public:
A(std::string s): bookNo(s) {}
void combin(A) {}
}
A itemA;
itemA.combin(A("Hello"));
itemA.combin(static_cast<A>("Hello"));

19.聚合类使得用户可以直接访问其成员,并且具体特殊的初始化语法形式。当满足以下条件时,才是聚合类:

  • 所有成员都是public的
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有virtual函数。
    这种类在初始化时使用花括号括起来的成员初始值列表进行初始化,初始值的顺序必须与声明的顺序一致。
    eg:
    1
    2
    3
    4
    5
    struct Data {
    int ival;
    string s;
    };
    Data val = {0, "test"};

21.数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合以下要求,也可以是一个字面值常量类:

  • 数据成员都必须是字面值类型
  • 类必须至少含有一个constexpr构造函数
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

22.通过static声明类的静态成员(可以是数据或者函数),类的静态成员直接与类本身关联,而不是与类的各个对象保持关联。静态成员也可以是public或private,静态数据成员的类型可以是常量、引用、指针、类类型等。类的静态成员被该类的所有对象所共享。静态成员函数不能声明成const的,而且也不能在static函数体内使用this指针。
由于非静态成员需要实例化后才会分配内存,而静态成员属于类,一直存在于内存中,所以静态成员不能访问非静态成员。但非静态成员可以访问类的静态成员

21.静态成员函数即可以定义在类的内部也可以定义在类的外部,但定义在类的外部时不能重复static关键字,static关键字只出现在类内部的声明语句中。

23.使用静态成员的方式:

  • 直接使用作用域运算符通过类名来访问静态成员,eg:double r = Account::rate();
  • 使用类的对象、引用或指针来访问静态成员,eg:
    1
    2
    3
    4
    5
    Account ac1;
    Account *ac2 = &ac1;
    //以下两种方式等价
    double r = ac1.rate();
    double ac2->rate();

标准IO库

  • iostream 对流进行读写 从istream和ostream派生而来 含istream和ostream
  • fstream 读写文件,由iostream派生而来 含ifstream和ofstream
  • sstream 对string对象进行读写,由iostream派生而来 含istringstream和ostringstream

73.标准IO为了对国际字符的支持,有一组带”w”前缀的IO库。如wostream、wistream、wiostream等对应的char类型为wchar_t类型。对应的标准输入输出对象为wcin、wcout,还有wcerr.

74.IO对象不可复制或复制,而只有支持复制的元素类型可以存储在容器类型中,所以IO对象不支持容器。形参或返回类型也不能为流类型,如果需要传递或者返回IO对象,则必须传递或返回指向该对象的指针或引用。

75.标准IO库的条件状态标志,适用于普通流、文件流以及string流。注意,这些只是状态标准,例如s.clear()只是将流的标志重置为有效状态,但并不会清空流的缓冲区。清空缓冲区需要使用cin.sync()。

failbit通常是可以修正,例如需要int却输入了字符串则为s.fail()返回true,此时可能通过s.clear()进行重置流状态为有效。而badbit是系统级的故障,出现这类错误时的流将无法再使用。

76.输出缓冲区的管理。每个IO对象管理一个缓冲区用于存储程序读写的数据。输出缓冲区的内容被刷新的方式有:
1).程序正常结束。作为main返回工作的一部分,将清空所有输出缓冲区。
2).在一些不确定的时候,缓冲区已经满了,此时缓冲区将会在下一个值写入之前刷新。
3).用操纵符显式地刷新缓冲区,例如结束符endl用于输出一个换行符并刷新缓冲区,flush用于刷新流但不在输出中添加任何字符,ends在缓冲区中插入字符null然后再刷新它。
4).在每次输出操作执行完成后,用unitbuf操纵符设置流的内部状态,从而清空缓冲区。
如果需要刷新所有输出,最好使用unitbuf,它每次执行完写操作后都刷新流:

1
cout << unitbuf << "first" << " second" << nounitbuf;

等价于:

1
cout << "first" << flush << " second" << flush;

nounitbuf 操纵符将流恢复为使用正常的、由系统管理的缓冲区刷新方式。
5).可将输出流与输入流关联(使用tie函数)起来,这样,在读输入流时将刷新其关联的输出缓冲区。
当输入流与输出流绑在一起时,任何读输入流的尝试都将首先刷新其输出流关联的缓冲区。标准库将cout与cin绑在一起。eg:
cin.tie(&cout);

77.程序在崩溃时不会自动刷新输出缓冲区

78.从标准输入一直读取直到遇到文件结束符,不管读到时是否发生了错误都进入循环,可有效检测错误原因,记得使用逗号。eg:

1
2
3
4
5
6
7
8
9
10
11
12
while (cin >> ival, !cin.eof())
{
if (cin.bad()) //检测流是否为不可恢复的的badbit
throw runtime_error("IO stream corrupted");
if (cin.fail()) //检测流是否为还可以再使用的failbit错误,如果是则重置failbit标志并重新使用该流
{
cerr << "bad data, try again";
cin.clear(istream::failbit); //重置流的状态标志,但不会清空输入缓冲区。
cin.sync(); //清空输入缓冲区中的所有内容。
continue;
}
}

79.当需要处理多种状态时可以使用按位或操作符在一次调用中生成“两个或更多状态位”。eg:

1
is.setstate(ifstream::badbit | ifstream::failbit);  //同时设置badbit和failbit位为真

80.cin.clear()重置cin的状态标志为有效,cin.sync()清空输入缓冲区中的内容,cin.ignor()可用于清空输入流中的部分内容。

文件的输入和输出

fstream头文件定义了三种支持文件IO的类型:

  • ifstream,由istream派生,提供读文件的功能。
  • ofstream,由ostream派生,提供写文件的功能。
  • fstream,由iostream派生,提供读写同一个文件的功能。
    eg:
    1
    2
    ifstream infile(ifile.c_str());  //定义并初始化输入文件流infile,并将其与将要读取的文件ifile绑定。 <=>`ifstream infile; infile.open("in");`//假设文件名为in并在当前文件夹
    ofstream outfile(ofile.c_str()); //定义并初始化输出文件流outfile,并将其与将要写入的文件ofile绑定。 <=>`ofstream outfile; outfile.open("out");`//假设文件名为out并在当前文件夹

82.检查文件是否已成功打开if (!infile)。如果要将文件流与新文件重新捆绑必须先将其关闭infile.close();

83.关于文件的读写实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
using namespace std;
int main(void)
{
// string file = "out.txt";
// cin >> file;
// ofstream outfile(file.c_str()); //定义并打开一个用于写入的文件流outfile,记得转成c风格字符串
// cin.clear(); //重置输入流标志防止cin有可能出现的错误
// cin.sync(); //清空输入流缓冲区的内容
// if (!outfile) //判断写文件流是否有效
// {
// cout << "Error to open the file " << file << endl;
// }
// else
// {
// cout << "Enter some words:" << endl;
// string str;
// getline(cin, str); //一次读到一行
// outfile << str; //将str写到文件输出流outfile中
// }
// outfile.close(); //关闭文件流
string file = "sales_item.h";
ifstream infile(file.c_str()); //定义并初始化文件输入流
if (!infile)
{
cout << "Error to open infile " << file << endl;
}
else
{
cout << file << ": " << endl;
// string str;
// getline(infile, str);
// cout << str << endl;
string str;
vector<string> sales;
while (!infile.eof())
{
getline(infile, str);
sales.push_back(str);
}
for (vector<string>::const_iterator ite = sales.begin(); ite != sales.end(); ++ite)
{
cout << *ite << endl;
}
}
infile.close();
return 0;
}

84.文件的打开模式,注意这些模式指的是文件的模式而不是流的模式。
其中out、trunc和app模式只能用于ofstream或者fstream,in模式只能用于ifstream或fstream。所有的文件都可以使用ate或binary模式。
eg:

1
ofstream outfile("filename", ofstream::out | ofstream::trunc);  //使用按位或操作符同时以out和trunc模式打开文件。

默认时ifstream流对象关联的文件将以in模式打开,ofstream默认以out模式打开。以out模式打开的文件内容会被清空,效果上等同于同时指定了out和trunc模式。

85.fstream既可以读也可以写它所关联的文件,如何处理它的文件取决于它打开的模式。eg:
fstream inout(“filename”, fstream::in | fstream::out); //同时以输入和输出模式打开文件

86.打开模式的有效组合:

87.打开文件前若未知原文件流的状态,一定记得首先使用in.close();in.clear();重置原来的流。

字符串流

88.字符串流的操作包含在头文件sstream头文件中。类似于文件流它包含对应的三种类型:istringstream、ostringstream和stringstream。像fstream一样,iostream的操作也都适用于sstream相关对象。

89.stringstream的特定操作strm.str();strm.str(s);

90.字符串流的用处:
1)可以同时对一行以及一行中的单词进行处理。eg:

1
2
3
4
5
6
7
8
9
string line, word;
while (getline(cin, line))
{
istringstream stream(line); //定义并初始化stringstream对象
while (stream >> word) //从一行中读取单词
{
processing;
}
}

2)stringstream可用于转换或格式化字符串。eg:
将数字转化为字符串:
int val1 = 512, val2 = 1024;
ostringstream format_message;
format_message << “val1: “ << val1 << “\n” << “val2: “ << val2 << “\n”; //format_message内容将为val1: 512\nval2: 1024

将字符串转化为数字:
istringstream input_istring(format_message.str());
string dump;
input_istring >> dump >> val1 >> dum >> val2; //非数字将被输入到dump而数字512和1024将分别被输入val1和val2。程序会自动忽略换行符。

顺序容器

元素在顺序容器中的顺序与其加入容器时的位置相对应。
关联容器中元素的位置由元素相关联的关键字值决定。
1.顺序容器类型:

  • vector 相当于可变大小的数组。支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢。
  • deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。
  • list 双向链表。只支持双向顺序访问。在list中任何位置进行插入或删除操作速度都很快。
  • forward_list(C++11) 单向链表。只支持单向顺序访问。在链表任何位置进行插入或删除都很快。
  • array(C++11) 固定大小的数组。支持快速随机访问,不能添加或删除元素。
  • string 与vector相似的容器,但专门用于保存字符。随机访问快,在尾部插入或删除速度快。
    除了固定大小的array外,其他容器都提供高效、灵活的内存管理。
    string和vector的元素保存的连续的内存空间中,所以对其使用下标访问元素速度依然很快,但在中间位置插入元素由于需要移动大量元素,所以会非常慢。
    list和forward_list可以方便地在任何位置添加或删除元素,速度都会很快,但不支持随机访问。而且与vector、deque和array相比,这两个容器的额外内存开销也很大。
    deque是一个更为复杂的数据结构,与string和vector类似,其支持随机访问,同样在中间位置添加或删除元素代价可能会很高。但在deque的两端添加或删除元素都很快,可以与list或forward_list添加删除元素的速度相当。
    与内置的数组相比,C++11新增加的array是一种更安全、更容易使用的数组类型,当然其大小也是固定的。C++11中的forward_list设计的目标是达到与最好的手写单向链表数据结构相当的性能,所以forward_list没有size操作。

2.通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。

3.每个容器都定义在其相应的头文件中,容器均定义为模板类。

4.可以定义元素类型为容器的容器,如vector<vector<string>> lines,C++11之前的版本需要在后面两个尖括号之前加一个空格。

5.每种容器都支持迭代器及对应的操作,其他forward_list的迭代器不支持--运算符。

6.容器的迭代器所描述的元素范围称为左闭合区间[begin, end),因为end指向的是容器最后一个元素的下一个位置。

7.在不需要修改容器元素的情况下记得使用const_iterator迭代器。

8.每个容器的迭代器也都有其对应的反向迭代器reverse_iterator,反向迭代器++会获取其前一个元素。

9.容器的索引类型为container::size_type,例如int的vector的索引类型为vector<int>::size_type

10.beginend返回的迭代器有多个版本,以r开头的版本返回反向迭代器,以c开头的版本返回const迭代器。例如:

1
2
3
4
5
list<string> a;
auto it1 = a.begin(); //list<string>::iterator
auto it2 = a.rbegin(); //list<string>::reverse_iterator
auto it3 = a.cbegin(); //list<string>::const_iterator
auto it4 = a.crbegin(); //list<string>::const_reverse_iterator

所以当不需要写访问时,应使用cbegin和cend

11.除了array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且顺序容器都可以接受指定容器大小和元素初始值的参数。使用方法与vector一样。

12.与内置数组一样,标准库array的大小也是类型的一部分,当定义一个array时,除了指定元素类型,还要指定容器大小:

1
2
array<int, 42> a1; //定义一个保存42个int的数组
array<int, 10>::size_type i; //定义类型为array<int, 10>::size_type的数组下标

13.array支持拷贝或对象赋值操作,由于类型必须一样(元素类型和大小都是类型的一部分):

1
2
array<int, 10> digits = {0, 1, 2, 3, 4, 5};
array<int, 10> copy = digits;

14.vector的6种创建和初始化方式及初始值:

1
2
3
4
5
6
vector<int> vec;    // 0
vector<int> vec(10); // 0
vector<int> vec(10, 1); // 1
vector<int> vec{ 1, 2, 3, 4, 5 }; // 1, 2, 3, 4, 5
vector<int> vec(other_vec); // same as other_vec
vector<int> vec(other_vec.begin(), other_vec.end()); // same as other_vec

15.用于容器的swap和assign函数:

1
2
3
4
5
6
swap(c1, c2); //交换c1和c2中的元素。c1和c2必须具有相同的类型
c1.swap(c2); //swap通常比从c2向c1拷贝元素快得多
//assign操作不适用于关联容器和array
seq.assign(b, e); //将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素
seq.assign(il); //将seq中的元素替换为初始化列表il中的元素
seq.assign(n, t); //将seq中的元素替换为n个值为t的元素

16.可变大小的容器都有三个与大小相关的操作:size()返回容器中的元素数目;empty()判断容器是否为空;max_size()返回一个大于或等于该类型容器所能容纳的最大元素数的值。

17.每个容器都支持相等运算符(==和!=),除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。

18.顺序容器都有其相应的添加和删除元素的操作(push_back、insert、emplace等),其中emplace系列的成员函数是C++11中引入的,它们执行的操作是构造而不是拷贝元素,所以速度会快一些,因为拷贝元素时会创建一个临时变量。

19.支持下标访问的容器有string、vector、deque、array,它们同时支持通过at来较安全的访问。下标越界的结果是未定义的,而at访问越界时会抛出out_of_range异常,用法为c.at(n)

20.访问元素的成员函数(即front、back、下标和at)返回的都是引用,即可以作为左值使用,如c.front() = 42;

21.容器操作可能会使迭代器失效,特别是在改变了容器的大小时要特别留意是否在改变容器之后依然使用了改变之前的迭代器。

22.容器大小管理操作:

  • shrink_to_fit 只适用于vector、string和deque。
  • capacity和reserve只适用于vector和string。
  • c.shrink_to_fit() 将capacity()减少为与size()相同大小
  • c.reserve(n) 分配至少能容纳n个元素的内存空间
    reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。
    capacity和size:容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。

22.string还支持另外三个构造函数:

1
2
3
4
//n、len2、pos2都是无符号值
string s(cp, n); //s是cp指向的数组中前n个字符的拷贝,此数组至少应该包含n个字符。
string s(s2, pos2); //s是string s2从下标pos2开始的字符的拷贝,若pos2>s2.size(),构造函数的行为未定义
string s(s2, pos2, len2); //s是string s2从下标pos2开始len2个字符的拷贝。若pos2>s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size()-pos2个字符

22.string有着丰富的成员函数,可以支持修改、查找、搜索、比较、数值转换等功能。

容器适配器

1.标准库中有三个顺序容器适配器:stack、queue和priority_queue,stack在stack头文件中,其他两个都在queue头文件中。容器、迭代器和函数都有适配器,容器适配器即一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。所有容器适配器都支持empty、size和swap函数。

2.每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。eg:

1
2
3
4
5
6
deque<int> deq;
stack<int> stk(deq); //从deq拷贝元素到stk,假设deq中已经有初始值
//在vector上实现的string类型的空栈
stack<string, vector<string>> str_stk;
//str_stk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);

默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。

3.所有适配器都要求容器具有添加、删除元素的能力,每一种容器适配器都有其对应的构造容器的限制。

4.栈适配器stack在标准库头文件stack中,stack默认基于deque实现,也可以在list或者vector之上实现,其支持的其他特有操作:

  • s.pop() 删除栈顶元素,但不返回该元素值
  • s.push(item) 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来
  • s.emplace(args) 同上,只是其由args构造
  • s.top() 返回栈顶元素,但不将元素弹出栈
    虽然适配器基于容器,但不能直接使用容器的操作。

5.队列适配器queue和priority_queue定义在queue头文件中,其中queue默认基于deque实现,priority_queue默认基于vector实现,queue也可以用list或vector实现,priority_queue也可以用deque实现。其支持的其他操作:

  • q.pop()
  • q.front()
  • q.back()
  • q.top()
  • q.push(item)
  • q.emplace(args)
    标准库queue使用一种先进先出(first-in,first-out,FIFO)的存储和访问策略。

泛型算法

由于标准库容器定义的操作集合很小,所以标准库为这些容器定义了一组通用(generic,或称泛型的)算法:它们可以用于不同类型的容器和不同类型的元素。通常这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。
1.大多数算法都定义在头文件algorithm中,头文件numeric中定义了一组数值泛型算法。
algorithm所有算法: http://www.cplusplus.com/reference/algorithm/

2.泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。所以算法永远不会改变底层容器的大小。算法可能改变容器中保存元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。算法可以通过操作标准库定义的一类特殊的迭代器——插入器(inserter)来完成容器添加元素的效果。

3.除了少数例外,标准库算法都对一个范围内的元素进行操作,我们将此元素范围称为“输入范围”,接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。

4.那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。

5.向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。注意vector等容器默认声明时如果不指定大小,是不分配初始空间大小的。

6.插入迭代器可以通过容器算法向容器中赋值,back_inserter是一个定义在头文件iterator中的函数,可以用其生成插入迭代器。back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。eg:

1
2
3
4
5
vector<int> vec;
auto it = back_inserter(vec);
*it = 42; //向vec中添加一个元素,值为42
fill_n(vec, 10, 0); //错误,fill_n无法向不存在的容器空间中写入数据。
fill_n(back_inserter(vec), 10, 0); //添加10个值为0的元素到vec中

7.拷贝算法copy接受三个迭代器,前两个指定输入范围,第三个表示目的序列的起始位置,返回目的位置迭代器(递增后)的值。

8.replace算法可以将给定范围内的某个值替换为另一个值。

9.重排容器元素的算法:sort()接受两个迭代器指定范围按字典序排序;unique()接受两个迭代器指定范围,将输入范围中重复值放在序列的后面,并返回指向不重复区域之后一个位置的迭代器;

10.sort函数的默认排序是通过<进行比较,可能通过向其传递第三个参数来指定比较方式,函数会对其每一个参数调用第三个参数代指的函数,通常第三个参数被称为谓词,谓词分为一元谓词(unary predicate)和二元谓词(binary predicate)。stable_sort可以维持相等元素的原有顺序。eg:

1
2
3
4
5
6
7
bool isShorter(const std::string &a, const std::string &b)
{
return a.size() < b.size();
}

//调用方法
stable_sort(words.begin(), words.end(), isShorter);

lambda表达式

C++11新增的lambda表达式相当于未命名的内联函数,可以定义在函数内部。表达式形式如下:

1
[capture list] (parameter list) -> return type {function body};

其中capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和function body分别表示返回类型、参数列表和函数体,lambda必须使用尾置返回来指定返回类型。可以忽略参数列表和返回类型,但必须包含捕获列表和函数体。

1.函数名加小括号即表示调用运算符,如f()

2.使用lambda表达式的例子:

1
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) {return a.size() < b.size();});

实现上例的功能,空捕获列表表示此lambda不使用它所在函数中的任何局部变量。一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。但lambda可以直接使用局部static变量和在它所有函数之外声明的名字。

3.使用for_each函数配合lambda表达式输出容器中的元素,每个元素后面接一个空格。eg:

1
for_each(words.begin(), words.end(), [](const string &s){cout << s << " ";});

4.当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。

5.与参数不同,lambda表达式中被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。

6.可以通过给lambda的捕获列表传递=或&告诉编译器隐匿地采用值捕获方式或引用捕获方式,当然也支持混合使用显示捕获与隐式捕获。eg:

1
for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c;});

当混合使用隐式捕获与显示捕获时,捕获列表的第一个元素必须是一个&或=,此符号即指定了默认捕获方式为引用或值,且显示捕获的变量必须采用与隐式捕获不同的方式。

7.默认情况下,对于一个值被拷贝的局部变量,传递给lambda时为只读变量,lambda不会改变其值。可能通过在参数列表首加上关键字mutable来使用lambda可以改变局部变量的值。eg:

1
2
3
4
5
6
7
8
void fcn3()
{
size_t v1 = 42;
//如果没有mutable,编译将报错,因为v1是只读的
auto f = [v1]() mutable {return ++v1;};
v1 = 0;
auto j = f(); //j为43
}

8.默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。

9.当需要手动指定lambda的返回类型时,需要使用尾置返回类型:

1
2
//transform算法可以遍历修改序列中的值
transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int {if (i < 0) return -i; else return i;});

10.标准库头文件functional中的bind函数可以将函数生成为一个新的可调用对象来“适应”原对象的参数列表。

iterator

11.标准库在头文件iterator中还定义了以下几种迭代器:

  • 插入迭代器(insert iterator):用于与一个容器绑定并向容器中插入元素。
    • back_inserter
    • front_inserter
    • inserter
  • 流迭代器(stream iterator):这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。
    • istream_iterator 读取输入流。
    • ostream_iterator 向一个流写入数据。
  • 反向迭代器(reverse iterator):即在容器中从尾元素向首元素反向移动的迭代器。
  • 移动迭代器(move iterator):这种迭代器不拷贝其中的元素,而是移动它们。

1.插入迭代器用法:std::unique_copy(lstr.begin(), lstr.end(), back_inserter(l_unique));

2.istream_iterator用法:

1
2
3
4
5
6
7
8
9
10
istream_iterator<T> in(is);  //in从流is读取类型为T的值
istream_iterator<T> end; //读取类型为T的值的istream_iterator迭代器,表示尾后位置。
//下面从标准输入读取数据存入vector
istream_iterator<int> in_ter(cin); //从cin读取int
istream_iterator<int> eof; //istream尾后迭代器
while (in_ter != eof) //当有数据可供读取时
vec.push_back(*in_ter++);
//等价于
istream_iterator<int> in_ter(cin), eof; //从cin读取int
vector<int> vec(in_ter, eof); //从迭代器范围构造vec

2.ostream_iterator用法:

1
2
3
4
5
6
7
8
9
10
ostream_iterator<T> out(os);  //out将类型为T的值写到输出流os中
ostream_iterator<T> out(os, d); //out将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符结尾的字符数组,例如字符串字面值。
//以下利用ostream_iterator输出vector的元素
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
*out_iter++ = e; //等价于out_iter = e;
cout << endl;
//以上等价于使用copy的如下实现
copy(vec.begin(), vec.end(), out_iter);
cout << endl;

3.iostream的迭代器举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//实现功能为使用istream_iterator从文件读取string到vector中,然后使用ostream_iterator将vector中的元素输出到标准输出cout中
#include <iostream>
#include <iterator>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
int main()
{
//定义文件流
std::ifstream in("test.cpp");
//使用文件流创建istream_iterator及文件结尾eof
std::istream_iterator<std::string> in_ter(in), eof;
//使用文件迭代器创建一个存放string的vector
std::vector<std::string> vecstr(in_ter, eof);
//创建标准输出迭代器
std::ostream_iterator<std::string> out(std::cout, " ");
//使用copy函数将vector中的内容拷贝的含有标准输出的迭代器
std::copy(vecstr.begin(), vecstr.end(), out);
std::cout << std::endl;
return 0;
}

5.除了forward_list之外,其他容器都支持反向迭代器。

6.每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。按算法要求可分为以下5类迭代器:

  • 输入迭代器 只读,不写;单遍扫描,只能递增;find和accumulate要求输入迭代器,istream_iterator是一种输入迭代器。
  • 输出迭代器 只写,不读;单遍扫描,只能递增;ostream_iterator类型是输出迭代器
  • 前向迭代器 可读写;多遍扫描,只能递增;forward_list上的迭代器是前向迭代器
  • 双向迭代器 可读写;多遍扫描,可递增递减;除了forward_list之外,其他标准库都提供双向迭代器
  • 随机访问迭代器 可读写;多遍扫描,支持全部迭代器运算;array、deque、string和vector的迭代器都是随机访问迭代器

7.算法命名规范:

  • _if版本的算法接受一个谓词代替元素值,适用于lambda表达式。eg:
    1
    2
    find(beg, end, val);  //查找输入范围中val第一次出现的位置
    find_if(beg, end, pred); //查找第一个令pred为真的元素
  • _copy版本的算法会将元素拷贝到目的位置eg:
    1
    2
    reverse(beg, end);  //反转元素
    reverse_copy(beg, end, dest); //反转元素并拷贝到dest

8.list和forward_list定义了它们自己的sort, merge, remove, reverse和unique算法。

关联容器

1.关联容器支持高效的关键字查找和访问。两个主要的关联容器类型是map和set。map使用关键字进行索引,set中每个元素只包含一个关键字,它支持高效的关键字查询操作——检查一个给定的关键字是否在set中。

2.对于关联容器来说,第一个元素就相当于数组下标,第二个元素是对应下标下存储的值。

2.标准库中一共定义了8个关联容器其中map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。

  • 按关键字有序保存元素的容器:
    • map 关联数组;保存关键字-值对
    • set 关键字即值,即只保存关键字的容器
    • multimap 关键字可重复出现的map
    • multiset 关键字可重复出现的set
  • 无序集合:
    • unordered_map 用哈希函数组织的map
    • unordered_set 用哈希函数组织的set
    • unordered_multimap 用哈希函数组织的map;关键字可以重复出现
    • unordered_multiset 用哈希函数组织的set;关键字可以重复出现

3.关联容器使用举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* testmap.cpp
* 从test.cpp文件中读取单词,并统计单词出现的次数,不统计排除列表中的单词,能够将大小写单词,以及结尾有符号的单词识别为同一个单词,例如"Expam","expam.","expam,"为同一个单词
* Copyright (C) 2015 <@BLUEYI-PC>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
#include <string>
#include <map>
#include <set>
#include <iterator>
#include <fstream>
#include <algorithm>
#include <cctype>
int main()
{
//声明文件流并初始化
std::ifstream in("test.cpp");
//使用文件流初始化流迭代器
std::istream_iterator<std::string> in_ite(in), eof;
//声明map,用于存放单词与其个数
std::map<std::string, size_t> word_count;
//声明set,存放排除列表
std::set<std::string> exclude = {"{", "}", "the", "and"};
std::string word;
while (in_ite != eof) {
word = *in_ite;
for(auto &ch : word) ch = std::tolower(ch);
word.erase(std::remove_if(word.begin(), word.end(), ispunct), word.end());
if (exclude.find(word) == exclude.end()) {
++word_count[word];
}
in_ite++;
}
for(const auto &w : word_count) {
std::cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << std::endl;
}
return 0;
}

4.list、vector、deque、map以及set的使用场景:

  • list : anytime when a doubly-linked list is required.
  • vector : anytime when a dynamic array is required.
  • deque : An answer from Stackoverflow.
  • map : dictionary.
  • set : when to keep elements sorted and unique.
    deque:

When modeling any kind of real-world waiting line: entities (bits, people, cars, words, particles, whatever) arrive with a certain frequency to the end of the line and are serviced at a different frequency at the beginning of the line. While waiting some entities may decide to leave the line…. etc. The point is that you need “fast access” to insert/deletes at both ends of the line, hence a deque.

I’m not sure how realistic this is. Consider that you can’t use a deque to model a real world line unless in that line only the last person can leave. Also in this hypothetical line if the third guy from the end wants to leave then everyone behind him better share his opinion because they’ll need to leave too.

I working on this right now: I have one program that displays images with 60Hz using OpenGL. Another program decides what should be drawn in the images. Unfortunately this other program occasionally stops for the garbage collection. I use a deque in the display program as a cache for future images. This way I can ensure that there will always be images available even when the garbage collector stops the producer occasionally.

6.关联容器的迭代器都是双向的

7.set和map都可以在声明时进行初始化,初始化时提供对应的值或值对即可。

8.pair类型定义在头文件utility中,一个pair保存两个不同类型的数据成员,分别通过pair.first和pair.second来访问。map的元素即是pair,所以map有点类似于成员为pair是vector,pair的定义方式如下:

1
2
3
4
5
//定义一个pair,两个类型分别为T1,T2并使用v1,v2对其进行值初始化。当然也可以使用=号进行初始化或者只定义,但定义时会调用默认初始化  
pair<T1, T2> p(v1, v2);

//返回一个用v1和v2初始化的pair,pair的类型从v1和v2的类型推断出来
make_pair(v1, v2);

C++11中可以让函数直接返回一个pair,即可以使函数一次返回两个值。

9.关联容器定义了以下额外的类型别名:

  • key_type 表示容器类型的关键字类型
  • mapped_type 表示每个关键字关联的类型,只适用于map
  • value_type 对于set,与key_type相同,对于map,为pair<const key_type, mapped_type>
    例如:
    1
    2
    3
    4
    5
    set<string>::value_type v1;  //v1是一个string
    set<string>::key_type v2; //v2是一个string
    map<string, int>::value_type v3; //v3是一个pair<const string, int>
    map<string, int>::key_type v4; //v4是一个string
    map<string, int>::mapped_type v5; //v5是一个int

10.对一个关联容器迭代器解引用时,会得到一个类型为容器的value_type的值的引用,对于map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值,所以只能通过map迭代器修改pair的值,但不能修改其关键字的值。记得可以使用->来直接访问引用之后的对象的成员。

11.set的迭代器虽然定义了iterator和const_iterator,但两种类型都只允许只读访问set中的元素,而不能修改其值。

12.当使用一个迭代器遍历一个map、multimap、set或multiset时,迭代器按关键字升序遍历元素。

13.由于关联容器的关键字总是会有const属性,而泛型算法一般需要向元素写入值,所以通常不对关联容器使用泛型算法。

14.向关联容器中插入元素使用insert和emplace成员函数,删除元素使用erase。向map中添加元素的4种方式,假定word_count存储的是单词和它出现的次数:

1
2
3
4
word_count.insert({word, 1}); //C++11中支持的花括号直接初始化
word_count.insert(make_pair(word, 1)); //调用make_pair来返回一个pair
word_count.insert(pair<string, size_t>(word, 1)); //显式构造一个pair
word_count.insert(map<string, size_t>::value_type(word, 1)); //构造一个恰当的pair类型,并构造该类型的一个新对象

15.关联容器的insert(或emplace)返回的值依赖与容器类型和参数,对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,该pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。

15.map和unordered_map容器提供了下标运算符和一个对应的at函数。它们的区别如下:

  • c[k] 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化。
  • c.at(k) 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常。

16.map下标运算符与解引用一个map迭代器所返回的类型是不一样的。当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。

17.关联容器支持多种元素访问方式,如find成员函数和count成员函数。

18.map下标操作的作用是:若key存在,则返回相应的value;若key不存在,则对该key对应的value赋一个对应于value类型数据的默认值并返回。例如对于类型为map<string,string>的变量m,如果m中不存在”str”,则当使用m[“str”]时,相当于向m中插入了一个(“str”, “”)

19.记得使用find进行查找某个值是否在map变量中。

18.map使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/*
//搜索input文件中的内容,根据map_file中的文件规则相应简写单词进行替换
//例如input中的某行为ho a y,map_file中的规则有
//ho how
//a are
//y you
//则该行输出应为how are you, 假定规则文件内容都是有效的
*/
#include <iostream>
#include <map>
#include <string>
#include <fstream>
#include <sstream>
#include <stdexcept>
//根据文件建立相应map
std::map<std::string, std::string> buildMap(std::ifstream &mapfile)
{
std::map<std::string, std::string> trans_map;
std::string key;
std::string value;
while (mapfile >> key && getline(mapfile, value)) {
if (value.size() > 1)
trans_map[key] = value.substr(1);
else
throw std::runtime_error("no rule for " + key);
}
return trans_map;
}

//根据map搜索单词
const std::string & transword(const std::string &str, const std::map<std::string, std::string> &trans_map)
{
std::map<std::string, std::string>::const_iterator it = trans_map.find(str);
if (it != trans_map.cend())
return it->second;
else
return str;
}
//输出
void transform(std::ifstream &infile, std::ifstream &mapfile)
{
auto trans_map = buildMap(mapfile);
std::string line;
while (getline(infile, line)) {
std::istringstream is(line);
std::string str;
while (is >> str) {
std::cout << transword(str, trans_map) << " ";
}
std::cout << std::endl;
}
}
int main()
{
std::ifstream infile("words.txt"), mapfile("trans_map.txt");
if (infile && mapfile)
transform(infile, mapfile);
else
std::cout << "Error, Can't open the file" << std::endl;
return 0;
}

9.C++11中定义了4个无序关联容器(unordered associative container)。这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。有序与无序关联容器的对比如下:

  • Ordered Associative Container
    • Standard Traversal encounters elements in sorted order
    • Order predicate may be specified
    • Default order predicate is “less than”, defined using operator< for the element type
    • Popular implementations: OrderedVector, BinarySearchTree
    • Search operations required to have O(log n) runtime
    • Insert, Remove operations should either be seldom used or have O(log n) runtime
  • Unordered Associative Container
    • Standard Traversal encounters elements in unspecified order
    • Search, Insert, Remove operations should have average-case constant runtime
    • Popular implementations use hashing

Welcome to my other publishing channels