目录
玩法4:构造函数不仅能够完成对象成员变量的初始化,还可以做判断功能:
一.类的六大特殊成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
二.构造函数
1.定义:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
2.其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
3.构造函数的玩法1:
class Date
{
public:
//构造函数——与类同名,且没有返回值
Date(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(2015, 1, 1);
Date d2(2022,9,1);
d1.Print();
d2.Print();
}
当Date类创建d1,d2对象时,会自动调用构造函数,实参2015传给形参year,1传给month,1传给day......完成d1,d2对象的成员变量初始化赋值。
玩法2:提供多种构造函数,完成多种初始化
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//无参构造函数
Date() {
_year = 1999;
_month = 10;
_day = 13;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2015, 1, 1);
Date d2(2022, 9, 1);
d1.Print();
d2.Print();
Date d3;
Date d4;
d3.Print();
d4.Print();
return 0;
}
注:创建d3,d4对象时,不带参数所以自动调用无参构造函数Date()
玩法3:二义性调用
class Date
{
public:
//全缺省构造函数
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
//无参构造函数
Date() {
_year = 1999;
_month = 10;
_day = 13;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d5;
Date d6;
d5.Print();
d6.Print();
return 0;
}
产生错误的原因:会引发二义性,对于不带参数的对象,既可以调用全缺省构造,也可以调用无参构造,让编译器为难,报错。对于这两种构造函数,可以同时存在,没有语法错误,但会产生歧义,不知道调用哪个。
玩法4:
构造函数不仅能够完成对象成员变量的初始化,还可以做判断功能:
class Date {
public:
Date(int year = 1199, int month = 12, int day = 15) {
//可以对对象的实参做判断功能
if(!(year >= 1 && (month >= 1 && month <= 12) && (day >= 1 && day <=
GetMonthDay(year, month)))) {
cout << "该日期为非法日期" << endl;
}
else {
cout << "该日期为合法日期" << endl;
}
_year = year;
_month = month;
_day = day;
cout << _year << "-" << _month << "-" << _day << endl;
}
//获得月份天数
int GetMonthDay(int year, int month) {
int MonthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if ((month == 2) && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
}
else {
return MonthDay[month];
}
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2022, 2, 40);
Date d2(-1999, 15, 30);
Date d3(2022, 10, 15);
}
4.C++实现栈的好处:
C语言实现栈代码(部分):
#include<stdlib.h>
#include<stdio.h>
typedef struct Stack
{
int* array;
int capacity;
int size;
}Stack;
//初始化函数
void StackInit(Stack* ps,int capacity)
{
ps->array = (int*)malloc(sizeof(int) * 4);
if (NULL == ps->array)
{
perror("malloc fail");
return;
}
ps->capacity = 4;
ps->size = 0;
}
//插入数据函数
void StackPush(Stack* ps, int data)
{
//...扩容
ps->array[ps->size] = data;
ps->size++;
}
int main(){
Stack st;
StackInit(&st,4);
StackPush(&st,1);
StackPush(&st,2);
StackPush(&st,3);
return 0;
}
C++实现:
class Stack{
public:
//构造函数
Stack(int capa=4){
_array = (int*)malloc(sizeof(int) * capa);
if (NULL == _array)
{
perror("malloc fail");
return;
_capacity = 4;
_size = 0;
}
void StackPush(int data){
//...扩容
_array[_size] = data;
_size++;
}
private:
int* _array;
int _capacity;
int _size;
};
int main(){
Stack st; //创建对象后自动调用构造函数——完成初始胡操作
StackPush(1);
StackPush(2);
StackPush(3);
return 0;
}
总结:C++就是针对于我们可能在实现代码的过程中忘记去写初始化构造函数,从而研发出构造函数,在后面介绍的析构函数也是如此。
5.默认构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
默认构造函数有三种形式:
1、无参构造(没有形参的构造函数)称之为默认构造函数
2、全缺省构造(形参都有缺省值)也称之为默认构造函数
3、程序员自己不写构造,编译器自动生成的默认构造函数
重点介绍一下编译器自己生成的:
class Date
{
public:
//没有写构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
当创建d1对象时,自动调用构造函数,因为我们没有亲自去写构造函数,所以编译器自动生成一个默认的无参构造函数,这种构造函数既不会让其成员变量置成零,也不会做其他任何事情,它生成的构造函数只为确保程序能够正常运行,赋值的事情不归它管,该类中的成员变量值经过构造后会变成随机值。
有的小伙伴会问了,系统生成的默认构造函数没什么用,毕竟这些成员变量并不能被赋我们想要的具体的值,最终还是得要我们自己去写它,那么创造出这个机制的意义在哪里?
其实C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,而自定义类型就是我们使用class/struct/union等自己定义的类型。对于内置类型的成员变量,编译器不会管,所以成员变量成为随机值(让其自生自灭),对于自定义类型的成员变量,编译器会调用其自定义类型的构造函数,也就是说编译器只处理自定义类型的成员变量。如下代码:
案例1:
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;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main(){
Date d;
return 0;
}
运行结果:
解析:_year,_month,_day都是随机值,而Time _t这个变量是class Time自定义类型,编译器会走到Time类中,调用该类的构造函数。
案例2:
typedef int DataType;
class Stack
{
public:
Stack(size_t capa = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capa);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capa;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
//析构函数
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQue {
private:
//自定义类型
Stack _pushST;
Stack _popST;
};
int main() {
MyQue q;
return 0;
}
运行结果:
对于MyQue类来说,它的成员变量全是自定义类型,编译会处理——去Stack类中调用Stack的构造函数,从而得到上图右上角的各变量初始值,MyQue不需要自己写构造函数,全靠调用Stack类的即可。
案例总结:Date类和Stack类的构造函数都需要自己去写,而MyQue类的构造使用默认构造即可,因为编译生成的构造函数并不能满足我们的需求。
三.析构函数
在之前,我写过很多用C语言实现链表、通讯录、栈、队列的代码,发现都需要用到数据结构的销毁函数Destory,于是析构函数因此诞生,即使我们忘记写了,系统也会帮助我们清理。
1.定义:
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2 特性:
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
3.析构练习:
class Stack
{
public:
//构造函数
Stack(int capa = 4){
_array = (DataType*)malloc(sizeof(DataType) * capa);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data){
// 扩容
_array[_size] = data;
_size++;
}
//析构函数
~Stack(){
if (_array){
free(_array);
}
_array = NULL;
_capacity = 0;
_size = 0;
}
int main(){
Stack st; //创建对象后自动调用构造函数——完成初始化操作
StackPush(1);
StackPush(2);
StackPush(3);
return 0;
}
析构函数的调用是在执行return 0后,函数即将销毁时自动调用它,然后堆区空间就会被释放,且其他变量清零。
4.默认析构函数
默认析构函数与默认构造函数同理,都是我们不生成时,编译器自动生成一个析构函数,但编译器生成的也只能处理一些简单类型的成员变量,例如日期类:
class Date {
public:
Date(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(2022, 10, 5);
Date d2(2021, 9, 1);
return 0;
}
对于复杂类型的成员变量,编译器自动生成的析构函数并不能完成最后的清理工作,比如:对于堆区空间的生成,使用fopen函数打开的文件,最后需要手动释放(fclose/free),可是默认析构函数并不能做到这么智能,所以还是得自己去写析构函数!!!
所以我总结出一个道理:若编译器默认生成就可以满足需求的话,就不用自己写,不满足需求就自己写。