C++Primer第五版【学习笔记】——第三章 Strings,Vectors,Arrays

1. String类型

1.1 定义和初始化

有以下几种初始化方法:

string s0;	//初始化为空串
string s1(s0);	//s1为s0的一个副本,direct initialization
string s11 = s0;	//同上,copy initialization
string s2("Hello");//s2为字符串"Hello",不包含NULL结束符,direct initialization
string s22 = "Hello";//同上,copy initialization
string s3(5, 'H'); //初始化为5为'H',direct initialization

使用=的初始化方法是告诉编译器,复制初始化(copy initialize)对象。而不带等号的方法则是直接初始化(direct initialization)。

string s33 = (5, 'H'); //copy initialization

s33初始化等价于:

string tmp(5, 'H');
string s33(tmp);

比s3的初始化方式效率要低。

1.2 输入个数未知

string word;
while (cin >> word){
	cout << word << endl;
}

每次读入一个word,就会测试流的状态,如果输入合法或未遇到文件结束符,则测试状态为真;否则为假,循环结束。

1.3 读入一整行

string line;
while (getline(cin , line){
	cout << line << endl;
}

getline会读入一行字符直到遇到第一个换行(new line)。如果第一个字符为换行,则该字符串为空。

1.4 string::size_type类型

string的成员函数返回的类型是string::size_type,可以看出,该类型是string的伴随类型( companion type)。这种伴随类型使库类型的使用与机器无关。size_type是unsigned类型的整型,我们可以不必关注具体是哪种类型,只需要知道它足够保存string的长度。
【C++11】
键入string::size_type会是一件很麻烦的事。所以在新标准下,我们可以使用auto或decltype,来让编译器自动分配合适的类型。
auto len = line.size(); //len的类型为string::size_type

1.5 字符串比较

比较规则:
  1. 如果两个string的长度不同,且短string中的所以字符和对应的长string中的字符都相同,则短string小于长string。
  2. 如果有不同的字符,则结果即为比较第一个不相同的字符的结果。

1.6 字符串相加

字符串的相加相当于字符串的连接操作,要求是+号左右两边至少有一个操作符必须是string类型:
string s0;
string s1 = "hello", s2 = "world";
string s3 = s1 + s2;      // ok
string s4 = s1 + "world"; // ok
string s5 = "hello" + s2; // ok
string s6 = "hello" + "world";      // error
string s7 = s0 + "hello" + "world"; // ok
s7可以解释为下面两个式子:
string tmp = s0 + "hello";
string s7 = tmp + "world";
这与cin,cout的串联规则相似。

1.7 处理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是大写字母,则转换为小写;否则不变
toupper(c)如果c是小写字母,则转换为大写;否则不变

控制字符:
  • 0(null,NUL,\0),许多编程语言用来标示字符串结束。
  • 7(bell,BEL,\a),会使接收它的设备发出类似警报的声音。
  • 8(backspace,BS,\b),擦除最后一个字符。
  • 9(horizontal tab,HT,\t),相当于键盘上的tab键。
  • 10(line feed,LF,\n),在UNIX系统及其变体中,作为行结束符。
  • 12(form feed,FF,\f),使打印机弹出纸,或使视频终端刷新屏幕。
  • 13(carriage return,CR,\r),在Mac OS,OS-9,FLFX(及变体)系统中作为行结束符。
    CR\LF组合应用于CP/M-80和包括DOS和Windows在内的衍生系统,以及像HTTP的应用层协议。
  • 27(escape,ESC,\e[GCC only])。
  • 127(delete,DEL,)。
  • ...
【建议】使用C++版本的头文件
除了专门为C++定义的工具,C++库还并入了C函数库。C语言中的头文件名的格式为.h。C++版本中的这些头文件的名字是将后缀.h去掉在前面加上字母c。c表示头文件出自C函数库。因此,cctype与ctype.h中的内容相同。不同的是,为了适应C++程序,定义在cname头文件中的名字都定义在std命名空间里。一般C++程序应该使用cname版本的头文件,而不是.h版本的。这样出自标准库中的名字就和std命名空间中的保持一致。而使用.h头文件,则使程序员需要记住哪些库名是出自C而哪些是C++中唯一的。

【C++11】range for

如果想对string里的每个元素做一些操作,新标准引入的一种新的for语句:range for。该语句遍历一个特定的序列,且可以对序列中的每个元素进行操作。语法格式为:

for (declaration : expresion)
    statement

expression表示要遍历的对象,declaration定义了一个变量用来访问并操作序列中的元素。每次迭代开始时,declaration就会被初始化为序列中的下一个元素。

下面看一个例子,将sting中的字符都转换为大写:

for (auto c : s0) {
	toupper(c);
}
这种写法确实很酷,而且使用auto类型就不需要考虑序列中元素的具体类型了。(效率如何就有待验证了)

细心的读者应该会发现上面的程序并没有改变s0中的元素值,这是因为如果要在range for语句中改变序列中的元素值,循环变量必须是引用类型(像不像函数调用时的参数传递?)。上面的程序可以修改为:

for (auto &c : s0) {
	c = toupper(c);
}
使用下标[ ]操作

对string使用下标操作类似于C语言中的字符数组的下标操作,唯一需要注意的是下标的范围为0到s.size() - 1。下标操作可以实现随机访问。


 2. vector类型

2.1 定义和初始化

vector是具有相同类型的一组对象的集合。我们可以用下标访问vector中对应的元素。vector是一个容器(container),也是一个类模板(class template)。

模板本身不是函数或者类,模板可以认为是一组指令,指示编译器生成类或函数。编译器根据模板生成类或函数的过程就叫实例化(instantiation)。在实例化是我们一般需要提供一些信息,来生成我们需要的类或函数。

vector<int> vi;    // hold int
vector<string> vstr; // hold string
vector<vector<int>> vvi; //hold vector<int>
vector可以保存大多数类型的对象,包括内置类型和类类型,当然也包括vector自身。 记住:vector不是类型,而是模板。
注:有些编译器在定义vector的vector时,需要在里面那对<>后面加空格,vector<vector<int>_>,即下划线的位置。

vector的初始化方法有多种,以vector<int>为例:

vector<int> v1;   // v1: 空,即元素个数为0
vector<int> v2(v1);  // v2: v1
vector<int> v3 = v1; // v3: v1
vector<int> v4(5, 1); // v4: 1, 1, 1, 1, 1
vector<int> v5(5);    // v5: 0, 0, 0, 0, 0
vector<int> v6{1, 2, 3, 4, 5}; // v6: 1, 2, 3 ,4, 5
vector<int> v7 = {1, 2, 3, 4, 5}; // v7: 1, 2, 3, 4, 5
vector<T> vt(n, val)是将vector初始化为n个val,vector<T> vt(n),则是用n个默认值初始化vector。

v6, v7的初始化方法是列表初始化,类似于数组的初始化,是C++11的新标准。

我们看到,C++提供了多种初始化的方法。在多数情况下,这些初始化形式都是可互换使用的。但是,也有例外的情况。当使用复制初始化时(使用=),只能提供单个初始化对象(initializer);当在类内(in-class)初始化时,必须使用复制初始化或使用大括号。第三个限制是,使用元素列表初始化时,必须使用大括号{},而不能使用圆括号()。

关于列表初始化,编译器首先会判断列表中提供的元素是否可以用来列表初始化,如果不行,就尝试其他的初始化方法(编译器越来越智能?)。

vector<string> vs1{"hello"}; // list initialization
vector<string> vs2("hello"); // error
vector<string> vs3{10};   // vs3被初始化为10个空串
vector<string> vs4{10, "hello"}; // vs4被初始化为10个"hello"
一般在定义vector时如果不知道其大小,则将其定义为空,然后在运行时使用 push_back成员函数往vector中添加元素。vector添加元素的操作是高效的,甚至比提前定义vectoer的大小还要快,这个跟C语言中的习惯先定义数组的大小是相反的。

警告:range for作用的迭代序列,不能在for结构体中改变序列的大小,比如对vector使用push_back操作。

2.2 vector的其他操作

vector操作
v.empty()如果为空返回true;否则返回false
v.size()返回v中的元素个数,类型为size_type
v.push_back(t)在v后面插入一个元素t
v[n]返回v中位置n出的元素的引用
v1 = v2将v1中的元素替换为v2中的元素的副本
v1 = {a, b, c...}将v1中的元素替换为以逗号分隔的列表中的元素
v1 == v2如果v1和v2中的元素相等且对应元素相同,则v1和v1相等
v1 != v2与v1 == v2相反
<, <=, >, >=返回用字典序比较的结果

注意:习惯使用数组的C语言程序员,往往会犯的一个错误是对vector的下标引用越界。vector是动态管理内存的容器(container),而C语言的数组则是在初始化的时候就分配好内存。错误的下标引用往往会造成缓冲区泄漏( buffer overflow),很多应用程序的安全问题就来自于此。

3 arrays

数组具有固定的长度,在运行时效率更高。但是灵活性较差。

3.1 数组的声明和定义

数组是复合类型,声明的格式为a[d]。其中a是数组名,d表示数组维度。d必须是大于0整型,且必须是常量表达式。

unsigned int uiSize = 10;
const unsigned int cuiSize = 10;
int iArray1[uiSize];  // error
int iArray2[cuiSize]; // ok
int iArray3[array_size()]; // ok if array_size() is constexpr, error otherwise
数组的初始化一般采用列表初始化,初始化列表的长度不能超过数组定义的长度,未初始化的元素会初始化为默认值。使用字符串字面值初始化字符数组时,会在字符串后面自动加上'\0'字符:

int a3[10];  // 数组元素初始化为0
int main() {
  int a0[10];  // 数组元素为初始化。使用为初始化的变量是危险的行为,编译器一般会给出警告。
  int a1[10] = {};  // 数组元素初始化为0
  char s0[] = "ok"; // 数组长度为3,s[2] = '\0'
  return 0;
}
更复杂的定义

可以定义指针的数组,也可以定义指向数组的指针或引用。不存在引用的数组:

int arr[10] = {};
int *ptrs[10];  // 包含10个指向int的指针的数组
int &refs[10];  // error
int (*pArr)[10] = &arr;  // pArr 是指向包含10个int类型元素数组的指针
int (&arrRef)[10] = arr; // arrRef 是包含10个int类型元素数组的引用
int *(&rpArr)[10] = ptrs;// rpArr is a reference to an array of ten pointers(英文表述更简洁)

从右往左解读,定义就很容易理解了。比如:ptrs是一个包含10个元素的数组,数组元素的类型为int*。如果包含括号,则需要先解读括号内的部分,比如:pArr是一个指针,该指针指向一个包含10个元素的数组,该数组的元素类型为int。有括号就先解读括号里面的,然后从右往左解读。这样rpArr的含义就很容易理解了。

3.2 数组与指针

对数组的操作实际上是对指向数组的指针的操作。数组的下标操作与对应的指针的引用操作有同样的效果。
int arr[10] = {};
int *pArr = arr;
arr[4] = 10;
*(pArr+4) = 10; // 与arr[4] = 10有相同的效果
使用auto和decltype声明数组,会有不同的结果:
int arr[10] = {};
auto arr1 = arr;   // arr1 : int *
decltype(arr) arr2 = {}; arr2 : int [10]
auto语句声明变量时,是根据初始化表达式的类型来推断变量的类型;而decltype则是根据其作用的变量的类型声明变量。于是,arr1是指向int类型的指针,arr2是包含10个int类型元素的数组。
操作数组元素的指针与vector的迭代器有相同的性质。包括对指针的加减运算,比较。我们同样可以定义超过数组末尾位置的指针(称其为,越界判定指针)。
int arr[10] = {};
int *b = &arr[0];
int *e = &arr[10]; 
注意:我们只能使用&arr[10]来初始化越界判定指针,其他操作均非法的。
【C++11】begin和end函数
新标准引入了函数begin和end专门用来初始化便利数组的开始指针和越界判定指针。
int arr[10] = {};
int *b = begin(arr);
int *e = end(arr); 
对string和vector的下标操作的索引值必须是非负的,而数组的下标则没有这个限制。
int arr[10] = {};
int *p = arr[4];
int a = *(p-4); // a = arr[0];



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

superbin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值