0x02_字符串、向量和数组
字符串、向量和数组
命名空间的using声明
std::cin表示从标准输入中读取内容,此处使用作用域操作符(::)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。因此,std::cin的意思就是要使用命名空间std中的名字cin。
最安全的方式是使用using声明来更简单的使用命名空间中的成员:
using namespace::name; #include <iostream> using std::cin; int main() { int i; cin >> i; cout << i; std::cout << i; return 0; }
按照规定,每个using声明引入命名空间中的一个成员。位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
string表示可变长的字符序列。
定义和初始化string对象
如何初始化类的对象是由类本身决定的。一个类可以定义很多种初始化对象的方式,只不过这些方式之间必须有所区别:或者是初始值的数量不同,或者是初始值的类型不同。
直接初始化和拷贝初始化:
如果使用等号初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象种去。与之相反,如果不使用等号,则执行的是直接初始化。
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:
string s5 = "hiya"; // 拷贝初始化 string s6("hiya"); // 直接初始化 string s7(10, 'c'); // 直接初始化
string对象上的操作
在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。
有时候希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定输入流中读入内容,直到遇到换行符为止(注意此时换行符也被读入缓冲),然后把所读的内容存入到那个string对象中去(此时不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,如果输入一开始就是换行符,那么所得的结果是个空string。
和输入运算符一样,getline也会返回它的流参数,可以以getline的结果作为判断条件。
int main() { string line; while (getline(cin, line)) cout << line << endl; return 0; }
触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。
对于size函数来说,其返回值是一个string::size_type类型的值。
string类及其他大多数标准库类型都定义了集中配套的类型。这些配套类型体现了标准库类型与机器无关的特性,size_type即是其中一种。在具体使用时,通过作用域操作符来表明名字size_type是在类string中定义的。
尽管不清楚string::size_type类型的细节,但有一点是肯定的:它是一个无符号类型的值而且能存放下任何string对象的大小,所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。
如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
字面值和string对象相加:
因为标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来替代,当把string对象和字符字面值及字符串字面值混淆在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string:
string s4 = s1 + ","; // 正确:把一个string对象和一个字面值相加 string s5 = "hello" + ","; // 错误:两个运算对象都不是string string s6 = s1 + "," + "world"; // 正确:每个加法运算符都有一个运算对象是string string s7 = "hello" + "," + s2; // 错误:不能把字面值直接相加
处理string对象中的字符
在cctype头文件中定义了一组标准库函数处理这部分工作:
C++11新标准提供一种语句:范围for语句,这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作,其语法形式是:
for (declaration : expression) { statement }
如果想在范围for语句中改变string对象中字符的值,必须把循环变量定义成引用类型。
string s("Hello World!!!"); for (auto &c : s) c = toupper(c);
标准库类型vector
C++既有类模板,也有函数模板。模板本身不是类或函数,可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数内置类型和类类型都可以构成vector对象,甚至也可以是vector。
定义和初始化vector对象
通常情况下,可以只提供vector对象容纳的元素数量,此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中的元素的类型决定。如果vector对象的元素是内置类型,如int,则元素初始值自动设为0,如果元素是某种类类型,如string,则元素由类默认初始化。
列表初始值还是元素数量?
某些情况下,初始化的真实含义依赖于传递初始值时用的花括号还是圆括号。
vector<int> v1(10); // v1有10个元素,每个的值都是0 vector<int> v2{10}; // v2有1个元素,该元素的值是10 vector<int> v3(10, 1); // v3有10个元素,每个的值都是1 vector<int> v4{10, 1}; // v4有2个元素,值分别是10和1
另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造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"的元素
向vector对象中添加元素
使用vector的成员函数push_back向其中添加元素。如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
C++标准要求vector能在运行时高效快速地添加元素,因此在定义vector对象的时候设定其大小没什么必要,事实上如果这样做性能可能更差。只有一种例外情况,就是所有元素的值都一样。
其他vector操作
要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型。
vector<int>::size_type // 正确 vector::size_type // 错误
只有当元素的值可比较时,vector对象才能被比较。
vector对象以及string对象的下标运算符可用于访问已存在的元素,而不能用于添加元素。
迭代器介绍
所有的标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。
类似于指针类型,迭代器也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。
使用迭代器
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。其中begin成员负责返回指向第一个元素的迭代器,end成员则负责返回指向容器“尾元素的下一个位置”的迭代器。
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
迭代器运算符:
只有string和vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。
迭代器类型:
就像不知道string和vector的size_type成员到底是什么类型一样,一般来说我们也不知道(无须知道)迭代器的精确类型。实际上,拥有迭代器的标准库类型使用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只能读字符,不能写字符
begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果不是常量,返回iterator:
vector<int> v; const vector<int> cv; auto it1 = v.begin(); // it1的类型是vector<int>::iterator auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator
为了便于专门得到const_iterator类型的返回值,C++11标准引入了两个新函数,分别是cbegin和cend:
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator
某些对vector对象的操作会使迭代器失效:
不能在范围for循环中向vector对象添加元素。任何一种可能改变vector对象容量的操作,如push_back,都会使该vector对象的迭代器失效。谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
迭代器运算
string和vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一个位置,就能将其相减,所得结果是两个迭代器的距离。其类型名是:difference_type的带符号整型数,因为距离可正可负,所以difference_type是带符号类型的。
数组
与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。
数组中元素的个数也属于数组类型的一部分,编译时维度应该是已知的,即维度必须是一个常量表达式。默认情况下,数组的元素被默认初始化。定义数组时必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。
可以对数组的元素进行列表初始化,此时允许忽略数组的维度,如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来。
字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。此时要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去。
不允许拷贝和赋值:
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
int a[] = {0, 1, 2}; // 含有3个整数的数组 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个整数的数组
访问数组元素
与标准库类型vector和string一样,数组的元素也能使用范围for语句或下标运算符来访问。使用数组下标时,通常将其定义为size_t类型,size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小(在ccstddef头文件中)。
指针和数组
在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:
string *p2 = nums; // 等价于p2 = &nums[0]
大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组,而当使用decltype关键字时上述转换不会发生,其类型还是数组。
int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // ia是一个含有10个整数的数组 auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素 ia2 = 42; // 错误:ia2是一个指针,不能用int值给指针赋值 decltype(ia) ia3 = {0, 1, 2, 3, ...}; // ia3是一个含有10个整数的数组 ia3 = p; // 错误:不能用整型指针给数组赋值 ia3p4] = 1; // 正确:把i的值赋给ia3的一个元素
指针也是迭代器:
尽管能计算得到尾后指针,但这种用法极易出错。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); // 指向ia尾元素的下一位置的指针
一个指针如果指向了某种内置类型数组的尾元素的下一个位置,则其具备与vector的end函数返回的与迭代器类似的功能。特别注意:尾后指针不能执行解引用和递增操作。
两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型,和size_t一样,ptrfidd_t也是定义在cstddef头文件中的机器相关的类型。
下标和指针:
对数组执行下标运算其实是对指向数组元素的指针执行下标运算:
int i = ia[2]; // ia[2]得到(ia + 2)所指的元素 int *p = ia; i = *(p + 2); // 等价于i = ia[2]
只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算:
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也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,
C风格字符串
字符串字面值是一种通用结构的实例,即C风格字符串。头文件cstring提供了一组函数,可用于操作C风格字符串。
所有的函数不负责验证其字符串参数,充满了风险且经常导致严重的安全泄漏。对大多数应用来说,使用标准库string要比使用C风格字符串更安全、高效。
与旧代码的接口
混用string对象和C风格字符串:
一般的情况是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
- 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算都是);在string对象的复合运算中允许使用以空字符结束的字符数组作为右侧运算对象。
string专门提供了一个名为c_str的成员函数,返回值是一个C风格字符串。即函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,结果指针的类型是const char*,从而确保不会改变字符数组的内容。我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了字符串的值就可能让之前返回的数组失去效用。
使用数组初始化vector对象:
允许使用数组来初始化vector对象,只需指明要拷贝区域的首元素地址和尾后地址:
int int_arr[] = {0, 1, 2, 3, 4, 5}; vector<int> ivec(begin(int_arr), end(int_arr)); vector<int> subVec(int_arr + 1, int_arr + 4);
多维数组
类似于一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化列表之内。如果仅仅想初始化每一行的第一个元素:
int ia[3][4] = {{ 0 }, { 4 }, { 8 }};
其他未列出的元素执行默认值初始化,在这种情况下如果省略内层的花括号,其结果就不一样了:
int ix[3][4] = {0, 3, 6, 9};
含义发生了变化,它初始化的是第一行的4个元素,其他元素被初始化成0。
多维数组的下标引用:
如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组。
ia[2][3] = arr[0][0][0]; // 用arr的首元素为ia最后一行的最后一个元素赋值 int (&row)[4] = ia[1]; // 把row绑定到ia的第二个4元素数组上
使用C++新标准中的范围for语句:
size_t cnt = 0; for (auto &row : ia) { for (auto &col : row) { col = cnt; ++cnt; } }
因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但还有一个深层次的原因:
for (const auto &row : ia) { for (auto col : row) { cout << col << endl; } }
这个循环中并没有任何写操作,但还需要将外层循环的控制变量声明为引用类型,这是为了避免数组被自动转成指针。假设不用引用类型,程序将无法通过编译,这是因为遍历ia的所有元素(实际上是大小为4的数组)时,因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。这样得到的row的类型就是int *,内层的循环就不合法了。
指针和多维数组:
当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针,因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia; // p指向含有4个整数的数组 p = &ia[2]; // p指向ia的尾元素
通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型:
for (auto p = ia; p != ia + 3; ++p) { // p指向含有4个整数的数组 for (auto q = *p; q != *p + 4; ++q) { // q指向4个整数数组的首元素 cout << *q << ' '; } cout << endl; }
类型别名简化多维数组的指针:
using int_array = int[4]; typedef int int_array[4]; for (int_array *p = ia; p != ia + 3; ++p) { for (int *q = *p; q != *p + 4; ++q) { cout << *q << ' '; } cout << endl; }
程序将类型“4个整数组成的数组”命名为int_array。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY