概述
在C++中创建一个对象时,通常需要做一些数据初始化的工作,因此便提供了一个特殊的成员函数 —— 构造函数。一般情况下,并不需要程序员主动调用构造函数,而是在创建对象时,由系统自动调用。构造函数可以由程序员定义,如果未定义,则编译器会提供默认的构造函数。构造函数没有返回值,也不需要加void类型声明,且其名称必须与类名相同。
默认构造函数
构造函数是可以重载的,我们把没有任何参数的构造函数称为默认构造函数。默认构造函数可以由程序员自己实现,也可以不实现,而交给编译器去提供。但编译器提供的默认构造函数并没有初始化成员变量,成员变量的值很可能是随机的,后续使用会导致各种不可预料的风险和问题。因此,最好由程序员自行实现默认构造函数,并在初始化列表中为每一个成员变量赋初始值。可参看下面的示例代码。
class CBase{public: CBase();private: int m_nNumber;};CBase::CBase() : m_nNumber(66){ NULL;}
当然,也可以不写初始化列表,而在构造函数的函数体中为每一个成员变量赋值(不推荐这种方式)。但如果类中有引用类型、常量类型的成员变量,则必须在初始化列表中进行赋值,不能在函数体内进行赋值。可参看下面的示例代码。
class CBase{public: CBase(bool &bCheck); void Show() { printf("%d\n", m_bCheck); }private: int m_nNumber; const std::string m_strText; bool &m_bCheck;};CBase::CBase(bool &bCheck) : m_nNumber(66), m_strText("CSDN"), m_bCheck(bCheck){ m_strText = "hello"; // 函数体内赋值,编译错误 m_bCheck = false; // 函数体内赋值,编译错误}bool bCheck = true;CBase base(bCheck);base.Show(); // 输出:1bCheck = false;base.Show(); // 输出:0
可以看到,常量成员变量m_strText和引用成员变量m_bCheck必须在初始化列表中赋值。如果在函数体内赋值,则会发生编译错误。还有一点需要注意:成员变量初始化的顺序,不是由初始化列表中的顺序决定的,而是由成员变量声明的顺序决定的。
带参数的构造函数
构造函数可以带一个或多个参数,一般用这些参数去初始化内部的成员变量。可参看下面的示例代码。
class CBase{public: CBase(int nNumber, const std::string &strText);private: int m_nNumber; const std::string m_strText;};CBase::CBase(int nNumber, const std::string &strText) : m_nNumber(nNumber), m_strText(strText){ NULL;}CBase base(66, "CSDN");
拷贝构造函数
创建一个对象时,可以从另一个已经创建好了的对象去复制或拷贝。程序员未提供拷贝构造函数时,编译器会提供一个默认版本的拷贝构造函数。可参看下面的示例代码。
class CBase{public: CBase(int nNumber, const std::string &strText); void Show();private: int m_nNumber; const std::string m_strText;};CBase::CBase(int nNumber, const std::string &strText) : m_nNumber(nNumber), m_strText(strText){ NULL;}void CBase::Show(){ printf("data is %d, %s\n", m_nNumber, m_strText.c_str());}CBase base1(66, "CSDN");CBase base2(base1);base2.Show(); // 输出:data is 66, CSDNCBase base3 = base1;base3.Show(); // 输出:data is 66, CSDN
可以看到,虽然我们没有提供拷贝构造函数,但上面的base2和base3却能够正常创建出来。这是因为编译器提供的默认版本的拷贝构造函数干活了:它会将已有对象的所有成员变量的值赋值给待创建对象的所有成员变量。这是不是意味着,我们就不需要自己实现拷贝构造函数了呢?来看看下面的示例代码。
class CBase{public: CBase(); ~CBase(); void Show();private: char *m_pszText;};CBase::CBase() : m_pszText(NULL){ m_pszText = new char[66]; strcpy(m_pszText, "CSDN");}CBase::~CBase(){ delete[] m_pszText; m_pszText = NULL;}void CBase::Show(){ printf("text is %s, 0x%08X\n", m_pszText, (unsigned int)m_pszText);}{ CBase base1; base1.Show(); // 输出:text is CSDN, 0x00935290 CBase base2 = base1; base2.Show(); // 输出:text is CSDN, 0x00935290}
可以看到,将base1赋值给base2后,两个对象调用Show函数,输出的内容和指针都是一模一样的。这段代码运行后,大概率会导致程序崩溃。这是因为,默认的拷贝构造函数只是单单对成员变量进行了赋值。在上例中,只是将base1的m_pszText直接赋值给了base2的m_pszText。当base1和base2离开作用域范围需要析构时,析构函数中会对各自的m_pszText进行释放操作。此时,对同一个指针释放了两次,第二次释放时,m_pszText其实已经变成了野指针,从而导致崩溃。
解决该问题的方法是:自行实现拷贝构造函数,对指针进行深拷贝,而不是浅拷贝。可参看下面的示例代码。
class CBase{public: CBase(); CBase(const CBase &base); ~CBase(); void Show();private: char *m_pszText;};CBase::CBase() : m_pszText(NULL){ m_pszText = new char[66]; strcpy(m_pszText, "CSDN");}CBase::CBase(const CBase &base){ m_pszText = new char[66]; strcpy(m_pszText, base.m_pszText);}CBase::~CBase(){ delete[] m_pszText; m_pszText = NULL;}void CBase::Show(){ printf("text is %s, 0x%08X\n", m_pszText, (unsigned int)m_pszText);}{ CBase base1; base1.Show(); // 输出:text is CSDN, 0x00EA5290 CBase base2 = base1; base2.Show(); // 输出:text is CSDN, 0x00EA4090}
可以看到,在拷贝构造函数中,我们自己分配了内存,并将内容拷贝了过来。base1和base2调用Show函数后,指针的输出值不一样,也验证了这一点。
转换构造函数
转换构造函数是指只有一个参数,且该参数的类型不同于自身类的类型,通过该参数可以构造对象的构造函数。可参看下面的示例代码。
class CBase{public: CBase(); CBase(int nData); void Show();private: int m_nData;};CBase::CBase() : m_nData(66){ NULL;}CBase::CBase(int nData) : m_nData(nData){ NULL;}void CBase::Show(){ printf("data is %d\n", m_nData);}CBase base = 88;base.Show(); // 输出:data is 88
可以看到,我们直接将88赋值给了base对象,这是因为发生了隐式类型转换,相当于自动执行了下面的代码。
CBase base = CBase(88);
首先调用转换构造函数创建了一个临时的CBase对象,然后调用默认的拷贝构造函数创建base对象。在有的编译器下,会自动优化上述代码,不会创建临时对象,而是直接调用转换构造函数创建base对象。如果不想发生隐式类型转换,可以在转换构造函数前添加explicit关键字。可参看下面的示例代码。
class CBase{public: CBase(); explicit CBase(int nData); void Show();private: int m_nData;};CBase base = 88; // 不能隐式类型转换了,会编译出错base.Show();CBase base2(88); // 正常调用转换构造函数base2.Show();
移动构造函数
在上面介绍拷贝构造函数时,提到了浅拷贝和深拷贝的概念。默认的拷贝构造函数属于浅拷贝,会造成重复释放指针的问题。自己实现的拷贝构造函数可以采用深拷贝,但深拷贝也有缺点:从一个临时的右值对象进行深拷贝时,会导致额外的内存创建和释放的开销。为了解决该问题,在C++ 11中,提出了移动构造函数的概念。所谓移动构造函数,就是将临时的右值对象内部的指针移动过来,新对象抢占指针的所有权,原来的右值对象失去指针的所有权。可参看下面的示例代码。
class CBase{public: CBase(); CBase(CBase &&base); // 移动构造函数 ~CBase();private: char *m_pszData;};CBase::CBase() : m_pszData(NULL){ m_pszData = new char[66]; strcpy(m_pszData, "CSDN"); printf("CBase default constructor: %s\n", m_pszData);}CBase::CBase(CBase &&base) : m_pszData(NULL){ m_pszData = base.m_pszData; base.m_pszData = NULL; printf("CBase move constructor: %s\n", m_pszData);}CBase::~CBase(){ if (m_pszData == NULL) { printf("CBase destructor: NULL\n"); } else { printf("CBase destructor: %s\n", m_pszData); delete[] m_pszData; m_pszData = NULL; }}CBase GetBase(){ CBase base; return base;}CBase base = GetBase();
上述代码执行后的输出如下:
CBase default constructor: CSDNCBase move constructor: CSDNCBase destructor: NULLCBase destructor: CSDN
可以看到,GetBase()函数返回的是一个右值对象,将其赋值给base对象时,正好适用移动构造函数。在CBase的移动构造函数内部,我们将m_pszData指针进行了转移。默认情况下,传入构造函数中的实参如果是左值对象,会调用拷贝构造函数,而不会调用移动构造函数。如果想要左值对象也调用移动构造函数,可以先调用C++ 11中的std::move()函数,将左值对象强制转换成右值对象。