构造函数
关于构造函数,我们耳熟能详,似乎都没有必要成为一个知识点,或者说是重要的知识点拿出来特殊说明,毕竟C++的编译器都能帮我们完成这个工作,只是,事情真的如想象的那么简单么;
可能不是。
本文试图挖掘关于构造函数,可能不是那么简单的一面,当然也不会很全面,权当一起学习了。
构造函数的概念:提供类的对象的初始化的方式,类通过一个或几个特殊的成员函数来控制对象的初始化过程。
有这个概念出发,我们可以知道,所有的构造函数都是在类的对象初始化时由系统调用的,具体调用哪个是按重载函数的调用规则来的。
备注:构造函数不能被声明为const。可以想想为何?
构造函数也不能是虚函数,这个应该好解释。
默认构造函数
这个最简单,在面向对象的世界里,万物皆是对象,因为万物皆需要构造函数,如果我们没有定义一个构造函数,那么就由C++的编译器帮我们完成,在《c++ primer》里叫做合成的默认构造函数。
下面开始我们的编码求学之旅:
首先,定义一个类设计者工具类:
#include <iostream>
using namespace std;
class ClassDesignTool
{
public:
void printSp(){
cout << sp_ << "\n";
}
private:
string *sp_;
};
在这样一个什么没有写构造函数的类里,默认构造函数依然会在编译阶段生成,测试代码如下:
ClassDesignTool tool;
tool.printSp();
在VS2010的编译环境下的结果是CCCCCCCC
,看到这个你应该很熟悉,这是Windows环境下对所有未显式赋值变量的默认赋值,这也就能证明,Windows系统在编译后使用默认合成构造函数,将成员变量sp_赋值为CCCCCCCC
了。
如果你不放心,可以把默认构造函数加上去,
ClassDesignTool(){};
测试的结果是一样的。
这说明,如果你不准备在类的对象初始化时做点什么,完全可以把这件事交给编译器。反之,我们需要做点别的工作了。
覆盖默认构造函数
可能,你认为默认的合成构造函数什么事也没做,对它心有怨恨,所以你决定出马把它改写(覆盖之)。
ClassDesignTool():sp_(new string("lcksfa")){
cout << "use override default constructor " << "\n";
}
//打印函数同时修改
void printSp(){
cout << "sp_ is " << sp_->c_str() << "\n";
}
测试结果:
use override default constructor
sp_ is lcksfa
现在,我们覆盖(override)了默认构造函数,合成的默认构造函数不会被调用,而调用我们自己的构造函数。
构造函数重载
函数重载(overload)的概念,我相信大家都不会陌生,对于构造函数,同样的也能将其重载。和调用普通的重载函数一样,系统会在初始化对象时,根据不同的参数类型去调用不同的重载构造函数:
在上面的代码里添加如下代码:
//overload constructor
ClassDesignTool(const string& str)
:sp_(new string(str)){
std::cout << "use overload constructor " << "\n";
}
以上,我们重载了一个构造函数,其参数为一个const string&类型。
ClassDesignTool tool4(string("4"));
tool4.printSp();
测试结果如下:
use overload constructor
sp_ is 4
这说明,当我们添加了构造函数的重载函数后,使用string("4")参数构造对象时,调用了我们的string参数的构造函数。
拷贝构造函数
上面的东西都很简单,下面,我们说下稍微复杂的。
从函数重载层面,拷贝构造函数也是构造函数的重载,只是其参数为本类的const引用,如下:
//copy constructor
ClassDesignTool(const ClassDesignTool&);
ClassDesignTool::ClassDesignTool(const ClassDesignTool& rhs)
{
std::cout << "use copy constructor from " << rhs.sp_->c_str() << "\n";
sp_ = new string(*(rhs.sp_));
}
什么时候调用?
ClassDesignTool tool("lcksfa");
ClassDesignTool tool2(tool);
tool2.printSp();
测试输出:
use overload constructor
use copy constructor from lcksfa
sp_ is lcksfa
以上代码说明,tool是使用的构造函数初始化,其参数为"lcksfa",而tool2是使用拷贝构造函数初始化,其参数为tool。
析构函数
说完构造函数,说下析构函数。我们知道对象在创建时调用了构造函数,而在销毁时则会调用析构函数。
//destructor
~ClassDesignTool(){
std::cout <<"use destructor "<<sp_->c_str()<<"\n";
delete sp_;
}
以上是析构函数,事实上,我已经把默认的析构函数给覆盖了,原因在于sp_的内存释放,如果使用合成的默认析构函数,系统将不会释放sp__的内存,从而导致内存泄漏。
和构造函数不同,析构函数没有重载函数。这一点和人生很像啊。
执行方式
每一个构造函数都是 由两部分组成的,一个是初始化部分,另一个才是函数体,成员的初始化是在函数体执行之前完成的,所以你的代码里也需要做这两个部分的区分,不要把成员的初始化和函数体混为一体,因为,可能会影响析构函数的执行(只是,没有你想的那么严重)。因为一个析构函数,其也是由函数体和其析构部分组成的,析构时,先执行函数体,再执行销毁操作,成员按构造的初始化列表的逆序销毁。
由析构函数体引起的
如果你需要覆盖重写析构函数体,那么几乎可以肯定你还需要拷贝构造函数和拷贝赋值运算符。
举例子,我在上面的程序中重写了析构函数,因为我需要显示释放sp_的内存,按上面的程序看,还可能出现什么问题呢?毕竟我没有拷贝赋值运算符函数。在测试函数中添加以下代码:
ClassDesignTool tool ;
{
ClassDesignTool tool2("not me");
tool2 = tool;
// tool.printSp();
tool2.printSp();
}
测试输出:
use override default constructor
use overload constructor
sp_ is lcksfa
use destructor lcksfa
use destructor
///奔溃了!!!
使用大括号{}将tool2的赋值部分封起来,确保tool2先析构。
程序输出后,到tool析构处就奔溃了!
原因何在?
因为这里的系统默认的赋值运算是直接将sp_ 的值进行赋值,而没有去拷贝sp_ 指向的内存,tool2离开作用域时调用析构将sp_ delete掉了,等到tool离开作用域时,尝试delete的还是同一块内存,于是就出现了double delete的问题!
赋值操作运算符
这种情况的解决方案之一就是我们自己定义一个赋值操作运算符:
ClassDesignTool&
ClassDesignTool::operator=(const ClassDesignTool& rhs)
{
std::cout << "use copy-assignment operaotr"<<"\n";
auto spNew = new string(*(rhs.sp_));
delete sp_;
sp_ = spNew;
return *this;
}
本函数的写法颇为模式化:
- 将待拷贝的对象拷贝到新内存
- 释放sp_原来指向的内存
- 使用新拷贝的指针值给sp_赋值。
- 最后将 * this的引用返回(可以说凡是期望返回ClassDesignTool& ,最后都是返回 * this)
总结起来就是 综合了析构和构造函数的操作。销毁了左值运算对象的资源,而从右值运算对象中拷贝资源。
小结:本文初略的说明了构造函数、析构函数和拷贝赋值运算符的重载,可以作为入门者的参考。