《C++ Primer》学习笔记(三):字符串、向量和数组
字符串、向量和数组
- 命名空间的using声明
- 标准库类型string
- 处理`string`对象中的字符
- 标准库类型vector
- 定义和初始化vector对象
- 迭代器
- 数组
- 数组和指针
- C风格字符串
- C标准库String函数
- 与旧代码的接口
- 多维数组
- 多维数组的初始化
- 多维数组的下标引用
- 使用范围for语句处理多维数组
- 指针与多维数组
- 类型别名简化多维数组的指针
- 练习
命名空间的using声明
using namespace::name
头文件不应包含using
声明:因为头文件的内容会拷贝到所有引用它的文件夹中去,如果头文件中有某个using声明,那么每个使用了该头文件的文件都会有这个声明,这样可能会不经意间包含了一些声明,发生始料未及的名字冲突。
标准库类型string
标准库类型 string
表示可变长的字符序列。
在执行读取操作时,string
对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cin >> s;
cout << "s:" << s << endl;
system("pause");
return 0;
}
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1, s2;
cin >> s1 >> s2;
cout << s1 << s2 << endl;
system("pause");
return 0;
}
读取未知数量的string对象:
int main()
{
string word;
while(cin >> word) //反复读取,直至到达文件末尾
cout << word << endl; //逐个输出单词,每个单词后面紧跟一个换行
return 0;
}
当希望在最终得到的字符串中保留输入时的空白符时,应该用getline
函数代替原来的>>
运算符。getline
的参数是一个输入流和一个string
对象,函数从给定输入流中读取内容,直到遇到换行符为止(注意换行符也被读进来了),然后将读取到的内容存储到string
对象中(注意不保存换行符)。如果输入的开始就是一个换行符,则得到空 string
。
和输入运算符一样,getline
也会返回它的流参数,故可以用getline
的结果作为条件。
int main()
{
string line;
//每次读取一整行,直到文件末尾
while(getline(cin, line))
cout << line << endl;
return 0;
}
string
类的size
函数的返回值是string::size_type
类型,这些配套类型体现了标准库类型与机器无关的特性。由于string::size_type
是一个无符号整数型,注意不要将带符号数(比如int
)与string::size_type
混用,避免发生不必要的错误。例如,假设n
是一个具有负值的int,则表达式s.size()<n
的判断结果几乎肯定是true,因为负数n
会自动转换成一个比较大的无符号值。
当把 string
对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是 string
。
string s4 = s1 + ", "; // ok: 把一个string对象和一个字面值相加
string s5 = "hello" + ", "; // error: 两个运算对象都不是string
string s6 = s1 + ", " + "world"; // ok: 每个加法运算符都有一个运算对象是string
为了与C兼容,C++语言中的字符串字面值并不是标准库 string
的对象。 切记,字符串字面值与 string
是不同的类型。
处理string
对象中的字符
在cctype
头文件中定义了一组标准库函数来处理string
对象中的字符,如表3.3所示。
当需要处理string
中的每个字符时,可以使用C++11提供的范围for语句,这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作。
//expression是一个对象,用于表示一个序列。
//declaration部分负责定义一个用于访问序列中基础元素的变量
for(declaration: expression)
statement
string str("Hello world!!");
for(auto c: str)
cout << c << endl;
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string str("Hello world!!!!");
decltype(str.size()) punct_cnt = 0;
//统计str中标点符号的数量
for(auto c : str){
if(ispunct(c)){
++punct_cnt;
}
}
cout << punct_cnt << " punctuation characters in "<< str << endl;
system("pause");
return 0;
}
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string str("Hello world!!!!");
//将str中的小写字母改为大写
for(auto &c : str){
c = toupper(c);
}
cout << str << endl;
system("pause");
return 0;
}
想要访问string
对象中的单个字符有两种方式:一种是使用下标,另一种是使用迭代器。
下标运算符[]
接受的参数是string::size_type
类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。string
对象的下标必须从0记起,范围是0至 size - 1,左闭右开。C++标准并不要求标准库检测下标是否合法。一旦使用了一个超出范围的下标,就会产生不可预知的结果。
标准库类型vector
标准库类型 vector
表示对象的集合,其中所有对象的类型都相同,也叫做
容器(container)。vector
是一个类模板(template),可以看作为编译器生成类或函数编写的一份说明,编译器根据模板创建类或者函数的过程称为实例化。当使用模板时,需要指出编译器应该把类或函数实例化为何种类型。
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象
vector
中能够容纳绝大多数类型的对象作为其元素,但是由于引用不是对象,所以不存在包含引用的vector
。
定义和初始化vector对象
可以通过push_back
向vector尾部添加元素。
vector<int> v2; // 空vector对象
for (int i = 0; i != 100; ++i)
v2.push_back(i); // 依次把整数值放到v2尾端
// 循环结束后v2有100个元素,值从0到99
迭代器
所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。类似于指针类型,迭代器也提供了对对象的间接访问。有效的迭代器指向某个元素或者指向容器中尾元素的下一位置,其他情况属于无效迭代器。
定义了迭代器的类型都拥有 begin
和 end
两个成员函数。begin
函数返回指向第一个元素的迭代器,end
函数返回指向容器 尾元素的下一位置(one past the end) 的迭代器。如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
在 for 或者其他循环语句的判断条件中,最好使用!=
而不是<
。所有标准库容器的迭代器都定义了 ==
和 !=
,但是只有其中少数同时定义了 <
运算符。
如果 vector
或 string
对象是常量,则只能使用 const_iterator
迭代器,该迭代器只能读元素,不能修改元素。begin
和end
返回的迭代器的类型由对象是否是常量决定,如果对象是常量,begin
和end
返回const_iterator
;如果对象不是常量,返回iterator
。
vector<int>::iterator it; // it能读写vector<int>的元素
string::iterator it2; // it2能读写string对象中的字符
vector<int>::const_iterator it3; // it3只能读元素,不能写元素
string::const_iterator it4; // it4只能读字符,不能写字符
为了便于获得const_iterator
,C++11引入了cbegin
和cend
两个函数。
注意:但凡是使用了迭代器的循环体,都不要向迭代器所属容器中添加元素。
//计算得到最接近最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.szie() / 2;
if(it < mid)
//处理vi前半部分的元素
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个送代器的距离。所谓距离,是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,difference_type
类型用来表示两个迭代器间的距离,这是一种带符号整数类型。
使用迭代器运算的一个经典算法是二分搜索,从一个有序序列中寻找某个给定值。
// text必须是有序的
// beg和end表示我们搜索的范围
// beg指向搜索范围内的第一个元素、end指向居元素的下一位置、mid指向中间的那个元素
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下的中间点
// 当还有元素尚未检查并且还没有找到sought时执行循环
whi1e (mid != end && *mid != sought)
{
if (sought < *mid) // 我们要找的元素在前半部分吗?
end = mid; // 如果是,调整搜索范围使得忽略掉后半部分
e1se // 我们要找的元素在后半部分
beg = mid + 1; // 在mid之后寻找
mid = beg + (end - beg)/2; // 新的中间点
}
数组
数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。默认情况下,数组的元素被默认初始化。和vector
一样,数组的元素应为对象,不存在引用的数组。
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[cnt]; // error:cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确,否则错误
const unsigned sz = 3;
int ia1[sz] = {
0,1,2}; // 含有3个元素的数组,元素值分别是0,1,2
int a2[] = {
0, 1, 2}; // 维度是1的数组
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,2}; // error:初始值过多
对于字符数组,有一种额外的初始化方式,可以使用字符串字面值对此类数组进行初始化,但要注意字符串字面值的结尾处的空字符也会被拷贝到字符数组中去:
char a1[] = {
'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {
'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
char a3[] = "C++"; // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // error:没有空间可存放空字符!
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
int a[] = {
O , 1 , 2}; // 含有3个整数的数组
int a2[] = a; // error:不允许使用一个数组初始化另一个数组
a2 = a; // error:不能把一个数组直接赋值给另一个数组
复杂的数组声明
int *ptrs[10]; // ptrs是含有10个整型指针的数组
int &refs[10] = /* ? */; // error:不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
数组下标通常被定义成 size_t
类型,这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。size_t
定义在头文件 cstddef
中。
数组和指针
数组有一个特性:在很多使用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:
string nums[] = {
"one", "two", "three"}; // 数组的元素是string对象
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}; // ia是一个含有10个整数的数纽
auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素
ia2 = 42; // error:ia2是一个指针,不能用int值给指针赋值
auto ia2(&ia[0]); // 显然ia2的类型是int*
当使用decltype
关键字不会发生这种转换,直接返回数组类型。
// ia3是一个含有10个整数的数组
decltype(ia) ia3 = {
0,1,2,3,4,5,6,7,8,9};
ia3 = p; // error:不能用整型指针给数组赋值
ia3[4] = i; // ok:把i的值赋给ia3的一个元素
为了更加便捷的获得数组的尾后指针(指向数组尾元素之后的那个并不存在的元素的地址),C++11引入了begin
和end
两个函数,这两个函数与容器中的两个同名成员功能类似,不过数组不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:
int ia[] = {
0,1,2,3,4,5,6,7,8,9}; // ia是一个含有10个整数的数组
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向arr尾元素的下一位置的指针
注意:尾后指针不能执行解引用和递增操作
constexpr size_t sz = 5;
int arr[sz] = {
1, 2, 3, 4, 5};
int *ip = arr; // 等价于int*ip = &arr[O]
int *p2 = ip + 4; // ip2指向arr的尾元素arr[4]
auto n = end(arr) - begin(arr); // n的值是5 ,也就是arr中元素的数量
C风格字符串
字符串字面值是一种通用结构的示例,这种结构即是C++由C继承而来的C风格字符串。按此习惯书写的字符串存放在字符数组中并以空字符结束(即字符串最后一个字符后面跟着一个\0
)。
C标准库String函数
C语言库提供了一组用于操作C风格字符串函数,放在cstring
头文件中,cstring
是C语言头文件string.h
的C++版本。
上述函数并不负责验证其字符串参数。传入此类函数的指针必须指向以空字符作为结束的数组。
char ca[] = {
'C', '+', '+' }; //不以空字符结束
cout << strlen(ca) << endl; //严重错误:ca没有以空字符结束
对大多数程序来说,使用标准库string
要比使用C风格字符串更加安全和高效。
注意要比较两个C风格字符串需要调用strcmp
函数,而不是像标准库string
一样直接用<
>
运算符。
与旧代码的接口
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:
- 因为允许使用字符串字面值来初始化
string
对象,因此允许使用以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值。 - 在 string 对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。
- 在 string 对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧运算对象。
上述性质反过来则不成立,如果程序的某处需要一个C风格字符串,无法直接用string
对象来替代。例如不能用string对象直接初始化指向字符的指针。为了完成该功能,可使用string
类提供的c_str
成员函数返回一个类型为const char*
的指针。但是要注意无法保证c_str
返回的指针一直有效,如果后续的操作改变了string
对象的值,那么返回的指针会失效。因此想要一直使用c_str
返回的指针的话最好重新拷贝一份。
string s("Hello World"); // s的内容是Hello World
char *str = s; // error: 不能用string对象初始化char*
const char *str = s.c_str();// ok
可以用数组来初始化vector
对象,只需要指明拷贝区域的首地址元素和尾后地址即可。
int int_arr[] = {
0, 1, 2, 3, 4, 5};
// ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));
多维数组
多维数组的初始化
int ia[3][4] =
{
// 三个元素,每个元素都是大小为3的数组
{
0, 1, 2, 3}, // 第1行的初始值
{
4, 5, 6, 7}, // 第2行的初始值
{
8, 9, 10, 11} // 第3行的初始值
};
// 没有标识每行的花括号,与之前的初始化语句是等价的
int ib[3][4] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显式地初始化每行的首元素
int ic[3][4] = {
{
0 }, {
4 }, {
8 }};
// 显式地初始化第1行,其他元素执行值初始化
int id[3][4] = {
0, 3, 6, 9};
多维数组的下标引用
如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;
如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组。
int ia[3][4] =
{
// 三个元素,每个元素都是大小为3的数组
{
0, 1, 2, 3}, // 第1行的初始值
{
4, 5, 6, 7}, // 第2行的初始值
{
8, 9, 10, 11} // 第3行的初始值
};
// 用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1]; // 把row绑定到ia的第二个4元素数组上
使用范围for语句处理多维数组
size_t cnt = 0;
for(auto &row: ia)
for(auto &col: row){
col = cnt;
++cnt;
}
第一个for
循环遍历ia
的所有元素,这些元素是大小为4的数组,即row
的类型为含有4个整数的数组的引用。第二个for
循环遍历那些4元素数组中的某一个,因此col
的类型为整数的引用。之所以声明成引用类型有两个原因:第一个原因是因为要改变元素的值;第二个原因是避免数组被自动转换为指针,这样第二个for循环就不合法了,如下所示:
for (auto row : ia)
for (auto col : row)
//这样得到的 row 就是 int* 类型,而之后的内层循环则试图在一个 int* 内遍历,程序将无法通过编译。
因此使用范围 for 语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。
指针与多维数组
定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组。
多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数纽
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素
C++11标准下可以通过使用 auto
和 decltype
来省略复杂的指针定义:
// 输出ia中每个元素的值,每个内层数组各占一行
// p指向含有4个整数的数组
for (auto p = ia; p != ia + 3; ++p)
{
// q指向4个整数数组的首元素,也就是说,q指向一个整数
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
使用标准库函数 begin
和 end
也能实现同样的功能,而且看起来更简洁一些。
// p指向ia的第一个数组
for (auto p = begin(ia); p != end(ia); ++p)
{
// q指向内层数组的首元素
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' '; // 输出q所指的整数值
cout << endl;
}
类型别名简化多维数组的指针
using int_array = int[4]; //新标准下类型别名的声明
typedef int int_array[4]; //等价的typedef声明
//输出ia中每个元素的值,每个内层数组各占一行
for(int_array *p = ia; p != ia+3; ++p){
for(int *q = *p; q != *p+4; ++q)
cout << *q << ' ';
cout << endl;
}
练习
请说明 string
类的输入运算符和 getline
函数分别是如何处理空白字符的。
标准库 string
的输入运算符自动忽略字符串开头的空白(包括空格符、换行符、制表符等),从第一个真正的字符开始读取,直至遇到下一处空白(包括空格符、换行符、制表符等)为止。
如果希望在最终的字符串中保存输入时的空白符,应该使用 getline
函数代替原来的 >>
运算符,getline
从给定个的输入流中读取数据,直到遇到换行符为止,此时换行符也被读取进来,但是并不存储在最后的字符串中。
读入一组整数并把他们存入一个vector对象,将每对相邻整数的和输出出来。改写你的程序,这次要求先输出第一个和最后一个元素的和,接着输出第二个和倒数第二个元素的和,以此类推。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vInt;
cout << "Please input a series of numbers"<< endl;
int value;
while(cin>>value){
vInt.push_back(value);
}
if(vInt.empty()){
cout << "No numbers" <<endl;
}else{
if(vInt.size()==1){
cout << vInt[0] << endl;
}else{
for(decltype(vInt.size()) i=0; i<vInt.size()-1; i++){
cout << vInt[i] + vInt[i+1] << " ";
}
cout << endl;
}
}
system("pause");
return 0;
}
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vInt;
cout << "Please input a series of numbers"<< endl;
int value;
while(cin>>value){
vInt.push_back(value);
}
if(vInt.empty()){
cout << "No numbers" <<endl;
}else{
if(vInt.size()==1){
cout << vInt[0] << endl;
}else{
decltype(vInt.size()) left, right;
left = 0;
right = vInt.size()-1;
while(left<right){
cout << vInt[left]+vInt[right] << " ";
left++;
right--;
}
if(left==right){
cout << vInt[left];
}
cout << endl;
}
}
system("pause");
return 0;
}
下面的程序是何含义,程序的输出结果是什么?
const char ca[] = {
'h', 'e', 'l', 'l', 'o' };
const char *cp = ca;
while (*cp) {
cout << *cp << endl;
++cp;
}
该程序的愿意是输出 ca
中存储的5个字符,每个字符占一行,但实际的执行效果无法符合预期。因为以列表初始化方式复制的c风格字符串与以字符串字面值赋值的有所区别,后者会在字符串最后额外增加一个空字符以示字符串的结束,而前者不会这样做。ca 的5个字符全都输出后,并没有遇到预期的空字符.
应该将程序修改为:
const char ca[] = {
'h', 'e', 'l', 'l', 'o', '\0' };
const char *cp = ca;
while (*cp) {
cout << *cp << endl;
++cp;
}
或者
const char ca[] = "hello";
const char *cp = ca;
while (*cp) {
cout << *cp << endl;
++cp;
}
编写一段程序,用整型数组初始化一个vector对象。
通过begin
和end
来获取数组的范围
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
using namespace std;
int main()
{
const int as = 10; //the size of a
int a[as];
srand((unsigned) time (NULL));
cout << "The contents of the array are" << endl;
for(auto &item : a)
{
item = rand() % 50;
cout << item << " ";
}
cout << endl;
vector<int> vInt(begin(a), end(a));
cout << "The contents of the vector are" << endl;
for(auto val : vInt)
{
cout << val << " ";
}
cout << endl;
system("pause");
return 0;
}
编写3个不同版本的程序,令其均能输出二维数组的元素。版本1使用范围for语句管理迭代过程;版本2和版本3都使用普通for语句,其中版本2要求使用下标运算符,版本3要求使用指针。此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto关键字和decltype关键字。
#include <iostream>
using namespace std;
using const_array4 = const int[4];
int main()
{
int ia[3][4] =
{
{
0, 1, 2, 3},
{
4, 5, 6, 7},
{
8, 9, 10, 11}
};
for (const int(&row)[4] : ia)
{
for (int col : row)
cout << col << " ";
cout << endl;
}
cout << "--------------------" << endl;
for (int i = 0; i != 3; i++)
{
for (int j = 0; j != 4; j++)
cout << ia[i][j] << " ";
cout << endl;
}
cout << "--------------------" << endl;
for (int(*p)[4] = ia; p != ia + 3; p++)
{
for (int *q = *p; q != *p + 4; q++)
cout << *q << " ";
cout << endl;
}
system("pause");
return 0;
}
改写上一个练习中的程序,分别使用类型别名和auto来代替循环控制变量的类型。
#include <iostream>
using namespace std;
using int_array4 = int[4];
int main()
{
int ia[3][4] =
{
{
0, 1, 2, 3},
{
4, 5, 6, 7},
{
8, 9, 10, 11}
};
for (const int_array4 &row : ia)
{
for (int col : row)
cout << col << " ";
cout << endl;
}
cout << "--------------------" << endl;
for (int i = 0; i != 3; i++)
{
for (int j = 0; j != 4; j++)
cout << ia[i][j] << " ";
cout << endl;
}
cout << "--------------------" << endl;
for (int_array4 *p = ia; p != ia + 3; p++)
{
for (int *q = *p; q != *p + 4; q++)
cout << *q << " ";
cout << endl;
}
system("pause");
return 0;
}
#include <iostream>
using namespace std;
using int_array4 = int[4];
int main()
{
int ia[3][4] =
{
{
0, 1, 2, 3},
{
4, 5, 6, 7},
{
8, 9, 10, 11}
};
for (const int_array4 &row : ia)
{
for (int col : row)
cout << col << " ";
cout << endl;
}
cout << "--------------------" << endl;
for (auto i = 0; i != 3; i++)
{
for (auto j = 0; j != 4; j++)
cout << ia[i][j] << " ";
cout << endl;
}
cout << "--------------------" << endl;
for (auto p = ia; p != ia + 3; p++)
{
for (int *q = *p; q != *p + 4; q++)
cout << *q << " ";
cout << endl;
}
system("pause");
return 0;
}
还没有评论,来说两句吧...