本文是通过第五版《C++ primer》进行的查漏补缺。

输入输出

标准库定义了4个IO对象:

  • cin : istream类型的对象,标准输入(standard input)
  • cout : ostream类型的对象,标准输出(standard output)
  • cerr : ostream类型的对象,标准错误(standard error)
  • clog : ostream类型的对象,用来输出程序运行时的一般信息

一种不用namespace std的写法:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main(){
std::cout<<"enter two numbers:"<<std::endl;
int v1=0, v2=0;

std::cin>>v1>>v2;
std::cout<<"the sum of "<<v1<<" and "<<v2<<" is "<<v1+v2<<std::endl;

return 0;
}

std::cout 两个冒号是一个运算符,作用域运算符。它表示我要把std作用域里面的cout拿出来用。

endl 操作符,结束当前行,将设备相关的缓冲区内容刷到屏幕上。

cin可以跳过空格、制表符、换行符等空白字符。

一般来说,自己创建的头文件,用双引号。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main(){
int sum=0, value=0;

while(std::cin>>value){ //读取数值直到文件结尾,或读入错误
sum+=value;
}

std::cout<<"the sum is "<<sum<<std::endl;
return 0;
}

在上例中,无效的istream对象会使条件变为假,例如输入一个字母(非int型)。

基本内置类型

char比较特殊,分为三种:charsigned charunsigned charcharsigned charunsigned char其中的一种,由编译器决定:VC编译器、x86上的GCC都把char定义为signed char,而arm-linux-gcc把char定义为unsigned char

为了保持程序的移植性,应当明确指出到底是哪一种。

三者都占1个字节。signed char取值范围是-128~127(有符号位),unsigned char取值范围是0~255

原始的ASCII标准里,定义的字符码值是只有0~127,所以怎么定义的char都刚好装得下。

变量

记一种写法:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int main(){
std::string book("newbrush");
//what...?
std::cout<<book<<std::endl;
return 0;
}
//运行结果(输出了字符串):
//newbrush

初始化:

1
2
3
4
int units_sold = 0; //以前的写法
int units_sold = {0}; //列表初始化(C++11)
int units_sold{0}; //列表初始化
int units_sold(0);

若使用列表初始化,且初始值存在丢失信息的风险,则编译器报错:

1
2
3
long double ld=3.1415926536;
int a{ld},b={ld}; //报错,转换未执行
int c(ld),d=ld; //旧写法,不报错,转换执行,同时丢失了部分值

若只声明而不定义,就在变量前添加extern关键字,且不要显式地初始化变量:

1
2
extern int i; //声明 i 而非定义 i
int j; //声明并定义 j

若不希望别的文件通过extern引用,可以使用static,这样作用域就是本文件。

总结:extern不是定义,是引入(声明)在其他源文件中定义的非static全局变量。

名字的作用域(scope):

  • 同一个名字出现在程序的不同位置,也可能指向不同的实体。
  • C++中大多数作用域都以花括号分隔。
  • 名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端作为结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//这是一个不好的例子
#include <iostream>
int reused = 42; // 全局作用域

int main(){
int unik = 0; //块作用域
std::cout<<reused<<" "<<unik<<std::endl;

int reused = 0; //同名的新建局部变量,覆盖了全局变量

std::cout<<reused<<" "<<unik<<std::endl;

std::cout<<::reused<<" "<<unik<<std::endl;
//显式地访问全局变量

return 0;
}
/*------------运行结果-----------
42 0
0 0
42 0
-------------------------------*/

复合类型(compound type)

引用(reference),为对象起的别名:

1
2
3
int ival = 1024;
int &refVal = ival; //refVal 指向ival (是ival的另一个名字)
int &refVal2; //报错,引用必须初始化

定义引用时,把引用和它的初始值绑定在一起,而不是把初始值拷贝给引用。引用不是对象,所以不能定义引用的引用,不能定义指向引用的指针。

1
2
3
reVal = 2; //把2给refVal指向的对象,即赋给了ival
int li = refVal; //等同于li=ival
int &refVal3 = refVal; //正确:refVal3绑定到了那个与refVal绑定的对象上,即绑定了ival

可以使用取地址符&获取指针所封装的地址:

1
2
3
int ival = 42;
int *p = &ival; //p是指向ival的指针
double *dp = &ival; //错误!类型不匹配

对于“指针的值+1”的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>

int main(){
std::cout<<"hello this is a test"<<std::endl;
int a = 233;
int *p = &a;
std::cout<<"adress: "<<p<<std::endl;
std::cout<<"neirong: "<<*p<<std::endl;
std::cout<<"adress+1: "<<p+1<<std::endl;
return 0;
}
/*---------------运行结果----------------
hello this is a test
adress: 0x61fe14
neirong: 233
adress+1: 0x61fe18
---------------------------------------*/

/*----------explanation-----------
p+1的效果反映在地址上(单词拼错小问题)
在博主本人的机器上,int型占4个字节
--------------------------------*/

可以使用解引用符*利用指针访问对象:

1
2
3
4
5
int ival = 42;
int *p = &ival; //p是指向ival的指针
std::cout<<*p; //输出42
*p=0;
std::cout<<*p //输出0

空指针(null pointer),不指向任何对象。在使用一个指针前,可以先检查它是否为空。

1
2
3
4
5
int *p1 = nullptr; //C++11 ,推荐写法
int *p2 = 0;
int *p3 = NULL; //需要 #include <cstdlib>
int zero = 0;
p1 = zero; //错误!类型不匹配

void *指针,纯粹的地址封装,与类型无关。可以用于存放任意对象的地址:

1
2
3
double obj = 3.14, *pd = &obj;
void *pv = &obj;
pv = pd;

指向指针的指针:

1
2
3
int ival = 1024;
int *pi = &ival;
int **ppi = &pi; //ppi指向一个int型的指针

指针的引用:

1
2
3
4
5
int i = 1024;
int *p;
int *&r = p; //r是一个对指针p的引用
r = &i; //r引用了一个指针,就是令p指向i
*r = 0; //解引用得到i,将i的值改为0

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
int main(){
int i = 1024;
int* p;
int*& r = p; //r是一个对指针p的引用
r = &i;
cout << *r << " " << *p ;
return 0;
}
/*-----输出------
1024 1024
---------------*/

const限定符

const对象必须初始化:

1
2
3
const int i = get_size(); //正确,运行时初始化
const int j = 42; //正确,编译时初始化
const int k; //错误!未初始化

默认状态下,const对象仅在文件内有效,若想在多个文件间共享const对象,必须在变量的定义之前添加关键字extern

1
2
3
4
//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file_1.h头文件
extern const int bufSize;

const的引用,对常量的引用:

1
2
3
const int ci = 1024;
const int &r1 = ci; //正确
int &r2 = ci; //错误!存在通过r2改变ci(const)的风险

一个奇怪的例子:

1
2
3
double dval = 3.14;
const int &ri = dval; //允许
int &ri = dval; //错误!因为改变的是编译器生成的中间量

指向常量的指针:

1
2
3
4
5
6
const double pi = 3.14;
double *ptr = &pi; //错误!存在通过ptr指针修改pi的风险
const double *cptr = &pi;
*cptr = 42; //错误!
double dval = 3.14;
cptr = &dval; //正确,但不能通过cptr修改dval的值

const指针(必须初始化):不变的是指针本身的值,而不是它指向的那个值。

1
2
3
4
5
6
7
8
9
10
11
int errNumb = 0;
int *const curErr = &errNumb; //常指针,顶层
const double pi = 3.14159;
const double *const pip = &pi; //指向常量的常量指针

if(*curErr){
errorHandler();
*curErr = 0; //正确,试图修改变量errNumb
}

*pip = 2.71; //错误!试图修改常量pi

顶层const:表示变量本身是一个常量。底层const:表示指针所指向的对象是一个const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 0;
int *const p1 = &i; //顶层
const int ci = 42; //顶层
const int *p2 = &ci; //底层
const int *const p3 = p2; //(左:底层),(右:顶层)

p2 = p3; //正确。从顶层的角度来说,p2是个变量,p3是个常量,
//这个赋值没有问题。从底层的角度来说,都是一样的,
//指向的内容都是不会去修改的

int *p = p3; //错误!存在通过*p修改*p3(const)的风险
//p3的底层是const int ,而这个是int

p2 = &i; //正确。只是不能通过p2修改i而已

constexpr变量(C++11标准):允许将变量声明为constexpr类型,以便由编译器来验证变量的值是否是一个常量表达式。

  • 一定是一个常量
  • 必须用常量表达式初始化
  • 自定义类型、IO库、string等类型不能被定义为constexpr
1
2
3
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size(); //只有当size是一个constexpr函数时才正确

指针和constexpr:限定符仅对指针有效,对其所指的对象无关。(对顶层有效,底层无效)

1
2
3
4
5
6
7
constexpr int *np = nullptr; //常指针
int j = 0;
constexpr int i = 42;
......
//i和j必须定义在函数之外
constexpr const int *p = &i; //p是常指针,指向常量
constexpr int *p1 = &j; //p1是常指针,指向变量j

typedef、auto、decltype

类型别名,提高可读性:

1
2
3
typedef double wages;
typedef wages base, *p; //base是double的同义词,p是double *的同义词
using SI = Sales_item; //C++11,别名声明。左边是别名

对于指针,类型别名的使用可能会产生意想不到的结果(平时不用就好了):

1
2
3
4
5
typedef char *pstring;
const pstring cstr = 0; //指向char的常量指针
const pstring *ps; //ps是指针变量,它的对象是指向char的常量指针

const char *cstr = 0; // ! 是对 const pstring cstr = 0; 的错误理解

auto类型说明符,C++11,让编译器通过初始值推断变量的类型:

1
2
auto i = 0, *p = &i; //正确
auto sz = 0, pi = 3.14; //错误!auto已经被推断为int,后面却不一致

看看就好,不要较真,我觉得一般不会用到这些:

17-1.png

decltype类型说明符,获取表达式的类型。在编译时推导出一个表达式的类型,并且不会计算表达式的值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
int x = 0;
decltype(x) y = 1; // y -> int
decltype(x+y) z = x + y; // z - > int

const int& i = x;
decltype(i) j = y; // j -> const int&

const decltype(z) *p = &z; // p-> const int *
decltype(z) *pi = &z; // pi -> int*
decltype(pi) *pp = π //pp -> int**

decltype(f()) sum = x; //sum的类型就是函数f返回的类型

auto与decltype类似但是又不同,auto只能根据变量的初始化表达式推导出变量应该具有的类型。decltype将精确的推导出表达式定义的类型,不会舍弃和弃用cv限定符。

一个例子:

1
2
3
4
5
6
int i = 42, *p = &i, &r = i;
decltype(*p) c; //错误!解引用表达式,c的类型为引用,需要初始化

decltype(i) e; //正确,e是一个未初始化的int
decltype((i)) d; //错误!d是int&类型,必须初始化
decltype(((i))) d1 = i; //正确,d1是int&类型,且已初始化

自定义数据结构、类和头文件

类定义可以使用关键字classstruct,二者默认的继承、访问权限不同,structpublic的,classprivate的。

编写自己的头文件:

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef SALES_DATA_H //习惯大写
#define SALES_DATA_H

#include <string>

struct Sales_data{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

#endif

函数的声明应该放在头文件中,内联函数的定义也应该放在头文件中。

【实例】分离式编译:

1
2
3
4
5
6
7
// Chapter6.h
#ifndef CHAPTER6_H_INCLUDED
#define CHAPTER6_H_INCLUDED

int fact(int);

#endif // CHAPTER6_H_INCLUDED
1
2
3
4
5
6
7
8
// fact.cpp
#include "Chapter6.h"
using namespace std;

int fact(int val){
if(val==0 || val==1) return 1;
else return val * fact(val-1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// factMain.cpp
#include <iostream>
#include "Chapter6.h"
using namespace std;

int main(){
int num;
cout << "input an int num: ";
cin >> num;
cout << num << "! = " << fact(num) <<endl;
system("pause");
return 0;
}

17-1dot5.png

如上图,在终端中输入以下命令:

1
g++ factMain.cpp fact.cpp -o scexamp

标准库类型string

17-2.png

1
2
3
4
5
6
string s1,s2;
cin>>s1>>s2;
cout<<s1<<s2<<endl;
//输入:hello world
//输出:helloworld
//s1装的是hello,s2是world

getline得到的string对象不包含换行符:

1
2
3
4
5
6
7
8
int main(){
string line;
//每次读入一整行,包括空白,直到文件末尾
while(getline(cin,line)){
cout<<line<<endl;
}
return 0;
}

字面值和string对象相加:

17-3.png

cctype中的一些函数:

17-4.png

1
2
for(string::size_type i=0; i!=s.size(); i=i+2)
s[i]='x'; //一个使用string::size_type的例子

从逻辑上讲,size()成员函数应该似乎返回整型数值,但事实上,size操作返回是string::size_type类型的值。string类类型和其他许多库类型都定义了一些配套类型(companion type)。通过这些配套类型,库函数的使用就与机器无关(machine-independent)。size_type就是这些配套类型中的一种。它定义为与unsigned型(unsigned intunsigned long)具有相同含义,而且保证足够大的能够存储任意的string对象的长度。string::size_type在不同的机器上长度可以不同,并非固定。但只要使用该类型,就使得程序适合机器。string对象的索引也应为size_type类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//程序例子
int main(){
string s("Hello World!!!");
decltype(s.size())punct_cnt = 0;
for(auto c : s){ //for every char in s
if(ispunct(c))++punct_cnt;
}
//略
string orig = s;
for(auto &c : s){ //需要修改字符串s
c = toupper(c);
}
cout<<s<<endl;
//略
}

标准库类型vector

17-5.png

访问vector的一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<vector>
#include<iostream>

int main(){
std::vector<int> v{1,2,3,4,5,6};
for(auto &i : v){ //与上例类似
i*=i;
}
for(auto i : v){
std::cout<<i<<" ";
}
std::cout<<std::endl;
return 0;
}

17-6.png

迭代器(iterator)

有迭代器的类型都拥有beginend成员。如果容器为空,则beginend返回的是同一个迭代器,都是尾后迭代器

1
2
auto b = v.begin(), e = v.end();
//b表示v的第一个元素,e表示v尾元素的下一位置

17-7.png

迭代器类型,iteratorconst_iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vector<int>::iterator it; //it能读写vector<int>元素
string::iterator it2; //it2能读写string对象中的字符
vector<int>::const_iterator it3; //it3只能读,不能写
string::const_iterator it4; //it4只能读,不能写

/*-----分--割--线--QAQ-----*/

vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); //it1的类型是vector<int>::iterator
auto it2 = cv.begin(); //it2的类型是vector<int>::const_iterator

/*-----分--割--线--QAQ-----*/

//有时我们希望即使对象不是常量,也使用const_iterator,
//C++11引入了cbegin和cend
auto it3 = v.cbegin(); //it3的类型是vector<int>::const_iterator

结合解引用的成员访问:

1
2
3
4
5
6
vector<string> v;
auto it = v.begin();

(*it).empty();
*it.empty(); //错误!
it->empty(); //箭头运算符:把解引用和成员访问两个操作合在一起

任何一种可能改变vector对象容量的操作,都会使得相应的迭代器失效。

迭代器运算:

17-8.png

数组

一种写法:int a[]={1,2,3};[]内可以不填数字。

字符数组的特殊性:字符串字面值的结尾处有一个空字符。

1
2
3
4
char a1[] = {'C','+','+'}; //列表初始化,没有空字符
char a2[] = {'C','+','+','\0'}; //列表初始化,显式写出了空字符
char a3[] = "C++"; //将自动包含空字符
const char a4[6] = "Danial"; //错误!没有空间放空字符

复杂的数组声明:

1
2
3
4
5
int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10] = /* ? */; //错误!不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针

17-9.png

数组的beginend函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <iterator>
int main(){
int a[]={1,2,3,4};
int *p1 = std::begin(a);
int *p2 = std::end(a);

for(;p1 != p2; p1++){
std::cout<<*p1<<" ";
}
return 0;
}
/*-----运行结果-----
1 2 3 4
-----------------*/

指针运算:

17-10.png

下标和指针:

1
2
3
4
int ia[]={0,2,4,6,8};
int *p = &ia[2]; // 指向“4”
int j = p[1]; // j = 6
int k = p[-2]; // k = 0 , string和vector的下标不可以为负

C风格字符串:处理函数定义在cstring头文件中。

17-11.png

1
2
char ca[]={'C','+','+'};
cout<<strlen(ca)<<endl; //错误!ca[]没有以 '\0' 结束

与旧代码的接口:

1
2
3
string s("Hello World");
char *str = s; //错误!不能这样用
const char *str = s.c_str(); //正确,c风格的string

使用数组初始化vector对象:

1
2
3
4
int int_arr[]={0,1,2,3,4,5};
vector<int> ivec(std::begin(int_arr),std::end(int_arr));
// 左闭右开
vector<int> subVec(int_arr+1,int_arr+4); // 1 2 3

多维数组

将所有元素初始化为0:int arr[10][20][30]={0};

初始化:

1
2
3
4
5
6
7
8
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}

下标引用:

1
int (&row)[4] = ia[1]; //把row绑定到ia的第二个4元素数组上

使用范围for语句处理多维数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
size_t cnt = 0;
for (auto &row : ia) //对于外层数组的每一个元素
for (auto &col : row) { //对于内层数组的每一个元素
col = cnt;
++cnt;
}
/*----------------------
在上面的例子中,因为要改变数组元素的值,所以使用引用类型。
对于没有写操作的,可以参考下例:避免数组被自动转成指针
----------------------*/
for (const auto &row : ia)
for (auto col : row)
cout << col << endl;

指针和多维数组:

17-12

1
2
3
4
5
6
7
//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;
}

类型别名简化多维数组的指针:

1
2
3
4
5
6
7
//和上例一样
using int_array = int[4];
for (int_array *p = ia; p!=ia+3; p++){
for (int *q = *p; q!=*p+4; q++)
cout << *q << ' ';
cout << endl;
}

表达式基础

概念:左值和右值,上网查。

如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。例如,对于int *p

  • 因为解引用运算符生成左值,所以decltype(*p)的结果是int&
  • 因为取地址运算符生成右值,所以decltype(&p)的结果是int **

如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

算术运算符

m%(-n)等于m%n(-m)%n等于-(m%n)

成员访问、条件、位运算符

成员访问运算符,ptr->mem等价于(*ptr).mem

1
2
3
4
string s1 = "a string", *p = &s1;
auto n = s1.size();
n = (*p).size();
n = p->size();

位运算符作用于整数类型。关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型

1
2
3
//假设char占8位,int占32位
unsigned char bits = 0233; //八进制,二进制为 10011011
bits << 8; //bits提升为int型,然后左移8位

使用位运算符:假设班级中有30个学生,用一个二进制位表示某个学生在测试中是否通过。例子如下:

1
2
3
4
unsigned long quizl = 0;
quizl |= 1UL << 27; //学生27通过了测试
quizl &= ~(1UL << 27); //学生27没有通过测试
bool status = quizl & (1UL << 27); //查询学生27是否通过了测试

sizeof和逗号运算符

1
2
3
4
5
6
7
8
9
10
11
12
Sales_data data, *p;
sizeof(Sales_data); // Sales_data类型的对象所占空间的大小
sizeof data; //和上一行结果一样
sizeof *p; //和上一行结果一样
sizeof p; //指针所占空间的大小

sizeof data.revenue; //对象里的成员的大小
sizeof Sales_data::revenue; //C++11,和上一行结果一样

//sizeof运算能够得到整个数组的大小
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz]; //正确

类型转换

看看就好,一般不用:

17-13.png

指针的转换:

  • 0或字面值nullptr能够转换成任意指针类型
  • 指向任意非常量的指针能够转换成void*
  • 指向任意对象的指针能够转换成const void*

显式转换:强制转换cast-name<type>(expression)cast-namestatic_castdynamic_castconst_castreinterpret_cast中的一种。

1
2
3
4
5
6
int i,j;
double slope = static_cast<double>(j) / i;

double d;
void *p = &d;
double *dp = static_cast<double*>(p); //正确

const_cast只能改变运算对象的底层const,对于将常量对象转换成非常量对象的行为,称为『去掉const性质(cast away the const)』。

const_cast可以移除底层const,或是给普通的类型添加底层const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(){
int a = 5;
const int *cp = &a;
int *p = const_cast<int*>(cp);
*p = 4; //允许
cout << a;
return 0;
}
/*-------- 对 比 ---------*/
int main(){
const int a = 5;
const int *cp = &a;
int *p = const_cast<int*>(cp);
*p = 4; //没有定义
cout << a;
return 0;
}

static_cast不能去掉const性质。const_cast不能改变类型。

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。

17-14.png

条件语句

switch-case,case标签必须是整型(小整型、bool型、short、char也都可以)常量表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//关于switch-case里面的初始化的一个例子
// ......
int main(){
// ......
switch(2){
case 1:
int c; //不能初始化!
break;
case 2:{
cout<<"before c = "<<c<<endl;
c = 1; //在case1中声明并定义的c可以在这里使用
int a = 1; //用大括号括起来,则可以初始化
cout<<"after c = "<<c<<endl;
break;
}
default: break;
}
// ......
}

迭代语句

范围for语句:

1
2
3
4
5
6
7
8
9
10
vector<int> v = {0,1,2,3,4,5,6};
//范围变量必须是引用类型,这样才能对元素执行写操作
for (auto &r : v){
r *= 2;
}
/*-------- 对 比 ---------*/
for (auto beg=v.begin(),end=v.end(); beg!=end; ++beg){
auto &r = *beg; //r是引用类型才能对元素执行写操作
r *= 2;
}

跳转语句

goto语句:无条件跳转到同一函数内的另一条语句。一般情况下不要使用goto

1
2
3
4
5
6
7
8
9
10
11
12
13
    //...
goto end;
int ix = 10; //错误!goto语句绕过了一个带初始化的变量定义
end:
//错误!此处的代码需要使用ix
ix = 42;

//向后跳过一个初始化的变量定义是合法的
begin:
int sz = get_size();
if(sz<=0){
goto begin; //goto语句执行后,将销毁sz
}

异常处理

运行时的反常行为,例如读取或写入数据时失去数据库链接。

throw表达式:异常检测部分使用throw表达式来表示它遇到了无法解决的问题。

runtime_error是标准库异常类型的一种,定义在stdexcept头文件。它抛出一个异常,终止当前的函数,并把控制权交给处理异常的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//对于简单的小程序
Sales_item item1,item2;
cin >> item1 >> item2;
if(item1.isbn() == item2.isbn()){
cout << item1+item2 << endl;
return 0; //表示成功
}else{
cerr << "Data must refer to same ISBN" << endl;
return -1; //表示失败
}
/*--------------- 对 比 -----------------*/
if (item1.isbn() != item2.isbn()){
throw runtime_error("Data must refer to same ISBN");
}
//如果程序执行到这里,表示两个ISBN是相同的
cout << item1+item2 <<endl;

try语句块:异常处理部分使用try语句块处理异常,可以有一个或多个catch

1
2
3
4
5
6
7
8
9
10
11
12
13
while(cin>>item1>>item2){
try{
//执行添加两个Sales_item对象的代码
//如果添加失败,代码抛出一个runtime_error异常
}catch(runtime_error err){
//提醒用户两个ISBN必须一致,询问是否重新输入
cout << err.what() //返回初始化对象时填入的参数(const char*)
<< "\nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if (!cin || c=='n') break; //跳出while
}
}

一套异常类:throw表达式和相关的catch子句之间传递异常的具体信息。这些异常分别定义在4个头文件中:

  • exception头文件:最通用的异常类exception,只报告异常的发生,不提供额外信息
  • stdexcept头文件:定义了几种常用的异常类
  • new头文件:bad_alloc异常类
  • type_info头文件:bad_cast异常类

函数基础

局部对象:

  • 自动对象:生命周期从变量声明开始,到函数块末尾结束
  • 局部静态对象:生命周期从变量声明开始,直到程序结束才销毁
1
2
3
4
5
size_t count_calls(){
int a; //自动对象
static size_t ctr = 0; //局部静态对象
return ++ctr;
}

参数传递

指针型变量在函数体中需要被改变的写法:

1
2
3
void f(int *&x){
//do something...
}

一维数组作为参数,除了将数组名传入函数外,为了规范化,还要将数组的大小作为参数传入:

1
2
3
int sum_arr (int att[] , int size){
// do something...
}

数组名是首元素的地址,因此还可以写成:

1
2
3
int sum_arr (int *att , int size){
// do something...
}

不管是哪种定义,使用函数时都是将数组名作为参数,比如:sum_arr (Ss , 66);。在函数内部对传入的数组进行修改,该数组本身的值也会改变。

若要防止在函数中修改数组,可以使用const

1
int sum_arr (const int att[] , int size)

前面将数组的首元素的地址和长度传入,这样就可以处理所有元素。C++中引入了新的方式,即数组区间:传入数组的首元素地址和末尾地址,参数就是【数组名,数组名+长度】,这样也可以处理所有元素。进一步,也可以传入任意区间。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sum_arr(const int *begin,const int *end)
{
const int *pt;
int total = 0;

for(pt = begin ; pt != end ; pt++)
{
total = total + *pt;
}
return total;
}

int A[Size] = {0,1,2,3,4,5,6,7} ;
int sum = sum_arr(A,A+8);

二维数组作为参数,与一维类似,有两种声明方式,但有所区别:

1
2
3
int sum(int A[ ][4] , int size) //不能写成 int sum(int A[ ][ ] , int size),必须将列数写进去,size的值是行数
//下面一种写法看看就好:
int sum(int (*A)[4] , int size) //同样,必须将列数写进去,size的值是行数,而且必须要将*和数组名括起来。

至于使用方法都是一样,sum(A,4);。第二维长度有较严格的要求:

1
2
3
4
5
6
7
8
void f(int x[][5] , int mysize){
//do something...
}

int a[10][5];
int b[10][3];
f(a,10); //正确
f(b,10); //错误!

如果函数无需改变引用形参的值,最好将其声明为常量引用:

1
2
3
bool isShorter(const string &s1, const string &s2){
return s1.size()<s2.size();
}

C++允许将变量定义为数组的引用:

1
2
3
4
5
6
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10]){ // ()不能少
for(auto elem : arr){
cout<<elem<<endl;
}
}

main处理命令行选项。有时需要给main传实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog内,可以向程序传递下面的选项:

1
prog -d -o ofile data0

这些命令行选项通过两个(可选的)形参传递给main函数:

1
2
3
4
5
6
int main(int argc, char *argv[]){/* do something */}
/* 第二个形参argv是一个数组,它的元素是指向C风格字符串的指
针;第一个形参argc表示数组中字符串的数量。因为第二个形参是
数组,所以main函数也可以定义成如下的方式: */
int main(int argc, char **argv){/* do something */}
//其中argv指向char*

以上面提供的命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:

1
2
3
4
5
6
argv[0] = "prog"; //或者argv[0]也可以指向一个空字符串
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0; //最后一个指针之后的元素值保证为 0

含有可变形参的函数:参数个数不固定。如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。

17-15.png

1
2
3
4
5
6
7
8
9
10
11
12
//和vector不一样,initializer_list对象中的元素永远是常量值
void error_msg(initializer_list<string> il){
for(auto beg=il.begin(); beg!=il.end(); ++beg){
cout<<*beg<<" ";
}
cout<<endl;
}
//excepted和actual是string对象
if(excepted!=actual)
error_msg({"functionX",excepted,actual});
else
error_msg({"functionX","okey"});

省略符形参:上网查。

返回类型和return语句

不要返回局部对象的引用或指针:

1
2
3
4
5
6
7
8
9
//严重错误:这个函数试图返回局部对象的引用
const string &manip(){
string ret;
//以某种方式改变一下ret
if(!ret.empty())
return ret; //错误!返回局部对象的引用
else
return "Empty"; //错误!"Empty"是一个局部临时变量
}

列表初始化返回值(C++11):

1
2
3
4
5
6
7
8
9
10
vector<string> process(){
//...
//expected和actual是string对象
if(expected.empty())
return {}; //返回一个空vector对象
else if(expected==actual)
return {"functionX","okay"}; //返回列表初始化的vector对象
else
return {"functionX",expected,actual};
}

main的返回值:

1
2
3
4
5
6
7
8
//一种写法
int main(){
//...
if(some_failure)
return EXIT_FAILURE; //定义在cstdlib头文件中
else
return EXIT_SUCCESS; //同上
}

17-16.png

17-17.png

函数重载

函数重载:函数名称相同但形参列表不同。

1
2
3
4
5
6
7
8
Record lookup(const Account&);
bool lookup(const Account&); //错误!有第一行的情况下不能这么写
Record lookup(const Phone&);
Record lookup(const Name&);
Account acct;
Phone phone;
Record r1 = lookup(acct);
Record r2 = lookup(phone);

const_cast和重载:

1
2
3
4
5
6
7
8
9
//比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2){
return s1.size()<=s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2){
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}

【实例】 函数重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

void f(){
std::cout << "该函数无须参数" << std::endl;
}

void f(int){
std::cout<< "该函数有一个整型参数" << std::endl;
}

void f(int, int){
std::cout<< "该函数有两个整型参数" << std::endl;
}

void f(double a, double b=3.14){
std::cout << "该函数有两个双精度浮点型参数" << std::endl;
}

int main(){
f(2.56, 42.0);
f(42);
f(42, 0);
f(2.56, 3.14);
return 0;
}

输出结果:

1
2
3
4
5
6
7
Active code page: 65001
PS C:\Users\arrogance> cd "d:\c++code\"
PS D:\c++code> if ($?) { g++ test.cpp -o test } ; if ($?) { .\test }
该函数有两个双精度浮点型参数
该函数有一个整型参数
该函数有两个整型参数
该函数有两个双精度浮点型参数

特殊用途语言特性

默认实参:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

1
2
3
4
5
6
7
8
9
10
11
typedef string::size_type sz;
string screen(sz ht=24, sz wid=80, char backgrnd=' ');

string mwindow;
mwindow = screen(); //等价于screen(24,80,' ')
mwindow = screen(66); //等价于screen(66,80,' ')
mwindow = screen(66,256); //screen(66,256,' ')
mwindow = screen(66,256,'#'); //screen(66,256,'#')

mwindow = screen(,,'?'); //错误!只能省略尾部的实参
mwindow = screen('?'); //会发生隐式转换

constexpr函数:能用于常量表达式的函数,函数的返回类型及所有的形参都是字面值类型。

  • 函数体中必须有且仅有一条return语句
  • constexpr函数被隐式地指定为内联函数
  • constexpr函数并不要求返回常量表达式
1
2
3
4
5
6
7
constexpr int new_sz(){return 42;}
constexpr int foo = new_sz(); //正确,foo是一个常量表达式

constexpr size_t scale(size_t cnt){return new_sz()*cnt;}
int arr[scale(2)]; //正确,scale(2)是常量表达式
int i = 2;
int a2[scale(i)]; //错误!scale(i)不是常量表达式

调试帮助:只在开发过程中使用的代码,发布时屏蔽掉。

assert预处理宏,位于cassert头文件中。

1
2
3
//如果表达式为假,assert输出信息并终止程序的执行
//如果表达式为真,assert什么也不做
assert(word.size()>threshold);

NDEBUG预处理变量:assert的行为依赖NDEBUG预处理变量的状态,如果定义了NDEBUG,则assert无效。

1
2
#define NDEBUG //关闭调试状态,必须在cassert头文件上面
#include <cassert>

除了用于assert外,也可以使用NDEBUG编写自己的调试代码:

1
2
3
4
5
6
7
void print(const int ia[], size_t size){
#ifndef NDEBUG
//__func__是编译器定义的一个局部静态变量,用于存放函数的名字
cerr << __func__ << ": array size is " << size << endl;
#endif
//...
}

17-18.png

函数匹配

这一节看看就好,感觉用处不太。

17-19.png

例子如下:

17-20.png

函数指针

函数指针,指针指向的是函数。

1
2
3
4
5
6
7
8
9
10
11
bool lengthCompare(const string&, const string&);
bool (*pf)(const string&, const string&); //括号不能少

pf = lengthCompare;
pf = &lengthCompare; //与上一行等价,取地址符是可选的
//可以直接使用指向函数的指针调用该函数,无需提前解引用
/*------------ 以下三行语句是等价的 ------------*/
bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye");
bool b3 = lengthCompare("hello","goodbye");
/*--------------------------------------------*/

在指向不同函数类型的指针间不存在转换规则(必须很精准的匹配才可以)。

1
2
3
4
5
6
7
//和上个例子连起来看
string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0; //正确,pf不指向任何函数
pf = sumLength; //错误!返回类型不匹配
pf = cstringCompare; //错误!形参类型不匹配
pf = lengthCompare; //正确,精确匹配

函数指针形参:

不能定义函数类型的形参,但形参可以是指向函数的指针。

1
2
3
4
5
6
7
8
9
//第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2,
bool pf(const string &, const string &));
//等价的声明:显式地将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2,
bool (*pf)(const string &, const string &));

//可以直接把函数作为实参使用,会自动转换成指针
useBigger(s1,s2,lengthCompare);

通过使用类型别名,简化使用函数指针:

1
2
3
4
5
6
7
8
9
10
//Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; //等价的类型
//FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; //等价的类型

//useBigger的等价声明,其中使用了类型别名
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);

【练习6.54】 编写函数的声明,令其接受两个 int 形参并且返回类型也是 int ;然后声明一个 vector 对象,令其元素是指向该函数的指针。

1
2
3
4
//满足题意的函数如下:
int func(int, int);
//满足题意的 vector 对象如下:
vector<decltype(func)* > vF;

返回指向函数的指针:虽然不能返回一个函数,但是可以返回指向函数类型的指针。必须把返回类型写成指针形式,编译器不会自动处理。

1
2
3
4
5
6
using F = int(int*, int); //F是函数类型,不是指针
using PF = int(*)(int*, int); //PF是指针类型

PF f1(int); //正确,PF是指向函数的指针,f1返回指向函数的指针
F f1(int); //错误!F是函数类型,f1不能返回一个函数
F *f1(int); //正确,显式地指定返回类型是指向函数的指针

也可以用下面的形式直接声明f1:

1
int (*f1(int)) (int*, int);

从里往外读:f1(int)是一个函数,这个函数返回的是一个指针(*f1(int)),这个指针指向的是一个函数 (int*, int),这个函数(int*, int)返回的是int型。

使用尾置返回类型的方式:

1
auto f1(int) -> int (*)(int*, int);

使用尾置返回类型的其他例子:

1
2
3
4
// 欲使函数返回数组的引用,该数组包含10个string对象
string (&func())[10];
// 等价于:
auto func() -> string(&) [10];

17-21.png

定义抽象数据类型

考虑如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Sales_data{
std::string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
double avg_price() const;

std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

成员都必须在类的内部声明,但成员函数体可以定义在类内也可以在类外。

常量成员函数:类的成员函数后面加const,表明这个函数不会修改这个类对象的数据成员。

两种效果相同的写法:

1
2
3
//在Sales_data内:
std::string isbn() const {return bookNo;}
std::string isbn() const {return this->bookNo;} //尽管没有必要

关于this的详细解释参考《C++ primer》。

编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。因此成员体可以随意使用类中的其他成员而无需在意这些成员出现的次序。

1
2
3
4
5
6
7
//在类的外部定义成员函数
double Sales_data::avg_price() const{
if(units_sold)
return revenue/units_sold;
else
return 0;
}

定义一个返回this对象的函数:

1
2
3
4
5
6
//模拟复合运算符+=,为了和+=一致,返回为左值,因此需要返回引用
Sales_data& Sales_data::combine(const Sales_data &rhs){ //right hand side
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}

定义类相关的非成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//如果非成员函数是类接口的组成部分,则应该与类在同一个头文件中声明
//IO对象不能拷贝,只能引用。 需要修改IO对象,不能用底层const
istream& read(istream &is, Sales_data &item){
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.uints_sold;
return is;
}
ostream& print(ostream &os, const Sales_data &item){
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs){
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}

构造函数:

  • 构造函数与类名同名,没有返回值,用来初始化类对象的数据成员。
  • 类可以包括多个构造函数。
  • 构造函数不能被声明为const
    • 当我们创建类的一个const对象时,直到构造函数完成初始化,对象才能真正得到“常量”属性

合成的默认构造函数(synthesized default constructor):如果我们的类没有显式地定义构造函数,编译器会为我们隐式地定义一个默认构造函数。对于大多数类来说,这个『合成的默认构造函数』将按照如下规则初始化类的数据成员:

  • 若存在类内的初始值,用它来初始化成员。
  • 否则,默认初始化该成员。

只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//这段代码慢慢读不难懂,详细解释可以参阅《C++ primer》
struct Sales_data{
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);
//之前已有的其他成员
std::string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//...之前的代码略
//在类的外部定义构造函数:
Sales_data::Sales_data(std::istream &is){
read(is, *this);
}

拷贝、赋值和析构:

管理动态内存的类通常不能依赖于编译器合成的版本。使用vectorstring除外。

访问控制与封装

使用访问说明符加强类的封装性:

  • public:类的接口,在整个程序内可以被访问
  • private:封装(即隐藏)类的实现细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//class和struct定义类唯一的区别就是默认的访问权限不同
//struct默认public, class默认private
class Sales_data{
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(std::istream&);
std::string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
private:
double avg_price() const{
return units_sold ? revenue/units_sold : 0;
}
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或函数成为它的友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Sales_data{
// 为Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream& read(std::istream&, Sales_data&);
friend std::ostream& print(std::ostream&, const Sales_data&);
// 其他内容与之前一致
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(std::istream&);
std::string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
private:
double avg_price() const{
return units_sold ? revenue/units_sold : 0;
}
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);

友元声明只能出现在类定义的内部,但具体位置不限。友元不是类的成员,不受访问控制级别的约束。友元的声明仅指定访问的权限,不是通常意义上的函数声明。因此若希望类的用户能调用某个友元函数,最好在友元声明之外再对函数进行一次声明(有些编译器必须声明,有些可以省略。出于移植性的考虑,最好声明一下)。

类的其他特性

定义一个类型成员:

1
2
3
4
5
6
7
8
9
10
//Screen表示显示器中的一个窗口
class Screen{
public:
typedef std::string::size_type pos;
//等价于 using pos = std::string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};

上面这样做的原因是,Screen的用户不需要知道Screen使用了一个string对象来存放它的数据,pos隐藏了细节。

成员函数也支持重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Screen{
public:
typedef std::string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd, char c): height(ht),width(wd),contents(ht*wd,c){}
// string初始化的一种方法:使用单个字符初始化。
// string s(10,'a'); //直接初始化,s的内容是aaaaaaaaaa
char get() const {return contents[cursor];} // 隐式内联
inline char get(pos ht, pos wd) const; // 显式内联
Screen& move(pos r, pos c); // 能在之后被设为内联
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
inline Screen& Screen::move(pos r, pos c){
pos row = r*width;
cursor = row+c;
return *this; //以左值的形式返回对象
}
char Screen::get(pos r, pos c) const{
pos row = r*width;
return contents[row+c];
}

可变数据成员(mutable data member):

1
2
3
4
5
6
7
8
9
10
11
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr; //即使在一个const对象内也能被修改
//其他成员与之前的版本一致,略
};
void Screen::some_member() const{
++access_ctr; //保存一个计数值,用于记录成员函数被调用的次数
//该成员需要完成的其他工作
}

类数据成员的初始值:

1
2
3
4
5
class Window_mgr{
private:
// 这个窗口管理类,管理一组Screen
std::vector<Screen> screens{Screen(24, 80, ' ')};
};

返回*this的成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Screen{
public:
Screen& set(char);
Screen& set(pos, pos, char);
// 其他成员和之前的版本一致
};
inline Screen& Screen::set(char c){
contents[cursor] = c;
return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch){
contents[r*width+col] = ch;
return *this;
}
// 把光标移动到一个指定的位置,然后设置该位置的字符值
myScreen.move(4,0).set('#'); // 神之一手

const成员函数返回*this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Screen{
public:
//根据对象是否是const重载了display函数
Screen& display(std::ostream &os){
do_display(os);
return *this;
}
const Screen& display(std::ostream &os) const{
do_display(os);
return *this;
}
private:
void do_display(std::ostream &os) const{
os << contents;
}
//其他成员与之前的一致
};

类的声明:我们可以仅声明类而暂时不定义它。

1
class Screen;   // Screen类的声明

这种声明也叫前向声明(forward declaration),对于类型Screen来说,它在声明之后定义之前是一个不完全类型(incomplete type),不完全类型只能在有限的情况下使用:

  • 可以定义指向这种类型的指针或引用
  • 可以声明(但不能定义)以不完全类型作为参数或返回类型的函数
1
2
3
4
class Link_screen{
Link_screen *next; //正确
Link_screen *prev; //正确
};

类之间的友元关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Screen{
// Window_mgr的成员可以访问Screen类的私有部分
friend class Window_mgr;
// Screen类的剩余部分,略
};
/* 如果一个类指定了友元类,则友元类的成员函
数可以访问此类包括非公有成员在内的所有成员。*/
class Window_mgr{
public:
// 窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
// 按编号将指定的Screen重置为空白
void clear(ScreenIndex);
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i){
// s是一个Screen的引用,指向我们想清空的那个屏幕
Screen &s = screens[i];
s.contents = string(s.height*s.width, ' ');
}

友元关系不存在传递性。

令成员函数作为友元:

1
2
3
4
5
6
7
8
9
class Screen{
// Window_mgr::clear必须在Screen类之前被声明
friend void Window_mgr::clear(ScreenIndex);
// Screen类的剩余部分,略
};
/*-------------- 顺 序 ---------------*/
//1、定义Window_mgr类,声明clear函数,但不能定义它
//2、定义Screen,包括对于clear的友元声明
//3、定义clear,此时才能使用Screen的成员

友元声明和作用域:参考《C++ Primer》

类的作用域

1
2
3
4
5
Screen::pos ht=24, wd=80; //使用Screen类定义的pos类型
Screen scr(ht, wd, ' '); //创建了一个Screen对象并初始化
Screen *p = &scr;
char c = scr.get(); //访问scr对象的get成员
c = p->get(); //访问所指对象的get成员

对比以下代码:

1
2
3
4
5
6
7
8
9
void Window_mgr::clear/*一旦遇到类名*/(ScreenIndex i){
Screen &s = screens[i];
s.contents = string(s.height*s.width, ' ');
} //直到定义的结束,都是类的作用域之内
/*------------------ 对 比 ---------------------*/
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s){
screens.push_back(s);
return screens.size()-1;
} //首先处理返回类型,之后才进入Window_mgr的作用域

成员定义中的名字查找。以下代码仅作原理展示,不能作为作为写代码的满分参考(:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int height;
class Screen{
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height){
cursor = width*height;
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
//尽管外层的对象被隐藏了,但我们仍可以用作用域运算符访问它
void Screen::dummy_fcn(pos height){
cursor = width * this->height; //成员height
cursor = width * Screen::height; //成员height
cursor = width * ::height; //全局height
}

构造函数再探

有时候初始化列表必不可少。如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始列表提供初始值:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ConstRef{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii){//赋值:
i = ii; //正确
ci = ii; //错误!
ri = i; //错误!ri没有初始化
}

正确做法,显式地初始化引用和const成员:

1
ConstRef::ConstRef(int ii):i(ii), ci(ii), ri(i){}

成员初始化的顺序,构造函数初始值列表中的顺序不会影响实际的初始化顺序:

1
2
3
4
5
6
7
8
9
class X{
int i;
int j;
public:
X(int val):j(val), i(j){} //未定义的:i在j之前被初始化
//尽量使用参数作为初始化值
// X(int val):j(val), i(val){}
//这样就与i和j的初始化顺序无关了
};

默认实参和构造函数:

1
2
3
4
5
6
class Sales_data{
public:
//定义默认构造函数,令其与只接受一个string实参的构造函数功能相同
Sales_data(std::string s = ""):bookNo(s){}
// ...
};

【练习7.38】有些情况下我们希望提供 cin 作为接受 istream& 参数的构造函数的默认实参,请声明这样的构造函数。

1
Sales_data(std::istream& is = std::cin){is >> *this;}

此时该函数具有了默认构造函数的作用,因此我们原来声明的默认构造函数Sales_data()=default;应该去掉,否则会引起调用的二义性。

委托构造函数(把自己的一些或全部职责给了其他构造函数):

1
2
3
4
5
6
7
8
9
10
11
class Sales_data{
public:
//非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price){}
//其余构造函数委托给另一个构造函数
Sales_data():Sales_data("",0,0){}
Sales_data(std::string s):Sales_data(s,0,0){}
Sales_data(std::istream &is):Sales_data(){read(is,*this);}
// ...
};

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。在Sales_data类中,受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。具体参阅配套习题第174页(练习7.41):

17-21dot5.png

默认构造函数的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
class NoDefault{
public:
NoDefault(const std::string&);
//还有其他成员,但没有其他构造函数了
};
struct A{
NoDefault my_mem;
};
A a; //错误!不能为A合成构造函数
struct B{
B(){} //错误!b_member没有初始值
NoDefault b_member;
};

在实际中,如果定义了其他构造函数,最好也提供一个默认构造函数。

隐式的类类型转换:

1
2
3
4
string null_book = "9-999-99999-9";
//构造一个临时的Sales_data对象
//该对象的units_sold和revenue等于0,bookNo等于null_book
item.combine(null_book);

只允许一步类类型转换:

1
2
3
4
item.combine("9-999-99999-9");
//错误!这里试图经历两种转换:
//1. 把“9-999-99999-9”转换成string
//2. 再把这个(临时的)string转换成Sales_data

下面这三种写法是允许的:

1
2
3
item.combine(string("9-999-99999-9"));
item.combine(Sales_data("9-999-99999-9")); //隐式地转换成string,再显式地转换成Sales_data
item.combine(cin);

抑制构造函数定义的隐式转换:explicit(清楚、明白的)

1
2
3
4
5
6
7
8
9
10
11
class Sales_data{
public:
Sales_data()=default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n){}
explicit Sales_data(const std::string &s):bookNo(s){}
explicit Sales_data(std::istream&);
// ...
};
item.combine(null_book); //错误!string构造函数是explicit的
item.combine(cin); //错误!istream构造函数是explicit的

explicit关键字只允许出现在类内的构造函数声明处:

1
2
3
4
// 错误!
explicit Sales_data::Sales_data(istream& is){
read(is, *this);
}

explicit构造函数只能用于直接初始化:

1
2
3
Sales_data item1(null_book); //正确,直接初始化
Sales_data item2 = null_book;
//错误!不能将explicit构造函数用于拷贝形式的初始化过程

为转换显式地使用构造函数:

1
2
3
4
//正确,实参是一个显式构造的Sales_data对象
item.combine(Sales_data(null_book));
//正确,static_cast可以使用explicit的构造函数
item.combine(static_cast<Sales_data>(cin));

聚合类 (aggregate class)

  • 所有成员都是public
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有virtual函数
1
2
3
4
5
struct Data{
int ival;
string s;
};
Data val1 = {0, "Anna"}; //可以使用初始值列表

字面值常量类:(或称“字面值类”)

  • 数据成员都必须是字面值类型
  • 类必须至少含有一个constexpr构造函数
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//一个例子,其具体作用暂时不必关心
class Debug{
public:
constexpr Debug(bool b=true):hw(b), io(b), other(b){}
constexpr Debug(bool h, bool i, bool o):hw(h),io(i),other(o){}
constexpr bool any() {return hw||io||other;}
void set_io(bool b) {io=b;}
void set_hw(bool b) {hw=b;}
void set_other(bool b) {hw=b;} //这里原书可能有误?
private:
bool hw; //硬件错误,而非IO错误
bool io; //IO错误
bool other; //其他错误
};

constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型:

1
2
3
4
5
6
constexpr Debug io_sub(false, true, false); //调试IO
if(io_sub.any()) //等价于if(true)
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); //无调试
if(prod.any()) //等价于if(false)
cerr << "print an error message" << endl;

类的静态成员

与类本身关联,而不需要与每个对象关联。

1
2
3
4
5
6
7
8
9
10
11
12
class Account{
public:
void calculate(){amount+=amount*interestRate;}
static double rate(){return interestRate;}
//static函数不包含this指针,所以不能定义为const函数
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};

静态成员存在于任何对象之外,所有对象共享:

1
2
3
4
5
6
double r;
r = Account::rate();
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2->rate(); //与上一行效果相同

可以在类的内部也可以在类的外部定义静态成员函数。在外部定义时,不能重复static关键字,static关键字只出现在类内部的声明语句中:

1
2
3
void Account::rate(double newRate){
interestRate = newRate;
}

静态数据成员:

17-22.png

想要确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放到同一个文件中。

静态成员的类内初始化。通常,类的静态成员不该在类的内部初始化(因为里面仅仅是一个声明)。以下是特殊情况:

1
2
3
4
5
6
7
8
9
10
11
class Account{
public:
static double rate(){return interestRate;}
static void rate(double);
private:
static constexpr int period = 30; //period是常量表达式,可以用字面值替换
double daily_tbl[period];
};
//如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了:
constexpr int Account::period;
//即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员

静态成员能用于某些场景,而普通成员不能:

1
2
3
4
5
6
7
8
class Bar{
public:
// ...
private:
static Bar mem1; //正确:静态成员可以是不完全类型
Bar *mem2; //正确
Bar mem3; //错误!数据成员必须是完全类型
};

可以使用静态成员作为默认实参,因为它本身不是对象的一部分:

1
2
3
4
5
6
7
class Screen{
public:
//bkground表示一个在类中稍后定义的静态成员
Screen& clear(char = bkground);
private:
static const char bkground;
};

Sales_data 综合(实例)

目前,我个人更倾向于char* p;的写法。

关于这之中可能的争议:

https://stackoverflow.com/questions/6990726/correct-way-of-declaring-pointer-variables-in-c-c

注意:以下程序尚未经过仔细的测试。(进一步的测试请转到【练习8.7】)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
#include <iostream>
class Sales_data{
friend Sales_data add(const Sales_data& lhs, const Sales_data& rhs);
friend std::istream& read(std::istream& is, Sales_data& item);
friend std::ostream& print(std::ostream& os, const Sales_data& item);
public:
Sales_data() = default;
Sales_data(const std::string& s):bookNo(s){}
Sales_data(const std::string& s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){}
Sales_data(std::istream& is);

std::string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data& item);

private:
double avg_price() const {return units_sold ? revenue/units_sold : 0;}

std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

// 非成员接口函数:友元函数
Sales_data add(const Sales_data& lhs, const Sales_data& rhs);
std::istream& read(std::istream& is, Sales_data& item);
std::ostream& print(std::ostream& os, const Sales_data& item);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Sales_data_Implementation.cpp
#include "Sales_data.h"

Sales_data::Sales_data(std::istream& is){
read(is,*this);
}

Sales_data& Sales_data::combine(const Sales_data& item){
units_sold += item.units_sold;
revenue += item.revenue;
return *this;
}

Sales_data add(const Sales_data& lhs, const Sales_data& rhs){
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}

std::istream& read(std::istream& is, Sales_data& item){
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}

std::ostream& print(std::ostream& os, const Sales_data& item){
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price()<<std::endl;
return os;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Sales_data_Main.cpp
#include "Sales_data.h"

using namespace std;
int main()
{
string name = "BOOKONE";
Sales_data book1(name,18,2.6);
Sales_data book2(cin);
print(cout,book1);
print(cout,book2);
system("pause");
}
/*
输入样例:
BOOKTWO
10
5.5
期望输出:
BOOKONE 18 46.8 2.6
BOOKTWO 10 55 5.5
*/

IO类

17-23.png

关于宽字符:参见《C++ primer》278 页。

类型ifstreamistringstream都继承自istream。因此,可以像使用istream对象一样来使用ifstreamistringstream对象。例如,可以对ifstreamistringstream对象调用getline,也可以用>>从一个ifstreamistringstream对象中读取数据。类似的,类型ofstreamostringstream都继承自ostream

IO对象无拷贝或赋值:

1
2
3
4
ofstream out1, out2;
out1 = out2; //错误!不能对流对象赋值
ofstream print(ofstream); //错误!不能将形参或返回类型设为流类型
out2 = print(out2); //错误!不能拷贝流对象

读写一个 IO 对象会改变其状态,因此传递和返回的引用不能是const的。

17-24.png

查询流的状态:

IO 库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用。IO 库定义了 4 个iostate类型的constexpr值,表示特定的位模式。这些值用来表示特定类型的 IO 条件,可以与位运算符一起使用来一次性检测或设置多个标志位。

1
2
3
4
5
// 不同机器里面可能不一样 (?)
goodbit = 0x0
eofbit = 0x1
failbit = 0x2
badbit = 0x4

badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。如果到达文件结束位置,eofbitfailbit都会被置位。goodbit的值为 0 ,表示流未发生错误。如果badbitfailbiteofbit任一个被置位,则检测流状态的条件会失败。

标准库还定义了一组函数来查询这些标志位的状态。操作good在所有错误位均未置位的情况下返回 true,而badfaileof则在对应错误位被置位时返回 true。此外,在badbit被置位时,fail也会返回true 。这意味着,使用goodfail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而eofbad操作只能表示特定的错误。

管理条件状态:

流对象的rdstate成员返回一个iostate值,对应流的当前状态。setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。

clear不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回 true 。我们可以这样使用这些成员:

1
2
3
4
auto old_state = cin.rdstate(); //记住 cin 的当前状态
cin.clear(); //使 cin 有效
process_input(cin); //使用 cin
cin.setstate(old_state); //将 cin 置为原始状态

带参数的clear版本接受一个iostate值:

1
2
3
4
5
6
7
8
9
//复位 failbit 和 badbit ,保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
/*-------------位运算,比较好理解:----------
这里 0 代表无错,1 有错。failbit 中的 1 取反后
变成 0,任何一个数(0/1) & 0 都变成 0,完成置位。
同时 failbit 中的 0 取反后变成 1,任何一个数(0/1)
& 1 都不变。
badbit 同理。
------------------------------------------*/

【练习8.1】编写函数,接受一个 istream& 参数,返回值类型也是 istream& 。此函数须从给定流中读取数据,直到遇到 eof 停止。它将读取的数据打印在标准输出上。完成这些操作后,在返回流之前,对流进行复位,使其处于有效状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <stdexcept>
using namespace std;

istream& f(istream& in){
int v;
while(in>>v, !in.eof()){
if(in.bad()) throw runtime_error("io-stream error");
if(in.fail()){
cerr << "Data error, please try again: " << endl;
in.clear();
in.ignore(100, '\n');
continue;
}
cout << v << endl;
}
in.clear();
return in;
}

int main(){
cout << "Please enter some integers and press Ctrl+Z to end: " << endl;
f(cin);
return 0;
}
//问题(未解决):某些配置无法处理中文,甚至会导致程序的错误

关于上例代码的一些解释(注意:整理自网络,不能严格保证其正确性)

  • 在函数f中,in.ignore(100, '\n')的作用是忽略输入流中的一些字符,直到遇到换行符为止,或者忽略了 100 个字符。这里的换行符'\n'是因为在输入整数时,用户可能会在输入后按下回车键,导致换行符被输入流中。通过忽略换行符,可以清除输入流中的垃圾数据,使得下一个整数输入操作能够得到正确的输入。忽略字符的操作是在输入流中移动指针,使得下一次读取操作可以从正确的位置开始。
  • 需要注意的是,如果忽略了指定的最大数量 n 仍然没有遇到终止字符 c,则会设置输入流的failbit标志,表示输入流状态错误。
  • 它的一个常用功能就是用来清除以回车结束的输入缓冲区的内容,消除上一次输入对下一次输入的影响。例如,cin.ignore(1024, '\n'),通常把第一个参数设置得足够大,这样是为了只有第二个参数 ‘\n’ 起作用。所以这一句就是把回车(包括回车)之前的所有字符从输入缓冲流中清除出去。
  • 如果默认不给参数的话,默认参数为cin.ignore(1, EOF),即把EOF前的1个字符清掉,没有遇到EOF就清掉一个字符然后结束。
  • in.clear()成员函数用于清除输入流的错误标志,但是它并不会清除输入流中的垃圾数据。

17-25.png

管理输出缓冲:

17-26.png

刷新输出缓冲区:

1
2
3
cout << "hi!" << endl;  //输出 hi! 和一个换行,然后刷新缓冲区
cout << "hi!" << flush; //输出 hi! ,然后刷新缓冲区
cout << "hi!" << ends; //输出 hi! 和一个空字符,然后刷新缓冲区

unitbuf操纵符:

1
2
cout << unitbuf;    //所有输出操作后都会立即刷新缓冲区
cout << nounitbuf; //回到正常的缓冲方式

警告:如果程序崩溃,输出缓冲区不会被刷新。

关联输入和输出流:

1
cin >> ival;   //会导致 cout 的缓冲区被刷新

tie有两个重载的版本:一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o

既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream

1
2
3
4
5
6
cin.tie(&cout);  //仅作展示:标准库将cin和cout关联在一起
// old_tie指向当前关联到cin的流(如果有的话)
ostream* old_tie = cin.tie(nullptr); // cin 不再与其他流关联
// 将cin和cerr关联:not a good idea,仅作展示
cin.tie(&cerr); //读取cin会刷新cerr,而不是cout
cin.tie(old_tie); //重建cin和cout的正常关联

每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream

文件输入输出

17-27.png

使用文件流对象:

1
2
ifstream in(ifile); //构造一个ifstream并打开给定文件
ofstream out; //输出文件流未关联到任何文件

在新 C++ 标准中,文件名既可以是库类型 string 对象,也可以是 C 风格字符数组。旧版本的标准库只允许 C 风格字符数组。

fstream代替iostream&:在要求使用基类型对象的地方,可以用继承类型的对象来替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//本例中,假定输入和输出文件的名字是通过传给main的参数来指定的
ifstream input(argv[1]);
ofstream output(argv[2]);
Sales_data total;
if(read(input, total)){ //读取第一条销售记录
Sales_data trans; //保存下一条销售记录的变量
while(read(input, trans)){
if(total.isbn()==trans.isbn())
total.combine(trans);
else{
print(output, total) << endl;
total = trans;
}
}
print(output, total) << endl;
}
else
cerr << "No data ?!" << endl;

上面的代码中,重要的是对readprint的调用。虽然两个函数定义时指定的形参分别是istream&ostream&,但我们可以向它们传递fstream对象。

成员函数openclose

1
2
3
4
5
6
7
8
ifstream in(ifile); //构造一个ifstream并打开给定文件
ofstream out; //输出文件流未关联到任何文件
out.open(ifile + ".copy"); //打开指定文件
if(out){ //检查open是否成功
/*---*/
}
in.close(); //关闭文件
in.open(ifile + "2"); //打开另一个文件

如果调用open失败,failbit会被置位。如果open成功,流的状态good()会为true

自动构造和析构:

1
2
3
4
5
6
7
8
9
//main接受一个要处理的文件列表
for(auto p = argv+1; p != argv+argc; p++){
ifstream input(*p);
if(input){ //如果文件打开成功,“处理”此文件
process(input);
}
else
cerr << "couldn't open: " + string(*p);
} //每个循环步input都会离开作用域,因此会被销毁

当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。 即:当一个fstream对象被销毁时,close会自动被调用。

【练习8.4】 编写函数,以读模式打开一个文件,将其内容读入到一个string的vector中,将每一行作为一个独立的元素存于vector中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// main.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
using namespace std;

int main(){ // 路径中最好不要有中文。
ifstream in("D:\\c++code\\exercise8-4\\data.txt");
if(!in){
cerr << "couldn't open file: data.txt" << endl;
return -1;
}

string line;
vector<string> words;
while(getline(in, line)){
words.push_back(line);
}

in.close();

auto it = words.begin();
while(it!=words.end()){
cout << *it <<endl;
++it;
}

return 0;
}
1
2
3
4
5
6
7
//data.txt
tianshangdetaiyang
yueliangyigeyang
yiduoyejuhua
zhengpiaoliang
nidemoyang
songniyiduoshan/san chahua
1
2
3
4
5
6
7
//输出结果
tianshangdetaiyang
yueliangyigeyang
yiduoyejuhua
zhengpiaoliang
nidemoyang
songniyiduoshan/san chahua

【练习8.5】重写上面的程序,将每个单词作为一个独立的元素进行存储。

【解答】将while(getline(in, line))改为while(in >> line)即可。

文件模式:

17-28.png

17-29.png

out模式打开文件会丢弃已有数据:

1
2
3
4
5
6
7
//在这几条语句中,file1都被截断
ofstream out("file1"); //隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out); //隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
//为了保留文件内容,我们必须显式指定app模式
ofstream app("file2", ofstream::app); //隐含为输出模式
ofstream app2("file2", ofstream::out | ofstream::app);

每次调用open时都会确定文件模式:

1
2
3
4
5
ofstream out; //未指定文件打开模式
out.open("scratchpad"); //模式隐含设置为输出和截断
out.close();
out.open("previous", ofstream::app); //模式为输出和追加
out.close();

【练习8.7】修改上一节的书店程序,将结果保存到一个文件中。将输出文件名作为第二个参数传递给 main 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 8_7main.cpp
#include <iostream>
#include <fstream>
#include "Sales_data.h"

using namespace std;

int main(int argc, char* argv[]){
if(argc != 3){
cerr << "Please give the input and output file names" << endl;
return -1;
}
ifstream in(argv[1]);
if(!in){
cerr << "Unable to open the input file" << endl;
return -1;
}
ofstream out(argv[2]);
if(!out){
cerr << "Unable to open the output file" << endl;
return -1;
}

Sales_data total;
if(read(in, total)){ //读取第一条销售记录
Sales_data trans; //保存下一条销售记录的变量
while(read(in, trans)){
if(total.isbn()==trans.isbn())
total.combine(trans);
else{
print(out, total) << endl;
total = trans;
}
}
print(out, total) << endl;
}
else
cerr << "No data" << endl;

return 0;
}

文件 8_7file_in.txt 内容如下:

1
2
3
4
5
6
war&peace 2 8.8
war&peace 1 9
taoteching 3 3.3
taoteching 6 9
Pride&Prejudice 2 89.64
hewhochangedchina 1926 0.817

创建 8_7file_out.txt 文件,初始为空。此外还有之前提到的文件 Sales_data.h 和 Sales_data_Implementation.cpp ,将它们放至同一个文件夹中。

打开 PowerShell ,更改路径(以博主的机器为例):

1
cd D:\c++code\exercise8-7

键入以下命令执行分离式编译:

1
g++ 8_7main.cpp Sales_data_Implementation.cpp -o 8_7prog

键入命令:

1
.\8_7prog.exe 8_7file_in.txt 8_7file_out.txt

17-30.png

打开 8_7file_out.txt ,发现输出如下:

1
2
3
4
5
6
7
8
9
war&peace 3 26.6 8.86667

taoteching 9 63.9 7.1

Pride&Prejudice 2 179.28 89.64

hewhochangedchina 1926 1573.54 0.817


如果键入:

1
.\8_7prog.exe 8_7file_in.txt 8_7file_out.txt hana.txt

则 powershell 会显示:

1
Please give the input and output file names

string 流

17-31.png

使用istringstream

考虑这样的例子:有一个文件,列出了一些人名和他们的电话号码。某些人只有一个号码,而另一些则有多个。输入文件格式如下:

1
2
3
morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct PersonInfo{
string name;
vector<string> phones;
};
// --------------------
string line, word;
vector<PersonInfo> people;
while(getline(cin, line)){
PersonInfo info;
istringstream record(line);
record >> info.name;
while(record >> word)
info.phones.push_back(word);
people.push_back(info);
}

【练习8.9】 使用 练习8.1 中编写的函数打印一个istringstream对象的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>

using namespace std;

istream& f(istream& in){
string v;
while(in>>v, !in.eof()){
if(in.bad())
throw runtime_error("io-stream error");
if(in.fail()){
cerr << "Data error, please try again: " << endl;
in.clear();
in.ignore(1000, '\n');
continue;
}
cout << v << endl;
}
in.clear();
return in;
}

int main(){
ostringstream msg;
msg << "C++ Primer 5th Edition" << endl;
istringstream in(msg.str());
f(in);
return 0;
}

输出结果:

1
2
3
4
5
C++
Primer
5th
Edition

重复使用字符串流时,每次都用调用clear

1
record.clear(); // record 是 istringstream 对象

使用ostringstream

考虑情景:我们需要验证并改变电话号码的格式。对于无效的电话号码,需要打印错误信息。

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ostringstream os;
for(const auto &entry : people){ //对people中的每一项
ostringstream formatted, badNums;
for(const auto &nums : entry.phones){ //对每个数
if(!valid(nums)){
badNums << " " << nums;
}
else
formatted << " " << format(nums);
}
if(badNums.str().empty())
os << entry.name << " "
<< formatted.str() << endl;
else
cerr << "input error: " << entry.name
<< " invalid number(s) " << badNums.str() << endl;
}
cout << os.str() << endl;

顺序容器概述

17-32.png

forward_listarray是新 C++ 标准增加的类型。

forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。

NOTE : 新标准库的容器比旧版本快得多。现代 C++ 程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。

17-33.png

容器库概览

一种合法的写法:

1
vector<vector<string>> lines; // vector 的 vector

较旧的编译器可能需要这样写:

1
vector<vector<string> > lines;

虽然可以在容器中保存几乎任何类型,但某些容器操作对元素类型有自己的特殊要求。我们可以定义某类容器(即便它的类型不支持特定操作),但这种情况下,就只能使用那些无特殊要求的容器操作。

例如,顺序容器构造函数的一个版本接受容器大小参数,它使用了元素类型的默认构造函数。但某些类没有默认构造函数。

1
2
3
// 假定 noDefault 是一个没有默认构造函数的类型
vector<noDefault> v1(10, init); //正确。提供了元素初始化器
vector<noDefault> v2(10); //错误!必须提供一个元素初始化器

一个更直观的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>

class A{
public:
A(std::string b){a = b;}
std::string getStr() const {return a;}
private:
std::string a;
};

int main(){
A aObj("hello");
std::vector<A> objA(5, aObj);
auto it = objA.cbegin();
for(; it!=objA.cend(); ++it){
std::cout << (*it).getStr() << std::endl;
}
return 0;
}
/*------- 输出结果 --------
hello
hello
hello
hello
hello
-------------------------*/

上面的代码中,若第 13 行改为std::string aObj("hello");也是可以的,这里存在隐式转换。

若第 14 行写成std::vector<A> objA(5);,就会报错。

17-34.png

forward_list迭代器不支持递减运算符--

迭代器范围(iterator range): [begin, end)

【练习9.5】 题目描述没什么看头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>

using namespace std;
using ivecit = vector<int>::iterator;

ivecit search_vec(ivecit beg, ivecit end, int val){
for(; beg!=end; beg++){
if(*beg==val) return beg;
}
return end;
}

int main(){
vector<int> ilist = {1, 2, 3, 4 , 5, 6, 7};
cout << search_vec(ilist.begin(), ilist.end(), 3)-ilist.begin() << endl;
cout << search_vec(ilist.begin(), ilist.end(), 8)-ilist.begin() << endl;
return 0;
}

输出结果:

1
2
3
2
7

通过类型别名,可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用referenceconst_reference

为了使用这些类型,必须显式地使用其类名:

1
2
list<string>::iterator iter;
vector<int>::difference_type count;

beginend

1
2
3
4
5
6
7
list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin();
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin();
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
auto it7 = a.begin(); // 仅当 a 是 const 时,it7 是 const_iterator
auto it8 = a.cbegin(); // it8 是 const_iterator

容器定义和初始化:

17-35.png

一个容器初始化为另一个容器的拷贝时,容器类型和元素类型必须相同。不过,当传迭代器参数来拷贝一个范围时(该方法不适用于array),无此要求。

1
2
3
4
5
6
7
8
9
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

list<string> list2(authors); //正确。类型匹配
deque<string> authList(authors); //错误!容器类型不匹配
vector<string> words(articles); //错误!
//正确。可以将const char* 转换为 string
forward_list<string> words(articles.begin(), articles.end());
deque<string> authList(authors.begin(), it); //it是一个迭代器,指向authors的一个元素

与顺序容器大小相关的构造函数:

1
2
3
4
vector<int> ivec(10, -1); //10个int元素,每个都初始化为-1
list<string> slis(10, "hi!"); //10个string,每个都初始化为 "hi!"
forward_list<int> ifli(10); //10个元素,每个都初始化为0
deque<string> sdeq(10); //10个元素,每个都是空string

如果元素类型是内置类型或是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。

只有顺序容器的构造函数才接受大小参数,关联容器并不支持。

标准库 array 具有固定大小:

1
2
3
4
5
6
array<int,5> ia1; //5个默认初始化的 int
array<int,5> ia2 = {0,1,2,3,4};
array<int,5> ia3 = {42}; //ia3[0]为42,剩余元素为0
//内置数组类型不能进行拷贝,或对象赋值操作。但array无此限制
array<int,5> digits = {0,1,2,3,4};
array<int,5> copy = digits; //正确。只要数组类型匹配即合法

array 允许赋值:

1
2
3
4
array<int,10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int,10> a2 = {0}; //所有元素值均为 0
a1 = a2; //替换a1中的元素
a2 = {0}; //错误!不能将一个花括号列表赋予数组

17-36.png

使用assign(仅顺序容器):

1
2
3
4
5
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; //错误!
names.assign(oldstyle.cbegin(), oldstyle.cend());
//正确。可以将 const char* 转换为 string

assign的第二个版本:

1
2
3
4
//等价于 slist1.clear();
//后跟 slist1.insert(slist1.begin(), 10, "Hiya!");
list<string> slist1(1); //1个元素,为空string
slist1.assign(10, "Hiya!"); //10个元素,每个都是 "Hiya!"

使用swap

1
2
3
vector<string> svec1(10);
vector<string> svec2(24);
swap(svec1, svec2); //调用完后svec1包含24个string元素

除 array 外,swap 不对任何元素进行拷贝、删除、插入操作,因此是常数时间开销。

元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如,假定iterswap之前指向svec1[3]string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。

与其他容器不同,swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。

因此,对于array,在swap操作之后,指针引用和迭代器所绑定的元素保持不变但元素值已经与另一个array中对应元素的值进行了交换。

在新标准库中,容器既提供成员函数版本的swap,也提供非成员版本的swap。而早期标准库版本只提供成员函数版本的swap非成员版本的swap在泛型编程中是非常重要的。统一使用非成员版本的swap是一个好习惯。

关系运算符:

1
2
3
4
5
6
7
8
vector<int> v1 = {1,3,5,7,9,12};
vector<int> v2 = {1,3,9};
vector<int> v3 = {1,3,5,7};
vector<int> v4 = {1,3,5,7,9,12};
v1 < v2 //true
v1 < v3 //false
v1 == v4 //true
v1 == v2 //false

比较两个容器实际上是进行元素的逐对比较。这些运算符的工作方式与string的关系运算类似:

  • 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
  • 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
  • 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。

只有当容器的元素类型也定义了相应的比较运算符时,才可以用关系运算符比较两个容器:

1
2
3
vector<Sales_data> storeA, storeB;
/*------ some code ------*/
if(storeA < storeB){/*---*/} //错误!Sales_data没有定义<运算符

顺序容器操作

17-37.png

由于 string 是一个字符容器,我们也可以用push_back在 string 末尾添加字符:

1
2
3
4
void pluralize(size_t cnt, string& word){
if(cnt>1)
word.push_back('s'); //等价于 word += 's';
}

容器元素是拷贝。

listforward_listdeque容器支持将元素插到容器头部:

1
2
3
4
list<int> ilist;
for(size_t ix = 0; ix != 4; ++ix)
ilist.push_front(ix);
//执行完毕后,ilist保存序列 3、2、1、0

insert成员提供了更一般的功能:

1
2
// insert 函数将元素插入到迭代器所指定的位置之前
slist.insert(iter, "Hello!"); //将"Hello!"添加到iter之前的位置
1
2
3
4
5
6
7
8
vector<string> svec;
list<string> slist;

//等价于调用 slist.push_front("Hello!");
slist.insert(slist.begin(), "Hello!");

//vector不支持push_front,但可以插入到begin()之前
svec.insert(svec.begin(), "Hello!");

插入范围元素:

1
2
3
4
5
6
7
8
9
10
11
//将10个元素插入到svec的末尾,并将所有元素都初始化为"Anna"
svec.insert(svec.end(), 10, "Anna");

//接受一对迭代器,或一个初始化列表
vector<string> v = {"quasi", "simba", "frollo", "scar"};
//将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(), v.end()-2, v.end()); //插入元素会保持v中原有顺序
slist.insert(slist.end(), {"these", "words", "will", "go", "at", "the", "end"});

//运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end()); //错误!

使用insert返回值:

1
2
3
4
5
6
//C++11,insert返回新加入元素的迭代器,如果不插入任何元素,返回第一个参数
list<string> lst;
auto iter = lst.begin();
while(cin>>word){
iter = lst.insert(iter, word); //等价于调用 push_front
}

使用emplace操作:

1
2
3
4
5
6
7
8
9
//在c的末尾构造一个Sales_data对象
c.emplace_back("101-1-1", 24, 15.99);

c.push_back("101-1-1", 24, 15.99); //错误!
c.push_back(Sales_data("101-1-1", 24, 15.99)); //正确

c.emplace_back(); //使用Sales_data的默认构造函数
c.emplace(iter, "101-1-1"); //使用Sales_data(string)
c.emplace_front("101-1-1", 24, 15.99);

【练习9.22】 一个有点奇怪的题目,闲得慌可以看看。

访问元素:

17-38.png

访问成员函数返回的是引用:

1
2
3
4
5
6
7
if(!c.empty()){
c.front() = 42; //改变了
auto &v = c.back();
v = 1024; //改变了c中的元素
auto v2 = c.back(); //v2不是一个引用,它是c.back()的一个拷贝
v2 = 0; //未改变c中的元素
}

在上面的代码中,使用 auto 变量保存这些函数的返回值,如果希望使用此变量改变元素的值,应定义为引用类型。

17-39.png

从容器内部删除元素:

1
2
//调用后,elem1指向原先elem2所指向的位置
elem1 = slist.erase(elem1, elem2);

特殊的forward_list操作:

关于这些操作的实现细节,请参阅《C++ Primer》第313页。

17-40

1
2
3
4
5
6
7
8
9
10
11
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin();
auto curr = flst.begin();
while(curr != flst.end()){
if(*curr%2)
curr = flst.erase_after(prev);
else{
prev = curr;
curr++;
}
}

【练习9.28】编写函数,接受一个forward_list<string>和两个string共三个参数。函数应在链表中查找第一个string,并将第二个string插入到紧接着第一个string之后的位置。若第一个string未在链表中,则将第二个string插入到链表末尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <forward_list>

using namespace std;

void test_and_insert(forward_list<string>& sflst, const string& s1, const string& s2){
auto prev = sflst.before_begin();
auto curr = sflst.begin();
bool inserted = false;
while(curr!=sflst.end()){
if(*curr==s1){
curr = sflst.insert_after(curr, s2);
inserted = true;
}
prev = curr;
curr++;
}
if(!inserted)
sflst.insert_after(prev, s2);
}

int main(){
forward_list<string> sflst = {"Hello", "!", "world", "!"};
test_and_insert(sflst, "Hello", "Sucrose");

for(auto curr=sflst.cbegin(); curr!=sflst.cend(); curr++){
cout << *curr << " ";
}
cout << endl;

return 0;
}
//输出:
//Hello Sucrose ! world !

改变容器大小:

1
2
3
4
list<int> ilist(10, 42); //10个int,每个值都是42
ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
ilist.resize(25, -1); //将10个值为-1的元素添加到ilist的末尾
ilist.resize(5); //从ilist末尾删除20个元素

17-41.png

容器操作可能使迭代器失效。 这在不同情况下会有不同的表现,如果你是一个纠结于此类无聊问题的人,请翻阅《C++ primer(第五版)》第315页。

不要保存 end 返回的迭代器。 添加或删除元素的循环程序必须反复调用 end ,而不能在循环之前保存 end 返回的选代器,一直当作容器末尾使用。通常 C++ 标准库的实现中 end() 操作都很快,部分就是因为这个原因。

vector对象是如何增长的

vector 的底层其实仍然是定长数组,它能够实现动态扩容的原因是增加了避免数量溢出的操作。首先需要指明的是 vector 中元素的数量(长度)n 与它已分配内存最多能包含元素的数量(容量)N 是不一致的,vector 会分开存储这两个量。当向 vector 中添加元素时,如发现 n>N,那么容器会分配一个尺寸为 2N 的数组,然后将旧数据从原本的位置拷贝到新的数组中,再将原来的内存释放。尽管这个操作的渐进复杂度是 O(n),但是可以证明其均摊复杂度为 O(1),而在末尾删除元素和访问元素则都仍然是 O(1) 的开销。 因此,只要对 vector 的尺寸估计得当并善用resize()reserve(),就能使得 vector 的效率与定长数组不会有太大差距。

17-42.png

reserve并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间。

只有当需要的内存空间超过当前容量时,reserve调用才会改变 vector 的容量。如果需求大小大于当前容量,reserve至少分配与需求一样大的内存空间(可能更大)。
如果需求大小小于或等于当前容量,reserve什么也不做。特别是,当需求大小小于当前容量时,容器不会退回内存空间。因此,在调用reserve之后,capacity将会大于或等于传递给reserve的参数。
这样,调用reserve永远也不会减少容器占用的内存空间。类似的,resize成员函数只改变容器中元素的数目,而不是容器的容量。我们同样不能使用resize来减少容器预留的内存空间。
在新标准库中,我们可以调用shrink_to_fit来要求deque、vector或string退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_to_fit也并不保证一定退回内存空间。

capacitysize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
vector<int> ivec;
//size应该为0,capacity的值依赖于具体实现
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

//向ivec添加24个元素
for(vector<int>::size_type ix = 0; ix!=24; ix++)
ivec.push_back(ix);

//size应该为24,capacity应该大于等于24,具体值依赖于标准库实现
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

/*-------- possible output ----------
ivec: size: 0 capacity: 0
ivec: size: 24 capacity: 32
------------------------------------*/

//预分配一些额外空间
ivec.reserve(50); //将capacity至少设定为50,可能会更大
//size应该为24,capacity应该大于等于50,具体值依赖于标准库实现
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

/*-------- possible output ----------
ivec: size: 24 capacity: 50
------------------------------------*/

//接下来可以用光这些预留空间
while(ivec.size()!=ivec.capacity())
ivec.push_back(0);
//capacity应该未改变
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

/*-------- possible output ----------
ivec: size: 50 capacity: 50
------------------------------------*/

ivec.push_back(42); //再添加一个元素
//size应该为51,capacity应该大于等于51,具体值依赖于标准库实现
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

/*-------- possible output ----------
ivec: size: 51 capacity: 100
------------------------------------*/

ivec.shrink_to_fit(); //要求归还内存
//size应该未改变,capacity的值依赖于具体实现
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;
//调用 shrink_to_fit() 只是一个请求,标准库并不保证退还内存

只有在执行insert操作时sizecapacity相等,或者调用resizereserve时给定的大小超过当前capacity,vector 才可能重新分配内存空间。会分配多少超过给定容量的额外空间,取决于具体实现。

额外的string操作

17-43.png

17-44.png

substr 操作:

17-45.png

1
2
3
4
5
string s("hello world");
string s2 = s.substr(0, 5); // s2 = "hello"
string s3 = s.substr(6); // s3 = "world"
string s4 = s.substr(6, 11); // s4 = "world"
string s5 = s.substr(12); //抛出一个 out_of_range 异常

【练习9.41】编写程序,从一个vector<char>初始化一个string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//vector提供了data成员函数,返回其内存空间的首地址。
#include <string>
#include <vector>
#include <iostream>

int main(){
std::vector<char> chvec = {'h','e','l','l','o'};
std::string s1(chvec.data(), chvec.size());
std::cout << s1 << std::endl;
return 0;
}
/*---- output -----
hello
------------------*/

【练习9.42】假定你希望每次读取一个字符存入一个 string 中,而且知道最少需要读取 100 个字符,如何提高程序性能?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>
#include <string>

using namespace std;

void input_string(string& s){
s.reserve(100);
char c;
while(cin>>c){
s.push_back(c);
}
}

int main(){
string s;
input_string(s);
cout << s << endl;
return 0;
}
/*--------------------------------------
asdfa dfhdfh easdf;d>
dfaf
asdgasgas
^Z
asdfadfhdfheasdf;d>dfafasdgasgas
--------------------------------------*/

除了接受迭代器的inserterase版本外,string 还提供了接受下标的版本:

1
2
s.insert(s.size(), 5, '!'); //在s末尾插入5个感叹号
s.erase(s.size()-5, 5); //从s删除最后5个字符

还提供了接受 C 风格字符数组的insertassign版本:

1
2
3
const char* cp = "Stately, plump Buck";
s.assign(cp, 7); // s == "Stately"
s.insert(s.size(), cp+7); // s == "Stately, plump Buck"

我们也可以指定来自其他 string 或子字符串的字符插入到当前 string 中:

1
2
3
4
string s = "some string", s2 = "some other string";
s.insert(0, s2); //在s中位置0之前插入s2的拷贝
//在s[0]之前插入s2中s2[0]开始的s2.size()个字符
s.insert(0, s2, 0, s2.size());

appendreplace函数:

1
2
3
4
5
6
7
8
9
10
11
12
//append 操作是在 string 末尾进行插入操作的一种简写形式
string s("C++ Primer"), s2 = s;
s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed."
s2.append(" 4th Ed."); // 等价方法,s == s2

//replace 操作是调用 erase 和 insert 的一种简写形式
//将 "4th" 替换为 "5th" 的等价方法
s.erase(11, 3); // s == "C++ Primer Ed."
s.insert(11, "5th"); // s == "C++ Primer 5th Ed."
//从位置 11 开始,删除3个字符并插入 "5th"
s2.replace(11, 3, "5th"); //等价方法:s==s2
//s.replace(11, 3, "Fifth"); 也可以,长度无需一样

17-46.png

【练习9.43 & 练习9.44】如果你真的觉得题目描述有看头的话,就翻书看吧。

博主的代码,使用 KMP 算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <string>
#include <vector>
#include <stack>

void getNext(std::vector<int>& mynext, std::string& t){
int j = 0;
mynext.push_back(0);
for(int i=1; i<t.size(); i++){
while(j>0 && t[i]!=t[j]) j=mynext[j-1];
if(t[i]==t[j]) j++;
mynext.push_back(j);
}
}

std::stack<int> kmpfind(std::string& s, std::string& t){
std::vector<int> mynext;
std::stack<int> tmpans;
getNext(mynext, t);
int j = 0;
for (int i = 0; i < s.size(); i++) {
while(j > 0 && s[i] != t[j]) {
j = mynext[j-1];
}
if(s[i]==t[j]) j++;
if (j==t.size()) {
tmpans.push(i-t.size()+1);
j = mynext[j-1];
}
}
return tmpans;
}

std::string convert2sth(std::string& s, std::string& oldVal, std::string& newVal){
std::stack<int> ayaka = kmpfind(s, oldVal);
while(!ayaka.empty()){
s.replace(ayaka.top(), oldVal.size(), newVal);
ayaka.pop();
}
return s;
}

int main(){
std::string s, oldVal, newVal;
std::cin >> s >> oldVal >> newVal;
std::cout << convert2sth(s, oldVal, newVal);
return 0;
}
/*
* 测试样例:前三行是输入,末行是输出
* * * * * * * * * * * * * * * * *
* ayaka_is_my_waifu_.ayakaayaka_ayakayaka_ayaka123ayaka
* ayaka
* xiangling
* xiangling_is_my_waifu_.xianglingxiangling_xianglingiangling_xiangling123xiangling
* * * * * * * * * * * * * * * * *
* 虽然没有经过仔细的测试,但这个样例真的很令人信服/幸福 (* /ω\*)
* 最后我不知道这样的代码好不好,但肯定没有我的算法板子跑的快
*/

当然,我的这么一通操作到底是不是画蛇添足,不得而知。相关的讨论:C++string中find函数是用什么算法实现的?他的时间复杂度如何?实际比手写KMP效率相比如何?

书中给出的参考答案:

1
2
3
4
5
6
7
8
9
// 前略
void replace_string(string& s, const string& oldVal, const string& newVal){
int p = 0;
while((p=s.find(oldVal, p))!=string::npos){
s.replace(p, oldVal.size(), newVal);
p += newVal.size();
}
}
// 后略

【练习9.45 & 练习9.46】 题干没什么可看的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
void name_string(string& name, const string& prefix, const string& suffix){
name.insert(name.begin(), 1, ' ');
name.insert(name.begin(), prefix.begin(), prefix.end());
name.append(" ");
name.append(suffix.begin(), suffix.end());
}
// ...
// ===================================== //
// ...
void name_string(string& name, const string& prefix, const string& suffix){
name.insert(0, " ");
name.insert(0, prefix);
name.insert(name.size(), " ");
name.insert(name.size(), suffix);
}
// ...

17-47.png

string 搜索函数返回一个string::size_type值,该类型是一个unsigned类型。

1
2
3
4
5
6
7
8
9
10
string name("AnnaBelle");
auto pos1 = name.find("Anna"); // pos1 == 0

string numbers("0123456789"), name("r2d2");
//返回1,即,name 中第一个数字的下标
auto pos = name.find_first_of(numbers);

string dept("03714p3");
//返回5,字符 'p' 的下标
auto pos = dept.find_first_not_of(numbers);

查找一整个字符串的例子:

1
2
3
4
5
6
7
8
9
10
11
12
string numbers("0123456789"), name("r2d2");
string::size_type pos = 0;
while((pos=name.find_first_of(numbers, pos))!=string::npos){
cout << "found number at index: " << pos << " element is " << name[pos] << endl;
pos++;
}
/* output
* * * * * * * *
* found number at index: 1 element is 2
* found number at index: 3 element is 2
* * * * * * * *
*/

逆向搜索:

1
2
3
string river("Mississippi");
auto first_pos = river.find("is"); //返回 1
auto last_pos = river.rfind("is"); //返回 4

【练习9.49】 如果一个字母延伸到中线之上,如 d 或 f ,则称其有上出头部分(ascender)。如果一个字母延伸到中线之下,称其有下出头部分(descender)。编写程序,读入一个单词文件,输出最长的既不包括上出头部分,也不包括下出头部分的单词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

void find_longest_word(ifstream& in){
string s, longest_word;
int maxlen = 0;
while (in >> s){
if(s.find_first_of("bdfghjklpqty")!=string::npos) continue;
cout << s << " ";
if(maxlen < s.size()){
maxlen = s.size();
longest_word = s;
}
}
cout << endl << "the longest string is: " << longest_word << endl;
}

int main(int argc, char* argv[]){
ifstream in(argv[1]);
if(!in){
cerr << "cannot open file." << endl;
return -1;
}

find_longest_word(in);

return 0;
}

输入文件:

1
2
3
4
5
6
asfdfva  asdfe mm
asdf/:o? asdf sfg aabb jjj
kkk
s
d
asdfasf werg aaa

命令及输出:

1
2
3
4
5
PS C:\Users\arrogance> cd D:\c++code\exercise9-49
PS D:\c++code\exercise9-49> g++ 949main.cpp -o 949prog
PS D:\c++code\exercise9-49> .\949prog.exe 949in.txt
mm s aaa
the longest string is: aaa

compare函数:

17-48.png

数值转换:

17-49.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

using namespace std;

int main(){
int i = 42;
string s = to_string(i); cout << s <<endl;
double d = stod(s); cout << d << endl;
double akashi = 42.23;
s = to_string(akashi);
cout << stod(s) << endl << stoi(s) << endl;

string s2 = "pi=3.14!!!??";
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
cout << d << endl;

return 0;
}
/* output
* * * * * *
* 42
* 42
* 42.23
* 42
* 3.14
* * * * * *
*/

【练习9.51】设计一个类,它有三个 unsigned 成员,分别表示月、日、年。为其编写构造函数,接受一个表示日期的 string 参数。你的构造函数应该能处理不同数据格式,如January 1,19901/1/1900Jan 1 1900等。

我的代码,注意没有检查格式错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <string>
#include <map>

class mdate{
friend std::ostream& print(std::ostream& os, const mdate& item);
public:
mdate() = default;
mdate(const std::string& s){
std::string::size_type pos = 0, pre = 0;
std::string wilddata[3] = {"","",""};
int k = 0;
while((pos=s.find_first_of(" ,/", pos))!=std::string::npos){
wilddata[k]=s.substr(pre, pos-pre);
pos++; k++;
pre = pos;
}
wilddata[2] = s.substr(pre);
dd = stol(wilddata[1]);
yy = stol(wilddata[2]);
if(mm2num.count(wilddata[0])!=0){
mm = mm2num[wilddata[0]];
}
else mm = stol(wilddata[0]);
}
private:
unsigned long yy;
unsigned long mm;
unsigned long dd;
static std::map<std::string,unsigned long> mm2num;
};

std::map<std::string, unsigned long> mdate::mm2num = {
{"January", 1}, {"Jan", 1},
{"February", 2}, {"Feb", 2},
{"March", 3}, {"Mar", 3},
{"April", 4}, {"Apr", 4},
{"May", 5}, {"May", 5},
{"June", 6}, {"Jun", 6},
{"July", 7}, {"Jul", 7},
{"August", 8}, {"Aug", 8},
{"September", 9}, {"Sept", 9},
{"October", 10}, {"Oct", 10},
{"November", 11}, {"Nov", 11},
{"December", 12}, {"Dec", 12}
};

std::ostream& print(std::ostream& os, const mdate& item){
os << "中文表述习惯为:" << item.yy << "年" << item.mm << "月"
<< item.dd << "日" << std::endl;
return os;
}

int main(){
std::string line;
while(getline(std::cin,line)){
if(line=="quit")break;
mdate akashi(line);
print(std::cout, akashi);
}
return 0;
}

输入输出及命令行信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Oct 23 2003
中文表述习惯为:2003年10月23日
November 11 2018
中文表述习惯为:2018年11月11日
Jan 1,1998
中文表述习惯为:1998年1月1日
Aug,2/2015
中文表述习惯为:2015年8月2日
May/12 2098
中文表述习惯为:2098年5月12日
2 2 2001
中文表述习惯为:2001年2月2日
2/1/1023
中文表述习惯为:1023年2月1日
quit

Press any key to continue . . .

以下给出检查某些错误的可能思路:

  • 若某些特立独行的用户输入诸如/////之类的数据,或者连续多个空格等,会导致数组越界。为此需要重新编写 while 循环内的语句。
  • 我们的思路是先将输入分成三块,然后分别在块内检查是否合法。如果依靠合法的分隔符都无法分为三块,则直接提示输入格式错误。
  • 其他细节不表。
1
2
3
4
5
6
7
while((pos=s.find_first_of(" ,/", pos))!=std::string::npos){
if(pos!=pre) wilddata[k++]=s.substr(pre, pos-pre);
pre = ++pos;
}
if(k!=2){/* error msg */}
wilddata[2] = s.substr(pre);
/* check if wilddata[0,1,2] is valid */

习题册给出的案例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// 9_51head.h
#ifndef DATE_H_INCLUDED
#define DATE_H_INCLUDED

#include <iostream>
#include <string>
#include <stdexcept>

using namespace std;

class date{
friend ostream& operator<<(ostream&, const date&);
public:
date() = default;
date(string& ds);

unsigned y() const {return year;}
unsigned m() const {return month;}
unsigned d() const {return day;}

private:
unsigned year, month, day;
};

const string month_name[] = {"January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"};

const string month_abbr[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sept", "Oct", "Nov", "Dec"};

const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

inline int get_month(string& ds, int& end_of_month){
int i,j;
for (i=0; i<12; i++){
// 检查每个字符是否与月份简写相等
for (j=0; j<month_abbr[i].size(); j++){
if(ds[j]!=month_abbr[i][j]) break;
}
if(j==month_abbr[i].size())break;
}

if(i==12) throw invalid_argument("不是合法月份名");

if(ds[j]==' '){ // 空白符,仅是月份简写
end_of_month = j+1;
return i+1;
}

for(; j<month_name[i].size(); j++)
if(ds[j]!=month_name[i][j]) break;

if(j==month_name[i].size() && ds[j]==' '){ //月份全称
end_of_month = j+1;
return i+1;
}

throw invalid_argument("不是合法月份名");
}

inline int get_day(string& ds, int month, int& p){
size_t q;
int day = stoi(ds.substr(p), &q); //从p开始的部分转换为日期值
if(day<1 || day>days[month])
throw invalid_argument("不是合法日期值");
p += q;
return day;
}

inline int get_year(string& ds, int& p){
size_t q;
int year = stoi(ds.substr(p), &q); //从p开始的部分转为年
if(p+q<ds.size())
throw invalid_argument("非法结尾内容");
return year;
}

date::date(string& ds){
int p;
size_t q;

if((p=ds.find_first_of("0123456789"))==string::npos)
throw invalid_argument("没有数字,非法日期");

if(p>0){ // 月份名格式
month = get_month(ds, p);
day = get_day(ds, month, p);
if(ds[p]!=' ' && ds[p]!=',')
throw invalid_argument("非法间隔符");
p++;
year = get_year(ds, p);
} else{ // 月份值格式
month = stoi(ds, &q);
p = q;
if(month<1 || month>12)
throw invalid_argument("不是合法月份值");
if(ds[p++]!='/')
throw invalid_argument("非法间隔符");
day = get_day(ds, month, p);
if(ds[p++]!='/')
throw invalid_argument("非法间隔符");
year = get_year(ds, p);
}
}

ostream& operator<<(ostream& out, const date& d){
out << d.y() << "年" << d.m() << "月" << d.d() << "日" << endl;
return out;
}

#endif // DATE_H_INCLUDED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 9_51main.cpp
#include <iostream>
#include <string>
#include "9_51head.h"

using namespace std;

int main(){
string dates[] = {"Jan 1,2014", "February 1 2014", "3/1/2014",
//"Jcn 1,2014",
//"Janvary 1,2014",
//"Jan 32,2014",
//"Jan 1/2014",
"3 1 2014"};
try {
for(auto ds : dates){
date d1(ds);
cout << d1;
}
} catch(invalid_argument e){
cout << e.what() << endl;
}

return 0;
}

输出:

1
2
3
4
5
2014年1月1日
2014年2月1日
2014年3月1日
非法间隔符

注意这个案例代码的格式是严格按照题目要求的,稍有不符就判错了。

容器适配器

三个顺序容器适配器:stackqueuepriority_queue

一个适配器是一种机制,能使得某事物的行为看起来像另一种事物一样。

  • 例如,stack适配器接受一个顺序容器(arrayforward_list除外),并使其操作起来像一个stack一样。

所有的适配器都要求容器具有添加、删除及方便访问尾元素的能力。

17-50.png

定义一个适配器。该部分内容较晦涩且实际用途不明(至少在我看来是绕了一个大圈实现了某种功能?),具体参阅第五版《C++ primer 中文版》第 329 页。

默认情况下,stackqueue是基于deque实现的,priority_queue是在vector之上实现的。我们可以创建适配器时,通过第二个参数来指定容器类型。

1
stack<int, vector<int>> intStack;

17-51.png

17-52.png

注意:上图中,q.pop()注释写错了。应该为:“删除首元素 … 不返回此元素”。

泛型算法概述

顺序容器只定义了很少的操作:在多数情况下,我们可以添加和删除元素、访问首尾元素、确定容器是否为空以及获得指向首元素或尾元素之后位置的迭代器。

我们可以想象用户可能还希望做其他很多有用的操作:查找特定元素、替换或删除一个特定值、重排元素顺序等。

标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法(generic algorithm):称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索;称它们是“泛型的”,是因为它们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,如 vector 或 list,还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列。

大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。

一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。

例如,我们有一个 int 的 vector :

1
2
3
4
int val = 42;
auto result = find(vec.cbegin(), vec.cend(), val);
cout << "The value" << val
<< (result == vec.cend() ? " is not present" : " is present") << endl;

例如,一个 string 的 list :

1
2
string val = "a value";
auto result = find(lst.cbegin(), lst.cend(), val);

类似的,由于指针就像内置数组上的迭代器一样,我们可以用 find 在数组中查找值:

1
2
3
int ia[] = {27, 210, 12, 47, 109, 83};
int val = 83;
int* result = find(begin(ia), end(ia), val);

上例中使用了标准库的beginend函数,来获得指向 ia 中首元素和尾元素之后位置的指针,并传递给 find .

还可以在序列的子范围中查找。例如,在ia[1]ia[2]ia[3]中查找给定元素:

1
auto result = find(ia+1, ia+4, val);

17-53.png

迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。

未完待续

17-53dot5.png

https://www.bilibili.com/video/BV1z64y1U7hs?p=52

OOP 概述

面向对象程序设计(object-oriented programming)的核心思想:

  • 数据抽象:接口与实现分离
  • 继承:定义相似的类,并对其相似关系建模
  • 动态绑定:在一定程度上忽略相似类的区别,以统一的方式使用它们

通过继承(inheritance),联系在一起的类构成一种层次关系

  • 基类(base class):定义共同拥有的成员
  • 派生类(derived class):定义特有的成员
  • 虚函数(virtual function):基类希望派生类各自定义自己合适的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
class Quote{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};

//派生类必须通过使用类派生列表明确指出基类
class Bulk_quote : public Quote{
public:
double net_price(std::size_t) const override;
};
//因为 Bulk_quote 在它的类派生列表中使用了 public 关键字,因此
//我们完全可以把 Bulk_quote 对象当成 Quote 对象来使用。

如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。

动态绑定(dynamic binding),我们能用同一段代码分别处理派生类和基类。

1
2
3
4
5
6
7
8
9
10
double print_total(ostream& os, const Quote& item, size_t n){
//根据传入item形参的对象类型调用 Quote::net_prize 或 Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN: " << item.isbn()
<< "#sold: " << n << "total due: " << ret << endl;
return ret;
}
//basic的类型是Quote, bulk的类型是Bulk_quote
print_total(cout, basic, 20);
print_total(cout, bulk, 20);

使用基类的引用(或指针)调用一个虚函数时,将发生动态绑定(也叫运行时绑定:run-time binding)。

定义基类和派生类

protected访问运算符:基类希望它的派生类有权访问该成员,同时禁止其他用户访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Quote{
public:
Quote() = default;
Quote(const std::string& book, double sales_price):
bookNo(book), price(sales_price){}
std::string isbn() const{return bookNo;}
virtual double net_price(std::size_t n) const{
return n*price;
}
virtual ~Quote() = default; //对析构函数进行动态绑定
//基类通常都应该定义一个虚析构函数,即使
//该函数不执行任何实际操作也是如此。

private:
std::string bookNo;

protected: //派生类需要访问的基类(受保护的)成员
double price = 0.0;
};

class Bulk_quote : public Quote{ //Bulk_quote 继承自 Quote
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0; //适用折扣政策的最低购买量
double discout = 0.0;
};
//派生类经常(但不总是)覆盖它继承的虚函数

protected访问说明符的作用是控制派生类从基类继承来的成员是否对派生类的用户可见。

如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。同时,在需要基类的引用或指针的地方,都可以使用派生类的对象。

17-54.png

派生类必须使用基类的构造函数来初始化继承来的成员:

1
2
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc){}

重写net_price()

1
2
3
4
5
6
double Bulk_quote::net_price(size_t cnt) const{
if(cnt>=min_qty)
return cnt*(1-discount)*price;
else
return cnt*price;
}

继承与静态成员:

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base{
public:
static void statmem();
};

class Derived : public Base{
void f(const Derived&);
};

void Derived::f(const Derived& derived_obj){
Base::statmem(); //ok
Derived::statmem(); //ok
derived_obj.statmem(); //ok
statmem(); //通过this对象访问,ok
}

派生类的声明:

1
2
class Bulk_quote : public Quote;  //错误!
class Bulk_quote; //正确

如果要将某个类用作基类,则该类必须已经定义。

一个类是基类,同时也可以是一个派生类:

1
2
3
class Base{/*...*/};
class D1 : public Base{/*...*/};
class D2 : public D1{/*...*/};

防止继承发生:

1
2
3
4
5
class NoDerived final{/*...*/}; //不能作为基类
class Base{/*...*/};
class Last final : Base{/*...*/};
class Bad : NoDerived{/*...*/}; //错误!
class Bad2 : Last{/*...*/}; //错误!

使用基类的引用(或指针)时,实际上编译器并不清楚所绑定对象的真实类型。

  • 静态类型(static type):编译时已知
  • 动态类型(dynamic type):运行时才可知

不存在从基类向派生类的隐式类型转换:

1
2
3
4
5
6
7
8
Quote base;
Bulk_quote* bulkP = &base; //错误
Bulk_quote& bulkRef = base; //错误

Bulk_quote bulk;
Quote* itemP = &bulk; //正确
Bulk_quote* bulkP = itemP; /*错误:编译器只能通过检验静态类型来推断
但这里可以通过 dynamic_cast 或 static_cast 进行转换*/

在对象间不存在类型转换:

1
2
3
4
5
Bulk_quote bulk;   //派生类对象

//派生类特有的部分会被切掉(sliced down):
Quote item(bulk); //使用 Quote::Quote(const Quote&) 构造函数
item = bulk; //使用 Quote::operator = (const Quote&)

【练习15.10】 回忆在 8.1 节中的讨论,解释第 284 页中将ifstream传递给Sales_dataread函数的程序是如何工作的。

【答】 在要求使用基类型对象的地方,可以使用派生类型的对象来代替,是静态类型和动态类型不同的典型例子。

虚函数

对虚函数的调用可能在运行时才被解析:

1
2
3
4
5
6
7
8
9
10
Quote base("0-201-1", 50);
print_total(cout, base, 10);
Bulk_quote derived("0-201-1", 50, 5, .19);
print_total(cout, derived, 10);
//--------- 对 比 ------------
/*
* 动态绑定只有在通过指针或引用调用虚函数时才会发生
*/
base = derived; //把 derived 的 Quote 部分拷贝给 base
base.net_price(20); //调用 Quote::net_price()

基类中的虚函数在派生类中隐式地也是一个虚函数。该函数在基类中的形参必须与派生类中的形参严格匹配。

finaloverride说明符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
};

struct D1 : B{
void f1(int) const override; //ok
void f2(int) override; //错误!
void f3() override; //错误!
void f4() override; //错误!
};

struct D2 : B{
//从 B 继承 f2() 和 f3() ,覆盖 f1(int)
void f1(int) const final;
};

struct D3 : D2{
void f2(); //ok
void f1(int) const; //错误!在 D2 中已经声明为 final
};

如果虚函数使用默认实参,基类和派生类中定义的默认实参最好一致。

回避虚函数的机制:

1
2
3
//强制调用基类中定义的函数版本而不管 baseP 的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);
//该调用将在编译时完成解析

什么时候我们需要回避虚函数的默认机制呢?通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

【练习15.11】 为你的Quote类体系添加一个名为debug的虚函数,令其分别显示每个类的数据成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//虚函数的构造练习
class Quote{
public:
// ...
virtual void debug(){
cout << "bookNo=" << bookNo << " price=" << price << endl;
}
// ...
private:
// ...
protected:
// ...
};

class Bulk_quote : public Quote{
public:
// ...
virtual void debug(){ // void debug() override{} ?
Quote::debug(); //bookNo 变量为 private, 所以不能直接访问 bookNo
//只能调用基类的 debug() 函数来显示
cout << "min_qty=" << min_qty << " discount=" << discount << endl;
}
private:
// ...
};

抽象基类

需求:现在我们需要支持多种不同的折扣策略。共同点是每个折扣策略都需要一个购买量值和折扣值。

分析:我们可以定义一个Disc_quote类来支持不同的折扣策略,其中Disc_quote负责保存购买量值和折扣值。但是,Disc_quote类中的net_price()函数是没有实际含义的,为了防止用户编写出无意义的代码(具体查阅第五版《C++ primer(中文版)》第 540 页),我们必须重新考虑。我们根本就不希望用户创建Disc_quote对象,Disc_quote类表示的是一本打折书籍的通用概念,而非某种具体的折扣策略。

做法:将net_price()定义为纯虚(pure virtual)函数。一个纯虚函数无须定义,在声明语句的分号之前书写=0就可以将一个虚函数说明为纯虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
//用于保存折扣值和购买量的类,派生类使用这些数据可以实现不同的折扣策略
class Disc_quote : public Quote{
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price,
std::size_t qty, double disc):
Quote(book, price), quantity(qty), discount(disc){}
double net_price(std::size_t) const = 0;
protected:
std::size_t quantity = 0; //折扣适用的购买量
double discount = 0.0; //表示折扣的小数值
};

可以为纯虚函数提供定义,但必须定义在类的外部。

含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。不能创建抽象基类的对象。

派生类构造函数只初始化它的直接基类(而不是最顶层的那个):

1
2
3
4
5
6
7
8
class Bulk_quote : public Disc_quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double price,
std::size_t qty, double disc):
Disc_quote(book, price, qty, disc){}
double net_price(std::size_t) const override;
};

访问控制与继承

protected说明符:

  • 和私有成员类似,受保护的成员对类的用户不可访问
  • 和公有成员类似,受保护的成员对派生类的成员和友元可访问
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{
protected:
int prot_mem;
};

class Sneaky : public Base{
friend void clobber(Sneaky&); //能访问 Sneaky::prot_mem
friend void clobber(Base&); //不能访问 Base::prot_mem
int j; // j 默认是 private
};

void clobber(Sneaky& s){s.j = s.prot_mem = 0;} //正确
void clobber(Base& b){b.prot_mem = 0;} //错误!

某个类对其继承来的成员的访问权限受到两个因素的影响:

  • 在基类中该成员的访问说明符
  • 在派生类的派生列表中的访问说明符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base{
public:
void pub_mem();
protected:
int prot_mem;
private:
char priv_mem;
};

struct Pub_Derv : public Base{
//这个类里有两个成员:pub_mem()函数,public的;prot_mem数据成员,protected的
int f() {return prot_mem;} //正确。派生类能访问 protected 成员
char g() {return priv_mem;} //错误!private 成员对于派生类不可访问
};

struct Priv_Derv : private Base{
//这个类里有两个成员:pub_mem()函数、prot_mem数据成员。这两个权限都是private
// private 不影响派生类的访问权限,只是影响对象的访问权限
int f1() const {return prot_mem;}
};

Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem(); //正确
d2.pub_mem(); //错误!pub_mem()在派生类Priv_Derv中是private的

//派生访问说明符还可以控制继承自派生类的新类的访问权限
struct Derived_from_Public : public Pub_Derv{
//两个成员:一个public,一个protected
int use_base() {return prot_mem;} //正确
};
struct Derived_from_Private : public Priv_Derv{
//一个都没有了
int use_base() {return prot_mem;} //错误!
};

严格来说,private成员可以继承,但只能通过内存地址等非常规方式进行访问。 下面举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// https://blog.csdn.net/k346k346/article/details/49652209
/*如果基类中并没有提供访问私有成员的公有函数,那么其私有成员是否
“存在”呢?还会不会被继承呢?其实,这些私有成员的确是存在的,而且
会被继承,只不过程序员无法通过正常的渠道访问到它们。考察如下程序,
通过一种特殊的方式访问了类的私有成员。
*/
#include <iostream>
using namespace std;

class A {
int i;
void privateFunc()
{
cout<<"this is a private function of base class"<<endl;
}

public:
A(){i=5;}
};

class B:public A {
public:
void printBaseI() {
int* p=reinterpret_cast<int*>(this);//获取当前对象的首地址
cout<<*p<<endl;
}

void usePrivateFunction() {
void (*func)()=NULL;
_asm
{
mov eax,A::privateFunc;
mov func,eax;
}
func();
}
};

int main() {
B b;
b.printBaseI();
b.usePrivateFunction();
}

/*=========== 程序输出结果 ==============//
5
this is a private function of base class
//======================================*/

/*-------------------------- 解释 -----------------------------//
(1)虽然类 A 没有提供访问私有成员变量 i 的公有方法,但是
在类 A(以及类 A 的派生类)对象中,都包含变量 i。
(2)虽然类 A 并没有提供访问私有成员函数 privateFunc()
的公有函数,但是在程序代码区依然存有函数 privateFunc()
的代码,通过内联汇编获取该函数的入口地址,仍然可以顺利调用。

综上所述,类的私有成员一定存在,也一定被继承到派生类中,从大小也可
以看出派生类包含了基类的私有成员,读者可自行考证。只不过受到 C++
语法的限制,在派生类中访问基类的私有成员只能通过间接的方式进行。
————————————————
版权声明:本文为CSDN博主「恋喵大鲤鱼」的原创文章,遵循CC 4.0 BY-SA
版权协议,转载请附上原文出处链接及本声明。
//-------------------------------------------------------------*/

另外补充:书中的某些表述比较模糊,下面给出一个例子进一步说明某个情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//只有在派生类中才可以通过派生类对象访问基类的protected成员。
#include <iostream>
using namespace std;

class Base {
protected:
int i;
};

class Derived : public Base{
public:
void fun(Derived d){
d.i = 3; //只有在派生类中才可以通过派生类对象访问基类的protected成员。
}
};

int main(){
Derived derived;
// derived.i = 3; //只有在派生类中才可以通过派生类对象访问基类的protected成员。
return 0;
}

友元关系不能继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base{
//其他成员与之前版本一致
friend class Pal; // Pal 在访问 Base 的派生类时不具有特殊性
};
class Pal{
public:
int f(Base b) {return b.prot_mem;} //正确
int f2(Sneaky s) {return s.j;} //错误!
int f3(Sneaky s) {return s.prot_mem;} //正确,虽然看上去有点奇怪
//Pal能够访问Base的成员,这种访问包括了Base对象内嵌在其派生类对象中的情况
};

class D2 : public Pal{
public:
int mem(Base b) {
return b.prot_mem; //错误!友元关系不能继承
}
};

通过使用using改变个别成员的可访问性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
};

class Derived : private Base {
public:
using Base::size;
protected:
using Base::n;
};

//派生类只能为那些它可以访问的名字提供 using 声明

默认的继承保护级别:

1
2
3
class Base {/*===*/};
struct D1 : Base {/*---*/}; //默认 public 继承
class D2 : Base {/*---*/}; //默认 private 继承

未完待续

两个未完待续之间的内容并未学习。

17-55.png

https://www.bilibili.com/video/BV1z64y1U7hs?p=79