3.1 命名空间的using声明
- 每个名字都需要独立的using声明。
- 头文件不应包含using声明,头文件的内容会拷贝到所有引用它的文件中去,某些文件可能产生始料未及的名字冲突。
3.2 标准库类型string
- string表示可变长的字符序列,需要包含string头文件。
- 严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。
3.2.1 定义和初始化string对象
- 如何初始化类的对象是由类本身决定的。
- 拷贝初始化:使用等号初始化一个变量,编译器把等号右侧的初始值拷贝到新创建的对象中去。
- 初始化要用到的值有多个,一般来说只能使用直接初始化的方式。
string s1; //空字符串
string s2 = s1; //副本,等价于 s2(s1)
string s3 = "hiya"; //副本,等价于 s3("hiya")
string s4(10,'c'); //cccccccccc
3.2.2 string对象上的操作
- getline:读取一整行,得到的string对象中并不包含换行符。
- 如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
- 切记:字符串字面值与string是不同的类型。
- 当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string。
string s1 = "hello", s2 = "world";
string s3 = s1 + "," + s2 + "\n";
string s4 = s1 + ",";
string s5 = "hello" + ","; //错误,不能把字面值直接相加
string s6 = s1 + "," + "world";
string s7 = "hello" + "," + s2; //错误
string s8 = "hello" + ("," + s2);
- C++将C语言标准库的内容,命名为cname(不含.h)。
3.2.3 处理string对象中的字符
- 建议:使用C++版本的C标准库头文件,标准库中的名字总能在命名空间std中找到。
- 范围for(C++11)
for(auto c : str) { //对于字符串str中的每个字符c,执行某某操作
cout << c << endl;
}
要想改变string对象中字符的值,必须把循环变量定义成引用类型
for(auto &c : str) {
c = toupper(c); //c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;
}
- 访问string对象中单个字符的两种方式:
使用下标(索引)
注意检查下标的合法性(可以设下标的类型为string::size_type,并确保下标小于size()的值)
使用迭代器
3.3 标准库类型vector
- vector表示对象的集合,其中所有对象的类型都相同。
- vector是一个类模板,由vector生成的模板必须包含vector中元素的类型。
- 引用不是对象,所以不存在包含引用的vector。
3.3.1 定义和初始化vector对象
- 列表初始化vector对象(C++11)
vector<string> v1{“a”, “an”, “ang”};
- 几种初始化方式的例外情况:
使用拷贝初始化时(即使用=时)只能提供一个初始值。
如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化。
如果提供的是初始化元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里。 - 创建指定数量的元素
vector<string> v1{10, “av”, };
//10个string类型的元素,每个都被初始化为”av”
- 通过传递初始值时用的是花括号还是圆括号来区分是列表初始值还是元素数量。
vector<int> v1(10, 1); //v1有10个元素,每个的值都是1
vector<int> v2{10, 1}; //v2有2个元素,值分别是10和1
vector<string> v3(“av”); //错误:不能使用字符串字面值构建vector对象
vector<string> v4{10}; //v4有10个默认初始化的元素
圆括号:可以说提供的值是用来构造vector对象的。
花括号:可以表述成向列表初始化该vector对象。确认无法执行列表初始化后,编译器会尝试用默认值初始化vector对象。
- 对vector对象来说,直接初始化的方式适用于三种情况:
初始值已知且数量较少。
初始值是另一个vector对象的副本。
所有元素的初始值都一样。
3.3.2 vector对象操作
- 如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
- 要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型,例:
vector<int> ::size_type //正确
vector::size_type //错误
- vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
- 只能对确知已存在的元素执行下标操作。
- 确保下标合法的一种有效手段就是尽可能使用范围for语句。
- vector的两个限制:
不能在范围for循环中向vector对象添加元素。
任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。
3.4 迭代器介绍
- 迭代器:容器类型内置的“指针”。
- 使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。
- 所有迭代器的类型都拥有begin和end成员
begin:返回指向第一个元素(或字符)的迭代器。
end:尾后迭代器,即尾元素的下一个位置。 - 尾后迭代器指示的是容器的一个本不存在的尾后元素,表示已经处理完了容器中的所有元素。
- 尾后迭代器并不实际指示某一个元素,所以不能对其进行递增或解引用。
- 如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
- 有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其他情况都属于无效。
- 两个迭代器相等:指向的元素相同或者都是同一容器的尾后迭代器。
3.4.1 使用迭代器
- 拥有迭代器的标准类型使用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返回const_iterator,否则返回iterator。
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); //it1的类型是vector<int>::iterator
auto it2 = cv.begin(); //it2的类型是vector<int>::const_iterator
- 有时候希望即使对象不是常量,也要使用const_iterator,C++11引入了cbegin和cend。
vector<int> v;
auto it = v.cbegin(); //it的类型为vector<int>::const_iterator
// 不论vector对象本身是否是常量,返回值都是const_iterator。
- 结合解引用的成员访问
vector<string> v;
auto it = v.begin();
(*it).empty();
*it.empty(); //错误:试图访问it的名为empty的成员,但it是迭代器
it->empty(); //箭头运算符:把解引用和成员访问两个操作合在一起
- 谨记:但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
3.4.2 迭代器运算
- 两个迭代器相减的结果是两个迭代器的距离,其类型是名为difference_type的带符号整型数。
- 使用迭代器运算的一个经典算法是二分搜索。
- 可以令迭代器和一个整数相加(减),其返回值是向前(后)移动了若干位置的迭代器。
3.5 数组
3.5.1 定义和初始化内置数组
- 复合类型(声明形如:T a[d];)
T:元素类型
a:数组名称
d:元素个数(必须是常量表达式)
unsigned cnt = 42;
constexpr unsigned sz = 42; //常量表达式
int arr[10];
int *parr[sz];
string bad[cnt]; //错误:cnt不是常量表达式
string strs[get_size()]; //当get_size是constexpr时正确;否则错误
- 注:const只限制只读,并不要求在编译时就确定,可以在运行时确定。只有constexpr才可以表示数组大小。
- 数组的元素应为对象,不存在引用的数组。
- 可以使用列表初始化,但必须指定数组类型,不允许用auto关键字由初始值的列表推断类型。
const unsigned sz = 3;
int ial[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] = "Danial"; //错误,没有空间存放空字符!
- 不允许拷贝和赋值。
int a[] = {0,1,2};
int a2[] = a; //初始化时拷贝错误
a2 = a; //赋值错误
- 要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
int *ptrs[10]; //ptrs是含有10个元素(整型指针)的数组
int &refs[10]; //错误:不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //Parray引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; //arry是数组的引用,该数组包含10个指针
3.5.2 指针和数组
- 在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。
- 当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。
- 当使用数组作为decltype的初始值时,推断得到的类型是数组而非指针。
string nums[] = {"one","two","three"}; //数组的元素是string对象
string *p = &nums[0]; //p指向nums的第一个元素
string *p2 = nums; //等价于p2 = &nums[0]
int ia[] = {0,1,2,3,4,5,6,7,8,9}; //ia是一个含有10个整数的数组
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素,等价于ia2(&ia[0]);
ia2 = 42; //错误:ia2是一个指针
//当使用decltype时,上述转换不会发生
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; //ia3是数组
ia3 = p; //错误:不能用整型指针给数组赋值
ia3[4] = 42; //正确
- 指针也是迭代器。
- 就像尾后迭代器一样,尾后指针也不指向具体的元素,因此不能对尾后指针执行解引用或递增的操作。
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; //p指向arr的第一个元素
++p; //p指向arr[1]
int *e = &arr[10]; //指向arr尾元素的下一个位置的指针
for(int *b = arr; b!=e; ++b)
cout<<*b<<endl; //输出arr的元素
// 尽管能计算得到尾后指针,但这种用法极易出错。
- 标准库函数begin和end(C++11)(不是成员函数)
使用方式:将数组作为它们的参数。
int main()
{
//begin和end函数定义在iteator头文件中
int ia[] = { 0,1,2,3,4,5,6,7,8,9 };
int *beg = std::begin(ia);
int *end = std::end(ia);
//寻找第一个负数
while (beg != end && *beg >= 0)
++beg;
if (beg != end)
cout << *beg << endl;
else
cout << "没找到!" << endl;
}
- 两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型。
constexpr std::size_t sz = 5;//#include <cstddef>
int arr[sz] = { 1,2,3,4,5 };
int *ip = arr; //等价于int *ip=&arr[0]
int *ip2 = ip + 4; //ip2指向arr的尾元素arr[4]
int *p3 = arr + sz; //正确,但不要解引用
int* ip4 = arr + 10; //错误:arr只有5个元素
auto n = std::end(arr) - std::begin(arr); //n=5
int *b = std::begin(arr), *e = std::end(arr);
while (b<e)//只要两个指针指向同一个数组,或该数组的尾后元素就可以比较
{
//使用*b
++b;
}
- 解引用和指针运算交互。
int ia[] = { 0,2,4,6,8 }; //含有5个整数的数组
int last = *(ia + 4); //正确:last = 8
last = *ia + 4; //正确:last = 0 + 4 = 4
- 标准库类型限定使用下标必须是无符号型,而内置的下标运算无此要求。
int ia[] = { 0,2,4,6,8 };
int i = ia[2];//与下面两条等价
int *p = ia;
i = *(p + 2);
int *p = &ia[2];
int j = p[1]; //6
int k = p[-2]; //0,string,vector的下标不可以为负
3.5.3 C风格字符串
- C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。
- C风格字符串的处理函数定义在cstring头文件(string.h的C++版本)中。
- 最好不要在C++程序中使用C风格字符串,因为不仅使用起来不太方便,而且极易引发程序漏洞。
- 对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。
char ca[] = { 'C','+','+' };
cout << strlen(ca) << endl; //严重错误:ca没有以空字符串结束
string s1 = "A string example";
string s2 = "A different string";
if (s1 < s2) //false:s2小于s1
{
//do something
}
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if (ca1<ca2) //未定义的:试图比较两个无关的指针
{
//do something
}
if (strcmp(ca1,ca2)<0)//和两个string对象的比较效果一样
{
//ca1小于ca2
}
3.5.4 与旧代码的接口
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
- 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
- 如果程序的某处需要一个C风格字符串,无法直接用string对象来代替它。例如不能用string对象直接初始化指向字符的指针。c_str成员函数返回一个C风格的字符串。
string s("Hello World"); //s的内容是Hello World
char *str = s; //错误:不能用string对象直接初始化字符的指针。
const char *str=s.c_str(); //正确,c风格的string
- 如果执行完c_str函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
- 不允许使用vector对象初始化数组,但允许使用数组来初始化vector对象。
//使用数组初始化vector对象
int int_arr[] = {0,1,2,3,4,5};
//ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(std::begin(int_arr),std::end(int_arr));
//拷贝三个元素:int_arr[1]、int_arr[2]、int_arr[3]
vector<int> subVec(int_arr+1,int_arr+4);
- 建议:尽量使用标准库类型而非数组。
3.6 多维数组
- 严格来说C++没有多维数组,多维数组为数组的数组。
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
int arr[10][20][30] ={0}; //将所有元素初始化为0
- 多维数组初始化。
//允许使用花括号初始化多维数组
int ia2[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
int ia3[3][4] ={ 0,1,2,3,4,5,6,7,8,9,10,11};
int ia4[3][4] = {{0},{4},{8}};//{ 0,0,0,0,4,0,0,0,8,0,0,0};
int ia5[3][4] = {0,3,6,9}; // {0,3,6,9,0,0,0,0,0,0,0,0};
- 要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
//用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int(&row)[4] = ia[1];//把row绑定到ia的第二个4元素数组上
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt]; //12个未初始化的元素
//对于每一行
for (size_t i = 0; i != rowCnt; ++i)
//对于每一列
for (size_t j = 0; j != colCnt; ++j)
ia[i][j] = i * colCnt + j;//将位置索引作为值
size_t cnt = 0;
for (auto& row : ia)
for (auto& col : row) {
col = cnt;
++cnt;
}
for (auto row : ia) //取出来的数组,会被编译器转换为指针
for (auto col : row) //错误:int *row没有合适的begin函数
;
for (auto& row : ia) //row声明为引用,可以避免被自动转换为指针
for (auto col : row)
cout << col << endl;
- 指针和多维数组
int ia[3][4];
int(*p)[4] = ia;
p = &ia[2]; //p指向ia的尾元素
//输出ia中每个元素的值,每个内层数组各占一行
//p指向含有4个整数的数组
for (auto p = ia; p != ia+3; ++p){
//q指向4个整数数组的首元素,也就是说,q指向一个整数
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
//p指向ia的第一个数组
for (auto p = std::begin(ia); p != std::end(ia); ++p)
//q指向内层数组的首元素
for (auto q = std::begin(*p); q != std::end(*p); ++q)
cout << *q << ' '; //输出q所指向的整数值
cout << endl;
- 类型别名简化多维数组的指针
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;
}