扬仔的杂货铺

分享微不足道的经验,记录稀奇古怪的玩意

0%

C++核心编程

本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。

内存分区模型

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

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

内存四区意义:

不同区域存放的数据,赋予不同的声明周期,给我们更大的灵活编程

程序运行前

在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

代码区:

存放CPU执行的机器命令

代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

全局区:

全局变量和静态变量存放在此

全局区还包含了常量区,字符串常量和其他常量也存放在此

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

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
// 全局变量
int g_a = 10;
int g_b = 10;

// const修饰的全局变量,全局常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {
// 全局区

// 全局变量、静态变量、常量
int a = 10;
int b = 10;
cout << "局部变量a的地址为:" << &a << endl;
cout << "局部变量b的地址为:" << &b << endl;
cout << "全局变量g_a的地址为:" << &g_a << endl;
cout << "全局变量g_b的地址为:" << &g_b << endl;

// 静态变量 在普通变量前面家static,属于静态变量
static int s_a = 10;
static int s_b = 10;
cout << "静态变量s_a的地址为:" << &s_a << endl;
cout << "静态变量s_b的地址为:" << &s_b << endl;

// 常量
// 字符串常量
cout << "字符串常量的地址为\t:" << &"hello world" << endl;

// const修饰的变量
// const修饰的全局变量,const修饰的局部变量
cout << "全局常量c_g_a地址为:" << &c_g_a << endl;
cout << "全局常量c_g_b地址为:" << &c_g_b << endl;

// const修饰的局部变量
const int c_l_a = 10;
const int c_l_b = 10;

cout << "局部常量c_l_a地址为:" << &c_l_a << endl;
cout << "局部常量c_l_b地址为:" << &c_l_b << endl;

return 0;
}
/*
// 栈区
局部变量a的地址为:0x7ffee47a8b98
局部变量b的地址为:0x7ffee47a8b94
// 全局区
全局变量g_a的地址为:0x10b45f0b8
全局变量g_b的地址为:0x10b45f0bc
静态变量s_a的地址为:0x10b45f0c0
静态变量s_b的地址为:0x10b45f0c4
字符串常量的地址为:0x10b45aece
全局常量c_g_a地址为:0x10b45af54
全局常量c_g_b地址为:0x10b45af58
// 栈区
局部常量c_l_a地址为:0x7ffee47a8b90
局部常量c_l_b地址为:0x7ffee47a8b8c
*/

总结:

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放const修饰的全局常量和字符串常量

程序运行后

栈区

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

注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 栈区数据注意事项
int * func(int b) {// 形参数据也会放在栈区
// 局部变量 存放在栈区,栈区的数据在函数执行完后自动释放
int a = 10;
// 反悔局部变量的地址
return &a;
}


int main() {
int *p = func(1);
// 第一次可以打印正确的数字,是因为编译器做了保留
cout << *p << endl; // 打印了10
// 第二次这个数据就不再保留了
cout << *p << endl; // 打印了32767,之前的数据已经被清除
return 0;
}

堆区

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int * func()
{
// 利用new关键字 可以将数据开辟到堆区
// 指针 本质也是局部变量,放在栈上,指针保存的数据是放在堆区
int *p = new int(10);
return p;
}

int main() {
// 在堆区开辟数据
int *p = func();
cout << *p << endl;
return 0;
}

总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内存

new操作符

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

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

语法:new 数据类型

利用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
// 1、new的基本语法
int * func()
{
// 在堆区创建整型数据
// new返回的是该数据类型的指针
int *p = new int(10);
return p;
}

void test01(){
int *p = func();
cout << *p << endl;
cout << *p << endl;
// 堆区的数据 由程序员管理开辟,程序员管理释放
// 如果想释放堆区的数据,利用关键字 delete
delete(p);
cout << *p << endl;
}
// 2、在堆区利用new开辟数组
int * test02(){
// 在堆区创建整型数据
// new返回是 该数据类型的指针
int * arr = new int[10]; // 10代表数组有10个元素
for (int i = 0; i < 10; ++i) {
arr[i] = i + 100;
}
for (int i = 0; i < 10; ++i) {
cout << arr[i] << endl;
}
// 释放堆区数组
// 释放数组的时候 要加[]才可以
delete[] arr;

for (int i = 0; i < 10; ++i) {
cout << arr[i] << 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
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;
}
/*
a = 10
b = 10
a = 100
b = 100
*/

引用注意事项

  • 引用必须初始化(int &b;// 错误的)
  • 引用在初始化后,就不可以更改了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main ()
{
int a = 10;
// 1、引用必须初始化
// int &b;// 错误,必须要初始化(给初始值)
int &b = a;

// 2、引用在初始化后,不可以改变
int c = 20;
b = c; // 赋值操作,而不是更改引用

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
49
50
51
52
// 交换函数

// 1、值传递
void swap01(int a, int b)
{
int temp = a;
a = b;
b = temp;
cout << "(swap01) a = " << a << endl;
cout << "(swap01) b = " << b << endl;
}

// 2、地址传递
void swap02(int * a, int * b)
{
int temp = *a;
*a = *b;
*b = temp;
cout << "(swap02) a = " << *a << endl;
cout << "(swap02) b = " << *b << endl;
}


// 3、引用传递
void swap03(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
cout << "(swap03) a = " << a << endl;
cout << "(swap03) b = " << b << endl;
}

int main ()
{
int a = 10;
int b = 20;

// 值传递,形参不会修饰实参
// swap01(a,b);

// 地址传递,形参会修饰实参
// swap02(&a,&b);

// 引用传递,形参会修饰实参
swap03(a,b);

cout << "(main) a = " << a << endl;
cout << "(main) b = " << b << 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
// 引用做函数的返回值
// 1、不要返回局部变量的引用
int & test01()
{
// 局部变量存放在四区中的栈区
int a01 = 10;
cout << "a01 = " << a01 << endl;
return a01;
}
// 2、函数的调用可以作为左值
int & test02(){
// 静态变量,存放在全局区
static int a02 = 10;
cout << "a02 = " << a02 << endl;
return a02;
}

int main ()
{
int &ref01 = test01();
// 此时a的内存已经被释放
cout << "ref01 = " << ref01 << endl;
int &ref02 = test02();
cout << "ref02 = " << ref02 << endl;

// 可以作为左值,所以这里test02() 就是static int a02的别名
test02() = 1000;
cout << "ref02 = " << ref02 << endl;

return 0;
}

引用的本质

本质:引用的本质在c++内部实现是一个指针常量

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 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
22
23
24
25
26
// 打印数据函数(形参使用const int & 防止引用的值被修改)
void showValue(const int & val){
cout << "val = " << val << endl;
// val = 1000; // 这里修改编译器会报错,防止数据会被修改
}


int main ()
{
// 常量引用
// 使用场景,用来修饰形参,防止误操作
// int a = 10;
// int & ref1 = a; // 引用必须引一块合法的内存空间(int & ref = 10;不可以 因为10是常量,在全局区)

// 加上const之后 编译器将代码修改 int temp = 10; const int & ref = temp;
const int & ref2 = 10;
// ref = 20; // 加入const之后变为只读,不可以修改

int a = 100;
showValue(a);

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


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
// 函数声明
// 声明和实现只能有一个有默认参数
int func2(int a, int b = 20, int c = 30);// 这里有默认参数,实现不能有默认参数

// 函数的默认参数
int func(int a, int b = 20, int c = 30)
{
return a + b + c;
}

// 注意事项
// 1、如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
// 2、如果函数声明有默认参数,函数实现就不能有默认参数
int func2(int a, int b, int c)
{
return a + b + c;
}

int main ()
{
int result = func(20);
cout << "result = " << result << endl;
return 0;
}

函数占位参数

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

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

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

1
2
3
4
5
6
7
8
9
10
11
12
13
// 占位参数
// 目前阶段的占位参数 我们还用不到,后面课程中会用到
// 占位参数 还可以有默认参数
void func(int a, int = 10)
{
cout << "this is func" << endl;
}

int main ()
{
func(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
// 函数重载
// 可以让函数名相同,提高复用性

// 函数重载的满足调价你
// 1、同一个作用域下
// 2、函数名称相同
// 3、函数参数类型不同,或者个数不同,或者顺序不同
void func(){
cout << "hello" << endl;
}

void func(int a){
cout << "helloa" << endl;
}


int main ()
{
func();
func(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 func(int &a){
cout << "func(int &a)调用" << endl;
}
void func(const int &a){
cout << "func(const int &a)调用" << endl;
}

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

int main ()
{
int a = 10;
func(a);

func(10);

// 因为默认参数的存在,只传一个参数就会产生二义性,所以会报错
// func2(10);
return 0;
}

类和对象

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

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

例如:

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

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

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

封装

封装的意义

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

封装的意义:

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

封装意义一:

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

语法: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
const double PI = 3.14;

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

// class 代表设计一个类,类后面紧跟着的就是类名称
class Circle {
// 访问权限
public :
// 属性
// 半径
int m_r = 0;

// 行为
// 获取圆的周长
double calculateZC(){
return 2 * m_r * PI;
}
};
int main ()
{
// 通过圆类 创建具体的圆(对象)
// 实例化(通过一个类创建一个对象的过程)
Circle cl;
// 给圆对象 的属性进行赋值
cl.m_r = 10;
cout << "圆的周长为" << cl.calculateZC() << 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
class Student {
public: // 公共权限

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

// 属性
string m_Name;
int m_Id;

void setMName(const string &mName) {
m_Name = mName;
}

void setMId(int mId) {
m_Id = mId;
}
void showStudent() {
cout << "姓名:" << m_Name
<< " 学号:" << m_Id << endl;
}
};

int main() {
// 创建一个具体学生 实例化对象
Student s1;
// 给s1对象 进行属性赋值操作
s1.setMName("张三");
s1.setMId(1);

s1.showStudent();

Student s2;
// 给s1对象 进行属性赋值操作
s2.setMName("李四");
s2.setMId(2);

s2.showStudent();
return 0;
}

封装意义一:

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

访问权限有三种:

  1. public 公共权限
  2. protected 保护权限
  3. private 私有权限

struct和class区别

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

区别:

  • struct 默认权限为公共
  • class 默认权限为私有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class C1{
int m_A;// 默认是私有权限 private
void setMA(int mA) {
m_A = mA;
}
};

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

};

int main() {
C1 c1;
// c1.m_A = 10; // 由于默认的权限是私有权限,所以不能直接访问,编译报错
// c1.setMA(10); // 方法也是一样

struct C2 c2;
c2.m_A = 10;
return 0;
}

成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限

优点2:对于写权限,我们可以检测数据的有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 成员属性设置为私有
// 1、可以自己控制读写的权限
// 2、对于写可以检测数据的有效性

class Person{
public:
// 设置姓名
void setMName(string mName){
m_Name = mName;
}

// 获取姓名
string getMName(){
return m_Name;
}

// 获取年龄
int getAge(){
return m_Age;
}

// 设置年龄
void setAge(int age){
if (age >= 0 && age <= 150){
m_Age = age;
} else {
m_Age = 0;
}
}

// 设置情人
void setLover(string lover){
m_Lover = lover;
}

private:
// 姓名 可读可写
string m_Name;
// 年龄 可读可写
int m_Age;
// 情人 只写
string m_Lover;
};

int main() {
Person p;
p.setMName("张三");
cout << p.getMName() << endl;

// p.m_Age = 10; // 不可以设置,因为是私有属性
p.setAge(18);
cout << p.getAge() << endl;

p.setLover("李四");
return 0;
}

练习案例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
// 立方体类设计
// 1、创建立方体类
// 2、设计属性
// 3、设计行为 获取立方体面积和体积
// 4、分别利用全局函数和成员函数 判断两个立方体是否相等

class Cube {
public:
// 行为

// 获取面积
int calS() {
return 2 * (m_H * m_L + m_H * m_W + m_L * m_W);
}

// 获取体积
int calV() {
return m_H * m_L * m_W;
}

// 设置获取长宽高
void setMH(int mH) {
m_H = mH;
}

void setML(int mL) {
m_L = mL;
}

void setMW(int mW) {
m_W = mW;
}

int getMH() const {
return m_H;
}

int getML() const {
return m_L;
}

int getMW() const {
return m_W;
}

bool isEqual(const Cube &c) {
if (m_H == c.getMH() &&
m_L == c.getML() &&
m_W == c.getMW()) {
return true;
} else {
return false;
}
}

private:
// 属性
// 高
int m_H;
// 长
int m_L;
// 宽
int m_W;
};

bool isEqual(const Cube &c1, const Cube &c2) {
if (c1.getMH() == c2.getMH() &&
c1.getML() == c2.getML() &&
c1.getMW() == c2.getMW()) {
return true;
} else {
return false;
}
}

int main() {
// 创建立方体对象
Cube c1;
c1.setMH(10);
c1.setML(10);
c1.setMW(10);

// 600
cout << "c1的面积为:" << c1.calS() << endl;
// 1000
cout << "c1的体积为:" << c1.calV() << endl;

// 创建第二个立方体
Cube c2;
c2.setMH(10);
c2.setML(10);
c2.setMW(10);

cout << (c1.isEqual(c2)?"(成员函数 c1.isEqual(c2) )c2 和 c1 是相等的":"c2 和 c1 是不相等的") << endl;

cout << (isEqual(c1,c2)?"(全局函数 isEqual(c1,c2) )c1 和 c2 是相等的":"c1 和 c2 是不相等的") << endl;
return 0;
}

练习案例2:点和圆的关系

设计一个圆形类(Circle), 和一个点类(Point), 计算点和圆的关系

文件分拆:

point.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include<iostream>
using namespace std;

class Point {
public:
int getMX() const;

void setMX(int mX);

int getMY() const;

void setMY(int mY);
private:
int m_X;
int m_Y;
};

point.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "point.h"

int Point::getMX() const {
return m_X;
}

void Point::setMX(int mX) {
m_X = mX;
}

int Point::getMY() const {
return m_Y;
}

void Point::setMY(int mY) {
m_Y = mY;
}

circle.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#include<iostream>
#include "point.h"
using namespace std;


class Circle{
public:
const Point &getMCenter() const;

void setMCenter(const Point &mCenter);

int getMR() const;

void setMR(int mR);

private:
// 在类中可以让另一类 作为本类中的成员
Point m_Center; // 圆心
int m_R; // 半径
};

circle.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "circle.h"

const Point &Circle::getMCenter() const {
return m_Center;
}

void Circle::setMCenter(const Point &mCenter) {
m_Center = mCenter;
}

int Circle::getMR() const {
return m_R;
}

void Circle::setMR(int mR) {
m_R = mR;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include "point.h"
#include "circle.h"

using namespace std;

// 判断点和圆的关系
// 三种情况:点在圆内 点在圆外 点在圆上
// 判断点和圆的关系
void isInCircle(Circle & circle, Point & point){
int cx = circle.getMCenter().getMX();
int cy = circle.getMCenter().getMY();
int px = point.getMX();
int py = point.getMY();
int distance = (cx - cy) * (cx - cy) + (px - py) * (px - py);
int rDistance = circle.getMR() * circle.getMR();
if (distance > rDistance){
cout << "点在圆外" << endl;
} else if (distance < rDistance) {
cout << "点在圆内" << endl;
} else {
cout << "点在圆上" << endl;
}

}
int main() {
// 创建圆
Point point1;
point1.setMX(10);
point1.setMY(0);
Circle circle;
circle.setMCenter(point1);
circle.setMR(10);

// 创建点
Point point2;
point2.setMX(10);
point2.setMY(9);

// 判断关系
isInCircle(circle,point2);

return 0;
}

对象的初始化和清理

  • 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会删除一些自己信息数据保证安全
  • C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置

构造函数和析构函数

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

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

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

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
// 对象的初始化和清理
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 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 getAge() const {
return age;
}

private:
int age;
};

// 调用
void test01(){
// 1、括号法
Person p1; // 默认构造函数调用
Person p2(10); // 有参构造函数
Person p3(p2); // 拷贝构造函数调用

// 注意事项1
// 调用默认构造函数的时候,不要加()
// 因为Person p1(); 这个代码 编译器会认为是一个函数的声明,不会认为在创建对象

cout << "p2的年龄为:" << p2.getAge() << endl; // 10
cout << "p3的年龄为:" << p3.getAge() << endl; // 10


// 2、显式法
Person p4;
Person p5 = Person(10); // 有参构造
Person p6 = Person(p5); // 拷贝构造

Person(10); // 匿名对象 特点:当前行执行结束后,系统会立即收回掉匿名对象
// 注意事项2
// 不要利用拷贝构造函数 初始化匿名对象
// 编译器会认为 Person(p3) 为 Person p3
// 对象是不能重名的,所以会报错

// 3、隐式转换法
Person p7 = 10; // 相当于写了 Person p7 = Person(10); 有参构造
Person p8 = p7; // 拷贝构造
}

int main() {
test01();
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
// 拷贝构造函数调用时机

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

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

// 拷贝构造函数
Person(const Person &p) {
// 将传入的人身上的所有属性,拷贝到我身上
m_Age = p.m_Age;
cout << "Person的拷贝构造函数调用" << endl;
}

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

int getAge() const {
return m_Age;
}

private:
int m_Age;
};

// 1、使用一个已经创建完毕的对象来初始化一个新对象
void test01(){
Person p1(20);
Person p2(p1);

cout << "p2的年龄为:" << p2.getAge() << endl;
}

// 2、值传递的方式给函数参数传值
void doWork(Person p){
// 进入函数的时候就会调用Person的拷贝构造函数
}
void test02(){
Person p;
doWork(p);
}

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

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

int main() {
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
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:
// 构造函数
Person() {
cout << "Person的默认构造函数调用" << endl;
}

Person(int age, int height){
m_Age = age;
m_Height = new int(height);
cout << "Person的有参构造函数调用" << endl;
}

// 拷贝构造函数
// 如果利用编译器提供的拷贝构造函数,会做浅拷贝操作
Person(const Person &p) {
// 将传入的人身上的所有属性,拷贝到我身上
m_Age = p.m_Age;
// 深拷贝
m_Height = new int(*p.m_Height);
// 浅拷贝
// m_Height = p.m_Height;
cout << "Person的拷贝构造函数调用" << endl;
}

// 析构函数
~Person() {
if (m_Height != NULL) {
delete m_Height;
m_Height = NULL;
}
cout << "Person的析构函数调用" << endl;
}

int getAge() const {
return m_Age;
}

int *getMHeight() const {
return m_Height;
}

private:
int m_Age;
int *m_Height;
};

// 1、使用一个已经创建完毕的对象来初始化一个新对象
void test01(){
Person p1(18,160);
cout << "p1的年龄为:" << p1.getAge() << " 身高为:" << *p1.getMHeight() << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.getAge() << " 身高为:" << *p2.getMHeight() << endl;
}
int main() {
test01();
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
// 初始化列表
class Person
{
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)
{
}

int m_A;
int m_B;
int m_C;
};

int main() {
Person p(10,20,30);
cout << "m_A = " << p.m_A << endl;
cout << "m_B = " << p.m_B << endl;
cout << "m_C = " << p.m_C << endl;
return 0;
}

类对象成为类的成员

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

静态成员

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

静态成员分为:

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

C++对象模型和this指针

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 成员变量 和 成员函数 分开存储的
class Person
{

public:
int m_A; // 非静态成员变量 属于类的对象上 // 4
static int m_B; // 非静态成员变量 不属于类的对象上 // 0
void func() { // 非静态成员函数 不属于类的对象上 // 0

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

}
};

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

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

this指针概念

通过上一节我们知道c++中成员变量和成员函数是分开存储的

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

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

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

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

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

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1、解决名称冲突
// 2、返回对象本身用 *this

class Person {
public:
Person(int age)
{
// this指针指向 被调用的成员函数所属的对象
this -> age = age;
}
// 这里的返回值要是this的引用
Person& PersonAddAge(Person &p){
this -> age += p.age;
// this指向p2的指针,而*this指向的就是p2这个对象本体
return *this;
}
int age;
};

int main() {
Person p(18);
Person p2(1);
p.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2);
cout << p.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
// 空指针调用成员函数
class Person {
public:
void showClassName(){
cout << "this is Person class" << endl;
}

void showPersonAge(){
if(this == NULL){
return;
}
cout << "age = " << m_Age << endl;
}

int m_Age;
};

int main() {
Person * p = NULL;
p->showClassName();
p->showPersonAge();
return 0;
}

const修饰成员函数

常函数:

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

常对象:

  • 声明对象前加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
// 空指针调用成员函数
class Person {
public:
Person(){

}

// this指针的本质 是指针常量 指针的指向是不可以修改的
// const Person * const this;
// 在成员函数后面加入const,修饰的是this指向,让指针指向的值也不可以修改
void showPerson() const
{
this->m_B = 100;
// m_A = 100; // 不可以修改了
}
void func(){

}

int m_A;
mutable int m_B;// 特殊变量,即使在常函数中,也可以修改这个值,加关键字mutable
};

// 常对象

int main() {
const Person p; // 在对象前加const,变为常对象
// p.m_A = 10;
p.m_B = 10;// m_B是特殊值,在常对象下也可以修改

// 常对象只能调用常函数
p.showPerson();
// p.func(); // 常对象 不可以调用普通成员函数,因为普通成员函数可以修改属性
return 0;
}

友元

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

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

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

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

友元的关键字为friend

友元的三种实现

  • 全局函数做友元

    friend void goodGuy(Building *building);

  • 类做友元

    friend class GoodGuy;

  • 成员函数做友元

    friend void GoodGuy::visit();

全局函数做友元

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 Building
{
// goodGuy全局函数是Building的好朋友,可以访问Building中私有成员
friend void goodGuy(Building *building);
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom; // 客厅

private:
string m_BedRoom; // 卧室
};

// 全局函数
void goodGuy(Building *building){
cout << "好基友全局函数 正在访问:" << building -> m_SittingRoom << endl;
cout << "好基友全局函数 正在访问:" << building -> m_BedRoom << endl;
}

int main() {
Building building;
goodGuy(&building);
}

类做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 类做友元
class Building;
class GoodGuy{
public:
void visit(); // 参观函数 访问Building中的属性
GoodGuy();
Building * building;
};
class Building{
friend class GoodGuy;
public:
Building();
string m_SittingRoom; // 客厅
private:
string m_BedRoom; // 卧室
};

// 类外写成员函数
Building::Building(){
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGuy::GoodGuy(){
// 创建建筑物的对象
building = new Building;
}
void GoodGuy::visit(){
cout << "好基友类正在访问:" << building -> m_SittingRoom << endl;
cout << "好基友类正在访问:" << building -> m_BedRoom << endl;
}

int main() {
GoodGuy goodGuy;
goodGuy.visit();
}

成员函数做友元

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
// 类做友元
class Building;
class GoodGuy{
public:
GoodGuy();
void visit(); // 让visit函数可以访问Building中私有成员
void visit2(); // 让visit2函数不可以访问Building中私有成员
Building * building;
};
class Building{
friend void GoodGuy::visit();
public:
Building();
string m_SittingRoom; // 客厅
private:
string m_BedRoom; // 卧室
};

// 类外写成员函数
Building::Building(){
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGuy::GoodGuy(){
// 创建建筑物的对象
building = new Building;
}

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

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

int main() {
GoodGuy goodGuy;
goodGuy.visit();
goodGuy.visit2();
}

运算符重载

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

加号运算符重载

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;
int m_B;
// 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;
}
};
// 2、通过全局函数重载+号
// 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;
// }

int main() {
Person p1 ;
p1.m_B = 13;
p1.m_A = 10;
Person p2 ;
p2.m_B = 10;
p2.m_A = 12;

Person p3 = p1 + p2;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
}