目录
默认成员函数
构造函数
引例
构造函数的概念及特性
析构函数
析构函数的特性
文章导读
本章节我们将学习类的6个默认成员函数中的构造函数
与析构函数
,并对比C语言阶段的内容来学习它们的各自的特性。
正文
默认成员函数
上一章中我们谈到,如果一个类中什么成员也没有,那么这个类就叫作空类
。其实这么说是不太严谨的,因为一个类不可能什么都没有
。
当我们定义好一个类,不做任何处理时,编译器会自动生成以下6个默认成员函数
:
默认成员函数
:如果用户没有手动实现,则编译器会自动生成
的成员函数。
构造函数
:主要完成初始化
工作;析构函数
:主要完成清理
工作;拷贝构造
:使用一个同类的对象初始化创建一个对象;赋值重载
:把一个对象赋值
给另一个对象;取地址重载
:普通对象
取地址操作;取地址重载
(const):const对象
取地址操作;- 本章我们将学习两个默认成员函数——
构造函数
与析构函数
。
构造函数
引例
在C语言阶段,我们实现栈
的数据结构时,有一件事很苦恼,就是每当创建一个stack对象(之前叫作定义一个stack类型的变量)后,首先得调用它的专属初始化函数StackInit
来初始化对象。
typedef int dataOfStackType; typedef struct stack { dataOfStackType* a; int top; int capacity; }stack; void StackInit(stack* ps); //... int main() { stack s; StackInit(&s); //... return 0; }
这不免让人觉得有点麻烦。在C++中,构造函数
为我们很好的解决了这一问题。
构造函数的概念及特性
构造函数
是一个特殊的成员函数
。构造函数虽然叫作构造,但是其主要作用并不是开辟空间创建对象,而是初始化对象
。
构造函数之所以特殊,是因为相比于其它成员函数,它具有如下特性
:
- 函数名与类名相同;
- 无返回值;
- 对象实例化时,编译器
自动调用
对应的构造函数; - 构造函数可以重载;
举例
class Date { public: //无参的构造函数 Date() {}; //带参的构造函数 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(2023, 3, 29);//调用带参构造函数(自动调用) }
特别注意
- 创建对象时编译器会自动调用构造函数,若是
调用无参构造函数
,则无需在对象后面使用()
。否则会产生歧义:编译器无法确定你是在声明函数还是在创建对象
。
错误示例
//错位示例 Date d3();
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date { public: //若用户没有显示定义,则编译器自动生成。 /*Date(int year,int month,int day) { _year = year; _month = month; _day = day; }*/ private: int _year; int _month; int _day; };
默认生成构造函数,对内置类型成员不作处理;对自定义类型成员,会调用它的默认构造函数;
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char、double…,自定义类型就是我们使用class、struct、union等自己定义的类型。
🌼举例🌼
🌼默认构造函数对内置类型🌼
class Date { public: //此处不对构造函数做显示定义,测试默认构造函数 /*Date() {}*/ void print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; void TestDate1() { Date d1; d1.print(); }
- 如图所示,默认构造函数的确未对内置类型做处理。
- 🌼默认构造函数对自定义类型🌼
class stack { public: //此处对stack构造函数做显示定义 stack() { cout <<"stack()" << endl; _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; class queue { public: //此处不对queue构造函数做显示定义,测试默认构造函数 /*queue() {}*/ private: //自定义类型成员 stack _s; }; void TestQueue() { queue q; }
如图所示,在创建queue对象时,默认构造函数对自定义成员_s做了处理,调用了它的默认构造函数stack()。
这一波蜜汁操作让很多C++使用者感到困惑与不满,为什么要针对内置类型和自定义类型做不同的处理呢?终于,在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:
内置类型成员变量在类中声明时可以给默认值;
举例🌼
class Date { public: //... void print() { cout << _year << "-" << _month << "-" << _day << endl; } private: //使用默认值 int _year = 0; int _month = 0; int _day = 0; }; void TestDate2() { Date d2; d2.print(); }
默认值
:若不对成员变量做处理,则使用默认值。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个;
🌼举例🌼
class Date { public: //无参的默认构造函数 //Date() //{ //} //全缺省的默认构造函数 Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } void print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year = 0; int _month = 0; int _day = 0; };
析构函数
析构函数与构造函数的特性相似,但功能有恰好相反。构造函数是用来初始化对象的,析构函数是用来销毁对象的。
需要注意的是,析构函数并不是对对象本身进行销毁(因为局部对象出了作用域会自行销毁,由编译器来完成),而是在对象销毁时会自动调用析构函数,对对象内部的资源做清理(例如stack _s中的int* a)。
同样,有了析构函数,我们再也不用担心创建对象(或定义变量)后由于忘记释放内存而造成内存泄漏
了。
🌼举例🌼
class Stack { public: Stack() { //... } void Push(int x) { //... } bool Empty() { // ... } int Top() { //... } void Destory() { //... } private: // 成员变量 int* _a; int _top; int _capacity; }; void TestStack() { Stack s; st.Push(1); st.Push(2); //过去需要手动释放 st.Destroy(); }
析构函数的特性
- 析构函数名是在类名前加上字符
~
; - 无参数;
- 无返回值;
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数;
- 析构函数不能重载;
举例
class Date { public: Date() { cout << "Date()" << endl; } ~Date() { cout << "~Date()" << endl; } private: int _year = 0; int _month = 0; int _day = 0; }; void TestDate3() { Date d3; //d3生命周期结束时自动调用构造函数 }
为便于观察,我们可以在析构函数内部写点儿东西。
- 编译器生成的默认析构函数,对自定类型成员调用它的析构函数;
举例
class stack { public: //此处对stack构造函数做显示定义 stack() { cout <<"stack()" << endl; _a = nullptr; _top = _capacity = 0; } ~stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; class queue { public: //此处不对queue构造函数做显示定义,测试默认构造函数 /*queue() {}*/ private: //自定义类型成员 stack _s; }; void TestQueue1() { queue q; }
这里可能有小伙伴会好奇:为什么析构函数不像构造函数那样区分内置类型与自定义类型呢?
答案是:因为内置类型压根不需要我们担心清理工作,在其生命周期结束时会自动销毁。而自定义类型需要担心,因为自定义类型里可能含有申请资源(例如:malloc申请内存须手动释放)。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如stack类。
本章到这里就结束了,下一章节我们将学习剩余的4个默认成员函数~