一.C语言中%p与%x的区别

“%p”中的p是pointer(指针)的缩写。%p是打印地址的, 而%x是以十六进制形式打印。
%p是打印地址(指针地址)的,是十六进制的形式,但是会全部打完,即有多少位打印多少位。

32位编译器的指针变量为4个字节(32位),64位编译器的指针变量为8个字节(64位)。

所以,在32位编译器下,使用%p打印指针变量,则会显示32位的地址(16进制的);在64位编译器下,使用%p打印指针变量,则会显示64位的地址(16进制的),左边空缺的会补0。

%x:无符号十六进制整数(字母小写,不像上面指针地址那样补零)

%X:无符号十六进制整数(字母大写,不像上面指针那样补零)

%x、%X和%p的相同点都是16进制,不同点是%p按编译器位数长短(32位/64位)输出地址,不够的补零

二.C++中如何打印字符的地址

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

int main()
{
int a = 17;
char c1 = 'a';
char c2 = 'b';
char *ch1 = &c1;
char *ch2 = &c2;
printf("%p\n",&c1);
printf("%p\n",ch1);
printf("%x\n",a);
cout<<"static_cast<void *>(&c1)="<<static_cast<void*>(&c1)<<endl;
cout << (int *)&c1 << endl; // 同样可以输出字符地址
cout << (string *)&c1 << endl; // 同样可以输出字符地址
return 0;
}

解析:

1
2
3
printf("%p\n",&c1); //打印c1字符的地址
printf("%p\n",ch1); //打印ch1指针变量的值
printf("%x\n",a); //将整型a以16进制形式输出

重点

1
2
3
4
5
6
7
8
C++标准库中I/O类对输出操作符<<重载,在遇到字符型指针时会将其当做字符串名来处理,输出指针所指的字符串。既然这样,我们就别让他知道那是字符型指针,所以得进行类型转换,即:希望任何字符型的指针变量输出为地址的话,都要作一个转换,即强制char *转换成void *,如下所示:

cout<<"static_cast<void *>(&c1)="<<static_cast<void*>(&c1)<<endl;
// static_cast是一个强制类型转换操作符。强制类型转换,也称为显式转换
链接:https://blog.csdn.net/zongyinhu/article/details/49512919

cout << (int *)&c1 << endl; // 同样可以输出字符地址
cout << (string *)&c1 << endl; // 同样可以输出字符地址

三.简单文件的输入输出

《C++ Primer Plus》第六章第八小节

1. 文件的输出

​ 将文件内容输出至文本中

类比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// cout控制台输出---cout输出在屏幕上
1. 包含头文件 iostream
2. 头文件iostream定义一个用于处理输出的ostream类
3. 头文件iostream声明一个名为cout的ostream变量(对象)
4. 必须指明名称空间;必须使用编译指令using或者std::
5. 可以使用cout和运算符<< 来显示各种类型的数据

// 文件输出---ofstream对象输出在对象所关联的文件中
1. 包含头文件fstream
2. 头文件fstream定义一个用于处理输出的ofstream类
3. 需要声明一个或多个ofstream变量(对象)
4. 需将ofstream对象与文件关联起来,例如方法:open();
5. 使用完文件后,用close()将其关闭
6. 可结合使用ofstream对象和运算符<<来输出各种类型的数据

// outFile 可以使用 cout 可使用的任何方法
将文件与ofstream关联起来
1
2
3
4
5
6
7
ofstream outFile;
// 方法1:
outFile("fish.txt");
// 方法2:
char filename[50];
cin >> filename;
outFile(filename);

示例程序
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
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
char automobile[50];
int year;
double a_price;
double d_price;

ofstream outFile;
outFile.open("carinfo.txt"); // associate with a file

cout << "Enter the make and model of automobile : ";
cin.getline(automobile , 50);
cout << "Enter the model year : " ;
cin >> year;
cout << "Make the original asking price : ";
cin >> a_price ;
d_price = 0.913 * a_price;

cout << "-----------------------" << endl;
cout << fixed;
cout.precision(2); // 保留两位小数
cout.setf(ios_base::showpoint);
cout << "Make and model: " << automobile << endl;
cout << "Year: " << year << endl;
cout << "Was asking: $" << a_price << endl;
cout << "Now asking: $" << d_price << endl;

cout << "-----------------------" << endl;
outFile << fixed;
outFile.precision(2); // 保留两位小数
outFile.setf(ios_base::showpoint);
outFile << "Make and model: " << automobile << endl;
outFile << "Year: " << year << endl;
outFile << "Was asking: $" << a_price << endl;
outFile << "Now asking: $" << d_price << endl;
outFile.close();
return 0;
}
1
2
3
4
5
cout.setf()的作用是通过设置格式标志来控制输出形式,其中ios_base::fixed表示:用正常的记数方法显示浮点数(与科学计数法相对应);ios_base::floatfield表示小数点后保留6位小数。

setf()的第一原型:
C++为标准输入和输出定义了一些格式标志:
例如 : cout.setf(ios_base::left); //对所有cout的输出进行左对齐调整.

image-20220326105933847


2. 文件的读取

类比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// cout 的文件输入
1. 头文件包含iostream
2. 头文件定义用于处理输入的istream
3. 头文件iostream声明了一个名为cin的istream变量(对象)
4. 必须指明名称空间
5. 可结合使用cin和运算符>> 来读取各种类型数据
6. 可使用cin和get()方法来读取一个字符,使用cin和getline()来读取一行字符
7. 可结合使用cin和eof()、fail()判断输入是否成功
8. 对象cin本身被用作测试条件时,若最后一个读取操作成功,它将被转换为布尔值true 否则 转为false

// 文件的读取
1. 头文件fstream
2. 头文件fstream定义一个用于处理输入的ifstream类
3. 需要声明一个或多个ifstream变量(对象)
4. 必须指明名称空间
5. 需要将ifstream对象与文件关联起来,使用方法open()
6. 使用完文件后,使用close()方法将其关闭
7. 结合使用ifstream对象和运算符>> 来读取各种类型的数据
8. 可以使用ifstream对象和get()方法来读取一个字符,使用ifstream对象和getline()读取一行字符
9. 可结合使用ifstream和eof()、fail()判断输入是否成功
10. 对象ifstream本身被用作测试条件时,若最后一个读取操作成功,它将被转换为布尔值true 否则 转为false
将文件与ofstream关联起来
1
2
3
4
5
6
7
ifstream inFile;
//方法1:
inFile.open("123.txt");
//方法2:
char filename[50];
cin >> filename;
inFile.open(filename);
使用
1
2
3
4
double wt;
inFile >> wt; // read a num from 123.txt
char line[80];
inFile.getline(line,80); // read a line of text
is_open()
1
2
3
4
5
6
// 检查文件是否成功被打开使用方法is_open()
inFile.open(123.txt);
if(!inFile.is_open()) // 当文本打开失败---> 不允许读写 ---> 或者文件不存在时
{
exit(EXIT_FALLURE); // exit()在头文件cstdlib中定义,还定义了一个同操作系统通信的参数值EXIT_FAILURE ----> exit()终止程序
}
other
1
2
3
4
5
6
7
eof()方法用于判断最后一次读取数据时候是否遇到EOF,若是返回true  

fail()方法用于判断最后一次读取数据的时候是否遇到了类型不配的情况,若是返回true(如果遇到了EOF,该方法也返回true

bad() 如果出现意外的问题,如文件受损或硬件故障,最后一次读取数据的时候发生了这样的问题,方法bad()将返回true

good() 该方法在没有发生任何错误的时候返回true。该方法也指出的最后一次读取输入的操作是否成功。
实例程序
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
#include <iostream>
#include <fstream>
#include <cstdlib>
const int Size = 60;
using namespace std;

int main()
{
char filename[Size];.

ifstream inFile;
cout << "Enter name of data file :";
cin.getline(filename,Size);
inFile.open(filename); // associate inFile with a file
if(!inFile.is_open()) // failed to open the file
{
cout << "Could not open the file " << filename << endl;
cout << "Program terminating.\n" ;
exit(EXIT_FAILURE); // Abnormal exit
}
double value;
double sum = 0.0;
int count = 0;

cout << "------ Data Reading -------" << endl;
inFile >> value;
while(inFile.good()) // Judge whether the last read was successful
{
++count;
sum += value;
inFile >> value;
}
// If the last reading fails, judge the reason
if(inFile.eof()) // the end of the file
{
cout << "End of file reached.\n";
}
else if(inFile.fail())
{
// Determine whether the last read encountered a type mismatch
cout << "Input terminated by data minmatch.\n";
}
else
{
cout << "Input terminated for unknown reason.\n";
}
// Judge whether there is data in the file
if(0==count)
{
cout << "No data processed.\n";
}
else
{
cout << "Items read : " << count << endl;
cout << "Sum: " << sum <<endl;
cout << "Average: " << sum/count <<endl;
}
inFile.close();
return 0;
}

四.指针数组与数组指针

[]的优先级高于*

1
2
int (*arr)[4];             // 数组指针,本质为一个指针,指向一个数组,数组中有四个元素,每一个元素都是int 类型----指向二维数组
int *arr[4]; // 指针数组,本质为一个数组,有四个元素,每一个元素都是int * 类型

实例:

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

using namespace std;

int main()
{
int date[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*arr)[4] = date;
cout << sizeof(date[1]) << endl;
cout << sizeof(arr[1]) << endl;
cout << arr[1][2] << endl;
return 0;
}
// 结果: 都为 sizeof结果都为16 第一个date[1],第一行的数组,为第一个元素,有4*4=16个字节
// 第二个arr[1], 指向第一个数组元素,即date的第一行也为4*4=16个字节

易混淆

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
#include <iostream>

using namespace std;


int sum(int (*arr)[4])
{
int result = 0;
for(int i = 0;i<4;i++)
{
result += (*arr + i);
}
return result;
}

int main()
{
int arr[]{1,2,3,4,5};
int result = sum(arr);
cout << "the result is: " << result << endl;
return 0;
}

// 编译报错:
13.cpp: In function ‘int main()’:
13.cpp:19:19: error: cannot convert ‘int*’ to ‘int (*)[4]’
19 | int result = sum(arr);
| ^~~
| |
| int*
13.cpp:6:15: note: initializing argument 1 of ‘int sum(int (*)[4])
6 | int sum(int (*arr)[4])
| ~~~~~~^~~~~~~

// 解析:
int arr[] 一维数组,的arr本质是一个int * 类型的指针变量,存储的为地址。即arr数组的首地址
int (*arr)[4] 数组指针,本质为一个指针,指向一个数组,指向由4个int组成的数组指针。其类型为 int (*)[4] 无法相互转换

五.数组替代

1.vector模板类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 头文件 #include <vector>
// vector包含在命名空间std中
// 声明:
vector<typeName> vt(n_elem)
// typeName 为类型 , n_elem 元素个数

// 实例:
vector<int> vi; // 长度为0的vector,可使用vector的方法对其进行插入或者添加值 ---> 自动调整长度 ---> 动态数组
int n;
cin >> n;
vector<double> vd(5);
// 功能比数组强大,但是其效率较低
// 是使用New创建动态数组的替代品,使用new与delete管理内存,但是该过程是自动完成的
// 使用堆(new) ,动态内存分配,自由存储区

2.array模板类(C++11)

1
2
3
4
5
6
7
8
9
10
// 头文件 #include <array>
// array包含在命名空间std中
// 声明:
array<typeName,n_elem> arr
// typeName 为类型 , n_elem 元素个数

// 实例:
array<int,5> ai;
// array 长度固定,效率与数组一样,安全性相对数组更高
// 使用栈,静态内存分配

3.超界问题解决

vector 与 array 不会对于错误进行检查

1
2
3
// 例如
array<double,5> arr;
arr[-2] = 2.3; // 超界问题,但是系统不会报错 ---> 编译器代码转换为 *(arr-2) = 2.3;

使用成员函数 at() 进行非法索引的捕获

1
2
3
4
5
6
7
arr.at(-2);   
// 非法,at对于-2非法索引进行捕获\
// 报错:
terminate called after throwing an instance of 'std::out_of_range'
what(): array::at: __n (which is 18446744073709551614) >= _Nm (which is 5)
已放弃 (核心已转储)


六. 函数指针

1
2
// 函数定义
double pf(int) // 可以不写形参变量名字

函数指针

1
2
3
double (*pf)(int) 
// () 和[] 一样优先级高于*
// 上述定义 解释 --> pf是一个指向返回值为double,形参类型为int的函数指针

区分指针函数

1
2
3
double *pf(int)
// 由于 () 优先级高于 *
// 上述定义解释 --> 首先是一个函数 形参类型为 int ,函数的返回值为 double * 类型

1. 使用指针来调用函数

1
2
3
4
5
6
7
8
double pam(int);
double (*pf)(int);
pf = pam; // pf指针指向pam函数
double x = pam(5);
double y = (*pf)(5); // 通过函数指针来调用函数

// C++同样允许以下方式:
double z = pf(5);

2. 深入(C++ primer plus 202页)

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
#include <iostream>

using namespace std;

// 定义三个指针函数,其返回值类型为 double *
const double* f1(const double *ar,int n)
{
return ar;
}

const double* f2(const double ar[],int n)
{
return ar+1;
}

const double* f3(const double ar[],int n)
{
return ar+2;
}


int main()
{
double av[3] = {1.1,1.2,1.3};

// part1
cout << "PART1-----------" << endl;
// 定义一个 double * 类型的函数指针,指向函数f1
const double* (*p1)(const double*,int) = f1;
// C++11 特性 --> 自动推断类型
auto p2 = f2;
// 等价于 --> double* (*p2)(const double*,int) = f2;
cout << "Address Value" << endl;
// 调用f1输出av数组的地址 输出av数组第一个元素的值
// (*p1)(av,3) == f1(av,3)
cout << (*p1)(av,3) <<" " << *(*p1)(av,3) << endl;

// p2(av,3) == f2(av,3) 对地址使用解除引用* 得到其内存中的数据
cout << p2(av,3) << " " << *p2(av,3) << endl;


// part2
cout << "PART2-----------" << endl;
cout << "Address Value" << endl;
// 定义一个指针数组,数组有三个元素,每一个元素都为函数指针
const double* (*pa[3])(const double * ,int) = {f1,f2,f3};
auto pb = pa;
// 等价于 const double* (**pb)(const double *,int) = pa;
for(int i = 0;i<3;i++)
{
cout << (*pa[i])(av,3) <<" " << *(*pa[i])(av,3) << endl;
}
for(int i = 0;i<3;i++)
{
cout << (*pb[i])(av,3) <<" " << *(*pb[i])(av,3) << endl;
}

// part3
cout << "PART3-----------" << endl;
cout << "Address Value" << endl;
// pc 是一个指针,指向由三个函数指针组成的数组
auto pc = &pa;
// 等价于 const double *(*(*pc)[3])(const double *,int) = &pa;
// (*pc)[3] --> 数组指针,pc是一个指针,指针指向由三个元素组成的数组
// (*(pc)[3]) 数组内的每一个元素都是指针
// const double *(*(*pc)[3])(const double *,int) 数组内的每一个元素都为函数指针
cout << (*pc)[0](av,3) << " " << *(*pc)[0](av,3) << endl;
// (*pc) == pa --> (*pc)[0](av,3) = pa[0](av,3)


return 0;
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PART1-----------
Address Value
0x7fffbc4b7000 1.1
0x7fffbc4b7008 1.2
PART2-----------
Address Value
0x7fffbc4b7000 1.1
0x7fffbc4b7008 1.2
0x7fffbc4b7010 1.3
0x7fffbc4b7000 1.1
0x7fffbc4b7008 1.2
0x7fffbc4b7010 1.3
PART3-----------
Address Value
0x7fffbc4b7000 1.1

七.左值引用 右值引用

1
对一个值取地址可以成功取出的为 左值,无法成功取出则为 右值
(1). 左值引用
1
2
3
4
5
6
7
8
9
10
int a = 10;
int& c = a; //左值引用,赋值运算符右侧,一定要是左值,(常)普通变量a是左值

int a = 10;
int b = 20;
int& c = (a+b); // error ,因为(a+b)是一个右值

const int& d = 10;
const int& c = (a+b); // 常引用成功! const 会将 10,(a+b)计算的结果,放置到内存的临时变量中,使得引用与临时变量产生关联
// 使用常引用后,仅能通过引用来读取数据,无法修改数据
(2).右值引用
1
2
3
4
int a= 10; // 常值是右值,其地址是随机的不确定的
int b = 20;
int &&x = 10; // 合法,右值引用
int &&y = (a+b); // 右值引用

八.函数显示具体化与实例化区别

1.形式区别

1
2
3
4
5
6
7
8
9
10
11
template<> void Swap<typeName>(typeName &a,typeName &b);
template<> void Swap(typeName &a,typeName &b);
// 显示具体化,选择以上一种 ---> typeName必须指定为特定的数据类型
// 非函数模板 > 显示具体化 > 函数模板
// 当传入函数的类型为指定的符合显示具体化中对应的参数类型时,此时优先使用显示具体化
// 显示具体化,可以帮助当模板函数无法重载时

template void Swap<typeName>(typeName &a,typeName &b);
// 显示实例化 ---> typeName必须指定为特定的数据类型
// 函数模板只是用于生成函数定义的方案,并非函数定义
// 显示实例化:使用函数模板生成指定参数类型的函数定义

上述,代码可知,显示具体化声明在关键字 template后包含<>,而显示实例化没有


2.意义区别

显示具体化:

  • 显示具体化,指定模板函数中类型,意思是不要使用函数通用的模板来生成函数定义,而是要使用指定的数据类型来生成函数定义
  • 显示具体化,实际仍然是隐式实例化,仅在函数调用时,根据指定的参数类型生成指定的函数定义
  • 显示具体化为函数模板的特例

显示实例化:

  • 显示实例化,直接命令编译器创建特定的实例 —> 无论是否调用,均会生成函数定义,即函数定义一直存在。调用函数时,函数定义直接使用,不调用时函数定义已经存在
  • 其具体用途: 先创建模板的某个具体实例,而非使用时在隐式的创建(隐式实例化)。
    • 显示实例化,是为了编写库文件提供的。没有实例化的模板无法放置在目标文件(源文件编译之后生成目标文件,目标文件再经过链接生成可执行文件)中。当其他文件代码的目标文件调用(或者链接)该函数时,前提是该函数已经生成了目标文件(即该函数已经生成函数定义实例化后,倘若仍然为函数模板则无法调用)

九、ostream控制格式输出

注:该部分转载至:链接

1.使用流操作算子

C++ 中常用的输出流操纵算子如表 1 所示,它们都是在头文件 iomanip 中定义的;要使用这些流操纵算子,必须包含该头文件

注意:“流操纵算子”一栏中的星号*不是算子的一部分,星号表示在没有使用任何算子的情况下,就等效于使用了该算子。例如,在默认情况下,整数是用十进制形式输出的,等效于使用了 dec 算子。

流操纵算子 作 用
*dec 以十进制形式输出整数 常用
hex 以十六进制形式输出整数
oct 以八进制形式输出整数
fixed 以普通小数形式输出浮点数
scientific 以科学计数法形式输出浮点数
left 左对齐,即在宽度不足时将填充字符添加到右边
*right 右对齐,即在宽度不足时将填充字符添加到左边
setbase(b) 设置输出整数时的进制,b=8、10 或 16
setw(w) 指定输出宽度为 w 个字符,或输人字符串时读入 w 个字符
setfill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)
setprecision(n) 设置输出浮点数的精度为 n。 在使用非 fixed 且非 scientific 方式输出的情况下,n 即为有效数字最多的位数,如果有效数字位数超过 n,则小数部分四舍五人,或自动变为科学计 数法输出并保留一共 n 位有效数字。 在使用 fixed 方式和 scientific 方式输出的情况下,n 是小数点后面应保留的位数。
setiosflags(flag) 将某个输出格式标志置为 1
resetiosflags(flag) 将某个输出格式标志置为 0
boolapha 把 true 和 false 输出为字符串 不常用
*noboolalpha 把 true 和 false 输出为 0、1
showbase 输出表示数值的进制的前缀
*noshowbase 不输出表示数值的进制.的前缀
showpoint 总是输出小数点
*noshowpoint 只有当小数部分存在时才显示小数点
showpos 在非负数值中显示 +
*noshowpos 在非负数值中不显示 +
*skipws 输入时跳过空白字符
noskipws 输入时不跳过空白字符
uppercase 十六进制数中使用 A~E。若输出前缀,则前缀输出 0X,科学计数法中输出 E
*nouppercase 十六进制数中使用 a~e。若输出前缀,则前缀输出 0x,科学计数法中输出 e。
internal 数值的符号(正负号)在指定宽度内左对齐,数值右对 齐,中间由填充字符填充。

使用这些算子的方法是将算子用 << 和 cout 连用。例如:

1
cout << hex << 12 << "," << 24; // 这条语句的作用是指定以十六进制形式输出后面两个数

2.setioflags()算子

setiosflags() 算子实际上是一个库函数,它以一些标志作为参数,这些标志可以是在 iostream 头文件中定义的以下几种取值,它们的含义和同名算子一样。

标 志 作 用
ios::left 输出数据在本域宽范围内向左对齐
ios::right 输出数据在本域宽范围内向右对齐
ios::internal 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充
ios::dec 设置整数的基数为 10
ios::oct 设置整数的基数为 8
ios::hex 设置整数的基数为 16
ios::showbase 强制输出整数的基数(八进制数以 0 开头,十六进制数以 0x 打头)
ios::showpoint 强制输出浮点数的小点和尾数 0
ios::uppercase 在以科学记数法格式 E 和以十六进制输出字母时以大写表示
ios::showpos 对正数显示“+”号
ios::scientific 浮点数以科学记数法格式输出
ios::fixed 浮点数以定点格式(小数形式)输出
ios::unitbuf 每次输出之后刷新所有的流
ios::stdio 每次输出之后清除 stdout, stderr

多个标志可以用|运算符连接,表示同时设置。例如:

1
cout << setiosflags(ios::scientific|ios::showpos) << 12.34;

如果两个相互矛盾的标志同时被设置,如先设置 setiosflags(ios::fixed),然后又设置 setiosflags(ios::scientific),那么结果可能就是两个标志都不起作用。因此,在设置了某标志,又要设置其他与之矛盾的标志时,就应该用 resetiosflags 清除原先的标志。例如下面三条语句:

1
2
3
cout << setiosflags(ios::fixed) << 12.34 << endl;
cout << resetiosflags(ios::fixed) << setiosflags(ios::scientific | ios::showpos) << 12.34 << endl;
cout << resetiosflags(ios::showpos) << 12.34 << endl; //清除要输出正号的标志

3.调用cout成员函数

ostream 类有一些成员函数,通过 cout 调用它们也能用于控制输出的格式,其作用和流操纵算子相同.

成员函数 作用相同的流操纵算子 说明
precision(n) setprecision(n) 设置输出浮点数的精度为 n。
width(w) setw(w) 指定输出宽度为 w 个字符。
fill(c) setfill (c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)。
setf(flag) setiosflags(flag) 将某个输出格式标志置为 1。
unsetf(flag) resetiosflags(flag) 将某个输出格式标志置为 0。

setf 和 unsetf 函数用到的flag,与 setiosflags 和 resetiosflags 用到的完全相同。

1
2
3
cout.setf(ios::scientific);
cout.precision(8);
cout << 12.23 << endl; // 1.22300000e+001

十、类的自动类型转换与强制转换

1.构造函数用作自动类型转换的转换函数

只接受一个参数的构造函数才可以作为转换函数

1
2
3
4
5
6
Stock::Stock(double lab); // 构造函数

// 创建一个Stock对象
Stock one;
// 将创建一个Stock(double lab)的临时对象,并将19.6作为初始值,采用成员赋值的方法,将该临时对象的内容复制到one中
one = 19.6

explicit

可通过构造函数前,显示添加explicit,关闭该自动特性

1
2
explicit Stock::Stock(double lab);
// 只接受一个参数的构造函数定义了从参数类型到类类型的转换,使用explicit可以关闭这种自动的隐式转换.

2.转换函数

转换函数,定义了从 某种类型 到 类类型的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
Stock::Stock(double lab); // 构造函数
Stock::Stock(double lab,double mab)

// 若定义了从Stock到double类型的转换
Stock wolf(16.3);
Stock solf(30,5)

// 使用转换函数
double host = double(wolf);
double think = (double) wolf;
// 使用转换函数
double host2 = double(solf);
double think2 = (double) solf;

创建转换函数

1
2
3
4
5
6
7
8
9
operator typeName();

// 注意:
1.转换函数必须是类方法
2.转换函数不能指定返回类型
3.转换函数不能有参数

// 如:
operator double(); //此处的typeName(double)是指定要转换成的类型

explicit

若类方法中,只定义了 类类型到一种类型 的转换函数,这将避免二义性,而使得可以使用隐式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//类声明中
Stock::Stock(double a); // 构造函数
operator double() ;// 定义转换函数

// 隐式转换
Stock opp(5);
double lab = opp; // 而非显示转换 double lab = double(opp)

// 二义性
operator double() ;// 定义转换函数
operator int();

long gone = opp;
long gone = opp; //由于上面定义了两种转换函数,这样使用隐式转换编译器报错

explicit关键词,可以防止这样的隐式转换,只允许显示转换

1
2
3
4
5
explicit operator double() ;// 定义转换函数
explicit operator int();

long gone = (double)opp;
long gone = (int)opp;

十一、类的静态成员变量

静态类成员:无论创建多少个类对象,程序只创建一个静态类变量的副本

1
2
// 定义在StringBad类声明中
static int number;

不能在类声明中初始化静态成员变量,声明描述了如何分配内存,但是并不分配内存,若在类声明(.h头文件中)初始化静态成员变量,当将该头文件引入别的文件时,会违背单定义的原则

可以在类实现中(.cpp文件中)初始化静态变量

1
2
3
4
5
// 初始化在类实现文件中
int StringBad::number = 0;

// 通过类名进行调用
StringBad::number

若静态成员变量是const类型,则可以在类声明中初始化

1
2
// 类声明中进行初始化
static const int number = 0;

十二、复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。即用于初始化过程中(包括按值传递参数),而非常规的赋值过程,类的复制构造函数原型通常如下:

1
2
3
4
Class_Name(const Class_Name &)

// 如:
StringBad(const StringBad &)

1.何时调用赋值构造函数

1
2
1.当函数按值传递类对象时(意味着创建原始变量的副本,编译器将生成临时对象,将使用复制构造函数)
2.函数返回类对象时(返回类对象,也会生成临时对象)

按值传递对象将调用复制构造函数,因此尽可能按引用传递对象,节省调用复制构造函数的时间,节省创建副本的时间

2.定义显示复制构造函数

定义一个显示的复制构造函数进行深度复制,复制构造函数,应当复制对象的数据内容并将对象副本的地址赋给令一个对象,而不仅仅是复制对象的地址,从而去引用该对象

仅仅复制对象的地址赋给其他对象,从而引用该对象,为浅复制,当调用析构函数进行释放内存时,则会将一个对象进行两次释放。

一般的C++标准库均有复制构造函数的重载,若一类中的成员无使用动态的内存分配,程序调用默认的复制构造函数无影响,因为程序结束时,调用析构函数无动态的内存释放,不会对同一个开辟的内存空间进行两次释放。

重点

1
2
如果类中包含了使用New初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而非指针(对象地址),这被称为深度复制。
浅复制:仅浅浅的复制指针信息,而不会深入“挖掘”以复制指针引用的结构与其内容。

3.赋值问题

将一个对象赋值给另外一个对象,也会出现与隐式复制构造函数相同的问题,因此需要提供复制赋值运算符的定义进行(深度复制)

书中:P356

十三、成员列表的初始化

对类中声明的const常量无法在类定义中对其进行赋值,因为常量只能在声明时进行初始化,因此可以使用成员初始化列表的方式,对常量进行初始化操作。成员列表初始化会在执行构造函数之前,对创建的对象进行初始化。成员列表初始化的方式只能用于构造函数。

注意:

1
2
3
4
5
1.只能用于构造函数
2.必须使用成员列表初始化的操作,初始化非静态const数据成员
3.必须使用成员列表初始化的操作,初始化引用数据成员

成员列表初始化,在执行构造函数的函数体之前,先创建对象并对对象进行初始化
1
2
3
4
5
6
7
8
9
Queue::Queue(int qs):qsize(qs)
{
front = rear = NULL;
items = 0;
// qsize =qs; // 这是非法操作,对常量无法进行赋值操作,仅能初始化
}

//解析:
:qsize(qs) // 该部分是对类常量qsize 初始化为qs

qsize 是在类声明中定义的一个常量,无法通过 **qsize =qs** 对常量进行赋值操作,只能初始化,原因是从概念上来说,调用构造函数时,对象将在括号中的代码执行之前被创建,因此调用 Queue(int qs) 构造函数将导致程序首先给四个成员变量分配内存(此时分配内存相当于在做初始化),然后程序流程进入到括号中,使用常规的赋值方式,将值存储到内存中,因此对于 const 数据成员,必须在执行构造函数体之前,即创建对象时进行初始化

成员初始化列表,不用写在类声明中,只需要写在类定义中

十四、友元

  • 友元函数
  • 友元类
  • 友元成员函数

友元函数是一种非成员函数,可以赋予该函数与类的成员函数相同的访问权限,可以访问类中的私有成员变量

友元函数,将普通的函数,通过friend修饰,但是该修饰只出现在函数原型上,即类声明中。不要在类定义中使用关键字friend

十五、派生类的构造函数

1.成员初始化列表调用基类构造函数

派生类的构造函数必须使用基类的构造函数

创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表的方式创建

下面是一个派生类RatedPlayer继承自基类TableTennisPlayer构造函数的代码实例:

1
2
3
4
5
// 只在类实现中这样写,类声明中不写成员列表初始化
RatedPlayer::RatedPlayer(unsigned int r,const string & fn,const string & ln,bool ht):TableTennisPlayer(fn,ln,ht)
{
rating = r;
}

上述实例代码,:TableTennisPlayer(fn,ln,ht)是成员初始化列表。它是可执行代码,调用TableTennisPlayer构造函数。通过成员初始化列表,使用基类的构造函数为基类的私有成员进行赋值。

先创建基类对象(通过成员初始化列表的方式,隐式创建),在通过派生类构造函数创建派生类对象

2.省略成员初始化列表

若省略成员初始化列表,实例代码如下:

1
2
3
4
RatedPlayer::RatedPlayer(unsigned int r,const string & fn,const string & ln,bool ht)
{
rating = r;
}

必须首先创建基类对象,若不调用基类构造函数(即使不使用成员初始化列表方式调用基类的构造函数),程序也将使用默认基类构造函数,以下代码与上述等价:

1
2
3
4
5
// 等价于调用默认的构造函数
RatedPlayer::RatedPlayer(unsigned int r,const string & fn,const string & ln,bool ht):TableTennisPlayer()
{
rating = r;
}

除非使用默认的构造函数,否则应该显示的调用正确的基类构造函数

1
2
3
4
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer & tp):TableTennisPlayer(tp)
{
rating = r;
}

tp的类型是 TableTennisPlayer & ,因此将调用基类的复制构造函数,基类若没有定义复制构造函数,编译器将自动生成一个。

若愿意,也可以使用成员初始化列表对派生类成员进行初始化,代码实例如下:

1
2
3
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer & tp):TableTennisPlayer(tp),rating(r)
{
}

3.要点

1
2
3
首先创建基类对象
派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
派生类构造函数应初始化派生类新增的数据成员

注意:创建派生类对象时,程序首先调用基类构造函数,再调用派生类构造函数,基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类构造函数总是调用几个基类的构造函数。可以使用初始化列表的语法指明要使用的基类构造函数,否则将使用默认的基类构造函数

派生类对象过期(应该被释放时),程序首先调用派生类的析构函数,然后再调用基类的析构函数

十六、派生类与基类之间的关系

  • 派生类可以使用基类的方法,前提是方法非私有

  • 基类的指针与引用均可以不在显示转换的情况下指向派生类对象,但是基类指针与方法均只能调用基类的方法。**(单向)**,派生类新增的方法与成员基类的指针与引用无法调用

    • // ball 是 basketball 的基类
      basketball object("vs");
      ball *f_object1 = &object; // 基类指针指向派生类对象
      ball &f_object2 = object;  // 基类引用指向派生类对象
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      * 可以将派生类对象赋给基类对象**(会使用隐式的重载运算符)**

      * ```c
      // ball 是 basketball 的基类
      basketball object("vs");
      ball f_object1 = object; // 派生类对象赋值给基类对象

      // 这种情况,会使用隐式的重载运算符
      ball &f_object1 = (const ball &) object; // 基类引用指向派生类对象,object的基类部分,将被复制给f_object1
  • 在派生类中若定义了与基类相同名称的方法(重写),则需要通过::作用域解析运算符来调用基类的方法

    • // ball 是 basketball 的基类
      // 若基类与派生类中均定义了 view()方法,在派生类中想使用基类的方法,则需要使用 基类名称::方法名称 调用基类方法
      ball::view()
      
      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



      ## 十七、虚方法

      在类的类声明的函数原型中通过`virtual`关键字可以将函数进行**虚函数**的声明。**`virtual`关键词只用于类声明的方法原型中**,同友元函数的关键词`friend`

      **在基类中将派生类会重新定义的方法,声明为虚方法,方法在基类中被声明为虚方法之后,在派生类中不用指明关键字 virtual 也会自动成为虚方法**,好习惯,还是将**派生类中的虚方法通过`virtual`声明出来,增加代码的可读性**

      **基类声明一个虚的析构函数,这样可以确保释放对象时,按照正确的顺序调用析构函数**

      ### 1.重点

      **利用基类的指针与引用指向派生类对象,若方法没有声明为`virtual` 虚方法,程序将根据指针或者引用的类型去选择方法;若使用了`virtual`声明为了虚方法,程序则会根据指针或者引用则会根据指向对象的类型来选择方法**

      * 若`view()`没有被声明为虚方法,`view()`方法在基类与派生类中均有定义

      * ```c
      // ball 是 basketball 的基类
      basketball object("vs");
      ball opp("str");
      ball &f_object1 = opp; // 基类引用指向基类对象
      ball &f_object2 = object; // 基类引用指向派生类对象

      ball.view(); // 引用类型为基类引用,因此均是调用基类中定义的 view() 方法
      ball.view(); // 引用类型为基类引用,因此均是调用基类中定义的 view() 方法
  • view()被声明为虚方法,view()方法在基类与派生类中均有定义

    • // ball 是 basketball 的基类
      basketball object("vs");
      ball opp("str");
      ball &f_object1 = opp; // 基类引用指向基类对象
      ball &f_object2 = object;  // 基类引用指向派生类对象
      
      ball.view(); // 引用类型为基类引用,引用指向的对象为基类对象,因此使用基类的方法
      ball.view(); // 引用类型为基类引用,引用指向的对象为派生类对象,因此使用派生类的方法
      
      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

      ### 2.虚函数注意事项

      * 若定义的类被用作基类,则应该将那些**派生类中重新定义的方法声明为虚函数**
      * **构造函数**不能为虚函数
      * **析构函数应当是虚函数,除非类不做基类**,即使类不做基类,将其析构函数定义为虚函数也没有问题
      * **友元函数不能为虚函数,因为友元函数不是成员函数,为非成员函数**



      ## 十八、为何需要虚的析构函数

      **若析构函数不是虚函数**,通过一个基类指针/引用指向派生类对象时,当基类指针/引用被释放时,则将只调用对应于指针或者引用类型的析构函数。,这意味着只有基类的对象被调用,即使指向了一个派生类对象。

      **若析构函数为虚函数**,通过一个基类指针/引用指向派生类对象时,当基类指针/引用被释放时,则将先调用派生类即**指针/引用指向对象的析构函数**,然后**自动调用基类的析构函数**即指针/引用类型的析构函数。

      **总结:**使用虚的析构函数,可以确保正确的析构函数序列被调用。**好习惯:在基类中声明虚的析构函数**





      ## 十九、抽象基类(纯虚函数)

      抽象基类**(ABC)**包含其所有派生类的共有(共性)部分

      C++通过纯虚函数来提供为实现的函数,**纯虚函数在结尾处为=0**

      ```c
      virtual double Area()=0; // 纯虚函数

若一个类需要成为抽象基类(ABC)则该基类中必须包含一个纯虚函数,含有纯虚函数的类(ABC)只能做基类,无法创建该类对象。但是即使该基类为抽象的,我们仍可以在实现文件中提供方法的定义

二十、类模板

1.创建类模板与模板函数分为以下三步:

  • 模板类—如下代码打头:template<class Type>
  • 每个函数头都将以相同的模板声明打头:template<class Type>
  • 最后需要将类限定符 从 类名:: 改为 类名<Type>::

重点:

这些模板不是类和成员函数的定义,他们是c++编译器指令,说明了如何生成类和成员函数定义,模板的具体实现被称为实例化或者具体化。不能将模板成员函数放在独立的实现文件中,由于模板不是函数,他们不能单独编译. 模板必须与特定的模板实例一起使用,最简单的方法是:将所有模板信息放在一个头文件中

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class Type>
class Stack
{
private:
Type item[5];
public:
bool pop(Type & item);
}

// 函数实现
template <class Type>
bool Stack<Type>::pop(Type & item)
{

}

2.模板隐式实例化、显示实例化、具体化

隐式实例化:给模板传入特定的类型参数并创建类对象时,会同时生成类声明,该操作在程序运行阶段生成类定义

显示实例化:使用template打头,并给模板传入特定的类型参数(无需创建类对象)。该操作在程序编译阶段产生类声明

具体化:给模板使用具体的类型参数生成类声明。显/隐实例化均通过具体的类型生成类声明。实例化属于具体化

类模板与函数模板很相似,均可以有隐式实例化、显示实例化和显示具体化,都统称为具体化,模板以泛型的方式描述类,而具体化使用具体的类型生成类声明

模板仅仅描述类的样子并无类的定义与声明,而通过具体化可生成类声明

二十一、异常

1.调用abort()函数

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
#include <iostream>
#include <cstdlib>
using namespace std;

double hmean(double a,double b);


int main() {
double x,y,z;
cout << "Enter 2 number:" << endl;
while(cin>>x>>y)
{
z = hmean(x,y);
cout << "Harmonic mean of " << x << " and " << y << " is " << z <<endl;
cout << "Enter next set of number <q to quit>: ";
}
cout << "Bye!\n";
return 0;
}

double hmean(double a,double b)
{
if(a==-b) // 若当前 a==-b则会出现除数为0的情况
{
cout << " untenable arguments to hmean()" << endl;
abort();
}
return 2*a*b / (a+b);
}

abort()函数会在出现a==-b的情况时会直接中止程序,程序运行阶段错误

运行结果:

1
2
3
4
5
6
/home/zxz/c++/demo/9/cmake-build-debug/9
Enter 2 number:
10 -10
untenable arguments to hmean()

进程已结束,退出代码为 134 (interrupted by signal 6: SIGABRT)

2.异常机制