Tips for C++ Primer Chapter 3 字符串、向量和数组
第3章 字符串、向量和数组
命名空间
using std::cin;
using namespace std;
string
string s4(3, 'c'); //s4的值是"ccc"
使用getline读取一整行
string line;
getline(cin, line);
若成功读取一行,getline函数返回true(读到空行也返回true,因为成功读到一个'\n';此时line为空字符串),读取失败返回false。
getline读取遇到换行符为止,换行符也被读进来了,但在把字符串存入string对象时,换行符没有存进去。
string对象上的操作
os<<s 将s写到输出流os,返回os
is>>s 从输入流is中读取字符串赋给s,返回is
getline(is, s) 从输入流is中读取一行赋给s
s.empty()
s.size() s.length() 二者没有区别
s[i]
+
=、==、!=
<、<=、>、>=
string的size函数返回类型是size_type类型,它是一个无符号整数。
string s("abc"); //s.size()值为3,注意它是无符号数
bool b1 = s.size() < -1; //b1的值是true;因为s.size()是无符号数,混用带符号数与无符号数时,带符号数会自动转换成一个无符号数,此处-1将变成一个很大的正整数(转换的方式在第2章已讨论)
int len = s.size(); //无符号数s.size()先转换成一个带符号数len
bool b2 = len < -1; //b2的值是false;因为len是一个带符号数,与-1比较时不存在带符号数与无符号数混用而发生意想不到的自动转换问题
字符串的字典序大小比较
字符串大小的比较结果等于第一对相异字符的比较结果
"abc" < "ac"
若对应位置字符全都相同,则长度较小的字符串小
"abc" < "abcd"
否则两字符串相等
cname头文件和name.h头文件的内容是一样的,区别是,在名为cname的头文件中定义的名字从属于std命名空间,而name.h中的名字则不然。
遍历序列:基于范围的for语句
for(declaration : expression)
statement
expression部分是一个对象,用于表示一个序列;
declaration部分负责定义一个变量,该变量被用于访问(接收)序列中的元素;
每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。
例如:
string s("some string");
for(auto c : s)
cout<< c <<endl;
输出:
s
o
m
e
s
t
r
i
n
g
使用基于范围的for语句来修改序列
需要注意的是,想要改变expression对象的序列中元素的值,必须把declaration部分的循环变量定义成引用类型。
这个变量实际上依次绑定到了序列的每个元素上,通过这个引用可以修改其绑定对象的值。
例如:
string s("some string");
for(auto &c : s)
c = toupper(c);
cout<<s<<endl;
输出:
SOME STRING
下标运算符[]
下标的值称为“下标”或“索引”。任何表达式只要它的值是一个整型值就能作为索引,但是,如果某个索引是带符号类型的值,将自动转换成无符号类型的值(对string用[]则对应类型是string::size_type类型)。
例如,s[-1]中的-1将转换成一个很大的正整数,这将产生不可预知的后果。
C++标准并不检查下标是否合法,所以使用下标时要自己注意检查下标的合法性:对非空的string,索引的范围是[0, s.size()-1]的整数;对空字符串使用s[0]结果是未定义的。
vector
vector是一个类模板(class template);vector是一种容器(container)。
vector是模板而非类型,当使用模板时,需要指出编译器应把类(对函数模板就是函数)实例化成何种类型。
因为引用不是对象,所以不存在包含引用的vector。除此之外,其它大多数(非引用)的内置类型和类类型都可以构成vector对象。
注意,在C++11之前,以vector的元素还是vector为例,写法是:
vector<vector<int> > //C++11之前;有空格
而非:
vector<vector<int>> //C++11;无空格
定义和初始化vector对象
vector<T> v1
vector<T> v2(v1) 或 vector<T> v2 = v1
vector<T> v3(n, val) //v3包括n个重复元素,每个元素的值都是val
vector<T> v4(n) //v4包括n个重复元素,每个元素的值会置为一个确定的默认值(例如对int是0,对string是空串)
vector<T> v5{a,b,c...} 或 vector<T> v5 = {a,b,c...} //列表初始化vector对象
列表初始化 or 值初始化?
如果用的是圆括号,可以说提供的值是用来构造(construct)或者说值初始化(value initialize)vector对象的。
vector<int> v1(10); //v1有10个元素,每个元素值为0【构造】
vector<int> v2(10,1); //v2有10个元素,每个元素值为1【构造】
如果用的是花括号,可以表述成我们想要列表初始化(list initialize)该vector对象。
当使用了花括号时,编译器会尽量先尝试是否能进行合法的列表初始化(初始化列表中的值要符合vector中元素的类型):
例如:
vector<int> v3{10}; //v3有1个元素,该元素值为10【列表初始化】
vector<int> v4{10, 1}; //v4有2个元素,值分别是10和1【列表初始化】
vector<string> v5{"hi"}; //合法;v5有一个元素"hi"【列表初始化】
vector<string> v6("hi"); //非法;不能使用字符串字面值构建vector对象(显然,字符串字面值本身不是vector对象)【构造(失败)】
vector<string> v7{10}; //合法;显然不能用int初始化string对象,所以无法进行列表初始化;但是,接下来编译器会尝试用值初始化的方式初始化vector对象,最后v7有10个默认值初始化的元素(10个空串)【列表初始化->值初始化】
//也就是说,上一条语句实际被转换成:vector<string> v7(10)
vector<string> v8{10, "hi"}; //合法;列表的第一个值不是string,所以无法进行列表初始化;但是,接下来编译器会尝试用值初始化的方式初始化vector对象,最后v8有10个值为"hi"的元素【列表初始化->值初始化】
//也就是说,上一条语句实际被转换成:vector<string> v8(10, "hi")
补充:
vector<string> v9{"hi", 10}; //非法;无法进行列表初始化(元素类型不符合),也无法转换成值初始化(vector<string> v9("hi", 10)是无意义的操作)
如果循环体内部包含有向vector对象添加元素的语句,则避免使用范围for循环。
原因:向容器中添加元素或从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。
注意:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
其它vector操作
.empty() .size() .push_back(val) v[i]
v1 = v2
v = {a,b,c...}
== != < <= > >=(以字典序比较)
注意:
vector的size函数返回值类型是vector定义的size_type类型;
要使用size_type,需首先指定它是由哪种类型定义的;vector对象的类型总是包含着元素的类型。
vector<int>::size_type //正确
vector::size_type //错误
不能使用下标运算符对vector对象添加元素(可用[]访问或修改合法下标位置已存在的元素)。
试图用[]访问一个超出下标范围的元素,编译器不会检查这种错误,但运行时会得到一个不可预知的值,程序可能崩溃。
迭代器
迭代器运算符
*iter 返回迭代器iter所指元素的引用
iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++ -- 令迭代器指示容器中的下一个/上一个元素
== !=
注:->运算符综合了解引用操作和点操作,a->b等价于(*a).b
定义迭代器
vector<T>::iterator it1;
vector<T>::const_iterator it2;
const_iterator类似于常量指针,能通过它读取但不能修改它所指的元素值(iterator则可读可写)。
如果vector对象(或其它容器类的对象)是一个常量,只能用const_iterator。否则,可用const_iterator或iterator
迭代器类型
用auto自动推断迭代器类型
vector<T> v;
auto it3 = v.begin(); //it3是vector<T>::iterator类型
const vector<T> cv;
auto it4 = cv.begin(); //it4是vector<T>::const_iterator类型
注:begin()或end()返回的具体类型取决于对象是否常量。
注:不论对象是否常量,cbegin()或cend()返回的类型都是const_iterator。
注:严格来说,string对象不属于容器类,但string也支持迭代器。
string::iterator it3;
string::const_iterator it4;
迭代器运算
iter + n iter之后的第n个位置;若超出容器范围,则值是.end()
iter - n iter之前的第n个位置;若超出容器范围,则值也是.end()
iter += n (iter + n)的值赋给iter
iter -= n (iter - n)的值赋给iter
iter1 - iter2 结果是两个迭代器之间的距离(大的减小的得到正整数或0,小的减大的得到负整数或0)
> >= < <= 迭代器指向的位置在前的小
数组
数组的大小固定,相比vector,运行时性能较好,但损失了灵活性。
如果不清楚元素的确切个数,只能使用vector。
定义和初始化内置数组
a[size] size必须是一个常量表达式。
数组的元素会被默认初始化。和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,默认初始化会令数组元素含有未定义的值。
定义数组时需明确指出数组的类型,不允许用auto关键字由初始值的列表推断类型。
数组元素应为对象,不存在引用的数组。
列表初始化数组
int a1[3] = {0,1,2}; //ok
int a2[] = {0,1,2}; //ok;编译器会推断数组大小
int a3[4] = {0,1,2}; //ok;但下标为3的位置的值未显式指定,将执行默认初始化(int是内置类型;函数体外:0,函数体内:未定义)
string a4[4] = {"hi", "bye"}; //ok;等价于a4[] = {"hi", "bye", "", ""}
int a5[2] = {0,1,2}; //非法;初始值过多
字符数组的特殊性
对字符数组,有一种额外的初始化形式,可用字符串字面值。
char a1[] = {'C', '+', '+'};
char a2[] = {'C', '+', '+', '\0'};
char a3[] = "C++"; //将自动在a3中添加'\0'
char a4[3] = "C++"; //错误;没有空间存放'\0'
不允许用一个数组初始化另一个数组或把一个数组赋给另一个数组。
int a[] = {0,1,2};
int a2[] = a; //错误
a2 = a; //错误
注:也有一些编译器支持数组的赋值,这就是所谓的“编译器扩展(compiler extension)”,但要避免使用这种非标准特性(考虑程序的可移植性)。
复杂的数组声明
简单的数组声明,向以前一样从右向左理解其含义比较简单:
例如:
int *ptrs[10]; //ptrs是含有10个整型指针的数组
从右向左:首先,由[10]知道是一个大小为10的数组;它的名字是ptrs;然后知道数组中存放的是指向int的指针。
注:int &ptrs[10]; //非法;不存在引用的数组,数组元素应该是对象
要想理解复杂的数组声明的含义,最好从数组的名字开始按照由内向外的顺序阅读。
例如:
int (*pArr)[10] = &arr; //pArr是指针,指向一个含有10个整数的数组
由内向外:*pArr说明pArr是一个指针;接下来看右边,可知pArr是一个指向大小为10的数组的指针;最后看左边,可知数组中的元素是int。
int (&rArr)[10] = arr; //aArr是引用,引用一个含有10个整数的数组
由内向外:&rArr说明rArr是一个引用;再看右边,可知rArr引用的是一个大小为10的数组;最后看左边,可知数组中的元素是int。
补充:
int *(&rArr)[10] = ptrs; //rArr是数组的引用,该数组含有10个指针
由内向外:首先知道rArr是一个引用;再看右边,可知rArr引用的是一个大小为10的数组;最后看左边,可知数组中的元素是int*(指向int的指针)。
数组下标为size_t类型,是一种无符号整数。
两指针相减的结果类型是ptrdiff_t类型(pointer difference type),是一种带符号整数。
对数组a,使用数组名a相当于使用一个指针,其值等于&a[0]。
因此:
int a[10];
int *p = a; //相当于int *p = &a[0]
auto b(a); //相当于auto b(&a[0])
则b的类型是int*,而非int。
但要特别注意:
【特例】当使用decltype关键字时,上述转换不会发生。
int ia[] = {0,1,2};
decltype(ia) ia2 = {4,5,6}; //ia的类型是“由3个整数构成的数组”,故decltype推断ia2结果同样就是“由3个整数构成的数组”(而不是int*)
指向数组的指针也是“迭代器”,它支持迭代器的所有运算。(迭代器的运算前面已讨论,包括:解引用、递增/递减、比较大小、与整数相加/相减、两个指针相减等)
标准库函数begin和end
这两个函数定义在iterator头文件中(不同于类内的自定义begin和end)。
可以使用这两个函数获取数组的首元素的指针和尾元素的下一位置的指针。
int ia = {0,1,2};
int *be = begin(ia);
int *ed = end(ia);
内置的下标运算符所用的索引值不是无符号类型,可以处理负值,这一点与string和vector不一样。
int ia={0,2,4,6,8};
int *p = &ia[2]; //p指向索引为2的值(4)
int i = p[1]; //p[1]等价于*(p+1),也就是ia[3]表示的那个元素(6)
int j = p[-2]; //p[-2]等价于*(p-2),也就是ia[0]表示的那个元素(0)
C风格字符串
strlen(p) 返回字符串长度(不包含'\0')
strcmp(p1, p2) 若p1<p2返回负值
strcat(p1, p2) 将p2附加到p1之后,并返回p1
strcpy(p1, p2) 将p2拷贝给p1,并返回p1
注:p、p1、p2是表示字符串的指针。
以上函数不负责验证其字符串参数。
例如:
char a[] = "abcde", *p = a;
cout<< strlen(p) <<endl; //输出5
cout<< strlen(p+1) <<endl; //输出4;编译器不会报错,会计算当前指针位置到'\0'位置的距离
cout<< strlen(p+10) <<endl; //编译器不报错;可能输出不可预料的值或崩溃(strlen会从p+10的位置向前寻找第一个'\0'才停止)
混用string对象和C风格字符串
允许用C风格字符串来初始化string对象:
string s("hello");
但不能用string代替C风格字符串:
const char *str = "hello"; //ok
char *str = s; //错误
const char *str = s.c_str(); //ok
注意:c_str()函数返回string对应的C风格字符串,返回类型是const char*(从而保证了不会通过指针str改变字符数组的内容),因此也必须以const char*类型的变量接收它。
注意:我们无法保证c_str函数返回的数组一直有效,事实上,如果后续操作改变了s的值,就可能让之前返回的数组失去效用;所以,若执行完c_str后程序想一直正确地使用其返回的数组,最好将该数组拷贝一份。
允许用数组来初始化vector对象
int ia[] = {0,1,2,3};
vector<int> iv(begin(ia), end(ia)); //指明数组的首元素地址和尾后地址(尾元素之后一个位置的地址)即可
用于初始化vector的值也可以只是数组中的一部分:
vector<int> iv(a+1, a+3); //拷贝的下标范围是[1,3),即:将拷贝a[1]、a[2]
多维数组
多维数组实际就是数组元素为数组的数组。
多维数组的初始化
int a[3][4] = {0}; //每个元素都初始化为0
以下两种初始化效果一样:
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
int a[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
int a[3][4] = {{0}, {4}, {8}}; //显式地初始化每一行的第一个元素(其余元素执行默认初始化)
int a[3][4] = {0, 3, 6, 9}; //显式地初始化第一行的元素(其余元素执行默认初始化)
多维数组的下标引用
int a[3][4];
int (&row)[4] = a[1]; //把row绑定到a的第2个“4元素数组”上
注解:row是一个引用,绑定到的对象是一个包含4个元素的数组,数组元素的类型是int整数。
使用范围for语句处理多维数组
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
for(auto &row : a)
for(auto &var : row)
var += 1;
for(auto &row : a)
{
for(auto &var : row)
{
cout << var<<" ";
}
cout << endl;
}
输出:
1 2 3 4
5 6 7 8
9 10 11 12
注解:第一层循环,变量row引用(绑定到)了二维数组a的每一行(这一行是一个一维数组);第二层循环,变量var引用(绑定到)了某一行(二维数组中的某一个一维数组)中的某一个元素,并通过引用令其值修改为原来的值+1。
注:使用范围for处理多维数组,除了最内层循环,其余所有循环的控制变量都应该是引用类型。(不论循环中是否对数组有写操作)
原因:
看看如果不声明成引用类型会发生什么:
for(auto row : a)
for(auto var : row)
像之前一样,第一层循环遍历a的所有元素,这些元素实际是大小为4的数组;
因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其它数组类型一样)转换成指向该数组内首元素的指针,这样得到row的类型就是int*;
到了内层循环,编译器试图“在一个int*(一个指针)内遍历”,这显然是不合法的。
指针和多维数组
int ia[3][4];
int (*p)[4] = ia; //p是个指针,指向含有4个int整数的数组(这里p的值也就是ia的第一个内层数组的指针:&ia[0])
p = &ia[2]; //p指向ia的尾元素(该元素就是ia中的最后一个“4元素数组”)
Note:
int *ip[4]; //ip是一个元素类型是int型指针的数组(数组元素类型是int*)
int (*ip)[4]; //ip是一个指向含有4个整数的数组的指针
指针和多维数组使用示例
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
以下代码片均能把数组a如下打印出来:
0 1 2 3
4 5 6 7
8 9 10 11
方式1:使用范围for
for(int (&row)[4] : a)
{
for(int &var : row)
cout << var << " ";
cout << endl;
}
注解:当使用范围for处理多维数组,每层循环的控制变量都要使用引用。
方式2:使用普通的for语句+下标运算符
for(size_t i = 0; i != 3; ++i)
{
for(size_t j = 0; j!=4; ++j)
cout << a[i][j] << " ";
cout << endl;
}
方式3:使用普通的for语句+指针+标准库函数begin和end
for(int (*p)[4] = begin(a); p != end(a); ++p)
{
for(int *q = begin(*p); q != end(*p); ++q)
cout << *q << " ";
cout << endl;
}
PS:begin(a)在数值上等于begin(a[0]);end(a)在数值上等于end(a[2])。
方式4:使用普通的for语句+指针+auto运算符
for(auto p = a; p != a + 3; ++p)
{
for(auto q = *p; q != *p + 4; ++q)
cout << *q << " ";
cout << endl;
}
注解:第一层循环首先声明一个指针p,并令其指向a的第一个内层数组(p是指向“由4个整数组成的数组”的指针);然后依次迭代直到a的全部3行都处理完为止。其中++p负责将指针p移动到a的下一行。
第二层循环负责输出内层数组所包含的值。它首先令指针q指向p当前所在的行的第一个元素(q是指向int整数的指针)。
*p是一个含有4个整数的数组,数组名被自动地转换成指向该数组首元素的指针。
第二层循环不断迭代直到处理完当前内层数组的所有元素为止。
为了获取第二层for循环的终止条件,再一次解引用p得到指向内存数组首元素的指针,给它加上4就得到了终止条件。
方式5:使用普通的for语句+指针+类型别名
using int4_arr = int[4];
//上一行相当于:typedef int int4_arr[4];
for(int4_arr *p = a; p != a + 3; ++p)
{
for(int *q = *p; q != *p + 4; ++q)
cout << *q << " ";
cout << endl;
}
注解:将类型“由4个整数组成的数组”命名为“int4_arr”,再用“int4_arr”定义第一层循环的控制变量显得简洁明了。