⭐️ 往期相关文章
✨ 链接1:C++类和对象(上)
✨ 链接2:C++基础知识 (命名空间、输入输出、函数的缺省参数、函数重载)
✨ 链接3:C++基础知识 (引用)
✨ 链接4:C++基础知识(inline、auto、nullptr)
⭐️ 类和对象(下)
🌟 类的6个默认成员函数
如果一个类中什么成员都没有,简称空类。当类中什么都不写的时候,编译器会自动生成6个默认成员函数。
- 初始化和清理
- 构造函数: 主要完成类的初始化工作
- 析构函数: 主要完成类的清理工作
- 拷贝复制
- 拷贝构造: 是使用同类对象以初始化的方式创建对象
- 赋值重载: 把一个对象赋值给另一个对象
- 取地址重载
- 主要是普通对象和
const
对象取地址
- 主要是普通对象和
🌟 构造函数
构造函数是一个特殊的成员函数,名字与类名相同 ,创建类的时候编译器会自动调用类的构造函数,来做类的初始化操作 ,在对象整个生命周期内只会调用一次。
构造函数的特征:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显示定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦显示定义编译器将不在生成。
example1:
cpp
#include <iostream>
using namespace std;
class Date {
public:
// 无参的构造函数
Date() {
cout << "Date()" << endl;
}
// 带参的构造函数
Date(int year , int month , int day) {
cout << "Date(int year , int month , int day)" << endl;
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用无参的构造函数
Date d2(2023 , 8 , 6); // 调用带参的构造函数
return 0;
}
example2:
cpp
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 d1; // 调用编译器自动生成的默认构造函数
return 0;
}
ps:
如果当创建类的时候没有合适的默认构造函数会报错。
在C++中把类型分为内置类型和自定义类型,内置类型就是语言本身自带的类型,如:int
、double
、char
等等。自定义类型就是 class
、struct
、union
等等。默认构造函数并不会对内置类型进行处理 ,比如用日期类来举例:_year
、_month
、_day
依然是随机值,但是默认的构造函数会对自定义类型的成员处理并调用它们的默认构造函数。
example3:
cpp
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
这是C++当中比较不好理解的一个点,因为默认的构造函数只会处理自定义类型而不会处理内置类型,所以在C++11中,针对内置类型成员不初识化的缺陷给出了解决方案。内置类型成员变量在类中声明时可以给默认值。
example4:
cpp
class Date {
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};
ps:
这里的 =
并不是初始化值的意思,而是声明。这样当调用构造函数的时候如果在构造函数中初始化则会使用构造函数的初始化的结果,若没有则使用当前的默认值。总结一句话就是这样的操作也可以为内置类型初始化了。
默认构造函数的含义: 无参的构造函数 和 全缺省的构造函数 还有 没写编译器自动生成的构造函数 都称为默认构造函数,并且默认构造函数只能有一个。
🌟 析构函数
析构函数与构造函数的功能相反,析构函数不是完成对象本身的销毁,这些是由生命周期结束时编译器做的。而在对象销毁时会自动调用析构函数,完成对象当中资源的清理工作。 比如类中的成员有动态开辟的内存,当类销毁时使用析构函数释放内存还给操作系统。
析构函数的特征:
- 析构函数名是在类名前加上
~
- 无参数无返回值类型
- 一个类只能有一个析构函数,若没有定义,系统会自动生成默认的析构函数。析构函数不能重载
- 当对象生命周期结束时,编译器会自动调用析构函数。
example5:
cpp
#include <iostream>
using namespace std;
class Date {
public:
// 无参的构造函数
Date () {
cout << "Date" << endl;
}
// 析构函数
~Date() {
cout << "~Date()" << endl;
}
private:
int _year = 2023;
int _month = 8;
int _day = 6;
};
int main()
{
Date d;
return 0;
}
编译器自动生成的析构函数会做什么事情呢?
构造函数和析构函数都是只对自定义类型处理。内置类型的成员,销毁时不需要资源的清理,最后系统直接回收即可。对于自定义类型,当前类的默认析构函数会调用这个自定义类型的析构函数来销毁。
🌟 构造函数和析构函数的顺序
example6:
cpp
#include <iostream>
using namespace std;
class Test {
public:
Test(int order) {
_order = order;
cout << _order << " Date" << endl;
}
~Test() {
cout << _order << " ~Date()" << endl;
}
private:
int _order;
};
Test t1(1);
/*
构造顺序:1 2 3 4 5
析构顺序:3 2 5 4 1
*/
int main() {
Test t2(2);
Test t3(3);
static Test t4(4);
static Test t5(5);
return 0;
}
ps:
构造函数的顺序比较简单,因为在对象实例化时就会调用构造函数完成初始化。所以是从上往下依次构造。而析构函数在是对象的生命周期结束的时候调用,这里可以用栈来理解,先创建的就先压栈,所以析构的时候是先析构栈顶的对象,所以这里是创建的早就析构的晚,创建的晚就析构的早,而 static
在静态区中所以当出了 main
函数作用域中,局部对象优先被析构,其次是 static
对象再是全局对象(栈帧和栈里面的对象都要符合后进先出,也就是后定义先析构)。
🌟 拷贝构造函数
拷贝构造的含义是:使用已经存在的类对象创建新对象时编译器会自动调用类的拷贝构造。
拷贝构造函数的特征:
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是当前类的类对象的引用 ,使用传值方式编译器会直接报错,因为这样会引发无穷的递归调用。
ps:
因为如果是传值调用,那么当前类对象会拷贝一份给形参,这里又会调用形参的拷贝构造函数。而拷贝构造函数又要先传参,传参就调用拷贝构造,所以就引发了无穷的递归调用。
example7:
cpp
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 最好使用 const 修饰
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
// 拷贝构造
Date d2(d1);
// 拷贝构造
Date d3 = d2;
return 0;
}
- 若未显示定义,编译器会自动生成默认的拷贝构造函数。默认的拷贝构造函数按内存的字节完成拷贝,这种拷贝叫做浅拷贝。默认拷贝函数会对内置类型和自定义类型都会处理,内置类型是按字节方式直接拷贝的,自定义类型是调用其拷贝构造函数完成拷贝的。
ps:
当成员中有指向动态开辟内存的指针,如果使用浅拷贝,那么两个指针将会指向同一块内存空间,当修改其中一个指针里的数据时,另一个指针里数据也会被影响。当对象声明周期结束用析构函数的时候,会造成free
两次的情况导致运行崩溃。
拷贝构造函数的调用场景:
- 使用已存在对象创建新对象
- 当用对象当作函数实参传递(实参赋值给形参)
- 函数返回值为类类型对象(函数栈帧销毁会使用临时变量来存放返回值,所以这里造成一次拷贝构造)
🌟 运算符重载
运算符重载是为了让一些自定义类型也可以使用运算符,有较强的可读性。
语法:返回值 operator操作符(参数列表)
特性:
- 不能使用不存在的操作符
- 重载操作符必须有一个类类型参数
- 不能改变操作符的含义
- 作为类成员函数重载的时候,形参看起来比操作数少1,因为成员函数中第一个参数为隐藏的
this
.*
、::
、sizeof
、?:
、.
以上操作符不能重载
ps:
前置++和后置++运算符重载区分方法,带一个形参 int
的为后置++。前置++和后置++是运算符重载,而这两个运算符重载函数又构成了函数重载。
- 前置++
type operator++();
- 后置++
type operator++(int);
🌟 赋值运算符重载
将一个对象赋值给另一个对象时,就需要赋值重载。
- 参数:
const Type&
传递引用可以减少拷贝构造提升效率 - 返回值:
Type&
返回引用也可以减少拷贝构造的次数,而且有返回值可以支持连续赋值
example8:
cpp
#include <iostream>
using namespace std;
class Date1 {
public:
// 构造函数
Date1(int year = 2023 , int month = 8 , int day = 12) {
_year = year;
_month = month;
_day = day;
}
// 赋值拷贝
Date1& operator=(const Date1& d) {
// 若当前两个对象地址不同则赋值
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date1 d1;
Date1 d2(2024 , 8 , 12);
// 赋值重载
d1 = d2;
return 0;
}
ps:
赋值运算符只能重载成类的成员函数不能重载成全局函数 ,因为赋值运算符如果不显示实现,编译器会生成一个默认的,如果在类外实现一个全局的运算符重载,就会和编译器默认生成的赋值重载冲突。
若用户没有显示定义,编译器会生成一个默认的赋值运算符重载,是以浅拷贝的方式拷贝。内置类型是直接按照逐字节的拷贝方式,而自定义类型会调用对应类的赋值运算符的重载完成赋值。
🌟 const
将 const
修饰的成员函数叫做 const
成员函数,const
修饰类成员函数,实际是修饰该成员函数的 this
指针,意思是 this
指向的内容不能被修改。
example9:
cpp
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() const {
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
return 0;
}
ps:
由于 d2
是 const
修饰的对象,当 d2.Print()
实际上是 d2.Print(&d2)
,第一个参数会有一个隐含的 this
指针,而this
指针的类型是 Date * const this
,所以一个 const
的地址传给非 const
修饰的指针时,这是权限的放大所以会 error
,解决办法是在成员函数后加上 const
修饰。这里实际上是 const Date * const this
。
🌟 取地址及const取地址操作符重载
example10:
cpp
class Date {
public:
Date* operator&() {
return this;
}
const Date* operator&()const {
return this;
}
};
ps:
这两个运算符重载函数构成函数重载,这两个函数都要存在才可以,因为如果没有 const
修饰的取地址重载,虽然普通对象和 const
对象都可以接收,但是当返回的时候就有问题了,如果接收返回值的是普通对象,那你返回一个 const
的地址这里造成了权限的放大 error
,所以这里针对 const
和非 const
要采取两种不同的方式。
但是这两个函数一般情况不用自己定义,编译器会自动生成。