面向对象的三大特性:封装、继承、多态

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

封装

封装的意义

封装是C++面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事务
  • 将属性和行为加以权限控制

在设计类的时候,属性和行为写在一起,表现事物

语法:class 类名 { 访问权限: 属性 / 行为 };

示例1:设计一个圆类,求圆的周长

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

using namespace std;

class Circle {
private:
const double Pi = 3.14;
double radius; // 半径

public:
// 构造函数
Circle(double radius) {
this->radius = radius;
}

// Getter
double getRadius() {
return this->radius;
}

// Setter
double setRadius(double radius) {
this->radius = radius;
}

// 计算周长
double getPerimeter() {
return 2 * this -> Pi * this -> radius;
}


};

封装的权限控制

封装共有三种访问权限:

  1. 公共权限——public
    • 成员:类内可以访问,类外可以访问
  2. 保护权限——protect
    • 成员:类内可以访问,类外不可以访问
    • 当具有继承关系后,子类可以访问父类中具有保护权限的成员
  3. 私有权限——private
    • 成员:类内可以访问,类外不可以访问
    • 当具有继承关系后,子类不可以访问父类中具有保护权限的成员

示例:

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

using namespace std;

class Person {
// 公共属性
public:
string name;
int age;
// 私有属性
private:
string id;
string bankCard;

// 公共方法
public:
Person(string name, int age,string id,string bankCard) {
this->name = name;
this->age = age;
this->id = id;
this->bankCard = bankCard;
}

string getId() {
return id;
}

string getBankCard() {
return bankCard;
}

void setId(string id) {
this->id = id;
}

void setBankCard(string bankCard) {
this->bankCard = bankCard;
}
};


int main() {
Person p("张三", 18, "A001", "A123456789");

cout << "姓名:" << p.name << endl;
cout << "年龄:" << p.age << endl;
// cout << "身份证:" << p.id << endl; // 不能直接访问
cout << "身份证:" << p.getId() << endl;
// p.bankCard = "B123456789" // 不能直接修改
p.setBankCard("B123456789");

return 0;
}

struct和class区别

在C++中struct和class唯一的区别就在于默认的访问权限不同

区别:

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

示例:

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

using namespace std;

struct S1 {
int s1;
};

class C1 {
int c1;
};


int main() {
S1 s1;
C1 c1;

s1.s1 = 1;
c1.c1 = 1; // 不可访问

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

using namespace std;

class XiaoMi {
private:
int price = 2999;
string color;

public:
// 只提供访问价格的方法,不提供修改价格的方法
int getPrice() {
return price;
}

string getColor() {
return color;
}

void setColor(string color) {
// 对输入数据校验
if (color == "white" || color == "black") {
this -> color = color;
}
else {
cout << "没有该型号" << endl;
}
}
};



int main() {
XiaoMi xiaomi;
xiaomi.setColor("white");
cout << xiaomi.getColor() << endl; // white

xiaomi.setColor("red"); // 没有该型号
cout << xiaomi.getColor() << endl; // white

//xiaomi.setPrice(); // 报错,没有该方法
cout << xiaomi.getPrice() << endl; // 2999

return 0;
}

练习案例:设计矩形

设计矩形(Rectangle)

求出矩形的表面积和体积

分别用全局函数和成员函数判断两个立方体是否相等

参考答案:

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

using namespace std;

class Rectangle
{
private:
int height;
int length;
int width;

public:
Rectangle(int h, int l, int w) {
height = h;
length = l;
width = w;
}

// Getter And Setter
void setHeight(int h) {
height = h;
}

int getHeight() {
return height;
}

void setLength(int l) {
length = l;
}

int getLength() {
return length;
}

void setWidth(int w) {
width = w;
}

int getWidth() {
return width;
}

// 计算体积
int getVolume() {
return height * length * width;
}

// 计算表面积
int getSurfaceArea() {
return 2 * (height * length + height * width + length * width);
}


// 计算两矩形是否相等
bool isEqual(Rectangle c2) {
return height == c2.height && length == c2.length && width == c2.width;
}
};

// 计算两矩形是否相等
bool isEqual(Rectangle c1, Rectangle c2) {
return c1.isEqual(c2);
}

int main() {
Rectangle c1(3, 4, 5);
Rectangle c2(3, 4, 5);
Rectangle c3(5, 4, 3);

cout << "c1和c2是否相等:" << (isEqual(c1, c2) ? "是" : "否") << endl;
cout << "c1和c2是否相等:" << (c1.isEqual(c2) ? "是" : "否") << endl;
cout << "c1和c3是否相等:" << (isEqual(c1, c3) ? "是" : "否") << endl;

cout << "c1的体积为:\t" << c1.getVolume() << endl;
cout << "c1的表面积为:\t" << c1.getSurfaceArea() << endl;

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

using namespace std;

class C {
private:
int a;
public:
// Constructor
C() {} // 无参构造

C(int a) { // 有参构造
this->a = a;
}

// Getter And Setter
int getA() {
return a;
}

void setA(int a) {
this->a = a;
}
};

int main() {
C c1; // 创建在栈中
c1.setA(10);

C c2(11); // 创建在栈中

cout << c1.getA() << endl; // 10
cout << c2.getB() << endl; // 20

return 0;
}

堆区创建对象

和栈区创建对象类似,分为有初始值和无初始值

无初始值:类名* 对象名 = new 类名();

有初始值:类名* 对象名 = new 类名(值1);

示例:

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

using namespace std;

class C {
private:
int a;
public:
// Constructor
C() {}

C(int a) {
this->a = a;
}

// Getter And Setter
int getA() {
return a;
}

void setA(int a) {
this->a = a;
}
};

int main() {
C* c1 = new C();
c1->setA(10);

C* c2 = new C(20);

cout << c1->getA() << endl; // 10
cout << c2->getA() << endl; // 20

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

using namespace std;

class C {
private:
int a;
public:
// Constructor
C() {}

C(int a) {
this->a = a;
}

// Getter And Setter
int getA() {
return a;
}

void setA(int a) {
this->a = a;
}
};

int main() {
C(10); // 匿名对象

return 0;
}

构造函数和析构函数

对象的初始化和清理是两个十分重要的安全问题

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

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

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

对象的初始化和清理工作是编译器强制要求程序员完成的事情,因此如果程序员不提供构造和析构,编译器会自动提供构造函数和析构函数(不过是空实现)

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

构造函数语法:类名() {}

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

析构函数语法:~类名() {}

  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
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 {
private:
string name;

public:
// Constructor
Person() {
name = "";
cout << "无参构造函数被调用了" << endl;
}

Person(string name) {
this->name = name;
cout << "有参构造函数被调佣了" << endl;
}

// Getter And Setter
string getName() {
return name;
}

void setName(string name) {
this->name = name;
}


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

int main() {
// 为了不让编译器自动调用析构函数,所以在堆区创建对象
Person* p1 = new Person("张三"); // 有参构造函数被调佣了
Person* p2 = new Person(); // 无参构造函数被调佣了

delete p1; // 析构函数被调用了
delete p2; // 析构函数被调用了

return 0;
}

构造函数的分类及调用

两种分类方式:

  1. 按参数分为:
    • 有参构造
    • 无参构造
  2. 按类型分为:
    • 普通构造
    • 拷贝构造

三种调用方式:

  1. 括号法
  2. 显示法
  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
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
#include <iostream>
#include <string>

using namespace std;

class Person {
private:
string name;

public:
// Constructor
Person() {
name = "";
cout << "无参构造函数被调用了" << endl;
}

Person(string name) {
this->name = name;
cout << "有参构造函数被调佣了" << endl;
}

Person(const Person& p) {
name = p.name;
this->setName(name);
cout << "拷贝构造函数被调用了" << endl;
}

// Getter And Setter
string getName() {
return name;
}

void setName(string name) {
this->name = name;
}


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

int main() {
Person* p1 = new Person("张三");
Person* p2 = new Person(*p1);

cout << "p1的姓名是:" << p1->getName() << endl; // 张三
cout << "p2的姓名是:" << p2->getName() << endl; // 张三

// 括号法
Person p3;
Person p4("王五");

// 显示法
Person p5 = Person("赵六");

// 匿名对象
// 不要利用拷贝构造函数创建匿名对象,如Person(p3);编译器会认为Person(p3) === Person p3;
Person("李四");

// 隐式转换法
Person p6 = (string)"Hello"; // C++禁止连续的用户定义隐式转换,所以需要加上(string)
Person p7 = p5;

return 0;
}

拷贝构造函数调用时机

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <string>

using namespace std;

class C {
private:
int a;

public:
C() {
cout << "C()" << endl;
}

C(int a) {
this->a = a;
cout << "C(int)" << endl;
}

C(const C& c) {
this->a = c.a;
cout << "C(const C&)" << endl;
}

~C() {
cout << "~C()" << endl;
}


// Getter And Setter
int getA() {
return a;
}

void setA(int a) {
this->a = a;
}
};

// 1. 拷贝构造函数初始化对象
void func01() {
C c1(10); // C(int)
C c2(c1); // C(const C&)
}

// 2. 值传递方式给函数参数传递
void test1(C c) {};

void func02() {
C c1(10); // C(int)
test1(c1); // C(const C&) 相当于创建了一个临时对象当作函数参数
}

// 3. 值方式返回局部对象
C test2() {
return C(10); // C(int);
}

void func03() {
C c1 = test2(); // C(const C&) 注意这里不同编译器不同版本结果不同
}

int main() {
//func01();
//func02();
func03();

return 0;
}

构造函数的调用规则

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

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

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数

⭐深拷贝与浅拷贝⭐

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

特性 浅拷贝 (Shallow Copy) 深拷贝 (Deep 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
#include <iostream>
#include <string>

using namespace std;

class Person {
public:

string name;
int* age;

// 构造函数
Person(string name, int age) {
this->name = name;
this->age = new int(age); // 在堆区分配,还需要手动释放,new返回指针
cout << "构造函数" << endl;
}

// 析构函数
~Person() {
if (age != nullptr) {
delete age;
age = nullptr;
}
cout << "析构函数" << endl;
}
};


int main() {
Person p1("张三", 18);
cout << "p1的名字:" << p1.name << "," << "p1的年龄:" << *p1.age << endl;

Person p2(p1); // 如果是编译器生成的拷贝构造函数,那么就会进行浅拷贝
cout << "p2的名字:" << p2.name << "," << "p2的年龄:" << *p2.age << endl;

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

using namespace std;

class Person {
public:

string name;
int* age;

// 构造函数
Person(string name, int age) {
this->name = name;
this->age = new int(age); // 在堆区分配,还需要手动释放,new返回指针
cout << "构造函数" << endl;
}

// 自行定义拷贝函数
Person(const Person& p) {
this->name = p.name;
// this->age = p.age; 编译器默认实现就是这行
this->age = new int(*p.age); // 重新在堆区申请一个空间,并赋值
cout << "拷贝构造函数" << endl;
}

// 析构函数
~Person() {
if (age != nullptr) {
delete age;
age = nullptr;
}
cout << "析构函数" << endl;
}
};


int main() {
Person p1("张三", 18);
cout << "p1的名字:" << p1.name << "," << "p1的年龄:" << *p1.age << endl;

Person p2(p1); // 如果是编译器生成的拷贝构造函数,那么就会进行浅拷贝
cout << "p2的名字:" << p2.name << "," << "p2的年龄:" << *p2.age << endl;

return 0;
}

初始化列表(推荐初始方法)

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

语法:构造函数(): 属性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
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>

using namespace std;

class C {
private:
int a;
int b;
int c;
public:
// 初始化列表
C() : a(10), b(20), c(30) {} // 本质就是带有默认值的无参构造函数

C(int a, int b, int c){
this->a = a;
this->b = b;
this->c = c;
}

void print(){
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
};


int main() {
C c1;
c1.print(); // a = 10 b = 20 c = 30


return 0;
}

类对象作为类成员

C++中的成员可以是另一个类的对象

例如:

1
2
3
4
class A {}
class B {
A a;
}

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

示例:

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

using namespace std;

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

~A() {
cout << "调起了A析构函数" << endl;
}
};

class B {
private:
A a;

public:
B() {
cout << "调起了B构造函数" << endl;
}

B(A a) {
this -> a = a;
}

~B() {
cout << "调起了B析构函数" << endl;
}
};


int main() {
B b; // 顺序: 调用A构造函数 -> 调用B构造函数 -> 调用B析构函数 -> 调用A析构函数


return 0;
}

结论:构造函数顺序是先构造自身,再构造其它类,析构先析构其他类,最后析构自身

静态成员

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

静态成员分为:

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

静态成员变量

示例:

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 C {
public:
// 类内声明
static int a;
int b;

C(int b) {
this ->b = b;
}
};

// 类外初始化
int C::a = 10;





int main() {

C c1(20);
C c2(30);

cout << c1.a << endl; // 10
cout << c2.a << endl; // 10

C::a = 100;

cout << c1.a << endl; // 100
cout << c2.a << endl; // 100

c2.a = 200;

cout << c1.a << endl; // 200
cout << c2.a << endl; // 200

return 0;
}

注意:类的静态变量初始化不能写在main函数中

这是因为类的静态成员变量属于类本身而非对象实例,其生命周期与程序一致,必须在 main 函数执行前完成初始化
main 函数是程序执行的入口,此时已进入运行时阶段,无法满足静态成员在编译期/链接期的初始化要求

静态成员方法

示例:

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

using namespace std;

class C {
public:
// 类内声明
static int a;
int b;

C(int b) {
this ->b = b;
}

static void printStatic() {
cout << a << endl;
// cout << b << endl; // 静态方法不能访问非静态成员变量
}

void print() {
cout << a << endl; // 非静态方法可以访问静态成员变量
cout << b << endl;
}
};

// 类外初始化
int C::a = 10;


int main() {

C c1(1);
C::printStatic();
c1.print();

return 0;
}

C++对象模型和this指针

成员变量和成员函数分开存储

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

using namespace std;

// 成员变量和成员函数是分开存储的
class EmptyClass {};

class C {
public:
int a; // 属于类的对象上

static int b; // 不属于类的对象上

void func() {}; // 不属于类的对象上

static void staticfunc() {}; // 不属于类的对象上
};

int C::b = 100;

int main() {

EmptyClass ec;
cout << sizeof(ec) << endl; // 空对象占用内存大小为1字节

C c;
cout << sizeof(c) << endl;
// 占用内存大小为4字节,因为C类中有一个非静态成员变量数据类型为int
// 如果再添加一个非静态成员变量,且该变量数据类型为int,那么对象占用内存大小为8字节
// 但是静态成员变量、成员方法、静态成员方法都不属于类的对象,所以对象占用内存大小为4字节

return 0;
}

this指针概念

每一个静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码

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

using namespace std;


class C {
public:
int a;
int b;

C() {
a = 0;
b = 0;
}

C(int a,int b) {
this->a = a;
this->b = b;
}

C(const C& c) {
a = c.a;
b = c.b;
}

C getMySelf() {
return *this;
}

C& addC(const C& c) { // 返回值是对象引用,所以可以链式调用
this->a += c.a;
this->b += c.b;
return *this;
}

C addCwhitNoRef(const C& c) {
this->a += c.a;
this->b += c.b;
return *this;
}
};


int main() {
C c1(10, 20);

C c2(c1.getMySelf());

cout << "c1.a = " << c1.a << ",c1.b = " << c1.b << endl; // c1.a = 10,c1.b = 20
cout << "c2.a = " << c2.a << ",c2.b = " << c2.b << endl; // c2.a = 10,c2.b = 20

c1.addC(c2).addC(c2).addC(c2);

cout << "c1.a = " << c1.a << ",c1.b = " << c1.b << endl; // c1.a = 40,c1.b = 80

c2.addCwhitNoRef(c1).addCwhitNoRef(c1).addCwhitNoRef(c1);
/*
实际执行过程:
第一次addCwhitNoRef(c1):修改原c2对象值(c2.a=10+40=50,c2.b=20+80=100),返回临时拷贝对象(值为50,100)
第二次addCwhitNoRef(c1):在临时对象上操作(50+40=90,100+80=180),返回新临时对象
第三次addCwhitNoRef(c1):在第二个临时对象上操作(90+40=130,180+80=260)
最终结果:只有第一次调用修改了原c2对象,后续操作均在临时对象上进行,因此最终c2.a=50,c2.b=100
要获取正确的值需要创建一个新对象接收临时对象的值
*/

cout << "c2.a = " << c2.a << ",c2.b = " << c2.b << endl; // c2.a = 50,c2.b = 100

return 0;
}

空指针访问成员函数

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
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string>

using namespace std;


class C {
public:
int a;

void ShowClassName() {
// 空指针对象调用该方法没有问题
cout << "我是Person类" << endl;
}

void ShowNum() {
// 空指针对象调用该方法会出现问题(未定义行为)
// 这是因为此时this指针为空,表示对象不存在,所以访问对象成员变量会出错
cout << this->a << endl;
}

void ShowRightNum() {
if (this == NULL) {
return;
}
else {
cout << this->a << endl;
}
}
};


int main() {
C* c = NULL;

c->ShowClassName();
// c->ShowNum();
c->ShowRightNum();


return 0;
}

const修饰成员函数

常函数:

  • 成员函数后加const后称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改
  • 语法:void func() const {}

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
  • 语法:const 类名 对象名();

示例:

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 A {
public:
int a;
mutable int b;

A() {
cout << "A()" << endl;
}

A(int a,int b) {
cout << "A(int)" << endl;
this->a = a;
this->b = b;
}

// void setA(int a) const {
// this->a = a; // 报错,因为const函数不能改变成员变量的值
// }

void setB(int b) const {
this->b = b; // 允许,因为const函数可以改变mutable成员变量的值
}
};

class B {
public:
void ShowClassName() {
cout << "B" << endl;
}

void ShowWord() const {
cout << "Hello World" << endl;
}
};


int main() {
A a(1, 2);
a.setB(4);

cout << "a.a = " << a.a << "," << "b.b = " << a.b << endl;

const B b;

// b.ShowClassName(); // 报错,常对象不能访问非 常函数
b.ShowWord();


return 0;
}

注意事项1:常函数的const位置在函数名()后面加

注意事项2:常对象不是指创建类时添加const,而是在声明对象时添加

友元(目前不知道具体用处)

假设你有一个家庭,家里有一个 保险箱(类 SafeBox),它有一些私密的属性,比如密码、存放的贵重物品等。这些信息是 私有的(private),只有家庭成员才能知道和使用。
但有一天,你请了一个 家政服务人员(类 Cleaner) 来打扫卫生,他不是你的家人,但你想让他临时能打开保险箱,把一些贵重物品拿出来擦洗。
在现实生活中,你可以选择 信任他,并告诉他密码 —— 这就相当于在程序中将他设为“友元”。

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

友元的关键字: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
#include <iostream>
#include <string>

using namespace std;

class SafeBox {
friend void Cleaner(SafeBox &safebox); // friend可以使该函数变为公共函数,这样就能访问私有成员了

private:
int pin;
string valuable;

public:
SafeBox(int pin, string valuable) {
this->pin = pin;
this->valuable = valuable;
}
};

void Cleaner(SafeBox &safebox) {
cout << "Cleaning..." << endl;
safebox.pin = 0;
safebox.valuable = "";
cout << "Cleaned!" << endl;
cout << "Pin: " << safebox.pin << endl;
cout << "Valuable: " << safebox.valuable << endl;
}

int main() {

SafeBox safebox(1234, "Money");
Cleaner(safebox);


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

using namespace std;

class SafeBox {
private:
string password = "123456"; // 密码初始值
string valuable = "Necklace"; // 贵重物品初始值

// 类 Cleaner 是 SafeBox 的友元类
friend class Cleaner;
};

class Cleaner {
public:
void openSafeBox(SafeBox& box) {
cout << "Cleaner opened the safe box." << endl;
cout << "Treasure inside: " << box.valuable << endl;
cout << "Password is: " << box.password << endl;
}
};


int main() {
SafeBox box;
Cleaner cleaner;

cleaner.openSafeBox(box);



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

using namespace std;

class SafeBox; // 前向声明SafeBox类

class Cleaner {
public:
void cleanRoom(SafeBox& box); // 声明函数(定义后置)
void vacuumCarpet() {
cout << "Vacuuming the carpet. No access to safe box." << endl;
}
};

class SafeBox {
private:
string password = "123456";
string valuable = "Diamond Ring";
friend void Cleaner::cleanRoom(SafeBox& box); // ✅ 合法声明
};

// Cleaner成员函数实现(需在SafeBox定义之后)
void Cleaner::cleanRoom(SafeBox& box) {
cout << "Cleaner is cleaning the room." << endl;
cout << "He can see the treasure: " << box.valuable << endl;
cout << "And also knows the password: " << box.password << endl;
}




int main() {

SafeBox box;
Cleaner cleaner;

cleaner.cleanRoom(box); // ✅ 可以访问 SafeBox 的私有成员
cleaner.vacuumCarpet(); // ❌ 不能访问 SafeBox 的私有成员


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

using namespace std;

class Cleaner;

class SafeBox {
private:
string password = "123456";
string valuable = "Diamond Ring";

// 允许 Cleaner 类中的 cleanRoom 函数作为友元
friend void Cleaner::cleanRoom(SafeBox& box);
};

class Cleaner {
public:
void cleanRoom(SafeBox& box) {
cout << "Cleaner is cleaning the room." << endl;
cout << "He can see the treasure: " << box.valuable << endl;
cout << "And also knows the password: " << box.password << endl;
}

void vacuumCarpet() {
cout << "Vacuuming the carpet. No access to safe box." << endl;
// 不能访问 SafeBox 的 private 成员
// cout << box.password; // ❌ 编译错误
}
};


int main() {

SafeBox box;
Cleaner cleaner;

cleaner.cleanRoom(box); // ✅ 可以访问 SafeBox 的私有成员
cleaner.vacuumCarpet(); // ❌ 不能访问 SafeBox 的私有成员


return 0;
}

核心错误原因:

SafeBox类声明friend void Cleaner::cleanRoom(SafeBox& box)时:

  1. 此时Cleaner类只有前向声明(class Cleaner;
  2. 编译器不知道Cleaner类中有cleanRoom成员函数(类定义未完成)
  3. 引发编译错误:'cleanRoom' is not a member of 'Cleaner'

通俗类比:

想象您要给朋友(Cleaner)一把家门钥匙(友元权限),但:

  1. 您提前说了句”我有个朋友叫张三”(前向声明)
  2. 然后直接说”给张三的手枪装子弹权限”(声明友元成员函数)
  3. 结果警察发现张三根本没有手枪(成员函数未定义)

运算符重载

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

加号运算符重载

作用:实现两个自定义数据类型相加的运算,比如C c1 = c2 + c3;

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 FruitBasket {
public:
int apples;
int bananas;

// 构造函数
FruitBasket(int a = 0, int b = 0) : apples(a), bananas(b) {}

// 加号运算符重载(成员函数)
FruitBasket operator+(const FruitBasket& fruitbasket) const { // 常函数,保证不修改当前对象的状态
return FruitBasket(this->apples + fruitbasket.apples, this->bananas + fruitbasket.bananas);
}

// 打印函数
void print() const {
cout << "Apples: " << apples << ", Bananas: " << bananas << endl;
}
};


// 函数也可以运算符重载
FruitBasket operator+(const FruitBasket& f, int number) {
FruitBasket res;
res.apples = f.apples + number;
res.bananas = f.bananas + number;

return res;
}




int main() {

FruitBasket f1(10, 20);
FruitBasket f2(15, 20);

FruitBasket f3 = f1 + f2;

f3.print(); // Apples: 25, Bananas: 40

FruitBasket f4 = f1 + 5;
f4.print(); // Apples: 15, Bananas: 25

return 0;
}

成员函数重载本质调用为:Fruitbasket f3 = f1.operator+(f2);

普通函数重载本质调用为:Fruitbasket f4 = operator+(f1,5);

左移运算符重载(cout重写)

作用:可以输出自定义数据类型,符号位<<

提示:Java中的toString函数重写

注意:实现左移运算符重载,不能在类中实现,需要利用全局函数实现,这一点需要与Java区分开

示例:

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

using namespace std;

class FruitBasket {
public:
int apples;
int bananas;

// 构造函数
FruitBasket(int a = 0, int b = 0) : apples(a), bananas(b) {}

};

// 左移运算符重载,只能利用全局函数
void operator<<(ostream &out, const FruitBasket &fb) {
out << "There are " << fb.apples << " apples and " << fb.bananas << " bananas in the basket." << endl;
}




int main() {
FruitBasket fb(10, 20);
cout << fb;

return 0;
}
关于为什么左移运算符不能在类中实现

问题本质:cout << obj 的调用机制

你希望实现这样的语法:

FruitBasket fb(10, 20); cout << fb;

这实际上等价于:

operator<<(cout, fb);

也就是说,第一个参数是 ostream 对象(如 cout),而 第二个参数才是你的类对象。

为什么不能用成员函数?

成员函数重载的规则:

  • 当你将一个运算符重载为成员函数时,左边的操作数自动是当前对象(即 *this)
  • 所以对于 a + b,如果写成成员函数,它只能成为 a.operator+(b),即第一个操作数必须是该类的对象

应用于 <<:

  • 如果你在 FruitBasket 类中写成:void operator<<(const FruitBasket& other);
  • 那么调用会变成:fb1 << fb2; // 等价于 fb1.operator<<(fb2)
  • 但你想写的是:cout << fb; // ostream << FruitBasket

而 cout 是 ostream 类型的对象,并不是你的类对象,所以无法用成员函数处理。

如果尝试把 void operator<<ostream &out, const FruitBasket &fb)直接写在类中,是 不允许的,原因如下:

语法错误:不能将二元运算符重载为类成员函数时有两个参数

在 C++ 中,所有作为类成员函数的运算符重载,最多只能有一个显式参数(即右边的操作数)

其次在调用时,不符合直觉

        
FruitBasket fb1, fb2;
fb1 << (cout, fb2); // ❌ 非法调用方式,不符合直觉
        
    

如果输出时涉及到类的私有属性,还需要将重载函数添加为友元

递增运算符重载

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

效果如下:

1
2
3
cout << myint << endl;
cout << ++myint << endl;
cout << myint++ << endl;

示例:

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 FruitBasket {
friend ostream &operator<<(ostream &os, const FruitBasket &fb); // 将全局输出函数设置为友元

private:
int apples;

public:
// 构造函数
FruitBasket(int a = 0) : apples(a) {}

// 1. 递增函数重载
// 1.1 前置递增
FruitBasket& operator++() { // 这里返回的是引用对象,否则会返回一个对象副本
this->apples++; // 当++(++fb)这样的形式调用时,多次调用++时,会返回一个对象副本,而不是引用对象
return *this;
}

// 1.2 后置递增
FruitBasket operator++(int) {
FruitBasket temp = *this;
this->apples++;
return temp;
}
};

ostream &operator<<(ostream &os, const FruitBasket &fb) {
os << "There are " << fb.apples << " apples in the basket." << endl;
return os;
}




int main() {
FruitBasket fb(10);
cout << fb << endl; // 10
cout << ++fb << endl; // 前置递增
cout << fb++ << endl; // 后置递增
cout << fb << endl;
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
// 简化代码
// ...
FruitBasket operator++() {
this->apples++;
return *this;
}
// ...

int main() {
FruitBasket fb(10);
cout << ++(++fb) << endl; // 12
cout << fb << endl; // 11
}

/*
问题说明:
第一次 ++fb 执行:

原对象 fb.apples → 11
返回值:生成临时对象副本 temp1(apples=11)
第二次 ++(temp1) 执行:

临时对象 temp1.apples → 12
返回值:生成新临时对象 temp2(apples=12)
输出 temp2 → 显示 12
最终 fb 的状态:

仅被递增一次 → apples=11
*/

赋值运算符重载

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

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

深拷贝与浅拷贝这章,提到过如果像将一个对象直接赋值给另一个对象,可以通过拷贝构造函数实现C c1(c2)

这一章,则是利用赋值运算符实现C c1 = c2,正常来说,编译器会自动添加赋值运算符的重载函数,所以也会出深拷贝这一章中相同的问题重复释放导致崩溃(重复删除了同一个堆区的资源)

下面是错误案例

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

using namespace std;

class C {
public:
int *a;

C(int a) {
this->a = new int(a);
}

~C() {
if (a != nullptr) {
delete a;
a = nullptr;
}
}
};

int main() {
C c1(1);
C c2(2);
c2 = c1;

cout << *c1.a << endl;
cout << *c2.a << endl;

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

using namespace std;

class C {
public:
int *a;

C(int a) {
this->a = new int(a);
}

~C() {
if (a != nullptr) {
delete a;
a = nullptr;
}
}

C& operator=(const C &c) {
if (this->a != nullptr) { // 判断是否有值在堆区
delete this->a;
this->a = nullptr;
}

this->a = new int(*c.a);
return *this;
}
};





int main() {
C c1(1);
C c2(2);
C c3(3);
c3 = c2 = c1; // 想要实现连续赋值效果,需要赋值运算符函数的返回值为当前对象的自身(引用对象)

cout << *c1.a << endl;
cout << *c2.a << endl;
cout << *c3.a << endl;

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

using namespace std;

class C {
public:
int a;
string b;

C(int a,string b) {
this->a = a;
this->b = b;
}



bool operator==(const C &c) {
return (this->a == c.a && this->b == c.b) ? true : false;
}

bool operator!=(const C &c) {
return !(*this == c);
}
};





int main() {
C c1(10, "a");
C c2(10, "b");
C c3(10, "c");

cout << (c1 == c1) << endl; // 1
cout << (c1 != c1) << endl; // 0
cout << (c1 == c2) << endl; // 0
cout << (c1 != c3) << endl; // 1

return 0;
}

函数调用运算符重载

  • 函数调用运算符()也可以重载

  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数

  • 仿函数没有固定写法,非常灵活

示例:

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

using namespace std;

class Printer {
public:
// 重载调用函数
void operator()(string s) {
cout << s << endl;
}
};


int main() {
Printer p;
p("hello world"); // 与全局函数调用很相似,所以称为仿函数

Printer()("hello world"); // 匿名对象调用仿函数

return 0;
}

继承

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

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

1544861202252

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

这个时候就可以考虑继承的技术来减少代码冗余

继承的基本语法

语法: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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <iostream>
#include <string>

using namespace std;

class Animal {
private:
string name;
int age;
int weight;
string color;

public:
Animal(string name, int age, int weight, string color) : name(name), age(age), weight(weight), color(color) {}

void eat() {
cout << this->name << "is Eating..." << endl;
}

// Getter Setter
#pragma region Getter Setter
string getName() const {
return name;
}

void setName(string name) {
this->name = name;
}

int getAge() const {
return age;
}

void setAge(int age) {
this->age = age;
}

int getWeight() const {
return weight;
}

void setWeight(int weight) {
this->weight = weight;
}

string getColor() const {
return color;
}

void setColor(string color) {
this->color = color;
}
#pragma endregion
};

class Dog : public Animal { // 子类

public:
Dog(string name, int age, int weight, string color) : Animal(name, age, weight, color) {}

void bark() {
cout << this->getName() << " is Barking..." << endl; // 利用Getter访问私有属性
}
};

class Cat : public Animal { // 子类
public:
Cat(string name, int age, int weight, string color) : Animal(name, age, weight, color) {}
void meow() {
cout << this->getName() << " is Meowing..." << endl; // 利用Getter访问私有属性
}
};

// override operator<<
ostream& operator<<(ostream& os, const Animal& animal) { // 这里声明的常对象,常对象只能调用常函数,所以需要将Getter方法声明为常函数
os << "Name: " << animal.getName() << endl;
os << "Age: " << animal.getAge() << endl;
os << "Weight: " << animal.getWeight() << endl;
os << "Color: " << animal.getColor() << endl;
return os;
}


int main() {
Dog dog("Dog", 2, 5, "White");
dog.bark();

Cat cat("Cat", 3, 4, "Black");
cat.meow();

cout << dog << endl;
cout << cat << endl;

return 0;
}

继承方式

根据上一章继承的基本语法:class 子类 : 继承方式 父类

继承方式中一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

extend_02

父类中私有属性,无论是什么继承方式,都不可访问

当继承方式为共有继承时,子类会保留父类中的属性访问权限

当继承方式为保护继承时,子类会将父类中的属性访问权限都修改为保护权限

当继承方式为私有继承时,子类会将父类中的属性访问权限都修改为私有权限

示例:

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

using namespace std;

class A {
public:
int a;
protected:
int b;
private:
int c;

public:
A(int a, int b, int c) : a(a), b(b), c(c) {};
};

class B : public A {
public:
B(int a, int b, int c) : A(a, b, c) {};

void printB() {
this->a;
this->b;
// this->c; // error
}
};

class C : protected A {
public:
C(int a, int b, int c) : A(a, b, c) {};

void printC() {
this->a;
this->b;
// this->c;
}
};

class D : private A {
public:
D(int a,int b,int c) : A(a,b,c) {};

void printD() {
this->a;
this->b;
// this->c;
}
};


int main() {
// 公有继承
B b(1,2,3);
cout << b.a << endl;
// cout << b.b << endl; // b.b为protected


// 保护继承
C c(1,2,3);
// cout << c.a << endl; // c.c为protected
// cout << c.b << endl; // c.b为protected

// 私有继承
D d(1,2,3);
// cout << d.a << endl; // d.a为private
// cout << d.b << endl; // d.b为private

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

using namespace std;

class A {
private:
int a;
protected:
int b;
public:
int c;
};

class B : public A{
public:
int d;
};

class C : protected A {
public:
int d;
};

class D : private A {
public:
int d;
};

int main() {

B b;
cout << sizeof(b) << endl; // 16,说明继承了A类中的所有属性

C c;
cout << sizeof(c) << endl; // 16

D d;
cout << sizeof(d) << endl; // 16

/*
父类中私有属性成员,只是被编译器隐藏了,因此访问不到,但确实是继承下去的
所以会占有内存空间
利用开发人员命令提示工具查看对象模型
跳转到源码的地址下,输入命令:
cl /d1 reportSingleClassLayout类名 文件名
比如:cl /d1 reportSingleClassLayoutB B.cpp
*/

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

using namespace std;

class A {
public:
int a;
A(int a) : a(a) {
cout << "调用了A类的构造函数" << endl;
}

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

class B : public A {
public:
int b;
B(int a, int b) : A(a), b(b) {
cout << "调起了B类的构造函数" << endl;
}

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

int main() {
B b(1, 2); // A B B A
/*
调用了A类的构造函数
调起了B类的构造函数
调起了B类的析构函数
调用了A类的析构函数
*/

return 0;
}

结论:构造函数顺序:先有父后有子;析构与构造的顺序相反:先析构子后析构父

继承中同名成员的处理方式

在子类对象后添加作用域:b.A::a(b表示B类对象b,它的父类是A,所以访问A类中的属性a需要添加上作用域)

示例:

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 A {
public:
int a;
A() {
a = 10;
}

void print() {
cout << "A类中的属性a为:" << a << endl;
}
};

class B : public A {
public:
int a;

B() {
a = 20;
}

void print() {
cout << "B类中的属性a为:" << a << endl;
}
};

int main() {
B b;

// 同名成员变量的访问方式:
cout << b.a << endl; // 20,表示子类的成员变量
cout << b.A::a << endl; // 10,如果通过子类对象访问到父类中同名成员,需要加作用域

// 同名成员函数的访问方式:
b.print(); // B类中的属性a为:20
b.A::print(); // A类中的属性a为:10

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

using namespace std;

class A {
public:
static int a;

static void print() {
cout << a << endl;
}
};

int A::a = 10;

class B : public A {
public:
static int a;

static void print() {
cout << a << endl;
}
};

int B::a = 20;


int main() {
B b;

cout << b.a << endl; // 20
cout << b.A::a << endl; // 10

cout << B::a << endl; // 20
cout << B::A::a << endl; // 10

b.print(); // 20
b.A::print(); // 10

B::print(); // 20
B::A::print(); // 10

return 0;
}

总结:同名静态成员处理方式和非静态处理方式,只不过有两种访问的方式(通过对象和通过类名)

多继承语法

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

语法:class 子类 : 继承方式 父类1, 继承方式2 父类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
#include <iostream>
#include <string>

using namespace std;

class A {
public:
int a;
A() : a(10) {}
};

class B {
public:
int a;
B() : a(20) {}
};

class C : public A , public B{ // 继承于A和B
public:
int a;
C() : a(30) {}
};



int main() {
C c;
cout << c.a << endl; // 30
cout << c.B::a << endl; // 20
cout << c.A::a << endl; // 10


return 0;
}

菱形继承(虚继承)

lxjc

示例:

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 Top {
public:
int a = 10;
};

class Middle1 : public Top {};

class Middle2 : public Top {};

class Bottom : public Middle1, public Middle2 {};



int main() {
Bottom b;
//b.a = 20; // 报错,提示Bottom::a不明确

b.Middle1::a = 20;
b.Middle2::a = 30;

// 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
cout << "b.Middle1::a = " << b.Middle1::a << endl;
cout << "b.Middle2::a = " << b.Middle2::a << endl;

// 实际上,b只需要一份数据即可,菱形继承导致数据有两份,资源浪费


return 0;
}

解决方法——虚继承:

在继承之前加上virtual,如class Middle1 :virtual public Top{}

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

using namespace std;

class Top {
public:
int a = 10;
};

class Middle1 :virtual public Top {}; // 在继承前加上virtual,表示虚继承

class Middle2 :virtual public Top {};

class Bottom : public Middle1, public Middle2 {};



int main() {
Bottom b;
//b.a = 20; // 报错,提示Bottom::a不明确

b.Middle1::a = 20;
b.Middle2::a = 30;

// 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
cout << "b.Middle1::a = " << b.Middle1::a << endl; // 30
cout << "b.Middle2::a = " << b.Middle2::a << endl; // 30

// 实际上,b只需要一份数据即可,菱形继承导致数据有两份,资源浪费
b.a = 40;
cout << "b.a = " << b.a << endl; // 40

return 0;
}

虚继承(Virtual Inheritance)的底层原理是通过虚基表指针(vbptr)和虚基表(vbtable)的间接寻址机制实现的

1. 内存布局抽象示例

对象 D 的内存布局大致如下(以 MSVC 为例):

1
2
3
4
5
6
7
8
9
10
11
+-------------------+
| D 对象的内存布局 |
+-------------------+
| B 的成员(非虚基类)|
| vbptr_B (→vbtable)|
|-------------------|
| C 的成员(非虚基类)|
| vbptr_C (→vbtable)|
|-------------------|
| A 的成员 (a) | ← 唯一一份基类 A 的实例
+-------------------+
  • 虚基表指针(vbptr)BC 中各有一个,指向各自的虚基表。
  • 虚基表(vbtable):存储虚基类 A 相对于当前子类位置的 偏移量

2. 虚基表内容示例

假设 B 的虚基表内容为:

1
2
3
4
vbtable for B:
+----------------+
| offset to A: 20|
+----------------+
  • 中间类(B/C)的行为
    • B和C的成员变量 a 并不直接存储在自己的内存区域,而是通过虚基表指针(vbptr)和虚基表(vbtable)定位到唯一的 A::a
    • 例如,当通过 B访问a时:
      1. 通过 vbptr_B 找到虚基表。
      2. 查表得到 A 的偏移量(如 +20 字节)。
      3. 计算 vbptr_B 地址 + 偏移量 → 访问 A::a
  • 底层实现与引用的区别
    • 引用是编译期的别名,不占用内存,操作时直接绑定目标地址。
    • 虚继承访问是运行期的间接寻址,需要两次内存访问(取虚基表指针 → 查表 → 计算偏移量 → 访问目标数据)。

3. 访问虚基类成员

当访问 D::a 时:

  1. 通过 BCvbptr 找到对应的虚基表。
  2. 根据表中的 偏移量 定位到唯一的 A 实例。

多态

多态分为两类:

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

示例:

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>

using namespace std;

class Animal {
public:
string name;
int age;

Animal(string name, int age) {
this->name = name;
this->age = age;
}

virtual void eat() {
cout << "Animal is Eating" << endl;
}

virtual void speek() {
cout << "Animal is speeking" << endl;
}
};

class Dog : public Animal {
public:
Dog(string name, int age) : Animal(name, age) {

}

void eat() override {
cout << "Dog is Eating" << endl;
}

void speek() override {
cout << "Dog is speeking" << endl;
}
};



int main() {
Animal *animal = new Dog("Dog", 5);
animal->eat();
animal->speek();

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

using namespace std;

class A1 {
public:
void eat() {
cout << "A1 eat" << endl;
}
};

class A2 : public A1 {
public:
void eat() {
cout << "A2 eat" << endl;
}
};

class B1 {
public:
virtual void eat() {
cout << "B1 eat" << endl;
}
};

class B2 : public B1 {
public:
void eat() override { // 隐含virtual,重写虚函数后还是虚函数
cout << "B2 eat" << endl;
}
};

int main() {
// A2 是 A1 的子类,B2 是 B1 的子类
A1 a1;
B1 b1;

cout << sizeof(a1) << endl; // 1
cout << sizeof(b1) << endl; // 8

// 虽然两个继承类都是相似的,但是由于B1中的方法是虚函数
// 所以导致了最后父类之间的占用内存大小是不同的

return 0;
}
  1. 空类规则(A1 的情况)
    • 当类没有成员变量时,C++标准要求其对象至少占用 1字节(用于标识不同对象的内存地址)。
    • A1 中没有虚函数和成员变量,因此 sizeof(a1) = 1
  2. 虚函数表指针(B1 的情况)
    • 当类包含虚函数时,编译器会隐式地为其添加一个 虚函数表指针(vptr),在64位系统中占 8字节
    • B1 包含虚函数,因此 sizeof(b1) = 8(仅vptr大小,无其他成员变量)。

虚函数与多态底层原理

  1. 虚函数表(vtable)

    • 实现方式:每个包含虚函数的类都有一个隐藏的虚函数表,表中存储该类所有虚函数的函数指针。

    • 内存布局(以B1B2为例):

      1
      2
      3
      4
      5
      cpp复制// B1 的虚函数表
      [&B1::eat] --> 地址1(指向B1::eat)

      // B2 的虚函数表
      [&B2::eat] --> 地址2(指向B2::eat)
  2. 虚函数表指针(vptr)

    • 每个对象在构造时,会通过 vptr 指向其所属类的虚函数表。

    • 内存验证:

      1
      2
      B1* p = new B2();
      p->eat(); // 输出 "B2 eat",通过vptr找到B2的虚函数表
  3. 多态底层流程

    1. 编译阶段

      • 编译器为每个包含虚函数的类生成虚函数表。
      • 在对象内存首部插入vptr(验证方法:对比 B1B1 移除虚函数后的内存差异)。
    2. 运行时阶段

      • 通过对象的vptr找到对应的虚函数表。
      • 从虚函数表中取出目标函数的地址进行调用。

通过Visual Studio提供的开发者工具进行验证,结果如下:

  1. 输入命令cl /d1 reportSingleClassLayoutB1 Main.cpp,返回如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Main.cpp

    class B1 size(4):
    +---
    0 | {vfptr}
    +---

    B1::$vftable@:
    | &B1_meta
    | 0
    0 | &B1::eat
  2. 输入命令cl /d1 reportSingleClassLayoutB2 Main.cpp,返回结果如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Main.cpp

    class B2 size(4):
    +---
    0 | +--- (base class B1)
    0 | | {vfptr}
    | +---
    +---

    B2::$vftable@:
    | &B2_meta
    | 0
    0 | &B2::eat

多态案例:计算器类

案例描述:

分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

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

普通的实现方式:

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

using namespace std;

class Calculator {
public:
int num1;
int num2;

Calculator(int num1, int num2) {
this->num1 = num1;
this->num2 = num2;
}

int calculate(string opera) {
if (opera == "+") {
return num1 + num2;
}
else if (opera == "-") {
return num1 - num2;
}
else if (opera == "*") {
return num1 * num2;
}; // 假设功能就开发到这里
}
};


int main() {
Calculator c(10, 20);

cout << c.num1 << "+" << c.num2 << "=" << c.calculate("+") << endl; // 30
cout << c.num1 << "-" << c.num2 << "=" << c.calculate("-") << endl; // -10
cout << c.num1 << "*" << c.num2 << "=" << c.calculate("*") << endl; // 200

return 0;
}

弊端:当后期添加新功能时,需要对Calculator类进行修改,当功能变多时,类会变得臃肿,可读性变差。同时功能的增多,维护成本又会增加,一个功能出错就可能导致后面开发时各种问题

优势:开发时简单高效

多态实现:

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

using namespace std;

class AbstractCalculator {
public:
int num1;
int num2;

virtual int calculate(string op) { return 0; };
};

class AddCalculator : public AbstractCalculator {
public:
int calculate(string op) override {
if (op == "+") {
return num1 + num2;
}
throw "Operator not supported"; // throw 抛出异常
}
};

class MulCalculator : public AbstractCalculator {
public:
int calculate(string op) override {
if (op == "*") {
return num1 * num2;
}
throw "Operator not supported";
}
};

class SubCalculator : public AbstractCalculator {
public:
int calculate(string op) override {
if (op == "-") {
return num1 - num2;
}
throw "Operator not supported";
}
};

class DivCalculator : public AbstractCalculator {
public:
int calculate(string op) override {
if (op == "/") {
return num1 / num2;
}
throw "Operator not supported";
}
};




int main() {

AbstractCalculator* calculator = new AddCalculator();
calculator->num1 = 10;
calculator->num2 = 20;
cout << calculator->num1 << "+" << calculator->num2 << "=" << calculator->calculate("+") << endl; // 30

calculator = new SubCalculator();
calculator->num1 = 10;
calculator->num2 = 20;
cout << calculator->num1 << "-" << calculator->num2 << "=" << calculator->calculate("-") << endl; // -10

calculator = new MulCalculator();
calculator->num1 = 10;
calculator->num2 = 20;
cout << calculator->num1 << "*" << calculator->num2 << "=" << calculator->calculate("*") << endl; // 200

calculator = new DivCalculator();
calculator->num1 = 100;
calculator->num2 = 20;
cout << calculator->num1 << "/" << calculator->num2 << "=" << calculator->calculate("/") << endl; // 5

// 使用完最好可以手动释放内存
delete calculator;
return 0;
}

优点:以模块化的思想设计了类,当一个模块出现问题,不会影响其他模块问题;虽然代码量增大了,但是结构更加清晰,提高了可读性,后期更好维护

弊端:开发难度相对高一些,占用内存也会变大,开发效率降低

纯虚函数和抽象类

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

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

纯虚函数语法: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
#include <iostream>
#include <string>

using namespace std;

class AbstractClass {
public:
virtual void templateMethod() = 0;
};

class C1 : public AbstractClass {
public:
void show() {
cout << "C1::show()" << endl;
}
};

class C2 : public AbstractClass {
public:
void templateMethod() override {
show();
}

void show() {
cout << "C2::show()" << endl;
}
};

int main() {
//AbstractClass *abstractClass = new AbstractClass(); // 报错,抽象类不能实例化
//C1 *c = new C1(); // 如果没有实现抽象类中的虚函数,也被视为抽象类
C2 *c2 = new C2();
c2->templateMethod(); // 输出 C2::show()

return 0;
}

虚析构和纯虚析构

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

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

虚析构和纯虚析构的共性:

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

虚析构和纯虚析构区别:

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

虚析构语法:virtual ~类名(){}

纯虚析构语法: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
#include <iostream>
#include <string>

using namespace std;

class Animal {
public:
string* name;
Animal() {
cout << "Animal构造函数被调用了" << endl;
}

Animal(string name) {
this->name = new string(name);
}

virtual void speak() = 0;

~Animal() {
cout << "Animal析构函数被调用了" << endl;
if (this->name != nullptr) {
delete this->name;
this->name = nullptr;
}
}
};

class Cat : public Animal {
public:
Cat() {
cout << "Cat构造函数被调用了" << endl;
}

Cat(string name) : Animal(name) {
cout << "Cat构造函数被调用了" << endl;
}

void speak() override {
cout << *name << "叫了一声" << endl;
}

~Cat() {
cout << "Cat析构函数被调用了" << endl;
if (this->name != nullptr) {
delete this->name;
this->name = nullptr;
}
}
};

int main() {
Animal *cat = new Cat("小猫");
cat->speak();
delete cat;

/*
Animal构造函数被调用了
Cat构造函数被调用了
小猫叫了一声
Animal析构函数被调用了
*/

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <string>

using namespace std;

class Animal {
public:
string* name;
Animal() {
cout << "Animal构造函数被调用了" << endl;
}

Animal(string name) {
cout << "Animal构造函数被调用了" << endl;
this->name = new string(name);
}

virtual void speak() = 0;

virtual ~Animal() {
cout << "Animal析构函数被调用了" << endl;
if (this->name != nullptr) {
delete this->name;
this->name = nullptr;
}
}
};

class Cat : public Animal {
public:
Cat() {
cout << "Cat构造函数被调用了" << endl;
}

Cat(string name) : Animal(name) {
cout << "Cat构造函数被调用了" << endl;
}

void speak() override {
cout << *name << "叫了一声" << endl;
}

~Cat() override {
cout << "Cat析构函数被调用了" << endl;
if (this->name != nullptr) {
delete this->name;
this->name = nullptr;
}
}
};

int main() {
Animal *cat = new Cat("小猫");
cat->speak();
delete cat;

/*
Animal构造函数被调用了
Cat构造函数被调用了
小猫叫了一声
Cat析构函数被调用了
Animal析构函数被调用了
*/

return 0;
}

纯虚析构

纯虚析构如果按照virtual ~Animal() = 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
67
#include <iostream>
#include <string>

using namespace std;

class Animal {
public:
string* name;
Animal() {
cout << "Animal构造函数被调用了" << endl;
}

Animal(string name) {
cout << "Animal构造函数被调用了" << endl;
this->name = new string(name);
}

virtual void speak() = 0;

virtual ~Animal() = 0;
};

Animal::~Animal() {
cout << "Animal纯虚析构函数被调用了" << endl;
if (this->name != nullptr) {
delete this->name;
}
}

class Cat : public Animal {
public:
Cat() {
cout << "Cat构造函数被调用了" << endl;
}

Cat(string name) : Animal(name) {
cout << "Cat构造函数被调用了" << endl;
}

void speak() override {
cout << *name << "叫了一声" << endl;
}

~Cat() override {
cout << "Cat析构函数被调用了" << endl;
if (this->name != nullptr) {
delete this->name;
this->name = nullptr;
}
}
};

int main() {
Animal *cat = new Cat("小猫");
cat->speak();
delete cat;

/*
Animal构造函数被调用了
Cat构造函数被调用了
小猫叫了一声
Cat析构函数被调用了
Animal纯虚析构函数被调用了
*/

return 0;
}

C++开发规则

如果一个类包含虚函数,它几乎总是需要一个虚析构函数。 这是C++的一个重要原则,特别是当开发人员计划通过基类指针管理派生类对象时。

多态案例:组装计算机

以CPU、Graphics Card、Memory为基类,以对应零件的生产商家为子类,添加到Computer类中,Computer类中调用所有零件的运行接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <iostream>
#include <string>

using namespace std;
// 零件基类(抽象类)
class CPU {
public:
virtual void calculate() = 0;
virtual ~CPU() = 0;
};

class GraphicsCard {
public:
virtual void display() = 0;
virtual ~GraphicsCard() = 0;
};

class Memory {
public:
virtual void storeData() = 0;
virtual ~Memory() = 0;
};

// 实现纯虚析构函数,防止报错
CPU::~CPU() {}
GraphicsCard::~GraphicsCard() {}
Memory::~Memory() {}

// 电脑类
class Computer {
public:
CPU* cpu;
GraphicsCard* graphicsCard;
Memory* memory;

Computer(CPU* cpu, GraphicsCard* graphicsCard, Memory* memory) {
this->cpu = cpu;
this->graphicsCard = graphicsCard;
this->memory = memory;
}

void run() {
cpu->calculate();
graphicsCard->display();
memory->storeData();
}

// 防止内存泄露
~Computer() {
if (cpu != nullptr) {
delete cpu;
cpu = nullptr;
}

if (graphicsCard != nullptr) {
delete graphicsCard;
graphicsCard = nullptr;
}

if (memory != nullptr) {
delete memory;
memory = nullptr;
}
}
};

// 具体实现类
class IntelCPU : public CPU {
public:
void calculate() override {
std::cout << "Intel CPU 计算中..." << std::endl;
}

~IntelCPU() override {
std::cout << "Intel CPU 析构函数" << std::endl;
}
};

class AmdCPU : public CPU {
public:
void calculate() override {
std::cout << "Amd CPU 计算中..." << std::endl;
}

~AmdCPU() override {
std::cout << "Amd CPU 析构函数" << std::endl;
}
};

class NvidiaGraphicsCard : public GraphicsCard {
public:
void display() override {
std::cout << "Nvidia 显卡 显示中..." << std::endl;
}

~NvidiaGraphicsCard() override {
std::cout << "Nvidia 显卡 析构函数" << std::endl;
}
};

class AmdGraphicsCard : public GraphicsCard {
public:
void display() override {
std::cout << "Amd 显卡 显示中..." << std::endl;
}

~AmdGraphicsCard() override {
std::cout << "Amd 显卡 析构函数" << std::endl;
}
};

class SamsungMemory : public Memory {
public:
void storeData() override {
std::cout << "Samsung 内存 存储数据中..." << std::endl;
}

~SamsungMemory() override {
std::cout << "Samsung 内存 析构函数" << std::endl;
}
};

class KingstonMemory : public Memory {
public:
void storeData() override {
std::cout << "Kingston 内存 存储数据中..." << std::endl;
}

~KingstonMemory() override {
std::cout << "Kingston 内存 析构函数" << std::endl;
}
};


int main() {
Computer *computer_01 = new Computer(new IntelCPU(), new NvidiaGraphicsCard(), new SamsungMemory());
Computer *computer_02 = new Computer(new AmdCPU(), new AmdGraphicsCard(), new KingstonMemory());
Computer *computer_03 = new Computer(new AmdCPU(), new NvidiaGraphicsCard(), new KingstonMemory());

cout << "电脑_01开启:" << endl;
computer_01->run();
cout << endl;

cout << "电脑_02开启:" << endl;
computer_02->run();
cout << endl;

cout << "电脑_03开启:" << endl;
computer_03->run();
cout << endl;

delete computer_01;
delete computer_02;
delete computer_03;


return 0;
}