在之前的博文《 C++类的学习(一)——初识类》中,简单地讲述了类的一些基本特性。今天将讲述类中非常重要的三种特殊的成员函数构造函数、析构函数、拷贝构造函数以及this指针。
每个类都有对它的对象的初始化的方式,类通过一个或多个特殊的成员函数控制对象的初始化,这样的函数就叫构造函数。每当有类的对象被创建,甚至是使用new分配动态内存时,构造函数都会被执行。
构造函数的名字与类名相同,不含有返回类型,一个类可以通过函数重载包含多个构造函数。构造函数的参数列表中含有一个或多个需要进行初始化的数据成员。注:构造函数不能被声明为const;
以Time类为例,可以定义如下构造函数;
Time::Time(int h, int m) :hours(h), mintues(m)
{
cout << "调用显式构造函数" << endl;
}
或是这样定义;
Time::Time(int h, int m) :hours(h), mintues(m)
{
hours=h;
mintues=m;
cout << "调用显式构造函数" << endl;
}
上面的构造函数可以显式地初始化对象的两个数据成员。
C++提供了两种使用构造函数初始化对象的方法;第一种是直接显式地调用构造函数,例如:
Time sport =Time(1, 15);
第二种是隐式地调用,例如:
Time eating(1,45);
默认构造函数是在未提供初始值时进行对象初始化的构造函数。
当一个类没有提供任何的构造函数时,编译器会隐式地定义一个默认构造函数。以Time类为例,编译器提供的默认构造函数的形式应该是如下的;
Time() { }
但是,当程序员显式地定义了构造函数后,编译器便不会再提供默认构造函数。而很多时候不能没有默认构造函数,因为经常需要定义一些临时的类对象,当这些对象不带有初始值时,需要默认构造函数进行初始化。
所以我们需要显式地给出默认构造函数。当类已经将成员赋予了类内的初始值时,默认构造函数内不需要任何语句对称冠进行初始化。例如类内已经给定了数据成员的初始值。
class Time
{
//----------私有成员,类中的成员默认是私有的
private:
int hours = 0;
int mintues = 0;
};
此时,如下定义默认构造函数即可;
Time::Time() { }
但是当类没有给出成员的初始值时,需要默认构造函数进行赋值;
Time::Time()
{
hours = mintues = 0;
}
当构造函数中所有的参数都有默认值时,这个构造函数也是默认构造函数,例如如下这种情况;
Time(int hours=0,int mintues=0)
{
/*函数实现*/
}
用构造函数创建对象之后,程序跟踪该对象直到其过期为止,当对象过期时,程序会自动调用一个特殊的成员函数完成清理工作。这个函数就是析构函数。例如,构造函数使用new来分配内存,那么需要析构函数使用delete释放内存。
析构函数的名称也很特殊:在类名前面加上~ 即可。与构造函数一样,析构函数也没有返回值类型和声明类型,与构造函数不同的是,析构函数没有参数。
在析构函数中,不存在类似构造函数中初始化列表的东西来控制成员销毁,析构的过程是隐式的。当程序员不显式地定义析构函数时,编译器会提供一个析构函数,这个析构函数的函数体为空。
大多数情况下,程序员都可以不定义析构函数,而直接使用编译器提供的析构函数,但是当构造函数中使用了动态申请内存的方式为对象的属性进行赋值时,需要程序员自己提供析构函数,因为编译器提供的析构函数不会释放堆内的内存。
Time::~Time()
{
// delete *动态成员。
cout << "调用了析构函数" << endl;
}
一个对象被销毁,就会自动调用析构函数,析构函数销毁成员时,按照成员初始化的顺序逆序销毁。一个类有且只有一个析构函数,这就要求构造函数对于动态成员申请内存的方式是一致的。需要记住的是,析构函数的工作是清理对象,而不是删除对象。
1. 变量在离开其作用域时被销毁。
2. 当一个包含对象的容器被销毁时,容器中的对象被销毁。
3. 动态分配的对象,当指向它的指针被delete时,对象被销毁。如果一个动态分配的对象的内存不被显式释放,即便离开作用域析构函数也不会被调用。
4. 对于临时对象,当创建其完整表达式结束时被销毁。
除了构造函数,C++中还有一种用于初始化的成员函数,即拷贝构造函数。与构造函数一样,当程序员不定义拷贝构造函数时,编译器也会提供一个拷贝构造函数。以Time类为例,其拷贝构造函数的形式如下;
Time::Time(const Time &t)
{
hours = t.hours;
mintues = t.mintues;
//cout << "调用拷贝构造函数" << endl;
}
可以看到拷贝构造函数是有参数列表的,用参数列表中的对象的数据成员对对象进行拷贝初始化。要说明的是,我们常常使用的都是简单的对数据成员进行赋值的“浅拷贝”拷贝构造函数,这样的拷贝构造函数在一些时候会导致程序错误,尤其是当对象中有动态对象和静态成员时。这时候需要“深拷贝”的构造函数。在这里不做讲解,有需要的同学可以自行去了解。
1. 类的对象作为实参传递给一个非引用类型的形参。此时,需要用拷贝构造函数将对象的值传递给形参。例如;
Fun(Time t) { …………… }
这时,t被拷贝初始化,在函数Fun执行完毕后被析构。
2. 从一个返回类型为非引用类型的函数返回一个对象时。例如;
Time Fun(Time &t) { Time s; return s; }
这时会先产生一个临时变量temp;利用s将temp拷贝初始化,函数执行到最后,先析构s,当temp把值传递回去后(也就是表达式结束时)析构temp;
3. 用一个对象初始化另一个对象时
例如;
Time A(10,10); //直接初始化 Time B(A); //拷贝初始化
此时,A使用构造函数初始化,B使用拷贝构造函数进行初始化
在很多时候,成员函数需要对两个对象进行操作,比如比较两个对象。以Time类为例,比较两个对象时,函数如下;
Time::comp(Time &t) const;
比较对象A与B;调用的方式可以是;
A.comp(B);
函数实现如下;
TimeTime::comp(Time &t) const
{
int time1 = hours * 60 + mintues;
int time2 = t.hours * 60 + t.mintues;
if (time1 >= time2)
{
return ?????; //这里返回什么呢?
}
else
{
return t;
}
}
但是在comp()函数中对象A并没有别名,无法被作为返回值进行返回。C++为了解决这个问题,使用了一个特殊的指针this指针,this指针指向用于调用成员函数的对象。那么,上面的函数可以写成如下形式。
TimeTime::comp(Time &t) const
{
int time1 = hours * 60 + mintues;
int time2 = t.hours * 60 + t.mintues;
if (time1 >= time2)
{
return *this; //返回this指针的解引用
}
else
{
return t;
}
}
为了方便学习,在这里提供所有的代码;
//------mytime.h
#ifndef MYTIME_H
#define MYTIME_H
#include
using namespace std;
class Time
{
//----------私有成员,类中的成员默认是私有的
private:
int hours = 0;
int mintues = 0;
//----------共有成员
public:
Time(); //默认构造函数
Time(int h, int m = 0); //显式构造函数
Time(const Time &); //拷贝构造函数
~Time(); //析构函数
void AddMin(int m);
void AddHour(int h);
void reset(int h = 0, int m = 0);
Time comp(Time &t) const;
void show() const
{
cout << hours << "hourss" << " " << mintues << "mintues" << endl;
}
};
//-------时间重置,内联函数
inline void Time::reset(int h, int m)
{
hours = h;
mintues = m;
}
#endif
//--mytime.cpp
#include
#include "mytime.h"
using namespace std;
//-------默认构造函数
Time::Time()
{
//hours = mintues = 0;
cout << "调用默认构造函数" << endl;
}
//------显式的构造函数
Time::Time(int h, int m) :hours(h), mintues(m)
{
cout << "调用显式构造函数" << endl;
}
//------拷贝构造函数
Time::Time(const Time &t)
{
hours = t.hours;
mintues = t.mintues;
cout << "调用拷贝构造函数" << endl;
}
//------析构函数
Time::~Time()
{
cout << "调用了析构函数" << endl;
}
//-------小时相加
void Time::AddHour(int h)
{
hours += h;
}
//------分钟相加
void Time::AddMin(int m)
{
mintues += m;
hours += mintues / 60;
mintues %= 60;
}
Time Time::comp(Time &t) const
{
int time1 = hours * 60 + mintues;
int time2 = t.hours * 60 + t.mintues;
if (time1 >= time2)
{
return *this;
}
else
{
return t;
}
}
#include
#include "mytime.h"
using namespace std;
int main()
{
{
Time eating(1, 45);
Time sport = Time(1, 15);
eating.show(); //合法;
Time swiming; //非const对象,既可以调用const成员函数,也可以调用非const成员。
swiming.show();
const Time study(8); //const对象只能调用const成员函数。
Time new_studys = study;
new_studys.show();
Time *p_time = new Time(9, 10);
delete p_time;
}
system("pause");
return 0;
}
可以看到,结果中显式构造函数、默认构造函数、拷贝构造函数共出现了六次,析构函数也出现了六次,说明所有的对象都被析构了。
但是细心的人会发现,在test.cpp的main()函数内部,有一个作用域;为什么会有这个作用域呢?如果去掉这个作用域会怎样呢?我们去掉作用域,重新运行,查看输出。
可以看到,析构函数只被调用了一次,很明显,这次调用是delete p_time时被调用的。原因其实很简单,对象在退出其所属代码块时被销毁。如果没有大括号,对象所属的代码块为整个main()函数,当mian()函数执行完毕后,才会调用析构函数,但是这意味着,在析构函数被调用前程序已经退出执行,无法看到析构函数的输出。但是添加了大括号后,这5个析构函数将在到达返回语句前被调用,从而显示了相应的输出。
已完。。
参考书籍《C++ Primer 第五版》、《C++ Primer Plus 第六版》
【C++】C++类的学习(一)——初识类
【C++】C++类的学习(三)——运算符重载与友元函数
【C++】C++类的学习(四)——继承与虚函数
【C++】C++类的学习(五)——纯虚函数与模板类