本博客参照林锐《高质量程序设计指南C++/C语言》(第三版)第十三章完成
在C++中,每个类都有三种成员函数——构造函数、析构函数和赋值函数(ps:构造函数有构造函数和拷贝构造函数两种)。对于任意一个类A,如果不显式地声明定义以上函数,编译器会自动为A生成4个默认函数,如下:
A(); //默认构造函数
A(const A&); //默认拷贝函数
~A(); //析构函数
A& operator = (const A& a); //默认赋值函数1234
1. 构造函数和析构函数
一个类中只有一个析构函数,不能被重载,无参数,无返回值。
一个类中可以有多个构造函数,能被重载,有参数,无返回值。
对象的初始化工作放在构造函数中,清除工作放在析构函数中。创建对象时,构造函数被自动执行;当销毁对象时,析构函数被自动执行。
不要在构造函数内做与初始化对象无关的工作,不要在析构函数内做与销毁一个对象无关的工作,这样会降低效率甚至降低代码可读性。
构造函数和析构函数的名字规定:要与类同名,不可随意更改。因两者功能相反,析构函数加了前缀“~”来区分两者(有“取反”之意)。
C++对象可以用构造函数初始化,构造函数是任何对象创建时自动调用的第一个成员函数,它也只能被每个对象调用一次。
最好为每个类显式地定义构造函数和析构函数,即使它们暂时空着,尤其是当含有指针成员或引用成员的时候。
析构函数误区
类String的构造函数和析构函数实现示例:
class String {
public:
String(const char *str = ""); // 默认构造函数
String(const String& copy); // 拷贝构造函数
~String(); // 析构函数
String& operator = (const String& assign); // 赋值函数
private:
size_t m_size; // 当前当前长度
char *m_data; // 指向字符串的指针
};
// 构造函数
String::String(const char *str)
{
if (NULL == str) {
m_data = new char[1];
*m_data = '\0';
m_size = 0;
}else {
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
m_size = length;
}
}
// 析构函数
String::~String()
{
delete[]m_data;
}
1. 构造函数的成员初始化列表
开始时我们一般都在构造函数体内来初始化数据成员,但这并不是真正意义上的初始化,而是赋值。但因为构造函数是创建一个对象时自动调用的第一个成员函数,所以我们就把它体内的赋值语句当成初始化来看待。
真正的初始化是使用“初始化列表”来进行的。初始化列表位于构造函数参数列表后,在函数体{}之前。这说明该列表里的初始化工作发生在函数体内的任何代码被执行之前,大部分时候构造函数既可以使用初始化列表又能在函数体内赋值初始化,但当类成员变量属于以下三者之一时,必须使用初始化列表。
非静态 const 数据成员。
引用成员。
没有默认构造函数的类对象。
ps:在继承关系中,子类必须在初始化列表中调用父类的构造函数。
默认构造函数(default constructor)就是在没有显式提供初始化式时调用的构造函数(编译器自动生成的)。它由不带参数的构造函数,或者为所有的形参提供默认实参(全缺省)的构造函数定义。
初始化列表以一个冒号开始,接着一个逗号分隔数据列表,每个数据成员都放在一个括号中进行初始化。尽量使用初始化列表进行初始化,因为它更高效。
class A
{
public:
A() //构造函数
{
_a1 = 0;
_a2 = 0;
}
A(const A& a) //拷贝构造函数
{
_a1 = a._a1;
_a2 = a._a2;
}
private:
int _a1;
int _a2;
};
class B
{
public:
// (1)函数体内赋值方式初始化
B(int b1, int b2, const A& a)
{
_b1 = b1;
_b2 = b2;
_a = a;
}
// (2)初始化列表的方式初始化
B(int b1, int b2, const A& a)
: _b1(b1)
, _b2(b2)
, _a(a)
{}
private:
int _b1;
int _b2;
A _a;
};
void Test()
{
A a;
B b(1, 1, a);
}
方式(1),类B的构造函数再函数体内用赋值的方式将成员对象_a初始化。这里看起来只有一条赋值语句,但实际上B的构造函数做了两件事:先暗地里创建 _a 对象(调用了A的构造函数),再调用类A的赋值函数,才将参数 a 赋值给 _a。(上述只针对自定义类型)
方式(2),类B的构造函数在其初始化列表里调用了类A的拷贝构造函数,从而将成员对象 _a 初始化。
成员变量按声明顺序依次初始化,而非初始化列表出现的顺序。
< code 示例>
class A
{
public:
A(int n)
: _a2(n)
, _a1(_a2)
{}
private:
int _a1;
int _a2;
}
int main()
{
A a(1);
return 0;
}123456789101112131415161718
上面代码并不能如愿给 a 初始化。因为成员变量按声明顺序依次初始化,而非初始化列表出现的顺序。这里是先初始化 _a1 ,再初始化 _a2 ,而我们的初始化列表里, _a1 是使用 _a2 的值来初始化的,但此时 _a2还没有被初始化。
2. 拷贝构造函数
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
拷贝构造函数是这样一个函数:第一个参数为本类对象的引用、const引用、volatile引用或const volatile引用,并且无其他参数,或者其他参数都有默认值。
不可同时定义一个无参构造函数和一个参数全部都有默认值的构造函数,会造成二义性。
拷贝构造函数的参数必须是同类对象的引用,而不能是对象值。
示例:
class A
{
public:
A(A copy){...} //(1)
A(const A& other){...} //(2)
};123456
C++中不可能允许定义(1)这种值传递的拷贝构造函数,原因如下:
这个拷贝构造函数在传参过程中要调用拷贝构造函数本身,因为是“值传递”,而调用拷贝构造函数时要先进行参数传递,参数传递又要调用拷贝构造函数……这样就陷入了不停分配堆栈的无限递归中。
何时调用拷贝函数:
一个对象以值传递的方式传入函数体。
一个对象以值传递的方式从函数返回。
一个对象需要通过另一个对象进行初始化。
何时编译器会生成默认的拷贝构造函数:
如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不再生成。
如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。
深浅拷贝问题
先前说到如果程序员不显式地声明和定义这四个成员函数,C++编译器会默认生成。但是编译器生成的默认拷贝构造函数的工作方式是内存拷贝,也就是浅拷贝。如果此时类中含有指针成员或引用成员,那么就会出现问题。
浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的两个指针指向两个不同的地址空间。
为了避免此类错误,我们最好还是别偷懒,自己手动用深拷贝方式实现拷贝构造函数:
String::String(const String& other){
size_t len = strlen(other.m_data);
m_data = new char[len + 1];
strcpy(m_data, other.m_data);
m_size = len;
}1234567
3. 赋值函数
当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。
拷贝构造函数和赋值函数的区别:
拷贝构造函数是在对象被创建并用另一个已存在的对象来初始化它时调用的,而赋 函数只是把一个对象赋值给另一个已存在的对象,使得那个已存在的对象具有和源对象相同的状态。
一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象。
实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。
String a("Change");
String b("word");
String c(a); // 调用拷贝构造函数(此前c还不存在)
c = b; // 调用赋值函数(c已经存在了)1234
赋值函数实现分为四步:
(1)检查自赋值。(要检查地址而不是值)
(2)分配新的内存资源,并复制内容。(strlen不会把‘\0’计算在内,但是strcpy会拷贝‘\0’)
(3)释放原本的内存资源。(防止造成内存泄漏)
(4)返回本对象的引用。(返回 *this 而不是 this更不是 other)
String& String::operator=(const String& other)
{
if (this != &other) { //(1)检查自赋值
char* temp = new char[strlen(other.m_data) + 1]; //(2)分配新的内存资源,并复制内容
strcpy(temp, other.m_data);
delete[]m_data; //(3)释放原本的内存资源
m_data = temp;
m_size = strlen(other.m_data);
}
return *this; //(4)返回本对象的引用
}
如果我们实在不想编写拷贝构造函数和拷贝赋值函数,又不想编译器自动生成默认函数,这时候我们可以把拷贝构造函数和拷贝赋值函数声明为 private,不去实现他们。
class A
{
//...
const char *GetType();
private:
A(const A& a); //私有的拷贝构造函数
A& operator=(const A& a); //私有的赋值函数
};12345678
如果有人写了以下程序:
A b(a); // 调用了私有的私有的拷贝构造函数
b = a; // 调用了私有的赋值函数12
此时编译器就会报错,因为外接不能操作A的私有函数。
4. 构造、拷贝和赋值到底用哪个
对象不存在,要创建它,无其它对象给它初始化 —— 调构造函数
对象不存在,要创建它,有其它对象给它初始化 —— 调构造拷贝函数
对象已存在,要改变它的值,用其他对象给他来赋值 —— 调赋值函数
5. String类完整代码实现
class String {
public:
String(const char *str = ""); // 默认构造函数
String(const String& copy); // 拷贝构造函数
~String(); // 析构函数
String& operator = (const String& assign); // 赋值函数
private:
size_t m_size; // 当前当前长度
char *m_data; // 指向字符串的指针
};
// 构造函数
String::String(const char *str)
{
if (NULL == str) {
m_data = new char[1];
*m_data = '\0';
m_size = 0;
}else {
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
m_size = length;
}
}
// 构造拷贝函数
String::String(const String& other){
size_t len = strlen(other.m_data);
m_data = new char[len + 1];
strcpy(m_data, other.m_data);
m_size = len;
}
// 析构函数
String::~String()
{
delete[]m_data;
}
// 赋值函数
String& String::operator=(const String& other)
{
if (this != &other) { //(1)检查自赋值
char* temp = new char[strlen(other.m_data) + 1]; //(2)分配新的内存资源,并复制内容
strcpy(temp, other.m_data);
delete[]m_data; //(3)释放原本的内存资源
m_data = temp;
m_size = strlen(other.m_data);
}
return *this; //(4)返回本对象的引用
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
---------------------
作者:Tianzez
来源:CSDN
原文:https://blog.csdn.net/tianzez/article/details/79636250
版权声明:本文为博主原创文章,转载请附上博文链接!