0%

C++ Primer 第三章

字符串、向量和数组

​ 第2章介绍的内置类型是由C++语言直接定义的。这些类型,比如数字和字符,体现了大多数计算机硬件本身具备的能力。标准库定义了另外一组具有更高级性质的类型,它们尚未直接实现到计算机硬件中。
​ 本章将介绍两种最重要的标准库类型: string 和 vector。string表示可变长的字符序列,vector存放的是某种给定类型对象的可变长序列。本章还将介绍内置数组类型,和其他内置类型一样,数组的实现与硬件密切相关。因此相较于标准库类型string和 vector,数组在灵活性上稍显不足。

命名空间的using声明

有了 using声明就无须专门的前缀(形如std :)也能使用所需的名字。声明如下:
using namespace::name ;
例如:using std::cin; using std::cout;

头文件不应包含using声明,以免产生名字的冲突。

标准库类型string

使用前需包含

1
2
#include <string>
using std::string;

定义和初始化

1
2
3
4
5
6
7
string sl;//默认初始化,s1是一个空字符串
string s2 = s1;//s2是s1的副本
string s3(s1);//与上面相同
string s4 = "hiya";//s3是该字符串字面值的副本
string s5("hiya");//与上相同
string s6(10,'c' );//s4的内容是cccccccccc
string s7 = string(10, 'c');//与上相同

直接初始化与拷贝初始化

区分:使用等号的都是拷贝初始化,其余是直接初始化。

string对象上的操作

读写string对象

1
2
3
4
5
6
1nt main(
string s;//空字符串
cin >> s;//将string 对象读入s,遇到空白停止
cout << s <<endl;//输出s
return 0;
}

输入过程中自动忽略开头空白,直到下一处空白,如:“ Hello World! ”则只会输出”Hello”,输出结果没有任何空格。

1
2
3
string sl, s2;
cin >> sl >> s2; //把第一个输入读到s1中,第二个输入读到s2中
cout<< s1 << s2 <<endl;//输出两个string对象

这样输入上面的语句则会输出”HelloWorld!”。

读取未知数量的string对象

1
2
3
4
5
6
7
int main (){
string word;
while (cin >> word){//反复读取,直至到达文件末尾
cout << word << endl;//逐个输出单词,每个单词后面紧跟一个换行
return 0;
}
}

在遇到文件结束符或非法输入就结束了

getline读取一整行

getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。

1
2
3
4
5
6
7
int main ( ){
string line;
//每次读入一整行,直至到达文件末尾
while (getline(cin,line) )
cout<< line << endl;
return 0;
}

string的empty和size操作

  • empty函数根据string对象是否为空返回对应的布尔值。

  • size函数返回string对象的长度,其类型为string::size_type类型,它是一个无符号类型的值,所以尽量避免size()和int混用。

比较string对象

  • 两个string对象长度相同,所包含字符必须一模一样才算相等。
  • 长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,就说较短string对象小于较长string对象。
  • 如果两个string 对象在某些对应的位置上不一致,则string对象比较的结果
    其实是string对象中第一对相异字符比较的结果。

为string对象赋值

1
2
string st1(10, 'c'), st2;
st1 = st2;

两个string对象相加

1
2
3
string s1 = "hello, ", s2 = "world\n";
string s3 = s1 + s2; // s3 = "hello, world\n"
s1 += s2; //与上面等价

字面值和string对象相加

1
2
string s4 = s1 + ", " + s2; //可以,依次运算中至少保证有一个string对象
string s5 = "hello" + "world"//错误,不允许两个字面值直接相加

处理string对象中的字符

函数名称 返回值
isalnum() 如果参数是字母数字,即字母或者数字,函数返回true

isalpha() 如果参数是字母,函数返回true

iscntrl() 如果参数是控制字符,函数返回true

isdigit() 如果参数是数字(0-9),函数返回true
isgraph() 如果参数是除空格之外的打印字符,函数返回true

islower() 如果参数是小写字母,函数返回true

isprint() 如果参数是打印字符(包括空格),函数返回true

ispunct() 如果参数是标点符号,函数返回true

isspace() 如果参数是标准空白字符,如空格、换行符、水平或垂直制表符,函数返回true

isupper() 如果参数是大写字母,函数返回true

isxdigit() 如果参数是十六进制数字,即0-9、a-f、A-F,函数返回true

tolower() 如果参数是大写字符,返回其小写,否则返回该参数

toupper() 如果参数是小写字符,返回其大写,否则返回该参数

C++版本的标准库头文件为形如cname,而C头文件形如name.h,这里建议使用C++版本。

for处理每个字符

1
2
3
4
string str ( "some string" );
//每行输出str中的一个字符。
for (auto c : str) //对于str中的每个字符
cout << c << endl; //输出当前字符,后面紧跟一个换行符

如果需要改变str中的字符,则在c前面加上&。

处理部分字符

1
2
3
//依次处理s中的字符直至我们处理完全部字符或者遇到一个空白
for (decltype(s.size()) index =0;index != s.size ( ) && !isspace(s [index]); ++index)
s[index] = toupper(s[index]);//将当前字符改成大写形式

这里主要注意一点,index必须大于等于0,小于size()。

标准库类型vector

头文件:

1
2
#include <vector>
using std::vector;

vector是一个类模板,模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为**实例化(instantiation)**,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。

例如vector<int> ivec; vector<Sales_item> Sales_vec;

定义和初始化vector对象

1
2
3
4
5
6
7
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...}

拷贝构造(类型必须相同)

1
2
3
4
5
vector<int> ivec;				//初始状态为空
//在此处给ivec添加一些值
vector<int> ivec2 (ivec); //把ivec的元素拷贝给ivec2
vector<int> ivec3 = ivec; //把ivec的元素拷贝给ivec3
vector<string> svec(ivec2); //错误: svec的元素是string对象,不是int

列表初始化vector对象

1
2
3
vector<string> articles = { "a", "an" , "the" };
vector<string> v1 { "a", "an", "the" } ; //列表初始化
vector<string> v2( "a", "an", "the") ; //错误

值初始化

使用vector<T> v(n);需注意 T 支不支持默认初始化。

vector<int> vi = 10;//错误:必须使用直接初始化的形式指定向量大小

注意区分花括号和圆括号

1
2
3
4
vector<int> v1(10) ;		// v1有10个元素,每个的值都是0
vector<int> v2{10}; // v2有1个元素,该元素的值是10
vector<int> v3(101); // v3有10个元素,每个的值都是1
vector<int> v4 {10,1}; // v4有2个元素,值分别是10和1
1
2
3
4
vector<string> v5 { "hi"};//列表初始化:v5有一个元素
vector<string> v6("hi");//错误:不能使用字符串字面值构建vector对象
vector<string> V7 {10};// v7有10个默认初始化的元素
vector<string> v8{10,"hi" };// v8有10个值为"hi"的元素

上面的代码中,只有v5为列表初始化,使用花括号时,若提供的对象不可以作为对象的初始值,则编译器会尝试默认值初始化。

向vector对象中添加元素

1
2
3
vector<int> v2;//空vector对象
for (int i =0; i != 100; ++i)
v2.push_back(i); //依次把整数值放到v2尾端//循环结束后v2有100个元素,值从0到99

其他vector操作

1
2
3
4
5
6
7
8
9
v.empty () 				//如果v不含有任何元素,返回真;否则返回假
v.size () //返回v中元素的个数
v.push_back (t) //向v的尾端添加一个值为t的元素
v [n] //返回v中第n个位置上元素的引用
v1 = v2 //用v2中元素的铂贝替换v1中的元素
vl = {a,b,c... } //用列表中元素的拷贝替换v1中的元素
vl == v2 //v1和 v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
v1 != v2
<,<=,>,>= //顾名思义,以字典顺序进行比较

vector容器的size类型一定要包含元素类型

1
2
veetor<int>: :size_type		//正确
vector::size type //错误

vector容器的比较大小参照string。

迭代器

使用迭代器

1
2
3
//由编译器决定b和e的类型
// b表示v的第一个元素,e表示v尾元素的下一位置
auto b = v.begin(), e = v.end(); //b 和e的类型相同

迭代器运算符

1
2
3
4
5
6
* iter				//返回迭代器iter所指兀素的引用
iter->mem //解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter //令iter指示容器中的下一个元素
--iter //令iter指示容器中的上一个元素
iter1 == iter2 //判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元
iter1 != iter2 //素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等

迭代器的移动

采用++或–操作改变其位置

迭代器用法:

1
2
3
//依次处理s 的字符直至我们处理完全部字符或者遇到空白
for (auto it = s.begin(); it != s.end ( ) && !isspace (*it); ++it)
*it = toupper(*it); //将当前字符改成大写形式

迭代器类型

1
2
3
4
vector<int> : :iterator it; // it能读写vector<int>的元素
string: :iterator it2; // it2能读写string对象中的字符
vector<int> : : const_iterator it3; // it3只能读元素,不能写元素
string : :const_iterator it4; //it4只能读字符,不能写字符

begin和end

1
2
3
4
5
vector<int> v;
const vector<int> cv;
auto itl = v.begin(); //it1的类型是vector<int> : :iterator
auto it2 = cv.begin(); //it2的类型是vector<int>::const_iterator
auto it3 = v.cbegin(); //it3的类型是vector<int>::const iterator

如果对象只需读操作而无须写操作的话最好使用常量类型(比如 const_iterator)。为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend,无论对象是什么,返回值都是const_iterator。

结合解引用和成员访问操作

1
2
3
( *it) .empty()	//解引用it,然后调用结果对象的empty成员
*it.empty() //错误:试图访问it的名为empty的成员,但it是个迭代器,没有empty成员
it->empty() //此操作与上面操作相同

为了简化上述表达式,C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it) .mem表达的意思相同。

迭代器失效

迭代器运算

迭代器可以进行+- 等运算使它一次移动多个位置,大于小于操作则判断迭代器的相对位置,但必须在同一个容器里。

迭代器相减得到所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type 的带符号整型数。string 和vector都定义了difference_type ,因为这个距离可正可负,所以difference_type是带符号类型的。

使用迭代器运算

1
2
3
4
5
6
7
8
9
10
11
12
// text必须是有序的
// beg 和end表示我们搜索的范围
auto beg = text.begin ( ) , end = text.end ( ) ;
auto mid = text.begin () + (end - beg)/2; // 初始状态下的中间点
//当还有元素尚未检查并且我们还没有找到sought时执行循环
while (mid != end & & *mid != sought) {
if (sought< *mid) //我们要找的元素在前半部分吗?
end = mid; //如果是,调整搜索范围使得忽略掉后半部分
else //我们要找的元素在后半部分
beg = mid + 1; //在mid之后寻找
mid = beg + (end - beg)/ 2; //新的中间点
}

数组

与vector对比:

  • 数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。
  • 与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。

定义和初始化

1
2
3
4
5
6
unsigned cnt = 42;			//不是常量表达式
constexpr unsigned sz = 42; //常量表达式,关于constexpr,参见2.4.4节(第59页)
int arr[10] ; //含有10个整数的数组
int *parr[sz]; //含有42个整型指针的数组
string bad[cnt ] ; //错误:cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确;否则错误

定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和 vector一样,数组的元素应为对象,因此不存在引用的数组。

显示初始化数组元素

1
2
3
4
5
6
const unsigned sz = 3 ;
int ia1[sz] = {012]; //含有3个元素的数组,元素值分别是0,1,2
int a2[]= {0,1,2}; //若不指定维度,则根据初始值计算,这里是维度是3的数组
int a3[5] = {012}; //等价于a3[] ={0,1,2,0,0}
string a4 [3] = { "hi","bye" } ; //等价于a4 [] = { "hi","bye","" )
int a5[2]= {0,1,2}; //错误:初始值过多

字符数组的特殊性

1
2
3
4
char al[]= {'c' , '+', '+'};		//列表初始化,没有空字符
char a2[]= {'c', '+‘, ’+‘, ’\0‘}; //列表初始化,含有显式的空字符
char a3[]= "C++"; //自动添加表示字符串结束的空字符
const char a4 [6]="Daniel"; //错误:没有空间可存放空字符!

拷贝与赋值

1
2
3
int a[] = {0,12};			   //含有3个整数的数组
int a2[]= a; //错误:不允许使用一个数组初始化另一个数组
a2 = a; //错误:不能把一个数组直接赋值给另一个数组

一些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。

复杂数组的声明

1
2
3
4
5
int *ptrs[10] ;			//ptrs是含有10个整型指针的数组
int &refs[10]=/* ?*/; //错误:不存在引用的数组
int (*Parray)[10]= &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
int * (&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针

这里重点在于顺序,

  • 对于无括号情况,从右往左,例如ptrs,我们先看到的是[10],表明这是一个数组。
  • 对于右括号,从内向外,例如parray,先看到是一个指针,表明是一个指针,它指向了数组。
  • 对于arry,先从内向外,再从右往左。

访问数组元素

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在cstddef头文件中定义了size_t类型,这个文件是C标准库 stddef.h头文件的C++语言版本。

1
2
3
for (auto i : scores)		//对于scores中的每个计数值
cout<< i <<" "; //输出当前的计数值
cout << endl;

指针和数组

在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。

1
2
3
4
5
6
7
8
9
int ia[] = {0,1,2,3,4,5,6,7,8,9};	// ia是一个含有10个整数的数组
auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素
ia2 =42; //错误:ia2是一个指针,不能用int值给指针赋值
//尽管ia是由10个整数构成的数组,但当使用ia作为初始值时,编译器实际执行的初始化过程类似于下面的形式:
auto ia2 ( &ia[0]); //显然ia2的类型是int*
//当使用decltype关键字时上述转换不会发生,decltype (ia)返回的类型是由10个整数构成的数组:
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9} ;
ia3 = p; //错误:不能用整型指针给数组赋值
ia3 [4] = i; //正确:把i的值赋给ia3的一个元素

指针也是迭代器

指针可以做与迭代器同样的操作(我认为本质上没有区别),尾指针可以通过int *e = &arr[arr.len];的方法获取。

标准函数begin和end

1
2
3
int ia[ ] = {0,1,2,3,4,5,6,7,8,9); 	//ia是一个含有10个整数的数组
int *beg = begin (ia) ; //指向ia首元素的指针
int * last = end (ia) ; //指向arr尾元素的下一位置的指针

C++11新标准引入了两个名为begin和 end 的函数。这两个函数与容器中的两个同名成员,功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:

指针运算

这里与容器中的迭代器除了两个指针相减是ptrdiff_t类型基本一致,该类型也是带符号类型。

指针运算同样适用于空指针和所指对象并非数组的指针。在后一种情况下,两个指针必须指向同一个对象或该对象的下一位置。如果p是空指针,允许给p加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果当然是0。

1
2
3
int ia[] = {5,4,2,2,1,3}
int last1 = *(ia + 4); //为ia[4]的值
int last2 = *ia +4 //含义完全不同,

下标和指针

只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算:

1
2
3
4
5
6
int i = ia[2];				// ia转换成指向数组首元素的指针ll 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]表示的那个元素

数组下标类型是带符号类型,这与vector和string不一致。

C风格字符串

风险大,不推荐使用,故暂时跳过

多维数组

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用多维数组大有益处。

多维数组初始化

1
2
3
4
5
6
7
8
9
10
11
int ia[ 3][4]= {			 //三个元素,每个元素都是大小为4的数组
{ 0,123 } , //第1行的初始值
{ 4,567), //第2行的初始值
{ 8,91011} //第3行的初始值
};
//没有标识每行的花括号,与之前的初始化语句是等价的
int ia[3][4]= {0,1,2,3,4,5,6,7,8,9,10,11}
//显式地初始化每行的首元素
int ia[3][4] = {{O }, { 4 }, { 8 }};
//显式地初始化第1行,其他元素执行值初始化0
int ix[3][4]= {0,369};

多维数组的下标引用

1
2
3
//用arr的首元素为ia最后一行的最后一个元素赋值
ia [2][3] = arr[0][0][0];
int (&row)[4] = ia[1]; //把row绑定到ia的第二个4元素数组上

for循环遍历

1
2
3
4
5
6
size_t cnt = 0;
for (auto &row : ia) //对于外层数组的每一个元素
for (auto &col : row){ //对于内层数组的每一个元素
col = cnt; //将下一个值赋给该元素
++cnt; //将cnt 加1
}

指针和多维数组

1
2
int*ip[4];			//整型指针的数组
int (*ip)[4]; //指向含有4个整数的数组

声明指针注意区分以上区别,C++ 11推荐使用auto或者decltype也可避免加指针类型。

1
2
3
4
5
6
7
8
9
10
11
int ia [ 3][4];			//大小为3的数组,每个元素是含有4个整数的数组
int(*p)[ 4]= ia; //p指向含有4个整数的数组
p= &ia [2]; //p指向ia的尾元素
//输出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;
}

auto p = begin(ia)以可以更加简洁。

类型别名简化多维数组

1
2
3
4
5
6
7
8
using int_array = int [4];	//新标准下类型别名的声明,参见2.5.1节(第60页)
typedef int int_array[4]; //等价的typedef声明,参见2.5.1节(第60 页)
//输出ia中每个元素的值,每个内层数组各占一行
for (int_array *p = ia; p != ia + 3; ++p){
for (int *q= *p; q != *p + 4; ++q)
cout<< *q<<' ';
cout <<endl;
}

程序将类型“4个整数组成的数组”命名为 int_array,用类型名int_array定义外层循环的控制变量让程序显得简洁明了。