本文知识导图
本章介绍两种最重要的标准库类型:string和vector
string表示可变长的字符序列。
vector存放的是某种给定类型对象的可变长序列。
命名空间的using声明
使用using声明无须专门的前缀(形如:: )也能使用所需的名字了。
如下:
#include <iostream>
using std::cin;
int main(){
int i;
cin >> i; //正确
cout << i; //错误,没有对应的using声明
std::cout << i; //正确
return 0;
}
每个名字都需要独立的using声明
头文件不应包含using声明
标准库类型string
使用string需包含以下代码:
#include <string>
using std::string;
定义和初始化string对象
初始化string对象的方式 | 含义 |
---|---|
string s1 | 默认初始化,s1是一个空 |
string s2(s1) | s2是s1的副本 |
string s2 = s1 | 等价于s2(s1) |
string s3(“value”) | s3是字面值“value”的副本 |
string s3 = value | 等价于 string s3(“value” ) |
string s4(n, ‘c’) | 把s4初始化为由连续n个字符c组成的串 |
直接初始化和拷贝初始化
如果只用等号=初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右边的值拷贝到新创建的对象中去。
与之相反,如果不使用等号,则执行的是直接初始化。
string对象上的操作
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中所含的字符完全一样,则他们相等,大小写敏感 |
s1!=s2 | 如果s1和s2中并非所含的字符完全一样,则他们不相等,大小写敏感 |
<, <=, >, >= | 利用字符在字典中的顺序进行比较,且对字母大小写敏感 |
读写string对象
int main(){
string s;
cin >> s;
cout << s << endl;
return 0;
}
在输入时,string对象会自动忽略开头的空白(空格符,换行符,制表符等)并从一个真正的字符开始读起,直到遇到下一个空白为止。
所以如果输入为“ hello world! ”,输出将会是“hello”。
使用getline读取一整行
有时候我们希望能保存空白符,这时应该用getline。
函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的对象存入到那个string对象中去(不存换行符)。
int main(){
string line;
// 每次读入一整行, 直到到达文件末尾
while (getline(cin, line))
cout << line << endl;
return 0;
}
string::size_type类型
对于一个size函数来说,返回int或unsigned似乎都是合情合理的,但其实size函数返回的是string::size_type类型的值。
它是一个无符号类型的值,并且能够足以存放下任何string对象的大小。
字面值和string对象相加
必须确保每个加法运算符+的两侧对象至少有一个是string
string s4 = s1 + ", "; //正确。
string s5 = "hello" + ", "; //错误,
string s6 = s1 + ", " +"world"; //正确, s1+", "返回的是s1对象。
string s7 = "hello" + " ," + s2; //错误。
切记,字符串字面值和string是不同的类型。
处理string中的字符
我们经常需要单独处理string对象中的字符,比如检查一个string对象是否包含空白,或者把string对象中的字母改成小写,再或者检查某个特定的字符是否出现等。
在cctype头文件中定义了一组标准库函数处理这部分工作。
cctype头文件中的函数 | 含义 |
---|---|
isalnum( c ) | 当c是字母或数字时为真 |
isalpha( c ) | 当c是字母时为真 |
iscntrl( c ) | 当c是控制字符时为真 |
isdigit( c ) | 当c是数字时为真 |
isgraph( c ) | 当c不是空格但可以打印时为真 |
islower( c ) | 当c是小写字母时为真 |
isprint( c ) | 当c是可打印的字符时为真(即c是空格,或c具有可视形式) |
ispunct( c ) | 当c是标点符号时为真 |
isspace( c ) | 当c时空白时为真(空格,横向制表符,纵向制表符,回车符,换行符,进纸符) |
isupper( c ) | 当c是大写字母时为真 |
isxdigit( c ) | 当c时16进制数时为真 |
tolower ( c ) | 如果c时大写字母,输出对应小写,否则输出原样 |
toupper ( c ) | 如果c时小写字母,输出对应大写,否则输出原样 |
处理每个字符?使用基于范围的for语言
如果相对string中的每个字符做点什么,最好的方法是使用C++11中提供的范围for语句。语法如下:
for (declaration : expression)
statement
一个简单的例子:
string str("some string");
for (auto c : str)
cout << c <<endl; //输出每个字符,并紧跟一个换行符。
使用范围for语句改变字符串中的字符
如果想要改变string对象中的字符的值,必须把循环变量定义为引用类型。使用引用,就可以改变它绑定的字符了。
string s("Hello World!!!");
for (auto &c : s)
c = toupper(c);
cout<< s << endl;
只处理一部分字符?
想访问string对象中的单个字符有两种方式:一种是使用下标,另一种是迭代器。
迭代器会在后面的章节介绍。
下标运算符[]接受输入参数是string::size_type类型的值。返回值是该位置上字符的引用。
string对象的下标必须大于等于0而小于s.size()
使用下标进行迭代
for( decltype(s.size() ) index = 0;
index != s.size() && !isspace(s[index]); ++index)
{
s[index] = toupper(s[index]);
}
标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都相同。
集合中每个对象都有一个对应的索引,用于访问对象。
所以它也被称作为容器。
要想使用vector,需要包含以下头文件
#include <vector>
using std::vector;
C++语言有类模版也有函数模版,vector是一个类模版。
模版本身不是一个类或函数。编译器根据模版创建类或函数的过程称为实例化,当使用模版时,需要指出编译器应把类或函数实例化为何种类型。
对于类模版来说,我们通过一些额外信息来制定模版到底实例化成什么样的类。
vector<int> ivec; //ivec保存int类型对象
vector<Sales_item> Sales_vec; //保存Sales_item类型对象
vector<vector<string>> file; //该向量的元素时vecotr对象
定义和初始化vector对象
初始化vector对象的方法 | 含义 |
---|---|
vector v1 | v1是一个空vector |
vector v2(v1) | v2中包含所有v1的副本 |
vector v2 = v1 | 等价于v2(v1) |
vector v3(n, val) | v3包含n个重复的元素,每个元素值都是val |
vector v4(n) | v4包含了n个重复执行了初始化的对象 |
vector v5 {a, b, c…} | v5包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector v5 = {a, b, c…} | 等价于vector v5 {a, b, c…} |
值初始化
通常情况下,可以只提供vector对象容纳的元素数量而不用略去初始值。此时库会闯将一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象的元素类型决定。
如,int类型,元素初始值自动设置为0.
如,string类型,每个都是空string对象。
向vector对象中添加元素
push_back负责把一个值当作vector对象的尾元素“压到”vector对象的尾端。如:
vector<int> v2;
for (int i = 0; i != 100; i++)
v2.push_back(i);
从标准输入中读取单词,将其作为vector对象的元素存储。
string word;
vector<string> text;
while(cin >> word) {
text.push_back(word);
}
其他vector操作
vector支持的操作 | 含义 |
---|---|
v.empty() | 如果v不含任何元素,返回真 |
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相等且仅当他们的元素数量相同且对应位置的元素值都相同 |
v1 != v2 | |
<, <=, >, >= | 顾名思义,以字典顺序进行比较 |
访问vector对象中元素的方法与访问string对象中字符的方法差不多。
不能用下标添加元素
vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素
vector<int> ivec; //空vector对象
for (decltype(ivec.size()) ix = 0; ix !=10; ++ix)
ivec[ix] = ix; //严重错误:ivec不包含任何元素
上述代码应修改为
for (decltype(ivec.size()) ix = 0; ix !=10; ++ix)
ivec.push_back( ix );
迭代器介绍
使用迭代器
//由编译器决定b和e的类型,
//b表示v的第一个元素,e表示v尾元素的下一个位置。
auto b= v.begin(), e = v.end();
end成员负责返回指向容器尾元素的下一个位置的迭代器,该迭代器指示的事容器的一个不存在的为后元素。
迭代器运算符
标准容器迭代器的运算符 | 含义 |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter -> mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter指向容器中的下一个元素 |
- -iter | 令iter指向容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等,若指向同一元素或他们事同一个元素的尾后迭代器,则相等 |
iter1 != iter2 | 反之,不相等 |
把string的第一额字母改为大写:
string s("some string");
if (s.begin() != s.end()) //确保s非空
{
auto it = s.begin();
*it = toupper(*it);
}
将迭代器从一个元素转移到另外一个元素
把string所有字符改为大写:
for(auto it = s.begin(); it != s.end() && !isspace(*it); ++it){
*it = toupper(*it);
}
迭代器类型
一般来说我们无须知道迭代器的精确类型。而实际上,那些拥有迭代器的标准库使用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的类型由对象是否事常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。
c++11新引入两个新函数,cbegin和cend,这两个函数无论对象本身是否为常量,返回值都是const_iterato。
结合解引用和成员访问操作
(*it).empty()
圆括号必不可少,现对it解引用,然后解引用的结果再执行点运算。
it -> mem
箭头运算符把解引用和成员访问两个操作结合在一起。
迭代器运算
vector和string迭代器支持的运算 | 含义 |
---|---|
iter + n | 迭代器加一个整数仍得到一个迭代器,迭代器指示的新位置与原来的位置相比向前移动了若干个元素。 |
iter - n | 迭代器减一个整数仍得到一个迭代器,迭代器指示的新位置与原来的位置相比向后移动了若干个元素。 |
iter += n | 迭代器加法的复合赋值语句 |
iter -= n | 迭代器减法的复合赋值语句 |
iter1 - iter2 | 两个迭代器相减的结果是他们之间的距离。也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧迭代器 |
>, >= , < , <= | 如果某个迭代器指向的容器位置在另一个迭代器之前,则说前者小于后者 |
计算结果为difference_type类型的带符号整型,(距离可正可负)
数组
数组与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。
定义和初始化数组
数组是一个复合类型,数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,必须大于0。维度必须是一个常量表达式。
unsigned cnt = 42;
constexpr unsigned sz = 42;
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有10个整型指针的数组
string bad[cnt]; //错误,cnt不是常量表达式
string strs[get_size()]; //档get_size是constexpr时正确,否者错误
显式初始化数组元素
const unsigned sz = 3;
int ia1[sz] = {0, 1, 2};
int a2[] = {0, 1, 2}; //维度是3的数组
int a3[5] = {0, 1, 2}; //等价于 a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; //等价于a4[] = {"hi", "bye", ""}
int a5[2] = {0, 1, 2}; //错误,初始值过多。
字符数组的特殊性
char a1[] = {'C', '+', '+'};
char a2[] = {'C', '+', '+', '\0'};
char a3[] = "C++";
const char a4[6] = "Daniel"; //错误,没有空间可以存放空字符
字符数组还可以通过字符串字面值初始化,但要注意结尾处还有一个空字符。
a1维度为3,a2 a3维度为4, a4错误。
理解复杂的数组声明
int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10] = /* ? * /; //错误,不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
int *(&array)[10] = ptrs; //array是数组的引用,该数组含有10个指针
访问数组元素
在使用数组下标的时候,通常将其定义为size_t类型,size_t是一种机器相关的无符号类型,他被设计的足够大以便能表示内存中任意对象的大小。 在cstddef头文件中定义了size_t类型。
指针和数组
在C++语言中,指针和数组有非常紧密的联系,就如即将介绍的,使用数组的时候编译器会把它转换成指针。
使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。
string nums[] = {"one", "two", "three"};
string *p = &nums[0]; //p指向nums的第一个元素
数组有一个特性:很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:
string *p2 = nums; //等价于 p2 = &nums[0]
int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素
ia2 = 42; //错误:ia2是指针,不能用int值给指针赋值。
但需要指出的是,decltype不会返回指针,而返回的是数组类型。
指针也是迭代器
vector和string迭代器支持的运算,数组的指针全部支持。如++运算等。
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; //p指向arr的第一个元素
++p; //p指向a[1];
int *e = &arr[10]; //这里显然使用下标运算索引了一个不存在的元素。
//e指向了尾元素的下一位置的指针,就像尾后迭代器。
使用上面的指针重写循环
for(int *b = arr; b != e; ++b)
cout<< *b <<endl;
标准库函数的begin和end
在C++11中也为数组引入了begin和end函数,不过数组毕竟不是类类型,因此这两个函数不是成员函数,正确使用的方法是把数组作为参数。
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia);
int *last = end(ia);
指针运算
指向数组元素的指针可以执行所有迭代器运算,包括解引,递增,比较,与整数相加,两个指针向减等,意义完全一致。
和迭代器一样,两个指针向减的结果是她们之间的距离。参与运算的两个指针必须指向同一个数组中的元素。结果为ptrdiff_t的标准库类型,在cstddef头文件中。
两个指针进行比较,也需要他们都指向同一个数组,或者该数组尾元素的下一个位置。
c风格字符串
c风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的谢范。按此习惯书写的字符串存放在数组中并以空字符结束(’\0’)
c标准库String函数
定义在cstring头文件中
C风格字符串的函数 | 含义 |
---|---|
strlen§ | 返回p的长度,空字符不计算在内 |
strcmp(p1, p2) | 比较p1和p2的相等性,如果相等,返回0;如果p1>p2,返回正值,反之返回负值 |
strcat(p1, p2) | 将p2附加到p1后,返回p1 |
strcpy(p1, p2) | 将p2拷贝个p1后,返回p1 |
由于C语言字符串是一个数组,所以目标字符串的大小由调用者指定。对于大多数应用来说,使用标准库string比C风格字符串更安全高效。
与旧代码的接口
混用string对象和c风格字符串
string专门提供了一个名为c_str的成员函数,使string对象可以直接初始化指向字符的指针。
char *str = s; //错位,不能用string对象初始化char *
const char *str = s.c_str();//正确。
c_str返回了一个c风格的字符串。
使用数组初始化vector对象
不允许使用vector对象初始化数组,但是允许使用数组来初始化vector对象。只需要指明首元素地址和尾后地址就可以了
int int_arr[] = {0 , 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));
也可以仅用数组的一部分初始化
vector<int> ivec(int_arr +1, int_arr +4);
多维数组
严格来说C++没有多维数组,通常说的多维数组实际上是数组的数组。
多维数组的初始化
int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
}
其中内嵌括号并非必须
int ia[3][4] = {0, 1, 2, 3,4, 5, 6, 7,8, 9, 10, 11}
类似以为数组,多维数组初始化时也并非所有元素的值都必须包含在初始化列表之内,如果仅想初始化每一行的第一个元素:
int ia[3][4] = {{0},{4},{8}}
多维数组的下标引用
如果表达式含有下标运算符和数组的维度一样多,该表达式的结果将是给定类型的元素,反之,如果表达式含有的下标运算符比数组的维度小,则表达式的结果将是给定索引的一个内层数组
使用范围for循环语句处理多维数组
size_t cnt = 0;
for (auto &row : ia)
for (auto &col : row)
col = cnt;
++cnt;
使用范围for语句处理多维数组时,除了内层的循环外,其他所有循环的控制变量都应该是引用类型
类型别名简化多维数组的指针
using int_array = int[4]; //将4个整数组成的数组,定义为int_array
typedef int int_array[4]; //将4个整数组成的数组,定义为int_array,等价于上方代码
for(int_array *p = ia; p != ia +3 ; ++p){
for(int *q = *p; q != *p +4; ++q)
cout << *q << ' ';
cout<<endl;
}