目录
1.类的6个默认成员函数
2.构造函数
2.1概念
2.2特性
3.析构函数
3.1析构函数的概念
3.2特性
1.类的6个默认成员函数
下面我们来分别学习一下这6个函数~
2.构造函数
2.1概念
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
在日常使用时,我们可能会忘记对对象进行初始化,程序报错后反而检查半天才发现问题。在为了避免这种失误出现,C++中引入了构造函数。
2.2特性
构造函数在函数名上看起来好像是创建对象,实际上是完成对象的初始化。
构造函数特征如下:
①函数名和类名相同;
②没有返回值,不是在前加void,而是压根不需要写;
③对象实例化时编译器自动调用对应的构造函数;
④构造函数可以重载;
上面代码的构造函数形式如下:
(1)不带参数的构造函数,由于编译器自动调用构造函数,在定义对象时就已经初始化了,
(2)带参数的构造函数,在定义对象的时候就要将初始化的值加在后面。
调用带参的构造函数时在括号内加入要初始化的值跟在对象后,那调用无参的构造函数时可以直接跟括号在对象后吗?(为什么调用无参的构造函数时后面不用加括号)
不能。这种形式就成了函数声明。
如下图,声明了一个d3函数,该函数没有参数,返回一个Date类型对象。
以理论上两种形式可以同时存在,构成了函数重载,但是一般情况下我们不会同时写出这两个,因为调用存在歧义。
这两种形式可以合为一种,利用我们之前学过的缺省参数来修改,如下面图中的形式:
⑤如果在类中忘记显式定义构造函数也不用担心,C++编译器会自动生成一个无参的默认构造函数。
我们忘记定义构造函数时执行程序,发现可以正常运行,这是因为编译器会自动生成一个默认构造函数,但是看运行结果截图我们可以发现编译器没有对参数进行初始化,_year、_month、_day都是随机值。那么编译器生成的默认构造函数有什么用呢?
C++中把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,像int/char/double/指针等;自定义类型就是我们使用的struct、class等自己定义的类型。
C++98中规定对默认生成的构造函数对内置类型不作处理,对自定义类型则调用它的默认构造函数。如果自定义类型没有默认构造函数就会报错。
其实自定义类型的尽头就是内置类型。
以上Date类中的_year、_month、_day就是int类型,所以不作处理,是随机值,接下来我们验证一下自定义类型。
•有些编译器可能会对内置类型进行处理(int初始化为0,double初始化为0.0,指针初始化为nullptr),但C++标准并没有规定内置类型要处理。
C++11中对内置类型不处理的缺陷又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
分析一个类型成员和初始化要求,如果需要些构造函数我们就自己写,不需要时就靠编译器自动生成。结论:大多情况下都需要自己实现构造函数。
不止是编译器生成的构造函数叫默认构造函数,我们自己显式定义的无参构造函数,全缺省构造函数都是默认构造函数。即不需要传参就能调用的构造函数,就叫默认构造函数。
默认生成的构造函数不能与其他两种同时存在;无参构造函数不能与全缺省构造函数同时存在,会产生调用歧义。
一般情况下,我们建议使用全缺省构造函数。
3.析构函数
3.1析构函数的概念
前面我们学习了对对象进行初始化的构造函数(类似于栈中的初始化函数Init),那么怎样清理资源呢,这就要提到我们接下来要学习的析构函数(栈中的销毁函数Destory)。
析构函数也是一种特殊的成员函数,与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2特性
析构函数特征:
默认生成的析构函数与构造函数类似,对内置类型不作处理,自定义类型则调用它的析构函数。(Date类没有显式定义析构函数,系统将自动生成,Date类中定义了自定义类型Time类型的对象t,调用它的析构函数~Time()。)
以下面的日期类为例,定义一个析构函数~Date()。(其实日期类的析构函数没什么有用的,因为它的_year等都是属于对象,并没有额外开辟空间,也就没有什么资源需要清理,此处只是用来验证它的特性)class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } ~Date() { cout << this << endl; cout << "~Date()" << endl; } private: int _year; int _month; int _day; }; void func() { Date d2; } int main() { func(); Date d1; d1.Print(); return 0; }
如果我们写了一个栈,忘记写析构函数会发生内存泄漏。虽然函数结束栈帧销毁,但是在堆上额外开辟了一块空间没有释放。C语言中避免出现内存泄漏需要在函数结束前调用Destory函数来销毁,但是这种方法在使用时极有可能忘记,因而C++中引入了析构函数。
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(int data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
private:
int* _array;
int _capacity;
int _size;
};
int main()
{
Stack S;
return 0;
}
对以上代码加上析构函数,结果如下
**先定义的先调用构造函数,后定义的先调用析构函数
class Date { public: Date(int year) { _year = year; cout << "Date()->" << _year << endl; } ~Date() { cout << "~Date()->" << _year << endl; } private: int _year = 1; int _month = 1; int _day = 1; }; Date d5(5); static Date d6(6); void func() { Date d4(4); } int main() { Date d1(1); static Date d2(2); Date d3(3); func(); return 0; }
分析上面代码,猜测结果是什么?
我们先分析一下各个对象是全局还是局部变量。d1、d2、d3、d4是局部变量,d4是在函数 func() 内定义,函数 func() 在main函数内调用,所以d4的生命周期先结束。d2又被static修饰(称为静态局部变量),改变了它的存储位置,使得它的生命周期变长,直至程序结束,生命周期才结束,在销毁之前应先将main函数内的局部变量先销毁(后定义先析构)再销毁该静态局部变量。d5、d6是全局变量,虽然d6被static修饰(静态全局变量),但它与全局变量的存储位置没有区别,生命周期仍是整个程序的执行期间,也是后定义的先析构。
所以调用析构函数的顺序是d4-d3-d1-d2-d6-d5。
结果如下:
总结:
调用析构函数的顺序:局部对象(后定义的先析构)->静态局部对象->全局对象(后定义的先析构)
构造函数和析构函数的学习就到这了,下篇我们将一起学习其他的默认成员函数,谢谢观看!