目录
一、类的默认成员函数
🐥 类的默认成员函数:任何类中,如果我们在类中什么都不写,由编译器自动生成的成员函数
接下来就以日期类为例,分开学习每一个默认成员函数、由浅入深、生动易懂六个默认成员函数:
1️⃣构造函数
2️⃣析构函数
3️⃣拷贝构造函数
4️⃣赋值重载
5️⃣6️⃣两个取地址重载
二、构造函数
🍔如何理解构造函数
class Date { public: void Init(int year, int month, int day) { year_ = year; month_ = month; day_ = day; } private: int year_; int month_; int day_; };
这是一个日期类,我们在创建了对象后,需要用 Init函数 给对象初始化
在C++中, 构造函数 的 作用 就是 代替了 Init函数 (构造函数➡️初始化)
🍟构造函数的书写和特点
构造函数虽然叫“构造”,但是主要的任务是初始化,而不是开空间创建对象
无返回值(不写返回值) 类名(形参列表)
{
//初始化工作
}
构造函数是特殊的成员函数,其特点是:
1️⃣函数名称和类名相同
2️⃣没有返回值
3️⃣创建类对象时由编译器自动调用
4️⃣并且在对象整个生命周期内只调用一次
5️⃣支持函数重载
示例:class Date { public: // !!!函数名和类名相同 // !!!无返回值 Date(int year, int month, int day) { year_ = year; month_ = month; day_ = day; cout << "构造函数调用:" << year_ << endl; } // !!!支持重载 Date() { //... } private: int year_; int month_; int day_; };
6️⃣如果类中没有显示定义构造函数,编译器会自动生成一个无参的默认成员函数,一旦显示定义,编译器就不会再生成
验证6️⃣:
class A { public: // A(int a) // { // _a = a; // } private: int _a; }; void test() { A a; }
🌟解释:我们显示定义后,编译器就不会生成默认的成员函数,在创建对象时,传参不够,编译失败
注释我们显示定义的构造函数后,即可使用默认生成的成员函数,成功编译
7️⃣编译器生成的默认成员函数的特点:👉内置类型不做处理
👉自定义类型成员会调用这个成员的构造函数
验证7️⃣:class A { public: A() { cout << "A构造函数调用:" << endl; } private: int _a; }; class Date { public: void Print() { cout << year_ << "年" << month_ << "月" << day_ << "日" << endl; } private: //内置类型 int year_; int month_; int day_; //自定义类型 A _a; };
8️⃣默认构造函数还记得这个错误提示吗?
默认构造函数指的不止是由编译器自动生成的无参成员函数
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且只能有一个,多个存在会造成调用二义性的问题
三、析构函数
🍔如何理解析构函数
析构函数的学习可以类比构造函数的学习
学习了构造函数后,我们知道构造函数的作用是初始化对象
那么清理资源的工作又是谁来做呢?
析构函数:析构函数的作用和构造函数相反,析构函数的工作是在对象销毁时自动调用析构函数,完成对象中资源的清理
以一个简单的栈来举例:class Stack { public: //构造函数(完成初始化) Stack(int capcity=4) { _a = (int*)malloc(sizeof(int) * capcity); if (_a == nullptr) { perror("malloc fail!"); return; } _capcity = capcity; _size = 0; } //析构函数(完成资源的清理) ~Stack() { if (_a!=nullptr) { free(_a); _a = nullptr; _capcity = 0; _size = 0; } } private: int* _a; int _size; int _capcity; };
🍟构造函数的书写和特点
析构函数的写法:
~类名 (形参列表)
{
//资源清理}
析构函数是特殊的成员函数,其特点是:
1️⃣析构函数名是在类名前加~
2️⃣没有返回值没有参数
3️⃣对象生命周期结束时,由编译器自动调用
4️⃣一个类只能有一个析构函数
5️⃣若未显示定义,编译器自动生成默认的析构函数6️⃣编译器生成的默认析构函数的特点:
👉内置类型不做处理
👉自定义类型成员会调用这个成员的析构函数
🫵总结:
如果类中没有申请资源(malloc...),析构函数可以不写,因为在对象销毁后,我们也无法访问那块空间,可以直接使用编译器生成的默认析构函数,比如Date类
如果类中有资源的申请,就一定要写析构函数,否则就会造成内存泄漏,如Stack类
四、拷贝构造
🍔如何理解拷贝构造
在一些场景下我们需要用一个已存在的对象来创建一个新的对象:
🫗就像这样:int a=10; int b=a;
类类型的对象有时也有这样的需求,
Date d1(2024,1,1); Date d2=d1;
拷贝构造:用已存在的类类型对象创建新对象
🍟默认拷贝构造函数特点
默认拷贝构造的特点:
👉内置类型:值拷贝
👉自定义类型:调用它的拷贝构造
🫗代码示例:class Date { public: Date(int year = 1,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_; };
测试结果:
❓问题:默认生成的拷贝构造似乎就能完成我们期望的工作,为何有时还要自己写?
对于Date类确实可以,但如果是Stack类,我们再来看结果~
🫗解释:默认生成的拷贝构造函数对于内置类型会进行简单的值拷贝(浅拷贝),直接将a对象的值复制给b,所以a和b对象中的 _a 会指向同一块空间,这也就造成了问题,两个对象在销毁时都会调用析构函数,造成了同一块空间被释放两次的问题🫵总结:
如果类中没有申请资源(malloc...),拷贝构造函数可以不写,如果类中有资源的申请,就一定要写析构函数,否则就会在析构时出现二次析构程序崩溃,如Stack类
🌮拷贝构造函数书写
拷贝构造函数也是特殊的成员函数,特征如下:
1️⃣拷贝构造函数是构造函数的一个重载形式
2️⃣拷贝构造函数的参数只有一个,并且必须是同类型对象的引用
3️⃣若未显示定义,编译器会生成默认的拷贝构造函数
加入拷贝构造后的Stack类:class Stack { public: //构造函数(完成初始化) Stack(int capcity=4) { _capcity = capcity; _size = 0; cout << "Stack()构造函数" << endl; _a = (int*)malloc(sizeof(int) * capcity); if (_a == nullptr) { perror("malloc fail!"); return; } } //拷贝构造函数 Stack(const Stack& s) { _capcity = s._capcity; _size = s._size; _a =(int*) malloc(sizeof(int) * _capcity); if (_a == nullptr) { perror("malloc fail!"); return; } memcpy(this->_a, s._a, sizeof(int)*_size); } void Push(const int& data) { _a[_size] = data; _size++; //... } //析构函数(完成资源的清理) ~Stack() { cout << "~Stack()析构函数" << endl; if (_a!=nullptr) { free(_a); _a = nullptr; _capcity = 0; _size = 0; } } private: int* _a; int _size; int _capcity; };
🫗运行结果:正常运行
🥪拷贝构造书写时注意
拷贝构造的参数必须是类类型对象的引用,如果不是引用,将会直接报错
问题‼️:
小知识👓:
拷贝构造函数调用的场景:
1️⃣使用已存在的对象创建新对象
2️⃣函数参数类型为类类型参数
3️⃣函数返回值为类类型
五、赋值运算符重载
🍔运算符重载
学习赋值运算符重载之前,我们先来学习运算符重载
C++为了增加代码的可读性,引入了运算符重载
其字面意思就是让运算符能够有不同的意义,不过仅对类类型对象而言
拿日期类来讲,有了运算符重载,我们可以进行两个日期的比较、两个日期相减-,日期加天数+,减天数的操作可以写出下面的代码:
虽然是函数,但是使用起来十分明了
Date d1(2024,1,1); Date d2(2024,10,1); int day=d2-d1; cout<<"距离国庆还有"<<day<<"天"<<endl; cout<<(d1>d2)<<endl; Date d3=d1+100;
函数原型: 返回值类型 operator操作符(参数列表)
注意:
👉不能连接其他符号,创造操作符 例如:operator@
👉重载操作符中必须有一个类类型参数
👉不能重载内置类型间的操作符
👉作为类成员函数重载时,第一个参数this被隐藏
👉.* :: sizeof ?: . 这5个运算符不能重载
🫗示例:
bool operator>(const Date d) { if (year_ < d.year_) { return false; } else if (year_ == d.year_ && month_ < d.month_) { return false; } else if (year_ == d.year_ && month_ == d.month_ && day_ <= d.day_) { return false; } else { return true; } } bool operator==(const Date d) { if (year_ == d.year_ && month_ == d.month_ && day_ == d.day_) return true; return false; }
有了 > 和 == 后,我们可以复用 > 和 == 实现>= 、<、 <= 这些函数
调用时:
🍟赋值运算符重载
📗
赋值运算符重载即让类类型对象也可以使用 =
赋值运算符的重载形式:
参数类型:const T& ,传递引用可以提高传参效率
返回值类型:T& 返回引用可以提高返回效率,有返回值就可以支持连续赋值
Date& operator=(const Date& d) { if (this != &d) { year_ = d.year_; month_ = d.month_; day_ = d.day_; } return *this; }
📘
‼️赋值运算符只能重载成类的成员函数不能重载成全局函数
🫗原因:
赋值运算符是默认构造函数,如果不显示实现,编译器会生成一个默认的。
如果定义全局的赋值运算符重载,就和编译器在类中自动生成的赋值运算符重载冲突。因此,赋值运算符只能是类的成员函数。
📙
赋值运算符重载作为默认成员函数,如果用户没有显示定义,编译器就会自动生成,以值的方式逐字节拷贝
默认赋值运算符重载的作用:
👉内置类型:值拷贝
👉自定义类型:调用它的赋值运算符重载
📕
何时需要我们来显示定义呢?
其实和拷贝构造的定义的情况十分类似:如果类中没有申请资源(malloc...),赋值运算符重载可以不写,如果类中有资源的申请,就一定要写析构函数,否则就会在析构时出现二次析构程序崩溃,如Stack类
🌮拷贝构造和赋值运算符重载
大家是否学习完之后,就把这两个函数混淆了
我来带大家理清一下🌟
拷贝构造:已存在的对象给未存在的对象拷贝
赋值运算符重载:已存在的两个对象间的拷贝
🫗代码验证:
class A { public: A(int a = 0) { _a = a; } //拷贝构造 A(const A& data) { cout << "拷贝构造" << endl; _a = data._a; } //赋值运算符重载 A& operator=(const A& data) { cout << "赋值运算符重载"<< endl; _a = data._a; return *this; } private: int _a; }; A func(A data) { return data; } int main() { A a1; A a2; A a3 = a1;//拷贝构造 a2 = a1;//赋值运算符重载 a3 = func(a1); return 0; }
🥪 前置++和后置++重载
前置++和后置++都是一元运算符
🔥
由于无法通过返回值类型构成重载
为了能区分前置++和后置++,我们这样规定:
后置++在重载时多增加一个int类型的参数,但调用该函数时参数不用传递
//后置++ 返回调用之前的值 Date operator++(int) { Date tmp = *this; day_++; ++*this; return tmp; } //前置++ Date& operator++() { day_++; if (day_ > GetMonthDay(year_, month_)) { day_ = 1; month_++; if (month_ == 13) { year_++; month_ = 1; } } return *this; }
六、const成员
const成员:将const修饰的“成员函数”称之为const成员函数,实际修饰该成员函数隐含的this指针
❓问题引出:
那么如何来修饰隐含的this指针:在成员函数后加const
🫗几个小问题:
1️⃣const对象可以调用非const成员函数吗?
答:不可以,属于权限的放大
2️⃣非const对象可以调用const成员函数吗?
答:可以,属于权限的缩小
3️⃣const成员函数内可以调用其他非const成员函数吗?
答:不可以,属于权限的放大
4️⃣非const成员函数内可以调用其他的const成员函数吗
答:可以,属于权限的放大
七、取地址运算符重载
一般对象和const对象取地址重载
🌟最后两个默认成员函数一般不用重新定义,使用场景非常少
class Date { public: Date* operator& () { return this; } const Date* operator&()const { return this; } };
八、结语
🫡你的点赞和关注是作者前进的动力!
🌞最后,作者主页有许多有趣的知识,欢迎大家关注作者,作者会持续更新有意思的代码,在有趣的玩意儿中成长!