经典问题解析四(四十六)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了经典问题解析四(四十六)相关的知识,希望对你有一定的参考价值。

        我们在学习了 C++ 之后,对于动态内存分配便有了两种方式。new 和 malloc 的区别是什么呢?delete 和 free 又有何区别?new 关键字与 malloc 函数的区别可以从以下几个方面说:1、new 关键字是 C++ 的一部分,malloc 是由 C 库提供的函数;2、new 是以具体类型为单位进行内存分配,malloc 以字节为单位进行内存分配;3、new 在申请内存空间时可进行初始化,malloc 进根据需要申请定量的内存空间。

        下来我们以代码为例来进行说明

#include <iostream>
#include <string>
#include <cstdlib>

using namespace std;

class Test
{
    int* mp;
public:
    Test()
    {
        cout << "Test::Test()" << endl;
        
        mp = new int(100);
        
        cout << *mp << endl;
    }
    
    ~Test()
    {
        delete mp;
        
        cout << "~Test::Test()" << endl;
    }
};

int main()
{
    Test* pn = new Test;
    Test* pm = (Test*)malloc(sizeof(Test));
    
    delete pn;
    free(pm);
    
    return 0;
}

        我们在 main 函数中分别用 new 和 malloc 的方式来创建 Test 类。new 方式是生成了一个对象,它则会调用构造函数,而 malloc 则是只申请了 Test 类大小的空间,并没有生成对象,所以并不会去调用构造函数。接着我们使用了 delete 和 free 两种方式分别对他们进行释放。同理,delete 会进行析构函数,free 只是释放空间。我们来看看编译结果

技术分享图片

        如果我们去掉头文件 cstdlib 呢?编译看看结果

技术分享图片

        由此也证明了 new 是 C++ 语言的一部分,是一个关键字。而 malloc 只是 C 库的一个函数。下来我们试试用 new 生成对象 pn,而用 free 来释放 pn。看看会是什么结果

技术分享图片

        我们看到编译正常通过,但只是调用构造函数生成了对象,并没有去调用析构函数去销毁对象,因此会造成内存泄漏。再来试试用 malloc 方式申请空间,用 delete 方式去释放 pm。

技术分享图片

        编译也是正常通过,但是它只进行了析构,这样如果项目是长时间运行的话,便会造成莫名其妙的 bug。所以我们决不能在 C++ 编程中进行混合使用。那么通过上面的实验,我们可知:new 在所有的 C++ 编译器中都被支持,malloc 在某些系统开发中是不能调用的;new 能够触发构造函数的调用,malloc 进分配需要的内存空间;对象的创建只能使用 new,malloc 是不适合面向对象开发的。同理,delete 和 free 也是一样的:delete 在所有的 C++ 编译器中都被支持,free 在某些系统开发中是不能调用;delete 能够触发析构函数的调用,free 进归还之前分配的内存空间;对象的销毁只能使用 delete,free 不适合面向对象开发

        接下来是关于虚函数的,我们来想下,构造函数是否可以成为虚函数?析构函数又是否可以成为虚函数呢?构造函数是不可能成为虚函数的,因为在构造函数执行结束后,虚函数表指针才会被正确的初始化。而析构函数则可以成为虚函数,建议在设计类时将析构函数声明为虚函数。

        下来我们还是以代码为例进行分析

#include <iostream>
#include <string>

using namespace std;

class Base
{
public:
    Base()
    {
        cout << "Base()" << endl;
        
        func();
    }
    
    virtual void func()
    {
        cout << "Base::func()" << endl;
    }
    
    ~Base()
    {
        func();
        
        cout << "~Base()" << endl;
    }
};

class Derived : public Base
{
public:
    Derived()
    {
        cout << "Derived()" << endl;
        
        func();
    }
    
    virtual void func()
    {
        cout << "Derived::func()" << endl;
    }
    
    ~Derived()
    {
        func();
        
        cout << "~Derived()" << endl;
    }
};

int main()
{
    Base* p = new Derived;
    
    cout << endl;
    
    delete p;
    
    return 0;
}

        我们先来将构造函数声明为虚函数,看看编译是否会通过?

技术分享图片

        我们看到编译直接报错了,那么我们在来试试将析构函数声明为虚函数呢?

技术分享图片

        我们看到编译是通过的,也就证明了我们前面说的是对的。我们先将父类中析构函数的虚函数声明去掉,看看在 main 函数中用父类指针 p 来生成子类对象,根据赋值兼容性原则,这肯定是能通过的。它在执行构造函数时,必然先执行父类构造函数,进而调用父类中的 func 函数。再来执行子类的构造函数,进而调用子类中的 func 函数。在进行 delete p 时,先发生子类对象的析构,在析构前会先调用子类中的 func 函数。在去执行父类中的析构函数,进而调用父类中的 func 函数。我们来编译看看结果是否如此

技术分享图片

        结果跟我们分析的不一样哈,它在 delete 的时候并没有执行子类的析构函数。为什呢?因为父类中的析构函数没有进行虚函数的声明,所以当编译器执行到这的时候,它不会进行判断当前对象类型是什么,只是会根据指针的类型来进行析构。因此我们这进行 delete 的时候只会进行父类的析构,下来我们在父类中的析构函数前加上 virtual 关键字再来试试

技术分享图片

        我们看到已经是我们所想要的结果了,因此,在以后的类设计中,我们都建议将析构函数声明为虚函数。

        关于虚函数在多态这,在构造函数中是不可能发生多态行为的,因为在构造函数执行时,虚函数表指针未被正确初始化;析构函数中也不可能发生多态行为,因为在析构函数执行时,虚函数表指针已经被销毁了。所以构造函数和析构函数中不能发生多态行为,只调用当前类中定义的函数版本!

        我们之前学习了在继承中的强制类型转换的关键字是 dynamic_cast,而 dynamic_cast 要求相关类中必须有虚函数,它用于有直接或者间接继承关系的指针(引用)之间。当用于指针时:转换成功将得到目标类型的指针;转换失败的话将得到的是一个空指针。当用于引用时:转换成功将得到目标类型的引用;转换失败的话将得到一个异常操作信息编译器则会去检查 dynamic_cast 的使用是否正确,类型转换的结果只可能在运行阶段才能得

        下来我们还是以代码为例来进行分析

#include <iostream>
#include <string>

using namespace std;

class Base
{
public:
    Base()
    {
        cout << "Base()" << endl;
    }

    ~Base()
    {
        cout << "~Base()" << endl;
    }
};


class Derived : public Base
{

};

int main()
{
    Base* p = new Derived;
    
    Derived* pd = dynamic_cast<Derived*>(p);
    
    if( pd != NULL )
    {
        cout << "pd = " << pd << endl;
    }
    else
    {
        cout << "Cast error!" << endl;
    }
    
    delete p;
    
    return 0;
}

        我们之前说过,dynamic_cast 要求相关类中必须有虚函数,而我们的父类 Base 中并没有,看看编译是否可以通过?技术分享图片

        编译器报错了,那么我们是否还要去专门在父类中定义一个虚函数呢?其实并不需要,将析构函数声明为虚函数就行了。我们接下来是用子类对象指针 pd 去转换父类指针 p(其本质还是子类对象),那么指针 pd 应该不会为空。我们来看看编译结果呢

技术分享图片

        我们看到编译已经通过,并且如我们所愿。那么我们如果将父类指针 p 用于生成一个父类对象,再用子类对象指针 pd 去转换父类对象指针 p 呢(此时目标类型是不相同的)?看看还会转换成功吗?

技术分享图片

        我们看到转换是失败的,此时指针 pd 为空,所以 dynamic_cast 关键字在 C++ 中的地位还是蛮高的。通过对一些经典问题的探讨,总结如下:1、new / delete 会触发构造函数或者析构函数的调用;2、构造函数不能成为虚函数,析构函数可以成为虚函数;3、构造函数和析构函数中都无法产生多态行为;4、dynamic_cast 是与继承相关的专用转换关键字。


        欢迎大家一起来学习 C++ 语言,可以加我QQ:243343083

以上是关于经典问题解析四(四十六)的主要内容,如果未能解决你的问题,请参考以下文章

聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源代码分析

ABP源码分析四十六:ABP ZERO中的Ldap模块

分布式技术追踪 2017年第四十六期

#yyds干货盘点# js学习笔记四十六实现两栏布局的第二种方式

ArcGIS实验教程——实验四十六:地图概括功能实验教程

练习四十六:列表排序,删除list中重复的元素