《C++ Primer》学习笔记/习题答案 总目录
——————————————————————————————————————————————————————
使用 using
声明后,就无须再通过专门的前缀去获取所需的名字了。using
声明具有如下的形式:using namespace::name
。
举个例子:
#include
// using 声明,当使用名字cin时,从命名空间std中获取它
using std::cin;
// 或者使用spacename,就不用单独调用具体的声明了
// using namespace std;
int main()
{
int i;
cin >> i; // ok:cin和std::cin含义相同
cout << i; // error:没有对应的using声明,必须使用完整的名字
std::cout << i; // ok:显式地从std中使用cout
return 0 ;
}
程序中使用的每个名字都需要用独立的 using
声明引入,或者需要引入 spacename
。
头文件中通常不应该包含 using
声明。这是因为头文件的内容会拷贝到所有引用它的文件巾去,如果头文件里有某个 using
声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说, 由于不经意间包含了一些名字, 反而可能产生始料未及的名字冲突。
标准库类型 string
表示可变长的字符序列,定义在头文件 string 中。
初始化 string
的方式:
如果 使用等号 初始化一个变量,实际上执行的是 拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。如果 不使用等号,则执行的是 直接初始化(direct initialization)。
string s5 = "hiya"; // 拷贝初始化
string s6("hiya") ; // 直接初始化
string s7(10, 'c'); // 直接初始化,s7的内容是cccccccccc
string
的操作:
在执行读取操作时,string
对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
#include
#include
using namespace std;
int main()
{
string s;
cin >> s;
cout << s << endl;
system("pause");
return 0;
}
注意:_Hello_World_
,输入的时候有三个空格(这里用三个下划线标注出来),分别在前面、中间和后面,但是输出的时候没有任何空格。
#include
#include
using namespace std;
int main()
{
string s1, s2;
cin >> s1 >> s2;
cout << s1 << s2 << endl;
system("pause");
return 0;
}
当希望能在输出的字符串中保留输入的空白字符时,可以使用 getline
函数,getline
函数可以读取一整行字符。该函数只要遇到换行符就结束读取并返回结果(换行符也被读进来了),然后把所读内容存入到 string
对象中(不保存换行符)。如果输入的开始就是一个换行符,则得到空 string
。
触发
getline
函数返回的那个换行符实际上被丢弃掉了,得到的string
对象中并不包含该换行符。
ernpty
函数根据 string
对象是有为空返问一个对应的布尔值;size
函数返回 string
对象的长度(即 string
对象中字符的个数),返回值其实是 string::size_type
类型,这是一种无符号类型。要使用 size_type
,必须先指定它是由哪种类型定义的。
如果一个表达式中已经有了 size
函数就不要再使用 int
了,这样可以避免混用 int
和 unsigned int
可能带来的问题。
当把 string
对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是 string
。
string s4 = s1 + ", "; // ok: 把一个string对象和一个字面值相加
string s5 = "hello" + ", "; // error: 两个运算对象都不是string
string s6 = s1 + ", " + "world"; // ok: 每个加法运算符都有一个运算对象是string
为了与C兼容,C++语言中的字符串字面值并不是标准库 string
的对象。 切记,字符串字面值与 string
是不同的类型。
头文件 cctype 中的字符操作函数:
建议使用C++版本的C标准库头文件。C语言中名称为 name.h 的头文件,在C++中则被命名为 cname。
C++11提供了范围 for
(range for)语句,可以遍历给定序列中的每个元素并执行某种操作。
for (declaration : expression)
statement
其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration 部分的变量都会被初始化为 expression 部分的下一个元素值。
string str("some string");
// 每行输出str中的一个字符
for (auto c : str) // 对于str中的每个字符
cout << c << endl; // 输出当前字符,后面紧跟一个换行符
如果想在范围 for
语句中改变 string
对象中字符的值,必须把循环变量定义成引用类型。
#include
#include
using namespace std;
int main()
{
string s("Hello, World!!!");
// 转换成大写形式
for (auto &c : s) // 对于s中的每个字符(注意c是引用)
c = toupper(c); // c是一个引用,因此赋位语句将改变s中字符的值
cout << s << endl;
system("pause");
return 0;
}
下标运算符接收的输入参数是 string::size_type
类型的值,表示要访问字符的位置,返回值是该位置上字符的引用。
string
对象的下标必须从0记起,范围是0至 size - 1,左闭右开。使用超出范围的下标将引发不可预知的后果。
C++标准并不要求标准库检测下标是否合法。编程时可以把下标的类型定义为相应的 size_type
,这是一种无符号数,可以确保下标不会小于0,此时代码只需要保证下标小于 size
的值就可以了。另一种确保下标合法的有效手段就是使用范围 for
语句。
标准库类型 vector
表示对象的集合,也叫做 容器(container),定义在头文件 vector 中。vector
中所有对象的类型都相同,每个对象都有一个索引与之对应并用于访问该对象。
vector
是一个 类模板(template),模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说
明。编译器根据模板创建类或函数的过程,称为 实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象
vector
是模板而非类型,由 vector
生成的类型必须包含 vector
中元素的类型,如 vector
。
因为引用不是对象,所以不存在包含引用的 vector
。
在早期的C++标准中,如果 vector
的元素还是 vector
(或者其他模板类型),定义时必须在外层 vector
对象的右尖括号和其元素类型之间添加一个空格,如 vector
。但是在C++11标准中,可以直接写成 vector
,不需要添加空格。
初始化 vector
对象的方法:
vector
对象;vector
对象。vector<int> v1(10); // v1有10个元素,每个的值都是0
vector<int> v2{10}; // v2有1个元素,该元素的值是10
vector<int> v3(10, 1); // v3有10个元素,每个的值都是1
vector<int> v4{10, 1}; // v4有2个元素,值分别是10和1
可以只提供 vector
对象容纳的元素数量而省略初始值,此时会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。这个初值由 vector
对象中的元素类型决定。
push_back
函数可以把一个值添加到 vector
的尾端。
vector<int> v2; // 空vector对象
for (int i = 0; i != 100; ++i)
v2.push_back(i); // 依次把整数值放到v2尾端
// 循环结束后v2有100个元素,值从0到99
范围 for
语句体内不应该改变其所遍历序列的大小。
vector
支持的操作:
vector
和 string
对象的下标运算符只能用来访问已经存在的元素,而不能用来添加元素。
vector<int> ivec; // 空vector对象
cout << ivec[O]; // 错误:ivec不包含任何元素
vector<int> ivec2(10); // 含有10个元素的vector对象
cout << ivec2[10]; // 错误: ivec2元素的合法索引是从0到9
正确的添加方法是使用 push_back
:
vector<int> ivec; // 空vector对象
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
{
ivec[ix] = ix; // 严重错误:ivec包含任何元素
ivec.push_back(ix); // 正确:添加一个新元素,该元素的值是ix
}
迭代器的作用和下标运算类似,但是更加通用。所有标准库容器都可以使用迭代器,但是其中只有少数几种同时支持下标运算符。迭代器的作用也和指针类型类似,也提供了对对象的间接访问。
定义了迭代器的类型都拥有 begin
和 end
两个成员函数。begin
函数返回指向第一个元素的迭代器,end
函数返回指向容器 尾元素的下一位置(one past the end) 的迭代器。
// 由编译器决定b和e的类型;
// b表示v的第一个元素, e表示v尾元素的下一位置
auto b = ivec.begin(), e = ivec.end(); // b和e的类型相同
end
函数通常被称作 尾后迭代器(off-the-end iterator) 或者简称为 尾迭代器(end iterator)。
如果容器为空,则 begin
和 end
返回的是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符:
因为 end
返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。
在 for
或者其他循环语句的判断条件中,最好使用 !=
而不是 <
。所有标准库容器的迭代器都定义了 ==
和 !=
,但是只有其中少数同时定义了 <
运算符。
如果 vector
或 string
对象是常量,则只能使用 const_iterator
迭代器,该迭代器只能读元素,不能修改元素。
vector<int>::iterator it; // it能读写vector的元素
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::iterator
auto it2 = cv.begin(); // it2的类型是vector::const_iterator
C++11新增了 cbegin
和 cend
函数,不论 vector
或 string
对象是否为常量,都返回 const_iterator
迭代器。
auto it3 = v.cbegin(); // it3的类型是vector::const_iterator
if ( it < mid)
// 处理Vl前半部分的元素
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个送代器的距离。所谓距离,是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,difference_type
类型用来表示两个迭代器间的距离,这是一种带符号整数类型。
使用迭代器完成了二分搜索:
// text必须是有序的
// beg和end表示我们搜索的范围
// beg指向搜索范围内的第一个元素、end指向居元素的下一位置、mid指向中间的那个元素
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下的中间点
// 当还有元素尚未检查并且还没有找到sought时执行循环
whi1e (mid != end && *mid != sought)
{
if (sought < *mid) // 我们要找的元素在前半部分吗?
end = mid; // 如果是,调整搜索范围使得忽略掉后半部分
e1se // 我们要找的元素在后半部分
beg = mid + 1; // 在mid之后寻找
mid = beg + (end - beg)/2; // 新的中间点
}
数组类似 vector
,也是存放类型相同的对象的容器,但数组的大小确定不变,不能随意向数组中添加元素。
如果不清楚元素的确切个数,应该使用 vector
。
数组是一种复合类型,声明形式为 a[d]
,其中 a 是数组名称,d 是数组维度。维度说明了数组中元素的个数,因此必须大于0 。数组中元素的个数也属于数组类型的一部分, 编译的时候维度应该是己知的,即维度必须是一个常量表达式。
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[cnt]; // error:cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确,否则错误
默认情况下,数组的元素被默认初始化。
定义数组的时候必须指定数组的类型,不允许用 auto
关键字由初始值列表推断类型。
如果定义数组时提供了元素的初始化列表,则允许省略数组维度,编译器会根据初始值的数量计算维度。但如果显式指明了维度,那么初始值的数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化。
const unsigned sz = 3;
int ia1[sz] = {0,1,2}; // 含有3个元素的数组,元素值分别是0,1,2
int a2[] = {0, 1, 2}; // 维度是1的数组
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}; // error:初始值过多
可以用字符串字面值初始化字符数组,但字符串字面值结尾处的空字符也会一起被拷贝到字符数组中。
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
char a3[] = "C++"; // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // error:没有空间可存放空字符!
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
int a[] = {O , 1 , 2}; // 含有3个整数的数组
int a2[] = a; // error:不允许使用一个数组初始化另一个数组
a2 = a; // error:不能把一个数组直接赋值给另一个数组
从数组的名字开始由内向外阅读有助于理解复杂数组声明的含义。
int *ptrs[10]; // ptrs是含有10个整型指针的数组
int &refs[10] = /* ? */; // error:不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
数组下标通常被定义成 size_t
类型,这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。size_t
定义在头文件 cstddef 中。
数组除了大小固定这一特点外,其他用法与 vector
基本类似。
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。
string nums[] = {"one", "two", "three"}; // 数组的元素是string对象
string *p = &nums[0]; // p指向nums的第一个元素
string *p2 = nums; // 等价于p2 = &nums[0]
一维数组寻址公式:
当使用数组作为一个 auto
变量的初始值时,推断得到的类型是指针而非数组。但 decltype
关键字不会发生这种转换,直接返回数组类型。
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia是一个含有10个整数的数纽
auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素
ia2 = 42; // error:ia2是一个指针,不能用int值给指针赋值
auto ia2(&ia[0]); // 显然ia2的类型是int*
// ia3是一个含有10个整数的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; // error:不能用整型指针给数组赋值
ia3[4] = i; // ok:把i的值赋给ia3的一个元素
C++11在头文件 iterator 中定义了两个名为 begin
和 end
的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。
特别注意,后指针不能执行解引用和递增操作。
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia是一个含有10个整数的数组
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向arr尾元素的下一位置的指针
给(从) 一个指针加上(减去)某整数值,结果仍是指针。两个指针相减的结果类型是 ptrdiff_t
,这是一种定义在头文件 cstddef 中的带符号类型。因为差值可能为负值, 所以 ptrdiff_t
是一种带符号类型。
constexpr size_t sz = 5;
int arr[sz] = {1, 2, 3, 4, 5};
int *ip = arr; // 等价于int*ip = &arr[O]
int *p2 = ip + 4; // ip2指向arr的尾元素arr[4]
auto n = end(arr) - begin(arr); // n的值是5 ,也就是arr中元素的数量
表达式 * (ia+4)
计算 ia
前进4个元素后的新地址,解引用该结果指针的效果等价于表达式 ia[4]
。
int ia[] = {0, 2, 4, 6, 8}; // 含有5个整数的数组
int last = *(ia + 4); // ok:把last初始化成8,也就是ia[4]的值
标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。
尽管C++支持C风格字符串,但在C++程序中最好还是不妥使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞, 是诸多安全问题的根本原因。
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的 C风格字符串(C-style character string)。C风格字符串是将字符串存放在字符数组中,并以 空字符结束(null terminated)。这不是一种类型,而是一种为了表达和使用字符串而形成的约定俗成的书写方法。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符 \0
。
一般利用指针来操作这些字符串。
C风格字符串的函数:
C风格字符串函数不负责验证其参数的正确性,传入此类函数的指针必须指向以 空字符 作为结尾的数组。
对大多数程序来说,使用标准库string
要比使用C风格字符串更加安全和高效。
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:
string
对象或为 string
对象赋值。string
对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。string
对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧运算对象。不能用 string
对象直接初始化指向字符的指针。为了实现该功能,string
提供了一个名为 c_str
的成员函数,返回 const char*
类型的指针,指向一个以空字符结束的字符数组,数组的数据和 string
对象一样。
string s("Hello World"); // s的内容是Hello World
char *str = s; // error: 不能用string对象初始化char*
const char *str = s.c_str();// ok
针对 string
对象的后续操作有可能会让 c_str
函数之前返回的数组失去作用,如果程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
可以使用数组来初始化 vector
对象,但是需要指明要拷贝区域的首元素地址和尾后地址。
int int_arr[] = {0, 1, 2, 3, 4, 5};
// ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));
在新版本的C++程序中应该尽量使用 vector
、string
和迭代器,避免使用内置数组、C风格字符串和指针。
C++中的多维数组其实就是数组的数组。当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。
多维数组初始化的几种方式:
int ia[3][4] =
{ // 三个元素,每个元素都是大小为3的数组
{0, 1, 2, 3}, // 第1行的初始值
{4, 5, 6, 7}, // 第2行的初始值
{8, 9, 10, 11} // 第3行的初始值
};
// 没有标识每行的花括号,与之前的初始化语句是等价的
int ib[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显式地初始化每行的首元素
int ic[3][4] = {{ 0 }, { 4 }, { 8 }};
// 显式地初始化第1行,其他元素执行值初始化
int id[3][4] = {0, 3, 6, 9};
可以使用下标访问多维数组的元素,数组的每个维度对应一个下标运算符。
// 用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1]; // 把row绑定到ia的第二个4元素数组上
多维数组寻址公式:
使用范围 for
语句处理多维数组时,为了避免数组被自动转换成指针,语句中的外层循环控制变量必须声明成引用类型。
for (const auto &row : ia) // 对于外层数组的每一个元素
for (auto col : row) // 对于内层数组的每一个元素
cout << col << endl;
如果 row 不是引用类型,编译器初始化 row 时会自动将数组形式的元素转换成指向该数组内首元素的指针,这样得到的 row 就是 int*
类型,而之后的内层循环则试图在一个 int*
内遍历,程序将无法通过编译。
for (auto row : ia)
for (auto col : row)
使用范围 for
语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。
因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数纽
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素
声明指向数组类型的指针时,必须带有圆括号。
int *ip[4]; // 整型指针的数组
int (*ip)[4]; // 指向含有4个整数的数组
C++11新标准,使用 auto
和 decltype
能省略复杂的指针定义。
// 输出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;
}
使用标准库函数 begin
和 end
也能实现同样的功能,而且看起来更简洁一些。
// p指向ia的第一个数组
for (auto p = begin(ia); p != end(ia); ++p)
{
// q指向内层数组的首元素
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' '; // 输出q所指的整数值
cout << endl;
}