【C++】C++自学旅程(8):构造函数和析构函数
新引入了类这个概念,你别说突然之间各种概念就多起来麻烦起来了。今天来说说构造函数和析构函数。
一、构造函数和拷贝构造函数
1. 构造函数
简单的说,构造函数就是给类成员初始化的函数。那怎么使用呢?我们直接上代码:
1 //构造函数 2 #include<iostream> 3 using namespace std; 4 class Date 5 { 6 public: 7 int output(); 8 Date(int,int,int); 9 Date(); 10 private: 11 int year,month,day; 12 }; 13 14 Date::Date(int x, int y, int z):day(z) 15 { 16 year=x; 17 month=y; 18 } 19 Date::Date():year(1),month(1) 20 { 21 day=1; 22 } 23 int Date::output() 24 { 25 cout<<"输出:"<<year<<"年"<<month<<"月"<<day<<"日"<<endl; 26 } 27 int main() 28 { 29 Date d1(1997,4,24); 30 Date d2; 31 Date d3; 32 d3=Date(2015,12,13); 33 d1.output(); 34 d2.output(); 35 d3.output(); 36 return 0; 37 }
看代码说话,重点易错内容用圆圈序号标出:
第8、9行的Date(int, int, int)和Date(int)这两个函数就是构造函数,可以发现它与类类型名相同,①构造函数名只能和类类型名相同,且声明时都不能标函数类型(void也不能标)。
看到第14到23行,这里是函数的定义,写明了函数要做什么事情:主要就是初始化赋值。②定义时也不能标注函数类型,同时也不可以有返回值。③构造函数可以有参数也可以没有参数。这个小程序这里使用了函数重载,④构造函数也可以使用函数重载,调用时根据调用的方式确定到底是哪个。在定义构造函数时可以看出,⑤给变量赋值有两种方式,即在参数表圆括号后的“:”后面写变量(值),或是一般的在函数体中间写,运行的顺序是初始化区域→函数体。
接下来说说怎么调用构造函数。构造函数不需要手动调用,看到第29至30行,⑥构造函数会在定义类变量的时候自动调用,调用时根据调用时的参数表格式确定是函数重载重的哪个,特别注意的是定义变量要调用没有参数的构造函数时不要括号,即30行不是“Date d2()”而是“Date d2”。另外嘞,还可以⑦给已经定义声明过的变量重新使用构造函数赋值,方法同第32行,注意等号右边是“Date(......)”,即类型名(......)。
每一个类至少需要一个构造函数,如果用户没有编写构造函数,系统会自动创建一个,只会它不会做任何事情,输出出来就是乱码。所以需要用户自己写咯,⑧一旦有用户自己写的构造函数,调用时就不会调用自己创建的了,所以调用时的参数错了直接就提示:没有定义这个函数了。
2. 拷贝构造函数
拷贝构造函数用于使用一个已经存在的对象来初始化另一个同类型的对象。它具有唯一参数。
1 //构造拷贝函数:例1. 2 #include<iostream> 3 using namespace std; 4 class Date 5 { 6 public: 7 int output(); 8 Date(int, int, int); //构造函数 9 Date(Date &); //拷贝构造函数 10 private: 11 int year,month,day; 12 }; 13 Date::Date(int x,int y,int z) 14 { 15 year=x; 16 month=y; 17 day=z; 18 } 19 Date::Date(Date &d) //拷贝构造函数 20 { 21 year=d.year+1; //拷贝的year值+1 22 month=d.month; 23 day=d.day; 24 } 25 int Date::output() 26 { 27 cout<<"输出:"<<year<<"年"<<month<<"月"<<day<<"日"<<endl; 28 } 29 int main() 30 { 31 Date d1(2016,2,14); 32 Date d2(d1); 33 d1.output(); 34 d2.output(); 35 }
//结果:
输出:2016年2月14日
输出:2017年2月14日
可以看到拷贝构造函数初始化d2时是跟d1的初始化有关联的。拷贝构造函数在一下三种情况中会被调用:
①一个对象通过另一个对象初始化,如上面的例1.
②一个对象以值传递的方式使用
//例2.
void func(Date d) { date.output(); } int main() { Date d1(2015,6,7); func(d1); }
③一个对象以值传递的方式从函数返回
//例3.
Date fun2() { Date d1(2015,6,8); return d1; } int main() { fun2().output(); }
例2和例3中并没有出现定义拷贝构造函数,是因为用户没有自定义拷贝构造函数,系统会自动弄一个默认的拷贝构造函数,用于传值的过程。
下面说一下浅拷贝和深拷贝。
1 //例4. 2 #include<iostream> 3 #include<cstring> 4 using namespace std; 5 class student 6 { 7 public: 8 student(); 9 void setname (char *n); 10 void outputname(); 11 private: 12 char *name; 13 }; 14 student::student() 15 { 16 name = new char[10]; 17 if(name!=NULL) 18 { 19 strcpy(name,"XiaoMing"); 20 } 21 } 22 void student::setname(char *n) 23 { 24 if(name!=NULL) 25 { 26 strcpy(name,n); 27 } 28 } 29 void student::outputname() 30 { 31 cout<<name<<endl; 32 } 33 34 int main() 35 { 36 student s1,s2(s1); 37 cout<<"s1: "; 38 s1.outputname(); 39 cout<<"s2: "; 40 s2.outputname(); 41 s1.setname("Xiaoxin"); 42 cout<<endl<<"After Change"<<endl; 43 cout<<"s1: "; 44 s1.outputname(); 45 cout<<"s2: "; 46 s2.outputname(); 47 }
这样的结果是:
s1: XiaoMing s2: XiaoMing After Change s1: Xiaoxin s2: Xiaoxin
因为系统自动创建的拷贝构造函数不会自动新创建一个空间,让s2.name指针指向新空间,所以造成的结果是s2name的值随s1的更改而更改。它俩的name指针指向同一个空间。
而通过自定义拷贝构造函数使用深拷贝,可以自定义创建一个新的空间,以解决这个问题。
1 //例5. 2 #include<iostream> 3 #include<cstring> 4 using namespace std; 5 class student 6 { 7 public: 8 student(); 9 student(student &); 10 void setname (char *n); 11 void outputname(); 12 private: 13 char *name; 14 }; 15 student::student() 16 { 17 name = new char[10]; 18 if(name!=NULL) 19 { 20 strcpy(name,"XiaoMing"); 21 } 22 } 23 student::student(student &stu) 24 { 25 name = new char[10]; 26 strcpy(name,stu.name); 27 } 28 void student::setname(char *n) 29 { 30 if(name!=NULL) 31 { 32 strcpy(name,n); 33 } 34 } 35 void student::outputname() 36 { 37 cout<<name<<endl; 38 } 39 40 int main() 41 { 42 student s1,s2(s1); 43 cout<<"s1: "; 44 s1.outputname(); 45 cout<<"s2: "; 46 s2.outputname(); 47 s1.setname("Xiaoxin"); 48 cout<<endl<<"After Change"<<endl; 49 cout<<"s1: "; 50 s1.outputname(); 51 cout<<"s2: "; 52 s2.outputname(); 53 }
这样一来,每个对象都有独立的name资源空间,从而避免了使用混乱的情况。
二、析构函数
从上面的例5可以看出,我们用new创建了一个name的空间,当对象使用完并消亡后,那个创建的空间并没有用delete删去,这样当创建了很多这样的空间后,内存就会不堪重负了,但是每次用完后手动删除是不是太麻烦了,而且也耗费很大的精力。析构函数就是用于在对象消亡后自动释放创建的空间的。用法:
//析构函数取名为“~类名” public: ~student(); student::~student() { delete name; }
注意:①析构函数名称只能是"~类名()",也没有类型也没有返回值;②一个类只能有一个析构函数,若用户没有创建,系统还是自动创建一个不做事的;③析构函数在对象超出作用域时自动运行。
Over!