类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
下面的实例有助于更好地理解构造函数的概念:
#include
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created
Length of line : 6
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:
#include
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line(10.0);
// 获取默认设置的长度
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created, length = 10
Length of line : 10
Length of line : 6
使用初始化列表来初始化字段:
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}
上面的语法等同于如下语法:
Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}
假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
下面的实例有助于更好地理解析构函数的概念:
#include
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created
Length of line : 6
Object is being deleted
首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a = 100;
int b = a;
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。
#include
using namespace std;
class CExample {
private:
int a;
public:
//构造函数
CExample(int b)
{ a = b;}
//一般函数
void Show ()
{
cout<
运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
下面举例说明拷贝构造函数的工作过程。
#include
using namespace std;
class CExample {
private:
int a;
public:
//构造函数
CExample(int b)
{ a = b;}
//拷贝构造函数
CExample(const CExample& C)
{
a = C.a;
}
//一般函数
void Show ()
{
cout<
CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
在C++中,下面三种对象需要调用拷贝构造函数!
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a = b;
cout<<"creat: "<
调用g_Fun()时,会产生以下几个重要步骤:
(1).test对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).然后调用拷贝构造函数把test的值给C。 整个这两个步骤有点像:CExample C(test);
(3).等g_Fun()执行完后, 析构掉 C 对象。
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a = b;
}
//拷贝构造
CExample(const CExample& C)
{
a = C.a;
cout<<"copy"<
当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。
(2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3). 在函数执行到最后先析构temp局部变量。
(4). 等g_Fun()执行完后再析构掉XXXX对象。
CExample A(100);
CExample B = A;
// CExample B(A);
后两句都会调用拷贝构造函数。
很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:
Rect::Rect(const Rect& r)
{
width = r.width;
height = r.height;
}
当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:
class Rect
{
public:
Rect() // 构造函数,计数器加1
{
count++;
}
~Rect() // 析构函数,计数器减1
{
count--;
}
static int getCount() // 返回计数器的值
{
return count;
}
private:
int width;
int height;
static int count; // 一静态成员做为计数器
};
int Rect::count = 0; // 初始化计数器
int main()
{
Rect rect1;
cout<<"The count of Rect: "<
这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。
说白了,就是拷贝构造函数没有处理静态数据成员。
出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:
class Rect
{
public:
Rect() // 构造函数,计数器加1
{
count++;
}
Rect(const Rect& r) // 拷贝构造函数
{
width = r.width;
height = r.height;
count++; // 计数器加1
}
~Rect() // 析构函数,计数器减1
{
count--;
}
static int getCount() // 返回计数器的值
{
return count;
}
private:
int width;
int height;
static int count; // 一静态成员做为计数器
};
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:
class Rect
{
public:
Rect() // 构造函数,p指向堆中分配的一空间
{
p = new int(100);
}
~Rect() // 析构函数,释放动态分配的空间
{
if(p != NULL)
{
delete p;
}
}
private:
int width;
int height;
int *p; // 一指针成员
};
int main()
{
Rect rect1;
Rect rect2(rect1); // 复制对象
return 0;
}
在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:
在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
class Rect
{
public:
Rect() // 构造函数,p指向堆中分配的一空间
{
p = new int(100);
}
Rect(const Rect& r)
{
width = r.width;
height = r.height;
p = new int; // 为新对象重新动态分配空间
*p = *(r.p);
}
~Rect() // 析构函数,释放动态分配的空间
{
if(p != NULL)
{
delete p;
}
}
private:
int width;
int height;
int *p; // 一指针成员
};
此时,在完成对象的复制后,内存的一个大致情况如下:
此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。
通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
// 防止按值传递
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a = b;
cout<<"creat: "<
1. 拷贝构造函数里能调用private成员变量吗?
解答:这个问题是在网上见的,当时一下子有点晕。其时从名子我们就知道拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。
2. 以下函数哪个是拷贝构造函数,为什么?
X::X(const X&);
X::X(X);
X::X(X&, int a=1);
X::X(X&, int a=1, int b=2);
解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
X::X(const X&); //是拷贝构造函数
X::X(X&, int=1); //是拷贝构造函数
X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数
3. 一个类中可以存在多于一个的拷贝构造函数吗?
解答:类中可以存在超过一个拷贝构造函数。
class X {
public:
X(const X&); // const 的拷贝构造
X(X&); // 非const的拷贝构造
};
注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
class X {
public:
X();
X(X&);
};
const X cx;
X x = cx; // error
如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。
关于C++的拷贝构造函数,很多的建议是直接禁用。为什么大家会这么建议呢?没有拷贝构 造函数会有什么限制呢?如何禁用拷贝构造呢?这篇文章对这些问题做一个简单的总结。
这里讨论的问题以拷贝构造函数为例子,但是通常赋值操作符是通过拷贝构造函数来实现 的( copy-and-swap 技术,详见《Exceptional C++》一书),所以这里讨论也适用于赋 值操作符,通常来说禁用拷贝构造函数的同时也会禁用赋值操作符。
关于拷贝构造函数的禁用原因,我目前了解的主要是两个原因。第一是浅拷贝问题,第二 个则是基类拷贝问题。
编译器默认生成的构造函数,是memberwise
拷贝^1,也就是逐个拷贝成员变量,对于 下面这个类的定义
class Widget {
public:
Widget(const std::string &name) : name_(name), buf_(new char[10]) {}
~Widget() { delete buf_; }
private:
std::string name_;
char *buf_;
};
默认生成的拷贝构造函数,会直接拷贝buf_
的值,导致两个Widget
对象指向同一个缓 冲区,这会导致析构的时候两次删除同一片区域的问题(这个问题又叫双杀
问题)。
解决这个问题的方式有很多:
自己编写拷贝构造函数,然后在拷贝构造函数中创建新的buf_
,不过拷贝构造函数的 编写需要考虑异常安全的问题,所以编写起来有一定的难度。
使用 shared_ptr
这样的智能指针,让所有的 Widget
对象共享一片 buf_
,并 让 shared_ptr
的引用计数机制帮你智能的处理删除问题。
禁用拷贝构造函数和赋值操作符。如果你根本没有打算让Widget
支持拷贝,你完全可 以直接禁用这两操作,这样一来,前面提到的这些问题就都不是问题了。
如果我们不去自己编写拷贝构造函数,编译器默认生成的版本会自动调用基类的拷贝构造 函数完成基类的拷贝:
class Base {
public:
Base() { cout << "Base Default Constructor" << endl; }
Base(const Base &) { cout << "Base Copy Constructor" << endl; }
};
class Drived : public Base {
public:
Drived() { cout << "Drived Default Constructor" << endl; }
};
int main(void) {
Drived d1;
Drived d2(d1);
}
上面这段代码的输出如下:
Base Default Constructor
Drived Default Constructor
Base Copy Constructor // 自动调用了基类的拷贝构造函数
但是如果我们出于某种原因编写了,自己编写了拷贝构造函数(比如因为上文中提到的浅 拷贝问题),编译器不会帮我们安插基类的拷贝构造函数,它只会在必要的时候帮我们安 插基类的默认构造函数:
class Base {
public:
Base() { cout << "Base Default Constructor" << endl; }
Base(const Base &) { cout << "Base Copy Constructor" << endl; }
};
class Drived : public Base {
public:
Drived() { cout << "Drived Default Constructor" << endl; }
Drived(const Drived& d) {
cout << "Drived Copy Constructor" << endl;
}
};
int main(void) {
Drived d1;
Drived d2(d1);
}
上面这段代码的输出如下:
Base Default Constructor
Drived Default Constructor
Base Default Constructor // 调用了基类的默认构造函数
Drived Copy Constructor
这当然不是我们想要看到的结果,为了能够得到正确的结果,我们需要自己手动调用基类 的对应版本拷贝基类对象。
Drived(const Drived& d) : Base(d) {
cout << "Drived Copy Constructor" << endl;
}
这本来不是什么问题,只不过有些人编写拷贝构造函数的时候会忘记这一点,所以导致基 类子对象没有正常复制,造成很难察觉的BUG。所以为了一劳永逸的解决这些蛋疼的问题, 干脆就直接禁用拷贝构造和赋值操作符。
在C++11之前对象必须有正常的拷贝语义才能放入容器中,禁用拷贝构造的对象无法直接放 入容器中,当然你可以使用指针来规避这一点,但是你又落入了自己管理指针的困境之中 (或许使用智能指针可以缓解这一问题)。
C++11中存在移动语义,你可以通过移动而不是拷贝把数据放入容器中。
拷贝构造函数的另一个应用在于设计模式中的原型模式
,在C++中没有拷贝构造函数,这 个模式实现可能比较困难。
如果你的编译器支持 C++11,直接使用 delete
否则你可以把拷贝构造函数和赋值操作符声明成private
同时不提供实现。
你可以通过一个基类来封装第二步,因为默认生成的拷贝构造函数会自动调用基类的拷 贝构造函数,如果基类的拷贝构造函数是 private
,那么它无法访问,也就无法正常 生成拷贝构造函数。
class NonCopyable {
protected:
~NonCopyable() {} // 关于为什么声明成为 protected,参考
// 《Exceptional C++ Style》
private:
NonCopyable(const NonCopyable&);
}
class Widget : private NonCopyable { // 关于为什么使用 private 继承
// 参考《Effective C++》第三版
}
Widget widget(Widget()); // 错误
上不会生成memberwise
的拷贝构造函数,详细内容可以参考《深度探索C++对象模型》一 书
禁用原因主要是两个:
1. 浅拷贝问题,也就是上面提到的二次析构。
2. 自定义了基类和派生类的拷贝构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全,比如出现二次析构问题时,因为不会调用我们自定义的基类深拷贝,还是默认的浅拷贝。
Effective C++条款6规定,如果不想用编译器自动生成的函数,就应该明确拒绝。方法一般有三种:
1. C++11对函数声明加delete关键字:Base(const Base& obj) = delete;,不必有函数体,这时再调用拷贝构造会报错尝试引用已删除的函数。
2. 最简单的方法是将拷贝构造函数声明为private
3. 条款6给出了更好的处理方法:创建一个基类,声明拷贝构造函数,但访问权限是private,使用的类都继承自这个基类。默认拷贝构造函数会自动调用基类的拷贝构造函数,而基类的拷贝构造函数是private,那么它无法访问,也就无法正常生成拷贝构造函数。
Qt就是这样做的,QObject定义中有这样一段,三条都利用了:
第一种方法:最简单的方法是将拷贝构造函数声明为private
private:
Q_DISABLE_COPY(QMainWindow)
#define Q_DISABLE_COPY(Class) \
Class(const Class &) Q_DECL_EQ_DELETE;\
Class &operator=(const Class &) Q_DECL_EQ_DELETE;
类的不可拷贝特性是可以继承的,例如凡是继承自QObject的类都不能使用拷贝构造函数和赋值运算符。
(2)第二种方法 继承一个uncopyable类
C++的编译在链接之前,如果我们能在编译期解决这个问题,会节省不少的时间,要想在编译期解决问题,就需要人为制造一些bug。我们声明一个专门阻止拷贝的基类uncopyable。
class uncopyable{
protected:
uncopyable(){}
~uncopyable(){}
private:
uncopyable(const uncopyable&);
uncopyable& operator=(const uncopyable&);
}
接下来,我们的类只要继承uncopyable,如果要发生拷贝,编译器都会尝试调用基类的拷贝构造函数或者赋值运算符,但是因为这两者是私有的,会出现编译错误。
本文主要介绍C++中类对象的赋值操作、复制操作,以及两者之间的区别,另外还会讲到“深拷贝”与“浅拷贝”的相关内容。
本系列内容会分为三篇文章进行讲解。
如同基本类型的赋值语句一样,同一个类的对象之间也是可以进行赋值操作的,即将一个对象的值赋给另一个对象。
对于类对象的赋值,只会对类中的数据成员进行赋值,而不对成员函数赋值。
例如:obj1 和 obj2 是同一类 ClassA 的两个对象,那么对象赋值语句“obj2 = obj1;” 就会把对象 obj1 的数据成员的值逐位赋给对象 obj2。
下面展示一个对象赋值的代码示例(object_assign_and_copy_test1.cpp),如下:
#include
using namespace std;
class ClassA
{
public:
// 设置成员变量的值
void SetValue(int i, int j)
{
m_nValue1 = i;
m_nValue2 = j;
}
// 打印成员变量的值
void ShowValue()
{
cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;
}
private:
int m_nValue1;
int m_nValue2;
};
int main()
{
// 声明对象obj1和obj2
ClassA obj1;
ClassA obj2;
obj1.SetValue(1, 2);
// 对象赋值场景 —— 将obj1的值赋给obj2
obj2 = obj1;
cout << "obj1 info as followed: " << endl;
obj1.ShowValue();
cout << "obj2 info as followed: " << endl;
obj2.ShowValue();
return 0;
}
编译并运行上述代码,结果如下:
上面的执行结果表明,通过对象赋值语句,我们将obj1的值成功地赋给了obj2。
对于对象赋值,进行以下几点说明:
// 声明obj1和obj2
ClassA obj1;
ClassA obj2;
obj2 = obj1; // 此语句为对象的赋值
// 声明obj1
ClassA obj1;
// 声明并初始化obj2
ClassA obj2 = obj1; // 此语句属于对象的复制
下面从内存分配的角度分析一下对象的赋值操作。
4.1.4.1 C++中对象的内存分配方式
在C++中,只要声明了对象,对象实例在编译的时候,系统就需要为其分配内存了。一段代码示例如下:
class ClassA
{
public:
ClassA(int id, char* name)
{
m_nId = id;
m_pszName = new char[strlen(name) + 1];
strcpy(m_pszName, name);
}
private:
char* m_pszName;
int m_nId;
};
int main()
{
ClassA obj1(1, "liitdar");
ClassA obj2;
return 0;
}
在上述代码编译之后,系统为 obj1 和 obj2 都分配相应大小的内存空间(只不过对象 obj1 的内存域被初始化了,而 obj2 的内存域的值为随机值)。两者的内存分配效果如下:
4.1.4.2 默认的赋值运算符
延续上面的示例代码,我们执行“obj2 = obj1;”,即利用默认的赋值运算符将对象 obj1 的值赋给 obj2。使用类中默认的赋值运算符,会将对象中的所有位于 stack 中的域进行相应的复制操作;同时,如果对象有位于 heap 上的域,则不会为目标对象分配 heap 上的空间,而只是让目标对象指向源对象 heap 上的同一个地址。
执行了“obj2 = obj1;”默认的赋值运算后,两个对象的内存分配效果如下:
因此,对于类中默认的赋值运算,如果源对象域内没有 heap 上的空间,其不会产生任何问题。但是,如果源对象域内需要申请 heap 上的空间,那么由于源对象和目标对象都指向 heap 的同一段内容,所以在析构对象的时候,就会连续两次释放 heap 上的那一块内存区域,从而导致程序异常。
~ClassA()
{
delete m_pszName;
}
为了解决上面的问题,如果对象会在 heap 上存在内存域,则我们必须重载赋值运算符,从而在进行对象的赋值操作时,使不同对象的成员域指向不同的 heap 地址。
重载赋值运算符的代码如下:
// 赋值运算符重载需要返回对象的引用,否则返回后其值立即消失
ClassA& operator=(ClassA& obj)
{
// 释放heap内存
if (m_pszName != NULL)
{
delete m_pszName;
}
// 赋值stack内存的值
this->m_nId = obj.m_nId;
// 赋值heap内存的值
int nLength = strlen(obj.m_pszName);
m_pszName = new char[nLength + 1];
strcpy(m_pszName, obj.m_pszName);
return *this;
}
使用上面重载后的赋值运算符对对象进行赋值时,两个对象的内存分配效果如下:
这样,在对象 obj1、obj2 退出其的作用域,调用相应的析构函数时,就会释放不同 heap 空间的内存,也就不会出现程序异常了。
相对于“对已声明的对象使用赋值运算符进行的对象赋值”操作,使用拷贝构造函数操作对象的方式,称为“对象的复制”。
类的拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用为:在创建一个新对象时,使用一个已经存在的对象去初始化这个新对象。例如语句“ClassA obj2(obj1);”就使用了拷贝构造函数,该语句在创建新对象 obj2 时,利用已经存在的对象 obj1 去初始化对象 obj2。
对象的赋值与对象的复制,貌似都是只对类的成员变量进行拷贝,而不会对类的成员函数进行操作。—— 待进一步确认。
拷贝构造函数有以下特点:
这里展示一个自定义拷贝构造函数的代码示例(object_assign_and_copy_test2.cpp),如下:
#include
using namespace std;
class ClassA
{
public:
// 普通构造函数
ClassA(int i, int j)
{
m_nValue1 = i;
m_nValue2 = j;
}
// 自定义的拷贝构造函数
ClassA(const ClassA& obj)
{
m_nValue1 = obj.m_nValue1 * 2;
m_nValue2 = obj.m_nValue2 * 2;
}
// 打印成员变量的值
void ShowValue()
{
cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;
}
private:
int m_nValue1;
int m_nValue2;
};
int main()
{
// 创建并初始化对象obj1,此处调用了普通构造函数
ClassA obj1(1, 2);
// 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
ClassA obj2(obj1);
obj1.ShowValue();
obj2.ShowValue();
return 0;
}
编译并执行上述代码,结果如下:
上述执行结果表明,通过调用自定义的拷贝构造函数,我们在创建对象 obj2 时,结合对象 obj1 的成员变量的值,完成了我们自定义的初始化过程。
我们可以从调用形式上,对“对象的赋值”和“对象的复制”进行区分。在此,我们列出一些对应关系:
上面的对应关系是不严谨的,因为有些情况下,即使使用了赋值运算符“=”,但其实最终使用的仍然是类的拷贝构造函数,这就引出了拷贝构造函数的两种调用形式。
拷贝构造函数的调用语法分为两种:
拷贝构造函数的“赋值法”就很容易与“对象的赋值”场景混淆,其二者之间的区别是:对象的赋值场景必须是建立在源对象与目标对象均已声明的基础上;而拷贝构造函数函数的赋值法,必须是针对新创建对象的场景。代码如下:
【对象的赋值】:
// 声明对象obj1和obj2
ClassA obj1;
ClassA obj2;
obj1.SetValue(1, 2);
// 对象赋值场景 —— 将obj1的值赋给obj2
obj2 = obj1;
【拷贝构造函数的“赋值法”】:
// 创建并初始化对象obj1,此处调用了普通构造函数
ClassA obj1(1, 2);
// 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
ClassA obj2 = obj1;
当然,为了代码的清晰化,建议使用拷贝构造函数的“代入法”,更可以让人一眼就看出调用的是拷贝构造函数。
4.2.5.1 类对象初始化
当使用类的一个对象去初始化另一个对象时,会调用拷贝构造函数(包括“代入法”和“赋值法”)。示例代码如下:
// 创建并初始化对象obj1,此处调用了普通构造函数
ClassA obj1(1, 2);
// 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
ClassA obj2 = obj1; // 代入法
ClassA obj3 = obj1; // 赋值法
4.2.5.2 类对象作为函数参数
当类对象作为函数形参时,在调用函数进行形参和实参转换时,会调用拷贝构造函数。示例代码如下:
// 形参是类ClassA的对象obj
void funA(ClassA obj)
{
obj.ShowValue();
}
int main()
{
ClassA obj1(1, 2);
// 调用函数funA时,实参obj1是类ClassA的对象
// 这里会调用拷贝构造函数,使用实参obj1初始化形参对象obj
funA(obj1);
return 0;
}
说明:在上面的main函数内,语句“funA(obj1);”就会调用拷贝构造函数。
4.2.5.3 类对象作为函数返回值
当函数的返回值是类的对象、在函数调用完毕将返回值(对象)带回函数调用处,此时会调用拷贝构造函数,将函数返回的对象赋值给一个临时对象,并传到函数的调用处。示例代码如下:
// 函数funB()的返回值类型是ClassA类类型
ClassA funB()
{
ClassA obj1(1, 2);
// 函数的返回值是ClassA类的对象
return obj1;
}
int main()
{
// 定义类ClassA的对象obj2
ClassA obj2;
// funB()函数执行完成、返回调用处时,会调用拷贝构造函数
// 使用obj1初始化obj2
obj2 = funB();
return 0;
}
说明:在上面的main函数内,语句“obj2 = funB();”就会调用拷贝构造函数。由于对象obj1是函数funB中定义的,在函数funB结束时,obj1的生命周期就结束了,因此在函数funB结束之前,执行语句"return obj1"时,会调用拷贝构造函数将obj1的值拷贝到一个
临时对象中,这个临时对象是系统在主程序中临时创建的。funB函数结束时,对象obj1消失,但是临时对象将会通过语句“obj2 = funB()”赋值给对象obj2,执行完这条语句后,临时对象也自动消失了。
浅拷贝:就是只拷贝类中位于 stack 域中的内容,而不会拷贝 heap 域中的内容。
例如,使用类的默认的赋值运算符“=”,或默认的拷贝构造函数时,进行的对象拷贝都属于浅拷贝。这也说明,“浅拷贝”与使用哪种方式(赋值运算符或是拷贝构造函数)进行对象拷贝无关。
浅拷贝会有一个问题,当类中存在指针成员变量时,进行浅拷贝后,目标对象与源对象的该指针成员变量将会指向同一块 heap 内存(而非每个对象单独一块内存),这就会导致由于共用该段内存而产生的内存覆盖、重复释放内存等等问题。详情可参考本系列第一章内容。
所以,对于带有指针的类对象的拷贝操作,正确的做法应当使两个对象的指针指向各自不同的内存,即在拷贝时不是简单地拷贝指针,而是将指针指向的内存中的每一个元素都进行拷贝。由此也就引出了“深拷贝”的概念。
深拷贝:当进行对象拷贝时,将对象位于 stack 域和 heap 域中的数据都进行拷贝。
前面也提到了,类默认提供的赋值运算符或拷贝构造函数,进行的都是浅拷贝,所以,为了实现对象的深拷贝,我们需要对赋值运算符或拷贝构造函数进行重载,以达到深拷贝的目的。
这里展示一段重载赋值运算符的示例代码,如下:
// 重载赋值运算符
ClassA& operaton= (ClassA& obj)
{
// 拷贝 stack 域的值
m_nId = obj.m_nId;
// 适应自赋值(obj = obj)操作
if (this == &a)
{
return *this;
}
// 释放掉已有的 heap 空间
if (m_pszName != NULL)
{
delete m_pszName;
}
// 新建 heap 空间
m_pszName = new char[strlen(obj.m_pszName) + 1];
// 拷贝 heap 空间的内容
if (m_pszName != NULL)
{
strcpy(m_pszName, obj.m_pszName);
}
return *this;
}
private:
int m_nId;
char* m_pszName;
这里展示一段重载拷贝构造函数的示例代码,如下:
// 重载拷贝构造函数,重载后的拷贝构造函数支持深拷贝
ClassA(ClassA &obj)
{
// 拷贝 stack 域的值
m_nId = obj.m_nId;
// 新建 heap 空间
m_pszName = new char[strlen(obj.m_pszName) + 1];
// 拷贝 heap 空间的内容
if (m_pszName != NULL)
{
strcpy(m_pszName, obj.m_pszName);
}
}
private:
int m_nId;
char* m_pszName;
从上述两个示例代码可以看出,支持深拷贝的重载赋值运算符和重载拷贝构造函数相似,但两者也存在以下区别:
C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法。下面就详细比较下三者之间的区别以及它们的具体实现
构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)
首先说一下一个C++的空类,编译器会加入哪些默认的成员函数
·默认构造函数和拷贝构造函数
·析构函数
·赋值函数(赋值运算符)
·取值函数
**即使程序没定义任何成员,编译器也会插入以上的函数!
注意:构造函数可以被重载,可以多个,可以带参数;
析构函数只有一个,不能被重载,不带参数
而默认构造函数没有参数,它什么也不做。当没有重载无参构造函数时,
A a就是通过默认构造函数来创建一个对象
下面代码为构造函数重载的实现
class A
{
int m_i;
Public:
A()
{
Cout<<”无参构造函数”<
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象
A a;
A b(a);
A b=a; 都是拷贝构造函数来创建对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
先说下什么时候拷贝构造函数会被调用:
在C++中,3种对象需要复制,此时拷贝构造函数会被调用
1)一个对象以值传递的方式传入函数体
2)一个对象以值传递的方式从函数返回
3)一个对象需要通过另一个对象进行初始化
什么时候编译器会生成默认的拷贝构造函数:
1)如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不在生成。
2)如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。
因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。
下面说说深拷贝与浅拷贝:
浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在)
深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。
拷贝构造函数重载声明如下:
A (const A&other)
下面为拷贝构造函数的实现:
class A
{
int m_i
A(const A& other):m_i(other.m_i)
{
Cout<<”拷贝构造函数”<
当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。
当没有重载赋值函数(赋值运算符)时,通过默认赋值函数来进行赋值操作
A a;
A b;
b=a;
强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!
赋值运算的重载声明如下:
A& operator = (const A& other)
通常大家会对拷贝构造函数和赋值函数混淆,这儿仔细比较两者的区别:
1)拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
class A;
A a;
A b=a; //调用拷贝构造函数(b不存在)
A c(a) ; //调用拷贝构造函数
/****/
class A;
A a;
A b;
b = a ; //调用赋值函数(b存在)
2)一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象
3)实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。(这些要点会在下面的String实现代码中体现)
!!!如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。如:
class A
{
private:
A(const A& a); //私有拷贝构造函数
A& operate=(const A& a); //私有赋值函数
}
如果程序这样写就会出错:
A a;
A b(a); //调用了私有拷贝构造函数,编译出错
A b;
b=a; //调用了私有赋值函数,编译出错
所以如果类定义中有指针或引用变量或对象,为了避免潜在错误,最好重载拷贝构造函数和赋值函数。
下面以string类的实现为例,完整的写了普通构造函数,拷贝构造函数,赋值函数的实现。String类的基本实现见我另一篇博文。
String::String(const char* str) //普通构造函数
{
cout<
一句话记住三者:对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数(上面说了三种用它的情况!)
对象存在,用别的对象来给它赋值,就是赋值函数。
以上为本人结合很多资料和图书整理出来的,将核心的点都系统的理出来,全自己按条理写的,现在大家对普通构造函数,拷贝构造函数,赋值函数的区别和实现应该都清楚了。
在C++类中,编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。注意,这些编译器产生出来的函数都是public的,为了阻止这些函数被创建出来,我们可以把它们声明为private,这样就阻止了编译器暗自创建其对应版本函数。
class Node
{
public:
Node(int _data = 0) : data(_data) {}
int get() const { return data; }
void set(int _data) { data = _data; }
private:
Node(const Node &);
Node &operator=(const Node &);
int data;
};
在上面的class定义中,当程序企图拷贝Node对象时,编译器就会阻止该操作。这样的话,只要将copy构造函数和copy assign操作符声明为private就可以了,还有另外一种方式,我们可以专门定义一个阻止copying动作的base class。这个base class如下所示:
class Uncopyable
{
protected:
Uncopyable() {} // 允许derived对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable &); // 阻止copying
Uncopyable &operator=(const Uncopyable &);
};
class Node : private Uncopyable
{
public:
Node(int _data = 0) : data(_data) {}
int get() const { return data; }
void set(int _data) { data = _data; }
private:
int data;
};
这样的话,在程序中,甚至在member函数或friend函数中,尝试拷贝Node对象,编译器就会试着生成一个copy构造函数或copy assign操作符,这些函数的“默认版本”会尝试调用其base class的对应函数,但是这些调用会被阻止,因为它们是private的,即阻止了该类对象的copy操作。