一、内存分区模型#
C++程序执行时,内存大方向分为4个区域
- **代码区:**存放函数体的二进制代码
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量 - 特点:先进后出
- 堆区:由程序员分配和释放,若不主动释放则在程序结束时由操作系统回收
1.1 程序运行前#
编译后生成exe可执行程序,未执行该程序前分为两个区域
**代码区:**存放 CPU 执行的机器指令
- 共享 - 共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 只读 - 防止程序意外地修改了它的指令
**全局区:**该区域的数据在程序结束后由操作系统释放
**包括:**全局变量、静态变量、字符串常量、常量区和其他常量
1
2
| //静态变量
static int a = 10;
|
1.2 程序执行后#
栈区:
由编译器自动分配释放, 存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
1
2
3
| //使用new在堆区开辟内存
int* a = new int(10);
int* arr = new int[10];
|
1.3 new操作符#
语法:new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
使用 delete 指针
释放堆区内存
1
2
3
4
5
6
| //堆区开辟数组
int* func()
{
int* arr = new int[10];
return arr;
}
|
二、引用#
**作用: **给变量起别名
语法: 数据类型 &别名 = 原名
1
2
3
| //a的别名b
int a = 10;
int& b = a;
|
引用可以引用栈区和堆区,不能引用全局区的数据
1
| //int& c = 10; //这行代码会报错,访问非法内存空间
|
2.1 引用注意事项#
1
2
| //int &a; 这种定义方式是错误的
const int & ref = 10;//不会报错,本质是 int temp = 10;int& ref = temp;只不过temp是编译器临时定义的,我们找不到它
|
2.2 引用做参数传递#
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| //交换函数的 引用传递 实现方式
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 1;
int b = 2;
swap(a, b);
cout << a << endl;
cout << b << endl;
//分割
system("pause");
return 0;
}
|
通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
2.3 引用做函数返回值#
- 1.引用可以作为函数的返回值
- 2.如果函数的返回值是引用,则函数的调用可以作为左值
注意:不要返回局部变量引用
1
2
3
4
5
6
7
8
| //1.
int& IntReturn(){
static int a = 10;
return a;
}
//2.
int &ref = IntReturn();//即 ref 引用 变量a
IntReturn() = 1000;//即 a 的值变为1000
|
2.4 引用的本质#
引用在C++内部实现就是一个指针常量(指向不可以更改,指向的值可以更改)
1
2
3
4
5
6
| int a = 10;
int& ref = a;
//本质是 int* const ref = &a;
cout << ref << endl;
//本质是 cout << *ref << endl; 编译器自动做了 解引用 的操作
|
2.5 常量引用#
作用:常量引用主要用来修饰形参,防止误操作。
1
2
3
4
5
| //在函数形参列表中,可以加const修饰形参,防止形参改变实参
void showValue(const int& v){
//v += 10; //用来防止 这一行 发生,如果没有const修饰并执行了这行代码,则实参也被修改了
cout << v << endl;
}
|
三、函数提高#
3.1 函数默认参数#
1
2
3
4
5
6
| //在声明函数时设置默认参数
int func(int a=1, int b=2, int c=3);
int func(int a, int b, int c){
return a+b+c;
}
|
3.2 函数占位参数#
补:占位参数也可以有默认值
1
2
3
4
5
6
7
| //例
void func(int a, int){
cout << "占位参数" << endl;
}
//func(1)//这样是非法的
func(1,2);//这个2是用不到的
|
3.3 函数重载#
3.3.1 概述#
**作用:**函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数 类型不同 或者 个数不同 或者 顺序不同
注意: 函数的返回值不可以作为函数重载的条件
1
2
3
4
5
6
7
8
9
10
| void func(){
cout << "函数没有接收到参数" << endl;
}
void func(int a){
cout << "函数接收到整型参数" << a << endl;
}
void func(double b, int a){
cout << "函数接收到双浮点型参数" << b << endl;
cout << "函数接收到整型参数" << a << endl;
}
|
3.3.2 注意事项#
1
2
3
4
5
6
7
8
9
10
11
| void func(int &a){
cout << "func (int &a) 调用" << endl;
}
void func(const int &a){
cout << "func (int &a) 调用" << endl;
}
int a = 10;
func(a);
func(10)
|
四、类和对象*#
C++面向对象 三大特征:封装、继承、多态
万事万物皆为对象,对象上有其属性和行为
具有相同性质的对象,抽象称为类
类中的属性和行为 统一称为成员
属性也称为 - 成员属性 成员变量
行为也称为 - 成员方法 成员函数
语法: class 类名{ 访问权限: 属性 / 行为 };
4.1 封装#
4.1.1 封装的意义#
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
4.1.2 封装的权限控制#
- public - 公共权限 - 成员 类内可以访问 类外也可以访问
- protected - 保护权限 - 成员 类内可以访问 类外不可以访问,可以继承
- private - 私有权限 - 成员 类内可以访问 类外不可以访问,不可以继承
4.1.3 struct 和 class区别#
唯一的区别在于 默认的访问权限不同
- struct 默认权限为公共 public
- class 默认权限为私有 private
4.1.4 成员属性设置为私有#
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,可以检测数据的有效性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| //1.控制读写权限
class Person{
int age;
public:
void getAge(){ //只读权限
return age;
}
}
//2.判断数据的有效性
class Student{
int score;
public:
int setScore(int Score){ //只读权限
if(score > 60){
return age;
}
return 0;
}
}
|
4.2 对象的初始化和清理#
C++中的面向对象来源于生活,每个对象也都会有初始设置 以及 对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数#
对象的初始化和清理也是两个非常重要的安全问题
1. 一个对象或者变量没有初始状态,对其使用后果是未知
2. 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
4.2.1.1 语法#
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
|
4.2.2 构造函数的分类及调用#
两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| //拷贝构造函数 写法
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//注意:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();
Person p; //调用无参构造函数
//注意:不能利用拷贝构造函数初始化匿名对象 编译器认为是对象声明
//Person(p4);
//2.2 显式法
Person p = Person(10);
//Person(10)单独写就是匿名对象 当前 行结束之后,马上析构
//2.3 隐式转换法
Person p = 10; // Person p = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
|
4.2.2.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
| //1. 使用一个已经创建完毕的对象来初始化一个新对象
Person man(100); //p对象已经创建完毕
Person newman(man); //调用拷贝构造函数
//2. 值传递的方式给函数参数传值 - 在执行dowork函数时 新创建了一个临时p 即调用了拷贝构造函数
//相当于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();//doWork2函数返回了一个P1,将p1赋给p时 即调用了拷贝构造
cout << (int *)&p << endl;//验证 不是同一个对象 和上面的地址不同
}
|
4.2.4 构造函数调用规则#
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
4.2.5 深拷贝与浅拷贝#
深浅拷贝是面试经典问题
**浅拷贝:**简单的赋值拷贝操作
**深拷贝:**在堆区重新申请空间,进行拷贝操作
浅拷贝带来的问题是 堆区的内存重复释放 那么要利用深拷贝解决
析构函数执行时,也要将在堆区开辟的数据进行释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class Person {
public:
m_height = new int(height);
//深拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_height = new int(*p.m_height);
}
//析构函数 释放的标准写法
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
|
4.2.6 初始化列表#
**作用:**C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)... {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| //传统方式初始化
Person(int a) {
m_A = a;
m_B = b;
m_C = c;
}
//初始化列表方式初始化
//1.
Person() :m_A(1), m_B(2), m_C(2) { }
//2.
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {
//函数体就没什么可以写了
}
|
4.2.7 类对象作为类成员#
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
1
2
3
4
5
6
| //例 - B类中有对象A作为成员,A为对象成员
class A {}
class B
{
A a;//a就是对象成员
}
|
构造的顺序是 :先调用对象成员的构造,再调用本类构造
析构顺序与构造顺序相反
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class Phone{
public:
string m_PhoneName;
Phone(string name){
m_PhoneName = name;
cout << "Phone构造" << endl;
}
};
class Person{
public:
string m_Name;
Phone m_Phone;
//初始化列表可以告诉编译器调用哪一个构造函数
Person(string name, string pName) :m_Name(name), m_Phone(pName)//m_Phone(pName) 相当于是做了隐式转换法来调用构造函数Phone(string name)
{
cout << "Person构造" << endl;
}
};
void test01()
{
//当类中成员是其他类对象时,我们称该成员为 对象成员
Person p("张三" , "苹果X");
p.playGame();
}
|
4.2.8 静态成员#
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存 - 全局区
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
静态成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class Person{
public:
static int m_A; //静态成员变量 类内声明
private:
static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;//类外初始化
int Person::m_B = 10;//类外初始化
//两种访问方式
//1、通过对象
Person p1;
p1.m_A = 100;
//2、通过类名
cout << "m_A = " << Person::m_A << endl;
//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
|
静态成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class Person{
public:
static void func(){
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; //错误,不可以访问非静态成员变量
}
static int m_A; //静态成员变量
int m_B; //
};
int Person::m_A = 10;//静态成员变量初始化
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.func();
//2、通过类名
Person::func();
|
4.3 C++对象模型和this指针#
4.3.1 成员变量和成员函数分开存储#
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
空对象占用内存空间为:1,是为了区分空对象占内存的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class Person {
public:
Person() {
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};
|
4.3.2 this指针概念#
this指针的本质是一个指针常量,指针的指向不可修改
this指针指向被调用的成员函数所属的对象
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)//引用方式返回,是返回本体
{
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
void test01()
{
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;
}
|
4.3.3 空指针访问成员函数#
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
1
2
3
| Person * p = NULL;
p->ShowClassName(); //空指针,可以调用成员函数
p->ShowPerson(); //但是如果成员函数中用到了this指针,就不可以了
|
如果用到this指针,需要加以判断保证代码的健壮性
1
2
3
4
5
6
| void ShowPerson() {
if (this == NULL) {
return;
}
cout << mAge << endl;
}
|
4.3.4 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() {
m_A = 0;
m_B = 0;
}
//this指针的本质是一个指针常量,指针的指向不可修改 本质为Type* const pointer
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {//const的本质是 const this指针
//this = NULL; //不能修改指针的指向 Person* const this;
//this->mA = 100; //但是this指针指向的对象的数据是可以修改的
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}
void MyFunc() const {
//mA = 10000;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量
//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数
}
|
4.4类外写成员函数#
::表示作用域
1
2
3
4
5
6
7
8
| class Person{
public:
void func();
}
Person::func(){// ::表示作用域
//函数体
}
|
4.5 友元#
在程序里,有些私有(private)属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现
4.5.1 全局函数做友元#
1
2
3
4
5
| class Person
{
//在类中使用friend声明全局函数 就使类做友元了
friend void func(Person * person);//在主函数中 这个函数就能访问私人权限属性了
};
|
4.5.2 类做友元#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| class Building;
class goodGay
{
public:
goodGay();
void visit();
private:
Building *building;
};
class Building
{
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
|
4.5.3 成员函数做友元#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class goodGay{
public:
void visit(){
cout << "goodGay的成员函数" << endl;
}
private:
Building *building;
};
class Building{
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
private:
int a;//变量a的值 友元元素可以访问
}
|
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
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
| class MyInteger {
private:
int* m_num;
public:
MyInteger() {
this->m_num = new int(1);
//cout << "*this->m_num = " << *this->m_num << endl;
//cout << "this->m_num = " << this->m_num << endl;
}
MyInteger(int a) {
this->m_num = new int(a);
}
MyInteger(const MyInteger& myInteger) {
cout << "调用拷贝构造函数!" << endl;
if (myInteger.m_num != nullptr){
this->m_num = new int(*myInteger.m_num);
}
else {
this->m_num = nullptr;
}
}
~MyInteger(){
if (this->m_num != NULL){
cout << "已为值为" << *this->m_num << "的对象执行析构函数" << endl;
delete this->m_num;
}
}
int getter() {
return *m_num;
}
int* getterAddress() {
return m_num;
}
//加号运算符重载
MyInteger operator+(MyInteger& myInteger) {
MyInteger temp;
if (this->m_num != nullptr && myInteger.m_num != nullptr){
*temp.m_num = *this->m_num + *myInteger.m_num;
}
return temp;
}
//递增运算符重载 - 前置++ 先++,后返回
MyInteger& operator++() {
*m_num = *m_num + 1;
return *this;
}
//递增运算符重载 - 后置++ 先返回,后++
MyInteger operator++(int) {
MyInteger temp = *this;
*m_num = *m_num + 1;
return temp;
}
//赋值运算符重载 - 如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
MyInteger& operator=(const MyInteger& myIngeter) {
if (this->m_num != NULL) {
//delete this->m_num;
//this->m_num = NULL;
*this->m_num = *myIngeter.m_num;
return *this;
}
this->m_num = new int(*myIngeter.m_num);
return *this;
}
//关系运算符 - ==
bool operator==(const MyInteger& myIngeter) {
if (*this->m_num == *myIngeter.m_num) {
return true;
}
return false;
}
//关系运算符 - <=
bool operator<=(const MyInteger& myIngeter) {
if (*this->m_num <= *myIngeter.m_num) {
return true;
}
return false;
}
//关系运算符 - >=
bool operator>=(const MyInteger& myIngeter) {
if (*this->m_num >= *myIngeter.m_num) {
return true;
}
return false;
}
void operator()(string text) {
cout << text << endl;
}
};
//左移运算符重载 - 只能使用全局函数实现
ostream& operator<<(ostream& cout, MyInteger& myInt){
cout << myInt.getter();
return cout;
}
int main() {
MyInteger int1;
MyInteger int2(2);
MyInteger()("i love u");//匿名函数调用
cout << "++int1 = " << ++int1 << endl;
// cout << "int1++ = " << int1++ << endl;
cout << "int1 = " << int1.getter() << endl;
}
|
4.7 继承#
语法: class A : public B{};
A 类称为子类 或 派生类 , B 类称为父类 或 基类
4.7.1 继承方式#
父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
4.7.2 继承中构造和析构顺序#
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
4.7.3 继承同名成员处理方式#
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| //instance
class Father{
public:
int m_A;
};
class Son : public Father{
public:
int m_A
};
int main(){
Son s;
s.m_A;
s.Father::m_A;
}
|
4.7.4 继承同名静态成员处理方式#
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
4.7.5 多继承语法#
C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
4.7.6 菱形继承#
菱形继承概念:
两个派生类继承同一个基类
又有某个类同时继承者两个派生类
这种继承被称为菱形继承,或者钻石继承
菱形继承问题:
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
4.7.7 虚拟继承#
继承虚基类又通过虚基表指针 与 其指向的虚基表来实现,这就是虚拟继承的三板斧。
- 一个类调用构造函数的顺序:虚基类->直接基类->类
- 虚基类只初始化一次
virtual base pointer - vbptr
virtual base table - vbt - 虚基表
4.8 多态#
4.8.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
| class Animal{
public:
virtual speak(){//Speak函数就是虚函数 编译器在编译的时候就不能确定函数调用
cout << "Animal Speak!" << endl;
}
}
class Cat : public Animal{
public:
void speak(){
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal{
public:
void speak(){
cout << "小狗在说话" << endl;
}
};
void DoSpeak(Animal & animal){//多态使用条件:父类指针或引用指向子类对象
animal.speak();
}
int main() {
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
system("pause");
return 0;
}
|
注:重写:函数返回值类型 函数名 参数列表 完全一致称为重写
4.8.2 纯虚函数和抽象类#
纯虚函数: virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
| class Base{
public:
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;//纯虚函数
};
|
4.8.3 虚析构和纯虚析构#
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
虚析构语法: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
| class Animal {
public:
Animal(){
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//析构函数加上virtual关键字,变成虚析构函数
//virtual ~Animal()
//{
// cout << "Animal虚析构函数调用!" << endl;
//}
virtual ~Animal() = 0;
};
Animal::~Animal(){
cout << "Animal 纯虚析构函数调用!" << endl;
}
//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Cat : public Animal {
public:
Cat(string name){
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak(){
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat(){
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01(){
Animal *animal = new Cat("Tom");
animal->Speak();
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//怎么解决?给基类增加一个虚析构函数
//虚析构函数就是用来解决通过父类指针释放子类对象
delete animal;
}
int main() {
test01();
system("pause");
return 0;
}
|
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
五、文件操作#
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
文件类型分为两种:
- 文本文件 - 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作
- ifstream: 读操作
- fstream : 读写操作
5.1 文本文件的 读写#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| //读写文件步骤如下:
//1.包含头文件
#include <fstream>
//2.创建流对象
ofstream ofs;
ifstream ifs;
fstream fs;
//3.打开文件
ofs.open("文件路径",打开方式);
//4.读写数据 利用<<可以向文件中写数据
ofs << "写入的数据";
//5.关闭文件
ofs.close();
|
文件打开方式:
打开方式 | 解释 |
---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意: 文件打开方式可以配合使用,利用|操作符
**例如:**用二进制方式写文件 ios::binary | ios:: out
5.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
| #include <fstream>
#include <string>
void test01()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
//第一种方式
//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 (getline(ifs, buf))
//{
// cout << buf << endl;
//}
char c;
while ((c = ifs.get()) != EOF)
{
cout << c;
}
ifs.close();
}
int main() {
test01();
system("pause");
return 0;
}
|
5.2 二进制文件的 读写#
以二进制的方式对文件进行读写操作
打开方式要指定为 ==ios::binary==
5.2.1 写文件#
二进制方式写文件主要利用流对象调用成员函数write
函数原型 :ostream& write(const 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| #include <fstream>
#include <string>
class Person
{
public:
char m_Name[64];
int m_Age;
};
//二进制文件 写文件
void test01()
{
//1、包含头文件
//2、创建输出流对象
ofstream ofs("person.txt", ios::out | ios::binary);
//3、打开文件
//ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三" , 18};
//4、写文件
ofs.write((const char *)&p, sizeof(p));
//5、关闭文件
ofs.close();
}
int main() {
test01();
system("pause");
return 0;
}
|
总结:
- 文件输出流对象 可以通过write函数,以二进制方式写数据
5.2.2 读文件#
二进制方式读文件主要利用流对象调用成员函数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
25
26
27
28
29
30
31
32
| #include <fstream>
#include <string>
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;
}
int main() {
test01();
system("pause");
return 0;
}
|
- 文件输入流对象 可以通过read函数,以二进制方式读数据