1. 六个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数
**默认成员函数:**用户没有显式实现,编译器会生成的成员函数称为默认成员函数
六个默认成员函数可以说是六个天选之子:
2. 构造函数
2.1 概念
拿我们之前写过的栈来举个栗子:
//栈结构 struct stack { int* _a; int top; int capacity; }; //初始化函数 void Init(stack* st) { int* tmp = (int*)malloc(sizeof(int) * 4); if (tmp == nullptr) { exit(-1); } st->_a = tmp; st->top = 0; st->capacity = 4; } int main() { stack st; //创建一个栈 Init(&st); //需要手动初始化 return 0; }
之前我们使用C语言
创建一个栈结构时,首先需要手动将栈初始化,在现实过程中,我们每次创建栈都需要初始化一次很麻烦,而且很容易忘掉这一步骤,这里在C++
中就提出了构造函数
来解决初始化这一问题
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
构造函数的特性:
- 函数名与类名相同。
- 无返回值
- 对象实例化时编译器
自动调用
对应的构造函数。 - 构造函数可以重载,但
默认构造函数只能有一个
语法:
class Date { public: //默认构造函数(不带参数) Date() { _year = 2023; _month = 4; _day = 12; cout << "Date()" << endl; } //默认构造函数(全缺省参数) Date(int year = 2023, int month = 4, int day = 13) { //带参构造函数 (自动完成初始化) _year = year; _month = month; _day = day; cout << "Date(int year = 2023, int month = 4, int day = 13)" << endl; } //自定义构造函数(与默认构造函数构成重载就成立) Date(double a) { _year = 2023; _month = 7; _year = 11; cout << "Date(double a)" << endl; } private: int _year; int _month; int _day; };
默认构造函数:
不带参数或参数为全缺省或编译器默认生成的构造函数称为默认构造函数
构造函数使用:
默认构造函数的设计中留下了一个坑:
- 对于内置类型,默认构造函数不做处理,如
char
、int
、double
等- 对于自定义类型,默认构造函数会去调用属于它们的默认构造函数
这就导致如果类中只有内置类型,默认构造函数就会什么都不做(如上图),为了解决这一个坑,在C++11中新增了一个补丁:内置类型成员变量在类的声明中可以给默认值(缺省值)
class Date { private: //内置类型成员变量在类中声明时可以给默认值 int _year = 2023; int _month = 4; int _day = 12; };
3. 析构函数
3.1 概念
还是拿栈来举栗子:
//栈结构 struct stack { int* _a; int _top; int _capacity; }; //销毁函数 void Destroy(stack* st) { if (st->_a != nullptr) { free(st->_a); st->_a = NULL; st->_top = st->_capacity = 0; } } int main() { stack st; Destroy(&st); //需要手动销毁 return 0; }
之前我们使用C语言
创建一个栈结构使用完时,首先需要手动将销毁,在现实过程中,我们每次创建栈都需要销毁一次很麻烦,而且很容易忘掉这一步骤导致内存泄漏,这里在C++
中就提出了析构函数
来解决销毁这一问题
构造函数:
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而==对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
3.2 特性
析构函数的特性:
- 析构函数名是在类名前加上字符
~
- 无参数无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统
自动调用
析构函数
语法:
class Stack { public: //构造函数 Stack(int size = 4) { int* tmp = (int*)malloc(sizeof(int) * size); if (tmp == nullptr) { exit(-1); } _a = tmp; _top = -1; _capacity = size; } //析构函数 ~Stack() { if (_a != nullptr) { free(_a); _a = nullptr; _top = -1; _capacity = 0; } } private: int* _a; int _top; int _capacity; }; int main() { Stack d1(12); //程序结束时,自动调用析构函数 return 0; }
默认析构函数:
如果我们没写,编译器就会自动生成默认析构函数,如果我们写了,编译器就会用我们自己写的析构函数
同默认构造函数一样,默认析构函数也对内置类型不做处理,对自定义类型,会调用他的默认析构函数,在涉及到动态内存开辟时,为了避免发生内存泄露问题,我们建议自己编写默认析构函数
4. 拷贝构造函数
4.1 概念
这里假如我创建了一个对象,并对其进行了构造和赋值的操作,如果我想有一个同样的对象,这再重复上面的创建构造赋值的操作会很麻烦,于是C++天选之子构造函数
就诞生了
拷贝构造函数:
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用
const
修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
4.2 特性
拷贝构造函数的:
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
- 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
- 拷贝函数经典调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
语法:
class Date { public: Date(int year = 2023, int month = 4, int day = 12) { _year = year; _month = month; _day = day; } //拷贝构造函数,函数名与构造函数相同,参数类型为类 Date(const Date& d) { //d拷贝给*this _year = d._year; _month = d._year; _day = d._day; } private: int _year; int _month; int _day; }; int main() { //将d1拷贝给d2和d3 Date d1; //写法一: Date d2(d1); Date d3 = d1; return 0; }
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
默认拷贝构造函数:
默认拷贝构造函数能完成简单的内置类型的拷贝操作,在不涉及动态空间开辟时,拷贝构造函数没必要写,但是涉及到动态空间开辟的一定要自己写默认拷贝构造函数
这里我们思考一个问题:拷贝构造函数中的形参可以不使用引用吗
答案是不可以的,下面来进行解释:
//传值传参(拷贝) Date(const Date& d) { _year = d._year; _month = d._year; _day = d._day; } //传引用传参(起别名,共用一块空间) Date(const Date d) { _year = d._year; _month = d._year; _day = d._day; }
传值传参,首先需要生成临时变量再进行传递,而生成临时变量本身就是在调用拷贝构造函数,可是我们正在实现拷贝构造函数,而参数又需要使用拷贝构造函数,这就很矛盾,这就构成了无穷递归问题
,如上半段代码,这样写的拷贝构造函数编译器会直接报错
画图理解传值传参中,自定义类型调用拷贝构造(无穷递归):
因此,既然待拷贝的对象已经存在了,就可以用引用来比避免产生临时变量,再加上const
修饰来保护代拷贝对象,如下半段代码才是正确的拷贝构造函数实现方式
4.3 深浅拷贝
浅拷贝:
按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,但是为什么说涉及到动态空间开辟的一定要自己写默认拷贝构造函数呢?下面来看现象
typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
运行上面这段代发会发现程序崩溃了,这是为什么呢?下面来看图解:
可见在复杂的数据结构中使用浅拷贝是万万不可取的
深拷贝:
深拷贝需要我们自主实现,在面对空间问题时,深拷贝会先开辟一块与待拷贝对象同样大的空间,再将待拷贝对象的数据拷贝到该空间中
结论:
==类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝==
5. 运算符重载
C++
为了增强代码的可读性引入了运算符重载,运算符重载实现了自定义类型之间的符号运算
- 函数名字为:关键字
operator
后面接需要重载的运算符符号- 函数原型:返回值类型
operator
操作符(参数列表)
举个栗子:
class Date { public: Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2023, 4, 13); //内置类型进行运算 int a = 10; int b = 20; bool ab = a == b; //行为合法 //自定义类型进行运算 bool dd = d1 == d2; //行为非法 return 0; }
为了解决上面的自定义类型无法运算的问题,引入了运算符重载
class Date { public: Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } //运算符重载 d1 == d2 bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2023, 4, 13); //自定义类型进行运算 bool dd = d1 == d2; //行为合法法 return 0; }
注意:
- 不能通过连接其他符号(不是运算符的符号)来创建新的操作符:比如operator@
- 重载操作符必须有一个类的类型参数(因为是对自定义类型进行运算)
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- 注意
.*
、::
、sizeof
、? :
、.
这五个操作符不能重载
6. 赋值重载函数
赋值运算符重载的原理就是用到了运算符重载,此时的重载对象为 =
赋值重载的功能:将d1对象
赋值给d2对象
,而非拷贝构造,d1
、d2
均已存在
class Date { public: Date(int year = 2023, int month = 4, int day = 12) { _year = year; _month = month; _day = day; } //赋值运算符重载 Date& operator=(const Date& d) //用引用避免走拷贝构造,减少消耗 { if (this == &d) //相同没必要赋值 { return *this; } _year = d._year; _month = d._month; _day = d._day; return *this; //*this即为赋值完成后的值 } private: int _year; int _month; int _day; };
赋值运算符重载格式:
- 参数类型:
const T&
,传递引用可以提高传参效率 - 返回值类型:
T&
,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值 - 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义,如
d1 = d2 = d3
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
默认赋值重载函数:
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,默认赋值重载函数无论是内置类型还是自定义类型都会处理
不过默认赋值重载函数也是按字节序的浅赋值,在遇到动态内存开辟情况时,需要我们自行编写赋值重载函数,否则就会发生重复析构问题
深度赋值:
这里深度赋值和深拷贝的原理一样,也是将
对象1
的数据拷贝到对象2
的空间中,只不过拷贝构造
是其中一个对象尚未实例化
,赋值重载
是两个对象都是已经存在的
7. const修饰
7.1 概念
被const
为只读权限,当被指向对象为常量或临时变量时,必须使用const
修饰,避免出现权限放大问题
int* pa = 12; //err,12具有常性 const int* pa = (const int*)12; //OK int& ra = 24; //err,24具有常性 const int& ra = 24; //OK
const
常用来修饰引用参数和指针参数,避免此类参数在使用的过程中被修改
7.2 特性
被const
修饰的“成员函数”称之为const
成员函数,const
修饰类成员函数,实际修饰该成员函数隐含的this指针
,表明在该成员函数中不能对类的任何成员进行修改
举个栗子:
class Date { public: Date(int year = 2023, int month = 4, int day = 12) { _year = year; _month = month; _day = day; } void Print() { cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; int _month; int _day; };
在以上的代码中,如果我们在Print()
函数中不小心写出了类似_year = _day
的语句,就会使数据被修改。
我们在实现成员函数时,要确保*this
不被修改,即this
指针指向的内容不被修改
class Date { public: Date(int year = 2023, int month = 4, int day = 12) { _year = year; _month = month; _day = day; } void Print() const //此时this指针的类型为 const Date* this 其指向的内容无法被修改 { cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; int _month; int _day; };
注意:
- const对象不可调用非const成员函数
- 非const对象可调用const成员函数
- const成员函数不可调用其他非const成员函数
- 非const成员函数可调用其他const成员函数
8. 取地址及const取地址重载函数
下面来简单介绍一下最后两个天选之子
取地址及const取地址运算符重载的原理也是用到了运算符重载,此时的重载对象为 &
和const &
取地址重载函数
获取当前对象的地址
class Date { public: Date* operator&() { return nullptr; } private: int _year; int _month; int _day; };
const修饰的取地址重载函数
获取
const
修饰的对象的地址
class Date { public: const Date* operator&()const { return nullptr; } private: int _year; int _month; int _day; };
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。只有特殊情况,才需要重载,比如想让别人获取到指定的内容
C++类和对象(中)到这里就介绍结束了,本篇文章对你由帮助的话,期待大佬们的三连,你们的支持是我最大的动力!
文章有写的不足或是错误的地方,欢迎评论或私信指出,我会在第一时间改正