用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
6个默认成员函数的主要功能是:初始化和清理,拷贝和复制,对普通对象和const修饰的对象取地址重载。
本文注重介绍构造与析构。
默认成员函数
构造函数
构造函数定义
构造函数是特殊的成员函数,构造函数虽然名叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
举例:
class Data
{
public:
Data() //无参构造函数
{
_year=1;
_month=1;
}
Data(int year,int month) //有参构造函数
{
_year=year;
_month=month;
}
void Print()
{
cout<<_year<<_"-"<<_month<<endl;
}
private:
int _year;
int _month;
};
int main()
{
Date d1; //通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
d1.Print();
Date d2(2024,4);
d2.Print();
}
构造函数的语法及特点:
- 函数名和类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 如果类里面没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,用户若显式定义那么编译器就不再生成
- 编译器生成的默认构造函数对内置类型不做处理,对自定义类型调用其默认构造函数,所以C++11对默认构造函数对基本类型不做处理这一缺陷做了改进,可以对内置类型声明给缺省值。
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970; //声明给缺省值;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
补充知识–内置类型
那么什么是内置类型和自定义类型呢?
内置类型指的是编程语言中已经定义好的基本数据类型,通常由编译器直接支持。常见的内置类型包括整型(int、long、short等)、浮点型(float、double等)、字符型(char)、布尔型(bool)等。
自定义类型指的是通过用户自己定义的数据类型。在很多编程语言中,可以使用结构体(struct)或类(class)等机制来定义自己的数据类型。
默认构造函数
不传参就可以调用的构造函数是默认构造函数,默认构造函数只能有一个,常见的三种:无参构造函数,全缺省构造函数,编译器自己生成的构造函数。
对如何使用构造函数的理解
其实自定义类型无非是一些内置类型的组合,最后对数据的处理还是对内置类型处理。
如果我们没写任何一个构造函数,编译器就会自动生成一个默认的无参构造,且默认生成的构造函数,对于内置类型不做处理,对于自定义类型会去调用它的默认构造。
所以需要写构造函数就自己写,不用编译器会自己生成。
初始化列表
引入
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
> class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
// 这里可以再次对成员变量进行赋值
_year = _year + 1;
}
private:
int _year;
int _month;
int _day;
};
在构造函数体内,可以通过多次赋值语句为对象的成员变量赋值。这意味着在构造函数体内,可以通过多个赋值语句来修改对象的成员变量的值。
在上述例子中,_year 成员变量在构造函数体内首先被赋值为传入的 year 参数,然后再赋值为 _year + 1。这样,对象在创建时 _year 的初始值为 year + 1。
虽然在构造函数体内可以进行多次赋值,但这并不是进行多次初始化的意思,而只是为成员变量赋予不同的值。
初始化和赋值的区别在于:
初始化是在对象创建的时候为成员变量分配内存并设置初始值。
赋值是在对象创建后,已经有了初始值的情况下将新的值赋给成员变量。
C++中引入了初始化列表用来更高效地初始化对象的成员变量。
1.在初始化列表中,可以直接对成员变量进行初始化,而不需要先创建一个默认构造函数再通过赋值语句进行初始化。
2.初始化列表可以按照成员变量的声明顺序来初始化成员变量,而不是按照成员变量在构造函数体内的赋值顺序来初始化。这在某些情况下可以避免潜在的问题,比如成员变量之间存在依赖关系时。
3.对于某些成员变量,可能存在只能通过初始化列表来初始化的情况。
初始化列表的用法
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
举例:
#include <iostream>
class Person {
public:
Person(const std::string& name, int age)
: _name(name)
, _age(age) {
//可以在里面对变量进行赋值
//_age+=1;
}
void printInfo() {
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
private:
std::string _name;
int _age;
};
int main() {
Person john("John", 25);
john.printInfo();
return 0;
}
注意要点:
使用初始化列表时注意:
-
每个成员变量在初始化列表中只能出现一次(因为初始化只能初始化一次)
-
类中包含以下类型的成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量 (因为创建了就不能被修改了,只能在初始化时进行赋值)
自定义类型成员(且该类没有默认构造函数时) -
尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会优先使用初始化列表初始化。
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
析构函数
析构函数定义
析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特点:
- 函数名前加 ~
- 无参无返回值
默认的析构函数
- 一个类就只能有一个析构函数,析构函数不可以重载,若未显式定义,系统会自动生成默认的析构函数。与构造函数类似,编译器生成的默认析构函数对内置类型成员不做处理,对自定义类型成员调用它的析构函数。
可以结合下面的例子来理解一下:
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;
}
运行结果最后是~Time(),从结果不难看出,编译器调用了Time类的析构函数。可是并没有定义一个Time类的变量,为什么会调用它的析构函数呢?
这是因为main函数中创建了Date d,没有显示定义Date类的析构函数,编译器会给Date类生成一个默认的析构函数。
d中包括了三个内置类型成员变量_year,_month,_day,对于内置类型成员来说,销毁时不需要资源清理,最后系统会直接将其内存回收;但对于自定义类型成员变量_t来说,会调用其对应的析构函数。
(有种套娃的感觉,其实与构造函数类似,对自定义类型的处理归根结底还是对内置类型的处理)
- 对象生命周期结束时,编译器系统自动调用析构函数(调用顺序为局部->局部静态->全局(后定义的先析构))
显示定义析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成资源泄漏。一个类中若存在资源的申请,如堆内存的动态分配、文件的打开等,那么默认生成的析构函数就不能满足类的需求了。这是因为默认析构函数只会释放对象中的内存空间,并不会主动释放类所申请的资源。
Date
类是一个简单的日期类,它只包含三个整数类型的成员变量 year
、month
和 day
,并没有使用动态内存分配或其他资源的申请。在这种情况下,编译器生成的默认析构函数就足够了,不需要额外编写析构函数。因为在对象销毁时,这些成员变量也会被销毁,所以不需要额外的清理动作。
接下来,我们考虑一个有资源申请的类 Stack
。Stack
类是一个栈类,它使用动态内存分配来管理栈的元素。在 Stack
类的构造函数中,会通过 new
运算符申请一个动态数组作为栈的存储区域。在这种情况下,我们需要手动编写析构函数来释放这个动态数组,以避免内存泄漏。
class Stack {
private:
int* stackArray;
int top;
int size;
public:
Stack(int size) {
this->size = size;
stackArray = new int[size];
top = -1;
}
~Stack() {
delete[] stackArray;
}
};
在上面的代码中,使用了析构函数 ~Stack()
来释放 stackArray
动态数组所占用的内存。在对象销毁时,析构函数将会被调用,从而保证资源的正确释放。
总结来说,如果一个类中没有资源的申请,可以使用编译器生成的默认析构函数;而如果一个类中存在资源的申请,必须手动编写析构函数来释放这些资源,以防止资源泄漏。
小结
本文主要介绍默认成员函数中的构造与析构,从定义,特点,使用说明等方面具体阐述,尤其注意的是构造函数的初始化列表在后续的学习中会经常使用,后续接着介绍拷贝构造与赋值重载。