C++-Part2——核心编程

[TOC]

内存分区模型

内存分区概述

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

    • 代码区:存放函数体的二进制代码,由操作系统进行管理的
    • 全局区:存放全局变量、静态变量、字符串常量、全局常量,在程序结束后由操作系统释放
    • 栈区:由编译器自动分配释放,存放函数的参数值、局部变量、局部常量等
    • 堆区:由程序员分配和释放,若程序员不释放,则程序结束时由操作系统回收
  • 内存分区的意义:

    • 不同区域存放的数据,赋予不同的生命周期,提供更大的灵活编程

程序运行前

  • 在程序编译后,生成了 exe 可执行程序,在程序运行前分为全局区代码区
  • 代码区:
    • 存放 CPU 执行的机器指令
    • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
    • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
  • 全局区:
    • 全局变量、静态变量存放在此.
    • 全局区包含常量区,存放字符串常量、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
//全局变量
int g_a = 10;
int g_b = 10;

//全局常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {
//局部变量
int a = 10;
int b = 10;

//打印地址
cout << "局部变量a地址为: " << (int) &a << endl;
cout << "局部变量b地址为: " << (int) &b << endl;

cout << "全局变量g_a地址为: " << (int) &g_a << endl;
cout << "全局变量g_b地址为: " << (int) &g_b << endl;

//静态变量
static int s_a = 10;
static int s_b = 10;

cout << "静态变量s_a地址为: " << (int) &s_a << endl;
cout << "静态变量s_b地址为: " << (int) &s_b << endl;

cout << "字符串常量地址为: " << (int) &"hello world" << endl;
cout << "字符串常量地址为: " << (int) &"hello world1" << endl;

cout << "全局常量c_g_a地址为: " << (int) &c_g_a << endl;
cout << "全局常量c_g_b地址为: " << (int) &c_g_b << endl;

const int c_l_a = 10;
const int c_l_b = 10;
cout << "局部常量c_l_a地址为: " << (int) &c_l_a << endl;
cout << "局部常量c_l_b地址为: " << (int) &c_l_b << endl;

return 0;
}
/*
* 局部变量a地址为: 14088016
* 局部变量b地址为: 14088004
* 全局变量g_a地址为: 15974400
* 全局变量g_b地址为: 15974404
* 静态变量s_a地址为: 15974412
* 静态变量s_b地址为: 15974416
* 字符串常量地址为: 15965632
* 字符串常量地址为: 15965664
* 全局常量c_g_a地址为: 15965260
* 全局常量c_g_b地址为: 15965472
* 局部常量c_l_a地址为: 14087992
* 局部常量c_l_b地址为: 14087980
*/

程序运行后

  • 栈区:
    • 由编译器自动分配释放, 存放函数的参数值,局部变量等
    • 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
1
2
3
4
5
6
7
8
9
10
11
12
13
int *func() {
int a = 10;
return &a;
}

int main() {
int *p = func();

cout << *p << endl; // 在外部第一次调用时,编译器会对数据做一次保留
cout << *p << endl; // 第二次调用时彻底消失

return 0;
}
  • 堆区:
    • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
    • 在 C++ 中主要利用 new 在堆区开辟内存
1
2
3
4
5
6
7
8
9
10
11
12
13
int *func() {
int *a = new int(10);
return a;
}

int main() {
int *p = func();

cout << *p << endl;
cout << *p << endl;

return 0;
}

new 操作符

  • 利用 new 操作符在堆区开辟数据

  • 利用 delete 操作符在堆区释放数据

  • 语法:

    • new 数据类型
    • delete 变量名
  • 返回的是该类型的指针/地址

    删除的类型应当是指针/地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 堆区创建int
int *func() {
int *a = new int(10); // 利用new创建的数据,会返回该数据对应的类型的指针
return a;
}

int main() {
int *p = func();
cout << *p << endl;

// 利用delete释放堆区数据
delete p;
cout << *p << endl; // 报错,释放的空间不可访问

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 堆区开辟数组
int main() {
int *arr = new int[10]{1, 2, 3, 4, 5,};

for (int i = 0; i < 10; i++) {
arr[i] = i + 100;
}

for (int i = 0; i < 10; i++) {
cout << arr[i] << endl;
}
//释放数组 delete 后加 []
delete[] arr;

return 0;
}

引用

参考:c++中,引用和指针的区别是什么? - 编程指北的回答 - 知乎

引用的基本使用

  • 作用:给变量起别名

  • 语法:数据类型 &别名 = 原名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
int a = 10;
int &b = a;

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

b = 100;

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

return 0;
}

引用注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变
1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
int a = 10;
int b = 20;
// int &c; // 错误,引用必须初始化
int &c = a; // 一旦初始化后,就不可以更改
c = b; // 这是赋值操作,不是更改引用

cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << 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
//1. 值传递
void mySwap01(int a, int b) {
int temp = a;
a = b;
b = temp;
}

//2. 地址传递
void mySwap02(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

//3. 引用传递
void mySwap03(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}

int main() {
int a = 10;
int b = 20;
cout << "a:" << a << " b:" << b << endl; // a:10 b:20

mySwap01(a, b);
cout << "a:" << a << " b:" << b << endl; // a:10 b:20

mySwap02(&a, &b);
cout << "a:" << a << " b:" << b << endl; // a:20 b:10

mySwap03(a, b);
cout << "a:" << a << " b:" << b << endl; // a:10 b: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
// 返回局部变量引用
int &test01() {
int a = 10; // 局部变量
return a;
}

// 返回静态变量引用
int &test02() {
static int a = 20; // 静态变量,操作的是位于全局区的变量,不会新建一个对象
// int &a = *(new int(20)); // 堆区变量,两次调用返回两个不同的对象
return a;
}

int main() {
// 不能返回局部变量的引用
int &ref = test01();
cout << "ref = " << ref << endl; // ref = 13630156
cout << "ref = " << ref << endl; // ref = 13630156

// 如果函数做左值,那么必须返回引用
int &ref2 = test02();
cout << "ref2 = " << ref2 << endl; // ref2 = 20
cout << "ref2 = " << ref2 << endl; // ref2 = 20

test02() = 1000;

cout << "ref2 = " << ref2 << endl; // ref2 = 1000
cout << "ref2 = " << ref2 << endl; // ref2 = 1000

return 0;
}

引用的本质

  • 本质:引用的本质在 C++ 内部实现是一个指针常量
  • C++ 推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 发现是引用,转换为 int* const ref = &a;
void func(int &ref) {
ref = 100; // ref是引用,转换为*ref = 100
}

int main() {
int a = 10;

// 自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int &ref = a;
ref = 20; // 内部发现ref是引用,自动帮我们转换为: *ref = 20;

cout << "a:" << a << endl;
cout << "ref:" << ref << endl;

func(a);
return 0;
}

常量引用

  • 作用:常量引用主要用来修饰形参,防止误操作
  • 在函数形参列表中,加 const 修饰形参,防止形参改变实参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 发现是引用,转换为 int* const ref = &a;
// 引用使用的场景,通常用来修饰形参
void showValue(const int &v) {
// v += 10;
cout << v << endl;
}

int main() {
// int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
// 加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int &ref = 10;

// ref = 100; // 加入const后不可以修改变量
cout << ref << endl;

// 函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);

return 0;
}

函数提高

函数默认参数

  • 在 C++ 中,函数的形参列表中的形参是可以有默认值的。

  • 语法: 返回值类型 函数名 (参数= 默认值) {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int func1(int a, int b = 10, int c = 10) {
return a + b + c;
}

// 1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
// 2. 如果函数声明/实现有默认值,则函数实现/声明的时候就不能有默认参数
int func2(int a = 10, int b = 10);

int func2(int a, int b) {
return a + b;
}

int main() {
cout << "ret = " << func1(20, 20) << endl;
cout << "ret = " << func2(100) << endl;

return 0;
}

函数占位参数

  • C++ 中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

  • 语法: 返回值类型 函数名 (数据类型) {}

1
2
3
4
5
6
7
8
9
10
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int = 10) {
cout << "this is func" << endl;
}

int main() {
func(10, 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
//函数重载需要函数都在同一个作用域下
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, 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;
//}


int main() {
func();
func(10);
func(3.14);
func(10, 3.14);
func(3.14, 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
// 1、引用作为重载条件
void func1(int &a) { // int &a = 10; 不合法
cout << "func (int &a) 调用 " << endl;
}

void func1(const int &a) { // const int &a = 10; 合法
cout << "func (const int &a) 调用 " << endl;
}


// 2、函数重载碰到函数默认参数
void func2(int a, int b = 10) {
cout << "func2(int a, int b = 10) 调用" << endl;
}

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

int main() {
int a = 10;
func1(a); // 调用无const
func1(10);// 调用有const

//func2(10); // 碰到默认参数产生歧义,需要避免

return 0;
}

类和对象

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

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

封装

封装的意义

  • 封装的意义:

    • 将属性和行为作为一个整体
    • 将属性和行为加以权限控制
      • 公共权限(public):类内可以访问,类外可以访问,子类可以访问
      • 保护权限(protected):类内可以访问,类外不可以访问,子类可以访问
      • 私有权限(private):类内可以访问,类外不可以访问,子类不可以访问
  • 语法: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
class Person {
//姓名 公共权限
public:
string m_Name;

//汽车 保护权限
protected:
string m_Car;

//银行卡密码 私有权限
private:
int m_Password;

public:
void func() {
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
}
};

int main() {
Person p;
p.m_Name = "李四";
//p.m_Car = "奔驰"; // 保护权限类外访问不到
//p.m_Password = 123; // 私有权限类外访问不到
p.func();

return 0;
}

truct 和 class 区别

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

  • 区别:

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

成员属性设置为私有

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

对象的初始化和清理

构造函数和析构函数

  • 对象的初始化和清理是两个非常重要的安全问题

    • 一个对象或者变量没有初始状态,对其使用后果是未知
    • 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
  • 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
class Person {
public:
//构造函数
Person() {
cout << "Person的构造函数调用" << endl;
}

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

};

int main() {
Person p;

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
// 1、构造函数分类
class Person {

public:
int age;

public:
// 无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}

// 有参普通构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}

// 拷贝构造函数
// 可以不写,编译器提供的拷贝函数自动复制成员变量值
Person(const Person &p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}

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

};

// 2、构造函数的调用
int main() {
// 调用无参构造函数
Person p; //调用无参构造函数

// 调用有参的构造函数
// 2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();

// 2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
Person(10); // 单独写就是匿名对象 当前行结束之后,马上析构

// 2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//Person p6(p4);

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
// 1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person man(100); // p对象已经创建完毕
Person newman(man); // 调用拷贝构造函数
Person newman2 = man; // 拷贝构造
Person newman3;
newman3 = man; // 不是调用拷贝构造函数,赋值操作
}

// 2. 值传递的方式给函数参数传值
// 相当于 Person p1 = p;
void doWork(Person p1) {}

void test02() {
Person p; // 无参构造函数
doWork(p);
}

// 3. 以值方式返回局部对象
Person doWork2() {
Person p1;
cout << (int *) &p1 << endl;
return p1;
}

void test03() {
Person p = doWork2();
cout << (int *) &p << endl;
}


int main() {
test01();
// test02();
// test03();

return 0;
}

构造函数调用规则

  • 默认情况下,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
void test01() {
Person p1(18);
Person p2(p1); //如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
cout << "p2的年龄为: " << p2.age << endl;
}

void test02() {
// 如果用户提供有参构造,编译器不会提供默认构造,但会提供拷贝构造
Person p1; // 此时如果用户自己没有提供默认构造,会出错
Person p2(10); // 用户提供的有参
Person p3(p2); // 此时如果用户没有提供拷贝构造,编译器会提供

// 如果用户提供拷贝构造,编译器不会提供其他构造函数
Person p4; // 此时如果用户自己没有提供默认构造,会出错
Person p5(10); // 此时如果用户自己没有提供有参,会出错
Person p6(p5); // 用户自己提供拷贝构造
}

int main() {
test01();
test02();

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
class Person {

public:
int m_age{};
int *m_height{};

public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}

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

//拷贝构造函数
Person(const Person &p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,有可能会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}

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

int main() {
Person p1(18, 180);
Person p2(p1);

cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << 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
class Person {

private:
int m_A;
int m_B;
int m_C;

public:
// 传统方式初始化
//Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
//}

//初始化列表方式初始化
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}

void PrintPerson() const {
cout << "mA:" << m_A << endl;
cout << "mB:" << m_B << endl;
cout << "mC:" << m_C << endl;
}
};

int main() {
Person p(1, 2, 3);
p.PrintPerson();

return 0;
}

类对象作为类成员

  • C++ 类中的成员可以是另一个类的对象,我们称该成员为对象成员
1
2
3
4
5
class A {}

class B {
A a;
}
  • 一个对象 A 与其对象成员 B 的构造和析构的先后顺序?
    • 先构造对象成员 B
    • 先析构对象 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
class Phone {
public:
string m_PhoneName;

Phone(string name) {
m_PhoneName = name;
cout << "Phone构造" << endl;
}

~Phone() {
cout << "Phone析构" << endl;
}
};


class Person {
public:
string m_Name;
Phone m_Phone;

//初始化列表可以告诉编译器调用哪一个构造函数
Person(string name, string pName) : m_Name(name), m_Phone(pName) {
cout << this->m_Name << endl;
cout << "Person构造" << endl;
}

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

void playGame() const {
cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
}
};


int main() {
Person p("张三", "苹果X");
p.playGame();

return 0;
}

静态成员

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

  • 静态成员分为:

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

    1. 在编译阶段分配内存
    2. 类内声明,类外初始化
    3. 所有对象共享同一份数据
  • 静态成员函数特点:

    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
53
54
55
56
57
58
59
60
61
class Person {

private:
static int m_C; // 静态成员变量也是有访问权限的
int m_B;

// 静态成员函数也是有访问权限的
static void func2() {
cout << "func2调用" << endl;
}

public:
static int m_A; // 静态成员变量

static void func() {
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; // 错误,不可以访问非静态成员变量
}

};

int Person::m_A = 10;
int Person::m_C = 10;

void test01() {
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl;

Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
cout << "p2.m_A = " << p2.m_A << endl;

//2、通过类名
cout << "m_A = " << Person::m_A << endl;

//cout << "m_C = " << Person::m_C << endl; //私有权限访问不到
}

void test02() {
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.func();

//2、通过类名
Person::func();

//Person::func2(); // 私有权限访问不到
}

int main() {
test01();
test02();

return 0;
}

C++ 对象模型和 this 指针

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

  • 在 C++ 中,类内的成员变量和成员函数分开存储

  • 注意:只有非静态成员变量才属于类的对象上

    1. 空对象占用的是 1 字节
      • C++ 为每个空对象分配一个字节的空间,为了区分空对象占内存的位置
    2. 存在非静态成员变量时,按照非静态成员变量的大小和分配空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
public:
// 非静态成员变量占对象空间
int mA;
// 静态成员变量不占对象空间
static int mB;

Person() {
mA = 0;
}

// 函数也不占对象空间,所有函数共享一个函数实例
void func() const {}

//静态成员函数也不占对象空间
static void s_func() {}
};

int main() {
cout << sizeof(Person) << endl;

return 0;
}

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
class Person {
public:
int age;

Person(int age) {
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}

Person &PersonAddPerson(Person p) {
this->age += p.age;
//返回对象本身
return *this;
}
};

int main() {
Person p1(10);
cout << "p1.age = " << p1.age << endl;

Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl;

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
//空指针访问成员函数
class Person {
public:
int mAge;

void ShowClassName() {
cout << "我是Person类!" << endl;
}

void ShowPerson() {
if (this == nullptr) {
return;
}
cout << mAge << endl;
}
};

int main() {
Person *p = nullptr;
p->ShowClassName(); // 空指针,可以非静态调用成员函数
p->ShowPerson(); // 但是如果成员函数中用到了this指针,就不可以了

return 0;
}

const 修饰成员函数

  • 常函数:

    • 声明成员函数时在函数名后加 const ,即常函数
    • 常函数内不可以修改非静态成员变量
  • 常对象:

    • 声明对象前加 const ,则该对象为常对象
    • 常对象只能调用常函数
  • 用 mutable 修饰的非静态成员变量,在常函数、常变量中依然可以修改

  • 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
class Person {
public:
int m_A;
mutable int m_B; // 可修改、可变的

Person() {
m_A = 0;
m_B = 0;
}

void ShowPerson() const {
// const Type* const pointer;
// const 修饰成员函数,表示指针指向的内存空间的数据不能修改,除了 mutable 修饰的变量
this->m_B = 100;
}

void MyFunc() const {
//mA = 10000;
}
};

int main() {
const Person person; // 常量对象

cout << person.m_A << endl;
//person.mA = 100; // 常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; // 但是常对象可以修改mutable修饰成员变量

// 常对象访问成员函数
person.MyFunc(); // 常对象只能调用常函数

return 0;
}

友元

  • 针对问题:对于有些私有属性,也想让类外特殊的函数或类访问

  • 友元 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
class Building {
// 告诉编译器 goodGay 全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building *building);

private:
string m_BedRoom; //卧室

public:
string m_SittingRoom; //客厅

Building() {
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
};


void goodGay(Building *building) {
cout << "好基友正在访问: " << building->m_SittingRoom << endl;
cout << "好基友正在访问: " << building->m_BedRoom << endl;
}

int main() {
Building b;
goodGay(&b);

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
// 先声明类
class Building;

class goodGay {
private:
Building *building;

public:
goodGay();

void visit();
};

// 实现类
class Building {
// 告诉编译器 goodGay 类是 Building 类的好朋友,可以访问到 Building 类中私有内容
friend class goodGay;

private:
string m_BedRoom; // 卧室

public:
string m_SittingRoom; // 客厅

Building();
};

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

goodGay::goodGay() {
building = new Building;
}

void goodGay::visit() {
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}

int main() {
goodGay gg;
gg.visit();

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
class Building;

class goodGay {
private:
Building *building;

public:
goodGay();

void visit(); // 只让 visit 函数作为 Building 的好朋友,可以发访问 Building 中私有内容

void visit2();
};


class Building {
// 告诉编译器 goodGay 类中的 visit 成员函数是 Building 好朋友,可以访问私有内容
friend void goodGay::visit();

private:
string m_BedRoom;//卧室

public:
string m_SittingRoom; //客厅

Building();
};

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

goodGay::goodGay() {
building = new Building;
}

void goodGay::visit() {
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void goodGay::visit2() {
cout << "好基友正在访问" << building->m_SittingRoom << endl;
//cout << "好基友正在访问" << building->m_BedRoom << endl;
}

int main() {
goodGay gg;
gg.visit();

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
class Person {
public:
int m_A{};
int m_B{};

Person() = default;

Person(int a, int b) {
this->m_A = a;
this->m_B = b;
}

// 通过成员函数实现 + 号运算符重载
Person operator+(const Person &p) const {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
};

// 通过全局函数实现 + 号运算符重载
// 但是等价的运算符重载只能实现一次
//Person operator+(const Person& p1, const Person& p2) {
// Person temp(0, 0);
// temp.m_A = p1.m_A + p2.m_A;
// temp.m_B = p1.m_B + p2.m_B;
// return temp;
//}

//运算符重载 可以发生函数重载
Person operator+(const Person &p2, int val) {
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}

void test() {
Person p1(10, 10);
Person p2(20, 20);

Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;

Person p4 = p3 + 10; //相当于 operator+(p3,10)
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}

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

左移运算符重载

  • 作用:可以输出自定义数据类型

  • 总结:

    • 一般不会利用成员函数重载左移运算符,因为无法实现 cout 在左侧
    • 重载左移运算符配合友元可以实现输出自定义数据类型
  • 注意

    1. ostream 对象只能有一个,因此要使用引用
    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
class Person {
friend ostream &operator<<(ostream &out, Person &p);

private:
int m_A;
int m_B;

public:
Person(int a, int b) {
this->m_A = a;
this->m_B = b;
}

// 成员函数实现的 p << cout 不是我们想要的效果
//void operator<<(Person& p){
//}
};

// 全局函数实现左移重载
ostream &operator<<(ostream &out, Person &p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}

int main() {
Person p1(10, 20);
cout << p1 << "hello world" << 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
48
49
50
51
52
53
54
class MyInteger {
friend ostream &operator<<(ostream &out, MyInteger myint);

private:
int m_Num;

public:
MyInteger() {
m_Num = 0;
}

// 前置++
MyInteger &operator++() {
//先++
m_Num++;
//再返回
return *this;
}

// 后置++
// int 占位符可以区分前置后置
MyInteger operator++(int) {
MyInteger temp = *this; // 记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
};


ostream &operator<<(ostream &out, MyInteger myint) {
out << myint.m_Num;
return out;
}


//前置++: 先++, 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}

//后置++: 先返回, 再++
void test02() {
MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}

int main() {
test01();
test02();
return 0;
}

赋值运算符重载

  • 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
49
50
51
52
53
54
55
56
57
58
59
60
class Person {
public:
// 年龄的指针
int *m_Age;

Person(int age) {
// 将年龄数据开辟到堆区
m_Age = new int(age);
}

// 重载赋值运算符
Person &operator=(Person &p) {
if (m_Age != nullptr) { // 删除之前的数据,防止内存泄漏
delete m_Age;
m_Age = nullptr;
}
// 编译器提供的代码是浅拷贝
//m_Age = p.m_Age;

// 提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);

// 返回自身
return *this;
}

~Person() {
if (m_Age != nullptr) {
delete m_Age;
m_Age = nullptr;
}
}
};


void test01() {
Person p1(18);
Person p2(20);
Person p3(30);

p3 = p2 = p1; // 赋值操作
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main() {
test01();

int a = 10;
int b = 20;
int c = 30;

c = b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << 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
48
class Person {
public:
string m_Name;
int m_Age;

Person(string name, int age) {
this->m_Name = name;
this->m_Age = age;
};

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

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

void test01() {
Person a("孙悟空", 18);
Person b("孙悟空", 18);

if (a == b) {
cout << "a和b相等" << endl;
} else {
cout << "a和b不相等" << endl;
}
if (a != b) {
cout << "a和b不相等" << endl;
} else {
cout << "a和b相等" << endl;
}
}


int main() {
test01();
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
class MyPrint {
public:
void operator()(string text) {
cout << text << endl;
}
};

void test01() {
// 重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world");
}


class MyAdd {
public:
int operator()(int v1, int v2) {
return v1 + v2;
}
};

void test02() {
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;
//匿名对象调用
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

int main() {
test01();
test02();
return 0;
}

继承

子类是对父类的扩展。

继承的基本语法

  • 继承的好处:可以减少重复的代码
  • class A : public B;
    • 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
//公共页面
class BasePage {
public:
static void header() {
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}

static void footer() {
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}

static void left() {
cout << "Java,Python,C++...(公共分类列表)" << endl;
}

};

//Java页面
class Java : public BasePage {
public:
static void content() {
cout << "JAVA学科视频" << endl;
}
};

//Python页面
class Python : public BasePage {
public:
static void content() {
cout << "Python学科视频" << endl;
}
};

//C++页面
class CPP : public BasePage {
public:
static void content() {
cout << "C++学科视频" << endl;
}
};

int main() {
//Java页面
cout << "Java下载视频页面如下: " << endl;
Java::header();
Java::footer();
Java::left();
Java::content();
cout << "--------------------" << endl;

//Python页面
cout << "Python下载视频页面如下: " << endl;
Python::header();
Python::footer();
Python::left();
Python::content();
cout << "--------------------" << endl;

//C++页面
cout << "C++下载视频页面如下: " << endl;
CPP::header();
CPP::footer();
CPP::left();
CPP::content();

return 0;
}

继承方式

  • 继承的语法:class 子类 : 继承方式 父类
  • 继承方式一共有三种:
    • 公共继承
    • 保护继承
    • 私有继承
公共继承 保护继承 私有继承
公共成员 公共成员 保护成员 私有成员
保护成员 保护成员 保护成员 私有成员
私有成员 不可访问 不可访问 不可访问

继承中的对象模型

  • 总结:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
int m_A;
protected:
int m_B;
private:
int m_C; //私有成员只是被隐藏了,但是还是会继承下去
};

//公共继承
class Son : public Base {
public:
int m_D;
};

int main() {
cout << "sizeof Son = " << sizeof(Son) << endl;
// sizeof Son = 16

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

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
class Base {
public:
int m_A;

Base() {
m_A = 100;
}

void func() {
cout << "Base - func()调用" << endl;
}

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


class Son : public Base {
public:
int m_A;

Son() : m_A(200) {}

// 子类会隐藏父类中所有版本的同名成员函数
void func() {
cout << "Son - func()调用" << endl;
}
};

int main() {
Son s;
cout << "Son下的m_A = " << s.m_A << endl;
cout << "Base下的m_A = " << s.Base::m_A << endl;

s.func();
s.Base::func();
s.Base::func(10);

return EXIT_SUCCESS;
}

多继承语法

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

  • 语法: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
class Base1 {
public:
int m_A;

Base1() : m_A(100) {}
};

class Base2 {
public:
int m_A;

Base2() : m_A(200) {}
};

class Son : public Base2, public Base1 {
public:
int m_C;
int m_D;

Son() : m_C(300), m_D(400) {}
};


int main() {
//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;
cout << "s.Base1::m_A = " << s.Base1::m_A << endl;
cout << "s.Base2::m_A = " << s.Base2::m_A << endl;

return 0;
}

菱形继承

  • 菱形(钻石)继承概念:

    • 两个派生类继承同一个基类
    • 又有某个类同时继承者两个派生类
  • 典型的菱形继承案例:

  • IMG_256

  • 菱形继承问题:

    1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
    2. 草泥马继承自动物的数据继承了两份,其实这份数据我们只需要一份就可以,此为资源浪费。
  • 总结:

    • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
    • 利用虚继承可以解决菱形继承问题,从虚基类继承的成员变量只会有一份内存空间。
    • 虚基类是子类以虚继承方式继承的父类的别称,并不是抽象类,其依然能够创建实例对象
    • 虚继承的本质是将自己继承过来的成员作为一个指针(virtual base pointer,vbptr)指向父类的空间(vbtable)
      • 因此修改任何一个父类或者子类,实际上都是在修改同一份数据
      • 类似于 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
30
31
32
33
34
35
36
class Animal {
public:
int m_Age{};
};

// 继承前加virtual关键字后,变为虚继承
// 此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {
};

class Tuo : virtual public Animal {
};

class SheepTuo : public Sheep, public Tuo {
};

int main() {
Animal a;
a.m_Age = 15;
cout << "s.m_Age = " << a.m_Age << endl;
Sheep s;
s.m_Age = 10;
cout << "s.m_Age = " << s.m_Age << endl;
Tuo t;
t.m_Age = 20;
cout << "t.m_Age = " << t.m_Age << endl;

SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;

return 0;
}

多态

多态的基本概念

  • 多态分为两类

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

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

    • 多态满足条件
      • 有继承关系
      • 子类重写父类中的虚函数
    • 多态使用条件
      • 父类指针或引用指向子类对象
    • 我们希望传入什么对象,那么就调用什么对象的函数
      • 如果函数地址在编译阶段就能确定,那么静态联编
      • 如果函数地址在运行阶段才能确定,就是动态联编
    • 重写:函数返回值类型 函数名 参数列表 需要完全一致才称为重写
  • 多态的优点:

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

    • 加上 virtual 关键字之前,Animal 类的成员函数不存储在类对象中,因此一个 Animal 实例的大小为 1 字节
    • 加上 virtual 关键字之后,Animal 类将保存一个虚函数指针(virtual function pointer,vfptr)指向自己的虚函数表(virtual function table,vftable),并存储在该类对象中,因此一个 Animal 实例(无论有多少个虚函数)的大小为 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
class Animal {
public:
// 函数前面加上 virtual 关键字就是虚函数,编译器在编译的时候不能确定函数调用。
virtual void speak() {
cout << "动物在说话" << endl;
}

virtual void speak2() {
cout << "动物在说话" << endl;
}
};

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

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

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

int main() {
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
Animal animal;
DoSpeak(animal);
cout << "sizeof animal = " << sizeof animal << endl;

return 0;
}

纯虚函数和抽象类

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

  • 解决:因此可以将虚函数改为纯虚函数,要求子类必须重写纯虚函数

  • 语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

  • 只要类中有一个纯虚函数,这个类就称为抽象类

  • 抽象类特点:

    • 无法实例化对象
    • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
virtual void func() = 0;
};

class Son : public Base {
public:
void func() override {
cout << "func调用" << endl;
};
};

int main() {
//Base * = new Base; // 错误,抽象类无法实例化对象
Base *base = new Son;
base->func();
delete base;//记得销毁

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

      - 总结:

      1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

      2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

      3. 拥有纯虚析构函数的类也属于抽象类

      ```C++
      class Animal {
      public:
      Animal() {
      cout << "Animal 构造函数调用!" << endl;
      }

      // 析构函数加上virtual关键字,变成虚析构函数
      //virtual ~Animal()
      //{
      // cout << "Animal虚析构函数调用!" << endl;
      //}
      virtual ~Animal() = 0;

      virtual void Speak() = 0;
      };

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

      class Cat : public Animal {
      public:
      string *m_Name;

      explicit Cat(string name) {
      cout << "Cat 构造函数调用!" << endl;
      m_Name = new string(std::move(name));
      }

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

      void Speak() override {
      cout << *m_Name << " 小猫在说话!" << endl;
      }
      };

      int main() {
      Animal *animal = new Cat("Tom");
      animal->Speak();
      delete animal;

      return 0;
      }
      ## 文件操作

文件操作概述

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

  • 文件类型分为两种:

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

    1. ofstream:写操作
    2. ifstream:读操作
    3. fstream:读写操作
  • 文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式
  • 注意:文件打开方式可以配合使用,利用 | 操作符
    • 例如:用二进制方式写文件 ios::binary | ios:: out

文本文件

写文件

  • 写文件步骤如下:

    1. 包含头文件:#include <fstream>

    2. 创建流对象:ofstream ofs;

    3. 打开文件:ofs.open("文件路径", 打开方式);

    4. 写数据:ofs << "写入的数据";

    5. 关闭文件:ofs.close();

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

void test01() {
ofstream ofs;
ofs.open("test.txt", ios::out);

ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:18" << endl;

ofs.close();
}

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

读文件

  • 读文件步骤如下:
    1. 包含头文件:#include <fstream>
    2. 创建流对象:ifstream ofs;
    3. 打开文件:ifs.open("文件路径",打开方式);
    4. 检查是否打开成功:!ifs.is_open()
    5. 读数据,四种方式读取
    6. 关闭文件:ifs.close();
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 <fstream>
#include <string>

void test01() {
ifstream ifs;
ifs.open("test.txt", ios::in);

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

// 1. 第一种方式
char buf[1024] = { 0 };
while (ifs >> buf) {
cout << buf << endl;
}

// 2. 第二种方式
char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf))) {
cout << buf << endl;
}

// 3. 第三种方式
string buf;
while (getline(ifs, buf)) {
cout << buf << endl;
}

// 4. 第四种方式(不推荐)
char c;
while ((c = ifs.get()) != EOF) {
cout << c;
}

ifs.close();
}

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

二进制文件

写文件

  • 写二进制文件主要利用流对象的成员函数 write()

    • 函数原型 :ostream& write(const char * buffer, int len);
    • 参数解释:
      • 字符指针 buffer 指向内存中一段存储空间
      • len 是读写的字节数
  • 建议使用 char[] 进行二进制的写入

  • 对象指针要强转为 char *,同时计算读写的大小空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
public:
char m_Name[64];
int m_Age;
};

// 写二进制文件
void test01() {
ofstream ofs("person.txt", ios::out | ios::binary);

Person p = {"张三", 18};
ofs.write((const char *) &p, sizeof(p));

ofs.close();
}

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

读文件

  • 读二进制文件主要利用流对象的成员函数 read()
    • 函数原型: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
class Person {
public:
char m_Name[64];
int m_Age;
};

void test01() {
ifstream ifs("person.txt", ios::in | ios::binary);

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

Person p;
ifs.read((char *) &p, sizeof(p));
cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;

ifs.close();
}

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

模板

模板的概念

  • 模板的特点:
    • 模板不可以直接使用,只是一个框架
    • 模板的通用并不是万能的
  • C++ 另一种编程思想称为泛型编程,主要利用的技术就是模板
  • C++ 提供两种模板机制:函数模板类模板

函数模板

函数模板语法

  • 函数模板作用:

    • 建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
  • 语法:

    • template<typename/class T>
      函数声明或定义
      
      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

      - 解释:

      - `template`:声明创建模板
      - `typename/class`:表面其后面的符号是一种数据类型,可以用 class 代替
      - `T`:通用的数据类型,名称可以替换,通常为大写字母

      - 总结:

      - 使用函数模板有两种方式:自动类型推导、显式指定类型
      - 模板的目的是将类型参数化、为了提高复用性,需要熟练掌握

      - 注意:

      - 必须**确定出数据类型 T** 并且**同一个 T 推导结果一致**,才可以使用。

      ```C++
      // 利用模板提供通用的交换函数
      template<typename T> // typename 可以换成 class
      void mySwap(T &a, T &b) {
      T temp = a;
      a = b;
      b = temp;
      }

      void test01() {
      int a = 10;
      int b = 20;
      char c = 'c';

      // 利用模板实现交换
      // 1、自动类型推导
      mySwap(a, b);
      //mySwap(a, c); // 错误,推导不出一致的 T 类型

      // 2、显式指定类型
      mySwap<int>(a, b);

      cout << "a = " << a << endl;
      cout << "b = " << b << endl;
      }

      template<class T>
      void func() {
      cout << "func 调用" << endl;
      }

      void test02() {
      //func(); // 错误,模板不能独立使用,必须确定出 T 的类型
      func<int>(); // 利用显示指定类型的方式,给 T 一个类型,才可以使用该模板
      }

      int main() {
      test01();
      test02();
      return 0;
      }
      #### 普通函数与函数模板的区别
  • 普通函数与函数模板区别:

    • 普通函数调用时可以发生自动类型转换(隐式类型转换)
    • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换(因为需要确保推导的 T 结果一致)
    • 如果利用显示指定类型的方式,则可以发生隐式类型转换
  • 总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型 T

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
// 普通函数
int myAdd01(int a, int b) {
return a + b;
}

// 函数模板
template<class T>
T myAdd02(T a, T b) {
return a + b;
}

// 使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换
void test01() {
int a = 10;
int b = 20;
char c = 'c';

cout << myAdd01(a, c) << endl; // 正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99
//myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换
myAdd02<int>(a, c); // 正确,如果用显示指定类型,可以发生隐式类型转换
}

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

普通函数与函数模板的调用规则

  • 调用规则如下:
    1. 如果函数模板和普通函数都可以实现,优先调用普通函数
    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
void myPrint(int a, int b) {
cout << "调用的普通函数" << endl;
}

template<typename T>
void myPrint(T a, T b) {
cout << "调用的模板" << endl;
}

template<typename T>
void myPrint(T a, T b, T c) {
cout << "调用重载的模板" << endl;
}

void test01() {
// 1. 如果函数模板和普通函数都可以实现,优先调用普通函数
// 注意 如果告诉编译器 普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
int a = 10;
int b = 20;
myPrint(a, b); // 调用普通函数

// 2. 可以通过空模板参数列表来强制调用函数模板
myPrint<>(a, b); // 调用函数模板

// 3. 函数模板也可以发生重载
int c = 30;
myPrint(a, b, c); // 调用重载的函数模板

// 4. 如果函数模板可以产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2); // 调用函数模板
}

int main() {
test01();
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
class Person {
public:
string m_Name;
int m_Age;

Person(string name, int age) {
this->m_Name = name;
this->m_Age = age;
}
};

// 普通函数模板
template<class T>
bool myCompare(T &a, T &b) {
if (a == b) {
return true;
} else {
return false;
}
}

// 以 template<> 开头,具体化特定数据类型的重载模板函数,将优先于常规模板
template<>
bool myCompare(Person &p1, Person &p2) {
if (p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age) {
return true;
} else {
return false;
}
}

void test01() {
int a = 10;
int b = 20;

bool ret = myCompare(a, b);
if (ret) {
cout << "a == b " << endl;
} else {
cout << "a != b " << endl;
}
}

void test02() {
Person p1("Tom", 10);
Person p2("Tom", 10);

// 可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
bool ret = myCompare(p1, p2);
if (ret) {
cout << "p1 == p2 " << endl;
} else {
cout << "p1 != p2 " << endl;
}
}

int main() {
test01();
test02();
return 0;
}

类模板

类模板语法

  • 类模板作用:

  • 建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。

  • 语法:

    • template<typename T>
      类
      
      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

      - 解释:

      - template:声明创建模板
      - typename:表面其后面的符号是一种数据类型,可以用class代替
      - T:通用的数据类型,名称可以替换,通常为大写字母

      ```C++
      //类模板
      template<class NameType, class AgeType>
      class Person {
      public:
      NameType mName;
      AgeType mAge;

      Person(NameType name, AgeType age) {
      this->mName = name;
      this->mAge = age;
      }

      void showPerson() {
      cout << "name: " << this->mName << " age: " << this->mAge << endl;
      }
      };

      void test01() {
      // 指定 NameType 为 string 类型,AgeType 为 int 类型
      Person<string, int> P1("孙悟空", 999);
      P1.showPerson();
      }

      int main() {
      test01();
      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
//类模板
template<class NameType, class AgeType = int>
class Person {
public:
NameType mName;
AgeType mAge;

Person(NameType name, AgeType age) {
this->mName = name;
this->mAge = age;
}

void showPerson() {
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
};

// 1. 类模板没有自动类型推导的使用方式
void test01() {
// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导
Person<string, int> p("孙悟空", 1000); // 必须使用显示指定类型的方式,使用类模板
p.showPerson();
}

// 2. 类模板在模板参数列表中可以有默认参数
void test02() {
Person<string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
p.showPerson();
}

int main() {
test01();
test02();
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
class Person1 {
public:
void showPerson1() {
cout << "Person1 show" << endl;
}
};

class Person2 {
public:
void showPerson2() {
cout << "Person2 show" << endl;
}
};

template<class T>
class MyClass {
public:
T obj;

void fun1() { obj.showPerson1(); }

void fun2() { obj.showPerson2(); }
};

void test01() {
MyClass<Person1> m{};
m.fun1();
//m.fun2(); // 编译会出错,说明只有函数被调用才会去创建成员函数
}

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

类模板对象做函数参数

  • 学习目标:

  • 类模板实例化出的对象,向函数传参的方式

  • 一共有三种传入方式:

    1. 指定传入的类型:直接显示对象的数据类型
    2. 参数模板化:将对象中的参数变为模板进行传递
    3. 整个类模板化:将这个对象类型 模板化进行传递
  • 总结:

    • 通过类模板创建的对象,可以有三种方式向函数中进行传参
    • 使用比较广泛是第一种:指定传入的类型
  • 在 C++ 中查看一个模板类型 T 或者变量 var 的数据类型:typeid(T/var).name()

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
// 类模板
template<class NameType, class AgeType = int>
class Person {
public:
NameType mName;
AgeType mAge;

Person(NameType name, AgeType age) {
this->mName = name;
this->mAge = age;
}

void showPerson() {
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
};

// 1、指定传入的类型
void printPerson1(Person<string, int> &p) {
p.showPerson();
}

void test01() {
Person<string, int> p("孙悟空", 100);
printPerson1(p);
}

// 2、类模板参数的模板化
template<class T1, class T2>
void printPerson2(Person<T1, T2> &p) {
p.showPerson();
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}

void test02() {
Person<string, int> p("猪八戒", 90);
printPerson2(p);
}

// 3、整个类的模板化
template<class T>
void printPerson3(T &p) {
cout << "T的类型为: " << typeid(T).name() << endl;
p.showPerson();
}

void test03() {
Person<string, int> p("唐僧", 30);
printPerson3(p);
}

int main() {
test01();
test02();
test03();
return 0;
}

类模板与继承

  • 当类模板碰到继承时,需要注意一下几点:
    • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
    • 如果不指定,编译器无法给子类分配内存
    • 如果想灵活指定出父类中T的类型,子类也需变为类模板
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
template<class T>
class Base {
T m;
};

//class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承

// 1. 必须指定父类的模板类型
class Son : public Base<int> {
};

// 2. 用类模板继承类模板,用T2指定父类中的T类型
template<class T1, class T2>
class Son2 : public Base<T2> {
public:
Son2() {
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
};

int main() {
Son c;
Son2<int, char> child1;

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
template<class T1, class T2>
class Person {
public:
T1 m_Name;
T2 m_Age;

//成员函数类内声明
Person(T1 name, T2 age);

void showPerson();
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

void test01() {
Person<string, int> p("Tom", 20);
p.showPerson();
}

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

类模板分文件编写

  • 针对问题:类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

  • 解决方向:

    1. 直接包含 .cpp 源文件:#include "person.cpp"

    2. 将声明和实现写到同一个文件中,并更改后缀名为 .hpp:#include "person.hpp"(常用方法)

      • #ifndef CLEARNING_PERSON_HPP
        #define CLEARNING_PERSON_HPP
        
        
   #include <iostream>
   #include <string>

   using namespace std;

   template<class T1, class T2>
   class Person {
   public:
       T1 m_Name;
       T2 m_Age;

       Person(T1 name, T2 age);

       void showPerson();
   };

   //构造函数 类外实现
   template<class T1, class T2>
   Person<T1, T2>::Person(T1 name, T2 age) {
       this->m_Name = name;
       this->m_Age = age;
   }

   //成员函数 类外实现
   template<class T1, class T2>
   void Person<T1, T2>::showPerson() {
       cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
   }


   #endif //CLEARNING_PERSON_HPP
   
1
2
3
4
5
6
7
8
9
10
11
12
13

- ```c++
#include "Person.hpp"

void test01() {
Person<string, int> p("Tom", 10);
p.showPerson();
}

int main() {
test01();
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
// 2. 全局函数配合友元  类外实现 - 先做函数模板声明,下方在做函数模板定义,在做友元
template<class T1, class T2>
class Person;

// 如果声明了函数模板,可以将实现写到后面,否则需要将实现体写到类的前面让编译器提前看到
template<class T1, class T2>
void printPerson2(Person<T1, T2> &p) {
cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}

template<class T1, class T2>
class Person {
// 1. 全局函数配合友元 类内实现
friend void printPerson(Person<T1, T2> &p) {
cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}

// 全局函数配合友元 类外实现
// 添加空模板参数 <> 表示为类模板的函数
friend void printPerson2<>(Person<T1, T2> &p);

private:
T1 m_Name;
T2 m_Age;

public:
Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
};

// 1. 全局函数在类内实现
void test01() {
Person<string, int> p("Tom", 20);
printPerson(p);
}

// 2. 全局函数在类外实现
void test02() {
Person<string, int> p("Jerry", 30);
printPerson2(p);
}

int main() {
test01();
test02();
return 0;
}