【C++杂货铺】构造函数和析构函数
一、类的六个默认成员函数
📖默认成员函数
用户没有显式实现,编译器会自动生成的成员函数,称为默认成员函数。
- 构造函数:完成对象的初始化工作。
- 析构函数:完成对象空间的清理工作。
- 拷贝构造:使用同类对象初始化创建对象。
- 赋值重载:把一个对象赋值给另外一个对象(该对象已存在)。
- 取地址重载:获取对象的地址,这两个很少自己实现。
注意:构造和析构函数,不是创建对象和销毁对象。对象的创建和销毁都是编译器做的工作。
二、构造函数
📖为什么要有构造函数?
为了避免每次创建对象后,都要去调用专门的成员函数设置对象的信息,这样很麻烦,并且容易遗忘,那就想着能否在创建对象的同时,就将信息设置进去。因此,就有了构造函数。以日期类为例:
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;
}
如上面的代码,每次创建一个日期类对象后,都要手动的去调用Init
函数,完成对象的初始化,整个过程繁琐,而且容易遗忘,为此,提出了构造函数的概念。
📖定义
构造函数是一个特殊的成员函数,名字与类名相同,创建对象的时候由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象的整个生命周期中只调用一次。
📖构造函数的特性
- 函数名与类名相同。
- 无返回值。(无需void)
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
🎊示例:
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
Date d3();
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。即Date d3();
是声明了一个d3
函数,该函数无参,返回一个日期类对象,并不是创建了一个日期类对象d3
。
构造函数在语法上可以是私有的,但是在创建对象的时候就调不动了。在单例模式中,会把构造函数搞成私有,具体的我们以后再说。
📖编译器生成的构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不在生成。
🎊示例:
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;
return 0;
}
将Date类中的构造函数注释掉后,代码可以编译通过,因为编译器生成了一个无参的默认构造函数。将Date类中的构造函数放开后,代码编译失败,因为一旦显式定义了任何构造函数,编译器将不再生成默认构造函数。而此时Date中的构造函数需要三个参数,Date d1;
会去调用无参的构造函数,但是当前类中没有无参的构造函数,所以编译会报错。
📖编译器生成的构造函数干了什么?
C++中把类型分为内置类型和自定义类型。内置类型就是语言提供的数据类型,如:int
、char
……自定义类型就是我们使用class
、struct
、union
等自己定义的类型。(所有类型的指针都属于内置类型)。
编译器生成的默认构造函数,对内置类型不做处理,对自定义类型,会去调用它的默认构造函数。
🎊示例:
//先定义一个时间类
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;
}
上面代码,先定义了一个时间类Time
,它的成员变量都是内置类型,给这个类写了一个无参的构造函数,接下来,定义了一个日期类Date
,他有四个成员变量,其中_year
、_month
、 _day
都是内置类型,_t
是自定义类型,并且,我们没有写日期类的构造函数,这意味着,在创建对象的时候,会去使用编译器生成的无参默认构造函数。
📖内置类型给默认值
C++11中针对内置类型成员不初始化的缺陷,打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
🎊示例:
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 = 1970;//给默认值
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
📖默认构造函数
我们没写编译器自动生成的构造函数、无参构造函数、全缺省构造函数,这三种都叫做默认构造函数,它们都有一个共同的特点:可以不用传参。默认构造函数只能有一个,后面俩,在语法上可以构成函数重载,但是在无参调用的时候,会发生歧义,出现调用不明确。
注意:要把默认构造函数和默认成员函数区分清楚,默认成员函数是我们不写编译器会自动生成的,默认构造函数是不需要传参的构造函数。编译器生成的构造函数,既是默认构造函数,同时也是默认成员函数。
📖总结
一般情况下,都需要我们自己写构造函数。如果满足以下情况,即:内置类型的成员变量都有默认值,且初始化符合我们的要求,自定义类型都定义了默认构造,此时可以考虑不写构造函数,使用编译器自动生成的默认构造函数。自定义类型如果没有对应的构造函数,那就意味着初始化自定义类型需要传参,此时必须自己写构造函数,并且还会用到初始化列表。
三、析构函数
📖定义
与构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的。而对象在销毁的时候,会自动调用析构函数,完成对象中资源的清理工作。
📖特性
- 析构函数名是在类名前加上
~
。 - 无参数无返回值类型。
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译器自动调用析构函数。
小Tips:析构函数不能重载。
🎊示例:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_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;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
Stack
中的成员变量_array
、_capacity
、_size
都是内置类型,所以在对象s
生命周期结束要销毁的时候,不需要资源清理,最后系统直接将其内存回收即可,而_array
指向的空间是在堆区上申请的,这块空间不会随着对象生命周期的结束而自动释放(归还给操作系统),所以_array
被回收后,就找不到动态申请的那块空间,会造成内存泄漏,因此在对象销毁前,要通过析构函数去释放成员变量_array
指向的空间,这就是析构函数的作用。
📖编译器生成的析构函数干了什么?
我们不写,编译器会自动生成一个析构函数。该析构函数对内置类型不做处理,对自定义类型会去调用它的析构函数。
🎊示例:
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
main
方法中创建了Date
对象d
,而d
中包含4个成员变量,其中_year
、_month
、_day
、三个是内置类型成员,对象销毁时不需要资源清理,最后系统直接将其内存回收即可,而_t
时Time
类对象,在d
销毁时,要将器内部包含的Time
类的_t
对象销毁,所以要去调用Time
类的析构函数。但是main
函数中不能直接调用Time
类的析构函数,实际销毁的是Date
类对象d
,所以编译器会调用Date
类的析构函数,而Date
类没有显示提供,则编译器会给Date
类生成一个默认的析构函数,目的是在其内部调用Time
类的析构函数。
📖总结
一般情况下,有动态申请资源,就需要显式的写析构函数来释放资源,没有动态申请的资源,可以不写析构函数,需要释放资源的成员都是自定义类型,也不需要写析构函数。
🎁结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!
- 点赞
- 收藏
- 关注作者
评论(0)