C++梁哥笔记day17 | 我的日常分享

C++梁哥笔记day17

类与对象

一、虚析构

1.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
class Animal{
public:
Animal(){
cout<<"Animal无参构造函数"<<endl;
}
~Animal(){
cout<<"Animal析构函数"<<endl;
}
virtual void sleep(){
cout<<"Animal睡觉"<<endl;
}
};
class Cat:public Animal{
public:
Cat(){
cout<<"Cat无参构造函数"<<endl;
}
~Cat(){
cout<<"Cat析构函数"<<endl;
}
virtual void sleep(){
cout<<"Cat睡觉"<<endl;
}
};
void test01(){
Animal *p = new Cat();
p->sleep();//Cat睡觉

delete p;
}

运行结果:

图片

通过向上转型,子类重写父类方法,能够实现一个函数执行多种类对象。但是从上面运行结果可以看到,释放空间的时候仅仅只会调用父类的析构函数,不会调用子类的析构函数,这会造成子类空间内存泄漏。

原因分析:

图片

1.2 解决上面的问题使用虚析构(本质是虚函数)

虚析构的作用:通过基类指针或引用,释放子类所有空间

在析构函数前加virtual修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal{
public:
Animal(){
cout<<"Animal无参构造函数"<<endl;
}
virtual ~Animal(){
cout<<"Animal析构函数"<<endl;
}
virtual void sleep(){
cout<<"Animal睡觉"<<endl;
}
};
class Cat:public Animal{
public:
Cat(){
cout<<"Cat无参构造函数"<<endl;
}
virtual ~Cat(){
cout<<"Cat析构函数"<<endl;
}
virtual void sleep(){
cout<<"Cat睡觉"<<endl;
}
};
void test02(){
Animal *p = new Cat();
delete p;
}

运行结果:

图片

原理分析:

由于采用类虚函数,delete p调用的是子类Cat的析构函数,然后子类析构调用完,系统会自动调用父类析构,利用这个性质就可以从子类开始,全部释放完,不会造成内存泄漏。

Animal 类内存布局

&Animal::{dtor} 析构函数

Cat类内存布局


二、纯虚函数与抽象类

2.1 引入

纯虚函数:如果一个类中拥有纯虚函数,那么这个类就是抽象类

抽象类不能实例化对象

纯虚函数格式:virtual void fun() = 0; =0是一个表达方式,并不是给它赋0

2.2 概念

这设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际创建一个基类对象(其实这个基类对象现实中一般是不存在的,比如基类是Animal,派生类是Dog,Dog类实例化一个对象是实际上存在的,而动物实例化一个对象动物是抽象的没有具体化是哪个动物,实际上是不存在的)。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。

做到这点,可以在基类中加入至少一个纯虚函数,使得基类称为抽象类。纯虚函数使用关键字virtual,并在其后面加上=0,相当于声明后面加了个=0。如果试图去实例化一个抽象类,编译器会阻止这种操作。当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。virtual void fun() = 0;告诉编译器虚函数表vtable中为函数保留一个位置,但是这个特定位置不放地址。

建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要实现(或者不需要完全实现)。可以创建一个公共类。

完了完了,好像跟Java的接口和抽象类学混了。2021.1.12 10:32

总结:

  1. 如果父类是抽象类,在子类中必须实现父类中所有的纯虚函数,否则子类依然是抽象类,无法实例化对象。

2.3 纯虚函数和多继承

多继承带来了一些争议,但是接口继承可以收拾一种毫无争议的运用了。绝大多数面向对象语言都不支持多继承,但是绝大多数面向对象语言都支持接口的概念,C++中没有接口的概念,但是可以通过纯虚函数实现接口。

接口类中只有函数原型定义,没有数据定义。

多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。注意:除了析构函数外,其他声明都得是纯虚函数。

2.4 虚析构函数

虚析构函数作用:虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针释放派生类对象。

2.5 纯虚析构函数

纯虚析构函数在C++中是合法的,但是在使用的时候有一个额外的限制,必须为纯虚析构函数提供一个函数体。那么问题是:如果给虚析构函数提供函数体了,那怎么还能称为纯虚析构函数呢?纯虚析构函数和非纯虚析构函数之间唯一的不同之处在于纯虚析构函数是的基类是抽像类。

1
2
3
4
5
6
7
8
9
class A{
public:
//1. virtual 修饰 并加上=0
virtual ~A() = 0;
}
//2. 必须在外面实现
A::~A(){

}

普通的纯虚函数是不需要实现函数体的。纯虚析构函数要实现函数体的原因是:通过基类指针释放子类对象时,先调用子类析构,再调用父类析构,如果父类析构没有实现则无法调用程序会崩溃。


三、虚函数、纯虚函数、虚析构和纯虚析构的区别

3.1 虚函数

只是加virtual修饰,有函数体(作用域成员函数)

1
2
3
4
5
6
class A{
public:
virtual void fun(){

}
}

目的:通过基类指针或引用操作子类的方法或数据

3.2 纯虚函数

加virtual修饰同时在后面加=0,没有函数体

1
2
3
4
class A{
public:
virtual void fun() = 0;
}

目的:为子类提供固定的流程和接口

3.4 虚析构

加virtual修饰 有函数体

1
2
3
4
5
6
class A{
public:
virtual ~A(){

}
}

目的:为了解决通过释放基类指针时不会调用派生类的析构方法

3.5 纯虚析构

加virtual修饰同时后面添加=0 为了防止释放空间的时候没有函数体程序错误 要在类外面定义函数体

1
2
3
4
5
6
7
class A{
public:
virtual ~A() = 0;
}
A::~A(){

}

目的:为了提供固定的接口,并且防止为了解决通过释放基类指针时不会调用派生类的析构方法失效,所以纯虚析构必须要在外面定义函数体


四、重载、重定义和重写的区别

4.1 重载

同一作用域的同名函数

  1. 同一个作用域
  2. 参数个数,参数顺序,参数类型不同
  3. 和函数返回值没有关系
  4. const也可以作为重载天剑
    1
    2
    3
    4
    do(const Teacher &t){
    }
    do(Teacher &t){
    }
  5. 有继承
  6. 子类(派生类)重写定义父类(基类)的同名成员(非virtual函数)
1
2
3
int fun(int a){}
int fun(int a,int b){}
int fun(int a,int b,int c){}

4.2重定义(隐藏父类的成员)

  1. 有继承
  2. 子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
1
2
3
4
5
6
7
8
9
class Base{
public:
void fun(int a){}
void fun(int a,int b){}
}
class Son:public Base{
public:
void fun(参数可以不同){}//重定义
}

4.3 重写(覆盖)

  1. 有继承
  2. 子类(派生类)重写父类(基类)的virtual函数
  3. 函数返回值、函数名、函数参数必须和基类中的虚函数一致
1
2
3
4
5
6
7
8
class Base{
public:
virtual void fun(int a){}
}
class Son:public Base{
public:
virtual void fun(int a){}//参数返回值函数名要完全一致
}