前言
在前面简单的介绍了OOP,什么是类,在类中的this指针。接下来就深入理解类和对象。
默认成员函数
- 默认构造函数:用于在创建对象时初始化对象的成员变量。
- 默认拷贝构造函数:用于使用已存在的对象来初始化新创建的对象。
- 默认析构函数:在对象生命周期结束时执行清理操作,通常是删除动态分配的资源。
- 默认重载赋值运算符:用于将一个对象的值赋给另一个已存在的对象。
- 默认移动构造函数(C++11及以后版本):用于将资源从一个对象转移到另一个对象,而不是复制这些资源。
- 默认重载移动赋值操作符(C++11及以后版本):用于将一个对象的资源移动到另一个已存在的对象
接下来就依次介绍默认成员函数
一、构造函数
构造函数的特性
构造函数类似于初始化函数,在写Stack时候,首先需要初始化Stack。
构造函数并不是开辟空间创建对象,而是初始化
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用构造函数
- 构造函数可以重载
class Date { public: //构造函数 Date() //无参构造函数 { } Date(int year , int month , int day ) //带参构造函数 { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d0;//调用无参数构造 初始化的值是随机值 Date d1(2024, 7.1, 8);//调用带参构造 d1.Print(); return 0; }
注意:在调用无参构造函数,初始化时候是随机值。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
class Date { public: //Date(int year, int month, int day) //{ // _year = year; // _month = month; // _day = day; //} private: int _year; int _month; int _day; }; int main() { Date d0; Date d1; d1.Print(); return 0; }
- 在这段代码中,没有显示定义构造函数,那么编译器就会生成一个默认构造函数.
- 如果显示定义(即将代码取消注释),那么 d0 d1将没有可调用的默认构造函数 .
注意:在 (C++11) 中,针对内置类型不初始化打了补丁,成员变量在声明的时候可以给默认值
class Date { private: int _year = 1; int _month = 1; int _day = 1; };
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
//无参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //全缺省构造函数 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
小结
- 一般情况下,构造函数自己写
- 内置类型都有缺省值,并且符合我们的要求可以不写构造函数
- 类成员变量均为自定义类型(这些均有自己的构造函数),会调用他们自己的构造函数.
二、析构函数
析构函数的特性
- 析构函数名是类名前面加“~”
- 无返回值
- 在一个类中,如析构函数未显示定义,将自动生成.
- 不可以重载
- 对象生命周期结束时候C++编译系统自动调用析构函数
//在这里面以以 Stack 为例 class Stack { ~Stack() { free(_a); _a = nullptr; } private: int* _a; int _capacity; int _size; };
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
小结
- 内置类型不做任何处理,调用其自己的析构函数.
- 动态申请资源,就要显示定义析构函数.
- 没有动态申请资源可以不写析构函数.
三、拷贝构造函数
拷贝构造函数的特性
- 只有单个形参数,一般用const修饰.
- const修饰是为了防止赋值错误:例如:本应该_year = year 却写成了year = _year;
- 拷贝构造是析构函数的重载形式.
- 参数只有一个,且是类类型对象的引用
- 自定义传值调用C++编译器调用其拷贝构造函数.
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // DateP(const Date d); 错误示范 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,7,9); Date d2(d1); return 0; }
注意 : 如果拷贝构造函数不加引用,会发生无穷递归. 函数在传值调用会调用拷贝构造函数,如其函数不加入引用(即传值调用),就会发生无限递归.
- 若未显示定义拷贝构造函数,编译器自动生成.
- 内置类型完成值拷贝(浅拷贝)
- 自定义类型会调用它自己的拷贝构造。
深浅拷贝
浅拷贝:浅拷贝是指复制对象时只复制其基本数据类型成员和指针成员,而不复制指针所指向的内存内容
深拷贝:深拷贝则是在复制对象时,不仅复制基本数据类型成员和指针成员,还会为指针所指向的内存内容分配新的内存空间,并将原有内容复制过去。
四、赋值运算符重载(关键字:operator)
运算符重载
运算符重载的概念
- 运算符重载是一项允许开发者为用户自定义的数据类型(如类)定义运算符的特性。
- 通过运算符重载,可以使自定义类型的对象能够使用类似于内置类型的运算符进行操作,从而提高代码的可读性和自然性。
- 运算符重载实质上是函数重载的一种特殊形式,重载的函数通过关键字
operator
后跟要重载的运算符符号来命名。
运算符重载的规则
- 运算符重载的限制:C++不允许创建新的运算符,只能对已存在的运算符进行重载。重载运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
- 运算符重载的实现方式:运算符重载可以通过类的成员函数或非成员函数(通常作为友元函数)来实现。成员函数形式的运算符重载通常用于双目运算符,因为它们可以省略一个参数(即隐含的
this
指针)。非成员函数形式的运算符重载则适用于不涉及类对象或需要对称操作的场合。
- 运算符重载的设计原则:重载运算符应该保持其操作的直觉性,避免产生歧义。例如,重载加号运算符(
+
)应该执行加法操作,而不是其他操作。
注意:
- 作为类成员函数重载,其形参操作数少一个,形参的第一个参数隐含的this指针。
- (.*)(: :)(sizeof)(? : :)(.)这五种不可以重载。
赋值运算符重载
重载格式:
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
重载注意:
- 赋值运算符重载不能是全局函数,它必须是类的成员函数。
- 重载赋值运算符时,应当考虑异常安全性,确保即使在赋值过程中发生异常,也不会留下半更新的状态。
- 重载赋值运算符时,可以选择返回
const
引用,以避免在赋值后对当前对象进行不必要的修改。
class Date { public : Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date (const Date& d) { _year = d._year; _month = d._month; _day = d._day; } //赋值运算符重载 Date& operator=(const Date& d)//返回this,用来连续赋值。 { if(this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year ; int _month ; int _day ; };
- 赋值运算符只能重载成类的成员函数不能重载成全局函数(因为其属于默认成员函数)
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就 和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。(浅拷贝)
单目运算符号(自增、自减)
自增
在下面的例子中:
前置:
- 前置++:返回+1之后的结果
- 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
后置:
- 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
- C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
- 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存 一份,然后给this+1
- temp是临时对象,因此只能以值的方式返回,不能返回引用
// 前置++ Date& Date::operator++() { return *this += 1; } // 后置++ Date Date::operator++(int) { Date tmp; tmp += 1; return *this; }
自减
在这里和前置++ 后置++ 思想一致。
// 后置-- Date Date::operator--(int) { Date tmp; tmp -= 1; return *this; } // 前置-- Date& Date:: operator--() { return *this -= 1; }