第三章主要讲这么五个概念:
1.using声明,我知道挺多同学写代码练手都要在源文件前几句直接加using namespace std;然而using语句并不是什么情况都这么使用的,稍后我们将会看到详细的用法。
2.标准库类型string,和C的字符数组有区别的string,到底是怎么个构造,这章将会讲述。
3.标准库类型vector,vector和数组区别很大,这里将会提到,并引入一个“容器”的重要概念。
4.迭代器,迭代器用来代替下标这种传统方式访问容器或一些支持迭代器的类型。
5.数组和多维数组,经典概念。
命名空间的using声明(P75,3.1)
尽管我们可以在各种文件里都使用using namespace std;
或者using std::endl;
这种语句,但是,在头文件包含命名空间可能产生各种意外。因此,头文件不应包含using声明。
标准库类型string
本节介绍最常用的操作,9.5节将介绍另外的。
定义和初始化string对象(P76,3.2.1)
|
|
以下几种初始化语句被string支持:
|
|
最后,其实string s5={"value"}
和string s3="value"
一样,也是合法的。不过大括号初始化是严格检测匹配的,比如int a={3.5};
就是错误的。
string支持的操作(P77,3.2.2)
1.输入流中获取字符串
首先要强调是cin>>string
的操作,这种操作就是从输入流中读字符串,值得注意的是这个过程会忽略掉开头输入的各种空白(我们说空白时是在说 空格,换行符,制表符),读取输入流直到遇到字符后的第一个空白为止。
另一种getline(cin,string);
的操作(P78) 则可以读一行,也就是读入输入流的数据(包括空格,制表符),直到遇到换行符为止,这里输入流中的换行符本身已经被读过了,但是字符串里不保存这个换行符。下次再从输入流里读什么数据至少也要从这个换行符后面对输入流进行操作了。
2.string::type_size (P79)
为了更抽象,脱离机器特性,调用每个string对象的size成员函数,返回值都是一个string::type_size
类型,这个类型拥有无符号整形数的一些性质。在string对下标的支持中,[ ]中的数字也会被转换为string::type_size
类型。这里要强调的是string::type_size
是一个无符号类型。使用这个类型和int型这种有符号的类型一起进行计算可能出现一些错误。
如果一个表达式中已经有了
size()
函数,就不要再使用int
了,这样可以避免混用int
和unsigned
可能带来的问题。
3.string对象与字符串字面值相加 (P80)
字符串字面值是字符数组类型,字符串字面值和string类型的对象在一起计算时会被自动转换为string类型。
4.其他支持的操作
包括下标运算符[ ]、重载的+、==、!=、<、>、<=、>=。
处理每个字符的头文件cctype(P82, 3.2.3)
我们可以通过引用头文件<cctype>
的形式处理每一个字符。这个头文件包含很多方便处理字符的函数。列举如下:
- isalnum(c); //当c是字母或者数字时为真
- isalpha(c); //当c是字母时为真
- iscntrl(c); //当c是控制字符时为真
- isdigit(c); //当c是数字时为真
- isgraph(c); //当c不是空格但是可打印时为真
- islower(c); //当c是小写字母为真
- isprint(c); //当c可打印时为真
- isupper(c); //当c是大写字符时为真
- isxdigit(c); //当c是16位数字时为真
- ispunct(c); //当c是标点符号时为真(一个字符除了控制字符,字母,数字,可打印空白就是标点符号)
- isspace(c); //当c是空白时为真(空白包括空格,横向/纵向制表符,回车符,换行符,进纸符)
- tolower(c); //把大写字符转换为小写字符,本来就是小写字符的不变,返回转换后的字符
- toupper(c); //把小写字符转换为大写字符,本来就是大写字符的不变,返回转换后的字符
建议:使用C++版本的C标准库头文件。
C++标准库兼容了C语言的标准库。C语言的头文件形如name.h
,C++则会将这些文件命名为cname
。也就是去掉了.h
后缀,在文件名前添加了字母c。
范围for(range for)语句(P82,3.2.3)
范围for语句用于遍历元素。形如:
|
|
首次初始化,变量a的值会被初始化为对象b序列中的第一个元素,迭代之后每次访问下一个元素,直到序列被完全访问结束。
可以使用auto &a
的方式声明变量a,使变量绑定到具体的序列元素上,从而进行更改。如在for(auto a : str){}
中,每次把a初始化的行为实质上是使a获得str每个元素的副本(拷贝),而for(auto &a : str){}
这样的语句则使a成为了str对应的每个元素的”别名”,从而可以修改str。
使用范围for循环遍历多维数组,为了不手动打类名,也为了防止外层数组的名被auto类型转化成指针,要在对外层数组的访问上都加上&绑定。
范围for有空补,未详看。
标准库类型vector(P86, 3.3)
类模板、容器和实例化(P87, 3.3)
当我们在C++里面谈论容器这个概念时,我们应该知道容器是用来存储和组织一类特定对象的集合。下面提到的标准库类型vector
,就是一个容器。
类模板一般用于按照模板规定好的规则生成不同的类。我们无需很麻烦的一个一个写类的定义,只需使用模板,给出指定的少量信息,类模板就会帮助我们自动生成一个我们可以直接使用的类。vector也是一个类模板。
通过类模板创建类的过程,或者通过类型创建对象的过程,就叫做实例化。
定义和初始化vector对象(P87,3.3.1)
与string的定义和初始化一样,我们也可以使用多种方式定义和初始化一个vector对象。
以下几种初始化语句被vector支持:
|
|
当我们使用圆括号()
初始化对象时,IDE会认为我们在通过语句“构建”
(constract)这个对象 ;当我们使用花括号{ }
初始化对象时,IDE会认为我们在列表初始化(list initialize)对象。
当我们使用等号=初始化对象时,我们就执行了“拷贝初始化”;当我们不使用=初始化对象时,我们就执行了“直接初始化”。
但是当我们在花括号里面给一个不符合对象类型的值,IDE就会认为我们正在构建而非初始化对象,一个体现就是:
这个语句中,10不能转换为string,因此被系统理解为“这个string容器里有10个元素”。
当然,像vector <string>s1={10};
这样的语句是错误的,因为=就应该是拷贝初始化了,然而10并不能够被转化为string因此也无法赋值。
向vector对象添加元素(P90, 3.3.2)
向vector对象添加元素:push_back。
循环体内部包含向vector对象添加元素时,则不能使用范围for循环。
vector支持的操作(P91, 3.3.3)
向容器的后面添加元素:已存在
vector<T> v;
,可以使用v.push_back(vector<T> a)
的方式在集合v的尾部添加元素。empty
和size
函数成员:已存在vector<T> v;
,可以使用v.empty()
的方式判断v是否为空,可以使用v.size()
的方式返回v的大小。重载的运算符:vector支持的运算符包括下标运算符
[ ]
、重载的+、==、!=、<、>、<=、>=
。这一点和string
类似。
不能用下标形式添加元素。
只能对确知已存在的元素执行下标操作!
迭代器介绍(3.4)
为了访问容器的元素(有些容器可能不支持下标运算符),因此C++提供了迭代器(iterator)这个概念来访问容器中的指定元素。
支持迭代器的类都会提供名为begin和end的函数成员来供我们获取迭代器。如已定义vector<int> i1(10);
,这时使用auto ben=i1.begin();
这个语句获取指向第一个字符的迭代器,使用auto end=i1.end();
获取指向i1容器最后一个元素的下一个元素的迭代器,术语“尾后迭代器”。两个迭代器可以相减,但是两个迭代器相加后的行为是未定义的。
当使用vector <int>
创建类时,这个类的命名空间就是vector <int>
,命名空间中的迭代器类型写作vector<int>::iterator
。因为这个叫做"vector<int>::iterator"
的迭代器类型名太长了也不好记,这里我们使用auto
推导这个类型。用成员函数cbegin
和cend
可以推导出底层const迭代器,就是这个迭代器对迭代器指向的内容只读不写。第6章会详细说明。
迭代器 对 迭代器指向的容器内容 可以像 指针 对 指针指向的数组元素一样使用。
虽然数组不是直接支持迭代器的类型,但是可以引入<iterator>
头文件,使用begin(数组名)
和end(数组名)
的方式获得指向数组第一个元素和尾后第一个元素的指针。因为大多数容器不支持下标运算符,所以使用迭代器访问容器等结构中的元素是最好的方法。
数组(3.5, P101)
一维数组的定义和初始化(3.5.1,P102)
一维数组声明形式:类型名 数组名[一个常量]
。比如int a[15];
这里这个数组的名字是a,有15个元素,每个元素都是int型的。再比如int *a[15];
这里a数组的15个元素都是int *
型的,即指向int的指针,这样的指针有15个,构成了一个数组。虽然有指针数组,但是不存在元素都是引用类型的数组。
一维数组的初始化方式就是花括号初始化,形如int a[n]={1,2,3};
,大括号里面的内容就是初始化列表,n为数组大小,可以缺省,缺省时数组长度由初始化列表的元素个数决定。当初始化列表的值的个数比数组长度小,数组剩下的元素被初始化为默认的值,比如对于有10个元素的int型数组,如果只给出第一个元素的值,后几个元素将被初始化为0。
当我们声明int a[]
的时候代表通过数组名a访问这个数组。
我们也可以定义指向数组的指针和指向数组的引用来间接访问这个数组。
已有int arr[10];
的情况下,int (*ptr) [10]=&arr;
这条语句可以使指针ptr指向arr这整个数组。int (&ref)[10]=arr;
则会使ref作为整个arr数组的引用。int *(&ref)[10]=arr;
这个语句则是说ref是arr的引用,这个被引用的数组的类型是指针数组。
auto a=一个数组名
,a的类型将会是这个指针,指针指向的类型就是数组元素的类型。
用decltype(一个数组名) a;
这样的形式,a将会是和数组名属性一致的数组。
在大部分运算中,数组名都会被转化成相应的指针类型。如*(ai+4)
中,数组名ai是指向整个数组首元素的指针,这个指针+4就是向右侧移动4位,指针原来指向第一个元素,移动4位就指向了数组中的第五个元素。然后指向的值就是ai数组第五个元素的值,相当于ai[4]。
用数组初始化vector对象和用string对象赋值字符数组(3.5.5, P111)
作为与旧代码的接口,C++提供了方便的把数组转化为vector对象的方法。在声明vector对象时,我们可以通过迭代器用一个数组初始化vector。
在已经存在int oldarray[10];
的情况下,声明的语句形如:vector<int> arr( begin(oldarray) , end(oldarray) );
可以把arr初始化为oldarray。begin和end这两个函数在<iterator>
头文件里,作用是返回数组的首元素/尾后指针。这种初始化接受两个参数:拷贝开始部分指针和结束部分的指针。
我们也可以写形如int arr[10]={0}; vector <int> newarr( arr+1 , arr+6 );
这种方式拷贝数组arr的第2~第5号元素,并用它们初始化newarr。
类似地,我们可以通过string a("23333333\n"); const char *b=a.c_str();
这样的语句使string型的a被赋值给字符数组指针b。返回结果是const是为了确保我们不会通过这个指针改动返回的字符数组的值。
建议: 尽量使用标准库类型而非数组
使用指针和数组容易出错。现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。
术语表
1.拷贝初始化 copy initialization
2.直接初始化 direct initialization
3.范围for range for
4.容器 container
5.类模板 class template
6.实例化 instantiation
7.值初始化 value-initialized
8.构造 construct
9.列表初始化 list initialize
10.迭代器 iterator
11.迭代器运算 iterator arithmetic
12.C风格字符串 C-style character string
13.空字符 null terminated
14.缓冲区溢出 buffer overflow
15.编译器扩展 complier extension
参考资料
- C++ Primer 中文版 第5版
- https://zhuanlan.zhihu.com/p/23503699