《C++ primer》chapter 3:字符串,向量和数组
1. 命名空间的using声明
我们知道std::cin表示从标准输入中读取内容,作用域操作符::的含义是让编译器从操作符左侧名字的作用域寻找右侧那个名字,因此std::cin就是要使用命名空间std中的名字cin。
这个命名空间表示方法略显繁琐,一种简单便捷的获取命名空间内成员的方法是使用using声明:
using namespace::name;
一旦声明了上述语句就可以直接访问命名空间中的名字,
每个命名空间中的名字需要单独using声明。头文件的内容包括声明,会拷贝到所有引用它的文件中去,故头文件不应该包含using声明。
2. 标准库类型string
标准库类型string表示可变长的字符序列。使用string类型首先要包含string头文件,string定义在命名空间std中:
#include <string>
using std::string;
2.1 定义和初始化string对象
初始化string
对象的方式:
方式 | 解释 |
---|---|
string s1 |
默认初始化,s1 是个空字符串 |
string s2(s1) |
s2 是s1 的副本 |
string s2 = s1 |
等价于s2(s1) ,s2 是s1 的副本 |
string s3("value") |
s3 是字面值“value”的副本,除了字面值最后的那个空字符外 |
string s3 = "value" |
等价于s3("value") ,s3 是字面值"value"的副本 |
string s4(n, 'c') |
把s4 初始化为由连续n 个字符c 组成的串 |
如果使用等号初始化一个变量,实际上执行的是拷贝初始化,编译器把等会右边的初始值拷贝到新建对象中去,若不使用等号,则执行的是直接初始化。
2.2 string对象的操作
操作 | 解释 |
---|---|
os << s |
将s 写到输出流os 当中,返回os |
is >> s |
从is 中读取字符串赋给s ,字符串以空白分割,返回is |
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 |
用s2 的副本代替s1 中原来的字符 |
s1==s2 |
如果s1 和s2 中所含的字符完全一样,则它们相等;string 对象的相等性判断对字母的大小写敏感 |
s1!=s2 |
同上 |
< , <= , > , >= |
利用字符在字典中的顺序进行比较,且对字母的大小写敏感 |
读写string对象 IO执行读取string操作时,会忽略开头的空白(包括空格,换行符和制表符),直到遇到下一个空白为止。
getline函数可以读取一整行,直到遇到换行符为止。
string::size_type类型 string标准库定义的配套类型,表示存放string类的size函数的返回值的变量类型。C++11标准中,允许编译器通过auto或者decltype来推断变量的类型:
auto len = line.size(); // len的类型是string::size_type
如果一条表达式中已经有了size()函数,就不要再使用int了,这样可以避免int和unsigned混用可能带来的问题。
字符串字面值并不是标准库类型string的对象,字符串字面值与string是不同的类型。因此string类型和字符串字面值相加有一个要求,就是不能直接将字符串字面值相加,用“+”表示字符串拼接时,加号两侧必须至少有一个string对象。
2.3 处理string对象中的字符
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 是大写字母,输出对应的小写字母;否则原样输出c |
toupper(c) |
当c 是小写字母,输出对应的大写字母;否则原样输出c |
范围for循环处理每个字符
string s("Hello World!!!");
decltype(s.size()) punct_cnt = 0; // string::size_type类型
for (auto c: s)
if (ispunct(c))
++punct_cnt;
cout << punct_cnt << " punctuation characters in " << s << endl;
范围for循环改变字符串中的字符
string s("Hello World!!!");
for (auto &c : s) // 对于s中的每个字符(注意:c是引用)
c = toupper(c); // c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;
下标运算符 接收string::size_type类型的值作为参数,表示要访问的位置,返回值是该位置上的字符的引用。string对象的下标必须大于等于0而小于s.size()。
// 使用下标执行迭代,将第一个空白字符前的非空白字符改成大写
for (decltype(s.size()) index = 0;
index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]);
3. 标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都相同,集合中的每个对象都有一个与之对应的索引,索引用于访问对象,vector容纳着其他对象,故也常常被称作容器。
要使用vector,必须包含适当的头文件:
#include <vector>
using std::vector;
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector
3.1 定义和初始化vector对象
初始化vector
对象的方法
方法 | 解释 |
---|---|
vector<T> v1 |
v1 是一个空vector ,它潜在的元素是T 类型的,执行默认初始化 |
vector<T> v2(v1) |
v2 中包含有v1 所有元素的副本 |
vector<T> v2 = v1 |
等价于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<int> v1(10); // v1有10个元素,每个值都是0
vector<int> v2{10}; // v2有一个元素,该元素值是10
vector<int> v3(10, 1); // v3有10个元素,每个值都是1
vector<int> v4{10, 1}; // v4有2个元素,值是10, 1
vector<string> v5{"hi"}; // 列表初始化,v5有一个元素
vector<string> v6("hi"); //错误,不能用字符串字面值构建vector对象
vector<string> v7{10}; // v7有10个默认初始化的元素
vector<string> v8{10, "hi"}; // v8有10个值为"hi"的元素
3.1 vector上的操作
使用v.push_back(e)在vector尾部添加元素。
vector
支持的操作:
操作 | 解释 |
---|---|
v.emtpy() |
如果v 不含有任何元素,返回真;否则返回假 |
v.size() |
返回v 中元素的个数 |
v.push_back(t) |
向v 的尾端添加一个值为t 的元素 |
v[n] |
返回v 中第n 个位置上元素的引用 |
v1 = v2 |
用v2 中的元素拷贝替换v1 中的元素 |
v1 = {a,b,c...} |
用列表中元素的拷贝替换v1 中的元素 |
v1 == v2 |
v1 和v2 相等当且仅当它们的元素数量相同且对应位置的元素值都相同 |
v1 != v2 |
同上 |
< ,<= ,> , >= |
以字典顺序进行比较 |
- 范围
for
语句内不应该改变其遍历序列的大小。 vector
对象(以及string
对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能用于添加元素。
4. 迭代器iterator
除了下标索引访问string对象的字符或vector对象的元素之外,还有一种更通用的机制也可以实现同样的目的,这就是迭代器。所有标准库容器都可以使用迭代器,string对象严格来讲并不属于容器类型,但是string也支持迭代器。
迭代器类似指针,提供了对对象的间接访问。
有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一个位置。
4.1 使用迭代器
有迭代器的类型同时拥有返回迭代器的成员,这些类型都拥有名为begin和end的成员,begin返回指向第一个元素的迭代器,end返回指向容器或string对象尾元素的下一个位置的迭代器。
如果容器为空,则begin和end返回的是同一个迭代器。
迭代器运算符
运算符 | 解释 |
---|---|
*iter |
返回迭代器iter 所指向的元素引用 |
iter->mem |
等价于(*iter).mem |
++iter |
令iter 指示容器中的下一个元素 |
--iter |
令iter 指示容器中的上一个元素 |
iter1 == iter2 |
判断两个迭代器是否相等 |
// 用迭代器把对象的第一个字母改为了大写形式
string s("some string");
if (s.begin() != s.end()) { // 确保s非空
auto it = s.begin();
*it = toupper(*it); // 当前字符改为大写
}
// 用迭代器将第一个单词改为大写形式
for (auto it = s.begin(); it != s.end() && !isspace(*it); it++)
*it = toupper(*it);
所有标准库容器的迭代器都定义了==和!=,但是大多数都没有定义<运算符,因此要养成使用迭代器和!=的习惯。
拥有迭代器的标准库使用iterator和cons_iterator来表示迭代器的类型
vector<int>::iterator it; // it能读写vector<int>的元素
string::iterator it2; // it能读写string对象中的元素
vector<int>::const_iterator it3; // it3只能读不能写元素
string::const_iterator it4; // it4只能读不能写字符
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin();// it2的类型是vector<int>::const_iterator
auto it3 = v.cbegin(); //it3的类型是vector<int>::const_iterator
结合解引用的成员访问操作
// 依次输出text的字符串向量中的每一行字符直到遇到空行为止
for (auto it = text.cbegin(); it !=text.cend() && !it->empty(); ++it)
cout << *it << endl;
谨记,但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
迭代器运算
运算符 | 解释 |
---|---|
iter + n |
迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。 |
iter - n |
迭代器减去一个证书仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。 |
iter1 += n |
迭代器加法的复合赋值语句,将iter1 加n的结果赋给iter1 |
iter1 -= n |
迭代器减法的复合赋值语句,将iter2 减n的加过赋给iter1 |
iter1 - iter2 |
两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。 |
> 、>= 、< 、<= |
迭代器的关系运算符,如果某迭代器 |
// 使用迭代器进行二分搜索
// text必须是有序的
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // end - beg的类型是 // difference_type
while (mid != end && *mid != sought) {
if (sought < *mid)
end = mid;
else
beg = mid + 1;
mid = beg + (end - beg)/2;
}
5. 数组
与vector相似,数组也是存放类型相同的对象的容器,这些对象没有名字,需要通过其所在位置访问。与vector不同的地方是,数组大小确定不变。
5.1. 定义和初始化内置数组
数组是一种符合类型,声明形如a[d],其中a是数组名子,d是数组维度,维度必须是一个常量表达式。默认情况下,数组的元素被默认初始化。
定义数组的时候必须指定数组类型,不允许用auto关键字有初始值的列表推断类型,数组元素应为对象。
可以对数组元素进行列表初始,此时允许忽略数组维度,如果指明了维度,则初始值的总数量不应该超过指定的大小,如果维度大于提供的初始值数量,用提供的初始值初始化靠前的元素,剩下的元素被默认初始化。
字符数组用字面值初始化时,字符串字面值结尾的空字符也会被拷贝到字符数组中去。
不允许将数组内容拷贝给其他数组作为其初值,也不能用数组为其他数组赋值。
要理解复杂的数组声明,最好的办法是从数组的名字开始由内向外顺序阅读。
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[cnt]; // 错误,cnt不是常量表达式
string strs[get_size()]; //只有当get_size是constexpr时正确
const unsigned sz = 3;
int ial[sz] = {0, 1, 2};
int a2[] = {0, 1, 2}; // 维度是3的数组
int a3[5] = {0, 1, 2}; // 等价于a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; //等价于a4[] = {"hi", "bye", ""}
int a5[2] = {0, 1, 1}; // 错误,初始值过多
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; //列表初始化,含有显示的空字符
char a3[] = "C++"; // 自动添加表示字符串结束的空字符
int a[] = {0, 1, 2}; // 含有3个整数的数组
int a2[] = a; // 错误,不能用一个数组初始化另一个数组
a2 = a; // 错误,不能把一个数组直接赋值给另一个数组
int *ptrs[10]; // ptrs是含有10个整型指针的数组
int &refs[10] = /*?*/; // 错误,不存在引用的数组
int (*parray)[10] = &arr; //parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
5.2 访问数组元素
数组的元素也能像vector和string一样,使用范围for循环或下标运算符来访问。
在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,在cstddef头文件中定义了size_t类型。
数组的下标运算符合是由C++语言直接定义的, vector的下标运算符是库模板vector定义的,只能用于vector类型的运算对象。
与vector和string一样,数组的下标是否在合理范围内由程序员负责检查。
5.3 指针和数组
C++在使用数组的时候,编译器一般会把它转换为指针。
像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针。
在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。
string nums[] = {"one", "two", "three"};
string *p = &nums[0]; // p指向nums的第一个元素
string *p2 = nums; // 等价于p2 = &nums[0]
// 当使用数组作为一个auto变量的初始值,推断得到的类型是指针而非数组
int ia[] = {0,1,2,3,4,5,6,7,8,9};
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素,等价于
// auto ia2(&ia[0]), 显然ia2的类型是int*.
ia2 = 42; // 错误,ia2是一个指针,不能用int值给指针赋值
// 当使用decltype关键字时,上述转换不会发生
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; // 含10个整数的数组
ia3 = p; // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确,把i的值赋给ia3的一个元素
指针也是迭代器 指针也能遍历数组中的元素,通过数组名字或数组首元素的地址,能得到指向首元素的指针,获取尾后指针要用到数组的特殊性质。
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p指向arr的第一个元素
++p; // p指向arr[1]
int *e = &arr[10]; // 指向arr尾元素的下一个位置的指针,尾后指针不指向 // 具体元素,因此也不能对它解引用或递增操作
// 标准库函数begin和end
// 这两个函数不是类成员函数,需要将数组作为它们的参数,它们定义在iterator // 头文件中
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); //指向ia首元素的指针
int *last = end(ia); //指向arr尾元素的下一位置的指针
// 寻找arr中的第一个负数:
int *pbeg = begin(arr), *pend = end(arr);
while (pbeg != pend && *pbeg >= 0)
++pbeg
指针运算和下标
所有迭代器支持的运算,包括解引用,递增,比较,与整数相加,两个指针相减等,都可以用在指向数组元素的指针。
两个指针相减的结构的类型是ptrdiff_t的标准库类型,和size_t一样,也是定义在cstddef头文件中的机器相关类型,因为差值可能为负,所以ptrdiff_t是带符号的。
当对数组使用下标运算时,编译器会自动执行指针的转换操作,数组与标准库类型string和vector执行下标运算的一个区别是,标准库类型限定使用的下标必须是无符号类型,而内置下标运算无此要求。
constexpr size_t sz = 5;
int arr[sz] = {1,2,3,4,5};
int *ip = arr;
int *ip2 = ip + 4; // ip2指向arr的尾元素arr[4]
int *p = arr + sz; // 警告,不要解引用!
int *p2 = arr + 10; // 错误,arr只有5个元素,p2的值未定义
auto n = end(arr) - begin(arr); // n的值也是5,类型为ptrdiff_t
int *b = arr, *e = arr + sz;
while (b < e) {
//use *b
++b;
}
// 如果两个指针分别指向不相干的对象,则不能比较它们
int ia[] = {0, 2, 4, 6, 8}; // 含有5个整数的数组
int last = *(ia + 4); // 正确,把last初始化为ia[4]的值
int i = ia[2]; // ia为指向数组首元素的指针, ia[2]得到(ia+2)的元素
int *p = ia; // p指向ia的首元素
i = *(p+2); // 等价于i = ia[2]
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风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法,它存放在字符数组中并以空字符‘\0’结束,一般利用指针来操作这些字符串。
cstring头文件中的C标准库字符串函数
函数 | 介绍 |
---|---|
strlen(p) |
返回p 的长度,空字符不计算在内 |
strcmp(p1, p2) |
比较p1 和p2 的相等性。如果p1==p2 ,返回0;如果p1>p2 ,返回一个正值;如果p1<p2 ,返回一个负值。 |
strcat(p1, p2) |
将p2 附加到p1 之后,返回p1 |
strcpy(p1, p2) |
将p2 拷贝给p1 ,返回p1 |
C++程序最好不要使用C风格字符串,而使用标准库string,因为后者更安全,更高效。
混用string对象和C风格字符串
- 允许使用空字符结束的字符数组来初始化string对象或为string对象赋值。
- 在string对象的加法运算中允许以空字符结束的字符数组作为其中一个运算对象;在string对象的复合赋值运算中,允许使用以空字符结尾的字符数组作为右侧运算对象。
以上性质反过来不成立,不能用string对象直接初始化指向字符的指针,为完成这个功能,string提供了名为c_str的成员函数:
string s("Hello World");
char *str = s; // 错误,不能用string对象初始化char*
const char *str = s.c_str(); // 正确
允许使用数组来初始化vector对象,要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));
// 用于初始化vector对象的值也可能仅是一部分:
vector<int> subVec(int_arr + 1, int_arr + 4); // 拷贝了三个元素
现代C++程序应对尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格字符串。
6. 多维数组
所谓的多维数组其实是数组的数组。
int ia[3][4]; // 大小为3的数组,每个元素是含4个整数的数组
int arr[10][20][30] = {0}; // 三维数组,所有元素初始化为0
// 多维数组初始化
int ia[3][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 ix[3][4] = {0, 3, 6, 9};
多维数组的下标引用
可以使用下标运算来访问多维数组的元素,此时数组每个维度对应一个下标运算符。
ia[2][3] = arr[0][0][0]; // 用arr首元素为ia的最后一个元素赋值
int (&row)[4] = ia[1]; // 把row绑定到ia的第二个4元素数组上
// 两层for循环处理多维数组元素
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt]; // 12个未初始化的元素
for (size_t i = 0; i != rowCnt; ++i) {
for (size_t j = 0; j != colCnt; ++j) {
ia[i][j] = i * colCnt + j;
}
}
使用范围for循环处理多维数组
size_t cnt = 0;
for (auto &row : ia) {
for (auto &col : row) {
col = cnt;
++cnt;
}
}
// 错误,无法通过编译,row的类型是int*, 使得内层循环不合法
// 要使用范围for语句处理多维数组,除了最内层循环外,其他所有循环的控制
// 变量都应该是引用类型
for (auto row : ia)
for (auto col: row)
指针和多维数组
int ia[3][4];
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素
// C++11新标准提出,通过使用auto或者decltype就能尽可能地避免在数组前面
// 加上一个指针类型
// 输出ia中每个元素,每个内层数组各占一行
for (auto p = ia; p != ia + 3; ++p) {
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
// 使用标准库函数begin和end得到更简洁的实现
for (auto p = begin(ia); p != end(ia); ++p) {
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' ';
cout << endl;
}
// 类型别名简化多维数组指针
using int_arry = int[4];
for (int_arry *p = ia; p != ia + 3; ++p) {
for (int *q = *p; q != *p +4; ++q)
cout << *q << ' ';
cout << endl;
}