目录
构造函数
定义
特征
默认构造函数
特性
1.默认构造函数只能有一个。
2.初始化->随机值?
问题
多个非默认构造函数,可以同时存在吗?
析构函数
简述
特性
设想这样两个场景:a.我们在创建变量以后,常常会因为忘记初始化使结果出现随即值…… b.我们一下子创建了100个变量,为了一一初始化它们,得调用100次Init函数…… 是不是想想头都大了?对此,C++的解决方式是:用构造函数来自动初始化。
构造函数是特殊的成员函数(怎么特殊我们一会再说)。它的名字具有误导性,实际上构造函数并不是用来开空间创建对象的,而是初始化对象的。
构造函数的函数名与类名相同。
无返回值。注:不要写void,void代表空返回值。无返回值就是没有返回类型,函数的定义为“函数名+()+{}“
对象实例化时编辑器自动调用对应的构造函数。
下面的例子通过打印成员变量的值,来说明实例化时,的确调用了构造函数。
#include
class Date
{
public:
Date()
{
_year = 2000;
_month = 1;
_day = 1;
}
void print()
{
printf("%d %d %d", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //实例化出对象d1
d1.print(); //打印出d1中的成员变量,观察是否被初始化
putchar('\n');
return 0;
}
运行结果:可见实例化出d1时,编辑器自动调用了Date()函数。
构造函数可以重载。
即允许存在多个具有相同名称但参数列表不同的构造函数。
构造函数的重载可以通过以下几种方式实现:
1.参数个数不同:可以定义多个构造函数,每个构造函数具有不同数量的参数。例如:
class Date {
public:
Date() {
// 无参构造函数
}
Date(int num) {
// 带一个整型参数的构造函数
}
Date(int num1, int num2) {
// 带两个整型参数的构造函数
}
};
注意:在调用无参构造函数时,千万不要在对象后面加括号,不然就成了函数声明!
正确的调用方式为:Date d1;
错误的调用方式为: Date d1(); //这样的含义是:函数d1的声明。该函数无参,返回类型为Date类型
//我们说过,构造函数是特殊的!你不能像对待一般函数那样使用它,这里就是它的特殊点之一
2.参数类型不同:可以定义多个构造函数,每个构造函数具有不同类型的参数。例如:
class Date {
public:
Date() {
// 无参构造函数
}
Date(int num) {
// 带一个整型参数的构造函数
}
Date(double num) {
// 带一个双精度浮点型参数的构造函数
}
};
3.参数顺序不同:可以定义多个构造函数,每个构造函数具有相同类型的参数,但顺序不同。例如:
class Date {
public:
Date() {
// 无参构造函数
}
Date(int num1, int num2) {
// 带两个整型参数的构造函数(参数顺序为num1, num2)
}
Date(int num2, int num1) {
// 带两个整型参数的构造函数(参数顺序为num2, num1)
}
};
如果类中没有显式定义构造函数,那么C++编译器会自动生成一个无参的默认构造函数。一旦用户显式定义了,编译器便不再自动生成。下面的例子说明这一点:
#include
class Date
{
public:
/*Date(int year, int month, int day) //这个构造函数是我们显示定义出来的
{ //如果注释这段代码,那就没有显式定义的函数了,结果会如何呢?
_year = year; //如果放开这段代码,那一定会报错,为什么呢?
_month = month;
_day = day;
}*/
void print()
{
printf("%d %d %d", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.print();
putchar('\n');
return 0;
}
被注释掉的那部分如果不放开:
这里的结果表明:调用了编译器自动生成的无参默认构造函数。至于为什么是随机值?我们一会讲到。
注释掉的那部分如果放开:
报错:不存在默认构造函数!
这是因为:我们实例化对象时,并没有传三个参数过去,这就和类中的构造函数的形参列表不匹配,所以不会调用它。然而编译器也无法自动生成默认构造函数,因为类中已经存在显式构造函数了。所以,d1可以说是两边都不讨好,无法被初始化了。
默认构造函数有三种:无参的构造函数、全缺省的构造函数、(我们没写时)编译器自动生成的构造函数。
我们用这段代码来展示”默认构造函数只能有一个“:
#include
class Date
{
public:
/*Date() //当注释掉这段代码,就只有一个默认构造函数,可以编译通过。
{ //如果放开,就有两个默认构造函数了。能编译通过吗?
_year = 2000;
_month = 1;
_day = 1;
}*/
Date(int year=0, int month=0, int day=0)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
printf("%d %d %d", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.print();
putchar('\n');
return 0;
}
如果注释掉代码:
此时调用的是全缺省的构造函数,即在场唯一的默认构造函数。
如果放开代码:
报错:包含多个默认函数!可见,只能由一个默认构造函数。
我们刚刚留了个疑问,为什么这段代码调用了构造函数,结果还是随机值:
#include
class Date
{
public:
void print()
{
printf("%d %d %d", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.print();
putchar('\n');
return 0;
}
运行结果:
我们知道,在没有显示构造函数时,编译器会自动生成无参的默认构造函数。那为什么还是随机值?这个构造函数看起来并没有什么作用啊……
实际上,的确没什么作用。。。
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。编译器生成的默认构造函数会对自定义类型调用它的默认成员函数,而内置类型不做处理。简言之,只有自定义类型能被初始化,内置类型不能!
看下面的程序,如果控制台输出了"Time()",就证明自定义类型Time的默认构造函数被调用了:
#include
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl; //只要调用了默认构造函数Time(),就会输出"Time()"
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
return 0;
}
运行结果:Time的默认构造函数的确被调用了。
在监视中可以看到,只有自定义类型_time被初始化,内置类型仍为随机值:
不过,C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
这里的代码和上一个一样,只不过我们给内置类型赋上了默认值:
#include
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year=2000; //给内置类型赋上默认值
int _month=0;
int _day=0;
Time _time;
};
int main()
{
Date d1;
return 0;
}
打开监视:发现内置类型也可以被初始化了。
除去我们说到的那三个默认的,其他的,如半缺省的构造函数、没设缺省的构造函数,都不是默认构造函数。它们可以多个同时存在吗?
答案:如果能满足函数重载的话,就能!构造函数的重载允许我们定义多个具有不同参数列表的构造函数。
但注意:如果形参列表相同,单单缺省参数不同,是不能同时存在的。
例:
(问题待补充……)
析构函数的功能与构造函数相反,后者负责初始化,前者负责清理(对象占用的)内存。析构函数不是用来Destroy对象的,出了函数栈帧对象会被编译器销毁。它是负责对象中的资源清理工作,在程序中任职“清洁工”。
析构函数并非能清理一切,对于内置类型,它是不做处理的。它主要用于释放自定义类型所占的空间、释放动态分配的内存、关闭文件等。
为什么不处理内置类型?
因为int,char这些内置类型,当超出作用域是会被自动销毁的,不需要再由析构函数去释放了。
析构函数作为特殊的成员函数,是C++中非常重要的概念。它可以避免内存泄漏的问题,从此你不需要再挨个手动free了。
1.析构函数的函数名是在类名前加上~
2.无参数无返回类型。注:无返回类型就是不写。不要写void !
命名规则与构造函数相同,即以“~类名( )”的格式。
3.一个类只能有一个析构函数。析构函数分两种,一种是程序员自己写的,一种是系统自动生成的。若未显示定义,那么系统会自动生成默认的析构函数。析构函数不能重载。
4.对象生命周期结束时,系统会自动调用析构函数。
5.系统自动生成的默认析构函数,对内置类型不做处理。对于自定义类型,则会调用析构函数。但是!默认析构函数对自定义类型的处理也没有那么省心:它是不会主动free掉动态内存空间的,它只能帮你调用成员变量的析构函数,至于开辟的动态内存,还得你自己去释放掉。这种情况,就要自己写显示的析构函数了。
下面这个例子,展示了“程序自动调用析构函数,来处理自定义类型”:
#include
using namespace std;
class Time
{
public: //如果调用了Time的析构函数
~Time() //会输出“~Time()”
{ //会调用吗?
cout << "~Time()" << endl;
}
private:
int _hour;
};
class Date
{ //Date的析构函数会被调用吗?
private:
int _year; //内置类型
int _month;
int _day;
Time _time; //自定义类型
};
int main()
{
Date d1;
return 0;
}
结果:
疑惑:我们实例化的是Date类型的对象,Time类型压根没实例化,那为什么会调用Time的析构函数呢?
answer:在销毁 Date前,会调用Date的默认析构函数,目的是销毁Date中的自定义变量Time。而销毁Time,则又会调用Time的析构函数。(由于这里显示写了~Time(),那么编译器会直接调用,而不默认生成)
Tip:这里我们把~time()显示写出来,是为了打印出来方便理解。如果这里不写也是可以的,编译器会生成默认析构,仍会自动调用,销毁Time。
补充:我们厘清一个问题:到底什么时候写析构函数,什么时候不写?
当构造的多个对象时需要被析构时,它们是按照什么顺序被释放的呢?这篇旨在帮助我们学习:析构函数的调用顺序。
先说结论:先构造的后析构,后构造的先析构。其实构造和析构都遵循栈的数据结构,即后进先出。这是因为对象的初始化和清理是被定义在(构造/析构)函数里面的,调用函数时会建立栈帧,函数建立时会依次压入栈帧,函数销毁时又会依次弹出栈。
一般情况下,对象的析构顺序和它们的创建顺序相反。而当局部对象、静态局部对象、全局对象同时存在时,析构的顺序并不是简单的倒过来了。
下面这个程序可以让我们直观地看到三种对象的构造/析构顺序:
#include
using namespace std;
class A
{
public:
A(int num)
{
_a = num;
cout << "A()->" << _a << endl; //当调用构造函数时,会打印出来
}
~A()
{
cout << "~A()->" << _a << endl; //当调用析构函数时,会打印出来
}
private:
int _a;
};
A a0(0); //我们依次构造了全局的a0,局部的a1、a2,静态的a3
int main()
{
A a1(1);
A a2(2);
static A a3(3);
return 0;
}
运行结果:
结果表明,局部对象先按出栈顺序析构,然后是静态对象,最后是全局对象。
解释:
全局对象的作用域是全局作用域,这个作用域相当于一个程序最外层的容器,全局对象在一个工程里的不同文件内都可以使用。因此,全局对象在程序结束时析构。
局部对象的作用域在程序块中,它在程序块结束时析构。
被static修饰的静态局部对象,生命周期被延长至(它所定义在的)整个文件,当文件结束时,它被析构。
因此,局部对象先进行析构,析构的顺序为出栈的顺序;然后文件结束时,静态局部对象被析构,如果是多个对象,依然按照出栈顺序;最后,是全局变量。
tips:
"局部对象,在退出程序块时析构
静态对象,在定义所在文件结束时析构
全局对象,在程序结束时析构
继承对象,先析构派生类,再析构父类
对象成员,先析构类对象,再析构对象成员"
(tips from:@南方以北,http://t.csdn.cn/bTeud)
OK,我们来练习一下,试着写出这个程序的打印结果:
#include
using namespace std;
class A
{
public:
A(int num)
{
_a = num;
cout << "A()->" << _a << endl; //当调用构造函数时,会打印出来
}
~A()
{
cout << "~A()->" << _a << endl; //当调用析构函数时,会打印出来
}
private:
int _a;
};
A a0(0);
void f()
{
A a1(1);
A a2(2);
static A a3(3);
}
int main()
{
f(); //注意:调用了两次f()
f();
return 0;
}
运行结果:
这里需要注意的点在于:第一次调用已经在静态区创建了全局对象和静态对象,它们在f( )结束时,并不会释放。只有局部对象释放了。第二次调用f( )时,因为静态局部对象a3已经存在,所以不会再构造了,只会开辟a1和a2的栈帧。
关于构造函数和析构函数的知识点就分享到这里,后续会不断更新这篇博客,扩充这篇的知识容量。如有错误,还望指正(^ - ^)