【c++ Prime 学习笔记】第3章 字符串、向量和数组
-
string
和vector
是两类最重要的标准库类型-
strng
表示可变长的字符序列 -
vector
存放某种给定类型对象的可变长序列。
-
3.1 命名空间的using声明
using namespace::name; //using声明
using std::cin;
using std::cout;using std::endl;
using namespace std; //using指令
- 每个
using
引入一个名字,因此每个名字都必须有自己的using - 由于头文件的代码会被拷贝到引用它的文件中,故头文件的代码不应使用using
3.2 标准库类型string
#include <string>
using std::string;
//也可以使用 using namespace std;
3.2.1 定义和初始化string对象
string s1; //默认初始化,为空字符串
string s2(s1); //直接初始化,s2是s1的副本
string s2=s1; //拷贝初始化,s2是s1的副本,等价于上一行
string s3("hiya"); //直接初始化,初始化为字面值常量
string s3="hiya"; //拷贝初始化,初始化为字面值常量,等价于上一行
string s4(10,'c'); //直接初始化,初始化为10个字符'c'
-
直接初始化
:不用等号,而用括号初始化变量。调用构造函数 -
拷贝初始化
:用等号初始化变量。调用重载的赋值运算符
3.2.2 string对象上的操作
读写string 对象
- 从iostream中读取string:用
>>
读取时,string对象忽略开头的空白(包括空格、换行、制表符等),从第一个真正的字符读起,直到下一处空白为止。 - string对象的
<<
和>>
也是返回运算符左侧的iostream对象。
使用getline 读取一整行
- 如要在iostream中读取时保留空白,需用
getline函数
,该函数从iostream中读取内容,直到遇到换行(换行也被读入),然后把读到的内容存入string对象(不存换行符)。 - getline也返回它的iostream,故也可作为while的条件。
int main() {
string line;
while (getline(cin,line))
cout << line << endl;
return 0;
}
string 的 empty 和 size操作
empty函数
根据string对象是否为空返回一个boolsize函数
返回string对象的长度(字符数)
int main() {
string line;
while (getline(cin,line))
if (!line.empty())
cout << line << " size: "<< line.size()<< endl;
return 0;
}
string::size_type 类型
- size函数返回的类型是
string::size_type
(可用decltype得到),它是无符号
类型,且能放下任何string对象的大小。(attention:不能将size与int等有符号类型混合计算)
比较 string 对象
==
和!=
验证两字符串内容是否完全相同,<
、<=
、>
、>=
比较两字符串的字典顺序
(大小写敏感)
为 string 对象赋值
- 用
=
进行string对象的拷贝和赋值
两个 string 对象相加
- 用
+
拼接两string对象,也可拼接一个string对象和一个字符串字面值(类型转换),但不能拼接两个字符串字面值
。因为string和字符串字面值是不同的类型
string s1="hello", s2="world";
string s3=s1+", "+s2+'\n'; //正确
string s4=s1+", "; //正确
string s5="hello"+", "; //错误,两个都是字面值
string s6=s1+", "+"world"; //正确,(s1+", ")+"world"
string s7="hello"+", "+s2; //错误("hello"+", ")+s2
3.2.3 处理string对象中的字符
- 在
cctype
头文件中定义了一组函数用于处理字符,如下表:
C++标准库兼容了C标准库,C中命名为name.h的头文件,在C++中被命名为cname,它们内容一样,但cname中的名字属于命名空间std,name.h中的名字不属于任何命名空间。(所以最好不要用C标准库)
-
基于范围的 for 语句
expression是一个对象列,declaration定义一个变量,用于访问序列中的元素,常用auto。
for(declaration:expression) statement string s("hello,world!"); for(auto c:s) //每行输出str中的一个字符 cout<<c<<endl;
-
使用范围 for 语句改变字符串中的字符
使用范围for时,如果要改变序列中元素的值,必须把循环变量定义为引用类型。
string s("hello,world!"); for(auto &c:s) //要改变序列中的元素,必须声明为引用类型 c=toupper(c); cout<<s<<endl;
-
只处理一部分字符
访问string对象中的单个字符有两种方式:
下标
和迭代器
下标运算符[]
接受string::size_type
(unsigned)类型的值,返回该位置上字符的引用(因此可修改字符)。若给索引提供signed值,会转为string::size_type表示的unsignedif(!s.empty()) cout<<s[0]<<endl; string s("hello,world!"); if(!s.empty()) s[0]=toupper(s[0]);
-
使用下表执行迭代器
for(decltype(s.size()) index=0; //用decltype推出string::size_type类型 index!=s.size() && !isspace(s[index]);++index) s[index]=toupper(s[index]); //将第一个词改成大写
3.3 定义和初始化vector对象
vector
表示对象的集合,所有对象的类型都相同。- 由于vector容纳着其他对象,故称为
容器
#include <vector>
using std::vector;
vector
是一个类模板。C++中有类模板
和函数模板
。模板本身不是类或函数,可将模板看作为编译器生成类或函数的一份说明。编译器根据模板创建类或函数的过程称为实例化
- 由模板生成类或函数时,必须指定类型
vector<int> ivec; //元素是int型对象
vector<Sales_item> Sales_vec; //元素是Sales_item类型的对象
vector<vector<string>> file; //元素是vector<string>类型的对象
vector<vector<string> > file; //C++11之前的写法,元素是vector<string>类型的对象
3.3.1 定义和初始化vector对象
初始化
- 直接初始化
- 拷贝初始化
- 列表初始化
- 花括号{}括起来
- 但当花括号无法初始化时,会尝试将花括号代替为圆括号。
vector<int> v1(10); //有10个元素,都是0
vector<int> v2{10}; //有1个元素,值是10
vector<int> v3(10,1); //有10个元素,都是1
vector<int> v4{10,1}; //有2个元素,分别是10和1
vector<string> v5{"hi"}; //有1个元素,是字符串"hi"
vector<string> v6("hi"); //错,不能用字符串字面值构造vector
vector<string> v7{10}; //不能列表初始化,转为构造。有10个元素,都是空字符串
vector<string> v8{10,"hi"}; //不能列表初始化,转为构造。有10个元素,都是"hi"
3.3.2 向vector对象中添加元素
push_back函数
:把一个对象当vector的尾元素压入vector尾端- 循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环
vector<int> v2;
for (int i = 0; i != 100; i++)
v2.push_back(i);
string word;
vector<string> text;
while(cin>>word)
text.push_back(word);
3.3.3 其他vector操作
- 范围for语句
vector<int> v{1,2,3,4,5,6,7,8,9};
for(auto &i : v)
i *=i;
for(auto i : v)
cout<<i<<" ";
cout<<endl;
- vector的
size
方法返回该vector的元素数量,类型是vector<type>::size_type
类型。
使用size_type时,需首先指定是哪种类型的size_type。 - 只有当元素类型可比较时才能将vector按字典顺序比较。
- 对vector索引时,下标类型是相应的
size_type
类型 - 不能用下标添加元素,用下标访问不存在的元素会引发错误(编译不报错),例如
缓冲区溢出
等 - 确保下标合法的一种有效手段就是尽可能使用
范围for
3.4 迭代器介绍
迭代器
比下标访问更通用。所有标准库容器都支持迭代器,但只有几种支持下标。- string对象不属于容器,但操作上和容器很接近
- 迭代器提供了对元素对象的
间接访问
,类似于指针。其对象是容器中的元素,或string中的字符 有效
的迭代器或者指向某个元素,或者指向尾元素的下一位置,其他都是无效。
3.4.1 使用迭代器
- 有迭代器的类型都配套有返回迭代器的成员。其中
begin
方法返回指向首元素
的迭代器,end
方法返回指向尾元素的下一位置
(尾后)的迭代器。 - 如果容器为空,则begin()和end()返回同一迭代器,都是尾后迭代器。
迭代器运算符
string s("hello,world!");
if(s.begin() != s.end()){
auto it = s.begin();
*it = toupper(*it); //*it 返回迭代器it所指元素的引用
}
for(auto =s.begin();it!=s.end() && !isspace(*it);++it)
*it = toupper(*it);
迭代器类型
iterator
可读写 ——vector<int>::iterator
const_iterator
只读 ——vector<int>::const_iterator
begin 和 end 运算符
- 如果容器内对象为
常量
,则begin
和end
返回const_iterator
迭代器,否则返回iterator
迭代器 cbegin
方法和cend
方法对任何容器都返回const_iterator迭代器
结合解引用和成员访问操作
(*it).function()
,括号必不可少,不然it先取成员再解引用。- 通过迭代器调用元素对象的成员时,亦可使用简化的
->
操作符
it->function()
等价于(*it).function()
某些对vector对象的操作会使迭代器失效
- 任何可能改变容器容量的操作,如push_back,都会使容器的迭代器失效。
但凡使用了迭代器的循环体,都不要向迭代器所属容器添加元素
3.4.2 迭代器运算
- string和vector是顺序存储,故它们的迭代器支持更多的操作,如迭代器运算,这些操作可使迭代器每次移动跨越多个元素,也可对迭代器比较大小。
迭代器的算术运算
- 可使迭代器和整数值相加减,返回值是向前或向后移动若干位置的迭代器。
- 可使用关系运算符
>
、>=
、<
、<=
对迭代器所指位置比较大小 - 将迭代器相减,结果是两迭代器的
距离
,指的是右侧迭代器向前移动多少位置能和左侧迭代器重合,距离可正可负。其类型是容器类型对应的difference_type
,是signed
的整型数。
3.5 数组
- 数组也是存放类型相同的对象的容器,这些对象本身没有名字,通过在数组中的位置访问。
- 数组大小确定不变,不能增加元素。性能比vector等容器更好,但也失去一些灵活性。
如果不清楚元素的确切格式,请使用vector
3.5.1 定义和初始化内置数组
- 数组是一种复合类型,声明形如
int a[d]
;- a是数组名
- d是数组维度。维度说明数组元素个数,必须是常量表达式。
- 数组大小确定,不能随意向数组增加元素
- 定义数组必须指定类型,不允许用auto推断类型
- 数组元素为对象,不存在引用的数组
unsigned cnt =42;
constexpr unsigned sz=42;
int arr[10];
int *parr[sz]; //含有42个整型指针的数组
string bad[cnt]; //cnt不是常量表达式
string strs[get_size()] //当get_size()时constexpr时正确,否则错误
显示初始化数组元素
- 数组列表初始化没允许忽略维度;
- 声明时没有指明维度,编译器会根据初始值的数量计算出来;
- 指明维度后,初始值的数量不可超出维度;
- 如果维度大于初始值的数量,剩下的元素会初始化为默认值
const unsigned sz=3;
int a1[sz]={0,1,2};
int a2[]={0,1,2};
int a3[5]={0,1,2}; //等价{0,1,2,0,0};
string a4[3]={"hi","bye"}; //等价{"hi","bye",""}
int a5[2]={0,1,2}; //错误
字符数组的特殊性
- 字符数组比较特殊,可用字符串字面值初始化,且不需手动指定大小,此时会在最后加上空字符。
char a1[]={'C','+','+'}; //列表初始化,无空字符
char a1[]={'C','+','+','\0'}; //列表初始化,有空字符
char a3[]="C++"; //字符串字面值初始化,有空字符
const char a4[6]="Daniel"; //错,没有空间放空字符了
不允许拷贝和赋值
- 不能将数组拷贝给其他数组作为初值,也不能用数组为数组赋值。因为数组名是首元素地址,不能代表整个数组。有些编译器扩展可能支持数组的拷贝,尽量不使用。
int a[]={0,1,2};
int a2[]=a; //错误
a2=a; //错误
理解复杂的数组声明
- 理解技巧:从内向外,从右向左
指针数组
,数组指针
,数组引用
(不存在引用的数组)
int arr[10];
int *ptrs[10]; //指针的数组。是长度为10的数组,元素是指针,指向int型
int &refs[10]=/*?*/ //错,引用不是对象,不存在引用的数组
int (*Parray)[10]=&arr; //数组的指针。是指针,指向长度为10的数组,元素是int型
int (&arrRef)[10]=arr; //数组的引用。是引用,引用长度为10的数组,元素是int型
int *(&arry)[10]=ptrs; //指针数组的引用。是引用,引用长度为10的数组,元素是指针,指向int型
3.5.2 访问数组元素
- 数组元素可用范围for或下标访问
- 使用数组下标时,将其定义为
size_t
型,它是一种机器相关的unsigned类型,它足够大以表示内存中任意对象的大小,定义于cstddef头文件
中。 - 数组的下标类型由C++语言定义,vector等容器的下标类型由标准库定义。
3.5.3 指针和数组
- 使用数组名的时候,编译器一般会将其转换为指向首元素的指针。
- 一些情况下,对数组的操作实际上时对指针的操作
string nums[]={"one","two","three"};
string *p=&nums[0]; //指向nums的第一个元素
string *p=nums; //等价上一句
- 当数组名作为auto变量初值时,推出的类型是指针而非数组,效果相当于对首元素取地址再给初值。但用decltype时,得到的是数组类型。
int ia[]={0,1,2,3,4,5,6,7,8,9};
auto ia2(ia); //ia2是整型指针,指向ia的第一个元素;也相当于auto ia2(&ia[0]);
ia2=42 //错误,int值不能给指针赋值
decltype(ia) ia3={0,1,2,3,4,5,6,7,8,9};
ia3=p; //错误:不能用整形指针给数组赋值
ia3[4]=i; //正确,把i赋给ia3的一个元素
指针也是迭代器
-
指针也是迭代器,string和vector的迭代器支持的运算都可被数组指针支持。
-
用
begin
和end
得到数组的首元素指针和尾后指针,这两个函数定义于iterator
头文件中。由于数组不是类,故它们也不是成员方法
int arr[]={0,1,2,3,4,5,6,7,8,9};
int *p=arr; //指向第一个元素
++p; //指向arr[1]
int *beg=begin(arr); //首元素的指针
int *last=end(arr); //尾后指针
//寻找第一个负值
int *pbeg=begin(arr),*pend=end(arr);
while(pbeg!=pend && *pbeg>=0)
++pbeg;
指针运算
- 指针加(减)某个整数,表明地址前进(后退)该整数个位置
- 指针加整数,不得超过数组长度,否则会报错
- 两指针相减的结果类型是
ptrdiff_t
,定义于cstddef
头文件,是一种signed
类型。 - 两指针分别指向不相关的对象,则不能进行比较
解引用和指针运算的交互
表达式*(ia+4)
计算指针ia前进4个元素后的新地址并解引用,等价于ia[4]
下标与指针
- 对数组执行下标运算其实是对指向数组元素的指针执行下标运算
- 标准库内的迭代器下标必须为unsigned,但指针的下标是C++内置,可以处理负值,即指针可接受signed下标
3.5.4 C风格字符串
-
C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存在
字符数组
中且以空字符
结束。一般用字符指针
操作它们。 -
一些操作C风格字符串的函数被定义在
cstring
头文件中,传入这些函数的是字符指针,必须指向以空字符作为结束的数组。对这些函数而言,空字符是字符串结束的标志。
- 由于C风格字符串是字符指针,故:
-
用
==
比较string对象,但只能用strcmp函数
比较C风格字符串 -
用
+
拼接string对象,但只能用strcat函数
拼接C风格字符串 -
用
=
拷贝string对象,但只能用strcpy函数
拼接C风格字符串
-
- 对于strcat和strcpy,需提供存放结果的空间,并由程序员确保此空间不会溢出。
- 用标准库的string对象比C风格字符串更安全
3.5.5 与旧代码的接口
混用string对象和C风格字符串
- 针对string,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代
- 如果程序需要C风格字符串,不能直接用string对象代替。应该用string对象的
c_str
方法返回它对应的C风格字符串,且指针类型是const char *
,从而确保不会改变string的内容。
string s("Hello World");
char *str=s; //错误,不能用string初始化char*
const char *str=s.c_str();
使用数组初始化vector对象
- 不允许用数组初始化另一个数组,不允许用vector对象初始化数组,但允许用数组初始化vector对象。更进一步,可以用一对迭代器初始化vector对象。只需指明要拷贝区域的首元素地址和尾后地址就可以了
int int_arr={0,1,2,3,4,5};
vector<int> ivec(begin(int_arr),end(int_arr)); //用数组的一对迭代器初始化vector对象
尽量用vector和迭代器,不要用数组和指针
尽量用string,不要用C风格字符串
3.6 多维数组
-
C++语言并无多维数组,
多维数组是数组的数组
。 -
对于
二维数组
而言,第一个维度为行
,第二个维度为列
-
可对数组用列表初始化,花括号可嵌套也可不嵌套。因为
数组连续存储
int ia[3][4]={0,1,2,3,4,5,6,7}; //ia是3元素数组,每个元素是4元素数组,ia三行四列。将前两行初始化,第三行默认初始化
int ib[3][4]={{0},{4},{8}}; //ib是3元素数组,每个元素是4元素数组,ia三行四列。将第一列初始化,其余元素默认初始化
int (&row)[4]=ia[1]
- 如果要用范围for和auto处理多维数组,除了最内层循环,其他所有循环的auto类型都应该是引用。
// 修改元素
size_t cnt=0;
for(auto &row:ia){ //遍历行,外层循环声明为引用的原因是避免数组被auto为指针
for(auto &col:row){ //遍历列,内层循环声明为引用的原因是要修改元素
col=cnt; //修改元素
++cnt;
}
}
// 打印元素
for(const auto &row:ia) //避免数组被auto为指针
for(auto col:row) //最内层循环是元素不是数组,不存在被auto为指针的问题
cout<<col<<endl;
// 错误例子
for(auto row:ia) //ia被auto为指针类型
for(auto col:row) //错,指针不可遍历
指针和多维数组
使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型。
类型别名简化多维数组指针
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;
}