C++编程语言中有关复制构造函数的讨论
摘要:C++是一个在软件行业广泛使用的面向对象的程序设计语言。本文目的是讨论C++中的特殊构造函数:复制构造函数的概念和应用。作为基础知识,我们首先介绍构造函数和析构函数。为了说明复制构造函数的概念及其使用方法,我们给出了一些有关复制构造函数的例子,并提供了浅拷贝和深拷贝。通过分析复制构造函数的所有例子,讨论了复制构造函数,得出了如何定义一个复制构造函数以及如何使用它的属性。
关键词:复制构造函数;C ++编程语言;浅拷贝;深拷贝;析构函数
**之前的很多文章都在博客园,最近打算稍作整理放置在 CSDN 这篇文章也要重新进行布局、总结**
I.引论
C++是一种静态类型的,自由格式的,多范式的,编译的通用编程语言。它被认为是一种“中级”语言,因为它包含兼有高级和低级语言的特性。它是最流行的面向对象编程语言之一,在软件行业得到广泛的应用。掌握C++的基础知识是C++程序员的必备条件。大多数的程序员对如何使用拷贝构造函数感到困惑。本文的目的是帮助C++程序员正确的理解拷贝构造函数,并通过提供几个例子来自由的使用它。
II.构造函数和析构函数
复制构造函数是一种特殊的构造函数。在进行关于复制构造函数的讨论之前,我们首先应该知道构造函数和析构函数。
在C++中,类中的构造函数是创建对象时调用的一种特殊类型的子例程。它准备要使用的新对象。通常接受构造函数用于设置首次创建对象时所需的任何成员变量的参数。
构造函数通常与声明类具有相同的名称。它们的任务是初始化对象的数据成员以及建立类的不变量。如果不变量无效,则失败。正确编写的构造函数将使对象保持有效状态。不可变对象必须在构造函数中初始化。
C++编程语言允许重载构造函数,因为一个类可以有多个构造函数,每个构造函数都有不同的参数。一些语言考虑了一些特殊类型的构造函数:A)默认构造函数-一个不带参数的构造函数;B)复制构造函数-个构造函数,它带有一个类的类型的参数。
析构函数是一种在对象被销毁时自动调用的方法。其主要目的是清理并释放对象在其生命周期中获取的资源,并将其与其他对象或资源断开链接,废除这个过程中的任何引用。析构函数与类的名称相同,但在类名前面有一个波形符号(〜),并且没有参数,也没有返回类型。如果该对象是作为自动变量创建的,它的析构函数会在这个对象超出范围时被自动调用。如果该对象是用一个新的表达式创建的,那么当删除操作符被应用到指向该对象的指针时,它的析构函数就会被调用。通常,该操作通常发生在另一个析构函数中,通常是智能指针对象的析构函数。例子(1)演示了构造函数和析构函数。
//example 1
#include <iostream>
using namespace std;
class Point
{
public:
Point() { pc(); x = 0; y = 0; pp(); }
Point(int xx) { pc(); x = xx; y = 0; pp(); }
Point(int xx, int yy) { pc(); x = xx; y = yy; pp(); }
~Point() { cout << "Destructing..."; pp(); }
void pc() { cout << "Constructing..."; }
void pp() { cout << "x=" << x << ",y=" << y << endl; }
private:
int x, y;
};
int main()
{
Point p1, p2(2), p3(3, 3);
p1.pp();p2.pp();p3.pp();
return 0;
}
例如,我们定义了一个名为Point的类。它重载了具有不同参数的构造函数,分别为无参数,1个参数,2个参数。构造函数用于在函数中声明时根据参数数量创建对象。
例子(1)的输出结果是:
//outcome 1
Constructing...x = 0, y = 0
Constructing...x = 2, y = 0
Constructing...x = 3, y = 3
x = 0, y = 0
x = 2, y = 0
x = 3, y = 3
Destructing...x = 3, y = 3
Destructing...x = 2, y = 0
Destructing...x = 0, y = 0
结果显示:A)编译器在创建类型的对象时运行构造函数,调用哪个构造函数是由参数个数决定的;;B)对象p1,p2和p3是按照他们的声明顺序创建的。我们添加了一个返回类型为void的函数“pp”,用来输出数据成员的值以显示哪个对象被创建或被销毁;C)对象的破坏顺序与其创建顺序完全相反;D)所有对象在超出其空间范围时被销毁,并且调用析构函数的次数与调用构造函数的次数相同。
III.复制构造函数
让我们对例子(1)做一些改动:我们为类Point添加一个名叫“add”的友元函数,它可以直接引用Point类的私有成员,我们同样改动main函数来调用add函数。
//example 2
class Point
{
public:
friend Point add(Point pleft, Point pright);
......
private:
int x, y;
};
Point add(Point pleft, Point pright)
{
Point ptemp;
ptemp.x = pleft.x + pright.x;
ptemp.y = pleft.y + pright.y;
return ptemp;
}
int main()
{
Point p1(3,6), p2(p1), p3;
p3 = add(p1, p2); p3.pp();
return 0;
}
例子(2)的输出结果是:
//outcome 2
Constructing...x = 3, y = 6
Constructing...x = 0, y = 0
Constructing...x = 0, y = 0
Destructing...x = 6, y = 12
Destructing...x = 3, y = 6
Destructing...x = 3, y = 6
Destructing...x = 6, y = 12
x = 6, y = 12
Destructing...x = 6, y = 12
Destructing...x = 3, y = 6
Destructing...x = 3, y = 6
通常来说,“Constructing…”和“Destructing…”的数量是相等的,但是例子(2)
的输出结果显示了三个“Constructing…”和七个“Destructing…”。这是否正确?为什么?
原因是复制构造函数,默认的复制构造函数。复制构造函数是C++编程语言中的一个特殊构造函数,用于创建一个新对象作为现有对象的副本。这样一个构造函数的第一个参数是对正在构造的同一类型的对象的引用,后面可能跟有任何类型的参数(都有默认值)。正常情况下,编译器自动为每个类创建一个复制构造函数称为默认拷贝构造函数),但对于特殊情况,程序员创建拷贝构造函数,称为用户定义的拷贝构造函数。在这种情况下,编译器不会创建一个拷贝构造函数。
复制对象是通过使用复制构造函数和赋值运算符来实现的。复制构造函数的第一个参数是对其自己的类的类型的引用。它可以有更多参数,但其余参数必须具有与它们相关的默认值。
类Point的默认拷贝构造函数是:
Point(const Point &pt) { x = pt.x; y = pt.y; }
它与类Point具有相同的名称,并且具有类型也是Point类的const类型参数。它的功能是创建一个新对象,并将存在的对象的所有成员值复制到它。为了解复制构造函数的规则,我们为类Point添加了一个用户定义的复制构造函数。
//example 3
class Point
{
public:
Point(const Point &pt)
{
x = pt.x; y = pt.y;
cout << "Copy constructing...";
pp();
}
friend Point add(Point pleft, Point pright);
......
private:
int x, y;
};
Point add(Point pleft, Point pright)
{
......
}
int main()
{
Point p1(3, 3), p2(6, 6), p3(p1);
p3 = add(p1, p2);
p3.pp();
return 0;
}
为了在结果中清楚的显示调用复制构造函数时那个复制构造函数会被调用,我们在用户定义的复制构造函数的定义中添加了一些输出句子。现在,我们可以推断示例(3)的结果会告诉我们“Constructing…”和“Destructing…”出现的次数相等。
//outcome 3
Constructing...x = 3, y = 3
Constructing...x = 6, y = 6
Copy constructing...x = 3, y = 3
Copy constructing...x = 6, y = 6
Copy constructing...x = 3, y = 3
Constructing...x = 0, y = 0
Copy constructing...x = 9, y = 9
Destructing...x = 9, y = 9
Destructing...x = 3, y = 3
Destructing...x = 6, y = 6
Destructing...x = 9, y = 9
x = 9, y = 9
Destructing...x = 9, y = 9
Destructing...x = 6, y = 6
Destructing...x = 3, y = 3
输出结果显示“Constructing…”和“Copy constructing…”出现的次数之和与“Destructing…”出现的次数相同。让我们从main函数开始一步步分析这个例子和他的输出。
A Pointp1 (3, 3) , p2 ( 6, 6) ;
它调用构造函数两次,并按照定义顺序创建两个对象pl和p2,并分别用(3,3)和(6,6)设置他们的数据成员值。
B Pointp3 ( p1) ;
它首次调用用户定义的拷贝构造函数。它创建对象p3作为现有对象pl的副本。因此,在定义对象语句时,如果将某个对象置于括号内的初始化程序列表中,则会调用这个类的拷贝构造函数。
C p3= add ( p1, p2) ;
在这个句子中,p1,p2是参数。当一个对象被作为参数赋值时,拷贝构造函数也被调用。根据例子(3)的输出结果,我们可以看到转移参数的顺序是从右到左。首先它创建了对象pright作为对象p2的副本,然后它创建了对象pleft作为对象pl的副本。
当程序到达add函数时,它创建一个新的对象ptemp,其数据成员的值全为零。然后通过相加来计算ptemp的数据成员的值。
“'return ptemp;”,当一个对象值被返回时,复制构造函数也被调用。它创建一个临时对象作为ptemp的副本。
当add函数完成它的任务时,本地对象按照ptemp,pleft,pright和临时对象的顺序被析构函数销毁。
D p3.pp();
它输出对象p3数据成员的值。当主函数结束时,会自动调用析构函数以按p3,p2,p1的顺序销毁本地对象。
基于以上讨论,可以得出结论:以下情况可能导致对拷贝构造函数的调用:A)当一个对象按值返回时;B)当一个对象作为参数传递(给一个函数)时;C)当一个对象被放置在括号内的初始化列表中时。有人可能会混淆,在这种情况下,默认的拷贝构造函数或用户定义的拷贝构造函数具有相同的功能,为什么我们需要用户定义的拷贝构造函数?当情况变得复杂时,复制构造函数不能满足用户的需求,然后引入浅拷贝和深拷贝来解决复杂问题。
IV.浅拷贝和深拷贝
现在,想象一个非常简单的动态数组类,如下所示:
//example 4
#include <iostream>
#include <string.h>
using namespace std;
class Person
{
public:
Person(int page, char *pname)
{
cout << "Constructing..." << endl;
age = page;
int strl = strlen(pname);
name = new char[strl + 1];
strcpy(name, pname);
}
~Person()
{
cout << "Destructing..." << endl;
if (name)
{
delete[] name;
}
}
void Person_print()
{
cout << "Name:" << name << ",Age:" << age << endl;
}
private:
int age;
char *name;
};
int main()
{
Person p1(20, "Wang Li"), p2(p1);
p1.Person_print();
p2.Person_print();
return 0;
}
由于我们没有指定复制构造函数,编译器为我们生成了一个。 生成的构造函数看起来像这样:
Person ( const Person & copy)
age ( copy.age ),name ( copy.name ) { };
例子(4)的输出结果是:
//outcome 4
Constructing...
Name:Wang Li,Age:20
Name:Wang Li,Age:20
Destructing...
Destructing...
图1:消息盒中的错误信息
在程序的运行过程中产生了一个错误。错误信息出现在一个消息盒中。出了什么问题呢?让我们为程序添加更多的输出信息:在输出语句中添加this指针,例如:
cout << "Destructing..."<<this;
cout << "&name:"<<endl;
接下来输出变成了:
//outcome 5
Constructing...0x7fffb46293e0
Name:Wang Li,Age:20
Name:Wang Li,Age:20
Destructing...0x7fffb46293f0 &name:0x7fffb46293f8
Destructing...0x7fffb46293e0 &name:0x7fffb46293f8
(错误信息和图1一样)
结果(5)表明,同一个地址0x7fffb46293f8被毁坏了两次是错误的原因。 缺省拷贝构造函数的问题是它执行名称指针的浅表副本。它只复制原始名称成员的地址;这意味着它们共享一个指向同一块内存的指针,这不是我们想要的。当程序到达主函数的末尾时,副本的析构函数被调用(因为堆栈中的对象在其作用域结束时会自动销毁)到对象p2。数组的析构函数删除原始数据数组,因此当它删除副本的名称时,因为它们共享相同的指针,它也删除对象p1的名称。 现在访问无效的名称并写入它!这产生了着名的分段故障。
如果我们编写自己的拷贝构造函数来执行深层拷贝,那么这个问题就会消失。
//outcome 6
Constructing...0x7fffa56a0510
Copy constructing...0x7fffa56a0520
Name:Wang Li,Age:20
Name:Wang Li,Age:20
Destructing...0x7fffa56a0520 &name:0x7fffa56a0528
Destructing...0x7fffa56a0510 &name:0x7fffa56a0518
这时,p1的名字和p2的名字拥有不同的地址,所以他们将被正确的销毁。
V.总结
每当从一个对象创建一个新变量时,都会调用一个拷贝构造函数。这会发生在以下的情况:A)声明从另一个对象初始化的变量,例如:Person p2(p1),p3 = p2;B)一个值参数将从其相应的参数初始化,例如:add(p1,p2);C)一个对象被一个函数返回。
根据上面的讨论,可以的出下面的结论:A)如果对象没有指向动态分配的内存的指针,则浅拷贝可能就足够了。 因此默认的复制构造函数,默认赋值运算符和默认析构函数都可以,你不需要自己写。B)如果你需要一个拷贝构造函数,那是因为你需要类似于深拷贝或其他一些资源管理的东西。 因此几乎可以肯定你需要一个析构函数。
引用
[1] B.Stroustrup, "The C++ programminglanguage", Addison-Wesley, 1997.
[2] M. Ellis and B.Stroustrup, "TheAnnotated C++ Reference", Addison-Wesly, 1990.
[3] A.Alexandrescu, "Modern C++Design", Addison-Wesly, 2001.
[4] S.B.Lippman, J.Lajoie, B.E.Moo, "C++ Primer,Fourth Edition", Addison-Wesley Professional, 2005.
[5] H.M.Deitel, P.J.Deitel, "C++ Howto Program, ifth Edition", Prentice Hall, 2005.
[6] Bill Blunden, "Memory Management: Algorithmsand Implementation in C/C++, Wordware Publishin, 2003.