3.字符串、向量和数组

字符串、向量和数组

3.1命名空间的using声明

作用域操作符(::)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。

头文件不应包含using声明

位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会被拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

3.2标准库类型string
3.2.1定义和初始化string对象
string s1;  // 默认初始化,s1是一个空字符串
string s2 = s1; // s2是s1的副本
string s3 = "hiya"; // s3是该字符串字面值的副本
string s4(10, 'c'); // s4的内容是cccccccccc
直接初始化和拷贝初始化

如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化

3.2.2string对象上的操作

在这里插入图片描述

在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。
所以,有时候希望能在最终得到的字符串中保留输入的空白符,这时应该用getline函数替代原来的>>运算符。该函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。如果输入一开始就是换行符,那么所得的结果是一个空string
和输入运算符一样,getline也会返回它的流参数,因此可以作为判断的条件。

string::size_type类型

需要注意的是,string类的size函数返回的是一个string::size_type无符号类型的值,以此来实现标准库类型与机器无关的特性。
在c++11新标准中,允许编译器通过auto或者decltype来推断变量的类型:

auto len = line.size(); // len的类型是string::size_type

由于size函数返回的是一个无符号整型数,因此如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。

字面值和string对象相加

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

string s1 = "hello" + ", "; // 错误:两个运算对象都不是string
string s2 = "hello" + ", " + str;   // 错误:不能把字面值直接相加

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

3.2.3处理string对象中的字符

在这里插入图片描述

只要字符串不是常量,就能为下标运算符返回的字符赋新值

string s1 = "some string";

for (decltype(s1.size()) index = 0; index != s1.size() && !isspace(s1[index]); ++index) {
    s1[index] = toupper(s1[index]);
}
3.3标准库类型vector

C++语言既有类模板,也有函数模板,其中vector是一个类模板(因此由vector生成的类型必须包含vector中元素的类型,例如:vector<int>)。
模板本身不是类或函数,相反,可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。

3.3.1定义和初始化vector对象

在这里插入图片描述

C++提供了几种不同的初始化方式。在大多数情况下,这些初始化方式可以相互等价地使用,不过也并非一直如此。目前所知晓的几种例外情况是:

  • 使用拷贝初始化时,只能提供一个初始值。
  • 如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化
  • 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里。

确认无法执行列表初始化后,编译器会尝试用默认值初始化vector对象。

3.3.2向vector对象中添加元素
vector对象添加元素蕴含的编程假定

需要注意的是,范围for语句体内不应改变其所遍历序列的大小(会导致迭代器失效)。

3.3.3vector相关操作

在这里插入图片描述

要使用size_type,需首先指明它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:vector<int>::size_type

3.4迭代器介绍

除了vector之外,标准库还定义了其他几种容器。所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。

3.4.1使用迭代器

和指针不一样的是,获取迭代器不是使用地址符,有迭代器的类型同时拥有返回迭代器的成员,例如beginend。其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器:

auto b = v.begin(), e = v.end();  // b和e的类型相同

end成员则负责返回指向容器(或string对象)"尾元素的下一位置"的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的尾后元素。这样的迭代器没有什么实际含义,仅是个标记而已,表示已经处理完了容器中的所有元素。end成员返回的迭代器常被称作尾后迭代器或者简称为尾迭代器。特殊情况下如果容器为空,则beginend返回的是同一个迭代器,即尾后迭代器

迭代器运算符

在这里插入图片描述

和指针类似,也能通过解引用迭代器来获取它所指示的元素(返回的是一个引用),执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为

string s("some string");

if (s.begin() != s.end()) { // 确保非空
    auto it = s.begin();
    *it = toupper(*it);
}
将迭代器从一个元素移动到另外一个元素

迭代器使用递增运算符来从一个元素移动到下一个元素:

for (auto b = s.begin(); b != s.end() && !isspace(*b); ++b) {
    *b = toupper(*b);
}

需要注意的是,c++中会for循环中使用!=进行判断,原因在于这种编程风格在标准库提供的所有容器上都有效,养成这个习惯后就不用太在意用的到底是哪种容器类型。

迭代器类型

一般来说也不知道(其实是无需知道)迭代器的精确类型,而实际上,那些拥有迭代器的标准库类型使用iteratorconst_iterator来表示迭代器的类型:

vector<int>::iterator it; // it能读写vector<int>的元素
string::iterator it2; // it2能读且string对象中的字符

vector<int>::const_iterator it3;  // it3只能读元素,不能写元素
string::const_iterator it4;   // it4只能读字符,不能写字符
beginend运算符

beginend返回的具体类型由对象是否是常量决定,如果对象是常量,beginend返回const_iterator;反之,返回iterator

vector<int> v;
const vector<int> cv;
auto it1 = v.begin();    // it1的类型是vector<int> :: iterator
auto it2 = v.begin();   // it2的类型是vector<int> :: const_iterator

有时候这种默认行为并非所希望的,如果对象只需读操作而无需写操作的话最好使用常量类型。为了便于专门得到const_iterator类型的返回值,c++11新标准引入了两个新函数,分别是cbegincend
为了简化解引用和访问其成员的操作,c++语言定义了箭头运算符。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it -> mem等价于(*it).mem表达的意思相同。

某些对vector对象的操作会使迭代器失效

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

3.4.2迭代器运算

在这里插入图片描述

迭代器的算术运算

可以令迭代器和一个整数值相加或相减,其返回值是向前或向后移动了若干个位置的迭代器。执行这样的操作时,结果迭代器或指示原vector对象(或string对象)内的一个元素,或者指示尾元素的下一位置:

// 计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size() / 2;

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

使用迭代器运算

使用迭代器的一个经典算法是二分法:

// 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 = beg + (end - beg) / 2;    // 新的中间点
}
3.5数组

数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性上又有所不同,主要在于数组的大小是固定不变的,不能向数组中随意增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,当然也损失了一些灵活性。

3.5.1定义和初始化内置数组

维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的,也就是说,维度必须是一个常量表达式

unsigned cnt = 42;  // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式

string bad[cnt];    // 错误:cnt不是常量表达式。实测发现部分编译器允许这样的写法,可以自行研究。
string strs[get_size()];    // 当get_size是constexpr时正确;否则错误

默认情况下,数组的元素被默认初始化。需要注意的是,和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值

int a[] = {0, 1, 2,};
int a2[] = a;   // 错误:不允许使用一个数组初始化另一个数组
a2 = a; // 错误:不能把一个数组直接赋值给另一个数组
理解复杂的数组声明

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

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个指针

按照由内向外的顺序阅读,首先知道arry是一个引用,然后观察右边知道,arry引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素类型是指向int的指针。

3.5.2访问数组元素

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,定义在cstddef头文件中。

3.5.3指针和数组

数组有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针

string numbs[] = {"one", "two", "three"};
string *p = numbs;  // 等价于p = &numbs[0]

在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。因此,在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐含的意思。其中一层意思是,当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto ia2(ia);   // ia2是一个整型指针,指向ia的第一个元素
ia2 = 42;   // 错误:ia2是一个指针,不能用int值给指针赋值

尽管ia是一个数组,但当使用ia作为初始值时,编译器实际执行的初始化过程类似于:

auto ia2(&ia[0]);   // 显然,ia2的类型是int*

必须指出的是,当使用decltype关键字时上述转换不会发生:

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

指向数组元素的指针拥有更多功能。vectorstring的迭代器支持的运算,数组的指针全部支持:

int arr[] = {0, 1, 2, 3, 4};
int *p = arr;
++p;    // p指向arr[1]

就像使用迭代器遍历vector对象中的元素一样,使用指针也能遍历数组中的元素。但是需要注意的是,尾后指针也不指向具体的元素,因此,不能对尾后指针执行解引用或递增的操作:

for (int *p = arr, *e = &arr[5]; p != e; ++p) {
    cout << *p << endl;
}
标准库函数beginend

尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,c++11新标准引入了两个名为beginend的函数(定义在iterator头文件中)。这两个函数与容器中的两个同名成员功能类型:

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *beg = begin(ia);   // 指向ia首元素的指针
int *last = end(ia);    // 指向ia尾元素的下一位置的指针
指针运算

数组指针可以执行各种迭代器运算,这些运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致。
给(从)一个指针加上(减去)某整型值,结果仍是指针。新指针指向的元素与原来的指针相比前进(后退)了该整数个位置:

constexpr size_t sz = 5;
int arr[sz] = {1, 2, 3, 4, 5};
int *ip = arr;
int *ip2 = ip + 4;

给指针加上一个整数,得到的新指针仍需指向同一数组的其他元素,或者指向同一数组的尾元素的下一位置。
和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一数组当中的元素
两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型(定义在cstddef头文件中,是一种机器相关的类型)。因为差值可能为负数,所以ptrdiff_t是带符号类型。

下标和指针

在很多情况下,使用数组的名字其实用的是一个指向数组首元素的指针。一个典型的例子是当对数组使用下标运算符时,编译器会自动执行上述转换操作:

int ia[] = {0, 2, 4, 6, 8};
int i = ia[2];  // ia转换成指向数组首元素的指针,ia[2]得到(ia + 2)所指的元素
int *p = ia;
i = *(p + 2);   // *是解引用

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

int *p = &ia[2];
int j = p[1];   // p[1]等价于*(p + 1),就是ia[3]表示的那个元素
int k = p[-2];  // p[-2]是ia[0]表示的那个元素
3.5.4C风格字符串

C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串放在字符数组中并以空字符('\0')结束,一般利用指针来操作这些字符串。

C标准库String函数

在这里插入图片描述

定义在cstring头文件中,cstring是c语言头文件string.h的c++版本。
传入此类函数的指针必须指向以空字符作为结束的数组。

3.5.5与旧代码的接口
混用string对象和c风格字符串

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

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

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

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

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

使用数组初始化vector对象

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

int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));

现代的c++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用c风格的基于数组的字符串

3.6多维数组

严格来说,c++语言中没有多维数组,通常所说的多维数组其实是数组的数组。当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:

int ia[3][4];   // 大小为3的数组,每个元素是含有4个整数的数组

// 大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素是含有30个整数的数组
int arr[10][20][30] = {0};  // 将所有元素初始化为0

int temp[1][2][3] = {        
    {                
        {1, 2, 3},                
        {4, 5, 6}        
    }
};
使用范围for语句处理多维数组
for (const auto &row : ia) {
    for (auto col : row) {
        cout << col << endl;
    }
}

这个循环中并没有任何写操作,但是依然将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转成指针。假设不用引用类型:

for (auto row : ia) {
    for (auto col : row) {
        // ...
    }
}

程序将无法通过编译。这是因为,像之前一样,第一个循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。这样得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历。
因此,要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型

指针和多维数组

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

int ia[3][4];   // 大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia;   // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素

随着c++11新标准的提出,通过使用autodecltype就能尽可能地避免在数组前面加上一个指针类型了:

// 输出ia中每个元素的值,每个内层数组各占一行
// p指向含有4个整数的数组
for (auto p = ia; p != ia + 3; ++p) {
    // *p对内层数组解引用,获取指向的数组,此时数组会转换成指针,因此,q指向内层数组的第一个元素
    for (auto q = *p; q != *p + 4; ++q) {
        cout << *q << ' ';
    }
    
    cout << endl;
}

// 等价于

// 推荐这种写法
// p指向ia的第一个数组
for (auto p = begin(ia); p != end(ia); ++p) {
    // q指向内层数组的首元素
    for (auto q = begin(*p); q != end(*p); ++q) {
        cout << *q << ' ';
    }
    
    cout << endl;
}
类型别名简化多维数组指针

使用类型别名能让读、写和理解一个指向多维数组的指针这项工作变得简单一点:

using int_array = int[4];   // 新标准
typedef int int_array[4];

// p指向二维数组内的第一个数组对象
for (int_array *p = ia; p != ia + 3; ++p) {
    for (int *q = *p; q != *p + 4; ++q) {
        cout << *q << ' ';
    }
    
    cout << endl;
}
总结

位于头文件的代码一般来说不应该使用using声明。
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组,而当使用decltype关键字时上述转换不会发生。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值