一、默认成员函数
在使用C语言练习初阶数据结构,即线性表、链表、栈、队列、二叉树、排序等内容时,大家可能会经常犯两个错误,特别是第二个错误,可以说是十分普遍:
- 在使用数据结构创建变量时忘记对其进行初始化操作而直接进行插入等操作;
- 在使用完毕后忘记对动态开辟的空间进行释放而直接返回;
而C++是在C语言的基础上生长起来的 – 修正C语言中的一些不足,并加入面向对象的思想;面对上面C语言存在的问题,C++设计出了默认成员;
默认成员函数:当用户没有显式实现时,编译器会自动生成的成员函数称为默认成员函数;
其中,构造函数和析构函数针对的是我们上面提出的两个问题,而其他四个函数则是适用了其他场景与需求;下面,我们来仔细探讨这6个默认成员函数的细节;
二、构造函数
1、基础知识
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的任务并不是创建对象,而是当对象被创建之后完成对象的初始化工作;同时构造函数不能由用户调用,而是在创建类类型对象时由编译器自动调用,并且在对象整个生命周期内只调用一次。
构造函数有如下特性:
函数名与类名相同;
无返回值;
对象实例化时编译器自动调用对应的构造函数;
构造函数支持重载与缺省参数;
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,但一旦用户显式定义编译器将不再自动生成;
构造函数对内置类型不处理,对自定义类型调用它自身的默认构造;
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
我们以Date类为例:
class Date { public: Date() //无参构造 { _year = 1970; _month = 1; _day = 1; } Date(int year, int month = 1, int day = 1) //带参构造 { _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.Print(); Date d2(2022, 10, 4); //带参 d2.Print(); }
可以看到,我们并没有显示的去调用构造函数,而是由编译器自动调用;而构造函数的功能其实就是我们之前Init函数的功能;
这里有两个需要注意的地方:
1、构造函数虽然支持重载和缺省参数,但是无参构造和有参全缺省构造不能同时出现,因为在调用时会产生二义性;
同时,当参数有多个时可以构成很多个重载,使得构造函数变得十分冗余,所以一般我们只会显式定义一个全缺省的构造函数,因为这一种就可以构造就可以代表很多种参数情况;
2、当我们调用无参构造或者全缺省构造来初始化对象时,不要在对象后面带括号,这样使得编译器分不清这是在实例化对象还是函数声明;
2、特性分析 – 自动生成
在构造函数的第5点特性中我们提到:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,但一旦用户显式定义编译器将不再自动生成;下面我们来验证这个特性;
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
可以看到上面的日期类中我们是没有显式的去自己实现构造函数的,所以编译器应该会自己生成一个无参的默认构造函数完成初始化工作:
但是我们发现一个问题,默认的构造函数好像并没有完成初始化工作,即d1对象中的_year,_month,_day仍然是随机值;那么是因为这里编译器生成的默认构造函数并没有什么用吗?这个问题我们需要构造函数的第六个特性来回答;
3、特性分析 – 选择处理
内置类型与自定义类型:C++把类型分成内置类型 (基本类型) 和自定义类型;内置类型就是语言本身提供的数据类型,如:int/char/double/指针,自定义类型就是我们使用 class/struct/union 等自己定义的类型,如:Stack/Queue/Date;
而构造函数的第六点特性如下:构造函数对内置类型不处理,对自定义类型调用它自身的默认构造;
对于这个特性,我们使用 Date、Stack 和 Myqueue 三个类来对比理解:(注:Myqueue 即 用两个栈实现一个队列)
Date:
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
Stack:
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail\n"); exit(-1); } _top = 0; _capacity = capacity; cout << "Stack 构造" << endl; } void Push(int x) { _a[_top++] = x; } private: int* _a; int _top; int _capacity; };
Queue:
class MyQueue { public: void Push(int x) { _pushST.Push(x); } Stack _pushST; Stack _popST; };
可以看到,Stack 的成员变量全部为内置类型,所以当我们不显式定义构造函数时,编译器自动生成一个默认构造函数,但默认生成的构造函数并不会对内置类型进行处理,所以这里我们看到的是随机值;Date 类的情况也是如此:
也就是说,编译器自动生成的构造函数不能满足我们的需求,所以我们需要手动定义构造函数:
而对于MyQueue来说,它的成员变量全部为自定义类型,所以即使我们不提供构造函数时,编译器自动生成的构造函数也会去调用自定义类型的默认构造,满足需求:
总结
那么,到底什么时候需要我们自己提供构造函数,什么时候使用编译器默认生成的构造函数呢?是内置类型全部自己定义,自定义类型全部使用默认生成的吗?答案是:面向需求 – 当编译器默认生成的构造函数就能满足我们的需求时我们就不需要自己提供构造函数,如MyQueue;当编译器提供/的构造函数不能满足我们的需求时就需要我们自己定义,如Date/Stack;
4、特性分析 – 默认构造
构造函数的第七点特性如下:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个;
上面这句话的意思就是当我们使用无参的方式实例化一个对象时,编译器会自动去调用该对象的默认构造函数,而默认构造函数有三种:编译器自动提供的无参构造函数、显式定义的无参构造函数、显式定义的全缺省的构造函数;
如果类中没有默认构造函数,那么我们实例化对象时就必须传递参数: