C++快速入门(一)

C++ 是一门面向对象的语言,即 OO 思想。

1. 程序示例

首先,可以在之后的代码中经常看到 using namespace std; ,这是为了调用一个叫做命名空间的东西。

C++标准库使用的所有标识符(即类、函数、对象等的名称)都是在同一个特殊的名字空间(std)中来定义的。

如果不使用上述声明,则需要在用到每个名字空间中的标识符时,在标识符名称之前加上 std::

1.1 示例一

题目:任意输入数字和空格,返回整数之和。

C 语言实现:

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 <stdio.h>

int main(void) {
int i;
int sum = 0;
char ch;

printf("请输入一串整数和任意数目的空格:");

while ( scanf("%d", &i) == 1 ) {
sum += i;

while ( (ch = getchar()) == ' ' ) ; // 屏蔽空格

if ( ch == '\n' ) {
break;
}

ungetc( ch, stdin );
}

printf("结果是:%d\n", sum);

return 0;
}

C++实现

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

using namespace std; // 声明命名空间

int main() {
int sum = 0;

cout << "请输入一串整数和任意数目的空格:";

int i;
while ( cin >> i ) {
sum += i;
while ( cin.peek() == ' ' ) cin.get(); // .peek()即缓冲区中的第一个字符。 .get()获取一个字符
if ( cin.peek() == '\n' ) break;
}

cout << "结果是:" << sum << endl;

return 0;
}

“>>” 在 C 中定义为右移操作符,它在 C++ 中进行了重载,当它按照这里所示的方法使用时,它就用于从输入流对象提取信息。

该操作符对所有内键的数据类型都进行了重载,所以它可以从输入流对象提取出 int,float,double 型数据,也可以提取字符串等数据。

1.2 示例二

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

using namespace std;

int main() {
char buf[20]; // 最多读取 19 个字符

cin.ignore(7); // 忽略前七个字符
cin.getline(buf, 10); // 取出 10 个字符

cout << buf << endl; // 按字符串输出,所以只能输出 9 个字符

return 0;
}

1.3 示例三

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

using namespace std;

int main() {
const int SIZE = 50;
char buf[SIZE];

cout << "请输入一段文本:";
cin.read(buf, 20);

cout << "字符串收集到的字符数为:"
<< cin.gcount() << endl;

cout << "输入的文本信息是:";
cout.write(buf, 20);
cout << endl;

return 0;
}

1.4 示例四

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

using namespace std;

int main() {
double result = sqrt(3.0);

cout << "对 3 开方保留小数点后 0~9 位,结果如下:\n" << endl;

for (int i = 0; i <= 9; i++) {
cout.precision(i);
cout << result << endl;
cout << "当前的输出精度为:" << cout.precision() << endl;
}

return 0;
}

1.5 示例五

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

using namespace std;

int main() {
int width = 4;
char str[20];

cout << "请输入一段文本:\n";
cin.width(5);

while (cin >> str) {
cout.width(width++);
cout << str << endl;
cin.width(5);
}

return 0;
}

2. 文件操作

2.1 拷贝文件

copy 源文件 目的文件名

2.2 C 语言形式,检查文件 copy 输入是否正确

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
#include <stdio.h>
#include <stdlib.h>

int main( int argc, char *argv[] ) {
FILE *in, *out; // 声明两个文件指针,分别作为 I/O 流对象使用
int ch;

if ( argc != 3 ) { // 如果输入参数不是三个,报出错误提示
fprintf(stderr, "输入形式:copyFile 源文件名 目标文件名\n");
exit( EXIT_FAILURE );
}

if ( (in = fopen(argv[1], "rb") ) == NULL ) { // 以二进制的形式按可读方式打开文件并返回文件指针给 in,为确保正确打开,检查返回值
fprintf(stderr, "打不开文件:%s \n", argv[1]);
exit( EXIT_FAILURE );
}

if ( ( out = fopen( argv[2], "wb" ) ) == NULL ) { // 以二进制的形式按可写方式打开文件并返回文件指针给 out
fprintf( stderr, "打不开文件:%s \n", argv[2] );
fclose( in ); // 记得擦屁股
exit( EXIT_FAILURE );
}

while ( ( ch = getc( in ) ) != EOF ) { // 由于 getc 的返回值是 int 型,所以 ch 定义为 int
if ( putc( ch, out ) == EOF ) {
break;
}
}

if ( ferror( in ) ) {
printf("读取文件 %s 失败!\n", argv[2]);
}

printf("成功复制 1 个文件!\n");

fclose( in );
fclose( out );

return 0;
}

stderrstdin 类似,不过它指向的是屏幕这个“文件”,专门用来报错。

fprintf(stderr, "输入形式: copyfile 源文件名 目标文件名")

这行代码相当于 printf("输入形式: copyfile 源文件名 目标文件名")

argcargv[]

在程序中,main 函数有两个参数,整数变量 argc 和字符指针数组 argv[]

argc 的含义是程序的参数数量,包含本身。

argv[] 的每个指针指向命令行的一个字符串,所以 argv[0] 指向字符串 "copyFile.exe"argv[1] 指向字符串 sourceFile,argv[2] 指向字符串 destFile 。

ps: 感觉上面这种码风好好看,代码看起来胖胖的,条件语句的括号里前后加空格结构也变清晰了,向这个方向改进。

2.3 fstream

  • 从文件读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <fstream>
#include <iostream>

using namespace std;

int main() {
ifstream in;

in.open( "test.txt" );
if ( !in ) {
cerr << "打开文件失败" << endl;
return 0;
}

char x;
while ( in >> x ) {
cout << x;
}

cout << endl;
in.close();

return 0;
}

一开始没有创建 test.txt 显示打开失败。创建后,将文本内容读取出。

  • 向文件写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <fstream>
#include <iostream>

using namespace std;

int main() {
ofstream out;

out.open( "test.txt" );
if ( !out ) {
cerr << "打开文件失败!" << endl;
return 0;
}

for ( int i = 0; i < 10; i++ ) {
out << i;
}

out << endl;
out.close();

return 0;
}

可以看到文件正常写入了,并且之前的内容被覆盖了。

2.4 fstream 续

1
2
ifstream in("test.txt");
ofstream out("test.txt");

如上,可以在创建 ifstreamofstream 类的对象时,将文件的名字传递给它们的构造函数(对象默认使用的函数方法)。

此用法和前面的用法没什么区别,事实上,它还可以接收不知一个参数!

1
ifstream in(char* filename, int open_mode);

其中 filename 表示文件名称,open_mode 表示打开模式。

下面给出几种常见的打开模式:

1
2
3
4
5
6
7
ios::in // 打开一个可读取文件
ios::out // 打开一个可写入文件
ios::binary // 以二进制的形式打开一个文件
ios::app // 写入的所有数据将被追加到文件的末尾
ios::trunk // 删除文件原来已存在的内容
ios::nocreate // 如果要打开的文件并不存在,那么此参数调用 open 函数将无法进行
ios::noreplace // 如果要打开的文件已存在,试图用 open 函数打开时返回一个错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <fstream>
#include <iostream>

using namespace std;

int main() {
ofstream out;

out.open( "test.txt", ios::app ); // 在结尾追加的方式
if ( !out ) {
cerr << "打开文件失败!" << endl;
return 0;
}

for ( int i = 10; i > 0; i-- ) {
out << i;
}

out << endl;
out.close();

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

using namespace std;

int main() {
fstream fp( "test.txt", ios::in | ios:: out );
if (!fp) {
cerr << "打开文件失败!" << endl;
return 0;
}

fp << "Iloveawellfrog.cc!";

static char str[50];

fp.seekg(ios::beg); // 使得文件指针指向文件头。 ios::end 则时文件尾。
fp >> str;
cout << str << endl;

fp.close();

return 0;
}

为什么 | 连接的两种打开模式可以同时执行呢?

这是由于底层定义方式导致的:

可以看到底层定义是个枚举类型,每种类型只检查对应位是否为 1,如果同时多个位为 1 ,则同时执行多种方式。

光看不练没有长进滴!

2.4.1 练习1

题目:程序向用户提出一个“Y/N”问题,然后把用户输入的值赋给 answer 变量。

要求用户输入大小写均可。

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>

int main() {
char answer;

std::cout << "请问我可以格式化你的硬盘吗?!\n"; // 没有 using namespace std; 是为了更安全的使用不同的库
std::cin >> answer;

switch (answer) {
case 'Y':
case 'y':
std::cout << "随便格式化硬盘你是会后悔滴~\n";
break;
case 'N':
case 'n':
std::cout << "你的选择是明智的!\n";
break;
default:
std::cout << "你输入的啥玩意,我根本不认识?!\n";
break;
}

std::cin.ignore(100, '\n'); // 其后的100个输入字符,不是回车全部忽略,不加该句,则读取前面的回车直接结束
std::cout << "输入任意操作符结束" << std::endl;
std::cin.get(); // 获取一个字符才结束

return 0;
}

3. 函数的重载(overloading)

函数重载: 实质上就是同样名字再定义一个有着不同参数同样用途的函数。

注意:可以是参数 个数 的不同,也可以是参数 类型 的不同。

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

using namespace std;

void myPrint(int num);
void myPrint(float num); // 一定要提前声明,否则编译器执行时,不知道还有一个同名的重载函数

int main() {
int numInt = 3;
float numFloat = 5.1;

myPrint(numInt);
myPrint(numFloat);

return 0;
}

void myPrint(int num) {
cout << num << endl;
}

void myPrint(float num) { // 函数名相同,返回值相同,传入的参数形式不同,此之谓重载!
cout << num << endl;
}

对函数的重载,有时可以像上面的例子一样,简化变成工作、提高代码可读性。

有些点需要注意:

  • 程序中不能随意对函数(方法)进行重载。
  • 重载函数越多,该程序就越不容易看懂。
  • 注意区分重载和覆盖。(覆盖就是函数名相同,传入参数也相同。)
  • 对函数进行重载的目的是为了方便对不同数据类型进行同样的处理。

思考:为什么不能通过返回值不同来重载函数(方法)呢?

答:虽然返回值不同可以区分函数,但是在 main() 函数之前的声明是相同的,这会让编译器懵逼。。。

4. 复杂数据类型

下面这个例子主要学习一下输入保护,防止用户违法输入:

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

int main() {
const unsigned short ITEM = 10;
int num[ITEM];

std::cout << "请输入" << ITEM << "个整数数据!\n\n";

for (int i = 0; i < ITEM; i++) {
std::cout << "请输入第" << i + 1 << "个数据:";

while ( !(std::cin >> num[i]) ) { // 为了防止违法输入,这里做了保护
std::cin.clear(); // 先清空缓冲区
std::cin.ignore(100, '\n');
std::cout << "请输入一个合法的值" << std::endl;
}
}

return 0;
}

C++ 中有字符串数据类型 string

可以替代 C 中的字符数组,使用起来更方便。

还有一些内置的方法函数:

  • 提取子字符串
  • 比较字符串
  • 添加字符串
  • 搜索字符串
  • 等等
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>

int main() {
std::string str;
std::cout << "请随意输入一个字符串:";

// std::cin >> str; // 遇到空格即结束
std::getline(std::cin, str); // 遇到回车才结束
std::cout << str << std::endl;

return 0;
}

指针

对齐: 在计算机底层,对齐比比皆是。内存对齐,文件对齐。(程序在编译链接之后会被分割成一个一个的区块,而区块在文件和内存中要按照一定规律来对齐)

  • 一般 32 位系统内存对齐值是 : 1000H == 4KB(一页)
  • 一般 64 位系统内存对齐值是 : 2000H == 8KB
  • 文件对齐值是:200H(文件中存放着文件内容,不需要立即提交,为了节省内存,所以存放的更紧凑。)

关于指针的声明:

1
2
int *p1, p2, p3; // 一个指针变量和两个整形变量
int *p1, *p2, *p3; // 三个指针变量

建议不要怕多写几行,多出来的回车和空格会被编译器优化掉,不会影响程序内存的大小。

思考:既然指针类型的大小和 int 类型一样,为什么不直接用 int 类型存放地址,而要专门创建一个指针类型呢?

1、直接访问硬件

2、快速传递数据(指针表示地址)

3、返回一个以上的值返回一个(数组或者结构体的指针)

4、表示复杂的数据结构(结构体)

5、方便处理字符串

结构体指针示例

https://fishc.com.cn/thread-11989-1-1.html

5. 传值、传址、传引用

传值: 函数直接传递值,是形参,不会在调用的函数中修改原来的数值。

传址: 为了在调用函数中修改原值,使用指针传递地址。

传引用: 不使用指针,修改原值。(从汇编来看,还是传递地址。)

传引用实例:

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

using namespace std;

void swap(int &a, int &b);

int main() {
int a = 1;
int b = 3;

swap(a, b);
cout << a << " " << b << endl;

return 0;
}

void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}

栈溢出示例

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
char array[10];
scanf("%s", array);
printf("%s\n", array);

return 0;
}

可以看到,程序运行出现了问题。这是因为栈的空间分配是自上而下的,输入的字符串超过了 array 的长度,就覆盖了 main 函数存储的地址,return 到了被覆盖的奇怪地址,所以程序崩溃了。

6. 对象

类由变量和函数组成,对象将使用那些变量(属性)来存储信息,调用那些函数(方法)来完成操作。

给类添加方法:

  1. 先在类的声明里创建一个方法的原型
  2. 稍后再实现这个方法
1
2
3
4
5
6
7
8
9
10
class Car {
public:
std::string color;
std::string engine;
float gas_tank;
unsigned int Wheel;

void fill_tank(float liter);
// 方法的声明:方法是“fill_tank” ,参数是 "liter"
};

上面的类中只声明了方法,要想使用该方法,还需要对这个函数进行正式的定义。方法的定义通常安排在类的声明后面。

1
2
3
void Car::fillTank(float liter) {
gas_tank += liter;
}
  • 作用域解析符 :: ,作用是告诉编译器这个方法属于哪一个类。
  • std::cout 其实就是在使用对象。

举个例子:

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
#include <iostream>
#include <windows.h>

#define FULL_GAS 85

class Car {
public:
std::string color;
std::string engine;
unsigned int gas_tank;
unsigned int wheel;

void setColor(std::string col);
void setEngine(std::string eng);
void setWheel(unsigned int whe);
void fillTank(int liter);
int running(void);
void warning(void);
};

void Car::setColor(std::string col) {
color = col;
}

void Car::setEngine(std::string eng) {
engine = eng;
}

void Car::setWheel(unsigned int whe) {
wheel = whe;
}

void Car::fillTank(int liter) {
gas_tank += liter;
}

int Car::running(void) {
std::cout << "我正在以“全力以赴”的时速往前移动。。。跨过山和大海奔向美好的未来!\n";
gas_tank--;
std::cout << "当前还剩 " << 100 * gas_tank / FULL_GAS << "%" << "油量!\n";

return gas_tank;
}

void Car::warning(void) {
std::cout << "WARNING!!" << "还剩" << 100 * gas_tank / FULL_GAS << "%" << "油量!";
}

int main() {
char i;
Car mycar;

mycar.setColor("WHITE");
mycar.setEngine("V8");
mycar.setWheel(4);

mycar.gas_tank = FULL_GAS;

while (mycar.running()) {
if (mycar.running() < 10) {
mycar.warning();
std::cout << "请问是否需要加满油再继续行驶?(Y/N)\n";
std::cin >> i;
if ('Y' == i || 'y' == i) {
mycar.fillTank(FULL_GAS);
}
}
}

return 0;
}

7. 构造器和析构器

构造器

构造器是一个特殊的方法,常常用来初始化一些类的属性。

构造器和通常方法的主要区别:

  • 构造器的名字和它所在的类的名字一样
  • 系统在创建某个类的实例时会第一时间自动调用这个类的构造器
  • 构造器永远不会返回任何值

创建构造器,需要先把它的声明添加到类里

注意大小写与类名保持一致,在结束声明之后开始定义构造器本身

1
2
3
4
5
6
7
8
9
10
class Car {
Car(void);
}

Car::Car(void) { // 不需要声明返回值类型,因为没有返回值
color = "WHITE";
engine = "V8";
wheel = 4;
gas_tank = FULL_GAS;
}

修改上面的例子:

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
#include <iostream>
#include <windows.h>

#define FULL_GAS 85

class Car {
public:
std::string color;
std::string engine;
unsigned int gas_tank;
unsigned int wheel;

Car(void);
void setColor(std::string col);
void setEngine(std::string eng);
void setWheel(unsigned int whe);
void fillTank(int liter);
int running(void);
void warning(void);
};

Car::Car(void) {
color = "While";
engine = "V8";
wheel = 4;
gas_tank = FULL_GAS;
}

void Car::setColor(std::string col) {
color = col;
}

void Car::setEngine(std::string eng) {
engine = eng;
}

void Car::setWheel(unsigned int whe) {
wheel = whe;
}

void Car::fillTank(int liter) {
gas_tank += liter;
}

int Car::running(void) {
char ch;

std::cout << "我正在以“全力以赴”的时速往前移动。。。跨过山和大海奔向美好的未来!\n";
gas_tank--;
std::cout << "当前还剩 " << 100 * gas_tank / FULL_GAS << "%" << "油量!\n";

if (gas_tank < 10) {
std::cout << "请问是否需要加满油再行驶?(Y/N)\n";
std::cin >> ch;
if ('Y' == ch || 'y' == ch) {
fillTank(FULL_GAS);
}

if (0 == gas_tank) {
std::cout << "抛锚中。。。。。。";
return 1;
}
}

return 0;
}

void Car::warning(void) {
std::cout << "WARNING!!" << "还剩" << 100 * gas_tank / FULL_GAS << "%" << "油量!";
}

int main() {
Car mycar;

while (!mycar.running()) {
;
}

return 0;
}

每个类至少有一个构造器,如果没有在类中定义构造器,编译器就会使用如下语法替你定义一个:ClassName::ClassName(){} 这是一个没有代码内容的空构造器。

除此之外,编译器还会替你创建一个副本构造器(CopyConstructor)。

析构器

构造器用来完成事先的初始化和准备工作(申请分配内存)。

析构器用来完成事后所必须的清理工作(清理内存)。

析构器和构造器相辅相成:

  • 析构器有着和构造器(类)一样的名字, 只不过前面多了一个波浪符 ~ 前缀。

  • 析构器永远不返回任何值

  • 析构器是不带参数的,所以声明格式为 ~ClassName();

  • 刚刚的例子中析构器可有可无。但是在较复杂的类中,析构器往往至关重要,可能由于类的空间没有释放,而被占用无法访问(内存泄漏)。

    例如,某个类的构造器申请了一块内存,我们就必须在析构器里释放那块内存。

1
2
3
4
class Car {
Car(void);
~Car();
}

举个例子:

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 <string>
#include <fstream>

class StoreQuote {
public:
std::string quote, speaker;
std::ofstream fileOutput;

StoreQuote();
~StoreQuote();

void inputQuote();
void inputSpeaker();
bool write();
};

StoreQuote::StoreQuote() {
fileOutput.open("test.txt", std::ios::app); // 以追加的方式打开文件
}

StoreQuote::~StoreQuote() {
fileOutput.close();
}

void StoreQuote::inputQuote() {
std::getline(std::cin, quote);
}

void StoreQuote::inputSpeaker() {
std::getline(std::cin, speaker);
}

bool StoreQuote::write() {
if (fileOutput.is_open()) {
fileOutput << quote << "|" << speaker << "\n";
return true;
} else {
return false;
}
}

int main() {
StoreQuote quote;

std::cout << "请输入一句名言:\n";
quote.inputQuote();

std::cout << "请输入作者:\n";
quote.inputSpeaker();

if (quote.write()) {
std::cout << "成功写入文件^_^";
} else {
std::cout << "写入文件失败T_T";
return 1;
}

return 0;
}

运行成功,在文件中就可以看到写入的内容了。

8. this 指针和类的继承

this 指针

对象中有一个特殊的指针叫 this ,下面通过一个例子认识它:

1
2
3
4
5
6
7
8
class Human {
char awellfrog;
Human(char awellfrog);
};

Human::Human(char awellfrog) {
awellfrog = awellfrog;
}

可以看到 Human() 构造器有一个名为 awellfrog 的参数。这里虽然名字和 Human 类里的属性同名,却是不相干的两样东西,所以并没有报错。问题是构造器不知道是覆盖属性还是覆盖参数。这时就要用到 this 指针了:

1
this->awellfrog = awellfrog;

现在编译器就明白了,赋值操作符的左边将被解释为当前对象的 awellfrog 属性,右边将被解释为构造器的传入参数 awellfrog

注意: 使用 this 指针的基本原则是:如果代码不存在二义性的隐患,就不必使用 this 指针!

this 指针也会在一些更加高级的方法里用到。

类的继承

通过继承机制,程序员可以对现有的代码进行进一步的扩展,并应用在新的程序中。

编写一个 Animal 类作为 Turtle 类和 Pig 类的基类。

  • 基类:可以派生出其他的类,也称为父类或者超类。

    比如这里的 Animal 类是基类

  • 子类:是从基类派生出来的类,比如这里的 Turtle 类和 Pig 类是子类。

  • 子类共有的动作和特征,定义为基类的方法和属性。子类特有的部分,在子类中定义。

继承在 C++ 中的写法如下:

1
2
3
class SubClass : public SuperClass {...}
e.g.
class Pig : public Animal {...}

举例如下:

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

class Animal {
public:
std::string mouth;

void eat();
void sleep();
void drool();
};

class Pig : public Animal {
public:
void climb();
};

class Turtle : public Animal {
public:
void swim();
};

void Animal::eat() {
std::cout << "I'm eatting!" << std::endl;
}

void Animal::sleep() {
std::cout << "I'm sleeping! Don't disturb me!" << std::endl;
}

void Animal::drool() {
std::cout << "公的看到母的流口水。。。(小黄鱼这个例子真绝)" << std::endl;
}

void Pig::climb() {
std::cout << "母猪会上树" << std::endl;
}

void Turtle::swim() {
std::cout << "乌龟会游泳" << std::endl;
}

int main() {
Pig pig;
Turtle turtle;

pig.eat();
turtle.eat();
pig.climb();
turtle.swim();

return 0;
}

9. 继承机制中的构造器和析构器

当使用继承机制时,构造器和析构器就变得有点复杂了。

比如基类有个构造器,他将在创建子类对象时最先被调用,然后才调用子类的构造器。因为基类必须在子类之前初始化!

如果构造器带着输入参数,事情就变得稍微复杂了。

  • 当调用 Pig() 构造器时(以 theName 作为输入参数),Animal() 构造器也将被调用(theName 输入参数将传递给它)。
  • 当调用 Pig pig("小猪猪") 时,把字符串“小猪猪”传递给 Pig()Animal() ,赋值动作将实际发生在 Animal() 方法里。
1
2
3
4
5
6
7
8
9
10
class Animal {
public:
Animal(std::string theName);
std::string name;
}

class Pig : public Animal {
public:
Pig(std::string theName);
}

具体实例如下:

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

class Animal {
public:
std::string mouth;
std::string name;

Animal(std::string theName);
void eat();
void sleep();
void drool();
};

class Pig : public Animal {
public:
void climb();
Pig(std::string theName);
};

class Turtle : public Animal {
public:
void swim();
Turtle(std::string theName);
};

Animal::Animal(std::string theName) {
name = theName;
}

void Animal::eat() {
std::cout << "I'm eatting!" << std::endl;
}

void Animal::sleep() {
std::cout << "I'm sleeping! Don't disturb me!" << std::endl;
}

void Animal::drool() {
std::cout << "公的看到母的流口水。。。(小黄鱼这个例子真绝)" << std::endl;
}

Pig::Pig(std::string theName) : Animal(theName) {}

void Pig::climb() {
std::cout << "母猪会上树" << std::endl;
}

Turtle::Turtle(std::string theName) : Animal(theName) {}

void Turtle::swim() {
std::cout << "乌龟会游泳" << std::endl;
}

int main() {
Pig pig("小猪猪");
Turtle turtle("甲鱼");

std::cout << "这只猪的名字是:" << pig.name << std::endl;
std::cout << "每只乌龟都有个伟大的名字:" << turtle.name << std::endl;

pig.eat();
turtle.eat();
pig.climb();
turtle.swim();

return 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
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <string>

class BaseClass {
public:
BaseClass();
~BaseClass();

void doSomething();
};

class SubClass : public BaseClass {
public:
SubClass();
~SubClass();
};

BaseClass::BaseClass() {
std::cout << "进入基类构造器。。。\n";
std::cout << "我在基类构造器里边做事...\n\n";
}

BaseClass::~BaseClass() {
std::cout << "进入基类析构器。。。\n";
std::cout << "我在基类析构器里做事...\n\n";
}

void BaseClass::doSomething() {
std::cout << "我干了某些事。。。。。。\n\n";
}

SubClass::SubClass() {
std::cout << "进入子类构造器...\n";
std::cout << "我在子类构造器里做事。。。\n\n";
}

SubClass::~SubClass() {
std::cout << "进入子类析构器...\n";
}

int main() {
SubClass subclass;
subclass.doSomething();

std::cout << "完工,收工!\n";

return 0;
}

运行程序,可以看到基类与子类中构造器与析构器的调用过程如下:

10. 访问控制

关于构造器的设计越简明越好!我们应该只用它来初始化各种有关的属性。

作为一个基本原则,在设计、定义和使用一个类的时候,应该让它的每个组成部分简单到不能再简单!

别忘了,析构器的基本用途是对前面所做的事情进行清理。尤其是在使用了动态内存的程序里,析构器将至关重要。

C++ 的访问级别:

如果一个对象试图访问它没有权限的内容,编译器会报错。

级别 允许谁来访问
public 任何代码
protected 这个类本身和它的子类
private 只有这个类本身

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 按照前面的例子,name 设置为 public ,可以直接修改

int main() {
'''
Pig pig("小猪猪");
Turtle turtle("甲鱼");

pig.name = "小姑凉";

std::cout << "这只猪的名字是:" << pig.name << std::endl;
std::cout << "每只乌龟都有个伟大的名字:" << turtle.name << std::endl;
'''
}

我们可以看到,漂亮的小姑凉怎么能是 pig 的名字呢,这样是不行的,于是我们对 name 属性设置为 protected

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
public:
std::string mouth;

Animal(std::string theName);
void eat();
void sleep();
void drool();

protected:
std::string name;
};

再运行,我们就可以看到这里报错了,说 name 属性被保护了。

使用 private 的好处是,今后只修改某个类的内部实现,而不必重新修改整个程序。这时因为其他代码根本访问不到 private 保护的内容,所以不怕“牵一发而动全身”的惨剧发生!

同一个类定义里可以使用多个 public: , private: , protected: 语句,但最好把同类型的元素集中在一起,这样代码的可读性会好很多。

在编写类定义代码时,应该从 public: 开始写起,然后是 protected: ,最后是 private: 。

虽然编译器并不挑剔这些顺序,但这么做的好处是——好的顺序可以节省大量的时间。

11. 覆盖方法和重载方法

覆盖(基类声明,子类中重新声明并修改)

光有继承在有些场景下是不够用的。例如当我们在基类里提供了一个通用的函数,但是它的某个子类里需要修改这个方法的实现,在 C++ 里,覆盖(overriding) 就可以做到。

例如,虽然动物都会吃,但是不同动物会吃不同的东西,修改之前的代码如下:

只需要在类里重新声明这个方法,然后再改写一下它的实现代码(就像它是一个增加的方法那样)就行了。

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
class Animal {
public:
std::string mouth;

Animal(std::string theName);
void eat();
void sleep();
void drool();

protected:
std::string name;
};

class Pig : public Animal {
public:
Pig(std::string theName);
void climb();
void eat(); // new
};

class Turtle : public Animal {
public:
Turtle(std::string theName);
void swim();
void eat(); // new
};

'''

void Pig::eat() {
Animal::eat(); // 打印基类的实现
std::cout << name << "正在吃鱼!\n\n"; // 修改的新方法!
}

'''

void Turtle::eat() {
Animal::eat();
std::cout << name << "正在吃东坡肉!\n\n"; // new
}

可以看到子类中的方法覆盖了基类的方法。

重载(定义位置相同,名字相同,输入参数不同,直接继承,不用再子类中再次声明)

重载机制使你可以定义多个同名的方法(函数),只是它们的输入参数必须不同。(因为编译器是依靠不同的输入参数来区分不同的方法)。

重载可以简化编程工作,提高代码可读性。

重载并不是真正的面向对象特性,它只是可以简化编程工作的捷径,而简化编程工作正是 C++ 全部追求。

看一个重载的例子:

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
'''
class Animal {
public:
std::string mouth;

Animal(std::string theName);
void eat();
void eat(int eatCount); // new
void sleep();
void drool();

protected:
std::string name;
};

class Pig : public Animal {
public:
Pig(std::string theName);
void climb();
// 直接继承,不需要重新声明以覆盖
};

class Turtle : public Animal {
public:
Turtle(std::string theName);
void swim();
};

'''
void Animal::eat() {
std::cout << "I'm eatting!" << std::endl;
}

void Animal::eat(int eatCount) {
std::cout << "我吃了" << eatCount << "混沌" << std::endl;
}
'''

int main() {
Pig pig("小猪猪");
Turtle turtle("甲鱼");

pig.eat(15); // 根据输入一个 int 参数判断调用重载的基类方法
pig.eat(); // 没有输入,直接调用无输入的基类方法
turtle.eat();
pig.climb();
turtle.swim();

return 0;
}

1. 一定要合理使用重载,重载越多,程序就越不容易看懂。

2. 在对方法进行覆盖时,一定要看仔细,千万不要把输入参数打多或者打少了,因为只要声明的输入参数和返回值与原来的不一致,你编写的就是一个重载方法而不是覆盖方法。这种错误往往很难调!!!因为没有语法错误。

子类只能覆盖,不能重载基类的内容:

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
// 关键修改部分如下
'''
class Animal {
public:
std::string mouth;

Animal(std::string theName);
void eat(); // 基类声明
void sleep();
void drool();

protected:
std::string name;
};

class Pig : public Animal {
public:
Pig(std::string theName);
void climb();
void eat(int eatCount); // 子类中覆盖
};
'''
void Animal::eat() {
std::cout << "I'm eatting!" << std::endl;
}
'''
void Pig::eat(int eatCount) { // 子类中覆盖
std::cout << "我吃了" << eatCount << "混沌" << std::endl;
}
'''
int main() {
Pig pig("小猪猪");
Turtle turtle("甲鱼");

// protected 下在子类之外用不了基类的 name 属性
// std::cout << "这只猪的名字是:" << pig.name << std::endl;
// std::cout << "每只乌龟都有个伟大的名字:" << turtle.name << std::endl;

pig.eat(15);
pig.eat();
turtle.eat();
pig.climb();
turtle.swim();

return 0;
}

可以看到这里报错了,提示需要一个参数,而没有匹配到 Pig::eat() ,可见基类的 eat() 方法被子类覆盖了,原方法不在了。

12. 友元关系

在某些场合一个完全无关的类由于某些特殊原因需要访问到某个 protected 成员,甚至某个 private 成员,怎么办呢?

如果全部设置成 public 那就失去了保护的作用。

于是 友元关系 应运而生。

友元关系: 类之间的一种特殊关系,这种关系不仅允许友元类访问对方的public方法和属性,还允许友元访问对方的 protectedprivate 方法和属性。(——非常亲密的关系呦)

声明一个友元关系只需要在类声明里的某个地方加上一条 friend class ** 就可以。

注: 这条语句可以放在任何地方,放在 publicprotectedprivate 段落里都可以。

下面举个例子练习一下:

爸爸可以教孩子,也可以让孩子去办事。

爸爸的兄弟是孩子的长辈,也可以让孩子去办些简单的事。

设置爸爸的兄弟是父子类的友元。

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

class FatherAndSon {
public:
FatherAndSon(std::string theName);
void teach(FatherAndSon *child);
void ask(FatherAndSon *child, std::string something);

protected:
std::string name;

friend class Brothers; // 此处声明 Brothers 类为友元
};

class Father : public FatherAndSon {
public:
Father(std::string theName);
};

class Son : public FatherAndSon {
public:
Son(std::string theName);
};

class Brothers {
public:
Brothers(std::string theName);
void ask(FatherAndSon *child, std::string something);
protected:
std::string name;
};

FatherAndSon::FatherAndSon(std::string theName) {
name = theName;
}

void FatherAndSon::teach(FatherAndSon *child) {
std::cout << child->name << ",好孩子,来。" << name << "教你点东西" << std::endl;
}

void FatherAndSon::ask(FatherAndSon *child, std::string something) {
std::cout << name << "让" << child->name << "去" << something << std::endl;
}

Father::Father(std::string theName) : FatherAndSon(theName) {}

Son::Son(std::string theName) : FatherAndSon(theName) {}

Brothers::Brothers(std::string theName) {
name = theName;
}

void Brothers::ask(FatherAndSon *child, std::string something) {
std::cout << child->name << "帮" << name << "去" << something << std::endl;
}

int main() {
Father father("爸爸");
Son son("小花");

Brothers fatherbrother("大爹");

father.teach(&son);
father.ask(&son, "买包烟");

std::cout << "\n爸爸的兄弟,小花的大爹来啦!\n";
fatherbrother.ask(&son, "买包辣条");

return 0;
}

可以看到爸爸的兄弟可以让小花去买辣条。

但是如果把上面的友元声明 friend class Brothers; 注释掉,就会报错:大爹没法通过 child 调用 FatherAndSon 类的属性 name 。(同时注意到,继承了 FatherAndSonFatherSon 子类可以通过其对象调用 protected 的属性 name