第一部分:拷贝构造函数
类类型的变量需要使用拷贝构造函数来完成整个复制过程
拷贝构造函数的形式:
A (const A &a) // A为类名
对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
系统提供的默认拷贝构造函数可能为:
X::X(const X&) X::X(&)
/*拷贝构造函数*/
#include <iostream>
using namespace std;
class A{
public:
A(int n):m_n(n){
cout<<"A的构造函数被调用"<<endl;
}
A(const A&a){
m_n = a.m_n;
cout<<"A的拷贝构造函数被调用"<<endl;
}
// 析构函数
~A(void){
cout<<"A的析构函数被调用"<<endl;
}
A foo4(){
cout<<"成员函数foo4"<<endl;
A a(1);
return a;
}
void print(){
cout<<m_n<<endl;
A a(1);
}
private:
int m_n;
};
void gfoo1(A a){
cout<<"全局函数foo1"<<endl;
}
A gfoo2(){
cout<<"全局函数foo2"<<endl;
A a(1);
return a;
}
int main(void)
{
return 0;
}
一、拷贝构造函数的工作过程:下面两种写法都会调用拷贝构造函数
A a(1);
A a1 = a; 或者 A a2(a);
2、拷贝构造函数被调用的时机 即 对象复制的过程发生
1. 对象以值传递的方式传入函数参数
2. 对象以值传递的方式从函数返回
3. 对象需要通过另外一个对象进行初始化;
解析:
1、对象以值传递的方式传入函数参数
A a(1);
gfoo1(a);
其执行的过程:
1> 对象 a传入形参时,会先产生临时变量temp (临时变量的产生是不会调用构造函数)
2>然后调用拷贝构造函数把temp的值复制给形参param
3>等gfoo1函数执行完,先析构掉临时对象temp 、再析构掉param对象
结果:
A的构造函数被调用
A的拷贝构造函数被调用
全局函数foo1
A的析构函数被调用
A的析构函数被调用
2、对象以值传递的方式从函数返回
gfoo2();
其执行的过程:
1>先会产生一个临时变量,就叫temp吧。
2> 然后调用拷贝构造函数把a的值给temp。整个这两个步骤有点像:A temp(a)
3> 在函数执行到最后先析构a局部变量。
4> 等gfoo2函数执行完后 再析构掉临时temp对象。
结果:
全局函数foo2
A的构造函数被调用
A的析构函数被调用
注意:增加一个成员函数 结果是一样的
A a(1);
a.foo4();
A的构造函数被调用
成员函数foo4
A的构造函数被调用
A的析构函数被调用
A的析构函数被调用
3、对象需要通过另外一个对象进行初始化 a1、a2
A a(1);
A a1 = a;
A a2(a);
二、浅拷贝和深拷贝
系统提供的默认拷贝构造函数就是浅拷贝
/*含有静态成员的拷贝构造函数*/
#include <iostream>
using namespace std;
class A{
public:
A(){ sn++; }
~A(){ sn--; }
static int GetValue(){
return sn;
}
private:
static int sn;
};
int A::sn = 0;
int main(void)
{
A a1;
cout<<"1:"<<A::GetValue()<<endl;
A a2(a1);
cout<<"2:"<<A::GetValue()<<endl;
return 0;
}
这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象a1,输出此时的对象个数,然后使用a1复制出对象a2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。
原因:拷贝构造函数没有处理静态数据成员
再看下面程序:输出为 2
/*含有静态成员的拷贝构造函数*/
#include <iostream>
using namespace std;
class A{
public:
A(){ sn++; }
~A(){ sn--; }
static int GetValue(){
return sn;
}
A (const A&a){
sn++;
}
private:
static int sn;
};
int A::sn = 0;
int main(void)
{
A a1;
cout<<"1:"<<A::GetValue()<<endl;
A a2(a1);
cout<<"2:"<<A::GetValue()<<endl;
return 0;
}
结论:
浅拷贝是以字节的方式复制,申请的是同一块内存区域、如果是指针在析构的时候,同一块内存就会出现释放两次的情况
三、防止默认拷贝构造函数---
声明一个私有拷贝构造函数,甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,
如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
四、拷贝构造函数和赋值运算符的区别:
调用系统默认的拷贝构造函数,不再新分配资源内存。深拷贝:调用自己的拷贝构造函数,分配新的资源内存。
拷贝构造函数用已存在的对象创建一个相同的新对象。而赋值运算符用已存在的对象赋予一个已存在的同类对象。
拷贝构造函数发生在创建对象时,只发生一次拷贝赋值可以重复发生;
实例代码:
/* Sample实例 */
#include <iostream>
using namespace std;
class Sample{
public:
Sample(void):m_n(*new int(0)),m_ch('a'){}
Sample(int n,char ch):m_n(*new int(n)),m_ch(ch){}
~Sample(){
delete &m_n;
}
Sample(const Sample&sample):m_n(*new int(sample.m_n)),m_ch(sample.m_ch){}
Sample&operator =(const Sample& sample){
if(&sample != this){
m_n = sample.m_n;
return *this;
}
}
void printInfo(void){
cout<<m_n<<","<<m_ch<<endl;
}
private:
int &m_n;
const char m_ch;
};
int main(void)
{
Sample s1;
s1.printInfo();
Sample s2(100,'B');
s2.printInfo();
return 0;
}
拷贝构造函数可以借鉴:http://blog.csdn.net/lwbeyond/article/details/6202256 点击打开链接
=============================================================================================================
第二部分:拷贝赋值运算符
1、赋值运算符为什么要返回引用
一、c/c++赋值运算符的本意为“返回左值的引用”(左值:赋值号左面的变量而非其值)
实例:
int a, b = 3, c = 2;
(a = b) = c;
cout<<a<<b<<c<<endl;
结果:232
二、为了进行连续赋值,即 x = y = z
1、赋值返回引用
x = y = z 先执行y = z,返回y的引用,执行x = y
2、赋值不返回引用
x = y = z 先执行y = z,返回用y初始化的临时对象(注意临时对象都是常对象),再执行x = y的临时对象(要求operator=(const X&)
返回用x初始化的临时对象(此处要求拷贝构造函数必须为X(const X&) )。
所以也并非必须返回引用,返回引用的好处既可以于赋值的原始语义已知,又可避免拷贝构造函数和析构函数的调用。
下面介绍类的赋值运算符
1.C++中对象的内存分配方式
在C++中,对象的实例在编译的时候,就需要为其分配内存大小,因此系统都是在stack上为其分配内存的。因此,在C++中,只要申明该实例,在程序编译后,就要为其分配相应的内存空间,至于实体内的各个域的值,就由其构造函数决定了。
例如:
class A
{
public:
A()
{
}
A(int id,char *t_name)
{
_id=id;
name=new char[strlen(t_name)+1];
strcpy(name,t_name);
}
private:
char *username;
int _id;
}
int main()
{
A a(1,"herengang");
A b;
}
在程序编译之后,a和b在stack上都被分配相应的内存大小。只不过对象a的域都被初始化,而b则都为随机值。
其内存分配如下:
2. 缺省情况下的赋值运算符
如果我们执行以下:
b=a;
则其执行的是缺省定义的缺省的赋值运算。所谓缺省的赋值运算,是指对象中的所有位于stack中的域,进行相应的复制。但是,如果对象有位于heap上的域的话,其不会为拷贝对象分配heap上的空间,而只是指向相同的heap上的同一个地址。
执行b=a这样的缺省的赋值运算后,其内存分配如下:
因此,对于缺省的赋值运算,如果对象域内没有heap上的空间,其不会产生任何问题。但是,如果对象域内需要申请heap上的空间,那么在析构对象的时候,就会连续两次释放heap上的同一块内存区域,从而导致异常。
~A()
{
delete name;
}
3.解决办法--重载(overload)赋值运算符
因此,对于对象的域在heap上分配内存的情况,我们必须重载赋值运算符。当对象间进行拷贝的时候,我们必须让不同对象的成员域指向其不同的heap地址--如果成员域属于heap的话。
因此,重载赋值运算符后的代码如下:
class A
{
public:
A()
{
}
A(int id,char *t_name)
{
_id=id;
name=new char[strlen(t_name)+1];
strcpy(name,t_name);
}
A& operator =(A& a)
//注意:此处一定要返回对象的引用,否则返回后其值立即消失!
{
if(name!=NULL)
delete name;
this->_id=a._id;
int len=strlen(a.name);
name=new char[len+1];
strcpy(name,a.name);
return *this;
}
~A()
{
cout<<"~destructor"<<endl;
delete name;
}
int _id;
char *name;
};
int main()
{
A a(1,"herengang");
A b;
b=a;
}
其内存分配如下:
这样,在对象a,b退出相应的作用域,其调用相应的析构函数,然后释放分别属于不同heap空间的内存,程序正常结束。
=======================================================================================================
第三部分:关于拷贝构造函数、拷贝赋值函数的写法
类的深拷贝函数的重载
public class A
{
public:
...
A(A &a);//重载拷贝函数
A& operator=(A &b);//重载赋值函数
//或者 我们也可以这样重载赋值运算符void operator=(A &a);即不返回任何值。如果这样的话,他将不支持客户代买中的链式赋值 ,例如a=b=c will be prohibited!
private:
int _id;
char *username;
}
A::A(A &a)
{
_id=a._id;
username=new char[strlen(a.username)+1];
if(username!=NULL)
strcpy(username,a.usernam);
}
A& A::operaton=(A &a)
{
if(this==&a)// 问:什么需要判断这个条件?(不是必须,只是优化而已)。答案:提示:考虑a=a这样的操作。
return *this;
if(username!=NULL)
delete username;
_id=a._id;
username=new char[strlen(a.username)+1];
if(username!=NULL)
strcpy(username,a.usernam);
return *this;
}
//另外一种写法:
void A::operation=(A &a)
{
if(username!=NULL)
delete username;
_id=a._id;
username=new char[strlen(a.username)+1];
if(username!=NULL)
strcpy(username,a.usernam);
}
其实,从上可以看出,赋值运算符和拷贝函数很相似。只不过赋值函数最好有返回值(进行链式赋值),返回也最好是对象的引用(为什么不是对象本身呢?note2有讲解), 而拷贝函数不需要返回任何。同时,赋值函数首先要释放掉对象自身的堆空间(如果需要的话),然后进行其他的operation.而拷贝函数不需要如此,因为对象此时还没有分配堆空间。
note1:
不要按值向函数传递对象。如果对象有内部指针指向动态分配的堆内存,丝毫不要考虑把对象按值传递给函数,要按引用传递。并记住:若函数不能改变参数对象的状态和目标对象的状态,则要使用const修饰符
note2:问题:
对于类的成员需要动态申请堆空间的类的对象,大家都知道,我们都最好要overload其赋值函数和拷贝函数。拷贝构造函数是没有任何返回类型的,这点毋庸置疑。 而赋值函数可以返回多种类型,例如以上讲的void,类本身class1,以及类的引用 class &?
问:这几种赋值函数的返回各有什么异同?
答:
1 如果赋值函数返回的是void ,我们知道,其唯一一点需要注意的是,其不支持链式赋值运算,即a=b=c这样是不允许的!
2 对于返回的是类对象本身,还是类对象的引用,其有着本质的区别!
第一:如果其返回的是类对象本身。
A operator =(A& a)
{
if(name!=NULL)
delete name;
this->_id=a._id;
int len=strlen(a.name);
name=new char[len+1];
strcpy(name,a.name);
return *this;
}
其过程是这样的:
class1 A("herengnag");
class1 B;
B=A;
看似简单的赋值操作,其所有的过程如下:
1 释放对象原来的堆资源
2 重新申请堆空间
3 拷贝源的值到对象的堆空间的值
4 创建临时对象(调用临时对象拷贝构造函数),将临时对象返回
5. 临时对象结束,调用临时对象析构函数,释放临时对象堆内存
my god,还真复杂!!但是,在这些步骤里面,如果第4步,我们没有overload 拷贝函数,也就是没有进行深拷贝。那么在进行第5步释放临时对象的heap 空间时,将释放掉的是和目标对象同一块的heap空间。这样当目标对象B作用域结束调用析构函数时,就会产生错误!!
因此,如果赋值运算符返回的是类对象本身,那么一定要overload 类的拷贝函数(进行深拷贝)!
第二:如果赋值运算符返回的是对象的引用,
A& operator =(A& a)
{
if(name!=NULL)
delete name;
this->_id=a._id;
int len=strlen(a.name);
name=new char[len+1];
strcpy(name,a.name);
return *this;
}
那么其过程如下:
1 释放掉原来对象所占有的堆空间
1.申请一块新的堆内存
2 将源对象的堆内存的值copy给新的堆内存
3 返回源对象的引用
4 结束。
因此,如果赋值运算符返回的是对象引用,那么其不会调用类的拷贝构造函数,这是问题的关键所在!!