日后再整理
对一个简单变量的初始化方法是用一个常量或变量初始化另一个变量,例如:
int m = 80;如果一个类中有指针成员,使用缺省的复制构造函数初始化对象就会出现问题。为了说明存在的问题,我们假定对象A与对象B是相同的类,有一个指针成员,指向对象C。当用对象B初始化对象A时,缺省的复制构造函数将B中每一个成员的值复制到A的对应的成员当中,但并没有复制对象C。也就是说,对象A和对象B中的指针成员均指向对象C
注意:复制构造函数就是把一个对象复制给另一个对象时调用的构造函数
当类中没有指针类型的数据成员时可以通过调用默认的构造函数(是按位拷贝,所以两个对象指针所指向相同的内存,当对象被销毁时,会调用各自的析构函数,这样也就把这块内存释放了两次,引发错误)就可以。
而类中有指针类型的数据成员事就要调用自己写的拷贝构造函数 避免指针所指向的内存被释放两次!!
拷贝构造函数首先是一个构造函数,它调用的时候产生一个对象,是通过参数传进来的那个对象来初始化,产生的对象。 operator=();是把一个对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话就不做任何操作。
还要注意的是拷贝构造函数是构造函数,不返回值
而赋值函数需要返回一个对象自身的引用,以便赋值之后的操作 链式操作
你肯定知道这个:
int a, b;
b = 7;
Func( a = b ); // 把i赋值后传给函数Func( int )
同理:
CMyClass obj1, obj2;
obj1.Initialize();
Func2( obj1 = obj2 ); //如果没有返回引用,是不能把值传给Func2的
注:
CMyClass & CMyClass:: operator = ( CMyClass & other )
{
if( this == &other )
return *this;
// 赋值操作...
return *this
}
调用拷贝构造函数的生成对象的方式是,用一个以有的对象的样子去构造一个新的各属性值一样的对象。如有类定义如下:
在C++语言里,
String s2(s1);
String s3 = s1;
只是语法形式的不同,意义是一样的,都是定义加初始化,都调用拷贝构造函数。
#include
#include
using namespace std;
class A
{
private :
int param1;
char param2;
public :
A()
{
cout<<"default construct function"<
A(int p1 , char p2)
{
this->param1 = p1;
this->param2 = p2;
cout<<"general construct function"<
A(A &other)
{
this->param1 = other.param1;
this->param2 = other.param2;
cout<<"copy construct function"<
void print(void)
{
cout<<"param1 = "<
A& operator=(A &other)
{
this->param1 = other.param1;
this->param2 = other.param2;
cout<<"overload operate = function"<
}
};
int main(int argc, char * argv[])
{
A obj1(1,'A');//定义一个对像obj1确定属性由参数传入,调用普通构造函数。
obj1.print();//打印对象obj1的私有属性。
A obj2(obj1);//用对象obj1去生成一个属性一样的对象obj2,调用拷贝构造函数。
obj2.print();
A obj3;//调用默认构造函数。
obj3 = obj1;//调用赋值运算符重载函数。
obj3.print();
system("PAUSE");
return 0;
}
总结:拷贝构造函数发生在下面三种情况下:
在C++中,下面三种对象需要调用拷贝构造函数:
1) 一个对象以值传递的方式传入函数体;
2) 一个对象以值传递的方式从函数返回;
3) 一个对象需要通过另外一个对象进行初始化;
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源但复制过程并未复制资源的情况视为浅拷贝。
#include
using namespace std;
#include
class Namelist
{
char *name;
public:
Namelist (char *p)//带参数的构造函数
{
name= new char[strlen(p)+1];
if (name!=0) strcpy(name,p);
}
Namelist(){ };//缺省构造函数
Namelist(Namelist&);//拷贝构造函数
Namelist& operator=(char *p );
Namelist& operator=( Namelist& );
void display(){cout<
{
delete [] name ;
}
};
Namelist::Namelist(Namelist& a)//定义拷贝构造函数
{
name= new char[strlen(a.name)+1];
if (name!=0) strcpy(name,a.name);
}
Namelist& Namelist::operator=( char *p)//
//第一个重载赋值运算符,完成用常量给对象赋值
{
name= new char[strlen(p)+1];
if (name!=0) strcpy(name,p);
return *this;
}
Namelist& Namelist::operator=( Namelist& a)
//第二个重载赋值运算符,完成类对象之间的赋值
{
if (this!=&a)//首先查看是否赋值给自身,如果不是
{
delete[] name;//那么重新给它分配所需空间并拷贝字符串,保证目标对象和原对象各自拥有自己的拷贝字符串,从而完成深拷贝
name=new char[strlen(a.name)+1];
if (name!=0) strcpy(name,a.name);
}
return *this;
}
int main()
{
Namelist n1("first object"),n2("second object");//调用带参数的构造函数
Namelist n3;//缺省构造函数
cout<<"赋值前的数据:"<
n2.display();
n3="third object";//调用第一个重载赋值运算符函数
n2=n1;//调用第二个重载赋值运算符函数
Namelist n4(n2);//调用拷贝构造函数函数
Namelist n5=n3;//调用拷贝构造函数函数
Namelist n6="sixth object";//调用带参数的构造函数等同于()的方法
cout<<"赋值后的数据:"<
n2.display();
n3.display();
n4.display();
n5.display();
return 0;
}
———————————————————————————————————————————————————————————————————
也许很多C++的初学者都知道什么是构造函数,但是对复制构造函数(copy constructor)却还很陌生。也许写代码的时候能用得上复制构造函数的机会并不多,不过这并不说明复制构造函数没什么用,其实复制构造函数能解决一些我们常常会忽略的问题。
我们经常使用函数,传递过各种各样的参数给函数,不过把对象(注意是对象,而不是对象的指针或对象的引用)当作参数传给函数的情况我们应该比较少遇见吧,而且这个对象的构造函数还涉及到一些内存分配的操作。嗯,这样会有什么问题呢?
把参数传递给函数有三种方法,一种是值传递,一种是传地址,还有一种是传引用。前者与后两者不同的地方在于:当使用值传递的时候,会在函数里面生成传递参数的一个副本,这个副本的内容是按位从原始参数那里复制过来的,两者的内容是相同的。当原始参数是一个类的对象时,它也会产生一个对象的副本,不过在这里要注意。一般对象产生时都会触发构造函数的执行,但是在产生对象的副本时却不会这样,这时执行的是对象的复制构造函数。为什么会这样?嗯,一般的构造函数都是会完成一些成员属性初始化的工作,在对象传递给某一函数之前,对象的一些属性可能已经被改变了,如果在产生对象副本的时候再执行对象的构造函数,那么这个对象的属性又再恢复到原始状态,这并不是我们想要的。所以在产生对象副本的时候,构造函数不会被执行,被执行的是一个默认的构造函数。当函数执行完毕要返回的时候,对象副本会执行析构函数,如果你的析构函数是空的话,就不会发生什么问题,但一般的析构函数都是要完成一些清理工作,如释放指针所指向的内存空间。这时候问题就可能要出现了。
假如你在构造函数里面为一个指针变量分配了内存,在析构函数里面释放分配给这个指针所指向的内存空间,那么在把对象传递给函数至函数结束返回这一过程会发生什么事情呢?首先有一个对象的副本产生了,这个副本也有一个指针,它和原始对象的指针是指向同块内存空间的。函数返回时,对象的析构函数被执行了,即释放了对象副本里面指针所指向的内存空间,但是这个内存空间对原始对象还是有用的啊,就程序本身而言,这是一个严重的错误。然而错误还没结束,当原始对象也被销毁的时候,析构函数再次执行,对同一块系统动态分配的内存空间释放两次是一个未知的操作,将会产生严重的错误。
上面说的就是我们会遇到的问题。解决问题的方法是什么呢?首先我们想到的是不要以传值的方式来传递参数,我们可以用传地址或传引用。没错,这样的确可以避免上面的情况,而且在允许的情况下,传地址或传引用是最好的方法,但这并不适合所有的情况,有时我们不希望在函数里面的一些操作会影响到函数外部的变量。那要怎么办呢?可以利用复制构造函数来解决这一问题。复制构造函数就是在产生对象副本的时候执行的,我们可以定义自己的复制构造函数。在复制构造函数里面我们申请一个新的内存空间来保存构造函数里面的那个指针所指向的内容。这样在执行对象副本的析构函数时,释放的就是复制构造函数里面所申请的那个内存空间。
除了将对象传递给函数时会存在以上问题,还有一种情况也会存在以上问题,就是当函数返回对象时,会产生一个临时对象,这个临时对象和对象的副本性质差不多。
拷贝构造函数,经常被称作X(X&),是一种特殊的构造函数,他由编译器调用来完成一些基于同一类的其他对象的构件及初始化。它的唯一的一个参数(对象的引用)是不可变的(因为是const型的)。这个函数经常用在函数调用期间于用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。
在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。
1).一个对象以值传递的方式传入函数体
2).一个对象以值传递的方式从函数返回
3).一个对象需要通过另外一个对象进行初始化
以上的情况需要拷贝构造函数的调用。如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作赋共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。
拷贝构造函数不可以改变它所引用的对象,其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环。
除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。换句话说,你从函数返回得到的只是对象的一份拷贝。但是同样的,拷贝构造函数被正确的调用了,你不必担心。
如果在类中没有显式的声明一个拷贝构造函数,那么,编译器会私下里为你制定一个函数来进行对象之间的位拷贝(bitwise copy)。这个隐含的拷贝构造函数简单的关联了所有的类成员。许多作者都会提及这个默认的拷贝构造函数。注意到这个隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对于成员的关联方式。显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数除非另外一个构造函数在类初始化或者在构造列表的时候被调用。
拷贝构造函数是程序更加有效率,因为它不用再构造一个对象的时候改变构造函数的参数列表。设计拷贝构造函数是一个良好的风格,即使是编译系统提供的帮助你申请内存默认拷贝构造函数。事实上,默认拷贝构造函数可以应付许多情况。
附另外一篇关于复制构造函数的文章:
对一个简单变量的初始化方法是用一个常量或变量初始化另一个变量,例如:
int m = 80;
int n = m;
我们已经会用构造函数初始化对象,那么我们能不能象简单变量的初始化一样,直接用一个对象来初始化另一个对象呢?答案是肯定的。我们以前面定义的Point类为例:
Point pt1(15, 25);
Point pt2 = pt1;
后一个语句也可以写成:
Point pt2( pt1);
它是用pt1初始化pt2,此时,pt2各个成员的值与pt1各个成员的值相同,也就是说,pt1各个成员的值被复制到pt2相应的成员当中。在这个初始化过程当中,实际上调用了一个复制构造函数。当我们没有显式定义一个复制构造函数时,编译器会隐式定义一个缺省的复制构造函数,它是一个内联的、公有的成员,它具有下面的原型形式:
Point:: Point (const Point &);
可见,复制构造函数与构造函数的不同之处在于形参,前者的形参是Point对象的引用,其功能是将一个对象的每一个成员复制到另一个对象对应的成员当中。
虽然没有必要,我们也可以为Point类显式定义一个复制构造函数:
Point:: Point (const Point &pt)
{
xVal=pt. xVal;
yVal=pt. yVal;
}
如果一个类中有指针成员,使用缺省的复制构造函数初始化对象就会出现问题。为了说明存在的问题,我们假定对象A与对象B是相同的类,有一个指针成员,指向对象C。当用对象B初始化对象A时,缺省的复制构造函数将B中每一个成员的值复制到A的对应的成员当中,但并没有复制对象C。也就是说,对象A和对象B中的指针成员均指向对象C,实际上,我们希望对象C也被复制,得到C的对象副本D。否则,当对象A和B销毁时,会对对象C的内存区重复释放,而导致错误。为了使对象C也被复制,就必须显式定义复制构造函数。下面我们以string类为例说明,如何定义这个复制构造函数。
例10-11
class String
{
public:
String(); //构造函数
String(const String &s); //复制构造函数
~String(); //析构函数
// 接口函数
void set(char const *data);
char const *get(void);
private:
char *str; //数据成员ptr指向分配的字符串
};
String ::String(const String &s)
{
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
我们也常用无名对象初始化另一个对象,例如:
Point pt = Point(10, 20);
类名直接调用构造函数就生成了一个无名对象,上式用左边的无名对象初始化右边的pt对象。
构造函数被调用通常发生在以下三种情况,第一种情况就是我们上面看到的:用一个对象初始化另一个对象时;第二种情况是当对象作函数参数,实参传给形参时;第三种情况是程序运行过程中创建其它临时对象时。下面我们再举一个例子,就第二种情况和第三种情况进行说明:
Point foo(Point pt)
{
…
return pt;
}
void main()
{
Point pt1 = Point(10, 20);
Point pt2;
…
pt2=foo(pt);
…
}
在main函数中调用foo函数时,实参pt传给形参pt,将实参pt复制给形参pt,要调用复制构造函数,当函数foo返回时,要创建一个pt的临时对象,此时也要调用复制构造函数。
缺省的复制构造函数
在类的定义中,如果没有显式定义复制构造函数,C++编译器会自动地定义一个缺省的复制构造函数。
————————————————————————————————————————————————————————————————————
复制构造函数,直接初始化,复制初始化,赋值,临时对象 复制构造函数应弄清的几个问题:何时调用复制构造函数,复制构造函数有何功能,为什么要定义自已的复制构造函数。
1.复制构造函数:当用户没有定义自已的复制构造函数时系统将生成一个默认的复制构造函数。当按值传递对象时,就会创建一个形参的临时对象,然后调用复制构造函数把临时对象的值复制给实参。
2.默认复制构造函数的功能:将一个对象的非静态成员的值逐个复制给另一个对象,注意复制的是成员的值,这种复制方式也称为浅复制。因为静态成员属于整个类,而不属于某个对象,所以调用复制构造函数时静态成员不会受到影响。
3.何时生成临时对象:情形1:按值传递对象注意是按值传递对象,按值传递意味着会创建一个原始对象的副本,
情形2:函数反回对象时。
情形3:用一个对象初始化另一个对象时即复制初始化,语句hyong x=y和hyong x=hyong(y)这里y是hyong类型的对象。都将调用复制构造函数,但有可能创建临时对象也有可能不创建临时对象而用复制构造函数直接初始化对象,这取决于编译器。 4.临时对象是由复制构造函数创建的,当临时对象消失时会调用相应的析构函数。也就是说只要创建了临时对象就会多调用一次析构函数。 5.何时使用复制构造函数:按值传递对象,函数反回对象,用一个对象初始化另一个对象即复制初始化时,根据元素初始化列表初始化数组元素。这四种情况都将调用复制构造函数。记住,复制构造函数只能用于初始化,不能用于赋值,赋值时不会调用复制构造函数,而是使用赋值操作符。 6.直接初始化:直接初始化是把初始化式放在圆括号中的,对于类类型来说,直接初始化总是调用与实参匹配的构造函数来初始化的, 7.复制初始化与复制构造函数:复制初始化使用=等于符号来初始 化,复制初始化也是创建一个新对象,并且其初值来自于另一个已存在的对象,复制初始化总是调用复制构造函数来初始化的,复制初始化时首先使用指定的构造函 数创建一个临时对象,然后用复制构造函数将临时对象的每个非static成员依次的复制到新创建的对象。复制构造函数执行的是逐个成员初始化。注意这里是 用一个已存在的对象创建另一个新对象,与用构造函数直接创建一个新对象不一样,使用构造函数初始化时不会使用另一个对象。比如有类hyong,则语句 hyong m(1,2)调用构造函数直接初始化,而语句hyong n=m则是用已存在的对象m去初始化一个新对象n,属于复制初始化。 8.理解赋值与复制初始化的区别(重点):赋 值是在两个已存在的对象间进行的,也就是用一个已存在的对象去改变另一个已存在对象的值。赋值将调用赋值操作符对对象进行操作,赋值操作符将在操作符重载 中讲解。比如有类hyong,有语句hyong x(1);hyong y(1,2)则x=y;这就是赋值,因为对象x和y是已经存在的对象,而语句hyong x=y;则是复制初始化,是用一个已存在的对象y去创建一个新对象x,所以是复制初始化。 9.复制初始化和赋值是在两个对象之间进行的操作,而直接初始化则不是。 10.注意:使用复制构造函数不一定创建临时对象就如语句 hyong x=hyong(y),其中y是hyong类型的对象,就有可能不创建临时对象,这取决于编译器。这里如果创建了临时对象则当临时对象消亡时将调用一次析 构函数,而如果没有调用而是直接用复制构造函数初始化对象的就不会调用析构函数。 11.复制构造函数的形式:hyong(const hyong & obj);它接受一个指向类对象的常量引用作为参数。定义为const是必须的,因为复制构造函数只是复制对象,所以没必要改变传递来的对象的值,声明为 引用可以节省时间,如果是按值传递的话就会生成对象的副本,会浪费资源,而引用就不会。
*****12.为什么需要定义自已的复制构造函数:如果类只包含类类型 成员和内置类型的成员,则可以不用显示定义复制构造函数。但如果类中包含有指针或者有分配其他类型资源时就必须重新定义复制构造函数。因为类中有指针成 员,当把用一个对象初始化另一个对象时,这时两个对象中的指针都指向同一段内存,这时如果其中一个对象被消毁了,这时对象中指针所指向的内存也同样被消 毁,但另一个对象确不知道这种情况,这时就会出现问题。比如hyong类中含有一个成员指针p,当声明了hyong x=y其中y也是hyong类的对象,这时对象x和y中的指针成员p都指向同一段内存,而如果y被消毁,但x还没被消毁时就会出问题,这时y中对象的成员 指针p已经释放了该内存资源,而x中的成员指针p还不知道已经释放了该资源,这时就会出问题。因为对象x和y中的成员指针共享同一段内存,所以对y中的成 员指针p的修改就会影响到对象x中的成员指针。所有这些情况都需要重定义复制构造函数来显示的初始化成员的值,这种初始化方式也被称为深度复制。
13.如果显示定义了复制构造函数则调用显示复制构造函数来直接初始化对象,如果没有显示定义复制构造函数,则调用默认的复制构造函数直接初始化对象。
14.注意:1.在VC++中语句hyong n=m不生成临时对象,但如果显示定义了复制构造函数则调用显示复制构造函数来直接初始化对象n,如果没有显示定义复制构造函数,则调用默认的复制构造函数直接初始化对象n。 2.在VC++中语句hyong m1=hyong(m)有可能生成临时对象也有可能不生成临时对象,如果显示定义了复制构造函数则用复制构造函数直接初始化对象m1,不生成临时对象。如果没有显示定义复制构造函数则复制构造函数将创造临时对象,初始化对象m1 15.C++自动提供的成员函数,有:默认构造函数,复制构造函数,默认析构函数,赋值操作符,地址操作符即this指针,这五种函数如果用户没有定义,则系统会自动创建一个。 16.直接调用类中的构造函数:可以在类中的函数,类外的独立函 数,即main()函数中直接调用某一个类的构造函数,比如在main函数中可以有语句n=A(4);这里n是类A的对象,这里就是直接调用类A的构造函 数创建一个类A的临时对象,然后把该临时对象的值赋给类A的对象n。在类中的函数和在类外的函数调用类的构造函数的方法和这里类似。注意语句n.A(4) 是错误的语句,不能由对象调用类中的构造函数。 例:复制构造函数的使用 class hyong {public: int a,b,c; hyong(){a=b=c=0;cout<<"gouchao"<<"\n";} hyong(int i){a=b=c=i;cout<<"gouchao2"<<"\n";} ~hyong(){cout<<"xigou"<<"\n";} hyong(const hyong &obj){a=b=c=9;cout<<"fuzi"<<"\n";} //复制构造函数。}; void h(hyong k){cout<<"haoshu"<