经典问题解析四(四十六)
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源代码分析