1.命名空间的using声明
std::cin 是使用命名空间std中的名字cin,我们可以使用using声明来去除专门的前缀(std::)
声明后就可以直接访问命名空间中的名字。
using std::cin; // using namespace::name;
按照规定,每个using声明引入命名空间中的一个成员。
位于头文件的代码一般来说不应该使用using声明。原因在于头文件的内容会拷贝到所有引用它的文件中,可能会产生名字冲突。
2. 标准库类型string
类型string表示可变长的字符序列(char),使用string类型必须包含string头文件。
2.1 定义和初始化string对象
初始化string对象的方式
string s1 | 默认初始化,s1是一个空串 |
string s2(s1) | s2是s1的副本 |
string s2 = s1 | 等价于s2(s1) |
string s3("value") | s3是字面值“value”的副本,后面带有空字符 |
string s3 = "value" | 等价于s3("value") |
string s4(n,'c') | 把s4初始化为由连续n个字符c组成的串 |
如果使用等号初始化一个变量,实际上执行的是拷贝初始化,如果不使用等号,则执行的是直接初始化。
当初始值只有一个时,使用直接初始化或拷贝初始化都行,初始值有多个,一般来说只能使用直接初始化。
2.2 string对象上的操作
string对象的相等性判断对字母的大小写敏感。
getline(is,s) | 从is中读取一行赋给s,返回is |
s.empty() | s为空返回true,否则返回false |
s.size() | 返回s中字符的个数 |
s[n] | 返回s中第n个字符的引用,位置n从0开始 |
s1 + s2 | 返回s1和s2连接后的结果 |
s1 == s2 | 判断s1和s2中所含的字符是否完全相同 |
在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
当我们想保留字符串中的空白符时,应该使用getline函数来替代原来的>>运算符。
getline函数的参数是一个输入流和一个string对象,函数直到遇到换行符为止(换行符也被读入,但不保留在对象中和输出到设备上)。
int main()
{
string line;
while(getline(cin,line)) //每次输入一行,直至文件末尾
cout << line << endl;
return 0;
}
empty函数根据string对象是否为空返回一个对应的布尔值。
size函数返回string对象中字符的个数,但size函数返回的类型是string::size_type类型。
string::size_type是无符号类型的值,并且能足够存放下任何string对象的大小。
如果一条表达式中已经有size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
相等运算符(== 和 !=)分别检验两个string对象相等或不相等,string对象相等意味着它们的长度相等且所包含的字符也都相同。
当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string。
string s1 = "hello";
string s2 = "hello" + "world"; //错误
string s3 = s1 + "world"; //正确,运算符左侧是string对象
字符串字面值与string是不同的类型。
C++在cctype头文件中定义一组标准库函数来处理改变某个字符的特性的问题。
isalnum(c) | 当c是字母或数字时为真 |
isalpha(c) | 当c是字母时为真 |
iscntrl(c) | 当c是控制字符时为真 |
isdigit(c) | 当c是数字时为真 |
isgraph(c) | 当c不是空格但可打印时为真 |
islower(c) | 当c是小写字母时为真 |
isprint(c) | 当c是可打印字符时为真 |
ispunct(c) | 当c是标点符号时为真 |
isspace(c) | 当c是空白时为真 |
isupper(c) | 当c时大写字母时为真 |
isxdigit(c) | 当c是十六进制数字时为真 |
tolower(c) | 如果c是大写字母,输出对应的小写字母 |
toupper(c) | 如果c是小写字母,输出对应的大写字母 |
C++标准库中兼容了C语言的标准库,C++将C语言的头文件去掉.h后缀,在文件名name之前添加字母c;例如C语言中的ctype.h头文件改为cctype头文件。
范围for语句用来遍历给定序列中的每个元素并对序列中的每个值执行某种操作。
string str("some string");
for(auto c:str)
cout << c << endl;
//str 是一个对象,用来表示一个序列。
//c 负责定义一个变量,用来访问序列中的元素。(循环变量其实是对象变量的拷贝)
//每次迭代,c变量会被初始化为序列的下一个元素值。
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。
想要访问string对象中的单个字符有两种方式:一是使用下标(string对象的下标从0计起),另一种是使用迭代器。
下标的值被称作“下标”或“索引”,如果某个索引是带符号类型的值将自动转换成由string::size_type表达的无符号类型。
在访问指定字符之前,应先检查序列是否为空。
C++标准并不要求标准库检测下标是否合法,因此我们在使用下标时可以设置下标的类型为string::size_type,这样可以确保下标不会小于0,避免出现不可预知的结果。
3. 标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都相同,能容纳其他对象,因此也被称为“容器”。
C++语言既有类模板,也有函数模板,其中vector是一个类模板。
模板本身不是类或函数,编译器根据模板创建类或函数的过程称为实例化。
vector<int>ivec; //ivec保存int类型的对象
vector<vector<string>>file; //file的元素是vector对象
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector<int>。
vector能容纳绝大多数类型的对象作为其元素,组成vector的元素也可以是vector。
某些编译器可能仍需以老式的声明语句来处理元素为vector的vector对象,如vector<vector<int> >.
(在外层vector对象的右尖括号和其元素类型之间添加一个空格)
3.1 定义和初始化vector对象
vector<T> v1 | v1是一个空vector,元素类型是T类型,执行默认初始化 |
vector<T>v2(v1) | v2中包含有v1所有元素的副本 |
vector<T>v2 = v1 | 等价于v2(v1) |
vector<T>v3(n,val) | v3包含了n个重复的元素,每个元素的值都是val |
vector<T>v4(n) | v4包含了n个重复地执行了值初始化的对象 |
vector<T>v5{a,b,c...} | v5包含了初始值个数的元素,每个元素都赋予相应的初始值 |
vector<T>v5 = {a,b,c...} | 等价于v5{a,b,c...} |
事实上,定义一个空vector,然后向vector对象中添加元素是一种常见的、高效的操作。
在进行vector对象的拷贝操作时,两个vector对象的类型应保持一致。
通常情况下,可以只提供vector对象容纳的元素数量而略去初始值,库会自动创建一个值初始化所有元素。
有些类要求必须明确地提供初始值,如果只提供元素的数量而没有设定初始值,只能使用直接初始化,不宜使用拷贝初始化。
在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号,如果用的是圆括号,那么所提供的值是用来构造vector对象的,反之表述为我们想列表初始化vector对象。
如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,那就要考虑用这样的值去构造vector对象。
想要列表初始化vector对象,花括号里的值必须与元素类型相同。
3.2 向vector对象中添加元素
一般我们使用vector的成员函数push_back向vector对象添加元素,push_back函数会把一个值当成vector对象的尾元素压入vector对象的尾部。
C++标准要求vector能在运行时高效快速地添加元素,因此在定义vector对象时无需设定其大小,在运行时再动态添加元素,这样性能会更高。
如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
3.3 其他vector操作
v.empty() | 如果v不含有任何元素,返回真 |
v.size() | 返回v中元素的个数 |
v.push_back(t) | 向v的尾端添加一个值为t的元素 |
v[n] | 返回v中的n个位置上的元素的引用 |
v1 = {a,b,c...} | 用列表中元素的拷贝替换v1中的元素 |
v1 == v2 | v1和v2相等当且仅当它们的元素数量相同且对应位置的元素都相同 |
访问vector对象可以通过范围for语句处理vector对象。
vector<int> v1{1,2,3,4,5,6};
for(auto &i:v1)
i += 1;
//把循环变量i定义成引用类型,就能通过i给v1的元素赋值。
vector的size成员返回vector对象中元素的个数,返回值类型是由vector定义的size_type类型。
要使用size_type类型,需要先指定它是由哪种类型所定义的,vector对象的类型总是包含着元素的类型。
vector<int>::size_type //正确
vector::size_type //错误
只有当元素的值可比较时,vector对象才能被比较。
使用下标运算符能获取到指定的元素,和string一样,vector对象的下标也是从0开始计起,下标的类型是相应的size_type类型。
vector对象的下标运算符可用来访问已存在的元素,而不能用于添加元素。
string s1 = "hello world!";
vector<string> v1 = push_back(s1);
for(auto &c:v1)
for(auto i:c)
i = toupper(i);
4. 迭代器介绍
迭代器类似于指针类型,提供了对对象的间接访问,且支持所有标准库容器。
4.1 使用迭代器
有迭代器的类型拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素的迭代器,end成员返回指向容器“尾元素的下一个位置”的迭代器。
尾后迭代器是容器的一个本不存在的元素,这样的迭代器没什么实际含义,起标记作用。
特殊情况下,如果容器为空,则begin和end返回的是同一个迭代器。
*iter | 返回迭代器iter所指元素的的引用 |
iter->mem | 解引用iter并获得该元素的名为mem成员,等价于(*iter).mem |
++iter | 令iter指示容器中的下一个元素 |
iter1 == iter2 | 判断两个迭代器是否指向同一个元素 |
试图解引用一个非法迭代器或尾后迭代器都是未被定义的行为。
实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型。
const_iterator和常量指针类似,能读取但不能修改它所指的元素值。
常量对象只能使用const_iterator,非常量既能使用const_iterator,也能使用iterator。
begin和end返回的具体类型由对象是否是常量决定,若是常量则返回const_iterator.反之是iterator.
C++新标准引入了cbegin和cend两个新函数,方便于专门得到const_iterator类型的返回值。
解引用迭代器可获得迭代器所指的对象,如果该对象的类型是类,就可以进一步访问它的成员。
(*it).empty(); //it是迭代器,empty是it的成员
// 注意:先解引用,再访问其成员
C++语言定义了箭头运算符(->),箭头运算符把解引用和成员访问两个操作结合在一起,简化表达式。
任何一种可能改变vector对象容量的操作(比如push_back)都会使该vector对象的迭代器失效。
4.2 迭代器运算
iter1 - iter2 两个迭代器相减的结果是它们之间的距离,参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
两个迭代器相减得出的距离,其类型是名为difference_type的带符号整型数,因为该距离可负可正,所以difference_type是带符号类型的。
使用迭代器运算的一个经典算法是二分搜索,二分搜索适用于从有序序列中寻找某个值。
算法思想如下:
- 初始化左边界 left 指向首元素,右边界 right 指向尾元素的下一个位置。
- 在 while 循环中,使用一个指针 mid 来指向中间元素的索引。计算 mid 的方法是将 left 和 right 相加除以 2(mid是一个迭代器)。
- 比较中间元素与目标元素的值。如果中间元素等于目标元素,则返回 mid。
- 如果中间元素大于目标元素,则目标元素只可能在数组的左半部分,将右边界 right 更新为 mid 。
- 如果中间元素小于目标元素,则目标元素只可能在数组的右半部分,将左边界 left 更新为 mid + 1。
- 重复执行步骤 2 到步骤 5,直到找到目标元素或者 left 大于 right。
vector<int> text = {1,2,3,4,5,6,7,8,9,10};
int sought = 6;
auto left = text.begin();
auto right = text.end(); //right 指向尾元素下一个位置
auto mid = text.begin() + (left + right)/2;
while(mid != right && *mid != sought) //因为mid是一个迭代器,需要解引用
{
if(sought < *mid)
right = mid;
else
left = mid + 1;
mid = left + (right + left)/2; // 新的中间点
}
5. 数组
数组的大小确定不变,不能随意向数组中增加元素,这些特殊的应用使得程序更加高效,但损失了一些灵活性。
5.1 定义和初始化内置数组
数组是一种复合类型,由数组的名字和维度所组成,维度说明了数组中元素个数,因此维度应大于0,且在编译时维度应该是已知的,维度必须是一个常量表达式。
如果在函数体内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
定义数组时必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。
数组的元素应是对象,因此不存在存放引用的数组。
对数组的元素进行列表初始化,此时允许忽略数组的维度。
字符数组可以使用字符串字面值进行初始化,当使用这种方式初始化时,注意字符串字面值的结尾处还有一个空字符。
char c1[] = {'c','+','+'}; //列表初始化,没有空字符
char c2[] = {'c','+','+','\0'} //列表初始化,含有显式的空字符
char c3[] = "c++"; //字符串字面值初始化c3,尾部有表示字符串结束的空字符
char c4[3] = "c++"; //错误,没有空间去存放空字符
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
由于数组本身就是对象,所以允许定义数组的指针及数组的引用。
默认情况下,类型修饰符从右向左依次绑定。但要想理解数组声明的含义,最好还是从数组的名字开始按照由内向外的顺序阅读。
int *ptrs[10];
//从右往左,先是一个维度为10的数组,名为ptrs,指向数据类型是int*
int (*parray)[10] = &arr;
//从内向外,先是一个指针,指向一个维度为10的数组,指向数据类型为int
5.2 访问数组元素
访问数组元素可以通过范围for语句或下标运算符来访问。
在使用数组下标时,通常将其定义为size_t类型,该类型是一种无符号类型,能表示内存中任意对象的大小,且在cstddef头文件中被定义。
由于维度是数组类型的一部分,所以系统本身了解数组有多少个元素,使用范围for语句能减轻人为控制遍历过程的负担。
在函数体内部,未定义的变量不会默认初始化,会有未定义的数值。
5.3 指针和数组
在C++语言中,指针和数组有非常密切的联系,使用数组时,编译器一般会将其转换成指针。
数组的特性:在用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。
当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。
当使用decltype关键字时,上述转换却不会发生,类型是数组中的元素类型。
C++新标准引入了标准库函数begin和end,函数定义在iterator头文件中,这两个函数与容器中的同名成员函数功能相似,不过数组并不是类类型,因此这两个函数并不是成员函数。
begin函数返回指向数组首元素的指针,end函数返回指向尾元素下一个位置的指针。
int ia[] = {1,2,3,4,5,6,7,8,9,10};
int *beg = begin(ia); //指针类型
int *last = end(ia); //指针类型
两个指针相减的结果是它们之间的距离,距离的类型是一种名为ptrdiff_t的标准库类型,和size_t一样,因为差值可以为负,所以ptrdiff_t是一种带符号类型。
指针运算适用于空指针和所指对象并非数组的指针。
对数组执行下标运算其实是对指向数组元素的指针执行下标运算。
int i = ia[2];
//ia转换成指向数组首元素的指针,对指针加减并解引用,赋值给i
int *p = ia;
i = *(p + 2); //等价于 i = ia[2]
内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样。
int *p = &ia[2]; // p指向索引为2的元素
int j = p[1]; //p[1]等价于*(p+1),就是ia[3]位置的元素
int k = p[-2]; // p[-2]是ia[0]位置的元素
//内置下标运算符可以处理负值
5.4 C风格字符串
尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,且极易引发程序漏洞。
cstring是C语言头文件string.h的C++版本。
strlen(p) | 返回p的长度,空字符不计算在内 |
strcmp(p1,p2) | 比较p1和p2的相等性,若p1>p2,则返回正值 |
strcat(p1,p2) | 将p2连接在p1之后,返回p1 |
strcpy(p1,p2) | 将p2拷贝给p1,返回p1 |
传入此类函数的指针必须指向以空字符作为结束的数组,否则将会出错。
5.6 与旧代码的接口
如果程序的某处需要一个指向字符的指针,无法直接用string对象来初始化它,string专门提供一个名为c_str的成员函数来完成这个功能。
string s("hello world");
char *str = s; //错误,不能使用string对象初始化char*
const char *str = s.c_str(); //正确,使用成员函数c_str来初始化指针
允许使用数组来初始化vector对象,只需指明要拷贝区域的首元素地址和尾后地址就行了。
int arr[] = {1,2,3,4,5,6,7,8,9,10};
vector<int> max(begin(arr),end(arr)); //拷贝全部元素
vector<int> min(arr+1,arr+3); //拷贝部分元素
//vector对象的次序和值与数组arr完全相同。
6. 多维数组
严格来说,C++语言中没有多维数组,通常所说的多维数组其实数组的数组。
对于二维数组来说,常把第一个维度称为行,第二个维度称为列。
按照由内而外的顺序阅读此类数组的定义有助于更好地理解其真实的含义。
允许使用花括号括起来的一组值初始化多维数组。
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
//3行4列的多维数组
在初始化多维数组时并非所有元素的值都必须包含在初始化列表之内,未包含的元素执行默认初始化。
int arr[3][4] = {{1},{5},{9}};
//显式地初始化每行的首元素
可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。
使用范围for语句处理多维数组时,如果想改变元素的值,得把控制变量声明成引用类型。
size_t cnt = 0;
for(auto &row : ia) //引用每一行
for(auto &col : row) //引用每行中的每一个元素
col = cnt;
在使用范围for处理多维数组时,无论你是否要修改元素的值,都推荐将外层循环的控制变量声明成引用类型,这能避免数组被自动转换成指针。