【C++】类构造函数(深拷贝与浅拷贝)

原创Jacky_Feng 最后发布于2019-11-29 19:56:28 阅读数 16 收藏

1.什么是类的构造函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时被自动调用。没创建一个对象都必须调用一次构造函数。

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回void。构造函数可用于为某些成员变量设置初始值。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using namespace std;
class Counter
{
public:
// 类Counter的构造函数
// 特点:以类名作为函数名,无返回类型
Counter()
{
cout<<"object is being created"<<endl;
}
private:
// 数据成员
int m_value;
};

int main()
{
Counter obj1;
return 0;
}

以上代码编译和执行的结果如下:

object is being created

该类对象obj1被创建时,编译系统为对象分配内存空间,并自动调用构造函数Counter()完成对象成员变量的初始化工作。

2.构造函数的分类

1)按函数有无参数分类

有无参数分类

其中所有带有默认值的有参构造函数会转变成默认构造函数。

2)按参数的类型分类

按参数的类型分类

一个类中至少有上述两个构造函数,可以有更多的构造函数(构造函数允许重载),以实现不同形式对象的创建。

3.构造函数的重载

和普通成员函数一样,构造函数是允许重载的。一个类中可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪个构造函数。创建对象构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
#include<cstring>
using namespace std;

class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
private:
char *m_data; // 用于保存字符串
};
// String 的析构函数
String::~String(void)
{
delete [] m_data;
// 由于m_data 是内部数据类型,也可以写成 delete m_data;
}

// String 的普通构造函数
String::String(const char *str)
{
cout<<"Ordinary constructor is running"<<endl;
}
// 拷贝构造函数
String::String(const String &other)
{
cout<<"Copy constructor is running"<<endl;
}

int main()
{
String str1;
String str2("hello");
String str3(str2);
return 0;
}

上面代码编译和执行结果为:

结果

4.常见的构造函数

1)默认构造函数

如果用户自己没有定义构造函数,那么编译器会自动生成一个默认构造函数,只是这个构造函数没有形参,函数体也是空的,不执行任何操作。例如:Student类的默认生成的构造函数如下:

Student(){}

==一个类中必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管几个,也不管形参如何,编译器都不再自动生成。==

调用不带参数的构造函数也可以省略括号。即创建对象Student stu()和Student stu是等价的。

2) 拷贝构造函数

当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数。拷贝构造函数的名称与类的名称一致,它必须的一个参数是本类型的一个引用变量。

注意,默认构造函数(即无参构造函数)不一定存在,但是拷贝构造函数总是会存在。

拷贝构造函数常用的三种情况:

==①当用一个对象去初始化同类的另一个对象==。

例如:

​ A test_b(test_a);
​ A test_b = test_a;//这两条语句是等价的
【注意】第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发拷贝构造函数的调用。

A test_a,test_b;
 test_b = test_a;//这句不会引发拷贝构造函数的调用,因为test_b早已生成,已经初始化过了

②==一个对象以值传递的方式传入函数体==,而调用拷贝构造函数时的参数,就是调用函数时所给的实参。

③==一个对象以值传递的方式从函数中返回==,而调用拷贝构造函数的参数,就是return语句所返回的对象。

浅拷贝

浅拷贝是指在对象拷贝时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多数情况下“浅拷贝”可以很好的工作,但一旦对象存在动态成员,那么浅拷贝就会出问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:
Rect()
{
p=new int(100);
}
~Rect()
{
assert(p!=NULL);
delete p;
}
private:
int width;
int height;
int *p;
};
int main()
{
Rect rect1;
Rect rect2(rect1);
return 0;
} 这段代码运行结束后,会出现运行错误。原因就在于在进行对象拷贝时,对于动态分配的内容没有正确的操作。

原因分析:

在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间。

原因

在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

深拷贝

深拷贝对于动态成员,并不是简单地复制,而是重新动态分配空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:
Rect()
{
p=new int(100);
}
//深拷贝
Rect(const Rect& r)
{
width=r.width;
height=r.height;
p=new int(100);
*p=*(r.p);
}
~Rect()
{
assert(p!=NULL);
delete p;
}
private:
int width;
int height;
int *p;
};
int main()
{
Rect rect1;
Rect rect2(rect1);
return 0;
}

完成对象拷贝地过程为:

过程

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

小结:

拷贝构造函数有两种:深拷贝和浅拷贝

当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝.

深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

5.综合实例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <iostream>
using namespace std;

class CPoint
{
protected:
int x;
int y;

public:
//缺省构造函数,如果定义类时未指定任何构造函数,
//系统将自动生成不带参数的缺省构造函数
CPoint()
{
cout << "默认构造函数 " << this << " ";
x = 0;
y = 0;
}
//带一个参数的可用于类型转换的构造函数
// explicit //加上 explicit 可防止 CPoint pt1 = 1; 这种隐性转换
CPoint(int ix)
{
cout << "1参数构造函数 " << this << " ";
x = ix;
y = 0;
}
//带参数的构造函数
CPoint(int ix, int iy)
{
cout << "2参数构造函数 " << this << " ";
x = ix;
y = iy;
}

//拷贝构造函数,如果此函数不定义,系统将生成缺省拷贝构造函数功能,
//缺省拷贝构造函数的行为是:用传入的对象参数的成员初始化正要建立的对象的相应成员
// explicit //加上 explicit 可防止 CPoint pt2 = pt1; 这种隐性转换
CPoint(const CPoint &cp)
{
cout << "拷贝构造函数 " << this << " ";
x = cp.x;
y = cp.y;
}
CPoint &operator=(const CPoint &cp)
{
cout << "赋值重载函数 " << this << " ";
if (this != &cp)
{
x = cp.x;
y = cp.y;
}
return (*this);
}

//析构函数,一个类中只能有一个析构函数,如果用户没有定义析构函数,
//系统会自动未类生成一个缺省的析构函数
~CPoint()
{
cout << "析构函数 " << this << " ";
}

CPoint &operator=(const CPoint &cp)
{
cout << "赋值重载函数 " << this << " ";
if (this != &cp)
{
x = cp.x;
y = cp.y;
}
return (*this);
}

//析构函数,一个类中只能有一个析构函数,如果用户没有定义析构函数,
//系统会自动未类生成一个缺省的析构函数
~CPoint()
{
cout << "析构函数 " << this << " ";
}
};

int main(int argc, char* argv[])
{
CPoint p0(); //这是函数的声明,不是实例化类
cout << endl << "CPoint pt1;\t\t";
CPoint pt1; //缺省构造函数

cout << endl << "CPoint pt2(1);\t\t";
CPoint pt2(1); //一个参数的构造函数

cout << endl << "CPoint pt3(1, 2);\t";
CPoint pt3(1, 2); //两个参数的构造函数

cout << endl << "CPoint pt4 = 1;\t\t";
CPoint pt4 = 1; //等价于CPoint t4(1); //explicit

cout << endl << "CPoint pt5 = t1;\t";
CPoint pt5 = pt1; //CPoint(t1);

cout << endl << "CPoint pt6 = CPoint();\t";
CPoint pt6 = CPoint(); //CPoint(1); CPoint(1,2);

cout << endl << "pt6 = CPoint(1);\t";
pt6 = CPoint(1);

cout << endl << "pt6 = 1;\t\t";
pt6 = 1; //首先调用单个参数的构造函数,生成临时对象CPoint(1), 然后调用赋值运算符函数

cout << endl << "pt6 = t1;\t\t";
pt6 = pt1; //调用赋值运算符函数

cout << endl << endl;
return 0;
}

上述代码编译和执行结果如下:

执行结果

————————————————
版权声明:本文为CSDN博主「Jacky_Feng」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Jacky_Feng/article/details/103313208