《C++Primer》第三章 字符串、向量和数组

it2024-03-25  72

写这篇文章的目的

身为C++的零基础初学者,短期内把《C++Primer》啃下来是一个比较笨但是有效的方法,一方面可以掌握比较规范的C++语法(避免被项目中乱七八糟的风格带跑偏),另一方面又可以全面地了解C++语法以及C++11新标准(后续要做的事情就剩下查漏补缺,不断完善自己的知识体系)。

个人感觉从零学习一门新知识比较好的方法是快速了解知识的全貌,然后构建自己的知识地图,后续不断地补充相应的细节。

由于《C++Primer》和大多数的教科书一样废话连篇,因此想要精炼一下每篇文章的内容再打印成pdf,方便温故知新。

全文链接

命名空间的using声明

using namespace::name;指的是编译器应从操作符左侧名字所示的作用域寻找右侧的名字头文件中不应该出现using声明:这是因为头文件中的内容会拷贝到所有引用它的文件中去,这会导致每个使用了该头文件的文件就会有这个声明,这并非我们所预期的

标准库类型string

#include <string> using std::string;

1. 定义和初始化string对象

拷贝初始化copy initialization:使用等号=初始化一个变量,编译器把等号右侧的初始值拷贝到新创建的对象中直接初始化direct initialization:不使用等号 string s1 = "hiya"; // 拷贝初始化 string s2("hiya"); // 直接初始化 string s3(10, 'c'); // 直接初始化, s7内容为cccccccccc // 拷贝初始化 string s4 = string(10, 'c'); // 等价于 string temp = string(10, 'c'); string s4 = temp;

2. string对象上的操作

os<<s 将s写到输出流os中,返回os is>>s 从is中读取字符串赋给s,字符串以空白分隔,返回is getline(is, s) 从is中读取一行赋给s,返回is s.empty() s为空返回true s.size() 返回s中字符个数 s[n] 返回s中第n个字符的[引用] s1 + s2 返回s1和s2连接后的结果 s1 = s2 用s2的副本代替s1中原先的字符 s1 == s2 大小写敏感 s1 != s2 <, <=, >, >= 利用字符在字典中的顺序比较, 且对大小写敏感 读写string对象:cin读取时会忽略开头和结尾处的空白;getline()函数从给定的输入流中读入内容直到遇到换行符(注意换行符也被读进去了) string::size_type类型:size()函数返回的是一个string::size_type的类型,它本身是一个无符号正数,切记表达式中已经有了size()函数就不要再使用int了,否则可能会出现意想不到的错误:比如s.size() > n在n为负数时判断结果几乎肯定是true

3. 访问string元素

遍历获取string元素:

string str("some string"); for (auto c : str) cout << c << endl;

前面讲到当引用被用作初始值时,真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为auto类型。

因此如果我们需要获得字符的引用来改变string,需要把循环变量定义为引用类型:

string s("Hello World!!!"); // 转化为大写 for (auto &c : s) c = toupper(c); cout << s << endl;

标准库类型vector

#include <vector> using std::vector;

1. 定义及初始化

vector<T> v1; // 空vector, 执行默认初始化 vector<T> v2(v1); // v2包含v1所有元素的副本 vector<T> 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...}; // 同上

2. 添加元素及其他操作

C++标准要求vector应该能在运行时高效快速地添加元素,因此在定义vector对象时设定其大小也就没有必要了,事实上这么做可能性能更差。只有一种例外情况:即所有元素的值都一样。一旦元素的值有所不同,更有效的方式是先定义一个空的vector对象,再在运行时向其添加具体值。

大部分的操作都和string类似:

v.empty() // 是否为空 v.size() v.push_back(t) // 尾部插入 v[n] v1 = v2 v1 = {a,b,c...} // 用列表中的元素拷贝替换v1中的元素 v1 == v2 // 当且仅当元素数量相同且相应位置的元素值都相同 v1 != v2 <, <=, >, >= // 以字典顺序比较

注意:

如果想遍历并修改vector中的值,可以在循环条件写for (auto &i : v) vector<T>的下标类型为vector<T>::size_type vector和string对象的下标运算符可用于访问已存在的元素而不能用于添加元素,试图用下标访问一个不存在的元素将引发错误

迭代器iterator

// 由编译器决定b和e的类型, b表示v的第一个元素, e表示v尾元素的下一个元素 // 注意当容器为空时, begin和end都返回同一个迭代器, 都是尾后迭代器 auto b = v.begin(), e = v.end(); // b和e类型相同 *iter // 返回迭代器iter所指元素的引用 iter->mem // 解引用iter并获取该元素名为mem的成员, 等价于(*item).mem ++iter // 指向容器中下一个元素 --iter // 指向上一个元素 iter1 == iter2 // 如果两个迭代器是同一个元素或者都是同一个容器的尾后迭代器, 则相等 iter1 != iter2

使用迭代器遍历string:

for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it) *it = toupper(*it); // 将当前字符改成大写

迭代器类型:

C++11中通过cbegin()和cend()函数可以得到const_iterator迭代器。

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

数组

与vector不同的是,数组的大小确定不变,不能随意向数组中添加元素。这种特性使得对于某种特殊的应用而言程序的运行时性能更好,但是也损失了一些灵活性。

1. 定义和初始化内置数组

和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组内含有未定义的值。

// 显式初始化数组元素 int a1[3] = {0, 1, 2}; int a2[] = {0, 1, 2}; // 维度为3 int a3[5] = {0, 1, 2}; // 等价于a3[] = {0, 1, 2, 0, 0}; // 字符数组的特殊性 char a1[] = {'C', '+', '+'}; // 列表初始化, 不带空字符 char a2[] = {'C', '+', '+', '\0'}; // 列表初始化, 含有空字符 char a3[] = "C++"; // 自动在末尾添加空字符 不允许用一个数组初始化另一个数组,也不允许用一个数组给另一个数组赋值复杂的数组声明 int *ptrs[10]; // 含有10个整型指针的数组 int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组 int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组

2. 访问数组元素

与vector和string一样,当需要遍历数组的所有元素时,最好的方法是使用范围for语句:

for (auto i : scores) cout << i << " "; cout << endl;

3. 指针和数组

在C++中,指针和数组由非常紧密的联系,使用数组的时候编译器一般会把它替换为一个指向数组首元素的指针。

C++11新标准引入了两个名为begin和end的函数:

int *pbeg = begin(arr), *pend = end(arr); // 寻找第一个负值元素 while (pbeg != pend && *pbeg >= 0) ++pebg;

只要指针指向的是数组中的元素,都可以执行下标操作:

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

4. C风格字符串

虽然C++支持C风格字符串,但最好还是不要使用。这是因为不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。

一些可操作C风格字符串的函数定义在cstring头文件中,cstring是C语言头文件string.h的C++版本:

strlen(p) // 返回p的长度, 空字符不算在内 strcmp(p1, p2) // 比较p1和p2的相等性 strcat(p1, p2) // 将p2附加到p1之后, 返回p1 strcpy(p1, p2) // 将p2拷贝到p1, 返回p1

对于C++中的string,比较大小时是使用普通的关系运算符和相等性运算符;但是C风格字符串需要使用strcmp函数。

如果我们要实现两个C风格字符串拼接,正确的方法是使用strcat和strcpy函数,还需要使用一个用于存放结果字符串的数组,例如下面的代码虽然很常见,但是充满了安全风险,极易引发错误:

// 如果我们计算错了largeStr的大小将引发错误 strcpy(largeStr, cal); // 将ca1拷贝到largeStr strcat(largeStr, car2); // 将ca2连接到largeStr后面

大多数情况下,使用标准库string要比C风格字符串更安全

与旧代码的接口

现代的C++程序中充满了数组或者C风格字符串的代码,因此C++专门提供了一组功能:

1. 混用string和C风格字符串

允许使用以空字符结束的字符数组来初始化或赋值string对象在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个对象都是)如果程序需要一个C风格字符串,可以通过c_str成员函数将string转化为C风格字符串

2. 使用数组初始化vector对象

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

多维数组

严格来说,C++中没有多维数组,通常所说的多维数组其实都是数组的数组。

初始化:

int ia[3][4] = { // 三个元素, 每个元素都是大小为4的数组 {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} } // 显式地初始化每行的首元素 int ia[3][4] = {{ 0 }, { 4 }, { 8 }}; // 显式地初始化第一行, 其他元素执行值初始化 int ix[3][4] = {0, 3, 6, 9}

遍历元素:

int ia[rowCnt][colCnt]; for (size_t i = 0; i != rowCnt; ++i) { for (size_t j = 0; i != colCnt; ++i) { ia[i][j] = i * colCnt + j; } } // 使用范围for语句 size_t cnt = 0; for (autp &row : ia) // 对于外层数组每一个元素 for (auto &col : row) { // 对于内层数组每一个元素 col = cnt; ++cnt; }
最新回复(0)