文章目录
- 🌈 前言
- 🌈 Ⅰ 构造函数
- 1. 构造函数概念
- 2. 构造函数特性
- 3. 初始化列表
- 🌈 Ⅱ 析构函数
- 1. 析构函数概念
- 2. 析构函数特性
- 🌈 Ⅲ 拷贝构造
- 1. 拷贝构造概念
- 2. 拷贝构造特性
- 3. 深度拷贝构造
- 🌈 Ⅳ 赋值重载
- 1. 运算符重载
- 2. 赋值运算符重载
🌈 前言
1. 默认成员函数介绍
- 一个什么成员都没有的类简称为空类,编译器会自动为空类生成几个默认成员函数。
- 默认成员函数:用户不写出来的话,编译器就会生成的成员函数称为默认成员函数。
- 编译器自动生成的默认成员函数一般都比较挫,因此在大多数情况下就需要我们自己去编写这些个默认成员函数的执行逻辑。
2. 默认成员函数分类
函数 | 功能 |
---|---|
构造函数 | 主要完成对成员变量的初始化工作 |
析构函数 | 主要完成对成员变量的清理工作 |
拷贝构造 | 使用同类对象初始化新创建的对象 |
赋值重载 | 把一个对象赋值给另一个对象 |
🌈 Ⅰ 构造函数
1. 构造函数概念
- 现定义一个日期 (date) 类
class date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{date d1, d2;d1.Init(2024, 2, 6); // 调用公有成员函数 Init 为对象 d1 内的成员变量初始化d2.Init(2024, 2, 7); // 调用公有成员函数 Init 为对象 d2 内的成员变量初始化return 0;
}
- 对于 date 类,可以使用 Init 公有成员函数来给对象设置日期,但如果每次创建对象时都需要调用该函数来进行成员变量的初始化,需要写两行且很容易忘记,此时构造函数就诞生了。
- 构造函数是一个特殊的成员函数,该成员函数的名字与类名一致,实例化对象时由编译器自动调用。用来保证每个对象内的成员变量都有一个适当的初始值,且在对象整个生命周期内只调用一次。
2. 构造函数特性
- 构造函数的主要任务是初始化对象。
1. 构造函数特性
- 函数名和类型相同。
- 没有任何返回值。
- 实例化对象时编译器自动调用对应的构造函数。
- 构造函数也支持重载。
- 构造函数也支持缺省参数。
2. 构造函数示例
class date
{
public:// 构造函数:函数名和类名相同,没有返回值,支持缺省参数date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{date d1; // 不指定初始值时就用缺省参数初始化对象date d2(2024, 2, 7); // 在实例化对象的同时顺带就能初始化对象return 0;
}
- d1 使用缺省参数完成了对象初始化,d2 使用给的值完成了对象初始化。都自动调用了构造函数。
3. 初始化列表
1. 为何使用初始化列表
- 在构造函数的函数体内对成员变量初始化称为初始化赋值,并不是正儿八经的初始化,是赋值就存在多次赋值的问题。
- 初始化赋值的问题在构造函数的函数体内没办法解决。因此在构造函数时可以使用一种叫做初始化列表的方式进行初始化。用以确保每个成员变量都只被初始化一次。
class date
{
public:date(int year, int month, int day){_year = year;_month = month;_day = day;_year = 2023; // 成员变量 _year 被初始化了 2 次这咋个整}
private:int _year;int _month;int _day;
};
2. 初始化列表语法格式
类名(形参列表):成员变量1(成员变量 1 的初始值),成员变量2(成员变量 2 的初始值),成员变量n(成员变量 n 的初始值)
{}
3. 初始化列表示例
class date
{
public:date(int year = 1, int month = 1, int day = 1):_arr((int*)malloc(4 * sizeof(int))),_year(year),_month(month),_day(day){cout << "这是一个构造函数" << endl;}
private:int* _arr;int _year;int _month;int _day;
};
4. 初始化列表的特性
- 每个成员变量在初始化列表中只能出现一次 (只能初始化一次)。
- 以下成员变量必须放在初始化列表中进行初始化 (在函数体内对这些成员变量初始化会报错)。
- 引用成员变量
- const 成员变量
- 自定义类型成员 (且该类没有默认构造函数时)
- 尽量使用初始化列表进行初始化,因为编译器会优先使用初始化列表。
- 成员变量的声明顺序就是成员变量在初始化列表中初始化的顺序。
🌈 Ⅱ 析构函数
1. 析构函数概念
1. 概念
- 构造函数将对象内的成员变量初始化,那么析构函数就是将其销毁。
- 析构函数不是完成对对象本身的销毁,对象是在出了对象所在的作用域或者程序结束时自动销毁。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2. 格式
~类名()
{ // 函数体
}
2. 析构函数特性
1. 析构函数特性
- 析构函数的函数名由 ~ 和 类名 两部分构成。
- 析构函数没有任何参数,也没有返回值。
- 一个类中只能由一个析构函数。
- 析构函数不能实现函数重载。
- 对象的声明周期结束时,编译器会自动调用析构函数。
- 如果类初始化时没有申请资源 (开辟空间),析构函数可以不写,反之一定要写。
2. 析构函数示例
- 现有一个为了实现栈而定义的类。
class stack // 定义一个用于实现栈的类
{
public:stack(int capacity = 4) // 构造函数:_array((int*)malloc(4 * sizeof(int))), _top(-1),_capacity(capacity){cout << "stack(int capacity = 4)" << endl;}~stack() // 析构函数:对象的生命周期结束时自动调用析构函数{free(_array); // 如果有动态开辟的空间,就不用怕最后会忘记释放了_top = 0;_capacity = 0;cout << "~stack()" << endl;}
private:int* _array;int _top;int _capacity;
};
- 使用上述 stack 类定义出对象的话就肯定要动态开辟空间,如果没有析构函数自动将开辟的空间释放掉,而自己又忘了将开辟的空间手动释放,内存泄漏这不就来了。
🌈 Ⅲ 拷贝构造
1. 拷贝构造概念
1. 拷贝构造概念
-
在实例化对象时,可以不给初始值让构造函数使用缺省参数,也可以给初始值让构造函数对对象进行初始化。拷贝构造就是用一个现有的同类对象去初始化另一个对象。
-
拷贝构造函数只有一个形参 (只显示一个形参,this 指针不显示),该形参是对本类类型对象的引用 (一般常用 const 修饰),在用已存在的同类对象创建新对象时自动调用。
2. 拷贝构造语法格式
类名(const 类名& 形参名) // 实际上还是有两个形参,第一个形参为隐藏的 this 指针
{// 拷贝构造的函数体
}
3. 拷贝构造函数示例
class date
{
public:// 构造函数date(int year = 1, int month = 1, int day = 1): _day(day) ,_year(year),_month(month){}// 拷贝构造函数date(const date& d):_year(d._year),_month(d._month),_day(d._day){}
private:int _year;int _month;int _day;
};int main()
{date d1(2024, 2, 8); // 调用构造函数对 d1 进行初始化date d2(d1); // 调用拷贝构造使用 d1 对 d2 进行初始化return 0;
}
2. 拷贝构造特性
- 拷贝构造函数是构造函数的一个函数重载形式,本质还是构造函数。
- 拷贝构造函数的显示参数只有一个且必须是对实参对象的引用。
- 如果自己不写拷贝构造函数,编译器会自动生成,默认的拷贝构造函数执行的是浅拷贝。
- 拷贝构造函数最常用的调用场景:
- 使用现有对象初始化创建新对象。
- 函数参数类型为类类型对象。
- 函数返回值为类类型对象。
3. 深度拷贝构造
1. 默认的拷贝构造函数执行的是浅拷贝
- 浅拷贝:如果某个对象内的一个成员变量是一个指向一块连续空间的指针,那么浅拷贝就是将该地址拷贝给另一个对象的。两个对象各自的成员变量指向同一块空间。
- 深拷贝:为新对象重新开辟一块同样大小的空间,并且将已有对象内的值拷贝过去。
2. 深度拷贝构造示例
class stack
{
public:stack(int capacity = 4) // 构造函数{_array = (int*)malloc(sizeof(int) * capacity);assert(_array);_top = -1;_capacity = capacity;}stack(const stack& s) // 拷贝构造,this 是 st2,s 是 st1{int* tmp = (int*)malloc(s._capacity * sizeof(int));assert(tmp);// 将 st1 的 array 中的有效数据拷贝给 st2 的 arraymemcpy(tmp, s._array, sizeof(int) * (s._top + 1));_array = tmp;_top = s._top;_capacity = s._capacity;}
private:int* _array;int _top;int _capacity;
};
🌈 Ⅳ 赋值重载
1. 运算符重载
1. 运算符重载概述
- 有些时候函数名无法一眼看出该函数是为了实现什么功能 (如 func1、func2 这种函数名完全看不出该函数是要用来干什么)。
- 运算符重载是具有特殊函数名的函数,是为了增强代码的可读性而被引入。
2. 运算符重载格式
函数返回值类型 operator操作符(形参列表)
{函数体
}
3. 赋值运算符示例
- 现在要判断两个日期类的对象是否相等,内置操作符无法直接进行判断。因此将 == 进行重载以表示这是一个判断对象是否相等的成员函数。
class date
{
public:date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 将 == 重载成判断两个类类型对象是否相等的运算符bool operator== (const date& d) // 此处的 this 表示 d1,d 表示 d2{return _year == d._year && _month == d._month && _day == d._day;}
private:int _year;int _month;int _day;
};
4. 运算符重载本质
- 上述的 d1 == d2 实际上在编译器看来是 d1.operator==(d2),本质上还是调用对应的成员函数,然后将 d1 的地址传给 this 指针,形参 d 引用了 d2。
5. 运算符重载特性
- 不能通过连接其他符号来创建新的操作符,如 operator 和 @ 组成的 operator@ 不是一个新的操作符。
- 重载操作符必须有一个自定义类型的参数。
- 不能改变用于内置类型的运算符的含义,如 不能将 加法 的含义重载成 减法。
- 除了以下 5 种运算符,其余运算符都能被重载:
- 点星 ( .* ),域作用限定符 ( :: ),计算大小 ( sizeof ),三目运算符 ( ?: ),点 ( . )
2. 赋值运算符重载
1. 赋值重载功能
- 实现类类型对象之间的赋值,现在有两个已经被实例化好的对象 A、B,赋值重载就是将 对象 A 的值赋值给 对象 B。
- 和拷贝构造不一样,拷贝构造是用一个定义好的对象去初始化一个未被定义的对象。
2. 赋值重载格式
- 形参类型:const 类名&,传引用可以提高传参效率。
- 返回值类型:类名&,设置返回值是为了支持连续赋值 (A = B = C 这样)。
- 检测是否自己给自己赋值。
- 返回 *this,为了实现连续赋值。
3. 赋值重载示例
class date
{
public:date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}date& operator=(const date& d) // this 指针指向 d1,d 表示 d2{if (this != &d) // 避免自己给自己赋值{_year = d._year;_month = d._month;_day = d._day;}return *this; // 返回对 d1 的引用}
private:int _year;int _month;int _day;
};