类和对象(2)
一.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
它们主要有以下几个:
(补充说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,如:int、char、double、指针等;自定义类型就是我们使用class、struct等关键字自己定义的类型。)
二.构造函数
构造函数是C++类的特殊成员函数,作用是在创建对象时完成初始化。(即数据结构中的Init)
构造函数的特点:
1.名称与类名完全相同。
2. 无返回值(包括不写void)。
3.对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。
作用:
为对象的数据成员设置初始值,确保对象状态有效。
可执行资源分配等初始化操作(如打开文件、分配内存)。
分类:
默认构造函数:无参数,若用户未定义任何构造函数,编译器自动生成,对成员做默认初始化(基本类型值不确定,类类型调用自身默认构造函数)。
带参数构造函数:通过参数灵活初始化对象,支持不同创建方式(如根据传入数据设定成员值)。
构造函数可重载(参数列表不同),创建对象时根据参数匹配对应函数。
以上的段C++ 代码,其中 Date 类的构造函数是关键。代码里定义了 Date 类的默认构造函数 Date() ,它在创建对象时自动调用。当 main 函数中创建 Date d1; 时,默认构造函数发挥作用,将 _year 初始化为 1 , _month 初始化为 2 , _day 初始化为 2 。
默认构造函数的存在确保了对象在创建时能获得一组确定的初始值,让对象处于一个合理的初始状态。而另一个对象 d2 ,虽也先由默认构造函数创建,但之后通过 Init 函数重新设置了日期,这体现了构造函数与其他成员函数配合,可灵活控制对象的初始化及后续状态调整。
三.析构函数
析构函数与构造函数功能相反。析构函数并非完成对对象本身的销毁,例如局部对象存于栈帧中,函数结束栈帧销毁时它便自动释放,无需额外处理。C++规定对象在销毁时会自动调用析构函数,用于完成对象中资源的清理释放工作。(即数据结构中的Destroy)
析构函数的特点:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。(这里跟构造类似,也不需要加void)
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数。
6. 还需要注意的是我们显式写析构函数,对于自定义类型成员也会调用它的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
7. 如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数(如Date);若默认析构函数适用,也无需显式定义(如MyQueue);但当类中申请了资源时,必须自定义析构函数,否则会导致资源泄漏(如Stack)。
8. 在一个局部作用域内的多个对象,C++规定后定义的对象先析构。
以上代码C++ 代码的 Stack 类中,构造函数 Stack(int n = 4) 意义重大。它通过 malloc 为栈的数据存储区动态分配内存,参数 n 可指定初始容量,若内存分配失败,及时报错返回,保障程序健壮性。这是栈对象初始化的关键步骤,让对象拥有可用资源。
析构函数 ~Stack() 则是资源回收的保障。当 Stack 对象生命周期结束,像 main 函数中 st1 超出作用域时,析构函数自动调用。它释放构造函数申请的内存,将指针置空,相关变量归零,防止内存泄漏,确保系统资源合理回收。二者配合,实现了栈对象生命周期内资源从获取到释放的完整、正确管理。
不需要写构造函数与析构函数的例子:
以上 Myqueue 类(解释:Muqueue类表示的是用两个栈实现队列)中未显式定义构造函数和析构函数,原因如下:
1. 构造函数: Myqueue 类包含两个 Stack 类型的成员变量 pushst 和 popst 。当 Myqueue 对象创建时,编译器自动生成的默认构造函数会调用 Stack 类的默认构造函数,对这两个成员变量进行初始化,满足了基本初始化需求,所以无需额外定义。
2.析构函数:对象生命周期结束时,编译器自动生成的默认析构函数会依次调用成员变量 pushst 和 popst 对应的 Stack 类析构函数,完成资源释放等清理工作,不存在资源泄漏等问题,因此不必手动编写。
四.拷贝构造函数
拷贝构造函数是C++中构造函数的特殊形式,当构造函数的首个参数为自身类类型的引用,且其余参数均有默认值时,它便属于拷贝构造函数。其具有以下特性:
1. 函数重载:拷贝构造函数是构造函数的一种重载形式。
2. 参数要求:首个参数必须是类类型对象的引用。若采用传值方式,会因语法逻辑上的无穷递归调用导致编译器报错。它也可拥有多个参数,但首个参数需为类类型对象引用,后续参数必须有默认值。
3. 拷贝行为规定:C++要求自定义类型对象进行拷贝操作时,必须调用拷贝构造函数。所以,自定义类型在传值传参和传值返回时,都会触发拷贝构造函数的调用。
4. 编译器生成规则:若未显式定义拷贝构造函数,编译器会自动生成。对于内置类型成员变量,自动生成的拷贝构造函数执行值拷贝(即按字节逐个拷贝) ;对于自定义类型成员变量,则会调用其自身的拷贝构造函数。例如, Date 类成员全为内置类型且不涉及资源指向,编译器自动生成的拷贝构造函数就能满足需求,无需手动实现;而 Stack 类虽成员也是内置类型,但存在指针 _a 指向资源,自动生成的浅拷贝方式无法满足要求,需手动实现深拷贝(对指向资源也进行拷贝) ; MyQueue 类内部主要是自定义类型 Stack 成员,编译器自动生成的拷贝构造函数会调用 Stack 的拷贝构造函数,因此也无需手动实现。一般来说,若类显式实现了析构函数并释放资源,就需显式编写拷贝构造函数,反之则通常不必。
5. 返回值相关:传值返回会创建临时对象并调用拷贝构造函数;传引用返回则返回对象的别名,不产生拷贝。但如果返回对象是函数局部作用域内的局部对象,函数结束后该对象会被销毁,此时使用引用返回会产生野引用(类似野指针) 。所以,传引用返回虽能减少拷贝,但必须保证返回对象在函数结束后依然存在。
为什么要传引用调用?
在拷贝构造函数中要传引用而不能传值调用,原因如下: - 避免无穷递归:传值调用时,实参会将值拷贝给形参。若拷贝构造函数参数是按值传递,那么调用拷贝构造函数时,为了生成这个形参对象,又会调用拷贝构造函数(因为传值本质是一次拷贝) ,如此循环往复,就会形成无穷递归。就像图中展示的,每次调用拷贝构造函数前传值传参的拷贝行为,不断触发新的拷贝构造调用,导致程序崩溃。
- 提高效率:传引用不会产生新的对象拷贝,直接操作原始对象的引用,相比传值调用避免了不必要的对象复制开销,尤其是对于复杂对象,能显著提升程序运行效率。
拷贝构造函数的调用
拷贝构造函数定义 代码中 Date(const Date& d) 是 Date 类的拷贝构造函数,以引用形式接收参数,避免传值调用带来的无穷递归问题。它将传入对象 d 的成员变量 _year 、 _month 、 _day 赋值给当前对象,实现对象间数据成员的拷贝。
拷贝构造函数调用 在 main 函数中, Date d2(d1); 语句调用了拷贝构造函数,将 d1 对象的数据成员拷贝给新创建的 d2 对象,使得 d2 和 d1 数据一致,之后 d2.Print() 输出与 d1 相同的日期信息。 而 Date(Date* d) 并非拷贝构造函数(拷贝构造函数参数应为类对象引用),但也能实现从一个对象指针获取数据来初始化当前对象。
在拷贝构造函数中:对比传引用与传指针
五.运算符重载
(1) 当运算符被用于类类型的对象时,C++ 允许对类类型对象进行运算符重载,赋予运算符新含义。若类类型对象使用运算符时无对应重载,会编译报错。以下是运算符重载要点:
(2)函数特性
运算符重载函数名字由 operator 与要重载的运算符组成,和普通函数一样,有返回类型、参数列表和函数体。
(3)参数规则
①一元运算符重载函数有一个参数;二元运算符重载函数有两个参数,左侧运算对象传第一个参数,右侧传第二个。
②若重载为成员函数,首个运算对象由隐式 this 指针接收,参数比运算对象少一个 。
(4)重载限制
①重载后运算符优先级和结合性与内置类型运算符一致,不能创造语法中不存在的新运算符,如 operator@ 。
② .* 、 :: 、 sizeof 、 ?: 、 . 这 5 个运算符不能重载 。
③重载操作符至少有一个类类型参数,不能改变内置类型对象的运算含义 。
(6)特殊情况
①对于 Date 类等,要根据实际意义决定重载哪些运算符,如 Date 类重载 operator- 可能有意义,而 operator+ 可能无意义。
②重载 ++ 运算符时,前置 ++ 和后置 ++ 函数名都为 operator++ 。为区分,后置 ++ 重载函数增加一个 int 形参 。
③<< 和 >> 运算符需重载为全局函数。若重载为成员函数, this 指针会占据左侧运算对象位置(第一个形参),导致调用形式如 对象<<cout 不符合习惯。 ④全局重载时, ostream/istream 放第一个形参,类类型对象放第二个 。
小练习:operator==来判断日期是否相等
以上C++ 代码,定义了 Date 类,包含构造函数来初始化年、月、日。还重载了 == 运算符用于判断两个日期是否相等 。在 main 函数中创建了两个 Date 对象,并通过调用重载的运算符进行比较,最终输出比较结果,清晰展示了运算符重载在日期比较场景中的应用。