我的《C++ primer》笔记第三章:字符串、向量和数组

写在前面

第一天我们学习了C++内置类型,今天我们来学习一下C++内容丰富的抽象类型标准库。主要学习string和vector库,前者支持可变长字符串,后者则表示可变长集合。还会介绍更为基础的类型:数组,string和vector都是对数组的某种抽象。因为本人技术水平有限,文章难免会出现错误。如有错误恳请您可以批评指出。如果您觉得我写的不错,也可以点赞支持一下。您的鼓励是我前行的最大动力。

3.1 命名空间的using声明

在我们使用标准输入输出函数库时,在main函数里面使用我们经常是这样写的

int n = 0;
std::cin >> n ;

如果涉及到多种输入输出,这样做难免会过于麻烦。于是我们可以在函数外这样定义

using namespace::cin;

因为有时候我们还需要用到cout所以干脆我们就这样定义

using namespace std; //这样我们就可以在main函数里面所有地方使用标准输入输出库时,不需要加std::了

头文件不应该包括using声明

3.2 标准库类型string

3.2.1定义和初始化string对象

我们常用定义并初始化的方法有

string s1;//默认初始化
string s2 = "abc";//拷贝初始化

除了我们经常用的还有一些不经常用的例如

string s3(3,'c');//s3 = "ccc"初始化连续个数相同的字串

直接初始化和拷贝初始化

直接初始化可以理解成

string s1("123");

拷贝初始化可以理解成

string s1 = "123";

如果我们又想用到直接初始化又想用到拷贝初始化我们还可以这样写

string s1 = string("123");

这一段代码的内部实现和昨天学的常量引用非常量相似,都是用了临时量来构建程序。

string temp("123");
string s1 = temp;

3.2.2 对象上的操作

对对象的操作类似于对基础数据类似的操作。大致有以下几种经常使用的操作

代码形式 意义
os< 将s写入os流
is>>s 将is流中的内容写给s
getline(is , s) 将从is中读取一行给s
s.empty() 判断s是否为空
s.size() 返回s中字符个数
s[n] 读取s中第n个字符,n从0开始
s1 + s2 两个字符串进行叠加,s1在前
s1 = s2 将s2的值赋给s1
s1 == s2 判断s1和s2是否相等,相等返回布尔值1否者返回布尔值0
s1 != s2 判断s1和s2是否不相等,不相等返回布尔值1,相等返回布尔值0

读写string对象

读写string对象和读写基础数据类型相似

string s1;//定义s1对象
cin >> s1;//进行写入
cout << s1;//进行读取

注意:string对象会自动忽略开头的空白以及制表符、换行符等

使用getline读取一整行

getline(is , s)可以读取is一整行然后赋值给s,用法如下

string s1;
getline(cin ,s1);

注意getline遇到换行符结束,就算第一个字符为换行符也会结束,且会读入此换行符,但是不会赋给字符串对象可以测试理解下

string s1;
getline(cin , s1);//就按下enter
cout << s1 <<endl;
cout << "测试" << endl;//检测是否读入换行符

string的empty和size操作

empty判断字符串是否为空,如果是空返回布尔型1,否则返回0.

string s1;
cout << s1.empty() << endl;//1
s1 = "word";
cout << s1.empty() <<endl;//0

size返回的值是字符串的字符个数,返回值的类型是一个string::size_type类型(可以简单理解成是一个无符号整型)
size的用法如下

string s1 = "abc";
cout << s1.size() << endl;//输出3

string::size_type类型

size返回类型是一个string::size_type类型,这种类型体现了标准库类型与机器无关的特性。它是一个无符号类型
注意:如果一条表达式已经有size()了就不要使用int类型了,避免因为有无符号问题导致数值判断不准确等一系列问题

int n = -1;
string s1 = "ab";
cout << (n > s1.size()) << endl //输出1因为n被转换成了4,294,967,295

不过还好,现代绝大多数编译器都会提示这个地方有问题(warning)

比较string之间的大小

比较string之间的大小主要遵循两个准则:

  • 1.先比第一个相异字符的大小,按照字典集来对比第一个相异的字符,字典集相对优先的那个字串比较大
string s1 = "aBC";
string s2 = "Abc";
cout << (s1 > s2) <<endl;//1
  • 2.如果没有相异,按照字符数量的大小来决定字符串的大小
string s1 = "ABCD";
string s2 = "ABC";
cout << (s1 > s2) <<endl;//1

此外注意,字符串比对对大小写敏感

字面值和string相加

字符串之间可以相加

string s1 = "ABC";
cout << s1 + "DEF" <<endl;//输出ABCDEF ,"DEF"为字面值

但是如果一条语句中出现两个字面值可能会出现一些问题
下面这一段代码是合法的

string s1 = "ABC";
cout << s1 + "DEF" + "," <<endl;//ABCDEF,

他的内在形式是

string s1 = "ABC";
cout << (s1 + "DEF") + "," <<endl;//ABCDEF,

下面这段代码是不合法的

string s1 = "ABC";
cout << "DEF" + "," + s1 <<endl;//出错

出错原因是两个字符串字面值无法相加!字符串字面形和string是两种类型

3.2.3 处理string对象中的字符

在cctpye头文件中定义了一些标准库函数来给我们处理这方面的工作

用法 意义
isalnum( c ) 当c是字母或者数字的时候为真
isalpha( c ) 当c是字母的时候为真
iscntrl( c ) 当c是控制字符时为真
isdigit( c ) 当c是数字时为真
isgraph( c ) 当c不是空格但可打印时为真
islower( c ) 当c是小写字母时为真
isprint( c ) 当c是可打印字符时候为真(空格或具有可视化形式)
ispunct( c ) 当c是标点时为真
isspace( c ) 当c是空格时为真
isupper( c ) 当c是大写字母时为真
isxdigit( c ) 当c是十六进制数字时为真
tolower( c ) 当c是大写字母,变成小写字母,否则不作改变
toupper( c ) 当c是小写字母,变成大写字母,否则不作改变

在C++中要C的标准库可在标准库前加c,从而可以避免记忆哪些函数是C标准库里面的,又哪些函数是C++里面的,如 和前者是用C++时的定义,后者则是纯C的定义(当然你在C++里面也可以这样用但是就要记忆多一些东西了)。

  • 为什么要多记忆一点东西呢?因为在C++中调用cxxx标准库函数在命名空间std里面,不用再探他的由来。如果不是cxxx而是xxxx.h则需要牢记哪些函数是从C语言继承过来的了

处理每一个字符?使用基于范围的for语句

在C++11中定义了一种基于范围的for语句具体表现为

for (declaration : expression)
		statement

可能你不太懂英文,以下例子可以帮你更好理解

string s1 = "ABC";
for(auto i : s1)
	cout << i ;

上述程序执行完后,输出的结果是ABC!没错,这是不需要判断范围大小直接遍历整个对象内容的用法!

使用范围for语句改变字符串中的字符

在本小节前面,我们提到了cctype库中有很多关于字符串中的字符相关的函数,下面我们来用其中的一个toupper来将字符串全部大写化

string s1 = "aBc";
for(auto &i :s1)
	i = toupper(i);
cout << s1 <<endl; 

只处理一部分字符?使用下标进行迭代!

和前面提到的数组相似,我们也可以用下标来处理我们想要处理的那部分字符,下标和数组类似从0开始,并且是string::size_type类型,没错!就是无符号整型!例如:

string s1 = "Abc";
for(decltype(s1.size()) i = 0 ; i != s1.size() - 1; ++i)
{
	s1[i] = toupper(s1[i]);
}
cout << s1 << endl;//输出ABc  ,第三个字符并没发生改变

我们在使用下标(索引)运算时,下标数字会自动转为无符号型,所以得保证我们使用下标时不要使用负数,且我们还必须要保证下标对应的对象中的内容必须要存在,不然会发生意想不到的错误
注意:C++并不会帮我们检查内容是否存在

3.3 标准库类型vector

在我们存储同一类数据类型数据的时候,我们可以用到容器,例如数字1 、 2 、 3我们可以存入到整型容器,通过索引来访问它们。
在C++中我们使用容器我们得加上头文件

#include 

C++中有函数模板,还是类模板。这里面容器是类模板
注意:模板本身不是函数或者类,他们只是编译器生成类或者函数的一类声明。因此我们在使用它们的时候需要实例化它们
我们容器里面包括的东西是对象,C++中绝大多数对象都能包括,甚至可以包括vector

vector<vector<int>> v1 ;

但是我们需要注意,引用不是对象,因此容器里面不能包含引用。

3.3.1 定义和初始化vector对象

vector最常用的一些定义方法如下。

语法 意义
vector v1 v1是一个空的vector对象,类型为T
vector v1(v2) 将v2内容拷贝给v1
vector v1 = v2 将v2的内容拷贝给v1
vector v1(n , val) v1含有n个val
vector v1(n) v1有n个经过默认初始化的对象
vector v1{a , b , c} 定义了三个对象a,b,c存入到v1中
vector v1 = {a , b , c} 定义了对象a,b,c存入到v1中

列表初始化vector对象

C++11中定义了一种列表初始化vector对象的方法

vector<int> v1 = {1,2,3};

C++中有几种不同的初始化方式

  • 1.拷贝初始化,只能提供一个初始值
  • 2.如果提供的是一个类的初始值,只能提供拷贝初始化或者花括号初始化
  • 3.如果提供的是初始化列表,只能把初始化列表放进花括号里,不能放入圆括号内。(类似上面那段代码)

创建指定数量的元素

创建指定数量的元素就要用到圆括号了。例如

vector<int> v1(10,1);//创建十个对象,并都初始化为1
string s1(10,"1");//创建10个1的字符串

值初始化

如果vector的类型为内置类型,如int等,则默认初始化为0,。
如果是某种类的类型,由类默认初始化。

这里插一段错误的vector定义用法

vector<int> v1 = 10;//错误的,定义数量应该用圆括号,因为v1默认为空不可以直接等于一个字面值。

列表初始值还是元素数量?

我们刚刚提到了列表初始化有圆括号还有花括号,以下大致有几点帮助大家来理解下这两者的区别

  • 1.圆括号一般是采构造的方式,一般来说有一个值对应的是数量,两个值对应的就是元素数量和构造初始化的值
vector<int> v1(10); //构造10个初始值为0的对象
vector<int> v2(10,1);//构造10个初始值为1的对象
  • 2.花括号一般是列表初始化,但是当列表元素和类型不匹配时,就回去寻找其他方式
vector<int> v1{1 , 2 ,3};//列表初始化3个对象,并初始化为1 2 3
vector<string> v2{10};//定义十个string对象并存入v2,默认初始化为空串
vector<string> v3{10,"hi"};//定义10个值为hi的对象

当然在对于定义string容器中有一个很容易犯的错误

vector<string> v1{"hi"};//错误,不能使用字面值来构建vector对象(可以配合引用不是对象来加以理解)

3.3.2 向vector对象中添加元素

如果想向vector对象添加元素可以使用push_back方法。

vector<int> v1;
v1.push_back(10);
for(auto i :v1)
	cout << i << endl;

vector对象可以有效增长

问题:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环,这是为什么呢?我们会在今后学到

范围for语句体内不应改变其所遍历序列的大小

3.3.3 其他vector操作

下面列举了一些经常使用的vector操作

语法 意义
v.empty() 判断v中是否存在元素,如果存在返回真,否则为假
v.size() 返回v中的元素个数
v.push_back( t ) 往v的尾部添加元素t

这里需要注意v.size()返回的类型是在vector下定义的size_typem,当然这也是无符号型

vector对象相互之间大小比较基于相异第一个元素的大小,如果没有相异元素则比较两个对象的元素个数

不能用下标的形式添加元素

容器内下标使用只能改变原先存在的元素,不可以添加原先不存在的元素

vector<int> v1 ;
v1[0] = 1;//错误,原先v1为空

如果我们要添加元素只能用v.push_back()了

最后强调一遍,只能对确知以存在的元素执行下标操作

3.4 迭代器介绍

迭代器是类似于下标操作的一种东西,所有标准库几乎都支持迭代器,只有少数几种不支持,string对象虽然不属于容器类型,但是他也支持迭代器操作。
类似指针类型,迭代器也支持对对象的间接访问

3.4.1 使用迭代器

一般来说我们不清楚迭代器的类型,所以要用auto来加以定义

vector<int> v1(10,1);
auto it1 = v1.begin();//定义首迭代器
auto it2 = v1.end();//定义尾后迭代器

首迭代器指向vector对象中第一个元素,而尾迭代器指向vector对象中最后一个元素的下一个元素

如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器

迭代器运算符

语法 意义
*iter 返回迭代器当前所指元素的引用
iter → men 解引用并获取该元素名为men的成员,等价于(*iter).men
++iter 令iter指示容器中的下一个元素
–iter 令iter指示容器中的上一个元素
iter1 == tier2 判断两个迭代器是否相等

如何判断迭代器是否相等?
如果两个迭代器指向元素相等或者两个元素都是尾后迭代器,则相等,否则不相等

将迭代器从一个元素移动到另一个元素

我们可以使用++或者 - - 对迭代器进行操作,这里我们需要注意对尾迭代器不能进行递增或者解引用操作,因为尾迭代器并不实际指向哪个元素。但是我们可以对尾迭代器进行递减操作

    vector<int> v1{1, 2};
    auto itr1 = v1.end();
    cout << *(itr1-1) << endl;

关键概念泛型编程
对于迭代器而言,有一些迭代器不具有>,< 等操作,所以我们要养成习惯,多多使用!=操作来作为对比两个迭代器是否处于同一位置**

迭代器类型

拥有标准库的迭代器类型一般为iterator 和const_iterator

string::iterator s1;
vector<int>::iterator v1;
string::const_iterator s2;
vector<int>::const_iterator v1;

const_iterator和常量指针差不多,能读取但不能修改他的内容。
但是我们还是要看我们绑定的对象

  • 1.如果我们要绑定的是常量对象,则需要用const_iterator
  • 2.如果我们绑定的不是常量对象,则我们既可以使用iterator又可以使用const_iterator

begin和end运算符

一般而言,begin和end返回的类型由具体绑定类型决定,如果为常量则返回const_iterator,如果为非常量,则返回iterator。
在C++11中又定义了两个函数可以直接看出来返回类型为常量,那么就是cbegin和cend

const vector<int> v1;
auto itr1 = v1.cbegin();
auto itr2 = v1.cend();//返回类型均是const_iterator类型

结合解引用和成员访问操作

如果我们迭代器指向的是一个类容器,我们又需要访问其元素里面的成员,
这里我们有两种方法来访问。

auto itr1 = v1.begin();
itr1 -> men;//访问men成员
*(itr1).men;//和上语句同一个效果

其实理解迭代器的解引用很简单,我们把迭代器理解成指针就好(虽然这是两种不同的东西)。

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

具体为什么会失效,我们后面会讲到。现在我们只需要谨记,但凡是用来迭代器的循环体,都不要向迭代器所属的容器添加元素

3.4.2 迭代器运算

迭代器也就像我们的指针一样,具有运算。运算过后,还是迭代器

语法 意义
iter + n 暂时将迭代器后移n个位置,操作完之后会返回原来指向的地方
iter - n 暂时将迭代器前移n个位置,操作完之后会返回原来指向的地方
iter += n 将iter后移n个位置,操作后不返回原来的地方
iter -= n 将iter前移n个位置,操作后不返回原来的地方
iter1 - iter2 获取两个迭代器之间的距离
>、>=、<、<= 判断迭代器的位置,参与运算的迭代器必须在同一个容器中

迭代器的算数运算

在迭代器的算数运算中,最普遍的用法是

vector<int> v1(10,1);
auto iter1 = v1.begin();
auto iter2 = v1.end();
auto iter3 = v1.end() - v1.begin();//类型是difference_type

上面代码中iter3代表了容器首迭代器和尾迭代器的距离,因为距离有正有负,所以difference_type是有符号类型。

使用迭代器运算

说了这么多,就应该实战演练一波,以下是用迭代器进行二分法查找元素的算法

vector<int> v1(10,1);
auto beg = v1.begin();
auto end = v1.end();
auto mid = v1.begin() + (end - beg) / 2;
int sought = 0;
while(mid != end && *mid != sought)
{
	if(sought < *mid)
	{
		end = mid;
	}
	else
	{
		beg = mid + 1;
	}
	mid = beg + (end - beg) / 2;
}

3.5 数组

3.5.1定义和初始化内置数组

数组是一种类似于vector的数据结构,他和vector最大的区别就是,数组长度在定义那一刻就确定了,并且以后不会改变。且定义数组长度的大小必须要是常量,不可以用变量来定义数组长度大小。

const int i = 100;
int array[i] = { 0 };//正确
int j = 10;
int array1[j] = { 0 };//错误,因为j是变量

和内置类型的变量一样,如果在函数内部定义了数组,他会默认初始化未知值,所以我们要主动地初始化 又或者将数组定义在函数外面(写算法题时)可以有效避免栈溢出带来的问题

和vector一样,不存在存放引用的数组

显式初始化数组元素

所谓显式初始化数组元素就是通过列表来初始化数组,但是在这里有一种更特殊的写法

int array[] = {1 , 2 , 3};//定义了元素数量为3,元素值分别为 1 2 3的数组

在数组维度不清楚时,编译器会计算出数组的大小。但是如果指明了维度,那么在列表初始化时,总数量不应该超过指定大小
如果列表初始化并未填完数组,剩下未初始化的元素默认初始化为0

字符数组的特殊性

字符数组可以用字符串字面值进行初始化,但是注意字符串字面值的大小会比原来字符串字符个数多一位,多出来的一位是’\0’

	
    char a[] = {'c' , '\0'};
    cout << sizeof(a) << endl;//2
    char b[] = {'c'};
    cout << sizeof(b) <<endl;//1
    char a1[] = "hello";
    cout << sizeof(a1) << endl;//6
    char a2[3] = "ABC";//出错,因为字符串字面值ABC需要4个空间

不允许拷贝和赋值

数组和vector容器不同,数组之间不可以相互赋值,而vector容器直接可以相互赋值

int a[] = {1 ,2 };
int b[] = a ;//出错

理解复杂的数组声明

因为数组本身就是对象,所以允许定义数组的指针,和对数组的引用(区分于存放引用的数组,存放引用的数组是非法的)

int *p1[10]; //p1数组里面存放10个整型指针
int (*p2)[10] = &arr;//p2是指向10个整形元素数组的指针
int (&p3)[10] = arr;//p3是一个对10个整形元素数组的引用
int &p4[10] = ?; //出错,没有存放引用的数组

上面几类数组声明比较难理解,之前我们在看顶层const和底层const时
采用的技巧是从右到左,我们在理解数组定义的时候,采用的是从内到外

int *(&array)[10] = ptrs;

上面的语句我们进行由内到外的拆分,首先array是一个引用,引用什么呢?引用具有10个整型指针的数组

重复,要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读

3.5.2 访问数组元素

在我们使用数组下标时,使用的数据类型是size_t,这是一种机器相关的无符号类型。
我们遍历数组内元素也可以使用范围for,这样可以减少我们很多自定义负担。

int a[3] = { 1 , 2 , 3};
for(auto i : a)
{
	cout << i << " ";
}
cout << endl;

检查下标的值

和vector和string一样,在我们使用数组下标时,要防止下标越界。对于一个程序而言,下标越界可以顺利编译运行,但是在运行的时候可能会发生不可预见的错误。
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他数据类似的数据结构下标越界并试图访问非法内存区域时,就会产生这样的错误

3.5.3 指针和数组

通常情况下,取地址符服务于对象,因为数组元素也是对象,所以我们可以使用取地址符来服务于数组元素。

int a[2] = {1, 2};
int *p = &a[0];//p指针指向数组第一个元素

我们如果要指向一个数组的第一个元素我们还可以更为简便的使用

int a[2] = {1 , 2};
int *p = a;//指向a的首元素

这是因为编译器会将a自动替换成&a[0]
在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针
我们之前还学过auto,当使用数组作为一个auto时,他默认生成对应该数组元素基本类型的指针

int a[2] = {1 ,2 };
auto i = a;//*i值为1

但是用decltype则返回的是与数组相同类型的类型

int a[2]={1 , 2};
decltype(a) i = {1 ,2 };
cout << i[0] <<endl;//输出1

指针也是迭代器

我们使用指针也可以遍历数组中的所有元素,只用定义两个迭代器即可

int a[2] = {1 ,2};
int *p1 = a;//首迭代器
int *p2 = &a[3];//尾迭代器
for(auto i = p1;p1 != p2 ; ++p1)
{
	cout << *i <<" " ;
}

标准库函数begin和end

在iterator库中有两个函数begin和end可以对数组进行操作,意义在于不用知道数组大小,精确地确定首尾迭代器

int a[2] = {1 , 2};
int *p1 = begin(a);
int *p2 = end(a);

因为数组并非类,所以不能用类引用成员函数那样去使用begin和end。

此外我们还需注意,和vector一样,我们不能对尾迭代器使用解引用和递增操作

指针运算

指针运算和迭代器运算也差不多,我们需要注意要进行有意义的指针运算,两个指针必须指向同一数组中的元素
此外两个指向同一数组的指针相减得到的值是这两个指针的距离,他的类型也是定义在标准库cstddef中,它是ptrdiff_t,一种无符号类型。

解引用和指针运算的交互

我们可以通过指针运算访问读取与指针同一数组元素的值

int a[2] = {1 , 2};
int *p = a;
p++;
cout << *p <<endl;//输出2

下标和指针

如前面所说,数组名字其实就是一个指向数组首元素的指针。a[2]其实就是在执行以下操作

int a[3] = {1 ,2 ,3};
int i = a[2];

//执行以下操作
int *p = a;
i = *(p + 2);

内置的下标运算符所用的索引值不是无符号类型,这和string和vector不一样

3.5.4 C风格字符串

尽管C++可以使用C风格字符串,但是我们最好还是不要使用,因为C风格字符串使用起来不是很方便,还很容易引发程序漏洞,是诸多安全问题的根本问题
基本问题

  • 使用C风格字符串需要掌握字符串具体需要多大空间,并且得多留一位给截止符’\0’,如果少了’\0’很多C语言库中对C风格字符串操作的函数都无法使用

对于大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效

3.5.5 与旧代码的接口

混用string和C风格字符串

如果你需要将string对象转为C风格字符串我们可以使用c_str成员函数

  string s1 = "ABC";
  const char *a = s1.c_str();

使用数组初始化vector对象

我们不能够使用vector来初始化对象,但是我们可以使用数组来初始化vector对象。

int a[3] ={1 , 2 , 3};
vector<int> v1( begin(a) , end(a) );//全部复制
vector<int> v2(a , a + 2);//复制部分 

建议:尽量使用标准库类型而非数组
因为数组和指针很容易出错,一部分是概念问题,另一部分是指针常用于对底层的操作,容易引发一些与烦琐细节有关的错我。我们应当尽量使用vector和迭代器,避免使用内置数组和指针,就好像string一样,我们要尽量避免使用C风格的基于数组的字符串

3.6 多维数组

严格来说,C++并没有多维数组,我们常说的多维数组就是数组里面的数组。

多维数组的初始化

我们可以这样初始化多维数组

int a[2][2] = 
{
	{1 , 2},
	{3 , 4}
}

也可以这样初始化多维数组

int a[2][2] = {1, 2, 3, 4};

上面两者定义相同
和数组一样,如果初始化数量不足数组空间,其他空间将会执行默认初始化

多维数组的下标引用

int a[2][3] = arr[0][0];用arr中的第1行第1个元素对a中第2行第3个元素进行赋值
int (&row)[4] = a1[1];//定义一个引用具有4个元素数组的引用,与a1中第2行进行绑定。

使用范围for处理多维数组

int a[3][4] = { 0 };
for(auto &row : a) //row必须要为引用类型,不然下一行的row默认为元素
{
	for(auto i : row)
	{
		cout << i <<" ";
	}
	cout << endl;
}	

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

指针和多维数组

我们可以使用指针来操作多维数组

int arr[3][4] = { 0 };
int (*p)[4] = arr[0];
p = &a[2];//注意要加引用指明右值为一个指针。

创作不易,感谢您能看完。如果您喜欢,可以在线赞赏鼓励我,您的鼓励是我前行的动力!如需转载,请私信我,谢谢!

你可能感兴趣的:(费曼学习法)