基础

内存分区

代码区全局区栈区堆区
存放二进制代码,由操作系统管理存放全局变量、静态变量、常量由编译器自动分配和释放,存放函数参数值、局部变量由程序员分配和释放,若程序员不处理,程序结束时由操作系统回收
1. 程序运行前就存在;
2. 存放 CPU 执行的机器指令;
3. 共享:对于需要频繁执行的程序,只需在内存中有一份即可;
4. 只读:防止程序意外修改指令。
1. 程序运行前就存在;
2. 存放全局变量、静态变量、常量;
3. 程序结束由操作系统释放。
1. 由编译器自动分配和释放,存放函数参数值、局部变量;
2. 注意不要返回局部变量的地址,因为栈区开闭的数据由编译器自动释放。
1. 由程序员分配和释放;
2. 利用 new 在堆区开辟数据。

🤔通过代码分析:

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
int b = 20;//不在函数体中,全局变量,存放在全局区;
const int d = 30;//const修饰的全局变量,也叫全局常量;

int *func()
{
int *p = new int(10);//利用new把数据开辟到堆区;//作用类似C语言的malloc()
//指针也是局部变量,放在栈区,指针保存的数据放在堆区;
return p;
}

void test01()
{
int *p = func();
cout << *p << endl;
cout << *p << endl;
cout << *p << endl;
delete p;//用delete释放堆区数据;//类似C语言的free()
//cout << *p << endl;已释放,再次访问就访问不到;
}

void test02()
{
int *arr = new int[10];//在堆区创建数组;
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100;
cout << arr[i] << endl;
}
delete[] arr;//释放数组要加[];
}

int main()
{
int a = 10;//局部变量,只要是在函数体内的变量都是局部变量;

cout << (int)&a << endl;
cout << (int)&b << endl;

static int c = 15;//静态变量,存放在全局区;
cout << (int)&c << endl;//地址和全局变量很近,在一个区域内;
cout << (int)&"hello world" << endl;//常量离全局变量也很近,也在一个区内;

const int e = 25;//局部常量,存放在栈区;
cout << (int)&d << endl;
cout << (int)&e << endl;

int *q = func();
cout << *q << endl;
cout << *q << endl;
cout << *q << endl;
delete q;

test01();
test02();

system("pause");
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
void swap01(int a, int b)//值传递,实参不会被修改
{
int temp = a;
a = b;
b = temp;
}

void swap02(int *a, int *b)//地址传递,实参可以修改
{
int temp = *a;
*a = *b;
*b = temp;
}

void swap03(int &a, int &b)//引用传递,实参可以修改
{
int temp = a;
a = b;
b = temp;
}

//不要返回局部变量的引用;
//int& test01()
//{
//int a1 = 10;//局部变量,存放在栈区;
//return a1;//非法操作;
//}

int &test02()
{
static int a2 = 10;//静态变量,存放在全局区;
return a2;
}

void showValue(const int &val)//const修饰,常量引用,防止修改实参;
{
//val = 60;//不能修改val了;
cout << "val= " << val << endl;
}

int main()
{
int a = 10;
int &b = a; //系统自动转化为int *const b = &a;指针常量是指针指向不可更改,所以引用不可更改;
cout << a << endl;
cout << b << endl;

b = 20;//发现b是引用,系统自动转为*b = 20; 解引用;
cout << a << endl;
cout << b << endl;

int m = 4;
int n = 6;

swap01(m, n);//值传递,实参不会被修改
cout << m << " " <<n << endl;

swap02(&m, &n);//指针传参要加&,实参可以修改
cout << m << " " << n << endl;

swap03(m, n);//引用传递和值传递一样,只是实参可以修改
cout << m << " " << n << endl;


/*int &ref1 = test01();
cout << ref1 << endl;
cout << ref1 << endl;*/

int &ref2 = test02();
cout << ref2 << endl;
cout << ref2 << endl;

test02() = 1000;//如果函数的返回值是引用,函数调用可以作为左值;
cout << ref2 << endl;
cout << ref2 << endl;

//常量引用,修饰形参,防止误操作;
int d = 30;
int &ref3 = d;
//int &ref3 = 30;//引用必须引用一块合法的内存空间,常量在常量区,不能被引用;
const int &ref4 = 30;//加上const 后,编译器将代码自动修改为int temp = 30; int &ref4 = temp; 只读,不可修改;

int e = 40;
showValue(e);

system("pause");
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
//函数默认参数
int func( int a, int b = 20, int c = 30)//b和c是默认值;
{
return a + b + c;
}

//如果某个位置有了默认值,那从这个位置往后都要有默认值;
//如果函数的声明有了默认参数,那函数的实现就不能再有,声明和实现只能有一个有默认参数;

void func2(int a, int)//占位参数;
{
cout << "111" << endl;
}

void func3(int a, int = 30)//占位参数也可以有默认参数;
{
cout << "222" << endl;
}

int main()
{
int f = func(10);//函数加了默认值后,调用时可以省去实参;如果传了实参,就用实参;
cout << f << endl;

func2(10, 20);
func3(10);

system("pause");
return 0;
}

函数重载

让函数名相同,提高复用性。
🚀条件:

  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
68
69
70
71
72
73
74
75
76
void func()
{
cout << "func的调用" << endl;
}

void func(int a)
{
cout << "func(int a)的调用" << endl;
}

void func(double a)
{
cout << "func(double a)的调用" << endl;
}

void func(int a, int b)
{
cout << "func(int a, int b)的调用" << endl;
}

void func(int a, double b)
{
cout << "func(int a, double b)的调用" << endl;
}

void func(double a, int b)
{
cout << "func(double a, int b)的调用" << endl;
}

//int func(double a, int b)//函数返回值不可作为函数重载的条件!
//{
// cout << "func(double a, int b)的调用" << endl;
//}

//-------------------------------------------
void func1(int &a)//引用
{
cout << "func1(int &a)的调用" << endl;
}

void func1(const int &a)//加const可以重载;
{
cout << "func1(const int &a)的调用" << endl;
}

//--------------------------------------------
void func02(int a)
{
cout << "func02的调用" << endl;
}

void func02(int a, int b = 10)//加默认参数可以重载,尽量避免;
{
cout << "func02(a,b)的调用" << endl;
}

int main()
{
func();
func(3);
func(4, 5);
func(3.14);
func(5, 7.8);
func(5.6, 8);

int a = 10;
func1(a);//变量,调用不加const的;
func1(20);//常量,调用加const的;

//func2(20);//不能只调一个参数,尽量避免;
func02(20, 30);//传两个值可以;

system("pause");
return 0;
}

封装

相关基础知识

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

封装的意义:
将属性和行为作为一个整体,写在一起,表现生活中的事物;将属性和行为加以权限控制(public/protected/private);语法:class 类名{访问权限: 属性/行为};

class 和 struct 的区别:

1. 默认的访问权限不同,class 默认为私有,struct 默认为公共;
2. 类可以继承,结构体不可以。

Getter 和 Setter 一般放在头文件中,权限为 public,通过这个方式来 get/set 保护或者私有变量。

对象特性

构造、析构与拷贝函数

对象的初始化和清理:
构造函数:创建对象时给成员属性赋值,编译器自动调用;
析构函数:清理,对象销毁前编译器自动调用。

构造函数分类:有参、无参普通、拷贝

如果用户定义有参构造,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
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
class Person
{
public:
//构造函数:
//1.没有返回值,不用写void;
//2.函数名与类名相同;
//3.可以有参数,可以发生重载;
//4.创建对象时,会自动调用,只调用一次;
Person()//按参数分类,分为有参和无参,无参构造也叫默认构造
{
printf("Person的默认构造函数调用 \n");
}

Person(int a)//有参构造
{
age = a;
printf("Person的有参构造函数调用 \n");
}

Person(const Person &p)//拷贝构造函数,别的对象的属性拷贝进来,但不改变原对象的属性;其他都叫普通构造函数;
{
age = p.age; //别的对象的属性拷贝进来,但不改变原对象的属性;
printf("Person的拷贝构造函数调用 \n");
}

//析构函数:
//1.没有返回值,不用写void;
//2.函数名与类名相同,前面加~;
//3.可以有参数,可以发生重载;
//4.对象销毁前,会自动调用,只调用一次;
~Person()
{
printf("Person的析构函数调用 \n");
}

int getage()
{
return age;
}

private:
int age;
};

//调用
void test001()
{
//括号法
Person p0;//默认构造函数的调用,不要加小括号!!否则Person p0()会被认为是函数的声明!
Person p1(10);//有参构造函数的调用;
Person p2(p1);//拷贝构造函数的调用;使用一个已经创建完毕的对象来初始化一个新的对象;
printf("p1的年龄为:%d \n", p1.getage());
printf("p2的年龄为:%d \n", p2.getage());

//显示法
Person p00;
Person p01 = Person(30);//有参构造;Person(30)叫匿名对象!如果放在等号右侧,那等号左侧就是其对象名;
Person(40);//当前行执行结束后,系统会立即回收匿名对象;
printf("匿名对象\n");
Person p02 = Person(p01);//拷贝构造
printf("p01的年龄为:%d \n", p01.getage());
printf("p02的年龄为:%d \n", p02.getage());

//Person(p02);//不要利用拷贝构造函数来初始化匿名对象!编译器会认为Person(p02) == Person p02,就重定义了!

//隐式转换法
Person p3 = 25;//相当于Person p3 = Person(25);有参构造
printf("p3的年龄为:%d \n", p3.getage());
Person p4 = p3;//拷贝构造
printf("p4的年龄为:%d \n", p4.getage());
}

void doWork(Person p)//值传递的方式给函数参数传值,可以调用拷贝构造函数
{

}

void test002()
{
Person p;
doWork(p);
printf("p \n");
}

Person doWork2()//值方式返回局部对象,可以调用拷贝构造函数
{
Person p5;
return p5;
}

void test003()
{
Person p = doWork2();
printf("doWork2 \n");
}

int main()
{
test001();
test002();
test003();

system("pause");
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
67
68
69
class Person01
{
public:
Person01()
{
printf("Person01的默认构造函数调用 \n");
}

Person01(int a, int h)
{
m_Age = a;
printf("Person01的有参构造函数调用 \n");
m_Height = new int(h);//创建在堆区,返回的是指针;
}

//自己创建一个拷贝构造函数来解决浅拷贝的问题,深拷贝!
Person01(const Person01 &p)
{
printf("Person01的拷贝构造函数调用 \n");
m_Age = p.m_Age;
//m_Height = p.m_Height;//编译器默认提供的,在堆区会重复释放;
m_Height = new int(*p.m_Height);//深拷贝,重新开辟一块内存,让m_Height指向这块新的内存!这样就可以进行第二次释放!
}

~Person01()
{
printf("Person01的析构函数调用 \n");//将堆区数据释放;
if (m_Height != nullptr)
{
delete m_Height;//手动开辟,手动释放内存;
m_Height = nullptr;//防止野指针出现,即防止已经释放的内存再次被访问,滞空;
}
}

int getm_age()
{
return m_Age;
}

int *getHeight()
{
return m_Height;
}

private:
int m_Age;
int *m_Height;
};


void test02_01()
{
Person01 P1(18, 175);
printf("P1的年龄是: %d, 身高是: %d \n", P1.getm_age(), *P1.getHeight());

Person01 P2(P1);
printf("P2的年龄是: %d, 身高是: %d \n", P2.getm_age(), *P2.getHeight());
//如果用编译器自己提供的拷贝构造函数,会做浅拷贝操作!
//浅拷贝的问题:堆区的内存重复释放!第二次释放是非法操作!要用深拷贝来解决!
//如果有在堆区开辟的属性,一定要自己提供拷贝构造函数!用深拷贝来防止浅拷贝的问题!
}

int main()
{
test02_01();

system("pause");
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
class Person02
{
public:
//Person02(int a, int b, int c)//有参构造
//{
// m_A = a;
// m_B = b;
// m_C = c;
//}

//Person02() :m_A(10), m_B(20), m_C(30)//初始化列表
//{

//}

Person02(int a, int b, int c) :m_A(a), m_B(b), m_C(c)//初始化列表 可传不同值;注意冒号位置!
{

}

int m_A;
int m_B;
int m_C;
};

🚀注意:

  1. 定义顺序:成员变量的初始化顺序遵循它们在类声明中的顺序,而不是初始化列表中的顺序。
  2. 必要情况:如果成员变量是常量 const或者没有默认构造函数的类类型,则必须使用初始化列表进行初始化。
  3. 优点:初始化列表可以提高效率,因为它避免了构造函数体中的赋值操作,特别是在处理不需要或不能通过默认构造函数创建的复杂对象时。
  4. 执行顺序基类的构造函数按照它们在类继承列表中声明的顺序依次调用 ->成员对象的构造函数按照它们在类体中声明的顺序调用 -> 执行构造函数体内的代码。
  5. 使用条件:仅构造函数可以使用初始化列表。

现在具体解释注意点。
第 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
class BaseClass {
public:
BaseClass() : m_Value(0) {} // 默认构造函数
BaseClass(int value) : m_Value(value) {} // 有参构造函数
int m_Value;
};

class MyClass {
public:
MyClass(int baseValue, int constVal)
: m_BaseMember(baseValue), // 初始化BaseClass类型成员变量
m_ConstMember(constVal) // 初始化const int类型成员变量
{
cout << "Base member initialized with: " << m_BaseMember.m_Value << endl;
cout << "Const member initialized with: " << m_ConstMember << endl;
}
BaseClass m_BaseMember; // 类类型成员变量
const int m_ConstMember; // const类型成员变量
};

int main() {
MyClass obj(10, 20); // 创建MyClass对象,测试初始化列表
return 0;
}

第 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
class Base {
public:
// 基类构造函数
Base() {
cout << "Base class constructor" << endl;
}
};

class Member {
public:
// 成员类构造函数
Member() {
cout << "Member object constructor" << endl;
}
};

class Derived : public Base {
// 成员变量初始化使用成员初始化列表
Member obj;
public:
// 派生类构造函数
Derived() : obj() {
cout << "Derived class constructor body" << endl;
}
};

int main() {
Derived d; // 这将触发Derived类的构造过程
}

//输出结果应该是这样的顺序:父类->成员类->自身构造函数
//Base class constructor
//Member object constructor
//Derived class constructor body

🚩类对象作为成员

构造函数顺序:父类->成员类->自身构造函数
析构函数顺序:反之。后进先出,析构函数释放的顺序和构造函数相反。

代码详见“初始化列表—注意—第 4 点”。

静态成员

静态成员:就是在成员变量或成员函数前面加上 static

静态成员变量:

  1. 所有对象共享同一份数据,不单独属于某个对象(但这不意味着都是 public 或者说初始化的时候不需要加上作用域)
  2. 编译阶段分配内存(全局区)
  3. 类内声明,类外初始化
  4. 如果静态成员变量是 const 整数类型或 const 枚举类型,可以在类内部直接定义并初始化

静态成员函数:

  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
47
48
49
50
51
52
class Person04
{
public:
static int m_A;//静态成员变量,类内声明
int m_C;//非静态成员变量,不可被static void func调用

static void func()//静态成员函数
{
m_A = 400;//可调用静态成员变量
//m_C = 200;//非静态成员变量,不可被func调用,因为这个数据是属于某个具体对象的
printf("static void func的调用 \n");
}

private:
static int m_B;
static void func1()//静态成员函数有访问权限
{
printf("static void func1的调用 \n");
}
};

int Person04::m_A = 100;//类外初始化,一定要有
int Person04::m_B = 300;

void test_12_01()
{
Person04 p1;
printf("p1A= %d \n", p1.m_A);

Person04 p2;
p2.m_A = 200;//用p2把m_A的值修改
printf("p2A= %d \n", p1.m_A);//但是还是用p1去访问m_A,结果为200,共享同一份数据
}

void test_12_02()
{
//静态成员变量不属于某个对象上,共享同一个函数,所有对象都共享一份数据
//所以有两种访问方式:
Person04 p;
printf("pA= %d \n", p.m_A);//1,通过对象进行访问;
printf("pA= %d \n", Person04::m_A);//2,通过类名进行访问,不需要创建对象;
//printf("pB= %d \n", Person04::m_B);//静态成员变量也是有访问权限的,private在类外不可访问;
}

void test_12_03()
{
//静态成员函数不属于某个对象上,有两种访问方式:
Person04 p;
p.func();//1,通过对象进行访问;
Person04::func();//2,通过类名进行访问,不需要创建对象;
//Person04::func1();//静态成员函数有访问权限,private在类外不可访问;
}

🚀注意:

  • 可以在类内部以 static const int var = 10; 这样的代码直接初始化常量静态成员变量。
  • 对于其他类型的静态成员(例如 double 或其他类类型),必须在类外如 double MyClass::staticVar = 10; 进行定义和初始化,不能写作 double staticVar = 10;
  • C++11 开始,允许内联初始化静态成员变量。inline 关键字允许静态成员变量的定义在一个 .h 文件中(或任何包含它的翻译单元)进行,编译器会在需要时将其复制到多个翻译单元中。这样可以避免多个翻译单元中定义相同的静态成员变量,从而解决了多重定义的问题。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
//.h文件
class MyClass {
public:
static int staticVar; // 声明并内联初始化
};
// C++11的编译器特性使得这样就可以在使用静态成员的任何翻译单元中,编译器自动定义它
inline int MyClass::staticVar = 10;


//.cpp文件(非必要,只需要定义一次)
// 使用关键字inline可以在类的声明中直接初始化静态成员,无需在.cpp文件中具体定义
inline int MyClass::staticVar = 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
class test
{

};

class Person05
{
int m_A = 1;//非静态成员变量,属于类的对象上;
static int m_B;//静态成员变量,不属于类的对象上;

void func() {};//非静态成员函数,和变量分开存储的,不属于类的对象上!
static void func1() {};//静态成员函数,不属于类的对象上;
};
int Person05::m_B = 0;//类外初始化;

void test_13_01()
{
test p;
cout<<"sizeof p="<<sizeof(p)<<endl;
//空对象占用字节为1,C++编译器会给空对象分配1个字节空间,是为了区分空对象占内存的位置;
//每个空对象也应该有一个独一无二的内存地址;
}

void test_13_02()
{
Person05 p;
cout << "sizeof p=" << sizeof(p) << endl;//4个字节,因为是非静态成员变量,属于p的属性,int类型;
//仅m_A是p的属性
}

this 指针

特点:

  • 当形参和成员变量同名时,解决名称冲突;
  • this 指针指向的是被调用的成员函数所属的对象,谁调用就指向谁;
  • 在类的非静态成员函数中,返回对象本身用 *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
class Person07
{
public:
void showClassName()
{
cout << "this is person class" << endl;
}

void showPersonAge()
{
if (this == nullptr)//如果是空指针就return,避免报错,提高健壮性;
{
return;
}
cout << "age=" << this->m_Age << endl;//m_Age和this->m_Age在这里是等效的;
}

int m_Age;

};

void test_15_01()
{
Person07 *p = nullptr;
p->showClassName();
p->showPersonAge();//空指针没有对象,访问里面的属性会报错!
}

const 修饰成员函数

常对象 (const object) 无法直接修改属性,除非使用这几个方式:

  1. 将类成员变量声明为 mutable/static, 则对应变量可以修改;
  2. 使用 const_cast 移除 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
//常函数//常对象
class Person08
{
public:
//this指针本质是指针常量,指针的指向是不可修改的;
void showPerson() const//常函数,相当于const Person08 *const this,修饰的是this的指向,让指向的值也不可修改
{
//m_A = 100;//等效于this->m_A = 100;
m_B = 200; //m_B可修改;
}

void func()
{

}

int m_A;
mutable int m_B;//即使在常函数中也可以修改;
};

void test_16_01()
{
Person08 p;
p.showPerson();
}

void test_16_02()
{
const Person08 p;//常对象
//p.m_A = 300;//不可修改
p.m_B = 400;//m_B可修改;
p.showPerson();//常对象只能调用常函数!
//p.func();//常对象只能调用常函数!因为常函数可以修改属性,但是常对象不能修改!
}

了解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void setValue(int v) {
value = v;
}
int value;
};

//指向常对象的指针(const MyClass* const p)都不允许通过指针修改对象的内容
void modifyValue(MyClass* const cptr) {
cptr->setValue(10); // 正确:可以使用这个方法去间接改变成员值
}

int main() {
const MyClass obj;
modifyValue(const_cast<MyClass*>(&obj)); // 使用const_cast移除const限定符
}

🚩友元

定义:让一个函数或者类,访问另一个类中的所有成员

🚀注意:

  1. 友元函数一定要在类外实现,不然会报错!
  2. 需要先对友元类进行前向声明 (forward declare,可在类内类外), 然后再声明友元函数。
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
class Building;//前向声明,先让编译器知道有个Building类,先在前面声明,不然后面成员函数作友元会报错!

class Goodguy//创建一个好友类;
{
public:
Goodguy();//构造函数声明,赋初值;类外写成员函数;

void visit();//参观函数声明,访问对象building中的属性;类外写成员函数;

void sleep();//睡觉函数声明,访问对象building中的属性;类外写成员函数;成员函数作友元;
//如果想让sleep访问私有成员而不让visit访问,就需要用到成员函数作友元!
//如果类作友元是所有成员函数都能访问!

public:
Building *building;//指针作成员;
};

class Building//创建一个建筑物类
{
friend void goodguy(Building &building);//全局函数作友元,全局函数的声明,前面加friend意思就是这个函数可以访问Building的私有成员;
//friend class Goodguy;//类作友元,类的声明;
friend void Goodguy::sleep();//成员函数作友元,成员函数的声明;成员函数要加作用域,全局函数不加!

public:
//Building()//构造函数,赋初值;类内实现;
//{
// m_SittingRoom = "客厅";
// m_BedRoom = "卧室";
//}
Building();//构造函数,赋初值

public:
string m_SittingRoom;

private:
string m_BedRoom;
};

Building::Building()//类外实现构造函数;
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}

//1,全局函数作友元
void goodguy(Building &building)//全局函数,引用传递
{
cout << "01好友正在访问:" << building.m_SittingRoom << endl;
cout << "01好友正在访问:" << building.m_BedRoom << endl;
}

void test_01_01()
{
Building b;//创建Building的对象,这时候就会调用Building构造函数,来赋初值
goodguy(b);
}

//2,类作友元 //3,成员函数作友元
Goodguy::Goodguy()//构造函数定义,赋初值;类外写成员函数;
{
building = new Building();//在堆区创建一个Building的对象,用building去接收:new Building的时候会调用Building构造函数;
}

void Goodguy::visit()//visit函数定义,类外写成员函数;不用传值,因为同属一个类的成员里有对应变量!
{
cout << "02好友正在访问:" << building->m_SittingRoom << endl;//实际上还是函数访问成员,只不过这个函数是作为类的一个对象;
//cout << "02好友正在访问:" << building->m_BedRoom << endl;
}

void Goodguy::sleep()
{
cout << "03好友正在:" << building->m_SittingRoom << "里睡觉" << endl;
cout << "03好友正在:" << building->m_BedRoom << "里睡觉" << endl;
}

void test_01_02()
{
Goodguy g;//创建Goodguy的对象,这时候就会调用Goodguy构造函数,来赋初值;
g.visit();//调用visit函数,来访问Building的成员;
g.sleep();
}

🚩运算符重载

成员函数重载+号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person01
{
public:
Person01 operator+(Person01 &p)
{
Person01 temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}

public:
int m_A;
int m_B;
};

全局函数重载+号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Person01 operator+(Person01 &p1, Person01 &p2)
{
Person01 temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}

//运算符重载也可以发生函数重载;
Person01 operator+(Person01 &p1, int num)
{
Person01 temp;
temp.m_A = p1.m_A + num;
temp.m_B = p1.m_B + num;
return temp;
}

全局函数重载左移运算符<<

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
//左移运算符配合友元可以实现输出自定义的数据类型!
ostream &operator<<(ostream &out, Person01 &p)//因为cout<<p本质是operator<<(cout, p),简化版本是cout<<p; cout是输出流对象!
{
out << "m_A=" << p.m_A << endl;
out << "m_B=" << p.m_B << endl;
return out;
}

void test_02_01()
{
Person01 p1;
p1.m_A = 10;
p1.m_B = 15;

cout << p1 << "hello world" << endl;//左移运算符重载;

Person01 p2;
p2.m_A = 20;
p2.m_B = 30;

Person01 p3 = p1 + p2;//直接写+号,编译器不认识,要用重载;
cout << "p3.m_A = " << p3.m_A << endl;
cout << "p3.m_B = " << p3.m_B << endl;
//成员函数重载本质是Person01 p3 = p1.operator+(p2);
//全局函数重载本质是Person01 p3 = operator+(p1, p2);

Person01 p4 = p1 + 100;//函数重载
cout << "p4.m_A = " << p4.m_A << endl;
cout << "p4.m_B = " << p4.m_B << endl;
}

友元函数重载右移运算符>>

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
int value;
// 声明友元函数
friend istream& operator>> (istream& is, MyClass& obj);
};

// 友元函数定义
istream& operator>> (istream& is, MyClass& obj) {
is >> obj.value; // 从输入流读取数据到obj的成员变量
return is;
}

重载递增运算符++

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
class myint//自定义整型
{
friend ostream &operator<<(ostream &cout, myint m);//用友元访问private成员;

public:
myint()
{
m_Num = 0;//用构造函数初始化数据;
}

myint &operator++()//重载前置++运算符,用成员函数;要返回引用!不然会调用拷贝构造函数复制一份新的对象!
{
m_Num++;//先进行++运算;
return *this;//再返回自身;this指针的用法之一,非静态成员函数返回对象本身;
}

myint operator++(int)//重载后置++运算符,用成员函数;int为占位参数!可用于区分前置和后置递增!后置要返回值,不能返回引用!
{
myint temp = *this;//先记录当时的结果;
m_Num++;//再递增;
return temp;//再将记录的结果返回;
}


myint &operator--()//重载前置--运算符
{
m_Num--;
return *this;
}

myint operator--(int)//重载后置--运算符
{
myint temp = *this;
m_Num--;
return temp;
}


private:
int m_Num;
};

ostream &operator<<(ostream &cout, myint m)//全局函数重载左移运算符
{
cout << "数据的值是:" << m.m_Num;
return cout;
}


void test_02_02()
{
myint newint;
cout << newint << endl;//因为是打印自定义的整型,所以要重载<<;
cout << ++newint << endl;//需要重载前置++;
cout << ++(++newint) << endl;//用引用返回,就能对一个对象一直++;
cout << newint << endl;
}

void test_02_03()
{
myint newint;
cout << newint << endl;
cout << newint++ << endl;
cout << newint << endl;
}

void test_02_04()
{
myint newint;
cout << newint << endl;
cout << --newint << endl;
cout << --(--newint) << endl;
cout << newint << endl;
}

void test_02_05()
{
myint newint;
cout << newint << endl;
cout << newint-- << endl;
cout << newint << 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
47
48
49
50
51
52
53
54
55
56
57
//C++编译器至少给一个类添加4个函数,
//默认构造函数,无参,函数体为空;
//默认析构函数,无参,函数体为空;
//默认拷贝构造函数,对属性进行值拷贝;
//赋值运算符operator=,对属性进行值拷贝;
//如果类中有属性指向堆区,赋值操作也会出现深浅拷贝的问题!

class Person02
{
public:
Person02(int age)
{
m_Age = new int(age);//在堆区,要程序员手动开辟和释放,释放的时机是在析构函数中;
}

~Person02()
{
if (m_Age != nullptr)//释放堆区的数据;如果用默认的赋值运算会造成浅拷贝,所以要重载=,用深拷贝;
{
delete m_Age;
m_Age = nullptr;
}
}

Person02 &operator=(Person02 &p)//重载=运算符;返回引用,可返回自身,可实现链式编程;
{
//编译器默认:m_Age = p.m_Age;这样是浅拷贝;
//应该先判断是否有属性在堆区,如果有,应该释放干净!然后再深拷贝!
if (m_Age != nullptr)
{
delete m_Age;
m_Age = nullptr;
}

m_Age = new int(*p.m_Age);//深拷贝!重新开辟一块堆区空间!

return *this;//返回自身;
}

int *m_Age;
};

void test_02_06()
{
Person02 P1(18);
cout << "P1年龄是:" << *P1.m_Age << endl;
Person02 P2(20);
cout << "P2年龄是:" << *P2.m_Age << endl;

P2 = P1;//将P1的所有数据赋值给P2;
cout << "P1年龄是:" << *P1.m_Age << endl;
cout << "P2年龄是:" << *P2.m_Age << endl;

Person02 P3(30);
P3 = P2 = P1;
cout << "P3年龄是:" << *P3.m_Age << 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
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
//让两个自定义类型对象进行比较
class Person03
{
public:
Person03(string name, int age)
{
m_Name = name;
m_Age = age;
}

bool operator==(Person03 &p)//重载==
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}

bool operator!=(Person03 &p)//重载!=
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return false;
}
else
{
return true;
}
}

bool operator<(Person03 &p)//重载<
{
if (this->m_Age < p.m_Age)
{
return true;
}
else
{
return false;
}
}

bool operator>(Person03 &p)//重载>
{
if (this->m_Age > p.m_Age)
{
return true;
}
else
{
return false;
}
}

string m_Name;
int m_Age;
};

void test_02_07()
{
Person03 p1("Tom", 18);
Person03 p2("Jerry", 18);
if (p1 == p2)
{
cout << "p1和p2相等" << endl;
}
else if(p1 != p2)
{
cout << "p1和p2不等" << endl;
}
}

void test_02_08()
{
Person03 p3("Vettel", 35);
Person03 p4("Kimi", 40);
if (p3 < p4)
{
cout << "p3比p4小" << endl;
}
else if (p3 > p4)
{
cout << "p3比p4大" << 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
//非常灵活,没有固定写法
class MyPrint
{
public:
void operator()(string test)
{
cout << test << endl;
}
};

void test_02_09()
{
MyPrint print;
print("hello world");//非常类似函数调用,也叫仿函数
}

class MyAdd
{
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};

void test_02_10()
{
MyAdd add;
int ret = add(100, 200);//仿函数
cout << "ret=" << ret << endl;

cout << MyAdd()(200, 300) << endl;//匿名函数对象
}

🤔总结

运算符说明友元函数重载全局函数重载成员函数重载
+一元加
-一元减
!逻辑非
+ +自增
- -自减
=赋值
*解引用
&取地址
[ ]索引/下标
->箭头
( )类型转换
<<左移
>>右移
+, -, *, /二元加、减、乘、除等否(构造函数可以部分实现这些)
==, !=比较
<, >, <=, >=比较
++(作为后缀)自增后缀
—(作为后缀)自减后缀

🚀注意:

  • 一元运算符(如 +, -, !, ++, --不通过成员函数重载,因为它们没有足够的参数来使得它们成为成员函数。(+a)
  • 二元运算符可以使用任意类型的组合进行重载,只需要符合运算的逻辑,并且可以作为友元函数或全局函数,也可以作为成员函数。(a+b)
  • 三位运算符和一些特定情性需要特殊注意,比如赋值运算符 = 和下标运算符 [ ] 通常通过成员函数重载。
  • 构造函数用做类型转换,它实际上重载了转换运算符 ()
  • 对于某些特殊的运算符,如赋值运算符和下标运算符,常常会通过成员函数的方式来重载。

继承

类与类上下级之间有共性和特性,使用继承可以减少很多重复代码。

继承中的构造和析构顺序,同名成员属性、同名静态成员的处理

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
159
160
161
162
163
164
165
class Base1
{
public:
Base1()
{
cout << "Base1的构造函数" << endl;
m_F = 100;
}

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

void func()
{
cout << "Base1下成员函数func()调用" << endl;
}

void func(int a)
{
cout << "Base1下成员函数func(int a)调用" << endl;
}

static void func1()//静态成员函数
{
cout << "Base1下静态成员函数func1()调用" << endl;
}

int m_F;
static int m_G;//静态成员变量,编译阶段分配内存,所有对象共享同一份数据,类内声明类外初始化
};
int Base1::m_G = 50;//类外初始化

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

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

void test_03_07()
{
//Base1 b;
Son4 s;//先构造父类,再构造子类,析构与构造相反!先进后出!
}

class Son5 :public Base1
{
public:
Son5()
{
m_F = 200; //和父类成员变量同名;
}

void func()
{
cout << "Son5下成员函数func()调用" << endl;//和父类成员函数同名;
}

static void func1()//静态成员函数
{
cout << "Son5下静态成员函数func1()调用" << endl;
}

int m_F;
static int m_G;//和父类静态成员变量同名;
};
int Son5::m_G = 60;

void test_03_08()//同名成员变量访问方式
{
Son5 s;
cout << "子类 Son5下 m_F=" << s.m_F << endl;//直接访问是访问子类的成员
cout << "父类 Base1下 m_F=" << s.Base1::m_F << endl;//在s.后面加上Base1作用域,就访问父类中的同名成员
}

void test_03_09()//同名成员函数调用方式
{
Son5 s;
s.func();//直接调用是调用子类的成员函数
s.Base1::func();//在s.后面加上Base1作用域,就调用父类中的同名函数
s.Base1::func(20);//调用父类中重载的同名函数
//如果子类中出现父类同名的成员函数,会隐藏掉父类中所有的同名成员函数,包括所有重载的;对静态成员函数也适用!
}

void test_03_10()//同名静态成员变量访问方式
{
//通过创建对象来访问静态变量;
cout << "通过创建对象来访问静态变量" << endl;
Son5 s;
cout << "子类 Son5下 m_G=" << s.m_G << endl;//直接访问就是访问子类的成员;
cout << "父类 Base1下 m_G=" << s.Base1::m_G << endl;//在s.后面加上Base1作用域,就访问父类中的同名成员;
cout << endl;

//不创建对象,通过类名来访问静态变量,因为是共享的;
cout << "通过类名来访问静态变量" << endl;
cout << "子类 Son5下 m_G=" << Son5::m_G << endl;//这里::是通过类名访问,不是作用域
cout << "父类 Base1下 m_G=" << Son5::Base1::m_G << endl;//相当于Son5::(Base1::m_G),第一个::是通过类名访问,第二个::是父类作用域
}

void test_03_11()//同名静态成员函数调用方式
{
//通过创建对象来访问静态成员函数;
cout << "通过创建对象来访问静态成员函数" << endl;
Son5 s;
s.func1();//直接调用是调用子类的成员函数
s.Base1::func1();//父类静态成员函数调用
cout << endl;

//不创建对象,通过类名来访问静态成员函数,因为是共享的;
cout << "通过类名来访问静态成员函数" << endl;
Son5::func1();
Son5::Base1::func1(); //相当于Son5::(Base1::func1());
}

//C++允许一个类继承多个父类,但可能会引发父类中有同名成员的问题,因此实际开发中不建议用;
class Base01
{
public:
Base01()
{
m_A1 = 100;
}
int m_A1;
};

class Base02
{
public:
Base02()
{
m_B1 = 100;
m_A1 = 700;
}
int m_B1;
int m_A1;
};

class Son06 : public Base01, public Base02//同时继承Base01和Base02,中间用逗号隔开
{
public:
Son06()
{
m_C1 = 300;
m_D1 = 400;
}
int m_C1;
int m_D1;
};

void test_03_12()
{
Son06 s;
cout << "sizeof Son06 =" << sizeof(s) << endl;
cout << "Base01 m_A1=" << s.Base01::m_A1 << endl;
cout << "Base02 m_A1=" << s.Base02::m_A1 << 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
//也叫钻石继承,一个父类有两个子类,然后再有一个子类继承了这两个子类;
//以下代码使用虚继承解决菱形继承中的多重数据成员问题
class Animal//动物
{
public:
int m_Age;
};

class Sheep : virtual public Animal//羊,加上virtual,虚继承!Animal称为虚其类
{

};

class Camel : virtual public Animal//驼
{

};

class Alpaca : public Sheep, public Camel//羊驼
{

};

void test_03_13()
{
Alpaca al;
al.Sheep::m_Age = 3;
al.Camel::m_Age = 5;//两个父类相同名称成员,要加作用域区分
//菱形继承会导致资源浪费,需要用到虚继承!虚继承后只有一份数据!
cout << "Sheep age=" << al.Sheep::m_Age << endl;
cout << "Camel age=" << al.Camel::m_Age << endl;
cout << "Alpaca age=" << al.m_Age << endl;//虚继承不会出现不明确
}

菱形继承的弊端:

  1. 二义性:当基类中的两个子类都重写了基类的某个成员时,由此产生的最子类在选择要访问哪个版本会引发二义性。
  2. 内存冗余:两个子类分别继承了自身的副本基类,增加了内存占用。
  3. 构造和析构的复杂性:成员的构造和析构可能变得复杂,需要多个构造函数和析构函数来处理不同继承链的初始化和清理。

😊改进方案:

  1. 虚继承:通过使用 virtual 关键字在基类继承中,可以指定共享的基类为虚基类,从而避免每个派生类存储独立的基类副本。
  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
68
69
70
71
72
73
74
75
76
// 基类 Animal
class Animal {
public:
int m_Age;
};

// 子类 Sheep,组合了 Animal 而不是继承
class Sheep {
private:
Animal animal; // Sheep 拥有一个 Animal 属性

public:
Sheep(int age) : animal(age) {}

void setAge(int age) {
animal.m_Age = age;
}

int getAge() const {
return animal.m_Age;
}
};

// 子类 Camel,同样组合 Animal
class Camel {
private:
Animal animal; // Camel 也拥有一个 Animal 属性

public:
Camel(int age) : animal(age) {}

void setAge(int age) {
animal.m_Age = age;
}

int getAge() const {
return animal.m_Age;
}
};

// 复合动物类 Alpaca,它组合了 Sheep 和 Camel
class Alpaca {
private:
Sheep sheep;
Camel camel;

public:
Alpaca(int sheepAge, int camelAge) : sheep(sheepAge), camel(camelAge) {}

int getSheepAge() const {
return sheep.getAge();
}

int getCamelAge() const {
return camel.getAge();
}

void setSheepAge(int age) {
sheep.setAge(age);
}

void setCamelAge(int age) {
camel.setAge(age);
}
};

int main() {
Alpaca al(3, 5);
cout << "Sheep age=" << al.getSheepAge() << endl;
cout << "Camel age=" << al.getCamelAge() << endl;
al.setSheepAge(4);
al.setCamelAge(6);
cout << "Updated Sheep age=" << al.getSheepAge() << endl;
cout << "Updated Camel age=" << al.getCamelAge() << endl;
return 0;
}


多态

基本概念

静态多态:函数重载、运算符重载。函数地址早绑定:编译阶段确定函数地址。
动态多态:派生类和虚函数实现运行时的多态。函数地址晚绑定:运行阶段确定函数地址。

💭动态多态的满足条件:

  1. 有继承关系;
  2. 子类重写父类的虚函数,不是重载,是完全相同的函数。

动态多态的使用:父类的指针或者引用指向子类的对象。

🚀重写(overload)和重载(override)的区别:

  • 范围和作用域:重写发生在派生类与其基类之间,需在不同的类中定义;而重载发生在同一个类或同一个作用域中,涉及多个具有相同名称但参数不同的函数。
  • 参数列表:重写要求派生类中重写的函数与基类中的虚函数参数列表完全相同,以实现真正的函数替代;重载的函数则有不同参数列表,根据参数的类型、数量、顺序来区分。
  • 关键字virtual:进行重写的基类函数必须有virtual关键字,以确保它能够接收动态绑定(Runtime Polymorphism);而重载则不强制要求virtual关键字。
  • 返回类型:尽管返回类型相同可以作为重写和重载的一种情况,但重载更侧重于参数的不同,不同的返回类型并不影响函数的重载关系。
  • 访问级别:在重写中,派生类的函数不能有比基类中相应函数更严格的访问控制(如基类方法为public,而派生类方法为private)。
  • 概念上的区别:重写实现多态性(Polymorphism),一个基类引用或指针可以指向不同派生类的对象,并调用相应的重写函数;重载允许编译时基于参数列表使用函数名称来区分多个函数。
  • 隐藏(Hiding):与重写和重载不同,在隐藏情况下基类函数被派生类同名函数盖掉,即使基类函数为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
class Animal//动物类
{
public:
//void speak()
//{
//cout << "动物在说话" << endl;
//}

virtual void speak()//这样可以让地址晚绑定!
{
cout << "动物在说话" << endl;
}
};

class Cat : public Animal//猫类
{
public:
void speak()//函数重写,可加也可不加virtual
{
cout << "猫在说话" << endl;
}
};

class Dog : public Animal//狗类
{
public:
void speak()//函数重写,可加也可不加virtual
{
cout << "狗在说话" << endl;
}
};

void doSpeak(Animal &animal)//执行说话的函数//父类的引用可以直接指向子类的对象!Animal &animal = c或者d;
{
animal.speak();//之前是地址早绑定,编译阶段就确定了函数地址!
//如果要让猫说话,就要让地址在运行阶段才绑定!要在父类函数名前加virtual!
}

void test04_01()
{
Cat c;
doSpeak(c);

Dog d;
doSpeak(d);
}

🚩纯虚函数与抽象类

父类中的虚函数没用时,可以写成纯虚函数,因为主要都是调用子类重写的函数。有纯虚函数的类称为抽象类。C++中常使用纯虚函数和抽象类来实现接口。

语法:virtual 返回值 函数名(参数列表) = 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
class Base//抽象类
{
public:
virtual void func() = 0;//纯虚函数
};

class Son : public Base
{
public:
virtual void func()//重写抽象类中的纯虚函数,不然不能创建对象!但重写不是=0,而是要有实现!
{
cout << "func函数调用" << endl;
}
};

void test04_02()
{
//Base b;//不可实例化对象
//new Base;//堆区也不可实例化对象
Son s;//可以实例化对象
Base *p = new Son();//堆区数据,手动开辟,手动释放!//多态使用的条件:父类的指针或者引用指向子类对象!
p->func();
delete p;//手动释放!
}

🤔思考:如果 Base 并不是抽象类,即并没有纯虚函数,此时 func () 的代码为:

1
2
3
virtual void func() {
cout << "父类" << endl;
}

输出结果依然为:
1
func函数调用

分析函数执行顺序:(不包括 Son s 的情况下)

  1. 基类构造函数 Base()
  2. 派生类构造函数 Son()
  3. 派生类重写的函数 Son::func()
  4. 派生类析构函数 ~Base()
  5. 基类析构函数 ~Son()
    由此可见,即使 Base 不是抽象类,p->func() 仍然只调用子类对象的对应函数。结果和 Son s; s.func() 的输出结果一致。

进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Base {
public:
Base() {
cout << "Base constructor called." << endl;
}

virtual void func() {
cout << "Base func called." << endl;
}

virtual ~Base() {
cout << "Base destructor called." << endl;
}
};

class Son : public Base {
public:
Son() {
cout << "Son constructor called." << endl;
}

void func() override {
cout << "Son func called." << endl;
}

~Son() override {
cout << "Son destructor called." << endl;
}
};

void test04_02() {
Base* p = new Son();
p->func();
delete p;
}

int main() {
test04_02();
return 0;
}

🚩虚析构与纯虚析构

如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
💡解决方法:将父类中的析构函数写成虚析构或纯虚析构函数。

💭虚析构与纯虚析构共性:
1,可以解决父类指针释放子类的堆区对象
2,都需要有具体的函数实现

💭虚析构与纯虚析构区别:纯虚析构,该类属于抽象类,无法实例化对象。
如果子类中没有堆区数据(new),可以不写虚析构。

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
class Animal1
{
public:
Animal1()
{
cout << "Animal1构造函数调用" << endl;
}

//virtual ~Animal1()//虚析构
//{
// cout << "Animal1析构函数调用" << endl;
//}
virtual ~Animal1() = 0;//纯虚析构

virtual void speak() = 0;//纯虚函数;
};

Animal1::~Animal1()//纯虚析构函数一定要有具体实现函数
{
cout << "Animal1纯虚析构函数调用" << endl;
}

class Lion : public Animal1
{
public:
Lion(string name)
{
cout << "Lion构造函数调用" << endl;
m_Name = new string(name);//堆区
}

virtual void speak()
{
cout << *m_Name << "狮子在说话" << endl;
}

~Lion()
{
if (m_Name != nullptr)
{
cout << "Lion析构函数调用" << endl;//父类用虚析构,子类的虚析构才能被调用。现在可以使用虚析构函数释放掉堆区内存了
delete m_Name;
m_Name = nullptr;
}
}

string *m_Name;//指针;
};

void test04_03()
{
Animal1 *p = new Lion("Simba");
p->speak();
delete p;
}
//输出结果:
//Animal1构造函数调用
//Lion构造函数调用
//Simba狮子在说话
//Lion析构函数调用
//Animal1纯虚析构函数调用