C++ primer学习笔记之字符串,向量和数组

这里是学习C++ primer做的笔记

将介绍两种重要的标准库类型:string和vector。string表示可变长的字符序列,vector存放的是某种给定类型对象的可变长序列。

命名空间using声明

作用域操作符::的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。因此,std::cin的意思就是使用命名空间std中的名字cin。单只这种方法比较繁琐,本节使用一种最安全的方法,即使用using声明,有了using声明就无需专门的前缀,也能使用所需的名字了。using声明使用如下形式:

1
using namespace::name;

比如:

1
2
3
4
5
6
7
8
9
#include<iostream>
using std::cin;
int main(){
int i;
cin>>i;//正确,cin和std::cin含义相同
cout<<i;//错误,没有对应的using声明,必须使用完整的名字
std::cout<<i;//正确
return 0;
}

每个名字都需要独立的using声明,用到的每个名字都必须有自己的声明语句,而且每句话都得以分号结束。
头文件的代码一般来说不应该使用using声明,这是因为头文件的内容会拷贝到所有引用它的文件中,如果头文件里使用某个using声明,那么每个使用了该头文件的文件都会有这个声明,对于某些程序来说,反而会产生始料未及的名字冲突。

string

使用string都需要包含以下代码:

1
2
#include<string>
using std::string;

定义和初始化string对象

初始化string的几种方式:

1
2
3
4
5
string s1;//默认初始化,s1是一个空串
string s2(s1);//s2是s1的副本
string s3("value");//s3是字面值"value"的副本,除字面值最后那个空字符外
string s2="value";//等价s3("value")
string s4(n,'c');//把s4初始化为由连续n个字符c组成的串

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

string对象上的操作

在执行读取string操作时,string对象会自动忽略开头的空白,并从第一个真正的字符开始读起,直到遇见下一处空白为止。如下:

1
2
3
string s;
cin>>s;
cout<<s<<endl;

如果程序的输入是" Hello World!",则输出的将是"Hello",输出的结果中没有任何空格。

1
2
3
string s1,s2;
cin>>s1>>s2;
cout<<s1<<s2<<endl;

上面的代码如果输入" Hello World ! ",输出的结果是"HelloWorld!"
使用getline来读取一整行,可以保留输入时的空白符,参数是一个输入流和string对象,函数从给定的输入流中读入内容,直到遇到换行符为止。

1
2
3
4
string line;
//每次读入一整行,直到达到文件末尾
while(getline(cin,line))
cout<<line<<endl;

getline函数返回的换行符实际上是被丢掉了的得到的结果中并不包含该换行符。
string的empty根据string对象是否为空返回一个对应的布尔值,size函数返回string对象的长度。
这里提一下size函数,其返回类型是string::size_type,它是一个无符号类型的值,因此在比较的时候就不要使用int类型了,比如如果n是一个具有负值的int,表达式s.size()<n的结果肯定是true,这是因为负数n会自动转换为一个比较大的无符号值。
这里讲一下字面值和string对象相加,初始化string对象时,必须确保加法运算符的两侧有至少有一个string,而不都是字面值,也就是说不能把字面值直接相加

1
2
3
4
string s1="hello",s2="world";
string s4=s1+",";//正确,s1为string对象
string s5="hello"+",";//错误,两个运算对象都不是string
string s7="hello"+","+s2;//错误,不能把字面值直接相加

处理string对象中的字符

cctype头文件中的函数

1
2
3
4
5
6
7
isalnum(c);//当c是字母或数字时为真
isalpha(c);//当c是字母时为真
isdigit(c);//当c是数字时为真
islower(c);//当c是小写字母时为真
isupper(c);//当c是大写字母时为真
tolower(c);//当c是大写字母,输出对应的小写字母,否则原样输出
toupper(c);//当c时小写字母,输出对应的大小字母,否则原样输出

如果想要改变string对象中的字符的值,在使用for循环中,必须把循环变量定义成引用类型。

标准库类型 vector

标准库类型vector表示对象的集合,其中所有对象的类型都相同,其被称为容器,使用vector,必须包含头文件,如下:

1
2
#include<vector>
using std::vector;

c++中既有类模板,也有函数模板,其中vector就是一个类模板。

定义和初始化vector对象

如下几种初始化vector的方法

1
2
3
4
5
6
vector<T> v1;//v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1);//v2中包含有v1所有元素的副本
vector<T> v3(n,val);//v3包含了n个重复的元素,每个元素的值都是val
vector<T> v4(n);//v4包含了n个重复执行了值初始化的对象,比如n个0
vector<T> v5{a,b,c...};//v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v6={a,b,c...};//等价于v5{a,b,c...}

向一个vector对象中添加元素

对于vector对象来说,直接初始化的方式适用于数量少,初始值是另一个vector对象的副本,所有元素的初始值都一样的情况,但是一般我们不知道实际需要的元素个数。这里使用成员函数push_back向其中添加元素,负责把一个值当成vector对象的尾元素“压到”vector对象的“尾端”

1
2
3
vector<int> v2;
for(int i=0;i!=100;++i)
v2.push_back(i);

其他vector操作

一些重要的操作

1
2
3
4
5
6
7
v.empty(); //判断是否为空
v.size();//返回v中的元素个数
v.push_back(t);//向v的尾端添加一个值为t的元素
v[n];//返回v中第n个位置上元素的引用
v1=v2;//用v2中元素的拷贝替换v1中的元素
v1={a,b,c...};//用列表中元素的拷贝替换v1中的元素
v1==v2;//v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同

迭代器介绍

string和vector都支持迭代器,迭代器类拥有名为beginend的成员,讲一下迭代器类型与const

1
2
3
4
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返回const_iterator,如果对象不是常量,返回iterator

1
2
3
4
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,不管vector对象是什么,其返回值都是const_iterator

1
auto it3=v.cbegin();

凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素
迭代器之间的关系运算(>,>=,<,<=)的含义:比如某迭代器指向的容器在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一个元素。

数组

与vector的区别在于数组的大小是确定不变的,对于某些特殊的应用来说程序运行时性能较好,但是相应的也损失了一些灵活性。

定义和初始化内置数组

初始化时数组维度应该是一个常量

1
2
3
4
5
unsigned cnt=42;
constexpr unsigned sz=42;
int arr[10];//含有10个整数的数组
int *parr[sz];//含有42个整形指针的数组
string bad[cnt];//错误,cnt不是常量表达式

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

1
2
3
4
const unsigned sz=3;
int ial[sz]={0,1,2};
int a3[4]={1,2,3};
int a2[]={0,1,2};

字符数组中如果直接用字符串来赋值的话,注意字符串字符的结尾处还有一个空字符,会像其它字符串字符一样被拷贝到字符数组中。

1
2
3
char a1[]={'C','+','+'};
char a2[]="c++";
char a4[6]="daniel";//错误,因为还有一个空字符`\0`没有空间存放

数组中不允许赋值和拷贝

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

下面是比较复杂的数组声明

1
2
3
4
int *ptrs[10];//ptrs是一个含有10个整形指针的数组
int &refs[10]={1,2,3,4};//错误,不存在引用的数组
int (&arrRef)[10]=arr;//这里arrRef引用一个含有10个整形的数组,这里和上面是不同的,这里引用的是arr,而上面显然都不知道引用的是什么,正确的方式应该是对已有的数组名的引用
int (*Parray)[10]=&arr;//此处下面讲解

这里对于数组而言,由内向外阅读要比从右向左阅读好多了,举上面最后一个例子:首先是圆括号获取来的部分,*Parray意味着Parray是个指针,接下来观察右边,可知道Parray是个指向大小为10的数组指针,最后观察左边,知道数组中的元素是int,这样即:Parray是一个指针,指向一个int数组,数组中包含10个元素。
再比如下面一句:

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

首先array是一个引用,是ptrs的引用,然后先跟[]结合,表示是一个数组,再看*,表示是一个指向int的指针(int *结合)

访问数组元素

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

指针和数组

对数组元素使用取地址符就能得到指向该元素的指针,在使用数组名的地方,编译器会自动将其替换为一个指向数组首元素的指针。

1
2
3
string nums[]={"one","two","three"};
string *p=&nums[0];//p指向nums的第一个元素
string *p2=nums;//等价p2=&nums[0]

指针也是迭代器,vector和string的迭代器支持的运算,数组的指针全部支持。虽然如此,但是指针的迭代极易出错,为了让指针的使用更简单,安全,C++11引入两个名为beginend的函数,这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,正确的使用方法是将数组作为它们的参数:

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

C风格字符串

下面是C风格字符串的函数,这里的p是指针,字符数组的首地址。

1
2
3
4
strlen(p);//返回p的长度,空字符不计算在内
strcmp(p1,p2);//比较p1和p2的相等性,如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值。
strcat(p1,p2);//将p2附加到p1之后,返回p1
strcpy(p1,p2);//将p2拷贝给p1,返回p1

这里使用的时候传入此类函数的指针必须以空字符作为结束的数组:

1
2
char ca[]={'C','+','+'};
cout<<strlen(ca)<<endl;//错误,ca没有以空字符结束

strlen函数将有可能沿着ca在内存中的位置不断的寻找,直到遇到空字符才停下来。
这里讲了C风格的字符数组和标准库string的对比,可以知道对于字符数组,在使用的时候,一旦所存得内容改变,就必须重新检查其空间是否足够,而且这类代码充满了风险,经常导致严重的安全漏洞。使用标准库string要比使用C风格字符串更安全,更高效。

与旧代码的接口

使用数组初始化vector对象:前面讲到不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组,相反,允许使用数组来初始化vector对象。

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

上面代码中,用于创建ivec的两个指针实际上指明了用来初始化的值在数组arr中的位置,其中第二个指针应该是指向待拷贝区域尾元素的下一个位置。
现在的C++程序应该尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的机遇数组的字符串。

完!(20180123)

如果觉得有帮助,给我打赏吧!