一. C++基础知识

1.1 基础知识

集成开发环境

编辑器、编译器、链接器、调试器

程序的运行从main函数开始而开始,结束而结束

编译器是从上到下逐行编译

在语法描述中,[]表示可选的。

C++语言集结构化编程、面向对象编程、泛型编程和函数式编程于一身,特别适合大型应用程序开发。

C++的头文件是不带.h扩展名的

C++的所有关键字都是小写的

C++11,空指针:nullptr,// C语言的空指针是NULL(0)

1.2 new/delete 内存管理

C++中利用new操作符在堆中开辟数据

堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete

语法: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
//
// Created by YiJiale on 2021/12/21.
//

#include "iostream"
using namespace std;

//1.new的剧本语法
int *func(){
//在堆区创建整型数据
//new返回的是该数据类型的指针
int *p = new int(10);
return p;
}

int main(){
int *p=func();
cout << *p << endl;
cout << *p << endl;
cout << *p << endl;
//堆区的数据 由程序员管理开辟,程序员管理释放
//如果想释放堆区的数据,利用关键字 delete
delete p;
cout << *p << endl; //被释放后,再次访问就是非法操作,会报错或者是打印随机地址
}

示例2:开辟数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(){
//创建10整型数据的数组,在堆区
int * arr = new int[10]; //10代表有10个元素
for(int i=0;i<10;i++){
arr[i]=i+100; //给10个元素赋值,100~109
}

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

//释放堆区数组
//释放数组的时候 要加[]才可以
delete[] arr;
return 0;

}

1.3 函数的默认参数

在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
//
// Created by YiJiale on 2021/12/21.
//

#include "iostream"
using namespace std;

int func(int a,int b,int c){
return a+b+c;
}
/*
1.如果某个函数参数是有默认值的,那么从这个位置开始往后,从左向右,都必须有默认值
2.如果函数声明有默认值,那么函数实现的时候就不能有默认值.如果声明时没有默认值,那么实现的时候可以有默认值
*/
int func2(int a=10,int b=10,int c=10);
int func2(int a,int b,int c){
return a+b+c;
}


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

1.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
#include "iostream"
using namespace std;

//函数重载——可以让函数名相同,提高服用性

//函数重载的满足条件
//1、同一个作用域
//2.函数名称相同
//3、函数参数类型不同,或者个数不同,或者顺序不同

int func(int a,int b){
cout << "你好" <<endl;
return 0;
}

int func(char c,char d){
cout << "hello" <<endl;
return 0;
}

//注意事项
//函数的返回值不能作为重载的条件


int main(){
func('c','d');
return 0;
}

1.5 内联函数

1.6 引用

1.6.1基本语法

作用:给变量起别名

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// Created by YiJiale on 2021/12/21.
//

#include "iostream"
using namespace std;

int main(){
int a = 10;
//给a取个小名为b
int &b = a;


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

//给b重新赋值,a的值也会跟着发生变化,此时的a跟b是指向的同一个地址
b= 100;
cout << "b=" << b << endl;
cout << "a=" << a << endl;

}

1.6.2引用的注意事项

引用必须初始化

引用在初始化后,不可以改变

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//
// Created by YiJiale on 2021/12/21.
//
#include "iostream"
using namespace std;

int main(){
int a = 10;
int &b = a;
//1.引用必须初始化 直接int &b;是错误的
int c=20;
//2.引用一旦初始化后,就不可以更改了
b=c; //赋值操作,只是把20赋值给b,而不是更改引用,
//这样写就是错的,因为初始化后不能更改
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}

1.6.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
//
// Created by YiJiale on 2021/12/21.
//
#include "iostream"
using namespace std;


//1.值传递
void mySwap01(int a,int b){
int temp=a;
a=b;
b=temp;
/*cout << "Swap01 a=" << a <<endl;
cout << "Swap01 b=" << b <<endl;*/
}

//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;
//mySwap01(a,b); //值传递,形参不会修饰实参
//mySwap02(&a,&b); //地址传递,形参会修饰实参的
mySwap03(a,b);//引用传递,形参会修饰实参的,这里的实参就是上面形参的别名
cout << "a=" << a <<endl;
cout << "b=" << b <<endl;
}

1.6.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
//
// Created by YiJiale on 2021/12/21.
//

#include "iostream"
using namespace std;

//引用做函数的返回值
//1.不要返回局部变量的引用
/*int& test01(){
int a = 10;//局部变量存放在四区中的 栈区
return a;
}*/
//2.函数的调用可以作为左值
int& test02(){
static int a = 10;//静态变量,存放在全局区,全局区上的数据在程序结束后系统释放
return a;
}


int main(){
//int &ref =test01();
/*cout << "ref=" << ref << endl;//第一次 结果正确,是因为编译器做了保留
cout << "ref=" << ref << endl;//第二次 结果错误,是因为a的内存已经释放*/

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

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

cout << "ref2=" << ref2 << endl;
cout << "ref2=" << ref2 << endl;


}

1.6.5 引用的本质

本质:引用的本质在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
//
// Created by YiJiale on 2021/12/21.
//

#include "iostream"
using namespace std;

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;

}

结论:c++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

二. 类与对象

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

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

例如 :

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

车也可以作为属性,属性有轮胎、方向盘、车灯… 行为有载人、放音乐、开空调

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

2.1封装

2.1.1封装的意义

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

封装的意义:

1.将属性和行为作为一个整体,表现生活中的事务

2.将属性和行为加以权限控制

2.1.1.1封装的意义一

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//
// Created by YiJiale on 2021/12/22.
//
#include "iostream"
using namespace std;
//
const double PI = 3.14;

//class 代表设计一个类,类后面紧跟着的就是类名称
class Yuan{
//访问权限
//公共权限
public:

//属性
//半径
int m_r;

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

//设计一个圆类,求圆的周长
//圆求周长的公式:2 * PI * 半径



int main(){
//通过圆类 创建具体的圆(对象)
//实例化 (通过一个类创建一个对象的过程)
Yuan c1;
//给圆对象 的属性进行赋值
c1.m_r=10;

cout << "圆的周长为:" << c1.calculateZC() << 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
//
// Created by YiJiale on 2021/12/22.
//

#include "iostream"
#include "string"
using namespace std;

class Student{
//公共权限
public:

//类中的属性和行为 统一称为 成员
//属性 成员属性 成员变量
//行为 成员函数 成员方法

//属性
string Name;
long long int ID;


//行为 显示姓名和学号
void showID(){
cout << "姓名为:" << Name << "学号为:" << ID << endl;
}
//给姓名赋值
void setName(string name){
Name=name;
}
//给学号赋值
void setID(long long int id){
ID=id;
}
};




int main(){
//创建一个具体的学生 实例化对象
Student s1;
//s1.Name = "Le1a";
s1.setName("Le1aaaaaaaaaa");
s1.ID = 20310420203;
s1.showID();

Student s2;
s2.Name ="pl1rry";
s2.setID(20310420107);
//s2.ID= 20310420107;
s2.showID();
}

2.1.1.2封装的意义二

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

访问权限有三种:

1.public 公共权限

2.protected 保护权限

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
//
// Created by YiJiale on 2021/12/22.
//
#include "iostream"
using namespace std;
#include "string"

//访问权限
//三种:
//公共权限 public 成员 类内可以访问 类外也可以访问
//保护权限 protected 成员 类内可以访问 类外不可以访问 继承的时候:父类的保护权限 子类也能访问
//私有权限 private 成员 类内可以访问 类外不可以访问 儿子不可以 访问父类的私有内容

class Person{
public:
//公共权限
string Name; //姓名

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

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

public:
void func(){
//类内 三种权限 都能访问
Name = "Le1a";
Car = "法拉利";
Password = 918223;
cout << "姓名为" << Name << "开着是:" << Car << "银行卡密码是:" << Password << endl;
}

};

int main(){
//实例化一个对象
Person p1;

//p1.Name = "pl1rry"; //公共权限,类外可以访问
//p1.Car = "拖拉机"; //保护权限内容,在类外访问不到
//p1.Password = "123456"; //私有权限内容,在类外访问不到
p1.func();

}

2.2 成员

类中的属性和行为 统一称为 成员

2.3 访问权限

2.3.1 private

私有权限,类内可以访问,类外不能访问。父类的私有权限,子类也不能访问

2.3.2 protected

保护权限,类内可以访问,类外不能访问。继承时,父类的保护权限,子类可以访问

2.3.3 public

公共权限,类内和类外都能访问

2.3.4 C++中的struct

在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
24
25
26
//
// Created by YiJiale on 2021/12/22.
//

#include "iostream"
using namespace std;

class C1{
int m_A; //默认权限 是私有权限
};

struct C2{
int m_A; //默认权限 是公共权限
};

int main(){
//struct 和 class 区别
//struct 默认权限为 pubic
//class 默认权限为 private
C1 c1;
//c1.m_A=100; //报错,因为是私有权限,类外不可访问

C2 c2;
c2.m_A=100; //不报错,因为是公共权限,类外可以访问
cout << "m_A=" << c2.m_A << endl;
}

2.3.5 成员属性设为私有

优点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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//
// Created by YiJiale on 2021/12/22.
//
#include "iostream"
using namespace std;
//成员属性设为私有
//1.自己可以控制独写权限
//2.对于写 可以检测数据的有效性

//设计 人类
class Person{
private:
//姓名
string Name; //可读可写
//年龄
int Age; //只读
//情人
string Lover; //只写

public:
//设置姓名
void setName(string name){
Name=name;
}
//获取姓名
string getName(){
return Name;
}

//获取年龄
int getAge(){
Age = 20;
return Age;
}

//设置情侣 只写
void setLover(string lover){
Lover=lover;
}


};

int main(){
Person p;
//p.Name = "张三"; 这样直接访问是访问不到私有权限的
p.setName("张三");
cout << "姓名为:" << p.getName() << endl;
cout << "年龄为:" << p.getAge() << endl;
p.setLover("小葵"); //可以设置情侣是谁,但是不能读取
//cout << "情侣是:" << p.setLover("小葵") << endl;
}

优点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
62
63
64
65
//
// Created by YiJiale on 2021/12/22.
//
#include "iostream"
using namespace std;
//成员属性设为私有
//1.自己可以控制独写权限
//2.对于写 可以检测数据的有效性

//设计 人类
class Person{
private:
//姓名
string Name; //可读可写
//年龄
int Age; //只读
//情人
string Lover; //只写

public:
//设置姓名
void setName(string name){
Name=name;
}
//获取姓名
string getName(){
return Name;
}

//获取年龄 可读可写 如果想修改(年龄的范围必须是 0~150 之间)
int getAge(){
//Age = 20;
return Age;
}

//设置年龄
void setAge(int age){
if(age < 0 || age >150) // 判断数据的有效性
{
Age=0;
cout << "输入年龄有误!" << endl;
return;
}
Age=age;
}

//设置情侣 只写
void setLover(string lover){
Lover=lover;
}


};

int main(){
Person p;
//p.Name = "张三"; 这样直接访问是访问不到私有权限的
p.setName("张三");
cout << "姓名为:" << p.getName() << endl;
p.setAge(100);
cout << "年龄为:" << p.getAge() << endl;
//设置情人为小葵女士
p.setLover("小葵"); //可以设置情侣是谁,但是不能读取
//cout << "情侣是:" << p.setLover("小葵") << endl;
}

2.3.6练习案例1:设计立方体类

设计立方体类(Cube)

求出立方体的面积和体积

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

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
//
// Created by YiJiale on 2021/12/22.
//

#include "iostream"
using namespace std;

//立方体类设计
//1.创建立方体类
//2.设计属性和行为
//3.设计行为 获取 立方体的面积和题基
//4.分别利用全局函数和成员函数 判断两个立方体是否相等

class Cube{
//属性
private:
int L; //长
int W; //宽
int H; //高


//行为
//设置获取长宽高
public:
//设置长
void setL(int l){
L=l;
}
//获取长
int getL(){
return L;
}

//设置宽
void setW(int w){
W=w;
}

//获取宽
int getW(){
return W;
}

//设置高
void setH(int h){
H=h;
}

//获取高
int getH(){
return H;
}



//获取立方体面积
int getS(){
return 2*L*W +2*W*H + 2*L*H ;
}
//获取立方体体积
int getV(){
return L*W*H;
}

//利用成员 函数来判断两个立方体是否相等
bool isSameByClass(Cube &c){
if(L==c.getL() && W==c.getW() && H==c.getH()){
return true;
}
else{
return false;
}
}
};

//利用全局函数判断两个立方体是否相等
bool isSame(Cube &c1,Cube &c2){
if(c1.getL()==c2.getL() && c1.getW()==c2.getW() && c1.getH()==c2.getH()){
return true;
}
else{
return false;
}
}

int main(){
Cube c1; //创建第一个立方体
c1.setL(10); //设置长
c1.setW(10); //设置宽
c1.setH(10); //设置高
cout << "立方体的面积为:" << c1.getS() << endl;
cout << "立方体的体积为:" << c1.getV() << endl;

Cube c2; //创建第二个立方体
c2.setL(10); //设置长
c2.setW(10); //设置宽
c2.setH(10); //设置高

bool ret =isSame(c1,c2);
bool ret2=c1.isSameByClass(c2);
if(ret){
cout << "c1和c2是相等的!" << endl;
}
else{
cout << "c1和c2不相等!" << endl;
}

if(ret2){
cout << "用成员函数判断,c1和c2是相等的!" << endl;
}
else{
cout << "用成员函数判断,c1和c2不相等!" << endl;
}

}

2.4 构造函数与析构函数

生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会自动删除一些自己信息数据保证安全

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

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

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

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

C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。

2.4.1 作用

构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

析构函数: 主要作用在于对象销毁前系统自动调用,执行一些清理工作。

2.4.2构造函数和析构函数的语法

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

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
//
// Created by YiJiale on 2021/12/22.
//
#include "iostream"
using namespace std;
//对象的初始化和清理
//1.构造函数 进行初始化操作
class Person{
public:
//1.1构造函数
//没有返回值 不用写void
//函数名 与类名相同
//构造函数可以有参数, 可以发生重载
//创造对象的时候,构造函数会自动调用,而且只调用一次
Person(){
cout << "Person 构造函数的调用" <<endl;
}

//2.析构函数进行清理操作
//没有函数值 不写 void
//函数名跟类名相同 在名称前 加 ~
//析构函数不可以有参数,不可以发生重载
//对象在销毁前 会自动调用析构函数,而且只会调用一次
~Person(){
cout << "Person 析构函数的调用" <<endl;
}



};

//构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供 一个空实现的构造函数和析构函数
void test01(){
Person p; //创建对象的时候,自动调用构造函数
//在栈上的数据,test01在执行完毕后,释放这个对象

}


int main(){
test01();
//Person p;
system("pause");
return 0;
}

2.4.3 它们的四个特点

构造函数 析构函数
1 与类同名 与类同名,前面加~
2 不能有类型, void、return都不要
3 可以带参数,能够重载 没有参数,不能重载
4 一般为公有函数 基类一般采用虚析构函数

2.4.4 构造函数的分类及调用

两种分类方式:

​ 按参数类型分为:有参构造函数和无参构造函数

​ 按类型分为:普通构造和拷贝构造

三种调用方式:

​ 括号法

​ 显示法

​ 隐式转换法

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//
// Created by YiJiale on 2021/12/22.
//
#include "iostream"
using namespace std;

//1.构造函数的分类及调用
//分类
// 按照参数分类 无参构造(默认构造) 和 有参构造
// 按照类型分类 普通构造函数 和 拷贝构造函数
class Person{
public:
//构造函数
Person(){
cout << "Person的无参构造函数调用" << endl;
}
Person(int a){
age=a;
cout << "Person的有参构造函数调用" << endl;
}
//拷贝构造 函数
Person(const Person &p){
//将传入的人身上所有属性,拷贝到我身上
age=p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
//析构函数
~Person(){
cout << "Person的析构函数调用" << endl;
}
int age;
};

//2.构造函数的调用
//调用无参构造函数
void test01() {
Person p;
}
//调用有参构造函数
void test02(){
//1.括号法
Person p1(10); //有参构造函数
Person p2(p2); //拷贝构造函数
//cout << "p2年龄为:" << p2.age << endl;
//cout << "p3年龄为:" << p3.age << endl;

//注意事项1. 默认函数构造时候,不要加() 因为下面这行代码,编译器会认为是一个函数的声明
//Person p1();

//2.显示法
Person p3 = Person(10); //有参构造
Person p4 = Person(p3); //拷贝构造

//注意事项2. 不要利用拷贝构造函数 初始化匿名对象
//Person(p3); //编译器会认为在用无参构造函数调用 Person(p3) === Person p3

//Person(10); //匿名对象 特点:当前行 执行结束后,系统会立即回收掉匿名对象
//cout << "aaaaa" << endl;

//3.隐式转换法
Person p5=10; //相当于写了 Person p4 = Person(10); 有参构造
Person p6=p5; //相当于写了 Person p5 = Person(p4); 拷贝构造
}

int main(){
test01();
test02();
system("pause");
return 0;
}

1640243545907.png

匿名对象,当前行执行后,立刻就被回收了,所以立刻调用了析构函数,随后才 打印aaaaa

2.4.5 拷贝构造函数的调用时机

C++中拷贝构造函数调用的时机通常有三种情况

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
//
// Created by YiJiale on 2021/12/22.
//

#include "iostream"
using namespace std;

//拷贝构造函数调用时机

class Person{
public:
//默认构造函数
Person(){
cout << "Person的默认构造函数调用" << endl;
}
//有参构造函数
Person(int age){
Age=age;
cout << "Person的有参构造函数调用" << endl;
}
//拷贝构造函数
Person(const Person &p){
cout << "Person的拷贝构造函数调用" << endl;
Age=p.Age;
}

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

//1.使用一个已经创建完毕得对象来初始化一个新对象
void test01(){
Person p1(10);
Person p2(p1);
cout << "p2的年龄为:" << p2.Age << endl;
}
//2.值传递得方式给函数参数传值
void doWork(Person p){

}
void test02(){
Person p;
doWork(p);
}
//3.值方式返回局部对象
Person doWork2(){
Person p1; //这里先调用默认构造函数
return p1; //这里返回的是值,会常见一个新的来拷贝p1,然后返回到test03(),所以会调用拷贝构造函数
}
void test03(){
Person p = doWork2();
}
int main(){
//test01();
//test02();
test03();
system("pause");
return 0;
}

1640243608980.png

2.4.6初始化列表

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
//
// Created by YiJiale on 2021/12/22.
//

#include "iostream"
using namespace std;

//初始化列表
class Person{
public:
//传统初始化操作
/*Person(int a,int b,int c){
A=a;
B=b;
C=c;
}*/

//初始化列表初始化属性
Person(int a,int b,int c):A(a),B(b),C(c){}
void PrintPerson(){
cout << "A=" << A <<endl;
cout << "B=" << B <<endl;
cout << "C=" << C <<endl;
};

private:
int A;
int B;
int C;
};

void test01(){
//Person p(10,20,30);
Person p(30,20,10);
p.PrintPerson();
}

int main(){
test01();
system("pause");
return 0;
}

1640243630947.png

这里的初始化列表来初始化属性的方法,冒号在构造函数的形参列表括号后面,然后属性(),属性(),属性()…最后{}

1640243651422.png

2.4.7默认函数

2.5类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员

例如:

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

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

那么当创建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
//
// Created by YiJiale on 2021/12/22.
//

#include "iostream"
using namespace std;
#include "string"
//类对象作为类成员

//手机类
class Phone{
public:
Phone(string Pname){
cout << "Phone的构造函数调用" << endl;
PName=Pname;
}
~Phone(){
cout << "Phone的析构函数调用" << endl;
}
//定义手机品牌
string PName;


};


class Person{
public:
//Phone Phone = pName 隐式转换法 分别给这个类的Name属性初始化,和Phone类的PName初始化
Person(string name,string pname):Name(name), Phone(pname){
cout << "Person的构造函数调用" << endl;
}
~Person(){
cout << "Person的析构函数调用" << endl;
}
//姓名
string Name; //实例化一个 Name属性
//手机
Phone Phone; //把上一个类当作这里的成员
};
//当其他的类的对象作为本类的成员,构造的时候先构造类对象,再构造自身,析构的顺序?析构的顺序与构造相反!
void test01(){
Person p("张三","苹果MAX");
cout << p.Name << "拿着:" << p.Phone.PName << endl;
}


int main(){
test01();
system("pause");
return 0;
}

1640243673336.png

1640243700894.png

2.6 静态成员函数

静态成员就是在成员变量和成员函数前加上关键字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
//
// Created by YiJiale on 2021/12/23.
//

#include "iostream"
using namespace std;
//静态成员函数的特点
//1.所有对象共享同一个函数
//2.静态成员函数只能访问静态成员变量

class Person{
public:
//静态成员函数
static void func(){
//A=100; //静态成员函数可以访问 静态成员变量 大家共享一份儿
//B=200; //静态成员函数 不可以访问 非静态成员变量,无法区分到底是哪个对象的B
cout << "static void func调用" << endl;
cout << "静态成员变量A的值为:" << A << endl;
}

static int A; //静态成员变量
int B; //非静态成员变量

//静态成员函数也是有访问权限的
private:
static void func2(){
cout << "static void func2的调用" << endl;
}
};
int Person::A=0;
//有两种访问方式
void test01(){
//1.通过对象访问
Person p;
p.func();
//2.通过类名访问
Person::func();

//Person::func2(); 类外访问不到私有的静态成员函数
}

int main(){
test01();
}

2.7 C++对象模型和this指针

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//
// Created by YiJiale on 2021/12/23.
//

#include "iostream"
using namespace std;

//成员变量和成员函数 是分开存储的

class Person{
int A; //非静态成员变量,属于类的对象上的

static int B; //静态成员变量,不属于类的对象上

void func(){} //非静态成员函数,不属于类的对象上

static void func2(){}//静态成员函数,不属于类的对象上
};

int Person::B=0;

void test01(){
Person p;
//空对象 占用内存空间为: 1
//C++编译器会给每个空对象也分配一个字节空间,是为了区分 空对象 占用内存的位置。 因为两个空对象不能占用同一块儿内存
//每个空对象也应该有一个独一无二的内存地址
cout << "size of p =" << sizeof(p) << endl;
}

void test02(){
Person p;//非空对象 占用内存为实际对象 占用的内存
cout << "size of p =" << sizeof(p) << endl;
}

int main(){
//test01();
test02();
system("pause");
return 0;
}

2.7.2this指针概念

通过2.7.1我们知道在C++中成员变量和成员函数是分开存储的

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

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

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

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

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

this指针的用途:

​ 1.当形参和成员变量同名时,可以用this指针来区分

​ 2.在类的非静态成员函数中返回对象本身 ,可使用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
//
// Created by YiJiale on 2021/12/23.
//
#include "iostream"
using namespace std;

class Person{
public:
Person(int age){
this->age=age; //这个this指针指向的是 被调用的成员函数 所属的对象
}
int age;

Person& PersonAddAge(Person &p){ //这里 一定是返回的 对象的引用& 而不是值,因为如果是值的话,每一次返回的都是经过拷贝之后的一个新的p2,而不是当前p2
this->age += p.age;//把传入的这个对象的age加到自身身上
return *this;//this 指向p2的指针,而*this指向的p2这个对象本体
}

};


//1.解决名称冲突 方案1.改变量名,使得不重复 方案2。 this->age=age;
void test01(){
Person p1(18); //p1调用了Person的有参构造函数,所属对象就是p1,谁调用这个构造函数,this指针就指向哪个对象
cout << "p1的年龄为:" << p1.age << endl;// 如果是 age=age;此时打印出来并不是18,因为名称冲突,导致被认为三个age是同一个
}

//2.返回对象本身 return *this
void test02(){
Person p1(10);
Person p2(10);

//p2.PersonAddAge(p1); //这里把p1传进去,p1的age加给了p2

//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);//因为返回的是 p2的对象本体 所有这里可以一直调用这个函数进行一个累加操作
cout << "p2的年龄为:" << p2.age << endl;
}


int main(){
//test01();
test02();
system("pause");
return 0;
}

2.7.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
//
// Created by YiJiale on 2021/12/23.
//

#include "iostream"
using namespace std;

//空指针可以调用成员函数

class Person{
public:
void showClassName(){
cout << "This is Person Class" << endl;
}

void showPersonAge(){
if (this == NULL){
return;
}
cout << "age = " << m_Age << endl; //默认调用了this指针 指向当前对象的m_Age属性 this->m_Ag
//但是下面创建的是空指针,没有确定的对象,所以这里的this指针无法指向 空对象 里面的属性
}


int m_Age;


};

void test01(){
Person * p =NULL;

p->showClassName();

p->showPersonAge();
}




int main(){
test01();
system("pause");
return 0;
}

2.7.4const修饰成员函数

2.8 友元friend

2.8.1 友元函数

生活中你的家有客厅(Public),有你的卧室(Private)

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

但是呢,你也可以允许你的好闺蜜好基友进去

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

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

友元的关键字为 friend

友元的三种实现

​ 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
//
// Created by YiJiale on 2021/12/23.
//
#include "iostream"
using namespace std;
#include "string"
//全局函数做友元 用法:把全局函数整体粘贴到类的开始处,并且在前面加上friend关键字
//建筑物类
class Building{
friend void goodGay(Building &building);//goodGay全局函数是 Building好朋友,可以访问Building的私有成员
public:
Building(){ //无参构造函数给这两个成员变量赋初值
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}

public:
string m_SittingRoom; //客厅

private:
string m_BedRoom; //卧室
};

//全局函数
void goodGay(Building &building){
cout << "好基友的全局函数 正在访问: " << building.m_SittingRoom << endl;
cout << "好基友的全局函数 正在访问: " << building.m_BedRoom << endl;
}

void test01(){
Building building;
goodGay(building);
}


int main(){
test01();
system("pause");
return 0;
}

关键点:

1640243729399.png

这样写了之后,goodGay这个全局函数就可以访问类里面的私有成员了

2.8.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
//
// Created by YiJiale on 2021/12/23.
//

#include "iostream"
using namespace std;
#include "string"

class Building;

//类做友元
class GoodGay{
public:

GoodGay();
void visit();//参观函数 访问Building中的属性

Building * building;
};

class Building{
friend class GoodGay; //GoodGay这个类是本类的好朋友,可以访问本类的私有成员
public:
Building();

public:
string m_SittingRoom;//客厅

private:
string m_BedRoom;//卧室
};
//类外 写 成员函数(构造函数)
Building::Building(){
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}

GoodGay::GoodGay(){
//创建建筑物对象
building = new Building;//创建一个新的对象,上面的Building * building 指针指向这里 new的对象
}

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

void test01(){
GoodGay gg;//利用GoodGay类创建一个实例化对象
//1.先调用GoodGay的构造函数
//2.GoodGay的构造函数又创建了一个Building(new Building),就会再调用Building的构造函数就会给Building的成员变量赋初值
gg.visit();
//3.这里访问GoodGay类里的visit成员函数,便会打印 对象gg的 客厅
}

int main(){
test01();
}

2.8.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
//
// Created by YiJiale on 2021/12/23.
//
#include "iostream"
using namespace std;
#include "string"

class Building;

class GoodGay{
public:
GoodGay();

Building * building;
void visit(); //让visit函数可以访问Building中私有成员
void visit2(); //让visit2函数不可以访问Building中的私有成员
};

class Building{
//告诉编译器 GoodGay类下visit成员函数作为本类的好朋友,可以访问本类的私有成员
friend void GoodGay::visit();
public:
Building();
public:
string m_SittingRoom;//客厅
private:
string m_BedRoom; //卧室
};
//类外写 Building的构造函数
Building::Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}

//类外定义无参构造函数
GoodGay::GoodGay() {
building = new Building;//创建一个新的对象,上面的Building * building 指针指向这里 new的对象
}

void GoodGay::visit(){
cout << "visit函数正在访问: " << building->m_SittingRoom << endl;
cout << "visit函数正在访问: " << building->m_BedRoom << endl;
}
void GoodGay::visit2(){
cout << "visit2函数正在访问: " << building->m_SittingRoom << endl;
//cout << "visit2函数正在访问: " << building->m_BedRoom << endl;//非友元函数不能访问Building类的私有成员

}

void test01(){
GoodGay gg;
gg.visit();
gg.visit2();
}

int main(){
test01();
system("pause");
return 0;
}

三.运算符重载

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

3.1 两种重载形式

3.3.1加号运算符重载

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

对于内置数据类型,编译器知道如何进行运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
public:
int m_A;
int m_B;
};
Person p1;
p1.m_A=10;
p1.m_B=10;

Person p2;
p2.m_A=10;
p2.m_B=10;

Person p3=p1+p2; //这样是不行的,因为编译器不知道如何去做加法

通过自己写成员函数,实现两个对象相加属性后返回新的对象

1
2
3
4
5
6
7
Person PersonAddPerson(Person &p)//成员函数
{
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}

这个函数呢,编译器给起了一个通用的名称

1.通过成员函数重载+号

1
2
3
4
5
6
7
8
9
10
Person operator+(Person &p){
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}

Person p3 = p1.operator+(p2);
//简化为:
Person p3 = p1 + p2;

2.通过全局 函数重载+

1
2
3
4
5
6
7
8
9
Person operator+(Person &p1,Person &p2){
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
Person p3 = operator+(p1,p2);
//简化为:
Person p3 = p1 + p2;

完整示例:

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
//
// Created by YiJiale on 2021/12/23.
//

#include "iostream"
using namespace std;

//加号运算符重载
class Person{
public:
//1.成员函数重载+号
/*Person operator+(Person &p){
Person 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;
};
//通过全局函数重载+号
Person operator+(Person &p1,Person &p2){
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}

//函数重载版本
Person operator+(Person &p1,int num){
Person temp;
temp.m_A = p1.m_A + num;
temp.m_B = p1.m_B + num;
return temp;
}


void test01(){
Person p1;
p1.m_A=10;
p1.m_B=20;

Person p2;
p2.m_A=10;
p1.m_B=20;

//成员函数重载的本质调用
//Person p3 = p1.operator+(p2);
//全局函数重载的本质调用
//Person p3 = operator+(p1,p2);
Person p3 =p1 + p2;


//运算符重载 也可以发生函数重载
Person p4 = p3 + 10;

cout << "p3.m_A= " << p3.m_A << endl;
cout << "p3.m_B= " << p3.m_B << endl;

cout << "p4.m_A= " << p4.m_A << endl;
cout << "p4.m_B= " << p4.m_B << endl;
}


int main(){
test01();
}

总结1:对于内置的数据类型的表达式的 运算符 是不可能改变的

总结2:不要滥用运算符重载

四. 继承与派生

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

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

1640262023226.png

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

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

4.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
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
//
// Created by YiJiale on 2021/12/23.
//
#include "iostream"
using namespace std;


//普通实现页面
/*
//Java页面
class Java{
public:
void header(){
cout << "首页、公开课、登录、注册...(公共头部)" <<endl;
}

void footer(){
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" << endl;
}
void content(){
cout << "Java学科视频" << endl;
}
};

//Python页面
class Python{
public:
void header(){
cout << "首页、公开课、登录、注册...(公共头部)" <<endl;
}

void footer(){
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" << endl;
}
void content(){
cout << "Python学科视频" << endl;
}
};
//C++页面
class CPP{
public:
void header(){
cout << "首页、公开课、登录、注册...(公共头部)" <<endl;
}

void footer(){
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" << endl;
}
void content(){
cout << "C++学科视频" << endl;
}
};*/

//继承实现页面
//公共页面类
class BasePage{
public:
void header(){
cout << "首页、公开课、登录、注册...(公共头部)" <<endl;
}

void footer(){
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" << endl;
}
};
//继承的好处: 减少重复代码
//语法:class 子类:继承方式(public等) 父类
//子类 也称为派生类 父类也成为基类

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

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

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


void test01()
{
cout << "Java下载视频页面如下: " << endl;
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();

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

cout << "=====================" << endl;
cout << "CPP下载视频页面如下: " << endl;
CPP cpp;
cpp.header();
cpp.footer();
cpp.left();
cpp.content();
}


int main(){
test01();
system("pause");
return 0;
}

4.2继承方式

继承方式:默认的继承方式是私有继承,最常见的继承方式是公有继承

继承的语法: class 子类:继承方式 父类

继承方式一共有三种:

​ 1.公共继承

​ 2.保护继承

​ 3.私有继承

1640265669879.png

4.3继承中构造和析构的顺序

构造函数的调用顺序:父类->成员对象->子类

析构函数的调用顺序:子类->成员对象->父类

五. 虚函数与多态

5.1 虚函数

5.1.1 成员函数,区分三个概念:重载、隐藏、覆盖

5.1.2 同名函数的调用原则

5.2 多态的概念

多态时C++面向对象三大特性之一

多态分为两类:

​ 静态多态:函数重载运算符重载属于静态多态,复用函数名

​ 动态多态:父类和虚函数实现运行时多态

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

​ 静态多态的函数地址早绑定 - 编译阶段确定函数地址

​ 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

下面通过案例进行讲解多态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//
// Created by YiJiale on 2021/12/23.
//
#include "iostream"
using namespace std;

//多态

//动物类

class Animal{
public:
virtual void speak(){ //virtual 虚函数 此时就实现了 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
cout << "动物在说话" << endl;
}
};

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

//执行说话
//地址早绑定 在编译阶段就确定了函数的地址
//如果想让执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定

//动态多态满足条件
//1、有继承关系
//2、子类要重写父类的虚函数 重写的意思就是 完全相同的成员函数 即返回值类型 函数名 参数列表 完全相同 子类的virtual可写可不写

//动态多态的使用
//父类的指针或者引用 执行子类对象
void doSpeak(Animal &animal){ //Animal & animal =cat; 父类的引用指向子类的对象
animal.speak(); //这里无论传入什么,这里是调用的 animal的speak函数
}

void test01(){
Cat cat;
doSpeak(cat);
}


int main(){
test01();
}

1640267299547.png

1640267259604.png

总结:

多态满足条件

​ 有继承关系

​ 子类重写父类中的虚函数

多态使用条件

​ 指针或引用指向子类对象

重写:函数返回类型 函数名 参数列表 完全一致称为重写

5.3 纯虚函数与抽象类

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

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

纯虚函数的语法: 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//
// Created by YiJiale on 2021/12/24.
//
#include "iostream"
using namespace std;

//纯虚函数和抽象类
class Base{
public:
//纯虚函数
//只要有一个纯虚函数,这个类称为抽象类
//抽象类特点:
//1.无法实例化对象
//2.抽象类的子类 必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0; //虚函数=0; 就是纯虚函数

};

class Son:public Base{
public:
virtual void func(){
cout << "Son已经完成重写父类的纯虚函数" << endl;
}
};

void test01(){
//Base b; //抽象类是无法实例化对象
//new Base //抽象类是无法实例化对象

//Son s; //抽象类的子类 必须重写父类中的纯虚函数,否则也属于抽象类

Base * base = new Son; //父类的指针 指向 子类的对象。
base->func(); //指向不同的子类的对象时,指向谁 就调用的谁的成员成员函数
}

int main(){
test01();
}

5.4虚析构和纯虚析构

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

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

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

​ 可以解决父类指针释放子类对象

​ 都需要有具体的函数实现

虚析构和纯虚析构区别:

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

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

5.5 RTTI,运行时类型识别

运行时类型识别(Run-time type identification , RTTI),是指在只有一个指向父类的指针或引用时,确定所指对象的准确类型的操作。其常被说成是C++的四大扩展之一(其他三个为异常、模板和名字空间)。

使用RTTI的两种方法:

  1、typeid()

  第一种就像sizeof(),它看上像一个函数,但实际上它是由编译器实现的。typeid()带有一个参数,它可以是一个对象引用或指针,返回全局typeinfo类的常量对象的一个引用。可以用运算符“= =”和“!=”来互相比较这些对象,也可以用**name()**来获得类型的名称。如果想知道一个指针所指对象的精确类型,我们必须逆向引用这个指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{

virtual fun() = 0;

};

class B: public A{

void fb(){};

};

class C : public A{

void fc(){};

};

A* pa = new B; //父类指针指向子类对象

B* pb = dynamic_cast<B*>(pa);

pb->fb();

六.异常处理

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 trycatch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。

6.1抛出异常

您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。

以下是尝试除以零时抛出异常的实例:

1
2
3
4
5
6
7
8
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}

6.2捕获异常

catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。

1
2
3
4
5
6
7
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 异常的代码
}

上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 …,如下所示:

1
2
3
4
5
6
7
try
{
// 保护代码
}catch(...)
{
// 能处理任何异常的代码
}

下面是一个实例,抛出一个除以零的异常,并在 catch 块中捕获该异常。

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

double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}

int main ()
{
int x = 50;
int y = 0;
double z = 0;

try {
z = division(x, y);
cout << z << endl;
}catch (const char* msg) {
cerr << msg << endl;
}

return 0;
}

由于我们抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*。当上面的代码被编译和执行时,它会产生下列结果:

1
Division by zero condition!

七. IO流

7.1 四个预定义流对象

包括cin、cout、cerr和clog。

7.2 文件读写

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

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

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

文件类型分为两种:

​ 1.文本文件 - 文件以文本的ASCII码形式存储在计算机中

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

操作文件的三大类:

​ 1.ofstream: 只写操作

​ 2.ifstream: 只读操作

​ 3.fstream: 读写操作

7.2.1写文件

写文件步骤如下:

1.包含头文件

1
#include "fstream"

2.创建流对象

1
ofstream ofs;   //通过这个对象可以写文件

3.打开文件

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

4.写数据

1
ofs << "写入的数据" << endl;

5.关闭文件

1
ofs.close();

文件打开方式:

ios::in 为读文件而打开文件

ios::out 为写文件而打开文件

ios::ate 初始位置:文件尾部

ios::app 追加方式写文件

ios::trunc 如果文件存在,就先删除再创建

ios::binary 二进制方式

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

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

7.2.2读文件

1.包含头文件

1
#include "fstream"

2.创建流对象

1
ifstream ifs;

3.打开文件并判断文件是否打开成功

1
2
3
4
5
6
ifs.open("文件路径",打开方式)

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

4.读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第一种:
char buf[1024] = {0};
while(ifs >> buf){
cout << buf << endl;
}
第二种:
char buf[1024] = {0};
while(ifs.getline(buf,sizeof(buf))){
cout << buf << endl;
}
第三种:
string buf;

while(gerline(ifs,buf)){
cout << buf << endl;
}
第四种:
char c;
while((c = ifs.get())!=EOF){
cout << buf << endl;
}

5.关闭文件

1
ifs.close();

八.模版编程

  • 模板的特点: 模板不可以直接使用,他只是一个框架

  • 模板的通用并不是万能的

  • C++另一种编程思想称为 泛型编程,主要利用的技术 就是 模板

  • C++提供两种模板机制: 函数模板类模板

8.1 函数模板语法

函数模板作用:

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

语法:

1
2
3
4
5
6
template<typename T>
函数声明或定义

调用方法:
1.编译器自动推导
2.显示指定类型 例如:mySwap<int>() 在<>里直接声明变量类型

解释:

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
58
59
60
61
62
63
//
// Created by YiJiale on 2021/12/27.
//

#include "iostream"
using namespace std;

//模板函数

//交换两个整型函数
void swapInt(int &a,int &b){
int temp = a;
a=b;
b=temp;
}
//交换两个浮点型函数
void swapDouble(double &a,double &b){
double temp =a;
a=b;
b=temp;
}

//函数模板
template<typename T> //声明一个模板,告诉编译器后面代码中紧跟着 打的T不要报错,T是一个通用数据类型
void mySwap(T &a, T &b){
T temp = a;
a=b;
b=temp;
}

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

swapInt(a,b);
//cout << "a=" << a << "b=" << b << endl;

double c=1.1;
double d=2.2;
swapDouble(c,d);
cout << "c=" << c << "d=" << d << endl;
}
//利用函数模板交换
void test02(){
int a=10;
int b=20;

double c=1.1;
double d=2.2;
//两种方式使用函数模板
//1.自动类型推导
mySwap(a,b); //
cout << "a=" << a << "b=" << b << endl;
//2.显示指定类型 在调用函数模板的时候,在<>内指定参数类型,不让编译器自己去猜
mySwap<double>(c,d);
cout << "c=" << c << "d=" << d << endl;

}

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

8.2 类模版实例化为类

8.2.1类模板语法

类模板作用:

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

语法:

1
2
template<typename T>

解释:

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
//
// Created by YiJiale on 2021/12/28.
//
#include "iostream"
using namespace std;
#include "string"

//类模板
template<class NametType,class AgeType>
class Person
{
public:
Person(NametType name,AgeType age){
this->m_Name=name;
this->m_Age=age;
}

void showPerson(){
cout << "name: " << this->m_Name << "age= "<< this->m_Age << endl;
}

NametType m_Name;
AgeType m_Age;
};
void test01(){
Person<string,int> p1 ("孙悟空",999);
p1.showPerson();
}



int main(){
test01();
}

8.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
//
// Created by YiJiale on 2021/12/28.
//
#include "iostream"
using namespace std;

//类模板与函数模板区别
template<class NameType,class AgeType=int > //这里可以直接写class AgeType=int 只有在类模板中能用
class Person{
public:
Person(NameType name,AgeType age){
this->m_Name=name;
this->m_Age=age;
}

void showPerson(){
cout << "name: " << this->m_Name << "age= " << this->m_Age << endl;
}
NameType m_Name;
AgeType m_Age;
};

//1、类模板没有自动类型推导使用方式
void test01(){
//Person p("孙悟空",1000); //这种是错误的,无法自动类型推导
Person<string,int> p("孙悟空",1000); //正确,只能用显示指示类型
p.showPerson();

}
//2、类模板在模板参数列表中可以有默认参数
void test02(){
Person<string> p("猪八戒",999); //当初始化类模板的时候使用了默认参数 class AgeType=int 时 这里便可以省略 int的指示类型
p.showPerson();
}



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

总结:

  • 类模板使用只能用显示指定类型方式
  • 类模板中的模板参数列表可以有默认参数(函数模板不能这样)

九. STL

9.1 基本概念

  • STL(Stand Template Library,标准模板库)

  • STL从广义上分为: 容器(container)、算法(algorithm)、迭代器(iterator)

  • 容器算法之前 通过迭代器进行无缝连接

  • STL几乎所有的代码都采用了模板类或者模板函数

9.2 STL六大组件

STL大体分为六大组件,分别是:容器算法迭代器仿函数适配器(配接器)空间配置器

1.容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据

2.算法:各种常用的算法,如sort、find、copy、for_each等

3.迭代器:扮演了各种容器与算法之间的胶合剂

4.仿函数:行为类似函数,可作为 算法的某种策略

5.适配器:一种用来修饰容器或者仿函数或迭代器接口的东西

6.空间配置器:负责空间的配置与管理

vector 动态数组
list 双向链表
map 红黑树

容器分为:

  • 序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置
  • 关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系

算法分为:

  • 质变算法: 是指运算过程中 会更改区间内的元素的内容。例如拷贝,替换,删除等等
  • 非质变算法:是指运算过程中不会更改区间内的 元素内容,例如查找、计数、遍历、寻找极值等等

9.3vector存放内置数据类型

容器: vector

算法: for_each

迭代器: vector<int>::iterator

示例:

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
//
// Created by YiJiale on 2021/12/28.
//
#include "iostream"
#include "vector"
using namespace std;
#include "algorithm"

//vector容器存放内置数据类型


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

void test01(){
//创建了一个vector容器,数组
vector<int> v;

//向容器中插入数据 (尾插法)
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);

//通过迭代器来访问容器中的数据
vector<int>::iterator itBegin = v.begin(); //起始迭代器 指向容器中的第一个元素
vector<int>::iterator itEnd = v.end(); //结束迭代器 指向容器中最后一个元素的下一个位置

//第一种遍历方式
while(itBegin != itEnd){
cout << *itBegin << endl;
itBegin++;
}
//第二种遍历方式 利用STL提供遍历算法
for_each(v.begin(),v.end(),myPrint); //回调技术,一开始不调用,在循环期间再调用
}

int main(){
test01();
}

1640690665153.png

十. 设计模式

10.1 面向对象程序设计的思想

10.2 基本概念

10.3 单例模式

10.4 策略模式