类与对象--2
引言:实际上每一个类尽管没有显示实现任何成员函数,也会默认生成六个函数,我们称之为默认成员函数,我们目前本章节重点介绍前四个,后两个取地址重载不重要,简单介绍
本章重点:我们需要了解,我们不做实现,那么系统生成的这六个默认函数,会实现什么,能否满足我们需求,要是无法满足,那么我们需要自己实现,应该如何实现呢、
一.构造函数
主要作用:是对象实例化时候初始化对象,不是开空间创建对象,且自动调用
如果对象的某个数据成员需要独立分配堆内存,那么我需要在构造函数中手动 new
来分配,并记得在析构函数中 delete
。
int main()
{date d1; //注意这里编译器会自动在栈上分配内存date * d2; //这里是new操作符分配内存return 0;//也就是在析构之前内存就被分配好了,在对象创建之际
}
构造函数的特点:
- 函数名和类名相同
- 无返回值,(就比如一个date类 ,构造函数就是date() ,然后就开始实现即可)
- 如果类中没有显示定义构造函数,C++编译器会自动生成一个无参默认构造
- 对象实例化时,系统自动调用对应的构造函数。
- 构造函数可以根据需求重载
- 无参构造,全缺省构造,编译器默认生成的构造函数都是默认构造函数,且只能存1,简单来说就是不传实参就可以调用的构造函数就是默认构造函数,
内置类型(int, double, 指针等):编译器生成的构造函数不处理,保持随机值
自定义类型(string, vector, 自定义类等):编译器会调用其默认构造函数
我们写构造函数的目的就是内存初始化,后面会学到初始化列表。我们一般就用构造函数赋值,或者初始化列表来初始化。
初始化是因为在main函数中创建一个类类对象,然后在我们初始化对象的成员变量的时候,假设构造函数里有缺省参数,且我们构建对象的时候传足了参数,那么就会直接初始化,其他情况需要我们手动写构造函数。包括动态分配内存空间的情况。
二.析构函数
2.1析构函数的作用:
构造是用来初始化,析构作用相反,用来完成对象资源的清理释放工作。
既然如此,无资源申请则无需手动重写析构函数
2.2是释放而非销毁对象,对象的销毁:
对象的销毁是与创建对象的函数栈帧销毁有关。一般在主函数中创建,主函数的函数栈帧又包括有函数参数,函数的返回地址,函数的局部变量(这里就包括对象),以及一些寄存器状态。
2.3析构函数的特点
- 析构函数的命名:~类名()
- 析构函数无参无返回值
- 未显示定义会自动生成默认的析构
- 在对象生命周期结束后自动调用析构
- 无论你是否显式编写析构函数,类的所有自定义类型成员都会在适当的时候自动调用它们自己的析构函数。
- 如果一个局部域有多个对象,那么注意后定义的先析构
- 和构造一样,不会处理内置类型。自定义类型成员会调用他本身的析构
class Example { public:int* data;Example() {data = new int[100]; // 分配堆内存std::cout << "分配了100个int的内存" << std::endl;}// 没有自定义析构函数!// 默认析构函数不会 delete[] data; };
三.拷贝构造函数
3.1拷贝构造函数的作用
实现对于同类的构造,(拷贝构造函数的首个参数是自身类型的引用),别的参数都有默认值,也就是相当于构造函数的缺省参数
MyClass(const MyClass& other, int extra_param = 100, bool flag = true);
//缺省参数根据拷贝构造时候提供的参数来确定是否调用
3.2拷贝构造函数特点
- 拷贝构造函数是构造函数的一个重载
- 首个参数必须是类类型对象的引用,传值会引发无穷递归,如何理解呢,就是我们C++中的机制,传值时需要一个临时空间作为副本来存储这个临时对象,但我用对象传值拷贝我是不是还需要拷贝构造来构造这个对象的副本,我依旧要调用拷贝构造,反复进行就会引发无穷递归。当我们调用引用,引用本身就是对象的一个副本,不需要拷贝,则就解决了这个问题。
- 没显示定义拷贝构造的时候,编译器自动生成的默认拷贝构造会对内置类型完成浅拷贝(逐个字节拷贝),这就导致了数据混乱,因为地址是同一块。我们自己写拷贝构造是应当去主动申请新内存空间的,默认拷贝构造无法实现功能。 自定义类型默认拷贝构造就会直接调用自定义类型的拷贝构造。
- 无指向资源不必要手动实现拷贝构造,默认拷贝构造也可实现,但像比如说栈类,我们指向了资源,那么就需要深拷贝了,所以说,如果一个类手动实现了析构函数并释放了资源,那么就需要手动实现拷贝构造,
- 传值返回是产生一个临时对象调用拷贝构造然后返回;传引用返回是返回对象的别名,没产生拷贝。
Stack fun1() //传值返回 {stack st; return st; //这里是返回的st的副本 }int main() {stack ret = fun2(); //将副本值传给retreturn 0; }
Stack& func2() { // 返回Stack引用Stack st; // 局部对象return st; // 返回局部对象的引用!意思是想返回st的引用,这里随着函数栈帧销毁,走出了函数局部域就悬空引用了 }int main() {xStack ret = func2(); // 无法返回st的别名,早已被销毁return 0; }
所以我们返回引用时需要注意生命周期,解决返回引用出栈帧被销毁可加static延长生命周期
- 自定义类型拷贝必须调用拷贝构造,所以自定义类型(传值,传返回都需要调用拷贝构造)
这里我们使用老实编译器未优化时候的前置后置++来举例子说明传值,传返回都需要拷贝构造class Integer { private:int value; public:// 前置++Integer& operator++() {++value;return *this; // 直接返回自身引用,无拷贝}// 后置++Integer operator++(int) // int参数仅用于区分前后置{ Integer temp = *this; // 拷贝构造:第一次拷贝++value; // 调用前置++完成自增return temp; // 按值返回:第二次拷贝} };
四.赋值运算符重载
4.1运算符重载(实际上是一种函数)
实现目的:目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
比如要计算两个数的差n1 - n2; //重载后利用返回值可以很明了获得差reduce(n1,n2); //就是改善这种可读性差的情况
- 命名:operator 运算符名 (与构造析构不同,具有自己的返回值和参数列表
- 参数:重载运算符的参数个数和运算对象数量一样,就是几元运算符几个参数。二元是左侧运算对象传给第一个参数,右侧给第二个
//定义一个日期类 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;};//在全局重载运算符 == 来判断日期是否相等 //重载为全局的⾯临对象访问私有成员变量的问题 // 有⼏种⽅法可以解决: // 1、成员放公有 // 2、Date提供getxxx函数 // 3、友元函数 // 4、重载为成员函数//我们这里将私有成员放公有了 bool operator==(const Date& d1, const Date& d2) {return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day; }int main() {Date d1(2025, 10, 5);Date d2(2025, 10, 6);// 运算符重载函数可以显⽰调⽤operator==(d1, d2);// 编译器会转换成 operator==(d1, d2);d1 == d2;return 0; }
- 如果重载运算符是成员函数,则首个运算对象默认给this指针,因此参数第一个会隐式掉。
将重载放到类内,首个参数默认为thisbool operator==(const Date& d) {//这里实际上是this. _year 给类私有成员变量赋值,私有成员变量在类内隐式thisreturn _year == d._year&& _month == d._month&& _day == d._day; }int main() {Date d1(2025, 10, 5);Date d2(2024, 10, 6);// 运算符重载函数可以显⽰调⽤d1.operator==(d2);// 编译器会转换成 d1.operator==(d2);d1 == d2;return 0; }
- 运算符重载后优先级和结合性和重载之前一致
- 不可重载新的符号,只能重载已有的。还有 ( .* :: sizeof ?: .)这五个无法重载
- 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性.重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
- 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
Date& operator++() {cout << "前置++" << endl;//_day++;return *this; }Date operator++(int) //这个参数是为了区分前置后置++ {Date tmp; //用临时值保存++之前的值,函数栈帧销毁这个tmp也会销毁,所以需要返回值拷贝不能返回引用cout << "后置++" << endl;//...return tmp; }int main() {// 编译器会转换成 d1.operator++();++d1;// 编译器会转换成 d1.operator++(0);d1++;return 0; }
4.2赋值运算符重载
赋值运算符重载是默认成员函数,用于完成两个已经存在的对象的直接拷贝赋值(与拷贝构造不同,拷贝构造是一个存在的拷贝初始化给一个不存在但要创建的)
赋值运算符的特点
- 实际上是一个运算符重载,规定必须重载为成员函数.建议参数写为const 当前类类型引用,减少传值传参的拷贝
Date& operator=(const Date& d) {_year = d._year;_month = d._month;_day = d._day;return *this; }
- 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值。
- 没显示实现则和默认拷贝构造类似,浅拷贝内置类型,且自动调用自定义类型成员变量的赋值重载函数。
- 如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。 这个原则和拷贝构造一致。
五.取地址运算符重载
5.1const成员函数
- const放在成员函数参数列表后面。
- 实际上是限制this指针的修改能力,表明在该成员函数中不能对类的成员进行修改,基本上就是给那种不需要修改成员变量的函数后面都要加上const修饰来提高安全性
- 还用来使得const对象需要访问成员函数的情况,那就给这个成员函数加上const修饰
- 隐含的this指针由 Date* const this 变为 const Date* const this
void Print() const //这个不需要改变成员变量,加上const安全
{
cout << _year << "-" << _month << "-" << _day << endl;
}
5.2取地址运算符重载
分为普通取地址运算符和const取地址运算符,使用场景很有限,就是不希望被别人拿到当前类对象的地址,就可以自己重载
class Date
{
public :Date* operator&(){return this;// return nullptr;}const Date* operator&()const{return this;// return nullptr;}
private :int _year ; // 年int_month ; // ⽉int _day ; // ⽇};