阐明一下,我用的是gcc7.1.0编译器,规范库源代码也是这个版本的。

本篇文章解说c++中,构造函数的高级用法以及非凡应用状况。

1. 拷贝结构和挪动结构区别

对于拷贝结构和挪动结构,还是看一下这段代码:

#include <iostream>#include <string.h>using namespace std;class CPtr{    private:        char *m_pData;        int m_iSize;    public:        //without param constructors        CPtr()        {            m_iSize = 1024;            m_pData = new char[m_iSize];        }        ~CPtr()        {            if ( m_pData != nullptr )            {                delete []m_pData;                m_pData = nullptr;            }        }        //with param constructors        CPtr(const int p_iSize)        {            m_iSize = p_iSize;            m_pData = new char[p_iSize];        }        //copy constructors        CPtr(const CPtr& ptr)        {            if (ptr.m_pData != nullptr)            {                m_iSize = strlen(ptr.m_pData)+1;                m_pData = new char[m_iSize];                strncpy(m_pData, ptr.m_pData, m_iSize-1);            }        }        //move constructors        CPtr(CPtr&& ptr)        {            m_pData = ptr.m_pData;            m_iSize = ptr.m_iSize;            ptr.m_pData = nullptr;            ptr.m_iSize = 0;        }        //赋值构造函数        CPtr& operator=(const CPtr& ptr)        {            if (ptr.m_pData != nullptr)            {                m_iSize = strlen(ptr.m_pData)+1;                m_pData = new char[m_iSize];                strncpy(m_pData, ptr.m_pData, m_iSize-1);            }            return *this;        }            //挪动赋值构造函数        CPtr& operator=(CPtr&& ptr)        {            m_pData = ptr.m_pData;            m_iSize = ptr.m_iSize;            ptr.m_pData = nullptr;            ptr.m_iSize = 0;            return *this;        }        void setData(const char* str)        {            if (  str == nullptr)            {                cout << "str is nullptr" << endl;                return;            }            if ( m_iSize == 0)            {                cout << "the memory is nothing" << endl;                return;            }            int iSize = strlen(str);            if ( iSize < m_iSize )            {                strncpy(m_pData, str, iSize);            }            else            {                strncpy(m_pData, str, m_iSize-1);            }        }        void print(const char* object)        {            cout << object << "'s data is " << m_pData << endl;        }};int main(){    CPtr p1(1024);    p1.setData("lilei and hanmeimei");    p1.print("p1");    CPtr p2(p1);    p2.print("p2");    CPtr p3 = p1;    p3.print("p3");    CPtr p4(move(p1));    p4.print("p4");    CPtr p5 = move(p2);    p5.print("p5");    return 0;}

依据以上代码,咱们能够总结出如下两点:

  • 拷贝结构从拷贝类型上讲,是属于深拷贝,它会从新申请一块新的内存,并把另外一个对象的内容齐全复制过去,且不会毁坏另外一个对象的内容;
  • 挪动结构从拷贝类型上讲,是属于浅拷贝,依照字面意思,它就是把另外一个对象的内容挪动到以后对象来,至于之前的对象,咱们不确保它还是可用的,挪动结构个别用于对象数据须要保留,而对象则须要抛弃的状况;

2. 构造函数是否能够为虚函数

答案是不能够,看如下代码:

#include <iostream>using namespace std;class CPtr{    private:        char *m_pData;        int m_iSize;    public:        virtual CPtr()        {            m_iSize = 1024;            m_pData = new char[m_iSize];        }        ~CPtr()        {            if ( m_pData != nullptr )            {                delete []m_pData;                m_pData = nullptr;            }        }};int main(){    return 0;}

编译后报错:谬误:constructors cannot be declared ‘virtual’,可见构造函数是不能申明为virtual的,这与虚函数的机制无关,虚函数是寄存在虚表的,而虚表是在构造函数执行实现当前才建设的,构造函数申明为virtual就会陷入到是先有鸡还是先有蛋的难堪地步,所以编译器做了限度。

3. 构造函数是否能够抛出异样

答案是能够,看如下代码:

#include <iostream>using namespace std;class CPtr{    private:        char *m_pData;        int m_iSize;    public:        CPtr()        {            cout << "call constructors" << endl;            m_iSize = 1024;            m_pData = new char[m_iSize];            if ( m_iSize > 0)            {                throw 1024;            }        }        ~CPtr()        {            cout << "call Destructor" << endl;            if ( m_pData != nullptr )            {                delete []m_pData;                m_pData = nullptr;            }        }};int main(){    try    {        CPtr p1;    }    catch(...)    {        cout << "throw something" << endl;    }    return 0;}

编译能够通过,阐明构造函数容许抛出异样,然而这里有个隐含的问题,咱们执行一下程序,后果如下:

call constructorsthrow something

能够看到没有执行析构函数,那如果构造函数在申请动态内存当前抛出异样,就会呈现内存泄露的问题,那么为什么没有执行析构函数呢,因为构造函数没有执行实现,相当于对象都还没有建设,何谈执行虚构函数呢,咱们应该在构造函数抛出异样前,把所有动态内存先开释掉。

代码改为如下:

#include <iostream>using namespace std;class CPtr{    private:        char *m_pData;        int m_iSize;    public:        CPtr()        {            cout << "call constructors" << endl;            m_iSize = 1024;            m_pData = new char[m_iSize];            if ( m_iSize > 0)            {                delete []m_pData;                m_pData = nullptr;                throw 1024;            }        }        ~CPtr()        {            cout << "call Destructor" << endl;            if ( m_pData != nullptr )            {                delete []m_pData;                m_pData = nullptr;            }        }};int main(){    try    {        CPtr p1;    }    catch(...)    {        cout << "throw something" << endl;    }    return 0;}

总结:构造函数能够抛出异样,若有动静分配内存,则要在抛异样之前手动开释。

4. c++11减少的=default和=delete用法

还是先看一段代码:

#include <iostream>using namespace std;class CPtr{    private:        char *m_pData;        int m_iSize;    public:        CPtr()        {            cout << "call constructors" << endl;            m_iSize = 1024;            m_pData = new char[m_iSize];        }        ~CPtr()        {            cout << "call Destructor" << endl;            if ( m_pData != nullptr )            {                delete []m_pData;                m_pData = nullptr;            }        }        CPtr(CPtr &) =delete;        CPtr(CPtr &&) = default;};int main(){    CPtr p1;    CPtr p2(p1);    CPtr p3(move(p1));    return 0;}

编译时报错如下:

test.cpp: 在函数‘int main()’中:test.cpp:32:12: 谬误:应用了被删除的函数‘CPtr::CPtr(CPtr&)’  CPtr p2(p1);

阐明申明为=delete当前不再容许调用,去掉p2的定义,则编译通过,但此时执行的话,还是会报double free的问题,因为p3调用一次析构,p1调用一次析构,就double free啦。

实际上,=delete就相当于以前在private外面申明,即申明为=delete当前则不再容许调用,而申明为=default当前,则通知编译器,你帮我主动生成一下吧,我懒得去实现它了,但联合下面的问题,在存在动态内存的class外面应用挪动结构就要小心了,一不小心就会呈现问题哦,具体挪动结构怎么实现能够参考下面第一点中的代码。

5. 继承时构造函数执行程序

代码为先,如下:

#include <iostream>using namespace std;class CPtr{    private:        char *m_pData;        int m_iSize;    public:        CPtr()        {            cout << "call base constructors" << endl;            m_iSize = 1024;            m_pData = new char[m_iSize];        }        ~CPtr()        {            if ( m_pData != nullptr )            {                delete []m_pData;                m_pData = nullptr;            }        }};class CSon:public CPtr{    public:        CSon()        {            cout << "call son constructors" << endl;        }};int main(){    CSon son;    return 0;}

编译后执行后果如下:

call base constructorscall son constructors

所以对于子类对象而言,是先执行父类构造函数,再执行子类构造函数,那这里再思考一下下面第二点,如果构造函数能够为虚函数,那依据多态规定,父类的构造函数将不会被执行,这也是不成立的。

6. 什么状况下必须应用构造函数初始化列表而不能赋值

有这样一段代码:

#include <iostream>using namespace std;class CPtr{    private:        const int m_iSize;    public:        CPtr()        {            m_iSize = 2;        }};int main(){    return 0;}

咱们猜猜看编译这段代码会报错吗,答案是会报错,报错信息如下:

test.cpp: 在构造函数‘CPtr::CPtr()’中:test.cpp:9:3: 谬误:uninitialized const member in ‘const int’ [-fpermissive]   CPtr()   ^~~~test.cpp:7:13: 附注:‘const int CPtr::m_iSize’ should be initialized   const int m_iSize;             ^~~~~~~test.cpp:11:14: 谬误:向只读成员‘CPtr::m_iSize’赋值    m_iSize = 2;              ^

有两个报错,一个是未初始化常量成员,二个是向只读成员赋值。

实际上,咱们这里首先应该思考一下初始化列表和赋值有什么区别,初始化列表其实相当于调用一次构造函数,而赋值呢,是首先调用一次构造函数,而后再调用赋值函数,相当于先申明,而后又定义一次,但咱们首次接触c++的时候就应该晓得有些类型是必须要申明的时候就有初值的,这里我想到的有以下类型:

  • const申明的变量,必须要有初值;
  • reference援用申明的变量,必须要有初值;
  • 没有默认构造函数但存在有参构造函数的类,它必须初始化的时候给一个入参。

以上三种状况都必须应用初始化列表而不能在构造函数中进行赋值。

7. 什么构造函数会在main函数之前执行

想当年面试的时候我想破头都想不进去这个问题,因为main函数是程序入口嘛,但其实这个问题很简略,依据程序的执行规定,在main函数之前,会先解决全局变量和部分动态变量,那就很清晰了,在main函数执行以前,全局变量和动态变量的构造函数会先执行。

还是用一段代码来佐证:

#include <iostream>using namespace std;class CPtr{    private:        int m_iSize;    public:        CPtr()        {            cout << "call CPtr constructors" << endl;            m_iSize = 2;        }};CPtr ptr;int main(){    static CPtr ptr1;    cout << "exec main() " << endl;    return 0;}

执行后,输入如下:

call CPtr constructorscall CPtr constructorsexec main()

所以答案是全局变量和动态变量的构造函数会在main函数之前执行。

同理,如果发现程序解体,而调试的时候发现还没开始main函数的执行,那么就要检查一下是否有全局变量或者动态变量的构造函数解体了。

8. 怎么避免类对象被拷贝和赋值

避免类对象被拷贝和赋值,换句话说,就是不能调用类的拷贝函数和赋值运算符重载函数,咱们首先能想到的就是把这两个函数申明为private的,或者公有继承一个基类,而到了c++11,又多了一种方法,就是把构造函数加=delete,这里就不给代码了,具体的能够参考下面第4点。

9. 是否能够在构造函数中调用虚函数

答案是能够,首先看这段代码:

#include <iostream>using namespace std;class CPtr{    private:        int m_iSize;    public:        CPtr()        {            cout << "call CPtr constructors" << endl;            m_iSize = 2;            print();        }        virtual void print()        {            cout << "call virtual function" << endl;        }};int main(){    CPtr ptr1;    return 0;}

编译执行后果如下:

call CPtr constructorscall virtual function

对于这个类自身而言,其实是否虚函数没有区别,上面看看如果是继承,子类构造函数中调用虚函数会产生什么:

#include <iostream>using namespace std;class CPtr{    public:        CPtr()        {            cout << "call CPtr constructors" << endl;        }        virtual void print()        {            cout << "call virtual function" << endl;        }};class CSon:public CPtr{    public:        CSon()        {            cout << "call CSon constructors" << endl;             print();        }        virtual void print()        {            cout << "call son virtual function" << endl;        }};int main(){    CPtr* son = new CSon;    delete son;    return 0;}

编译执行后后果如下:

call CPtr constructorscall CSon constructorscall son virtual function

再把子类的print函数正文掉,再次执行,后果如下:

call CPtr constructorscall CSon constructorscall virtual function

也就是说,对于子类而言,在构造函数中调用虚函数也是调用的它本身的函数,而当子类没有实现的时候才调用父类的虚函数,这一幕是不是很相熟,实际上就是产生了多态的成果,通过gdb跟踪CSon的构造函数,输入以后对象的数据,如下:

(gdb) p *this$2 = (CSon) {<CPtr> = {_vptr.CPtr = 0x400dd0 <vtable for CSon+16>}, <No data fields>}

实际上构造函数执行的同时虚表曾经建设了,那虚表既然建设了,必然就会产生多态呀。

综上,不论是基类还是继承类,他们的构造函数中都能够间接调用虚函数。