C++ 构造函数、析构函数与虚函数

概要

类中包括成员变量和成员函数,但类中有一种函数比较特殊,函数名与类名相同,且没有返回值类型,这中函数称为构造函数,它承担着类初始化的工作,非常重要。

常用的构造函数有默认构造函数、一般构造函数、拷贝构造函数、转换构造函数、移动构造函数。

1. 默认构造函数

首先要有一个概念,一个类时必有构造函数的,即便程序员没有手动定义构造函数,编译器也会创建一个默认构造函数,具体形式如下

ClassName(){};

这是一个空的函数,即什么都不做。当然,实际上,默认构造函数也不是空的,这里只是简化一下,不作深究。
还值得注意的是,默认构造函数不止有一个,编译器还会创建其他类型的构造函数,比如,拷贝构造函数,这些后边再说。
所以,现在有一个认识就行,那就是如果程序员不手动定义构造函数,编译器会自动定义默认构造函数,即便这个构造函数可能什么都不做。

2.一般构造函数

当程序员手动定义构造函数之后,编译器便不再生成默认构造函数。构造函数一般承担初始化工作,比如对成员变量初始化:

#include <iostream>
using namespace std;
 
class Person {
public:
    int age;
    char* name;
 
    Person(int age, char* name) {
        this->age = age;
        this->name = name;
    }
};
 
int main(){
    Person p(12,"kang");
    cout<<p.age<<endl;
    cout<<p.name<<endl;
}

构造函数支持重载,也就是说,可以有多个同名构造函数,不过他们的形参需要有区别。

#include <iostream>
using namespace std;
 
class Person {
public:
    int age;
    char* name;
 
    Person();
    Person(int age, char* name);
    Person(char* name);
 
};
 
Person::Person() {
    this->age = -1;
    this->name = "-1";
}
 
Person::Person(int age, char* name) {
    this->age = age;
    this->name = name;
}
 
Person::Person(char* name) {
    this->age = -1;
    this->name = name;
}
 
int main(){
    Person p1(12,"kang");
    cout<<p1.age<<"  "<<p1.name<<endl;
 
    Person p2;
    cout<<p2.age<<"  "<<p2.name<<endl;
 
    Person p3("kang");
    cout<<p3.age<<"  "<<p3.name<<endl;
}
 

3. 拷贝构造函数

类在实例化的时候,有两步操作:

给对象分配内存,此时内存没有经过初始化,成员变量大多都是垃圾值;
对内存进行初始化,也就是调用构造函数对成员变量进行初始化。
对于一般构造函数,初始化的值来自于构造函数的输入参数,如果某个构造函数的输入参数是同类的对象,则称该构造函数为拷贝构造函数。

说白了,拷贝构造函数就是用别的对象来初始化新对象的内存

#include <iostream>
using namespace std;
 
class Person {
public:
    int age;
    char* name;
 
    Person(int age, char* name);  //一般构造函数
    Person(const Person &p);  //拷贝构造函数  
 
};
 
Person::Person(int age, char* name) {
    this->age = age;
    this->name = name;
}
 
Person::Person(const Person &p) {
    this->age = p.age;
    this->name = p.name;
}
 
int main(){
    Person p1(12,"kang");   //调用一般构造函数
    Person p3(14,"wang");
    Person p2 = p1;      //调用拷贝构造函数,等效于 Person p2(p1);
    cout<<p1.age<<"  "<<p1.name<<endl;
    cout<<p2.age<<"  "<<p2.name<<endl;
 
    p2 = p3;
}

结果:12 kang 12kang
注意! Person p2 = p1 语句不是赋值,是在创建对象,因此要调用拷贝构造函数,而p2=p3 是对象的赋值,调用的是运算符“=”的重载函数。所以要注意,这俩东西调用是不一样的,类似与变量创建和赋值的区别。

如果程序员没有手动创建拷贝构造函数,编译器会自动创建一个默认拷贝构造函数,如果手动创建了,编译器就不会再创建了。默认拷贝构造函数很简单,就是用老对象”的成员变量对“新对象”的成员变量进行一一赋值,和上面 Person 类的拷贝构造函数非常类似。所以,上边那个例子,不定义拷贝构造函数,程序也是没问题的,因为编译器帮我们创建了一个默认构造函数。

可能会有疑问,既然编译器自己会创建拷贝构造函数,那为什么还需要用户手动创建呢?答案是,默认构造函数里边只是简单的浅拷贝,碰到指针就G了。对于指针指向的数据,如果用默认构造函数,那么“新对象”的指针跟“老对象”的指针指的是同一块内存,”老对象“的修改会影响到“新对象”,所以这时候就需要用户手动创建拷贝构造函数,使用深拷贝创建对象。

4.转换构造参数

对于类型转换,大家可能并不陌生,C++自带很多类型转换规则,比如int转double,(int*)转(float*),以及向上型转等等,除了这些自带的转换规则之外,用户还可以自定义转换规则,转换构造函数就是将其他类型转化为当前类。

借助前边的例子,如果我的对象创建语句是这么写的:

Person p = "Kang";

这个代码是编译不过的,因为Person类没有对应的构造函数,但是如果定义一个转换构造函数,自定义一下转换规则,那么这条语句就是可行的,看一个详细的例子:

#include <iostream>
#include <string.h>
using namespace std;
 
 
class Person {
public:
    int age;
    char* name;
 
    Person(int age, char* name);  //一般构造函数
    Person(char* name);  //转换构造函数
 
};
 
Person::Person(int age, char* name) {
    this->age = age;
    this->name = name;
}
 
Person::Person(char* name) {  //转换构造函数
    this->name = name;
    this->age = 0;
}
 
 
int main(){
    Person p1(12,"kang");   //调用一般构造函数
    Person p2 = "Tom";      //调用转换构造函数,等效于 Person p2(“Tom”);
    cout<<p1.age<<"  "<<p1.name<<endl;
    cout<<p2.age<<"  "<<p2.name<<endl;
 
    p2 = "Jack";  //调用转换构造函数,因为=右边不是Person类型,所以不会调用运算符=的重载函数
    cout<<p2.age<<"  "<<p2.name<<endl;
}

5.移动构造参数

拷贝构造函数可以用其他对象初始化新对象,但是,如果成员指针变量指向的数据量很大,再进行深拷贝,那么拷贝函数的时间开销是巨大的。而移动构造函数的诞生,就是为了解决拷贝构造函数时间开销大的问题。

借助一个例子来说明移动构造函数是如何工作的:

Person p = Person();

这条语句会先调用一般构造函数或者默认构造函数(取决于有没有手动定义一般构造函数),创建一个匿名对象;再调用拷贝构造函数。如果Person成员指针变量指向的数据量很大,那么时间开销是很大的。

移动构造函数的优化思路是:既然一般构造函数创建出来的是一个匿名对象,我们不妨把新对象的指针直接指向匿名对象指向的空间,说简单一些就是,指针简单拷贝,然后将匿名对象的指针置空,因为匿名对象是无法被用户调用的,如此一来,匿名对象成员指针指向的数据都归新对象的指针变量了。有点鸠占鹊巢的意思==>

移动构造函数的声明形式如下:

ClassName(CalssNmae &&obj);

可以看到,为了能够引用匿名对象,移动构造函数的输入是右值引用。

光说不直观,看一个例子:

#include <iostream>
#include <string.h>
#include <chrono>
using namespace std;
 
class demo{
public:
    demo(){               //一般构造函数
        num = new int[100000000];
        cout<<"constructor"<<endl;
    }
    demo(const demo& de) {   //拷贝构造函数
        num = new int[100000000];
        memcpy(this->num, de.num, 100000000);
        cout<<"copy constructor"<<endl;
    }
//    demo(demo &&de) {     //移动构造函数
//        num = de.num;
//        de.num = NULL;
//        cout<<"move constructor"<<endl;
//    }
private:
   int *num;
};
 
demo get_demo(){
    return demo();
}
 
int main(){
 
    auto start = std::chrono::system_clock::now();
    demo a = demo();
    auto finish = std::chrono::system_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(finish-start);
    std::cout<<double(duration.count())<< "ms" <<std::endl;
 
    return 0;
}
 

这个代码不能用IDE去编译运行,因为IDE会进行优化,我们就无法观察程序原本的运行轨迹了。

在Linux系统下,找到cpp所在文件夹,打开终端,输入:

g++ main.cpp  -fno-elide-constructors -o main

其中,g++是选择的编译器,main.cpp是源文件,-fno-elide-constructors是禁用构造函数优化。然后,当前目录下会生成一个名为main的可执行程序,运行便可得到程序输出。
运行时间为35ms
打开移动构造参数注释行,
运行时间为0ms
可以看到,没有了大块的内存拷贝,运行时间已经不在ms量级了(大约0.1ms),这样就大大降低了拷贝构造函数的时间开销。

从这个代码也能看出来:

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。


析构函数

主要作用:在于对象销毁前系统会自动调用,进行一些清理工作。【收回创建对象时申请的空间】
格式:~类名(){函数体} — ~Person(){…}
(1)析构函数无返回值也不写void,并且没有函数类型;
(2)析构函数的函数名称要与类名相同;
(3)析构函数没有参数列表,不可以进行函数重载;【构造函数有参数列表,并且可以进行重载】
(4)析构函数在对象销毁时会自动调用,不需要进行手动调用,并且只调用一次。
析构函数调用规则:先构造的后析构,后构造的先析构
用例:

#include <iostream>
#include <string>
#include <stdlib.h>
 
using namespace std;
class Person {
public:
	//有参构造函数
	Person(int a, int b, int q)
	{
		Age = a;
		Data = b;
		p = (int*)malloc(sizeof(int));
		*p = q;
		cout << "调用了有参构造函数!!!" << endl;
	}
	//用户提供的拷贝构造函数
	Person(const Person& c)
	{
		Age = c.Age;
		Data = c.Age;
		p = (int*)malloc(sizeof(int));
		*p = *(c.p);
		cout << "调用了用户提供的拷贝构造函数!!!" << endl;
	}
	//析构函数
	~Person()
	{
		free(p);
		cout << "调用了析构函数!!!" << endl;
	}
	int Age;
	int Data;
	int* p;
};
int main()
{
	Person a(10, 20, 30);
	cout << "----------------------------" << endl;
	cout << "a中p的值:" << *(a.p) << endl;
	cout << "a中p的地址:" << a.p << endl;
	cout << "----------------------------" << endl;
	Person b = a; //使用隐式转化法调用拷贝构造函数【深拷贝】
	return 0;
}

在这里插入图片描述

虚函数

在实现c++多态时会用到虚函数。虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名(纯虚函数),为了提高程序的可读性,建议虚函数都加上virtual(override)关键字。一般格式:

class base
{
public:
 base();
 virtual void test(); //定义的一个虚函数
private:
 char *basePStr;
};

上述代码在基类中定义了一个test的虚函数,所有可以在其子类重新定义父类的做法这种行为成为覆盖(override),或者为重写。
常见用法:声明基类指针,利用指针指向任意一个子类对象,调用相关的虚函数,动态绑定,由于编写代码时不能确定被调用的是基类函数还是那个派生类函数,所以被称为“”虚“”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。

 
#include<iostream>  
using namespace std;  
  
class A  
{  
public:  
    void foo()  
    {  
        printf("1\n");  
    }  
    virtual void fun()  
    {  
        printf("2\n");  
    }  
};  
class B : public A  
{  
public:  
    void foo()  //隐藏:派生类的函数屏蔽了与其同名的基类函数
    {  
        printf("3\n");  
    }  
    void fun()  //多态、覆盖
    {  
        printf("4\n");  
    }  
};  
int main(void)  
{  
    A a;  
    B b;  
    A *p = &a;  //p为A型指针
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;  //A型指针指向B型对象
    p->foo();  //非虚成员函数取决于指针类型,输出1
    p->fun();  //虚成员函数取决于对象类型,输出4,体现了多态
    return 0;  
}

C++ 11 增加了两个继承控制关键字:override 和 final,两者的作用分别为:
override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名;
加了override,明确表示派生类的这个虚函数是重写基类的
final:阻止类的进一步派生 和 虚函数的进一步重写。
如果不希望某个类被继承,或不希望某个虚函数被重写,则可以在类名和虚函数后加上 final 关键字,加上 final 关键字后,再被继承或重写,编译器就会报错。

虚函数之vptr(虚指针)和vtbl(虚表)

类在继承中的内存是如何实现管理的?
在类的继承中,每一个class产生一堆指向virtual function的指针,放在vtbl(虚表)中。对于每一个class object 被添加了一个指针,指向相关的virtual table,这里指针称为vptr(虚指针)。
在这里插入图片描述
下面我们结合上图对vptr和vtbl做进一步的讲解:

图中定义了三个类,分别为class A、class B、class C;其中class A 是class B的基类,class B 是class C的基类。

class A 的 公共成员中有四个函数接口,分别为虚函数 virtual void vfunc1(); virtual void vfunc2();普通成员函数void func1();void func2()。class A中有两个私有成员,分别为m_data1和m_data2。
class B 继承了class A ,但是class B中的虚函数virtual void vfunc1()覆盖了基类的同名虚函数;此外class B的公共成员中还有一个属于自己的函数 void func2(),注意,这里面的函数func2虽然与class A中的函数func2同名,但是他们是互补相关的两个函数,也不存在谁覆盖谁,因为他们并没有将该同名函数申明为virture function(虚函数),所以class B的对象在调用void func2()的时候,它只能调用到class B自身公共成员函数中的void func2()函数,而无法调用到class A 中的void func2()函数。 对由于class B中的私有成员只有一个m_data3。
class C继承了class B,但是但是class C中的虚函数virtual void vfunc1()覆盖了基类class B中的同名虚函数;此外存在一个公共成员函数:void func2(),其调用原理,参考上一段文字(在此处就不做详细说明了)。

现在我们知道了A、B、C这三个类的继承关系,以及各自所拥有的公有成员、私有成员以及各自的virtual function 。那么我们接下来就来谈谈,各自对象的虚指针和虚表。

从图的左上角可以看到,当class中有虚函数时,那么他所创建的对象将会多出一个指针(如图中的黑点所示为一个指针),这也是为什么类中有虚函数比类中没有虚函数在进行对象所占字节的测试时,会多出4个字节,而这多出的四个字节其实就是vptr(虚指针);在图中虚指针对应的地址为0x409004,对于对象a的成分除了包括一个vptr(虚指针)外,还包函两个数据成员m_data1、m_data2.&EMSP;对象a通过虚指针指向虚表(A vtbl),表中放的都是函数指针,指向虚函数。从class A中可以看出class A有两个虚函数,所以vtbl中有两个虚指针,分别指向对应的虚函数。(注意同种相同颜色的框框)。

此处补充一下:对于图中给出的三个类(class A,class B,class C)他们一共有8个函数。对应的四个普通成员函数和四个虚函数,必须要明白的一点是,如果基类中有虚函数,那么子类中一定有虚函数,因为子类继承了基类的成份。并且虚指针只能调用虚函数,而不能调用普通成员函数。

对于B的对象b,因为class B继承了class A,而class A中有虚函数,那么刚才我们说了继承时,由于子类继承了基类的成分,所以对象b一定也有一个虚指针,指向对应的虚函数。而对象b中由于是继承了基类A,所以对象b的成分按照顺序将会包含:一个虚指针,对象A的m_data1,m_data2(基基类的成份),然后才到自身的m_data3。

对于C的对象c,他的成分同样是按顺序包括:一个虚指针,类A的m_data1,m_data2,类B的m_data3,和自身的m_data1、m_data4.

由于是虚指针,各自的虚指针(vptr)只会指向各自对象对应的虚表( vtbl ) ,对于class A的对象a的虚指针指向的虚表有两个虚函数分别为A::vfunc1()和A::vfunc2()。这个比较好理解。

那么对于class B的对象b,它的虚指针呢。因为class B是class A的子类,子类将会继承基类的成分,而基类有两个虚函数vfunc1()和vfunc2(),这两个虚函数都属于基类的成分,所以继承时都需要继承,但是class B的虚函数virtual void vfunc1()将基类的同名虚函数覆盖掉了,那么实际上对象b只有两个虚函数,分别为自身的虚函数vfunc1(),和来自基类继承的虚函数vfun2()。

对于class C的对象c而言,其原理可以参考对象b,但是需要说明的一点是,由于class C继承了class B,而class B 又继承了class A,虽然class B中没有写出虚函数vfunc2(),但是实际上class B中时包含了class A的的虚函数vfunc2()的成分的。又因为class C,继承了class B,这时候我们从上面的图中左下角可以看到,class C的对象中其实上是包含了class B从class A中继承过来的成分。所以此时对于class C的对象c来说,它也是包含了两个虚函数的,分别为自身的虚函数vfunc1(),和A::vfunc2()。

对于每个类对应的对象的vptr通过vtbl调用的虚函数如图中第二列和第三列之间的箭头所示(在同种可以通过图中相同的颜色进行区分)。
(非虚函数的地址位于公共代码区,对象所占的空间放在对象生成的地方(全局变量区或堆、栈区) 不论是否用inline声明(或默认为inline),成员函数的代码段都不占用对象的存储空间。用inline声明的作用是在调用该函数时,将函数的代码段复制插入到函数调用点,而若不用inline声明,在调用该函数时,流程转去函数代码段的入口地址,在执行完该函数代码段后,流程返回函数调用点。inline与成员函数是否占用对象的存储空间无关,它们不属于同一个问題,不应搞混。 放在公共代码区的成员函数,其形参为隐形“this”指针,以便找到自身对象
对于图中的(*(P->vptr)n)在图的左下角,有一个P,那么这行代码的实际意思是:通过指针找出它的虚指针,再找到它的虚表,第n个,把他当成函数指针来调用,由于是通过P来调用,所以P就是this point,所以括号里面的P就是this point。

 
#include<iostream>
using namespace std;
 
class A {
public:
	virtual void vfunc1() { cout << "A::vfunc1()" << endl; };
	virtual void vfunc2() { cout << "A::vfunc2()" << endl; };
	void func1() { cout << "A::func1()" << endl; };
	void func2() { cout << "A::func2()" << endl; };
private:
	int data1_;
	int data2_;
};
 
class B :public A {
public:
	virtual void vfunc1() override { cout << "B::vfunc1()" << endl; };
	void func2() { cout << "B::func2()" << endl; };
private:
	int data3_;
};
 
class C :public B {
public:
	virtual void vfunc1() override { cout << "C::vfunc1()" << endl; };
	void func2() { cout << "C::func2()" << endl; };
private:
	int data1_, data4_;
};
 
//演示了手动调用虚函数的过程
int main() {
	B a;
	typedef void(*Fun)(void);
	Fun pFun = nullptr;
	cout << "虚函数表地址:" << (int*)(&a) << endl;
	cout << "虚函数表第1个函数地址:"<<(int*)*(int*)(&a) << endl;
	cout << "虚函数表第2个函数地址:" << (int*)*(int*)(&a) + 1 << endl;
	pFun = (Fun)*((int*)*(int*)(&a));
	pFun();
	pFun = (Fun)*((int*)*(int*)(&a) + 1);
	pFun();
	return 0;
}

第一个pFun通过虚函数表调用了class b的 vfun1
第二个pFun通过虚函数表调用了class a的vfun2

  • 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:
virtual void funtion1()=0

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

  • 包含纯虚函数的类称为抽象类。

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

抽象类的作用

抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

使用抽象类时注意:

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
抽象类是不能定义对象的。

构造函数、析构函数与虚函数的关系

首先回答:
构造函数不能为虚函数,而析构函数可以且常常是虚函数
(1)构造函数不能为虚函数
涉及到C++对象的构造问题,C++对象在三个地方构建:(1)函数堆栈;(2)自由存储区,或称之为堆;(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
(2)析构函数可以是虚函数,且常常如此
因为此时 vtable 已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。

C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
如:

#include <iostream>
using namespace std;

//基类
class Base{
public:
    Base();
    ~Base();
protected:
    char *str;
};
Base::Base(){
    str = new char[100];
    cout<<"Base constructor"<<endl;
}
Base::~Base(){
    delete[] str;
    cout<<"Base destructor"<<endl;
}

//派生类
class Derived: public Base{
public:
    Derived();
    ~Derived();
private:
    char *name;
};
Derived::Derived(){
    name = new char[100];
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    delete[] name;
    cout<<"Derived destructor"<<endl;
}

int main(){
   Base *pb = new Derived();
   delete pb;
   cout<<"-------------------"<<endl;
   Derived *pd = new Derived();
   delete pd;
   return 0;
}
运行结果:
Base constructor
Derived constructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor

本例中定义了两个类,基类 Base 和派生类 Derived,它们都有自己的构造函数和析构函数。在构造函数中,会分配 100 个 char 类型的内存空间;在析构函数中,会把这些内存释放掉。

pb、pd 分别是基类指针和派生类指针,它们都指向派生类对象,最后使用 delete 销毁 pb、pd 所指向的对象。

从运行结果可以看出,语句delete pb;只调用了基类的析构函数,没有调用派生类的析构函数;而语句delete pd;同时调用了派生类和基类的析构函数。

在本例中,不调用派生类的析构函数会导致 name 指向的 100 个 char 类型的内存空间得不到释放;除非程序运行结束由操作系统回收,否则就再也没有机会释放这些内存。这是典型的内存泄露。

(1) 为什么delete pb;不会调用派生类的析构函数呢?

因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数;也就是说,指针指向哪个类就调用哪个类的函数,这在前面的章节中已经多次强调过。pb 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。

(2) 为什么delete pd;会同时调用派生类和基类的析构函数呢?

pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的,这在《2.8 C++析构函数详解[1]》一节中已经讲到了。

更改为虚析构函数后

class Base{
public:
    Base();
    virtual ~Base();
protected:
    char *str;
};
运行结果:
Base constructor
Derived constructor
Derived destructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor

将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;也就是说,指针指向哪个类的对象就调用哪个类的函数。pb、pd 都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。如此一来也就解决了内存泄露的问题。

所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

持续更新中,个人理解成分居多,有问题欢迎讨论

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值