程序的内存模型

四个区域

C++程序在执行时,会将内存大方向划分为四个区域

  • 代码区:存放函数体的二进制代码,也就是你写的代码,但是不包括注释,由操作系统进行管理的。
  • 全局区:存放全局变量和静态变量以及常量。
  • 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等等。
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

内存四区的意义:不同区域存放的数据,赋予不同的生命周期,可以方便我们进行编程。

代码区

在程序编译后会生成相应的exe可执行程序,这个时候就会产生代码区,其中会存放CPU执行的机器指令。对于这部分区域,它有共享只读两个特点。

共享

代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。例如一个程序可能不止启动一次,每一次启动的时候都不会创建一个新的代码区,而是会全都使用同一份代码,这样可以有效防止资源浪费。

只读

代码区是只读的,使其只读的原因是防止程序修改指令。

全局区

全局区和代码区一样,是在编译后和执行前就已经存在的了,这片区域存放的是全局变量和静态变量,其中还包含常量区,字符串常量和其他常量也存放在这个地方。

该区域在程序结束后由操作系统释放。

所以全局变量和局部变量的存储地址是不一样的,例如如下这段代码,可以看一下他们的值:

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 global_a = 0;
int global_b = 0;

int main()
{
//局部变量
int a = 0;
int b = 0;
cout << "局部变量a地址:" << int(&a) << endl;
cout << "局部变量b地址:" << int(&b) << endl;
cout << endl;
cout << "全局变量a地址:" << int(&global_a) << endl;
cout << "全局变量b地址:" << int(&global_b) << endl;
return 0;
}

输出结果:

1
2
3
4
5
局部变量a地址:1821374196
局部变量b地址:1821374228

全局变量a地址:1686692304
全局变量b地址:1686692308

通过观察可以看出,局部变量存在一起,全局变量存在一起,他们的存储地址是不一样的,但是各自又存在一起。

全局区 不在全局区
全局变量 局部变量
静态变量 局部常量
常量

栈区

由编译器自动分配释放,存放函数的参数值,局部变量等。

在我们使用函数的时候,不要去返回局部变量的地址,因为栈区开辟的数据由编译器自动释放。

堆区

由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。

C++中主要利用new在堆区开辟内存。

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

using namespace std;

int* fun()
{
int* p;
//在堆区开辟数据
p = new int(100);
return p;
}

int main()
{
int* q = fun();
cout << *q << endl;
return 0;
}

输出结果:

1
100

在上述代码中,使用new在堆区开辟了一个数据,存放了$100$这个数据,对于堆区的地址,不会在函数结束时自动释放,而是会一直保留,除非程序员主动释放这部分的内存空间。

使用new在堆区开辟数据,其语法如下所示:

1
new 数据类型

该返回值为数据所对应的类型的指针。

如果想要释放这部分空间,需要使用delete来实现,只需要去delete堆区的相应的地址即可。

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* fun()
{
int* p;
//在堆区开辟数据
p = new int(100);
return p;
}

int main()
{
int* q = fun();
cout << *q << endl;
delete(q);
cout << *q << endl;
return 0;
}

输出结果:

1
2
100
-572662307

可以发现,第一次输出的时候能够很好的输出目标值,第二次输出的时候,由于执行了delete操作,所以第二次输出会出现奇怪的值,因为这个时候这部分空间已经被释放掉了,再次访问其实是非法操作。

同理,也可以使用new来开辟一整个空间,也就是一个数组:

1
new int[10];

上述代码在堆区创建了一个有$10$个空间的数组。

可以使用这个方式,开辟一个有$10$个空间的数组,将其值赋值为$100\sim109$。

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

using namespace std;

int main()
{
int* a = new int[10];
for (int i = 0; i < 10; i++)
a[i] = 100 + i;
for (int i = 0; i < 10; i++)
cout << a[i] << " ";
return 0;
}

输出结果:

1
100 101 102 103 104 105 106 107 108 109

对于这种情况,如果要释放数组的话,需要添加一个中括号来进行释放,具体如下所示:

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

using namespace std;

int main()
{
int* a = new int[10];
for (int i = 0; i < 10; i++)
a[i] = 100 + i;
for (int i = 0; i < 10; i++)
cout << a[i] << " ";
cout << endl;
delete[] a;
for (int i = 0; i < 10; i++)
cout << a[i] << " ";
return 0;
}

输出结果:

1
2
100 101 102 103 104 105 106 107 108 109
-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307

可以发现,在释放完之后,输出这部分内容已经变成了非法操作了。

引用

基本语法

引用的作用是给变量起一个别名,其语法如下所示:

1
数据类型 &别名 = 原名

相当于给一个变量增加了一个新的标签,通过原名和这个标签都可以索引到这个变量。

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

using namespace std;

int main()
{
int a = 0;
int& b = a;
b = 100;
cout << a << endl;
return 0;
}

输出结果:

1
100

引用的注意事项

  • 引用必须初始化。
  • 引用在初始化后,不可以改变。

如果创建引用的时候没有初始化会直接报错。

引用在初始化之后如果使用类似于b = ab是引用,a是变量)的操作,会认为是赋值操作,而不是更改引用。

引用做函数参数

函数在传递参数时,可以利用引用的技术让形参修饰实参,可以简化指针修改实参的繁琐。

用引用参数实现交换两个变量的值:

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;

void Swap(int& a, int& b) //引用参数,在该函数中对变量的操作会直接影响实参
{
int temp;
temp = a;
a = b;
b = temp;
return;
}

int main()
{
int a = 3, b = 5;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}

输出结果:

1
5 3

通过引用参数产生的效果和按地址传递是一样的,引用的语法相对来说更加清楚简单一些。

引用做函数返回值

引用可以直接作为函数的返回值。

不要返回局部变量的引用,这是因为局部变量是在栈区被定义的,在函数结束时开辟的空间会直接被释放掉。引用其实就是返回其地址,这就会导致这部分的地址被释放掉。

但是可以返回全局变量或者静态变量一类的存放在全局区的变量的引用,因为这部分变量不会存放在堆区,在函数执行结束后不会直接释放掉。

现在以创建一个静态变量为例,进行如下操作:

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

using namespace std;

int& fun()
{
static int a = 100;
return a;
}

int main()
{
int &b = fun();
cout << b << endl;
return 0;
}

输出结果:

1
100

可以看到,fun函数是一个以引用作为函数返回值的函数,最后返回了a的引用,最后可以成功输出其值。

如果使用引用做函数返回值,那么这个函数还可以作为左值去进行运算。

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

using namespace std;

int& fun()
{
static int a = 100;
return a;
}

int main()
{
int &b = fun();
cout << b << endl;
fun() = 1234;
cout << b << endl;
return 0;
}

输出结果:

1
2
100
1234

上述代码的原理是,fun函数返回的是a的引用,也就是a所对应的地址。第$15$行的操作相当于让这块地址的值赋值为$1234$。与此同时,b作为引用也指向了这块地址,这也就是为什么可以作为左值去进行修改。

引用的本质

引用的本质相当于是在C++内部实现的一个指针常量,但是引用比指更加安全,相当于一个安全的指针,编译器也能对引用做出更多的优化,避免了空指针检查。

所以引用可以理解为一个指针常量,和普通常量的区别在于普通常量的值是不可变的,引用的关键在于指向的地址是不变的,但是都可以看作常量,这也就是为什么必须进行初始化赋值操作。

常量引用

常量引用可以用来修饰形参,从而防止误操作。

在函数形参列表中,可以加const修饰形参,防止形参改变实参。

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

using namespace std;

void fun(const int& value)
{
//value的值不可被修改
//value = 100;
cout << value << endl;
}

int main()
{
int a = 10;
fun(a);
return 0;
}

输出结果:

1
10

在函数中,如果有些值不希望被修改,并且还担心忘记这件事情,就可以将其设置为常量引用,那么就可以看做一个常量,之后便不可再修改了。

高级函数

函数默认参数

C++中,函数的形参列表中的形参是可以有默认值的,其语法如下所示:

1
2
3
4
返回值类型 函数名(参数 = 默认值)
{

}

通过使用默认参数的方式,即使传递参数的时候没有给默认参数传递值,它也可以用默认的参数来执行函数。

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

using namespace std;

int fun(int a, int b = 20, int c = 30)
{
return a + b + c;
}

int main()
{
cout << fun(10) << endl;
cout << fun(10, 10) << endl;
cout << fun(10, 10, 10) << endl;
return 0;
}

输出结果:

1
2
3
60
50
30

这个代码定义了一个函数fun,作用是将三个值相加在一起并返回。主函数中调用了三次这个函数,分别传入了不同的参数,可以发现,如果我们自己传入数据,就会用自己的数据,如果没有,那么函数会使用默认值。

注意事项:

  1. 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值。
  2. 如果函数声明有默认参数,函数实现就不能有默认参数,因为编译器不知道应该使用哪个作为默认参数。也就是说声明和实现只能有一个默认参数。

函数占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置。其语法如下所示:

1
2
3
4
返回值类型 函数名(数据类型)
{

}

函数占位需要在传入参数的时候在相同位置传入相同类型的参数。

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

using namespace std;

void fun(int a, int)
{
cout << "Hello World!" << endl;
}

int main()
{
fun(10, 10);
return 0;
}

输出结果:

1
Hello World!

占位参数也可以有默认参数。

现阶段函数的占位参数存在意义不大,后面会具体用到该方法。

函数重载

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

using namespace std;

void fun(int a, int b)
{
cout << "fun1:" << endl;
cout << a + b << endl << endl;
}

void fun(float a, float b)
{
cout << "fun2:" << endl;
cout << a + b << endl << endl;
}

void fun(int a, int b, float c)
{
cout << "fun3:" << endl;
cout << a + b + c << endl << endl;
}

void fun(float a, int b, int c)
{
cout << "fun4:" << endl;
cout << a + b + c << endl << endl;
}

int main()
{
int a = 1, b = 2;
float c = 3.0, d = 4.0;
fun(c, a, b);
fun(a, b, c);
fun(d, d);
fun(a, b);
return 0;
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
fun4:
6

fun3:
6

fun2:
8

fun1:
3

可以看出,使用函数重载,即使函数名是相同的,但是传入的参数不同,就可以调用不同的函数,可以提高复用性。

当引用作为重载的条件时,直接使用引用和使用常量引用会被认为是变量类型不同,可以使用函数重载。

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>

using namespace std;

void fun(int& a)
{
cout << "fun1:" << endl;
cout << a << endl << endl;
}

void fun(const int& a)
{
cout << "fun2:" << endl;
cout << a << endl << endl;
}

int main()
{
int a = 1;
const int b = 2;
fun(a);
fun(b);
fun(3);
return 0;
}

输出结果:

1
2
3
4
5
6
7
8
fun1:
1

fun2:
2

fun2:
3

根据上述结果,如果是一个变量的话,会执行直接使用引用的函数,如果是一个常量的话(单纯的数字、字符串等常量也算在内),会执行常量引用的函数。

还有一个注意事项,如果函数重载碰到了默认参数的情况,需要考虑其二义性。这种情确实可以发生重载,但是会导致无法调用,例如下面这种情况。

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

using namespace std;

void fun(int a, int b = 10)
{
cout << "fun1:" << endl;
cout << a << endl << endl;
}

void fun(int a)
{
cout << "fun2:" << endl;
cout << a << endl << endl;
}

int main()
{
//下面这行代码会报错
//fun(100);
return 0;
}

对于上述这种情况,就产生了二义性。第二十行的代码两个函数的条件均满足,导致它不知道该调用哪个函数了,因此会报错。

类和对象

C++面相对象的三大特性为:封装、继承、多态。

C++认为万事万物皆为对象,对象上有其属性和行为。

例如

人可以作为对象,属性有姓名、年龄、身高、体重……,行为有走、跑、跳、吃饭……

车可以作为对象,属性有轮胎、车灯、空调、窗户……,行为有载人、放音乐……

具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类。

封装

属性和行为作为整体

封装是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
#include <iostream>

using namespace std;

const double PI = 3.14; //将圆周率设置为常量

class Circle //class代表设计一个类,后面紧跟着类名称
{
//访问权限
public: //公共权限(后面会具体讲到)

//属性
double radius; //半径

//行为
double perimeter() //获取圆的周长
{
return 2 * PI * radius;
}
};

int main()
{
Circle c; //通过圆类 创建具体的圆(对象)

c.radius = 10; //给圆对象的属性赋值

cout << "圆的周长为:" << c.perimeter() << endl;
return 0;
}

输出结果:

1
圆的周长为:62.8

上述代码通过创建一个类,将属性和行为封装在一起,属性是半径,行为是求周长。通过这种方式,可以创建一个属于这一类的对象,然后对这个对象进行相应的操作。通过一个类创建一个对象的过程叫做实例化

在类中的函数,也可以为类中包含的属性进行赋值,例如下面这种方式:

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

using namespace std;

const double PI = 3.14; //将圆周率设置为常量

class Circle //class代表设计一个类,后面紧跟着类名称
{
//访问权限
public: //公共权限(后面会具体讲到)

//属性
double radius; //半径
double perimeter;

//行为
void set_perimeter() //获取圆的周长
{
perimeter = 2 * PI * radius;
}
};

int main()
{
Circle c; //通过圆类 创建具体的圆(对象)

c.radius = 10; //给圆对象的属性赋值
c.set_perimeter(); //设置其周长

cout << "圆的周长为:" << c.perimeter << endl;
return 0;
}

输出结果:

1
圆的周长为:62.8

与第一个例子相类似,不同的是,这次求周长的函数不返回其返回值,而是设计该对象的属性值,使其以后可以更方便地进行调用。

类中的属性和行为,我们统一称为成员。

属性也称为成员属性或者成员变量。

行为也称为成员函数或者成员方法。

访问权限

类在设计时,可以把属性和行为放在不同的权限下,加以控制。

访问权限有三种:

  1. public——公共权限(类内可以访问 类外可以访问)
  2. protected——保护权限(类内可以访问 类外不可以访问,儿子可以访问父亲中的保护内容)
  3. private——私有权限(类内可以访问 类外不可以访问,儿子不可以访问父亲中的私有内容)

struct和class的区别

C++structclass唯一的区别就在于默认的访问权限不同。

区别:

  • struct默认权限为公共。
  • class默认权限为私有。

注:struct中也可以写函数。

成员属性私有化

将成员属性设置为私有有两个优点:

  1. 将所有成员属性设置为私有,可以自己控制读写权限。
  2. 对于写权限,我们可以检测数据的有效性。

将成员属性私有化之后,我们就不可以直接访问了,因此一般会在public中创建一些端口供用户去使用,例如下面这种方法:

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

using namespace std;

class Person
{
public:
void set_name(string name)
{
m_name = name;
}

string get_name()
{
return m_name;
}

int get_age()
{
return m_age;
}

void set_password(string password)
{
m_password = password;
}

private:
string m_name; //姓名 可读 可写
int m_age = 18; //年龄 可读
string m_password; //密码 可写
};

int main()
{
Person a;
a.set_name("比格沃斯");
cout << a.get_name() << endl;
cout << a.get_age() << endl;
a.set_password("123456");
return 0;
}

输出结果:

1
2
比格沃斯
18

在上述结果中可以发现,通过成员属性私有化的方法,可以很好的控制读写权限,并且还可以在读取的时候判断一下用户的输入。

例如我们对于年龄这一属性,想要判断一下是否在$0\sim 150$岁之间,就可以创建一个设置年龄的端口,判断其年龄所处的范围,如果不在该范围内,返回报错信息。

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

using namespace std;

class Person
{
public:
void set_name(string name)
{
m_name = name;
}

string get_name()
{
return m_name;
}

void set_age(int age)
{
if (age >= 0 && age <= 150)
m_age = age;
else
cout << "非法年龄!" << endl;
}

int get_age()
{
return m_age;
}

void set_password(string password)
{
m_password = password;
}

private:
string m_name; //姓名 可读 可写
int m_age; //年龄 可读 可写
string m_password; //密码 可写
};

int main()
{
Person a;
a.set_age(18);
cout << a.get_age() << endl;
a.set_age(10086);
cout << a.get_age() << endl;
return 0;
}

输出结果:

1
2
3
18
非法年龄!
18

对象特征

对象的初始化和清理

在我们日常生活中,比如买一台手机,都会在刚使用的时候有一个出厂设置,当我们在某一天不用的时候,也会删除一些自己的信息保证数据安全。

C++中的面向对象来源于生活,每个对象也会有初始设置以及对象销毁前的数据清理设置。

构造函数和析构函数

对象的初始化清理也是两个非常重要的问题。

如果一个对象或者变量没有初始状态,对其使用后果也是未知的。

如果使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。

C++利用了构造函数和析构函数解决上述问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,如果我们不提供构造函数和析构函数,编译器会提供,但是提供的两个函数是空的。

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数的语法如下所示:

1
类名(){}
  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同。
  3. 构造函数可以有参数,因此可以发生重载。
  4. 程序在调用对象时会自动调用构造,无需手动调用,而且只会调用一次。

析构函数的语法如下所示:

1
~类名(){}
  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号~
  3. 构造函数不可以有参数,因此不可以发生重载。
  4. 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次。
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>

using namespace std;

class Person
{
public:
//构造函数
Person()
{
cout << "Person 构造函数调用" << endl;
}

//析构函数
~Person()
{
cout << "Person 析构函数调用" << endl;
}
};

int main()
{
Person p;
cout << "Hello World!" << endl;
return 0;
}

输出结果:

1
2
3
Person 构造函数调用
Hello World!
Person 析构函数调用

上述程序中,创建了一个类,同时在其中创建了一个构造函数和析构函数。

主函数中实例化一个该类的对象的时候,会调用其构造函数;在程序结束,系统自动回收这部分内存,也就是将该对象销毁掉,会调用其析构函数。

注:要把这两个函数写在public中。

构造函数的分类及调用

两种分类方式:

  • 按参数分:有参构造和无参构造
  • 按类型分:普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
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
#include <iostream>

using namespace std;

class Person
{
public:
//构造函数
//无参构造
Person()
{
cout << "Person 无参构造函数调用" << endl;
}

//有参构造
Person(int age)
{
m_age = age;
cout << "Person 有参构造函数调用" << endl;
}

//拷贝构造函数
Person(const Person& p)
{
cout << "Person 拷贝构造函数调用" << endl;
m_age = p.m_age;
}

//析构函数
~Person()
{
cout << "Person 析构函数调用" << endl;
}

//获取年龄
int get_age()
{
return m_age;
}

private:
int m_age;
};

void fun1()
{
cout << "括号法调用:" << endl;
Person p1;
Person p2(10);
Person p3(p2);
cout << p2.get_age() << endl;
cout << p3.get_age() << endl;
cout << endl;
}

void fun2()
{
cout << "显示法调用:" << endl;
Person p1 = Person(10);
Person p2 = Person(p1);
cout << p1.get_age() << endl;
cout << p2.get_age() << endl;
cout << endl;
}

void fun3()
{
cout << "隐式转换法调用:" << endl;
Person p1 = 10;
Person p2 = p1;
cout << p1.get_age() << endl;
cout << p2.get_age() << endl;
cout << endl;
}

int main()
{
fun1();
cout << endl;
fun2();
cout << endl;
fun3();
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
括号法调用:
Person 无参构造函数调用
Person 有参构造函数调用
Person 拷贝构造函数调用
10
10

Person 析构函数调用
Person 析构函数调用
Person 析构函数调用

显示法调用:
Person 有参构造函数调用
Person 拷贝构造函数调用
10
10

Person 析构函数调用
Person 析构函数调用

隐式转换法调用:
Person 有参构造函数调用
Person 拷贝构造函数调用
10
10

Person 析构函数调用
Person 析构函数调用

在上述代码中,创建了一个无参构造函数,一个有参构造函数,一个析构函数和一个拷贝函数。

拷贝函数可以执行一些拷贝功能,可以将一个该类型的对象传入,之后拷贝给调用该函数的对象。

该函数不能改变传入进来的对象,因此其参数需要使用常量引用。

对于括号法,不能直接使用如下方式:

1
Person p1();

因为这样会认为你是声明了一个函数,其返回值是Person,并不会调用你的无参构造函数。

对于显示法,如果直接使用Person(10)也会调用其构造函数,会创建一个临时的匿名对象, 当执行结束后,系统会立即回收掉匿名对象。

不要利用拷贝构造函数去初始化匿名对象,编译器会认为Person(p)Person p是一样的,导致它会觉得你是实例化了一个该类的对象。

对于隐式转换法,相当于对显示法的一个简写。

拷贝构造函数调用时机

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

using namespace std;

class Person
{
public:
Person()
{
cout << "无参构造函数调用" << endl;
}

Person(int age)
{
m_age = age;
cout << "有参构造函数调用" << endl;
}

Person(const Person& p)
{
m_age = p.m_age;
cout << "拷贝构造函数调用" << endl;
}

~Person()
{
cout << "析构函数调用" << endl;
}

int m_age;
};

void fun(Person p)
{
cout << p.m_age << endl;
}

int main()
{
Person p(10);
fun(p);
return 0;
}

输出结果:

1
2
3
4
5
有参构造函数调用
拷贝构造函数调用
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

using namespace std;

class Person
{
public:
Person()
{
cout << "无参构造函数调用" << endl;
}

Person(int age)
{
m_age = age;
cout << "有参构造函数调用" << endl;
}

Person(const Person& p)
{
m_age = p.m_age;
cout << "拷贝构造函数调用" << endl;
}

~Person()
{
cout << "析构函数调用" << endl;
}

int m_age;
};

Person fun(Person p)
{
cout << p.m_age << endl;
return p;
}

int main()
{
Person p(10);
fun(p);
return 0;
}

输出结果:

1
2
3
4
5
6
7
有参构造函数调用
拷贝构造函数调用
10
拷贝构造函数调用
析构函数调用
析构函数调用
析构函数调用

输出了两次拷贝构造函数调用,第一次是传递值的时候进行的拷贝构造,第二次是返回值时进行的拷贝构造。

构造函数调用规则

默认情况下,C++编译器至少给一个类添加$3$个函数:

  1. 默认构造函数(无参,函数体为空)。
  2. 默认析构函数(无参,函数体为空)。
  3. 默认拷贝构造函数,对属性进行值拷贝。

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造函数。
  • 如果用户定义拷贝构造函数,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
#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
int* m_age;

Person()
{
cout << "无参构造函数" << endl;
}

Person(int age)
{
cout << "有参构造函数" << endl;
m_age = new int(age);
}

~Person()
{
cout << "析构函数" << endl;
}

Person(const Person& p)
{
m_age = p.m_age;
cout << "拷贝函数" << endl;
}
};

void fun(Person p)
{
cout << *p.m_age << endl;
*p.m_age += 1;
}

int main()
{
Person p(18);
fun(p);
cout << *p.m_age << endl;
return 0;
}

输出结果:

1
2
3
4
5
6
有参构造函数
拷贝函数
18
析构函数
19
析构函数

根据上述结果可以发现,对于浅拷贝而言,只是把相应的地址进行了复制,现在两个指针指向的都是同一块地址,因此在$37$行处进行进行加一操作时,会对传入的对象产生影响。

不仅如此,我们在回收一个对象的时候,一般会回收当时分配的内存,类似于下述这段代码:

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

using namespace std;

class Person
{
public:
int* m_age;

Person()
{
cout << "无参构造函数" << endl;
}

Person(int age)
{
cout << "有参构造函数" << endl;
m_age = new int(age);
}

~Person()
{
cout << "析构函数" << endl;
if (m_age != NULL)
{
delete(m_age); //释放这部分空间
m_age = NULL; //避免出现野指针
}
}

Person(const Person& p)
{
m_age = p.m_age;
cout << "拷贝函数" << endl;
}
};

void fun(Person p)
{
cout << *p.m_age << endl;
*p.m_age += 1;
}

int main()
{
Person p(18);
fun(p);
cout << *p.m_age << endl;
return 0;
}

运行这段代码的时候会产生问题报错,主要原因在于析构函数那里,回收fun函数产生的对象的时候能够正确完成回收操作。但是回收主函数这个对象的时候,由于他们本质上指向的是同一个地址,会导致回收这部分的内存空间将会是一个非法操作。

为了解决上述问题,我们就需要使用深拷贝的方法。深拷贝相当于创建了一个新的地址,之后将相应的值拷贝过来,可以有效避免上述情况。

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

using namespace std;

class Person
{
public:
int* m_age;

Person()
{
cout << "无参构造函数" << endl;
}

Person(int age)
{
cout << "有参构造函数" << endl;
m_age = new int(age);
}

~Person()
{
cout << "析构函数" << endl;
if (m_age != NULL)
{
delete(m_age); //释放这部分空间
m_age = NULL; //避免出现野指针
}
}

Person(const Person& p)
{
m_age = new int(*p.m_age);
cout << "拷贝函数" << endl;
}
};

void fun(Person p)
{
cout << *p.m_age << endl;
*p.m_age += 1;
}

int main()
{
Person p(18);
fun(p);
cout << *p.m_age << endl;
return 0;
}

输出结果:

1
2
3
4
5
6
有参构造函数
拷贝函数
18
析构函数
18
析构函数

代码的$34$行实现了深拷贝,通过申请一个新的内存空间的方式,不仅在传递函数的时候不会更改传入对象的属性值,同时释放地址的时候也不会产生报错。

初始化列表

C++提供了初始化列表语法,用来初始化属性,其语法如下:

1
构造函数(): 属性1(值1), 属性2(值2) ... {}

具体代码如下:

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;

class Person
{
public:
Person(int a, int b, int c) :m_a(a), m_b(b), m_c(c)
{

}

int m_a, m_b, m_c;
};

int main()
{
Person p(1, 2, 3);
cout << p.m_a << " " << p.m_b << " " << p.m_c << endl;
return 0;
}

输出结果:

1
1 2 3

通过这种方法,可以更加便捷地进行初始化赋值操作。

类对象作为类成员

C++类中的成员可以是另一个类的对象,我们城改成员为对象成员。

例如:

1
2
3
4
5
6
7
8
9
class A
{

}

class B
{
A a;
}

B类中有对象A作为成员,A为对象成员。

那么当创建B对象时,AB的构造和析构的顺序是谁先谁后?

我们可以看一下下面这段代码:

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

using namespace std;

class A
{
public :
A()
{
cout << "A的构造函数" << endl;
}

~A()
{
cout << "A的析构函数" << endl;
}
};

class B
{
public:
B()
{
cout << "B的构造函数" << endl;
}

~B()
{
cout << "B的析构函数" << endl;
}

A a;
};

int main()
{
B b;
return 0;
}

输出结果:

1
2
3
4
A的构造函数
B的构造函数
B的析构函数
A的析构函数

可以发现,先进行类成员的构造,之后再进行大的对象的构造。可以理解为在生产一个产品的时候,需要先构造它的零件,之后才能构造他的整体。对于析构函数而言,会先析构大的对象,之后才会依次分解其内部的对象。

静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

静态成员变量

先来看一个静态成员变量的例子:

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;

class Person
{
public:
static int money;
};

int Person::money;

int main()
{
Person p1;
cout << p1.money << endl;
Person p2;
p2.money = 100;
cout << p1.money << endl;
return 0;
}

输出结果:

1
2
0
100

在上述例子中,我们创建了一个静态成员变量money,那么这个成员变量就是共享的了,但是使用前需要在全局变量的位置声明一下这个是一个成员静态变量(一定要在全局变量的位置进行声明)。在全局变量声明的时候,可以直接对他进行初始化赋值,如果不进行初始化赋值的话,默认值会设置为$0$。

接下来是主函数,我们实例化了Person的两个对象,可以发现,这两个对象都可以对money进行调用,并且更改其值都会产生相应影响。

静态成员变量,不属于某个对象上,所有对象都共享同一份数据。我们可以通过对象进行访问该静态成员变量,也可以通过类名进行访问。

静态成员函数

接着来看一个静态成员函数的例子:

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

using namespace std;

class Person
{
public:
static void fun1()
{
money = 100;
}

static void fun2(Person p)
{
money += p.increase_money;
}
static int money;
int increase_money;
};

int Person::money;

int main()
{
Person p1;
p1.fun1();
cout << p1.money << endl;
p1.increase_money = 50;
p1.fun2(p1);
cout << p1.money << endl;
return 0;
}

输出结果:

1
2
100
150

上述代码定义了两个静态成员函数,第一个的作用是给money赋值,第二个的作用是给money加上一个值。

静态成员函数只能对静态成员变量进行操作,但是可以通过传递值的方式来与类成员变量进行数据交换。

对象模型

成员的存储

C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。

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

using namespace std;

class Person
{

};

int main()
{
Person p;
cout << sizeof(p) << endl;
return 0;
}

输出结果:

1
1

对于一个空对象而言,其所占的内存空间为$1$。因为C++编译器会给每个空对象也分配了一个字节的空间,这是为了区分空对象占内存的位置。

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

using namespace std;

class Person
{
int m_A; //非静态成员变量
};

int main()
{
Person p;
cout << sizeof(p) << endl;
return 0;
}

输出结果:

1
4

非空的对象会分配其非静态成员变量所对应的内存空间。

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

using namespace std;

class Person
{
int m_A; //非静态成员变量

static int m_B; //静态成员变量
};

int Person::m_B;

int main()
{
Person p;
cout << sizeof(p) << endl;
return 0;
}

输出结果:

1
4

静态变量不属于对象。

this指针

通过上述例子我们知道,在C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用同一块代码。

那么问题是:这一块代码是如何区分哪个对象调用自己的呢?

C++通过提供特殊的对象指针,this指针,解决上述问题。**this指针指向被调用的成员函数所属的对象。**

this指针是隐含每一个非静态成员函数内的一种指针。

this指针不需要定义,直接使用即可。

this指针的用途:

  • 当形参和成员变量同名时,可使用this指针来区分。
  • 在类的非静态成员函数中返回对象本身,可使用return *this
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;

class Person
{
public:
Person(int age)
{
this->age = age;
}

int age;
};

int main()
{
Person p(18);
cout << p.age << endl;
return 0;
}

输出结果:

1
18

在上述代码中,可以使用this指针指向的是被调用的成员函数。

下面我们来看另外一种情况:

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>

using namespace std;

class Person
{
public:
Person(int age)
{
this->age = age;
}

Person& Add_age(Person& p)
{
this->age += p.age;
p.age += 1;
return *this;
}

int age;
};

int main()
{
Person p1(10);
Person p2(20);
p1.Add_age(p2).Add_age(p2);
cout << p1.age << endl;
cout << p2.age << endl;
return 0;
}

输出结果:

1
2
51
22

第$13$行使用了引用参数,这样可以保证传进来的是本体,而不是经过拷贝函数的一个复制体。同理,这个函数的返回值也需要是相应的对象的地址。在第$27$行的位置上,我们通过链式编程的思想进行调用,可以实现反复调用一个函数。

空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针。

如果用到this指针,需要加以判断保证代码的健壮性。

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

using namespace std;

class Person
{
public:
void fun()
{
cout << "Hello World!" << endl;
}

void set_age(int age)
{
if (this == NULL)
return;

this->age = age;
}

int age;
};

int main()
{
Person* p = NULL;
p->fun();
p->set_age(10);
return 0;
}

输出结果:

1
Hello World!

空指针也是可以调用相应的成员函数的,但是如果进行赋值操作的话,因为指针为空,会导致其报错,因此可以进行一个空指针的检查,类似于$15$行和$16$行的操作,从而提高代码的健壮性。

const修饰成员函数

常函数:

  • 成员函数后面加const后我们称这个函数为常函数
  • 常函数内不可以修改成员属性。
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改。

常对象:

  • 声明对象前加const称该对象为常对象。
  • 常对象只能调用常函数。
  • 常对象可以修改有mutable关键字的属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

class Person
{
public:
void set() const
{
this->money = 50;
}

int age;
mutable int money;
};

int main()
{
const Person p;
p.set();
cout << p.money << endl;
return 0;
}

输出结果:

1
50

这段代码中可以发现,常变量只能调用常函数,并且常函数内部只能修改有mutable关键字的成员变量。

友元

在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元技术。

友元的目的就是让一个函数或者类,访问另一个类中私有成员。

友元的关键字是friend

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

全局函数友元

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 <string>

using namespace std;

class Building
{
friend void Good_friend(Building& building);

public:
Building()
{
this->SittingRoom = "客厅";
this->BedRoom = "卧室";
}

string SittingRoom;

private:
string BedRoom;
};

void Good_friend(Building& building)
{
cout << "好朋友正在参观:" << building.SittingRoom << endl;
cout << "好朋友正在参观:" << building.BedRoom << endl;
}

int main()
{
Building building;
Good_friend(building);
return 0;
}

输出结果:

1
2
好朋友正在参观:客厅
好朋友正在参观:卧室

上述代码,在Building类中,客厅是公共权限,卧室是私有权限。正常来说,全局函数是没有办法对其私有成员属性进行访问的,但是我们可以在类中的任意一个位置声明函数,并且在前面加上friend关键字,这样就可以让全局函数访问其私有成员属性了。

类友元

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>

using namespace std;

class Building;

class Goodfriend
{
public:
Goodfriend();
void vist();

private:
Building* building;
};

class Building
{
friend class Goodfriend;

public:
Building();

string SittingRoom;

private:
string BedRoom;
};

Building::Building()
{
this->SittingRoom = "客厅";
this->BedRoom = "卧室";
}

Goodfriend::Goodfriend()
{
this->building = new Building;
}

void Goodfriend::vist()
{
cout << "好朋友正在参观:" << this->building->SittingRoom << endl;
cout << "好朋友正在参观:" << this->building->BedRoom << endl;
}

int main()
{
Goodfriend p;
p.vist();
return 0;
}

输出结果:

1
2
好朋友正在参观:客厅
好朋友正在参观:卧室

这段代码,我们在第$6$行的时候声明了一下Building类,但是我们没有定义它的内部情况。这行代码有点类似于函数声明,提前告诉编译器我们有这个类,但是我们还没有写它的内部代码。

第$8$行是一个Goodfriend的类,它内部有一个构造函数,一个visit函数,这两个函数没有在类内直接定义,只需要在类内声明一下,在外部定义也是完全可以的。

第$18$行是一个Building的类,并且声明了Goodfiend类是它的友元类,这样可以让Goodfriend所实例化出来的对象也可以访问Building的私有成员属性。

$31$行和$37$行分别写了Buildintg类和Goodfriend类的构造函数,需要在前面声明一下这个函数属于哪个类,其中Goodfriend的构造函数中初始化了一下其指针,申请了一个Building的内存空间并指向(这个时候会调用Building的构造函数)。

剩下的部分与前文的代码相类似,在这里就不过多赘述。

成员函数友元

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>

using namespace std;

class Building;

class Goodfriend
{
public:
Goodfriend();
void vist();

private:
Building* building;
};

class Building
{
friend void Goodfriend::vist();

public:
Building();

string SittingRoom;

private:
string BedRoom;
};

Building::Building()
{
this->SittingRoom = "客厅";
this->BedRoom = "卧室";
}

Goodfriend::Goodfriend()
{
this->building = new Building;
}

void Goodfriend::vist()
{
cout << "好朋友正在参观:" << this->building->SittingRoom << endl;
cout << "好朋友正在参观:" << this->building->BedRoom << endl;
}

int main()
{
Goodfriend p;
p.vist();
return 0;
}

输出结果:

1
2
好朋友正在参观:客厅
好朋友正在参观:卧室

上述代码中和类友元部分的代码基本完全一致,区别在于,类友元是把类作为友元进行声明,而这部分代码是将Goodfriend下面的visit函数声明为友元,只有它可以访问Building中的私有成员。

运算符重载

概念

对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

加号运算符重载

作用:实现两个自定义数据类型相加的运算。

首先讲一下通过成员函数来实现加号运算符重载:

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

using namespace std;

class Student
{
public:
Student operator+(Student s)
{
Student temp;
temp.name = this->name + "+" + s.name;
temp.Chinese = this->Chinese + s.Chinese;
temp.English = this->English + s.English;
temp.Math = this->Math + s.Math;
return temp;
}

string name;
int Math;
int Chinese;
int English;
};

int main()
{
Student s1, s2;
s1.name = "小明";
s1.Chinese = 80;
s1.English = 20;
s1.Math = 100;

s2.name = "小红";
s2.Chinese = 71;
s2.English = 99;
s2.Math = 64;

Student sum;
sum = s1 + s2;
cout << sum.name << endl;
cout << "语文:" << sum.Chinese << endl;
cout << "英语:" << sum.English << endl;
cout << "数学:" << sum.Math << endl;
return 0;
}

输出结果:

1
2
3
4
小明+小红
语文:151
英语:119
数学:164

上述代码重新定义了一个适用于两个Student类型的对象相加的加号运算符,这样就可以用于自定义该对象的加法了。

接着来看一下全局函数实现加号运算符重载:

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

using namespace std;

class Student
{
public:
string name;
int Math;
int Chinese;
int English;
};

Student operator+(Student s1, Student s2)
{
Student temp;
temp.name = s1.name + "+" + s2.name;
temp.Chinese = s1.Chinese + s2.Chinese;
temp.English = s1.English + s2.English;
temp.Math = s1.Math + s2.Math;
return temp;
}

int main()
{
Student s1, s2;
s1.name = "小明";
s1.Chinese = 80;
s1.English = 20;
s1.Math = 100;

s2.name = "小红";
s2.Chinese = 71;
s2.English = 99;
s2.Math = 64;

Student sum;
sum = s1 + s2;
cout << sum.name << endl;
cout << "语文:" << sum.Chinese << endl;
cout << "英语:" << sum.English << endl;
cout << "数学:" << sum.Math << endl;
return 0;
}

上述代码中,通过定义了一个全局函数来实现相应的加法重载运算。

左移运算符重载

作用:实现自定义数据类型的输出。

一般而言,不会利用成员函数重载左移运算符,因为无法实现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
#include <iostream>
#include <string>

using namespace std;

class Student
{
public:
string name;
int Math;
int Chinese;
int English;
};

Student operator+(Student s1, Student s2)
{
Student temp;
temp.name = s1.name + "+" + s2.name;
temp.Chinese = s1.Chinese + s2.Chinese;
temp.English = s1.English + s2.English;
temp.Math = s1.Math + s2.Math;
return temp;
}

ostream& operator<<(ostream& cout, Student p)

{
cout << p.name << endl;
cout << "语文:" << p.Chinese << endl;
cout << "英语:" << p.English << endl;
cout << "数学:" << p.Math << endl;
return cout;
}

int main()
{
Student s1, s2;
s1.name = "小明";
s1.Chinese = 80;
s1.English = 20;
s1.Math = 100;

s2.name = "小红";
s2.Chinese = 71;
s2.English = 99;
s2.Math = 64;

Student sum;
sum = s1 + s2;

cout << sum << endl;
return 0;
}

输出结果:

1
2
3
4
小明+小红
语文:151
英语:119
数学:164

在理解这段函数之前,需要先明确一点,cout其实本质上也是一个对象,其类名为ofstream,并且我们不能修改cout关键字,所以我们需要使用引用的方式去传递cout关键字。在这之后,我们就可以正常的使用左移运算符重载了,同时根据前文中链式编程的思想,我们在输出完一个数据的时候,还希望继续输出别的数据,因此需要再返回一个ofstream类型的类。

一般来讲,我们会把类中的成员属性设置为私有,因此为了方便起见,我们都会直接对左移运算符重载增加一个友元声明,让它作为对应类的友元,这样可以直接访问输出其私有成员属性。

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 <string>

using namespace std;

class Student
{
friend ostream& operator<<(ostream& cout, Student p);

public:
Student(string name, int Math, int Chinese, int English)
{
this->name = name;
this->Math = Math;
this->Chinese = Chinese;
this->English = English;
}

private:
string name;
int Math;
int Chinese;
int English;
};


ostream& operator<<(ostream& cout, Student p)

{
cout << p.name << endl;
cout << "语文:" << p.Chinese << endl;
cout << "英语:" << p.English << endl;
cout << "数学:" << p.Math << endl;
return cout;
}

int main()
{
Student s("小明", 50, 60, 70);
cout << s << endl;
return 0;
}
1
2
3
4
小明
语文:60
英语:70
数学:50

上述代码中,将左移运算符重载声明为了友元,这样可以直接访问其私有成员属性,成功实现了我们想要的效果。

递增运算符重载

作用:通过重载递增运算符,实现自己的整形数据。

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

using namespace std;

class MyInteger
{
friend ostream& operator<<(ostream& cout, MyInteger a);

public:
MyInteger()
{
this->value = 0;
}

MyInteger& operator++() //前置++
{
this->value++;
return *this;
}

MyInteger operator++(int) //后置++
{
MyInteger temp;
temp = *this;
this->value++;
return temp;
}
private:
int value;
};

ostream& operator<<(ostream& cout, MyInteger a)
{
cout << a.value;
return cout;
}

int main()
{
MyInteger a;
cout << ++a << endl;
cout << a++ << endl;
cout << a << endl;
return 0;
}

输出结果:

1
2
3
1
1
2

前置++的原理是直接在本身上加一个$1$,然后返回即可。为了实现链式编程,我们可以返回其引用类型,这样可以让它进行多次前置++

后置++是先用一个新的临时变量记录一下最开始的值,然后让实际上的变量进行加$1$操作,最后返回的是临时变量的值。要注意我们这个与前置的区别是返回值不是引用类型,因为返回的变量是一个临时的局部变量,函数结束后会将其回收。因此如果使用引用变量并对其使用链式编程的思想,会导致非法操作从而报错。为了和后置递增做一个区分,我们可以在传参的时候传入一个占位参数,否则没有办法进行相应的重载,并且这个参数一定要是整型。

综上所示,前置++可以实现链式编程,而后置++不可以实现链式编程。

赋值运算符重载

C++编译器至少给一个类添加$4$个函数:

  1. 默认构造函数(无参,函数体为空)。
  2. 默认析构函数(无参,函数体为空)。
  3. 默认拷贝构造函数,对属性进行值拷贝。
  4. 赋值运算符operator=,对属性进行值拷贝。

如果类中有属性指向堆,那么做赋值操作时也会出现深浅拷贝问题。

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>

using namespace std;

class Person
{
public:
Person(int age)
{
this->age = new int(age);
}

~Person()
{
if (this->age != NULL)
{
delete(this->age);
this->age = NULL;
}
}

Person& operator=(Person& p)
{
if (this->age != NULL)
{
delete(this->age);
this->age = NULL;
}

//深拷贝
this->age = new int(*p.age);
return *this;
}

int* age;
};

int main()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1;
cout << "p1的年龄为:" << *p1.age << endl;
cout << "p2的年龄为:" << *p2.age << endl;
cout << "p3的年龄为:" << *p3.age << endl;
return 0;
}

输出结果:

1
2
3
p1的年龄为:18
p2的年龄为:18
p3的年龄为:18

上述代码中,我们重载了赋值运算符,使其能够正确的赋值开辟在堆区的数据。同时使用该类作为返回值,可以实现相应的链式存储。

关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行比较操作。

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

using namespace std;

class Person
{
public:
Person(string name, int age)
{
this->name = name;
this->age = age;
}

bool operator == (Person p)
{
if (this->name == p.name && this->age == p.age)
return true;
else
return false;
}

bool operator != (Person p)
{
if (this->name != p.name || this->age != p.age)
return true;
else
return false;
}

private:
string name;
int age;
};

int main()
{
Person p1("Tom", 18);
Person p2("Tom", 18);
Person p3("Jerry", 18);
if (p1 == p2)
cout << "两个人是同一个人" << endl;

if (p1 != p3)
cout << "两个人不是同一个人" << endl;
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
#include <iostream>
#include <string>

using namespace std;

class MyPrint
{
public:
void operator()(string s)
{
cout << s << endl;
}
};

int main()
{
MyPrint p;
p("Hello World!");
MyPrint()("好耶!");
return 0;
}

输出结果:

1
2
Hello World!
好耶!

上述代码通过仿函数的方式实现了输出函数,$19$行使用的是匿名对象调用的方式,不创建一个具体的对象,直接对对象中的函数进行调用。

继承

基本语法

继承是面向对象三大特性之一

有些类与类之间存在特殊的关系,例如下图中:

继承关系

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复的代码。

继承的语法:

1
class 子类 :继承方式 父类

子类也称为派生类,父类也称为基类。

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>

using namespace std;

class EXAM
{
public:
void information()
{
cout << "考试科目为:" << this->name << endl;
cout << "该科目得分为:" << this->score << endl;
}

string name; //存储考试科目名称
int score; //存储得分
};

class MATH :public EXAM
{
public:
void other()
{
cout << "数学附加题得分:" << this->add << endl;
}

int add; //存储附加题得分(数学科目特有的附加题)
};

class ENGLISH :public EXAM
{
public:
void other()
{
cout << "英语听力得分:" << this->aural << endl;
}

int aural; //存储英语听力得分(英语科目特有的听力)
};

int main()
{
MATH exam1;
exam1.name = "数学";
exam1.score = 99;
exam1.add = 10;

ENGLISH exam2;
exam2.name = "英语";
exam2.score = 67;
exam2.aural = 25;


exam1.information();
exam1.other();
cout << endl;
exam2.information();
exam2.other();
return 0;
}

输出结果:

1
2
3
4
5
6
7
考试科目为:数学
该科目得分为:99
数学附加题得分:10

考试科目为:英语
该科目得分为:67
英语听力得分:25

这段代码定义了一个父类,用于存储和输出考试的信息,同时也是考试共有的信息,也就是每一个考试都拥有的基本信息。还有两个子类,分别继承了考试这个父类,两个子类各自有附加题和听力这两个独特的属性,因此需要在两个子类中分别定义一下。

继承方式

上一小节中,我们使用的继承方式是public,也就是公有继承,实际上,一共有三种继承方式:

  • 公有继承 public
  • 保护继承 protected
  • 私有继承 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
#include <iostream>

using namespace std;

class test
{
public:
int a;

protected:
int b;

private:
int c;
};

class son :public test
{
public:
int d;
};

int main()
{
cout << sizeof(son) << endl;
return 0;
}

输出结果:

1
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;

class Base
{
public:
Base()
{
cout << "Base构造函数" << endl;
}

~Base()
{
cout << "Base析构函数" << endl;
}
};

class Son : public Base

{
public:
Son()
{
cout << "Son构造函数" << endl;
}

~Son()
{
cout << "Son析构函数" << endl;
}
};

int main()
{
Son test;
return 0;
}

输出结果:

1
2
3
4
Base构造函数
Son构造函数
Son析构函数
Base析构函数

继承中的构造和析构顺序,应该是先构造父类,再构造子类,析构的顺序与构造的顺序相反。

同名成员处理方式

当子类和父类出现同名的成员,需要区别一下二者的访问方式:

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
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
#include <iostream>

using namespace std;

class Base
{
public:
Base()
{
this->value = 100;
}

int value;
};

class Son : public Base
{
public:
Son()
{
this->value = 200;
}

int value;
};

int main()
{
Son test;
cout << test.value << endl;
cout << test.Base::value << endl;
return 0;
}

输出结果:

1
2
200
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
#include <iostream>

using namespace std;

class Base
{
public:
static int value;
};

class Son : public Base
{
public:
static int value;
};

int Base::value = 100;
int Son::value = 200;

int main()
{
Son test;
cout << test.value << endl;
cout << test.Base::value << endl;
return 0;
}

输出结果:

1
2
200
100

需要注意的是,静态成员需要在类内声明,类外定义初始化,否则程序会报错。

多继承

C++允许一个类继承多个类。

基本语法:

1
class 子类 : 继承方式 父类1, 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分。

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

using namespace std;

class Base1
{
public:
Base1()
{
this->value = 100;
}

int value;
};

class Base2
{
public:
Base2()
{
this->value = 200;
}

int value;
};

class Son :public Base1, public Base2
{
public:
Son()
{
this->value = 300;
}

int value;
};

int main()
{
Son test;
cout << test.value << endl;
cout << test.Base1::value << endl;
cout << test.Base2::value << endl;
return 0;
}

输出结果:

1
2
3
300
100
200

上述代码的结果与同名成员的处理方式相类似,因此不在这里过多赘述。

总结一下:如果多继承中父类出现了同名情况,子类使用时要加作用域。

菱形继承

菱形继承指的是,两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承被称为菱形继承,或者钻石继承。

例如现在有一个动物类,有一个基本的属性是年龄,他的下面有两个子类,分别是马和驴,这两个子类还有一个共同的派生类叫做骡子。很明显,马和驴都各自有一个从动物类中继承过来的年龄属性,然后骡子类继承这两个类,就会拥有两个年龄属性。但是我们都知道,我们只需要一个年龄属性就够了,当使用骡子的年龄数据的时候,就会产生二义性。

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

using namespace std;

class Animal
{
public:
int age;
};

class ma :virtual public Animal
{

};

class lv :virtual public Animal
{

};

class luozi :public ma, public lv
{

};

int main()
{
luozi test;
test.ma::age = 6;
test.lv::age = 5;
test.age = 2;
cout << test.ma::age << endl;
cout << test.lv::age << endl;
cout << test.age << endl;
return 0;
}

输出结果:

1
2
3
2
2
2

为了解决上述问题,我们可以使用一个叫做虚继承的东西,即在继承之前,加上关键字virtual,让这个继承变为虚继承,被继承的类也叫做虚基类。使用这种方法,相当于继承下来一个地址,可以保证继承下来的数据只有一份,因此这三种方式都可以指向目标数据。

菱形继承带来的问题主要是子类继承两份相同的数据,导致资源浪费以及毫无意义,利用虚继承可以解决菱形继承问题。

多态

基本概念

多态是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
#include <iostream>

using namespace std;

class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};

class Cat :public Animal
{
public:
virtual void speak()
{
cout << "猫在说话" << endl;
}
};

class Dog :public Animal
{
public:
virtual void speak()
{
cout << "狗在说话" << endl;
}
};

void doSpeak(Animal& animal)
{
animal.speak();
}

int main()
{
Cat cat;
doSpeak(cat);

Dog dog;
doSpeak(dog);
return 0;
}

输出结果:

1
2
猫在说话
狗在说话

上述代码中,定义了一个基类,然后有定义了两个子类,他们都有speak函数。对于基类而言,将该函数定义为了虚函数,也就是前面加上了virtual关键字,这样可以实现动态多态,也就是所有继承该基类的子类,调用函数时可以调用自己的同名函数。这里有一个注意点,基类指针可以直接指向子类对象,不需要进行转换。

动态多态需要有继承关系,并且子类重写父类的虚函数,也就是函数返回值类型,函数名,参数列表完全一致。调用时使用父类的指针或者引用,可以直接执行子类对象。

多态原理

在父类中,如果只定义一个普通的函数,那么这个类占用的字节数为$1$(之前的小节中讲过这个问题)。如果我们定义的是虚函数,那么这个类就会占用$4$个字节,这就相当于定义了一个虚函数表指针(vfptr)。虚函数表中记录的是虚函数的地址,一个指针所占的字节数是$4$,所以其占用空间会发生变化。

如果子类重写父类的虚函数,那么子类中的虚函数表内部会替换成子类的虚函数地址,可以理解为覆盖掉父类的虚函数。当父类的指针或者引用指向子类对象时,会发生多态。

使用多态有如下优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

计算器案例

实现一个计算器,能够执行两个数字的加法、减法、乘法运算。

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

using namespace std;

class AbstractCalculator
{
public:
virtual int getRusult()
{
return 0;
}

int num1, num2;
};

class Add :public AbstractCalculator
{
public:
int getRusult()
{
return this->num1 + this->num2;
}
};

class Sub :public AbstractCalculator
{
public:
int getRusult()
{
return this->num1 - this->num2;
}
};

class Mul :public AbstractCalculator
{
public:
int getRusult()
{
return this->num1 * this->num2;
}
};

int main()
{
AbstractCalculator* calc;

//加法
calc = new Add;
calc->num1 = 5;
calc->num2 = 3;
cout << "加法运算结果:" << calc->getRusult() << endl;
delete calc;

//减法
calc = new Sub;
calc->num1 = 5;
calc->num2 = 3;
cout << "减法运算结果:" << calc->getRusult() << endl;
delete calc;

//乘法
calc = new Mul;
calc->num1 = 5;
calc->num2 = 3;
cout << "乘法运算结果:" << calc->getRusult() << endl;
delete calc;

return 0;
}

输出结果:

1
2
3
加法运算结果:8
减法运算结果:2
乘法运算结果:15

上述代码中,父类中只有一个虚函数和共有的两个数字的定义,这么做可以提高代码的可扩展性。在子类中重写父类的运算函数,通过定义不同类型的子类,来执行相应的运算。

如果使用传统方式进行代码编写,想要扩展新的功能的话,需要修改源码,但是在真实开发中,提倡开闭原则。

所谓开闭原则,就是对扩展进行开放,对修改进行关闭。

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。

因此可以将虚函数改为纯虚函数。

纯虚函数语法:

1
virtual 返回值类型 参数名(参数列表) = 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>

using namespace std;

class AbstractCalculator
{
public:
virtual int getRusult() = 0;

int num1, num2;
};

class Add :public AbstractCalculator
{
public:
int getRusult()
{
return this->num1 + this->num2;
}
};

class Sub :public AbstractCalculator
{
public:
int getRusult()
{
return this->num1 - this->num2;
}
};

class Mul :public AbstractCalculator
{
public:
int getRusult()
{
return this->num1 * this->num2;
}
};

int main()
{
AbstractCalculator* calc;

//加法
calc = new Add;
calc->num1 = 5;
calc->num2 = 3;
cout << "加法运算结果:" << calc->getRusult() << endl;
delete calc;

//减法
calc = new Sub;
calc->num1 = 5;
calc->num2 = 3;
cout << "减法运算结果:" << calc->getRusult() << endl;
delete calc;

//乘法
calc = new Mul;
calc->num1 = 5;
calc->num2 = 3;
cout << "乘法运算结果:" << calc->getRusult() << endl;
delete calc;

return 0;
}

输出结果:

1
2
3
加法运算结果:8
减法运算结果:2
乘法运算结果:15

在第$8$行定义了一个纯虚函数,因此这个类也是一个抽象类。

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。

例如下面这个例子:

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

using namespace std;

class Animal
{
public:
Animal()
{
cout << "Animal构造函数" << endl;
}

~Animal()
{
cout << "Animal析构函数" << endl;
}

virtual void speak() = 0;
};

class Cat :public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数" << endl;
this->name = new string(name);
}

~Cat()
{
cout << "Cat析构函数" << endl;
if (this->name != NULL)
{
delete this->name;
this->name = NULL;
}
}

void speak()
{
cout << *this->name << "猫正在说话" << endl;
}

string* name;
};

int main()
{
Animal* animal = new Cat("Tom");
animal->speak();
delete animal;
return 0;
}

输出结果:

1
2
3
4
Animal构造函数
Cat构造函数
Tom猫正在说话
Animal析构函数

上述例子中可以发现,就算是delete了父类对象,也不会去调用子类对象的析构函数,这就会导致内存没有办法直接释放,会造成内存占用。

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象。

虚析构语法:

1
virtual ~类名(){}

纯虚析构语法:

1
virtual ~类名() = 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
49
50
51
52
53
54
55
#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
Animal()
{
cout << "Animal构造函数" << endl;
}

virtual ~Animal()
{
cout << "Animal虚析构函数" << endl;
}

virtual void speak() = 0;
};

class Cat :public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数" << endl;
this->name = new string(name);
}

~Cat()
{
cout << "Cat析构函数" << endl;
if (this->name != NULL)
{
delete this->name;
this->name = NULL;
}
}

void speak()
{
cout << *this->name << "猫正在说话" << endl;
}

string* name;
};

int main()
{
Animal* animal = new Cat("Tom");
animal->speak();
delete animal;
return 0;
}

输出结果:

1
2
3
4
5
Animal构造函数
Cat构造函数
Tom猫正在说话
Cat析构函数
Animal虚析构函数

上述代码中,只更改了第$14$行,在前面加上了一个virtual关键字。通过这种方法,就会先去执行子类的析构函数,再执行父类的析构函数。

再来看一下另外一种方法,即纯虚析构:

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

using namespace std;

class Animal
{
public:
Animal()
{
cout << "Animal构造函数" << endl;
}

virtual ~Animal() = 0;

virtual void speak() = 0;
};

Animal::~Animal()
{
cout << "Animal纯虚析构函数调用" << endl;
}

class Cat :public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数" << endl;
this->name = new string(name);
}

~Cat()
{
cout << "Cat析构函数" << endl;
if (this->name != NULL)
{
delete this->name;
this->name = NULL;
}
}

void speak()
{
cout << *this->name << "猫正在说话" << endl;
}

string* name;
};

int main()
{
Animal* animal = new Cat("Tom");
animal->speak();
delete animal;
return 0;
}

输出结果:

1
2
3
4
5
Animal构造函数
Cat构造函数
Tom猫正在说话
Cat析构函数
Animal纯虚析构函数调用

纯虚析构与虚析构有一个区别,需要在外部再次定义一下这个函数。因为析构函数是对象在释放后必须执行的部分,所以必须对其进行内部操作的定义。语法强制纯虚析构函数必须有函数实现,有时父类也有一些数据开辟在堆区。

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类

文件操作

基本概念

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放。

通过文件可以将数据持久化

C++中对文件操作需要包含头文件<fstream>

文件类型分为两种:

  1. 文本文件 文件以文本的**ASCII码**形式存储在计算机中
  2. 二进制文件 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream:读操作
  3. fstream:读写操作

文本文件

写文件

写文件步骤如下:

  1. 包含头文件 #include <fstream>
  2. 创建流对象 ofstream ofs;
  3. 打开文件 ofs.open("文件路径", 打开方式);
  4. 写数据 ofs << "写入的数据";
  5. 关闭文件 ofs.close();

文件的打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意:文件打开方式可以配合使用,利用|操作符。

例如:用二进制方式写文件ios::binary | ios::out

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

using namespace std;

int main()
{
ofstream ofs; //创建流对象
ofs.open("test.txt", ios::out); //指定打开文件和打开方式
ofs << "Hello World!" << endl; //写内容
ofs.close(); //关闭文件
return 0;
}

文件中结果:

1
Hello World!

如果文件没有指定相对路径或者绝对路径,那么会在该程序所在目录创建一个相应的文件。

总结:

  • 文件操作必须包含头文件fstream
  • 读文件可以利用ofstream,或者fstream
  • 打开文件时需要指定操作文件路径和打开方式
  • 利用<<可以向文件中写数据
  • 操作完毕后,要关闭文件

读文件

读文件与写文件步骤相似,但是读取方式相对较多。

读文件步骤如下:

  1. 包含头文件 #include <fstream>
  2. 创建流对象 ifstream ifs;
  3. 打开文件 ifs.open("文件路径", 打开方式);
  4. 读数据 四种方式读取
  5. 关闭文件 ifs.close();

我们先来看一下文件中准备的数据:

1
2
Hello World!
My name is Bigglesworth.

第一种方式:

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

using namespace std;

int main()
{
ifstream ifs;
ifs.open("test.txt", ios::in);

if (!ifs.is_open())
{
cout << "文件打开失败!" << endl;
return 0;
}

char buf[1024] = { 0 };
while (ifs >> buf)
{
cout << buf << endl;
}

ifs.close();
return 0;
}

输出结果:

1
2
3
4
5
6
Hello
World!
My
name
is
Bigglesworth.

第二种方式:

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

using namespace std;

int main()
{
ifstream ifs;
ifs.open("test.txt", ios::in);

if (!ifs.is_open())
{
cout << "文件打开失败!" << endl;
return 0;
}

char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf)))
{
cout << buf << endl;
}

ifs.close();
return 0;
}

输出结果:

1
2
Hello World!
My name is Bigglesworth.

第三种方式:

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

using namespace std;

int main()
{
ifstream ifs;
ifs.open("test.txt", ios::in);

if (!ifs.is_open())
{
cout << "文件打开失败!" << endl;
return 0;
}

string buf;
while (getline(ifs, buf))
{
cout << buf << endl;
}

ifs.close();
return 0;
}

输出结果:

1
2
Hello World!
My name is Bigglesworth.

第四种方式:

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

using namespace std;

int main()
{
ifstream ifs;
ifs.open("test.txt", ios::in);

if (!ifs.is_open())
{
cout << "文件打开失败!" << endl;
return 0;
}

char c;
while ((c = ifs.get()) != EOF) //EOF end of line
{
cout << c;
}

ifs.close();
return 0;
}

输出结果:

1
2
Hello World!
My name is Bigglesworth.

总结:

  • 读文件可以利用ifstream,或者fstream
  • 利用is_open函数可以判断文件是否打开成功
  • close关闭文件

二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为ios::binary

写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型:

1
ostream& write(const char* buffer, int len);

字符指针buffer指向内存中一段存储空间,len是读写的字节数。

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

using namespace std;

int main()
{
ofstream ofs("data.txt", ios::out | ios::binary);
int a[10];
for (int i = 1; i <= 10; i++)
a[i - 1] = i * i;

ofs.write((const char*)&a, sizeof(a));

ofs.close();
return 0;
}

通过这种方式,可以得到一个二进制文件。

读文件

二进制方式读文件主要利用流对象调用成员函数red

函数原型:

1
istream& read(char* buffer, int len);

字符指针buffer指向内存中一段存储空间,len是读写的字节数。

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

using namespace std;

int main()
{
ifstream ifs("data.txt", ios::in | ios::binary);

if (!ifs.is_open())
{
cout << "文件打开失败!" << endl;
return 0;
}

int a[10];

ifs.read((char*)&a, sizeof(a));

for (int i = 0; i < 10; i++)
cout << a[i] << " ";

ifs.close();
return 0;
}

输出结果:

1
1 4 9 16 25 36 49 64 81 100

虽然二进制文件在输出时是乱码,但是读取的时候可以使用二进制方式正常读取。