##C++ Primer 学习笔记(第三章:字符串、向量和数组)
[TOC]
###3.1 命名空间的using
声明
-
using
声明语句可以一行放多条。 -
位于头文件的代码,一般来说不应该使用
using
声明。因为其内容会拷贝到每个使用该头文件的文件中,可能会产生名字冲突。
###3.2 标准库类型string
-
使用
string
类型必须首先包含string
头文件,作为标准库的一部分,string
定义在命名空间std
中。 -
定义和初始化
string
对象
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
对象中。 -
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
- 需要注意字符数组可以用字符串字面值来初始化,此时尾部有一个空字符'\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;