类的构造函数、析构函数、复制构造函数和赋值操作符
作者:lyb661 时间:20140318
首先,回顾一下有关函数的知识。
函数声明一般由三个部分组成:返回类型、函数名和由圆括号括起来的参数表。
(1)函数可以没有返回类型,这时返回类型由void表示;
(2)函数名标志函数的接口,也是与其他函数区别的标志;
(3)函数可以没有参数,也可以有一至多个参数;
(4)可以根据参数类型、数量的差别重载函数。例如:
int sum(int a,int b);
void write(char c);
void display()const;
重载函数的例子:
void swap(int x,int y);
void swap(double x,double y);
上面是一些带有内置类型参数的函数的例子。类类型同样有与自己相关的函数。其中有几个特殊的成员函数,对类来说至为重要。
1、类的构造函数(consructor):构造函数用于创建类对象时完成必要的初始化工作。所有的类都需要构造函数。它没有返回类型,函数与类同名,可以没有,也可以有多个参数,如果必要。
//////////////////////////////////
//【例1】Data_class1
#include
using std::cout;
using std::endl;
class Data
{
int value;
public:
Data(int initial=0):value(initial){} //构造函数constructor
int read()const{return value;}
void write(int i){value=i;}
};
int main()
{
Data a(10);
cout< Data b; cout< b.write(20); cout< return 0; } 运行结果: 10 20 ////////////////////////////////// 关于构造函数有以下几点: (1)对于任何类,编译器会自动调用一个默认的构造函数; (2)如果定义了自己的构造函数,则默认的构造函数将不再工作; (3)同一般的函数一样,构造函数也可以被重载。 (4)可以定义默构造函数,如编译器合成的那样工作。 Data(int initial=0):value(initial){}就是一个重载的有默认值的构造函数。当创建对象a(10)时只有调用它才能正常工作。注意到该构造函数的参数有个默认值"0"。如果没胡这个0,则对象b就不能被建立。它代替默认构造函数的部分工作。 对于这个简单的Data类,它的默认构造函数应该是这样:Data(){}。这个由编译器合成的构造函数用于创建一个对象而不显式地初始化。如果用这个默认构造函数替换我们自己定义的构造函数,则a对象的建立是无效的,而对象b的成员初始值是无定义的,有兴趣的话可以自己试一试。当然,也可以用成员函数read()来完成一些初始化工作。 总之,像这样一个简单的类,默认构造函数的工作也不是尽如人意的。 对于复杂一点的类呢? ////////////////////////////////// //【例2】Data_class2(警告:本程序有内存泄漏风险不要上机运行) #include using std::cout; using std::endl; class Data { int *ip; public: Data(int &i):ip(new int(i)){} ~Data(){delete ip;} //destructor int read()const{return *ip;} void write(int n){*ip=n;} }; int main() { int obj=10; Data a(obj); cout< Data b=a; cout< return 0; } ////////////////////////////////// 本例是上一例的改进,由于类具有指针成员,并且申请自由存储,所以重新定义了析构函数的行为,用于删除指针。 2、类的析构函数(destructor) :析构函数与类同名,在名字前加符号~以与构造函数相区别。 析构函数用于在类对象超出作用域时按类中声明次序的逆序撤销各个成员。与构造函数有所不同:尽管类定义了自己的析构函数,编译器合成的析构函数也会正常工作。由于函数重载规则的限制,析构函数是不能被重载的。为什么?我们所谓自定义的意思,不过在它常规工作量外给它加了点的担子。 对于上面的Data类来说,它的默认的析构函数~Data(){}的工作并不能令人满意。该类有一个int类型的指针成员,在程序运行过程中由该指针申请了自由存储,所以必须定义一个析构函数,在程序结束后删除指针,进而撤销自由存储。这个工作默认的析构函数是不能胜任的。 本程序潜在的风险当然还不只这些。当对象b建立的时候,b从a复制了指针成员。由于只是复制指针的值,这两个指针共享一个int变量。因为析构对象与创建对象的顺序相反,后建立的对象首先被析构(“后进先出”last int first out)。当析构函数将b的指针删除,然后这个int变量也将撤销,致使a的指针不再指向任何对象,而成了悬垂指针。而当这个指针被删除时,同一块内存撤销了两次,内存泄漏的风险就在这里。 一般将上述复制行为称为浅复制,而规避风险的办法就是定义一个复制构造函数来完成深层复制。 ////////////////////////////////// //【例3】Data_class3 #include using std::cout; using std::endl; class Data { int *ip; public: Data(int &i):ip(new int(i)){} ~Data(){delete ip;} Data(const Data&); int read()const{return *ip;} void write(int n){*ip=n;} }; Data::Data(const Data &dt) { ip=new int(*dt.ip); } int main() { int obj=10; Data a(obj); cout< Data b=a; cout< b.write(20); cout<<"a:"< return 0; } 运行结果: 10 10 a:10,b:20 ////////////////////////////////// 3、类的复制构造函数(copy constructor) 从上边运行结果,a与b的指针成员不同,指向的变量也不同,复制后各归各家,不再纠缠不清。 Data::Data(const Data &dt);就是所谓的复制构造函数。它也与类同名,参数是同类的常引用。与默认构造函数一样,复制构造函数可由编译器隐式调用。 也就是说第例2的程序中,编译器合成了复制构造函数,只是不能有效工作罢了。 与合成的默认构造函数不同,即使我们定义了其他构造函数,也会合成复制构造函数。合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。这说明了复制构造函数也不能重载的。不过,我们可以通过编写代码,来规定构造函数的行为,令其有效地工作。 例2中的复制构造函数之所以不能有效地工作,是因为它是浅复制,只复制指针的值,而不涉及指针指向的对象。 有另外的办法来规避内存泄漏的风险,就是对类的指针成员进行引用计数,当指针的引用计数多于1时绝不删除指针,只当引用计数为1,也就是最后一个指针时才删除指针,这就是所谓智能指针。这略复杂些,稍后再讨论。 上边例3实际还有风险存在。 ////////////////////////////////// //【例4】Data_class4 #include using std::cout; using std::endl; class Data { int *ip; public: Data(int i=0):ip(new int(i)){}//constructor //Data(){} //default constructor ~Data(){delete ip;} //destructor Data(const Data&);//copy constructor Data &operator=(const Data&); //assignment operator int read()const{return *ip;} void write(int n){*ip=n;} }; Data::Data(const Data &dt) { ip=new int(*dt.ip); } Data &Data::operator=(const Data &dt) { if(this!=&dt) *ip=*dt.ip; return *this; } int main() { int obj=10; Data a(obj); cout< Data b=a; cout< b.write(20); Data c; c=b;// uses assignment operator c.write(30); cout<<"a:"< return 0; } 运行结果 10 10 a:10,b:20,c:30 ////////////////////////////////// 例4又增加一个赋值操作符。因为类Data中有赋值工作:c=b。如果像常规那样进行赋值,内存泄漏的风险依然存在。 4、类的赋值操作符(assignment operator) :赋值操作符是类成员函数重载操作符的一个的例子。它使应用于一般内置变量的赋值操作符"="一样应用于类对象。就是说,我们可以将一个类对象赋给另一个同类的对象,而它们成员之间的赋值行为将由我们编写的代码来规定。 与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个。类也可以定义自己的赋值操作符。一般而言,如果类需要复制构造函数,它也会需要赋值操作符,同时也需要析构函数。 本例中,默认的赋值操作符不能正常工作,默认的赋值操作符,只复制了指针的值,还属于浅复制。赋值符两边对象的指针纠缠在一起。 Data &Data::operator=(const Data &dt) { ip=dt.ip; return *this; 只有重新定义赋值操作符,才能进行深复制。 Data &Data::operator=(const Data &dt) { if(this!=&dt) *ip=*dt.ip; return *this; } 1、首先用条件语句if(this!=&dt)检验左右操作数是否相同,避免自我赋值; 2、然后通过语句*ip=*dt.ip;完成深复制; 3、最后返回调用对象。 我们的复制控制技术,属于《C++Primer》所谓的值语义:值型副本是独立的:对副本的改变不会影响原有对象。 如果使用计数与共享对象一起存储。需要创建一个单独类指向共享对象并管理使用计数。使用辅助类来管理指针,即智能指针技术。这项技术本身并不复杂,由于篇幅所限,将在以后讨论。 最后,我们用另一段代码来验证本文所阐述的技术,做为结尾。 ////////////////////////////////// //【例5】Data_class5 #include using std::cout; using std::endl; class Data { int *ip; static int count; public: Data(int *i):ip(new int(*i)){++count;}//constructor Data():ip(NULL){++count;} //default constructor ~Data(){cout< Data(const Data&);//copy constructor Data &operator=(const Data&); //assignment operator int read()const{return *ip;} void write(int n){*ip=n;} }; int Data::count=0; Data::Data(const Data &dt) ip=new int(*dt.ip); ++count; } Data &Data::operator=(const Data &dt) { if(this==&dt) return *this; delete ip; ip=new int(*dt.ip); return *this; } int main() { int obj=10; Data a(&obj); cout< Data b=a; cout< b.write(20); Data c; c=b;// uses assignment operator c.write(30); cout<<"a:"< < return 0; } 这里添加了一点新内容: 1、用空指针为指针成员类增加一个默认构造函数;以便能创建一个未初始化的类对象,并且在后面编写赋值操作时,可以删除它的指针。因为删除空指针是安全的。 2、在析构函数里输出引用计数来验证它是否正常工作,这与共享对象指针管理中的引用计数无关; 3、赋值操作符的代码也与上例不同。这样更好理解:先验证左右操作数是否相同;然后,删除左操作数,并将右操作数赋给它;最后,返回调用对象自身。本质上和以前没多少区别。 ////////////////////////////////// C#默认构造函数的作用 本文详细介绍C#默认构造函数的作用 构造函数主要用来初始化对象。它又分为静态(static)和实例(instance)构造函数两种类别。大家应该都了解如果来写类的构造函数,这里只说下默认构造函数的作用,以及在类中保留默认构造函数的重要性。实际上,我说错了。正确的说法是:以及在类中保留空参数构造函数的重要性。我们来写一个类A,代码如下: view plaincopy to clipboardprint? public class A { public int Number; //数字 public string Word; //文本 } //在Test类中实例化 public class Test { static void Main() { A a = new A(); //实例化,A()即为类A的默认构造函数 Console.WriteLine(“Number = {0}"nWord = {1}”,a.Number,a.Word); Console.read(); } } 输出的结果是: Number = 0 Word = ******************************* using System; class Point { public int x, y,z; public Point() { x = 0; y = 0; z = 0; } public Point(int x, int y,int z) { //把函数内容补充完整 this.x = x; this.y =y; this.z =z; } public override string ToString() { return(String.Format("({0},{1},{2})", x, y,z)); } } class MainClass { static void Main() { Point p1 = new Point(); Point p2 = new Point(10,20,30); Console.WriteLine("三维中各点坐标:"); Console.WriteLine("点1的坐标为{0}", p1); Console.WriteLine("点2的坐标为{0}", p2); } } ******************************************************************************* ********* C#类的继承,构造函数实现及其调用顺序 类层层派生,在实例化的时候构造函数的调用顺序是怎样的? --从顶层基类开始向子类方向顺序调用无参构造. 默认构造(无参构造)和带参构造什么时候调用?--默认将从顶层父类的默认构造一直调用到当前类的默认构造. 下面是示例: /**//*--===------------------------------------------===--- 作者:许明会 日期:类的派生和构造函数间的关系,调用层次及实现 日期:2008年1月18日 17:30:43 若希望类能够有派生类,必须为其实现默认构造函数. 若类没有实现带参构造,编译器将自动创建默认构造函数. 若类实现了带参构造,则编译器不会自动生成默认构造. --===------------------------------------------===---*/ using System; namespace xumh { public class MyClass { public MyClass () { 有的、已经存在的数据创建出一份新的数据,最终的结果是多了一份相同的数据。例如,将Word 文档拷贝到U盘去复印店打印,将D 盘的图片拷贝到桌面以方便浏览,将重要的文件上传到百度网盘以防止丢失等,都是「创建一份新数据」的意思。 在C++ 中,拷贝并没有脱离它本来的含义,只是将这个含义进行了“特化”,是指用已经存在的对象创建出一个新的对象。从本质上讲,对象也是一份数据,因为它会占用内存。 严格来说,对象的创建包括两个阶段,首先要分配内存空间,然后再进行初始化: ?分配内存很好理解,就是在堆区、栈区或者全局数据区留出足够多的字节。这个时候的内存还比较“原始”,没有被“教化”,它所包含的数据一般是零值或者随机值,没有实际的意义。 ?初始化就是首次对内存赋值,让它的数据有意义。注意是首次赋值,再次赋值不叫初始化。初始化的时候还可以为对象分配其他的资源(打开文件、连接网络、动态分配内存等),或者提前进行一些计算(根据价格和数量计算出总价、根据长度和宽度计算出矩形的面积等)等。说白了,初始化就是调用构造函数。 很明显,这里所说的拷贝是在初始化阶段进行的,也就是用其它对象的数据来初始化新对象的内存。 那么,如何用拷贝的方式来初始化一个对象呢?其实这样的例子比比皆是,string 类就是一个典型的例子。 1.#include 定义类的构造函数 作者:lyb661 时间:20150613 定义类的构造函数有如下几种方法: 1、使用默认构造函数(类不另行定义构造函数):能够创建一个类对象,但不能初始化类的各个成员。 2、显式定义带有参数的构造函数:在类方法中定义,使用多个参数初始化类的各个数据成员。 3、定义有默认值的构造函数:构造函数原型中为类的各个成员提供默认值。 4、使用构造函数初始化列表:这个构造函数初始化成员的方式显得更紧凑。 例如:有一个学生类。其中存储了学生的姓名、学号和分数。 class Student { private: std::string name; long number; double scores; public: Student(){}//1:default constructor Student(const std::string& na,long nu,double sc); Student(const std:;string& na="",long nu=0,double sc=0.0); Student(const std:;string& na="none",long nu=0,double sc=0.0):name(na),number(nu),scores(sc){} ……….. void display() const; //void set(std::string na,long nu,double sc); }; ......... Student::Student(const std::string& na,long nu,double sc) { name=na; number=nu; scores=sc; } void Student::display()const { std::cout<<"Name: "< 拷贝构造函数 一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。 #include C默认构造函数的作用
C++拷贝构造函数(复制构造函数)
定义构造函数的四种方法
(完整版)拷贝构造函数