C++ Primer 学习笔记——第三章

第三章 字符串、向量和数组

前言

标准库是C++必不可少的一部分,作为C++的延伸,标准库的优雅令人陶醉。

如标题所言,在这一章我们将要学习数组、字符串和向量。若学习过其他编程语言,相信对数组并不陌生。其作为固定存储序列,能够为我们提供很多数据结构的解决思路,但是其在灵活性方面的不足使之诞生向量:向量,给定类型对象的可变长序列。

同时,也会对字符串做一些解释。


3.1 命名空间的using声明

在之前的编写代码的过程中,对std并不陌生:

std::cin;  
std::cout;  

“std”属于是命名空间,“::”作用域操作符则表明:编译器应该从操作符左侧名字所示的作用域中寻找右侧名字。

由于每次使用命名空间中的成员都需要显式添加“std::”,较为繁琐。所以C++提供一种方式:使用“using”声明(using declaration)。

其具体形式:using namespace::name;,例如:

using std::cin; /* 当我们声明后,之后使用cin无需再添加std:: */  

注意

头文件不应当包含using声明


3.2 标准库类型 string

string,字符串类型,是一种可变长的字符序列。

具体示例:

string str_1; /* 默认初始化,为空串 */  
string str_2(str_1); /* str_2为str_1的副本 */  
string str_3=str_1; /* 等价于str_2(str_1) */  
string str_4("this"); /* str_4是字面值“this”的副本(删去字面值最后那个空字符)*/  
string str_5="this"; /* 等价于str_4("this") */  
string str_6(n,"this"); /* 将str_6初始化为由n个“this”组成的串 */  

由上面示例可以看出,在string初始化上分为两种:

  • “string str_1="1"”的方式称为拷贝初始化(copy initialization)
  • “string str_1("1")”的方式称为直接初始化(direct initialization)

在设计类的时候,我们不光需要规定其初始化对象的方式,还需要定义对象上能够执行的操作。

在这方面,string有如下操作:

操作 作用
os<<s 将s写到输出流os中,返回os
is>>s 从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is,s) 从is中读取一行赋给s,返回is
s.empty() s为空返回true,反之返回false
s.size() 返回s中字符个数
s[n] 返回s中第n个字符的引用,n从0开始
s1+s2 返回s1和s2的字符串合并的字符串
s1=s2 将s2的副替换s1中原本的字符
s1==s2,!=,<,<=,>,>= s1与s2字符串做比较,大小写敏感

我们曾通过使用iostream中的标准输出/输出来读写内置类型,同样其也能用于对string类型的读写。

例如:

std::string str;  
std::cin>>str;  
std::cout<<str;  

需要注意的是:从标准输入流中读取到string对象时,string对象会删除开头空白,同时遇到空白结束读取。

参考上述代码,若输入流为:“ this ”,那么str的内容将是“this”。

若想要读取到空白字符,有两种方式:

std::string str;  
while (std::cin>>str){ /* 方式① */  
  std::cout<<str;  
  if (std::cin.get()=='\n')break;  
}  
  
while (getline(std::cin, str)){ /* 方式② */  
  std::cout<<str;  
}  

补充

关于“size()”函数,其返回值类型为“string::size_type”类型,可以将其理解为无符号整型数。

注意关键点:无符号。所以“str.size()<n”,n为负整数,那么其结果判断几乎是true,因为n将自动转换一个较大的无符号值。(可以查看第二章的基本内置类型)

同时注意,如果一个表达式中已经存在size()函数就不要再使用int,这样避免混用int和unsigned可能带来的问题。

关于string对象的对比规则,注意点:

  • string对象的对比依赖于对大小写敏感的字典顺序
  • 在对比两对象大小时,若二者字符相同,则进行串长度对比;若二者字符不同,则对比二者第一处相异字符大小。

例如:

string str_1="this";  
string str_2="this is good!";  
string str_3="thity";  

在上述例子中,str_1小于str_2,而str_3比str_1和str_2都大。 原因:

  • 对比str_1和str_2,二者对比所有字符序列,满足较短者与较长者相同则比较长度,明显str_2更长。
  • 将str_3与前两者做对比,“thi”序列都相同,比较“s”和“t”的大小,显然“t”更大,所以str_3大于前两者。

关于将字面值与string对象相加,存在一个注意事项:

我们运行将string对象与字面值相加,但是不允许仅存在字面值:string str="this"+" is world"
这种方式是错误的!究其原因,“this”和“is world”都不是string对象,不允许相加。

同时,我们也不允许这样的用法:

string str_1="word";  
string str_2="this"+" is "+str_1; /* 错误 */  

其原因和上述的注意一样,我们可以这样看str_2的相加过程:str_2=("this"+" is ")+str_1;,这与上述错误一样!

补充

或许在其他程序设计语言,例如python上可以使用字面值相加的方式为string类型赋值。但是由于历史原因:为了和C兼容,C++的字符串字面值并不是string对象。也就是说:字面值和string是两种类型!,这也就解释上述的注意点。

当我们想要处理string对象中单个字符时,其往往需要库来处理它们。

例如:cctype库:

函数名 解释
isalnum(c) 当c为字母或数字时,返回true
isalpha(c) 当c为字母时,返回true
iscntrl(c) 当c是控制字符时,返回true
isdigit(c) 当c是数字时,返回true
isgraph(c) 当c是可见字符(即不是空格且可打印)时,返回true
islower(c) 当c是小写字母时,返回true
isprint(c) 当c是可打印字符时,返回true
ispunct(c) 当c是标点符号时,返回true
isspace(c) 当c是空白时,返回true
isupper(c) 当c是大写字母时,返回true
isxdigit(c) 当c是十六进制字符时,返回true
tolower(c) 当c是大写字母时,转换为小写字母
toupper(c) 当c是小写字母时,转换为大写字母

补充

cctype头文件与C语言的ctype.h头文件内容一致,只不过使用了C++版本的C标准头文件。在C++版本下:将会将.h删除且在文件名前加上c

在使用这些C语言标准库时,应该遵循C++标准下的头文件名,而非使用C语言的头文件名。使用C++标准下的C标准库名,其库中名字总能够在命名空间std下找到,而使用C标准头文件命名则不一定。

对于想要处理每一个字符,在这里介绍一种新的遍历方式:范围for(range for)语句

具体的语法形式为:

for (declaration:expression)  
  statement;  

解释:

  • declaration,声明。用于定义一个变量,该变量用于访问序列中的基础元素。一般情况下,使用auto定义该变量。
  • expression,表达式。表示目标对象,一般为一个序列。
  • statement,称述。对对象进行操作。

示例:

string str = "Hello World!";  
for(auto i: str)  
  cout << i <<" ";  

当然,我们也可以写一个较复杂的语句:

#include <iostream>  
#include <cctype>  
#include <string>  
  
int main(){  
/**  
* 统计语句中的标点符号数目  
*/  
std::string str_1="Hello,World!";  
std::string str_2("你好,世界!");  
  
/* 定义number用于统计数目,使用decltype保证其类型与string对象长度类型一致 */  
decltype(str_1.size()) number_1=0;  
decltype(str_2.size()) number_2=0;  
  
for(auto i:str_1)  
if(std::ispunct(i)) /* ispunct函数检测是否为标点符号,位于cctype库,注意:检测中文则需要单独设置locale */  
  ++number_1;  
  
for(auto c:str_2)  
if(std::ispunct(c))  
  ++number_2;  
  
std::cout<<"The str_1 is '"<<str_1<<"' and the str_2 is '"<<str_2<<"'\n"  
         <<"The number of punctuation marks for the string str_1 is "<<number_1  
         <<" and str_2 is "<<number_2<<"\n";  
}  

其结果为:

The str_1 is 'Hello,World!' and the str_2 is '你好,世界!'  
The number of punctuation marks for the string str_1 is 2 and str_2 is 0  

上式的注意点:使用decltype关键字来声明number变量,是由于string的size()函数返回类型是特殊的:string::size_type
,这样做到保证能够极限接受string对象中的字符数。

相信对如何操作string对象和如何使用范围for语句有一定了解,那么接下来我们尝试去操作一下如何改变字符串的字符:

将字符串的字母大写:

string str_1="hello,world!";  
for(auto &c:str_1)  
  c=std::toupper(c);  
  
cout<<"The string is now: "<<str_1<<"\n";  

注意点:如果想要改变string对象中字符的值,必须将循环变量(也就是 declaration)定义为引用类型

芜湖,是不是很简单🤩

上式我们尝试将整个字符串大写,但是我们想要对部分字符修改该怎么办呢🧐

有两种方法:使用下标或使用迭代器。

注意

使用下标时,注意其字符串范围([0,size()-1)),使用超出此范围的下标将引发不可知后果。所以在使用下标时必须判断string对象是否为空以及判断其是否超过string对象的范围。

string str="hello,world!";  
  
if(!str.empty()){ /* 使用下标方式 */  
  s[0]=std::toupper(s[0]);  
}  
  
if(decltype(str.size()) index=0; /* 使用下标方式进行迭代 */  
index!=str.size() && !std::isspace(str[index]));++index)  
  s[index]=std::toupper(s[index]);  

3.3 标准库类型vector

标准库类型vector表示对象的集合,其中所有对象的类型都是相同的。通常我们也会称其为容器(container)。

需要注意的是:vector是模板而非类型,所以由vector生成的类型必须包含vector中元素的类型。

初始化vector对象

方法 解释
vector<T> v1 v1表示一个空vector,其潜在元素为T,执行默认初始化
vector<T> v2(v1) v2中包含v1的副本
vector<T> v3(n,val) v3中包含n个重复的元素,每个元素的值都是val
vector<T> v2=v1 等价于vector<T> v2(v1)
vector<T> v4(n) v4包含了n个重复地执行了值初始化的对象

除此之外,还有这两种方式:

  • vector<T> v5{a,b,c...} v5包含了初始值个数的元素,每个元素被赋予了相应的初始值
  • vector<T> v5={a,b,c...} 等价于上式

在C++ 11标准下,还有新的初始化方式:vector<T> v6={"a","b","c"}

注意

对vector初始化需要考虑初始化方式:
例如:vector<int> v1(10)就表明我初始化10个元素,每个元素的值都是默认值0。但是当我们不采用标准的方式初始化那么编译器则会尝试使用默认值初始化vector对象。例如:

vector\<string> v1{10}; /* v1具有10个默认初始化的元素 */  
vector\<string> v1{10,"hi"}; /* v1具有10个值为“hi”的元素 */  

向vector对象添加元素

有些时候,我们并不能获知vector对象的所有值,所以在这个时候我们就需要使用push_back功能来为对象添加元素。

push_back的运作方式:

将一个值当做vector对象的尾元素“压入”到vector对象的尾部中。形象的来讲就是push to back。

例如:

std::vector<int> v;  
  
for(int i=0;i<10;i++){  
  v.push_back(i); /* v对象中将存在0~9共10个依次排列元素 */  
}  

补充与注意

在我们使用其他程序设计语言时,也许会在一开始便为存储对象赋予其大小。但是这在使用vector对象上是没必要的更甚可以说是有损性能的。

在C++的标准下:vector能够在运行时高效的添加元素。这样就显得在初始化时设定其大小是无意义的。(当然也有例外:vector中所有元素值都是一样的)

所以,建议在使用vector时,应该在开始阶段仅创建空的vector对象,在运行时才动态添加元素。

同时还需要注意一点:无法通过使用范围for循环为vector添加元素,因为范围for针对已经确定其容器大小的对象进行遍历,无法改变遍历对象本身的大小。

注意点: 不能通过下标方式为vector添加元素

其他vector操作与技巧

vector作为C++中常用的容器,当然不仅仅只存在push_back这一个操作,还有:

操作名 解释
v.empty() 如果v不含有任何元素,则返回真
v.size() 返回v中个数(而不是下标数)
v.push_back(t) 在v的尾部压入值t
v[n] 返回v中第n个位置上元素的引用
v1=v2 将v2中元素拷贝替换v1中元素
v1==v2 比较v1和v2相等,其元素数目和对应位置元素值皆相等
<,<=,>,>= 以字典顺序比较

在C++中还提供一种方式:使用列表方式替换vector对象中列表内容:v1={a,b,c...}

补充

缓冲区溢出(buffer overflow),指通过下标访问不存在元素的行为所导致的错误后果。由于其无法被编译器所发现,且运行时会产生一个不可预知值(即垃圾值),导致运行在应用程序上出现严重安全错误。

示例:

vector\<int> array(10,0);  
std::cout<<array[10]; /* buffer overflow */  

在本书中提到:确保下标合法的一种有效手段就是尽可能使用范围for语句

当然,可以使用不固定容器的大小的vector对象来存储元素(这是较优解),那么访问不存在元素的下标返回值便为默认设定值。例如:vector<int> array默认返回值为0

3.4 迭代器

在此之前,我们通过下标运算符访问string或vector对象元素,但是下标运算符并不通用(指只有少数几种标准库容器才支持该运算符)。对此我们可以使用“迭代器”这一通用机制(所有的标准库容器均支持迭代器),迭代器类似于指针类型,其本身支持对对象的间接访问。

使用迭代器

这里介绍两个成员:beginend

begin用于返回容器中第一个元素,end用于返回容器最后一个元素的下一个位置。当容器为空时,其begin和end为同一值。

迭代器运算符

运算符 解释
*iter 返回迭代器iter所指元素的引用,解引用
iter ->mem 解引用iter并获取该元素的名为mem的成员,等价于(*item).mem
++iter 令iter指示容器的下一个元素
--iter 令iter指示容器中的上一个元素
iter1==iter2 判断两个迭代器是否相等,如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等

类似于指针类型,可以通过解引用的方式获取其迭代器所指示的元素,但是试图解引用尾后迭代器是非法的!

通过使用“--”递减运算符和“++”递增运算符实现迭代器的移动,但是需要注意的是无法对尾后迭代器进行递增操作。

示例:

for(auto iter=str.begin(); iter!=str.end(); ++iter){  
  *iter = toupper(*iter);  
}  

补充

在C++开发中,我们常用!=来对for循环条件进行判断,虽然使用“<”依旧可行,但是究其原因与“相较于使用下标运算符,更原因使用迭代器”原因一样,这种编码风格在标准库中所有容器上都能生效。为了方便与安全,使用这种风格方式更为可靠。

迭代器类型

关于迭代器类型其实与指针类型类似:

string::iterator iter1; /* iter1能够读写string对象中的元素 */  
string::const iterator iter2; /* iter2能够读string对象中的元素,但是不能够写;常量仅能使用该迭代器 */  

在上面的示例中,我们可以明确的看到迭代器(iterator)的两个类型:iteratorconst iterator

迭代器其本身有三种含义:

  • 迭代器概念本身
  • 迭代器类型
  • 迭代器对象

根据具体使用场景即可分辨。

begin和end运算符

在上文中,我们已经了解了begin和end所指定的对象,但是其返回类型我们可以聊聊:

根据所指向的对象类型,这两个运算符均可返回iteratorconst iterator类型,但是我们通常使用auto来承担迭代器类型,导致在某些情况下,无法具体得知其对象返回类型。同时,在某些操作下:比如某个对象仅需要读操作而不需要写操作,这个时候为了安全考虑:我们必须将其返回值设定为const类型。

这个时候,C++ 11引入两个新的运算符:cbegincend,这两个运算符决定其指示对象的对象类型是否是常量,返回值类型均为const iterator

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

在前文中我们提到:无法在范围for语句中对vector对象添加元素。在此,我们也需要注意到的是任何一种能够改变vector对象容器的操作都会使得该对象的迭代器失效!

注意

但凡改变了迭代器的循环体,都不要向迭代器所属的容器添加元素。

迭代器运算

类似之前我们提到的vector和string对象所具有的如:!=、==,迭代器也同样提供了一些支持特定功能的运算符,称为“迭代器运算”(iterator arithmetic)

运算符 解释
iter+n/iter-n 迭代器加上一个整数仍然得到一个迭代器。其表示相比原来向后(向前)移动若干个元素。
iter+=n/iter-=n 将iter加上(减去)n的结果赋值给iter
iter1-iter2 两个迭代器之间距离(仅表示迭代器距离,非实际距离)
>、>=、<、<= 比较两个迭代器距离大小,若为真返回1,否则返回0

注意

无论是对两迭代器进行相减还是比较,其迭代器所指向的容器必须为同一个。

补充

两迭代器相减距离指的是被减迭代器需要移动x位到达iter1的位置所需要的值。该值是类型名为“difference_type”的带符号整型数。

在使用自增与自减运算时,iter++与++iter效率却是不同的:

  • ++itr(或–itr)返回引用
  • itr++(或itr–)返回临时对象

究其原因,由于iterator是类模板,使用iter++这种方式会返回一个临时的对象,同时iter++方式进行的是函数重载,编译器无法进行优化。
所以在实际遍历下,每使用一次iter++,就会创建一个临时对象并销毁。在效率上有差距。所以建议在使用自增自减运算符时使用“++i/--i”的方式。

3.5 数组

数组类似于vector的数据结构,其本质还是容器。只不过相较于vector在性能和灵活上做了权衡,数组的大小是固定的,所以假如你并不知道需要存入容器的元素的确切个数,应当选择vector。

定义、初始化数组

数组本质上是一种复合类型。

array[i]; /* array表示数组名,i表示数组的维度 */  

数组的维度说明了数组中元素的个数,所以维度必须大于0。同时由于数组的大小是固定的,所以元素的维度必须预先确定,即维度应当为常量。

unsigned i=12;  
constexper unsigned j=12; /* 常量表达式 */  
int array_1[j]; /* 合法 */  
int *array_2[j]; /* 合法 */  
int array_3[i]; /* 不合法 */  

补充

int *array[n]表示含有n个int类型指针的数组,int (*array)[n]表示含有n个int值的数组指针

说完定义,我们再来谈一谈初始化。与之前的数据结构初始化类似:

当我们不指明维度的个数时,编译器会根据初始值数量进行推算;但是,若编译器指明了维度,如果初始值个数超过了指定大小则会报错。

同时,我们需要注意到一种特殊的数组:字符数组,其除了常规的初始化方式外,字符数组还可以使用字符串字面值进行初始化。例如:

char array_1[]="this"; /* array_1:'t'、'h'、'i'、's'、'\0' */  

需要注意的点:使用字符串字面值进行初始化,字面值结尾处的一个空字符“\0”也会被添加入数组中。

在某些时候,我们认为数组作为对象,应该可以进行赋值、拷贝操作。但这是严禁的!不允许将数组的内容拷贝给其他数组作为其初始值,也不能使用数组为其他数组进行赋值操作。

补充

也许你在其他编译器上进行数组的赋值、拷贝操作并未出现问题,但是不能够保证使用其他编译器编译你的程序时不会出现问题。编译器是否能够支持该操作取决于编译器扩展(compiler extension),一般来讲,我们最好是选择避免使用非标准特性。

有些时候,对数组的声明看起来有些不友好,例如你能区分指向数组的指针和存放指针的数组吗?

这里我们遵循一个阅读语句的规律:从内向外,从右向左

例子:

int *array_1[10];  
int &array_2[10]=array;  
int (*array_3)[10]=&array;  
int (&array_4)[10]=array;  
int *(&array_5)[10]=array;  

我们一一解答:

  • 第一个,遵循从右向左读,那么首先定义一个大小为10的数组,其名字叫array_1,同时根据“int *”明白其存放的是int类型指针。所以,该语句解释为:存放10个int类型指针的数组。
  • 第二个,依照上述,首先定义一个大小为10的数组,名字为array_2,根据“&”明白该数组中元素为引用,但是数组的元素应当为对象,不存在引用情况,所以第二个是错误的。
  • 第三个,如果我们仅依靠从右向左读的方式理解有些模糊,所以这个时候我们就要借用从内向外读的方式:首先我们先读括号内部,“*array_3”表明一个名为array_3的指针,之后从右向左读,定义一个大小为10的数组,其指针array_3指向该数组。所以,该语句解释为:一个指向存放了10个int类型值的数组的指针。
  • 第四个,同样的道理,我们可以得出该语句解释为:一个存放10个int类型值的数组的引用对象。
  • 第五个,本质上就是第三个和第四个的结合,应当解释为:一个存放10个int类型指针数组的引用对象。

访问数组元素

同vector和string数据结构,数组也可以通过下标的方式访问数组元素,其下标定义为size_t类型。(size_t是一种与机器相关的无符号类型,其被设计得足够大)。该类型在cstddef头文件中被定义。

指针和数组

在C++中,指针和数组关系非常密切,例如:编译器通常将数组转换为指针。

由于数组和数组中的元素本身就是对象,所以我们可以使用指针来指向数组或数组中的元素:

int num[]={1,2,3};  
int *p=&num[0];  

同时,数组还具有一个特性:在很多用到数组名字的地方,编译器都会将其自动替换为指向数组首元素的指针。所以在大多数表达式中,使用数组类型的对象实际上是使用一个指向该数组首元素的指针。

综上所述,在一些情况下,我们对数组的操作其实是对指针的操作,例如:

int i[]={1,2,3};  
auto i2(i); /* i2其实是“int *” */  

注意

使用decltype关键字并不会发生上述转换,而是依照下面的转换规则:

int i[]={1,2,3};  
decltype(i) i2={1,2,3}; /* decltype(i)转换为:int[3] */  

decltype关键字会按照实际情况进行转换:i为存放3个int类型元素的数组,那么decltype关键字按照i的类型转换为“int[3]”。

指针也是迭代器

我们在之前了解了迭代器,其实会发现迭代器与指针工作方式相似。

其实指针也是迭代器,在之前提及的迭代器操作,数组指针全部支持。

我们之前使用到的begin和end方法,在C++ 11中得到,例如:

int i[]={1,2,3};  
int *begin=begin(i); /* 指向i首元素的指针 */  
int *end=end(i); /* 指向i尾元素下一位置元素的指针 */  

由于这两个函数在“iterator”头文件中被定义,所以与上述我们对vector进行迭代操作使用迭代器的方式不相同。

在C++ 17中还定义有cbegin和cend两个函数。

当两个指向同一个数组的指针相减时,其结果是它们二者之间的距离:

int arr[]={1,2,3};  
auto distance=end(arr)-begin(arr); /* distance的值为3 */  

得到的distance的类型为ptrdiff_t的标准库类型,和size_t类型,ptrdiff_t也是被定义在cstddef头文件中的机器相关的类型,只不过该类型是带符号类型,而size_t为无符号类型。

C风格字符串

注意

虽然C++支持C风格字符串,但是最好不要使用!

何为C风格字符串(C-style character string)?

C风格字符串是为了表达和使用字符串而形成的一种约定俗成的写法。此写法习惯于将书写的字符串存放在字符数组中并以空字符结束(null terminated)。即在字符串的最后一个字符后面添加一个“\0”。

首先,先了解“cstring”头文件,在此C++版本的string.h头文件中:

函数 解释
strlen(p) 返回p的长度,空字符不在计算内
strcmp(p1,p2) 比较p1和p2的想等性,若等于,返回0;若p1>p2,返回正值;若p1<p2,返回负值
strcat(p1,p2) 将p2附加到p1后,返回p1
strcpy(p1,p2) 将p2拷贝给p1,返回p1

需要注意的是:传入此类函数的指针必须指向一空字符结尾的数组,若字符数组未以空字符结尾,那么函数将有可能继续沿着数组所在内存中的位置不断查找,直到遇见空字符。

与旧代码的接口

由于C++诞生与1983年8月,而C++标准则诞生与1999年1月,所以为了与过去的C++程序进行连接,于是诞生了与旧代码的接口。

混用string对象和C风格字符串

在前文介绍string时,我们提到过使用字符串常量初始化string对象:

string str("this time");  

为了与之前的C风格字符串做连接,这种方式更为一般:

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。(准确来讲,假如字符数组中没有空白字符,初始化string对象时会从数组中依次查找空白字符直到找到,所以哪怕字符数组中最后结尾的不是空白字符也能成功初始化,只不过string对象的字符串可能多了些不确定值)
  • 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能够两个都是);在string对象的复合赋值运算中允许使用以空白字符结束的字符数组作为右侧的运算对象。

示例:

const char str_1[] = {'1', '2', '3', '\0'};  
const char str_1_c[] = {'9', '8', '7'};  
std::string str_2;  
str_2 = str_1;  
std::cout << str_2 << "\n"; /* 123 */  
std::string str_3 = str_1 + str_2;  
std::cout << str_3 << "\n"; /* 123123 */  
std::string str_4 = str_3;  
str_4 += str_1;  
std::cout << str_4 << "\n"; /* 123123123 */  

但是,这种对旧代码的连接只适合以字符数组初始化、赋值string对象,不能反过来。为了实现反过来的功能,string提供名为“c_str”的成员函数。

c_str函数的返回值是一个C风格的字符串。

示例:

std::string str_5 = "this is good time";  
const char *str_6 = str_5.c_str();  
for (auto i = str_6; *i != '\0'; ++i) { std::cout << *i; } /* this is good time */  

因为c_str函数会创建一个以空白字符结束的内容与string对象一致的字符数组并返回该数组的首元素指针。所以我们通过const保存该首元素指针用来读取该字符数组。

但是我们无法保证c_str函数返回的数组一致有效,也就是说只要后续我们修改了string对象,那么返回数组就可能会失效。所以最好的方式还是需要拷贝一份数组。

使用数组初始化vector对象

实际上,用数组初始化vector对象的方式,我们应该遇见过:

int array[]={1,2,3};  
vector<int> vector(begin(array),end(array));  

是不是很简单😆

建议

虽然我们可以通过对于旧代码的接口、下标和数组的方式对容器进行操作,但是建议尽量使用标准库类型而非数组。

指针常用于底层操作,当你对指针不熟练时常常会引发未知的、难以查找、难以调试测试的错误😅。同时你也可能因为对语法的不熟悉或疏忽导致错误,特别是对指针的声明。

现如今,我们应当输入使用迭代器和vector来替代数组,避免使用内置数组和指针。 同时我们也因当尽量使用string,而非C语言风格字符串。

多维数组

从本质上来讲,多维数组其实是数组的数组,并不存在多维之说。

在使用范围for语句处理多维数组时,需要注意到对循环控制变量应当采用引用类型:

constexpr size_t rowCnt=3,colCnt=4;  
int cnt=0;  
int array[rowCnt][colCnt];  
for(auto &row: rowCnt)  
  for(auto &col:colCnt){  
    col=cnt;  
    ++cnt;  
  }  

这里使用引用的原因是编译器可能会将控制变量自动转化为指针,例如,若row不设置为引用类型,那么row将转化为第二维数组的首元素指针。那么,内层范围for语句就无法执行。

注意

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

关于指针和多维数组

遍历多维数组,如果使用下标的方式遍历往往会导致我们对多维的数组不清晰,通过使用C++ 11的新特性auto来解决此问题:

for(auto p=ia;p!=ia+3;++p)  
for(auto q=*p;q!=*p+4;++q){  
  cout<<*q<<" ";  
}  

当然,我们可以使用迭代器来解决指针复杂的问题:

for(auto p=begin(ia);p!=end(ia);++p)  
  for(auto q=begin(*p);q!=end(*p);++q)  
    cout<<*q<<" ";  

如果厌倦指针,那么我们还可以使用类型别名的方式让遍历简单一些:

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<<" ";  
  }  
}  

总结

学习到两类标准库类型:string和vector,string用于存放可变长的字符序列,vector用于存放同类型对象。

通过迭代器我们可以间接访问容器,特别是对string和vector还可以在元素之间移动。

一般来讲,我们应该尽量少用数组,对于旧C++代码(标准库出现之前)我们可以使用与旧代码的接口使用vector和string替代数组和C风格字符串。

posted @ 2023-05-19 12:53  木木亚伦  阅读(83)  评论(0编辑  收藏  举报