##C++ Primer 学习笔记(第三章:字符串、向量和数组)git
[TOC]express
###3.1 命名空间的using
声明数组
using
声明语句能够一行放多条。安全
位于头文件的代码,通常来讲不该该使用using
声明。由于其内容会拷贝到每一个使用该头文件的文件中,可能会产生名字冲突。函数
###3.2 标准库类型string
性能
使用string
类型必须首先包含string
头文件,做为标准库的一部分,string
定义在命名空间std
中。学习
定义和初始化string
对象spa
string s1;//默认初始化,空串 string s2(s1);//s2是s1副本 string s2 = s1;//和上一条等价 string s3("value");//s3是字面值副本,除了字面值最后那个空字符外,直接初始化 string s3 = "value";//与上一条相同,拷贝初始化 string s4(n,'c');//把s4初始化为由连续n个字符c组成的串,直接初始化
string
上的操做os<<s;//输出流 is>>s;//读取字符赋给s,字符串以空白分隔,返回is(自动忽略空白) getline(is,s);//从is中读取一行赋给s,返回is s.empty();//为空返回true s.size();//字符个数 s[n];//返回第n个字符的引用 s1 + s2;//链接 s1 = s2;//用s2的副本代替s1 s1 == s2;s1 != s2;//相等性判断,对字母大小写敏感
<, <=, >, >+;字典序比较,对大小写敏感指针
getline(is,s)
参数是一个输入流和一个string
对象,从给定的输入流读入,直到遇到换行符为止,并把所读内容(不含换行符)存入到string
对象中。code
string::size_type
类型 size
函数返回的就是一个string::size_type
类型的值,是一个无符号类型 可使用auto
或者decltype
来推断变量类型:
auto len = line.size();
因为无符号数和带符号数混用会产生其余问题,若是一个表达式中有size
就不要再用int
了。
比较string
对象 依照字典顺序: (1)若是公共部份内容相同,则较短的小于较长的。 (2)若是存在不一致,则以第一对相异字符比较的结果为准。
在使用相加运算符时,必须确保每一个加法运算符(+
)的两侧运算对象至少一个是string
:
string s6 = s1 + ", " + "world";//正确 string s7 = "hello" + ", " + s2;//错误:不能把字面值直接相加
切记:字符字面值('\n'
)和字符串字面值("word"
)string
是不一样类型,能够用来初始化或相加的缘由是标准库容许把字符字面值和字符串字面值转换成string
对象。
string
对象中的字符,在cctype
头文件中定义了一组标准库函数。isalnum(c);//c是字母或数字 isalpha(c);//c是字母 iscntrl(c);//c是控制字符 isdigit(c);//c是数字 isgraph(c);//c不是空格但可打印 islower(c);//c为小写字母 isprint(c);//c是可打印字符(即c是空格或c具备可视形式) ispunct(c);//c是标点符号(即c不是控制字符、数字、字母、可打印空白中的一种) isspace(c);//c是空白(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种) isupper(c);//c是大写 isxdigit(c);//c是十六进制数字 tolower(c);//把c变小写 toupper(c);//把c变大写
C++
标准库兼容了C
的标准库,将name.h
命名为cname
,并去掉了后缀。即cctype
与ctype.h
的内容是同样的。 在名为cname
的头文件中定义的名字从属于命名空间std
,而定义在.h
头文件中则否则。 通常来讲C++
程序应该使用cname
的头文件而不使用.h形式,标准库中的名字总能在std
命名空间中找到。
遍历string
中的每一个字符使用范围for
语句(C++11
):
for (declaration : expression)//这种遍历只能处理元素,不能改变内容 statement
expression:一个对象,用于表示一个序列 declaration:定义一个变量,用于访问序列中的基础元素,每次迭代会被初始化为expression部分的下一个元素值 example1:
string str("some string"); for(auto c : str) cout << c << endl;
example2:
string s("Hello World!!!"); decltype(s.size()) punct_cnt = 0; for(auto c : s) if(ispunct(c)) ++punct_cnt; cout << punct_cnt << endl;
for
必须把循环变量定义成引用类型。string s("Hello World!!!"); for (auto &c : s) c = toupper(c); cout << s << endl;
string
对象中的单个字符有两种方式:下标和迭代器。 下标运算符[]
接受的输入参数是string::size_type
类型值,返回该位置上字符的引用。 s[s.size() - 1]
是最后一个字符 在访问指定字符时必定要首先检查字符串对象是否为空。(s.empty()
) 只要字符串不是常量,就能为下标运算符返回的字符赋新值。 例子:(第一个单词大写)for(decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index) s[index] = toupper(s[index]);
###3.3 标准库类型vector
(vector
表示对象的集合,也被称为容器)
模板自己不是类或函数,相反能够讲模板当作编译器生成类或函数编写的一份说明。编译器根据模板建立类或函数的过程称为实例化。
vector
能容纳绝大多数类型的对象做为其元素,可是由于引用不是对象,因此不存在包含引用的vector
。
定义和初始化:
vector<T> v1;//默认初始化,即空 vector<T> v2(v1);//v2包含v1全部副本,这必须保证T一致 vector<T> v2 = v1;//与(2)等价 vector<T> v3(n, val);//n个重复元素val vector<T> v4(n);//n个重复执行初始化的对象,初始值由元素类型决定(类型T必须支持默认初始化) vector<T> v5{a, b, c...};//就是这些元素构成的向量,列表初始化(必须是大括号{}) vector<T> v5 = {a, b, c...};//等价于上面 vector<int> v1(10);vector<int> v2{10}; vector<int> v3(10, 1);vector<int> v4{10, 1};
圆括号是构造对象,花括号是列表初始化。 直接初始化只适用于:初始值已知且数量不多、初始值是另外一个vector
的副本、全部元素初始值都同样。
若是循环体内包含有向vector
对象添加元素的语句,则不能使用范围for
循环(第五章)。
vector
的操做:
v.empty();//为空 v.size();//返回v中元素的个数,类型为vector<T>::size_type v.push_back(t);//添加元素 v[n];//返回位置上的引用,下标类型是vector<T>::size_type v1 = v2;//拷贝替换 v1 = {a, b, c...};//列表拷贝替换 v1 ==(!=) v2;//判断相等 <, <=, >, >=;//字典序比较。若是容量不一样,可是相同部分一致,则元素少的较小;其他以第一对相异元素比较结果为准。(元素的类型必须支持比较,不然不能比较)
vector
对象(以及string
对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。试图用下标方式访问一个不存在的元素会引起错误,不过这种错误不会被编译器发现。 而确保下标合法的一种有效手段就是尽量使用范围for
语句。
能够巧妙地使用退格键:
if(!v.empty()) cout<<"\b";
来覆盖多余的输出。(好比cout << a[i] << ",";
,到了最后一个元素能够覆盖掉)
###3.4 迭代器介绍
全部标准库容器(vector
)均可以使用迭代器,可是其中只有少数几种才同时支持下标运算符。严格来讲string
对象不属于容器类型,可是string
支持不少与容器相似的操做,例如和vector
同样都支持下标和迭代器。
迭代器相似指针类型,都提供了对象的间接访问,也有有效和无效之分。有效地迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其余全部状况都属于无效。
有迭代器的类型都有返回迭代器的成员:begin
和end
。 begin
返回指向第一个元素(或字符)的迭代器;end
返回指向容器(或string
对象)尾元素的下一位置(也被称做尾后迭代器)。 若是容器为空,则它俩返回同一个迭代器,都是尾后迭代器。(因此s.begin() != s.end()
能够用来判断容器或字符串是否非空) 一般用auto
来定义迭代器类型:auto b = v.begin();
迭代器运算符:
*iter;//返回所指元素引用(解引用),注意尾后迭代器没法解引用 iter->mem;//解引用并获取mem成员((*iter).mem) ++iter;//下一元素 --iter;//上一元素 iter1 == iter2;iter1 != iter2//判断两个迭代器相不相等,只有指示的是同一元素或同一容器的尾后迭代器才相等
end
返回的迭代器并不实际指示某个元素,因此不能对其进行递增或解引用操做。
循环中:迭代器用!=
,下标用<
。 标准库容器所有定义了==
和!=
,可是大多数都没有定义<
或者不支持下标运算符。所以要养成使用迭代器和!=
的习惯。
和下标类型(::size_type
)相对应,标准库的迭代器类型为iterator
和const_iterator
。 iterator
可读可写,const_iterator
相似常量指针,只读。 若是string
或者vector
对象是一个常量,只能使用const_iterator
。
若是对象是常量,那么此时的.begin()
和.end()
也是const_iterator
类型;若是不是常量返回iterator
。 若是对象只需读操做无须写操做最好使用常量类型(const_iterator
),这时可以使用cbegin
和cend
。(C++11
)不管对象是不是常量都将返回const_iterator
。
C++
的箭头运算符把解引用和成员访问两个操做结合一块儿。 即it->mem
和(*it).mem
相同
一些限制: 不能在范围for
循环向vector
对象添加元素。 任何一种可能改变vector
对象容量的操做(push_back
),都会使该vector
对象的迭代器失效。即但凡是使用了迭代器的循环体都不要向迭代器所属的容器添加元素。
vector
和string
迭代器支持的运算:
iter + n;//仍获得一个迭代器,位置向前移动n个元素 iter - n;//仍获得一个迭代器,位置向后移动n个元素 iter += n; iter -= n; iter1 - iter2;//两个迭代器之间的距离。参与运算的两个迭代器必须指向同一容器中的元素或者下一元素。
这个获得的类型为difference_type
的带符号整型,由于它可正可负。(注意迭代器之间的加法是没有意义的)
>, >=, <, <=;//前面小,后面大。参与运算的两个迭代器必须指向同一容器中的元素或者下一元素。
// text必须有序,查找的内容为sought auto beg = text.begin(), end = text.end(); auto mid = text.begin() + (end - beg) / 2; while(mid != end && *mid != sought){ if(sought < *mid) end = mid; else beg = mid + 1; mid = beg + (end - beg) / 2;//这里不用(beg + end) / 2的缘由是没有定义迭代器加法 } ###3.5 数组 1. 数组与`vector`: 类似:存放类型相同对象的容器,对象自己没有名字,须要经过位置访问。 不一样:数组大小肯定不变,不能随意添加元素。运行时性能较好,但牺牲了灵活性。 2. 数组是复合类型,其维度也属于数组类型的一部分,编译的时候必须已知。 也就是说维度必须是一个常量表达式。(`const`、`constexpr`) ```cpp unsigned cnt = 42; string bad[cnt];//错误 string strs[get_size()];//当get_size()是constexpr时正确,不然错误
数组定义时必须指定数组类型,不容许用auto
由初始值列表推断。另外,和vector
同样,数组的元素应为对象,不存在引用的数组。
对数组的元素进行列表初始化。 容许忽略数组维度,编译器会根据初始值数量计算并推测出维度; 若是指明了维度,而初始值总数量少于维度,则剩下元素将被默认初始化。
int a1[] = {1, 2, 3};维度是3 int a2[5] = {1, 2, 3};//1,2,3,0,0
char a1[] = {'C', '+', '+'}; char a2[] = {'C', '+', '+', '\0'}; const char a3[4] = "C++";
int a1[] = {0, 1, 2}; int a2[] = a1;//错误 a2 = a1;//错误
int *ptrs[10];//ptrs是含有10个整型指针的数组(从右向左依次绑定,首先是一个大小为10的数组,名字是ptrs,数组存放的类型为int*) int &refs[10] = /*?*/;//错误,不存在引用的数组 int (*ptrs)[10] = &arr;//ptrs指向一个含有10个整型的数组(从内向外绑定,ptrs是一个指针,它指向一个大小为10的数组,数组存放的类型是整型) int (&refs)[10] = arr;//refs引用一个含有10个整型的数组(也是从内向外) int *(&arry)[10] = ptrs;//arry是一个数组的引用,该数组有10个指针
使用数组下标时,一般将其定义为size_t
类型,在cstddef
头文件中定义。
和string
、vector
同样,当须要遍历数组全部元素时,最好的办法也是使用范围for
:
for(auto i : scores) cout << i << " "; cout << endl;
范围for
最大的好处是能够减轻人为控制遍历过程的负担。
undefined
):a[10] = {};
int ia[] = {1, 2, 3, 4, 5}; auto ia2(ia);//ia2是一个整型指针,至关于auto ia2(&ia[0])
decltype
时就不会发生上面的状况:decltype(ia) ia3 = {0, 1, 2}; ia[2] = 3; decltype(ia)返回的类型是由10个整数构成的数组
指针和迭代器同样,指针支持迭代器的所有操做。 首元素:ia
; 尾元素下一位置指针:int *e = &ia[5];
尾后指针不指向具体元素,不能对尾后指针执行解引用或递增等操做。
begin()
和end()
函数,将数组做为参数获取首元素指针和尾后指针。(C++11
)
int *beg = begin(ia); int *last = end(ia);//定义在<iterator>头文件中
ptrdiff_t
,也是定义在cstddef
头文件中,它是一个带符号类型。小结: size_t
,数组下标类型。 different_type
,迭代器之间距离。 ptrdiff_t
,指针距离。
int *p = &ia[2]; int j = p[1];//至关于*(p+1)即ia[3] int k = p[-2];//至关于*(p-2)即ia[0]
C
风格字符串(cstring
):char[]
,字符串存放在数组中以空字符('\0'
)结束。 (1)函数strlen(p);//返回p的长度,空字符不算在内 strcmp(p1, p2);//比较p1和p2,p1>p2返回正值 strcat(p1, p2);//p2附加到p1后,返回p1 strcpy(p1, p2);//将p2拷贝给p1,返回p1
传入此类函数的指针必须指向空字符做为结束的数组。 (2)比较字符串 不能按照标准库string
类型对象直接比较大小的方法,由于使用数组时其实使用的是首元素指针,它们并不是指向同一对象,比较的结果将是未定义的。 应该使用strcmp
函数。 (3)字符串拼接 标准库string
只须要相加就能够。 cstring
须要strcat()
strcpy(largeStr, ca1); strcat(largeStr, " "); strcat(largeStr, ca2);
可是这里在largeStr
所需空间上不容易估算准确,容易致使安全泄露。
string
和C
风格字符串 (1)容许使用以空字符结束的字符数组来初始化string
对象或为string
对象赋值。char *ch = "I am"; string s = ch;
(2)在string
对象的加法运算中容许使用以空字符结束的字符数组做为其中一个运算对象(但不能两个都是)。 (3)可是若是须要一个C
风格字符串,没法直接用string
对象代替它。解决的方法是使用string
的c_str
函数。
string s("Hello World"); char *str = s;//Wrong! const char *str = s.c_str();//注意必须是const,c_str()返回一个常量。
若是执行完c_str()
函数后程序想一直保留返回的数组,须要将数组拷贝一份,由于当s
改变后,c_str()
返回的数组也将改变。(这里c_str()
内部的机制?)
C
风格)和标准库Vector
不容许数组之间的直接赋值和初始化,vector
也不能给数组初始化。 可是容许数组为vector
初始化,方法就是前面的begin()
函数和end()
函数。 此外,用于初始化vector
对象的值也能够仅是数组的一部分:vector<int> subVec(int_arr + 1, int_arr + 4);//它包括int_arr[1-3]三个元素
总结:现代C++
程序应当尽可能使用vector
和迭代器,避免使用内置数组和指针;应该尽可能使用string
,避免使用C
风格的基于数组的字符串。
C++
语言中没有多维数组,一般说的多维数组就是数组的数组。
多维数组的初始化: 容许用花括号括起来的一组值初始化多维数组:
int ia[3][4] = {//从内向外阅读,ia是一个含有3个元素的数组,ia的元素是一个含有4个元素的数组 {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};//内层嵌套的花括号并不是必需 int ia[3][4] = {{0}, {4}, {8}};//容许并不是全部值都包含在初始化列表中。(显式初始化了每行首元素,但注意必须有大括号,不然就是顺序元素了)
int (&row)[4] = ia[1];//把row绑定到ia的第二个4元素数组上
for
在多维数组中的用法: (1)size_t cnt = 0; for (auto &row : ia) for(auto &col : row){ col = cnt; ++cnt; }
(2)
for (const auto &row : ia) for (auto col : row) cout << col <<endl;
(1)中使用引用是由于要改变元素的值 (2)中没有改变值却在外层还要使用引用,由于若是不用引用编译器初始化row
时会将数组形式的元素转换成一个指针,使row
的类型为int*
,这样内层的循环就不合法了。 注意:要使用范围for
语句处理多维数组,除了最内层的循环外,其余全部循环的控制变量都应该是引用类型。
int *ip[4];//表示整型指针的数组 int (*ip)[4];//表示指向含有4个整数的数组 //遍历二维数组ia: for (auto p = begin(ia); p != end(ia); ++p){ for(auto q = begin(*p); q != end(*p); ++q) cout << *q << " "; cout << endl; }
using int_array = int[4]; typedef int[4] int_array;