C++ Primer 3_字符串、向量和数组

字符串、向量和数组

之前学过的基础性的东西我将不会在此再做重复,如果有一些我之前从未注意过或从未深思过的事情以及一些重点,我都将会在此写下。

C++标准一方面对库类型所提供的操作做了详细规定,另一方面也对库的实现者做出一些性能上的需求。因此,标准库类型对于 一般应用场合 来说有足够的效率。

操作功能
getline(is, s)从 is 中读取一行赋给 s,返回 is
s.empty()s 为空返回 true,否则返回 false
s[n]返回 s 中第 n 个字符的引用,位置 n 从 0 计起

有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用 getline 函数代替原来的 >> 运算符。
getline 函数的参数是一个输入流和一个 string 对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个 string 对象中去(注意不存换行符)。getline 只要遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空 string。
和输入运算符一样,getline也会返回它的流参数。

对于 size 函数来说,返回一个 int 或者返回一个 unsigned 似乎都是合情合理的。但其实 size 函数返回的是一个 string::size_type 类型的值。
string::size_type 类型是个无符号类型的值而且能足够存放下任何 string 对象的大小。
所有用于存放 string 类的 size 函数返回值的变量,都应该是 string::size_type 类型的。
PS:可用 auto 或者 decltype 推断其类型,不用自己写。

由于 size 函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。例如,假设 n 是一个具有负值的 int,则表达式 s.size() < n 的判断结果几乎肯定是 true。这是因为负值 n 会自动地转换成一个比较大的无符号值。
如果一条表达式中已经有了 size() 函数就不要再使用 int 了,这样可以避免混用 int 和 unsigned 可能带来的问题。

如果两个 string 对象在某些对应的位置上不一致, 则 string 对象比较的结果其实是 string 对象中第一对相异字符比较的结果。

当把 string 对象和字符字面值及字符串字面值混在一条语 句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是 string:

string s4 = s1 + ",";
//正确:把一个string对象和一个字面值相加
string s5 = "hello" + ","; //错误:两个运算对象都不是string
//正确:每个加法运算符都有一个运算对象是string
string s6 = s1 + "," + "world";
string s7 = "hello" + "," + s2;//错误:不能把字面值直接相加

s6 的初始化形式之前没有出现过,它的工作机理和连续输入连续输出是一样的,可以用如下的形式分组:

string s6 = (s1 + ",") + "world";

因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型 string 的对象。切记,字符串字面值与 string 是不同的类型。

函数功能
isalnum(c)当 c 是字母或数字时为真
isalpha(c)当 c 是字母时为真
iscntrl(c)当 c 是控制字符时为真
isdigit(c)当 c 是数字时为真
isgraph(c)当 c 不是空格但可打印时为真
islower(c)当 c 是小写字母时为真
isprint(c)当 c 是可打印字符时为真(即 c 是空格或 c 具有可视形式)
ispunct(c)当 c 是标点符号时为真(即 c 不是控制字符、数字、字母、可打印空白中的一种)
isspace(c)当 c 是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种)
isupper(c)当 c 是大写字母时为真
isxdigit(c)当 c 是十六进制数字时为真
tolower(c)如果 c 是大写字母,输出对应的小写字母;否则原样输出 c
toupper(c)如果 c 是小写字母,输出对应的大写字母;否则原样输出 c

建议使用C++版本的C标准库头文件。
C++标准库中除了定义C+语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如 name.h,C++则将这些文件命名为 cname。也就是去掉了 .h 后缀,而在文件名name之前添加了字母 c,这里的 c 表示这是一个属于C语言标准库的头文件。
因此,cctype 头文件和 ctype.h 头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为 cname 的头文件中定义的名字从属于命名空间 std,而定义在名为 .h 的头文件中的则不然。
一般来说,C++程序应该使用名为 cname 的头文件而不使用 name.h 的形式,标准库中的名字总能在命名空间 std 中找到。如果使用 .h 形式的头文件,程序员就不得不时刻牢记哪些是从C语言那儿继承过来的,哪些又是C++语言所独有的。

范围 for(range for)语句

for (declaration : expression) {
	statement;
}

其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration 部分的变量会被初始化为 expression 部分的下一个元素值。

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

C++语言既有类模板(class template),也有函数模板,其中 vector 是一个类模板。

初始化 vector 对象的方法:

形式功能
vector< T > v1v1 是一个空 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…}

如果用的是花括号,可以表述成我们想列表初始化(list initialize)该 vector 对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。

vector<string> v5{"hi"}; //列表初始化:v5有一个元素
vector<string> v6("hi"); //错误:不能使用字符串字面值构建vector对象
vector<string> v7{10};   //v7有10个默认初始化的元素
vector<string> v8{10, "hi"};//v8有10个值为"hi"的元素

v7 和 v8 提供的值不能作为元素的初始值。确认无法执行列表初始化后,编译器会尝试用默认值初始化 vector 对象。

现在就要指出的:如果循环体内部包含有向 vector 对象添加元素的语句,则不能使用范围 for 循环。

范围for语句的定义来源于与之等价的传统for语句:

for (auto beg = v.begin(), end = v.end(); beg != end; ++beg){
	auto &r = *beg; //r必须是引用类型,这样才能对元素执行写操作
	r *= 2; //将v中每个元素的值翻倍

明白了范围 for 语句的原理之后,我们也就不难理解为什么不能通过范围 for 语句增加vector对象(或者其他容器)的元素了:
在范围 for 语句中,预存了 end() 的值。一旦在序列中添加(删除)元素,end 函数的值就可能变得无效。

vector 对象:
v.empty ()
如果 v 不含有任何元素,返回真;否则返回假。
v.size ()
返回v中元素的个数。

试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。
这种通过下标访问不存在的元素的行为的错误即所谓的缓冲区溢出(buffer overflow),这也是导致PC及其他设备上应用程序出现安全问题的一个重要原因。

确保下标合法的一种有效手段就是尽可能使用范围 for 语句

获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为 begin 和 end 的成员,其中 begin 成员负责返回指向第一个元素(或第一个字符)的迭代器。
end 成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后( off the end)”元素。这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。end 成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。特殊情况下如果容器为空,则 begin 和 end 返回的是同一个迭代器。

如果容器为空,则 begin 和 end 返回的是同一个迭代器,都是尾后迭代器。

原来使用C或Java的程序员在转而使用C++语言之后,会对 for 循环中使用 != 而非 < 进行判断有点儿奇怪。C++程序员习惯性地使用 !=,其原因和他们更愿意使用迭代器而非下标的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。
只有 string 和 vector 等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了 == 和 !=,但是它们中的大多数都没有定义 < 运算符。因此,只要我们养成使用迭代器和 != 的习惯,就不用太在意用的到底是哪种容器类型。

那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型:

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

迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。
重点是理解存在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。
每个容器类定义了一个名为 iterator 的类型,该类型支持迭代器概念所规定的一套操作。

begin 和 end 返回的具体类型由对象是否是常量决定,如果对象是常量,begin 和 end 返回 const_iterator;如果对象不是常量,返回 iterator。
有时候这种默认的行为并非我们所要。如果对象只需读操作而无须写操作的话最好使用常量类型(比如 const_iterator)。为了便于专门得到 const_iterator 类型的返回值,C++11标准引入了两个新函数,分别是 cbegin 和 cend。
类似于 begin 和 end,这两个新函数也分别返回指示容器第一个元素或最后元素下一位置的迭代器。有所不同的是,不论 vector 对象(或 string 对象)本身是否是常量,返回值都是 const_iterator。

虽然 vector 对象可以动态地增长,但是也会有一些副作用。已知的一个限制是不能在范围 for 循环中向 vector 对象添加元素。另外一个限制是任何一种可能改变 vector 对象容量的操作,比如 push back,都会使该 vector 对象的迭代器失效。
谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

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

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

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

定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一点了:

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

就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解 Parray 的含义:首先是圆括号括起来的部分,*Parray 意味着 Parray 是个指针,接下来观察右边,可知道 Parray 是个指向大小为 10 的数组的指针,最后观察左边,知道数组中的元素是 int。这样最终的含义就明白无误了,Parray 是一个指针,它指向一个 int 数组,数组中包含 10 个元素。
同理,(&arrRef) 表示 arrRef 是一个引用,它引用的对象是一个大小为10的数组,数组中元素的类型是 int。
当然,对修饰符的数量并没有特殊限制:

int *(&arry)[10] = ptrs; 
// arry是数组的引用,该数组含有10个指针

按照由内向外的顺序阅读上述语句,首先知道 arry 是一个引用,然后观察右边知道,arry 引用的对象是一个大小为 10 的数组,最后观察左边知道,数组的元素类型是指向 int 的指针。这样,arry 就是一个含有 10 个 int 型指针的数组的引用。
要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。

与 vector 和 string 一样,数组的下标是否在合理范围之内由程序员负责检查,所谓合理就是说下标应该大于等于 0 而且小于数组的大小。要想防止数组下标越界,除了小心谨慎注意细节以及对代码进行彻底的测试之外,没有其他好办法。对于一个程序来说,即使顺利通过编译并执行,也不能肯定它不包含此类致命的错误。

大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。

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

当使用数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组:

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 个整数构成的数组:

// ia3是一个含有10个整数的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p;    // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确:把i的值赋给ia3的一个元素

指针也是迭代器,vector 和 string 的迭代器支持的运算,数组的指针全都支持,就像使用迭代器遍历 vector 对象中的元素一样,使用指针也能遍历数组中的元素。当然,这样做的前提是先得获取到指向数组第一个元素的指针和指向数组尾元素的下一位置的指针。

尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,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尾元素的下一位置的指针

begin 函数返回指向 ia 首元素的指针,end 函数返回指向 ia 尾元素下一位置的指针,这两个函数定义在 iterator 头文件中。

两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型。因为差值可能为负值,所以 ptrdiff_t 是一种带符号类型。
两空指针也当然允许相减,结果当然是 0。

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

int *p = &ia[2]; // p指向索引为2的元素
int j = p[1];
// p[1]等价于 *(p + 1),就是ia[3]表示的那个元素
int k = p[-2]; // p[-2]是ia[0]表示的那个元素

虽然标准库类型 string 和 vector 也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,上面的最后一个例子很好地说明了这一点。内置的下标运算符可以处理负值,当然,结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。
内置的下标运算符所用的索引值不是无符号类型,这一点与 vector 和 string 不一样。

C风格字符串所用的数组必须足够大以便容纳下结果字符串及末尾的空字符。否则会充满安全风险,极易引发严重错误。
一个潜在的问题是,我们在估算C风格字符串所用数组所需的空间时不容易估准,而且数组所存的内容一旦改变,就必须重新检查其空间是否足够。不幸的是,这样的代码到处都是,程序员根本没法照顾周全。这类代码充满了风险而且经常导致严重的安全泄漏。
对大多数应用来说,使用标准库 string 要比使用C风格字符串更安全、更高效。

任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:

  • 允许使用以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值。
  • 在 string 对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在 string 对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。

上述性质反过来就不成立了:如果程序的某处需要一个C风格字符串,无法直接用 string 对象来代替它。例如,不能用 string 对象直接初始化指向字符的指针。为了完成该功能,string 专门提供了一个名为 c_str 的成员函数:

char *str = s; //错误:不能用string对象初始化
char*const char *str = s.c_str(); //正确

顾名思义,c_str 函数的返回值是一个C风格的字符串。
也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个 string 对象的一样。结果指针的类型是 const char*,从而确保我们不会改变字符数组的内容。
我们无法保证 c_str 函数返回的数组一直有效,如果后续的操作改变了 s 的值就可能让之前返回的数组失去效用。
如果执行完 c_str() 函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。

不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用 vector 对象初始化数组。相反的,允许使用数组来初始化 vector 对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:

int int_arr[] = { 0, 1, 2, 3, 4, 5};
// ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));

在上述代码中,用于创建 ivec 的两个指针实际上指明了用来初始化的值在数组 int_arr 中的位置,其中第二个指针应指向待拷贝区域尾元素的下一位置。此例中,使用标准库函数 begin 和 end 来分别计算 int_arr 的首指针和尾后指针。

用于初始化 vector 对象的值也可能仅是数组的一部分:

//拷贝三个元素: int_arr[l]、 int_arr[2]、int_arr[3]
vector<int> subVec(int_arr + 1, int_arr + 4);

这条初始化语句用 3 个元素创建了对象 subvec,3 个元素的值分别来自 int_arr[1]、int arr[2] 和 int_arr[3]。

使用指针和数组很容易出错。一部分原因是概念上的问题:指针常用于底层操作,因此容易引发一些与烦琐细节有关的错误。其他问题则源于语法错误,特别是声明指针时的语法错误。
现代的C++程序应当尽量使用 vector 和迭代器,避免使用内置数组和指针;应该尽量使用 string,避免使用C风格的基于数组的字符串。

可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组:

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

将外层循环的控制变量声明成引用类型,是为了避免数组被自动转成指针。假设不用引用类型,则循环如下述形式:

for (auto row : ia)
	for (auto col : row) // 错误!

程序将无法通过编译。这是因为,像之前一样第一个循环遍历 ia 的所有元素,注意这些元素实际上是大小为 4 的数组。因为 row 不是引用类型,所以编译器初始化 row 时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的 row 的类型就是 int*,显然内层的循环就不合法了,编译器将试图在一个 int* 内遍历,这显然和程序的初衷相去甚远。

正确示例:

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

要使用范围 for 语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值