【C++】C++------类的默认成员函数(构造、析构函数)(上)
前言
在上一期,我们学习了C++的一些基础语法,让我们能简易的写一个C++项目
而今天,我将给大家讲解C++中相当重要的部分------类的默认成员函数
一、类的默认成员函数
简介
所谓默认就是自动生成的
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数
⼀个类,编译器会默认⽣成6个默认成员函数
如图:

今天,我们讲解这六个默认成员函数中最重要的四个
1. 完成初始化工作的构造函数
2. 完成销毁工作的析构函数
3. 用同类对象拷贝创建初始化的拷贝构造函数
4. 将一个对象赋值给另一个对象的赋值重载函数
二、构造函数
构造函数就一句话:应写尽写,人机写的不放心!!!
1.简介
构造函数就是在对象实例化时初始化对象
跟我们以前手搓栈
Stack中写的Init函数的功能差不多就是将成员变量赋上初始值 ,可以完美的替代
Init函数
2.特点
构造函数有很多特点,也稍微有些复杂,这里我就一一用代码来解释
特性1&2
- 构造函数的函数名与类名相同
- 构造函数无需返回值,啥都不需要写
代码演示:
cpp
class Date
{
public:
//构造函数(无参构造)
Date()
{
_a = 1;
_b = 1;
}
private:
int _a;
int _b;
};
如上,在类 Date 中,我们写了一个构造函数,可以看到:
我们构造函数的函数名与类名相同,且没有返回值
特性3
- 构造函数可以函数重载
刚刚我们写的是无参构造,我们还可以对函数进行重载,使用带参构造和全缺省构造
代码演示:
cpp
//带参构造
Date(int a, int b)
{
_a = a;
_b = b;
}
cpp
//全缺省构造
Date(int a = 1, int b = 1)
{
_a = a;
_b = b;
}
如上,我们对函数进行重载,这样在传参时就更加灵活
特性4
- 对象实例化时系统会自动调用对应的构造函数
在对象实例化时,也就是调用时,会自动调用对应的构造函数进行初始化
代码演示:
cpp
class Date
{
public:
//无参构造
Date()
{
_a = 1;
_b = 1;
}
private:
int _a;
int _b;
};
int main()
{
//对象实例化
Date d;
return 0;
}
此时,在 d 对象中,a 和 b已经赋值为1
调用了构造函数
特性5
- 若用户没有显式定义构造函数,则编译器会自动生成⼀个无参的默认构造函数
代码演示:
cpp
class Date
{
//没有显式定义构造函数
private:
int _a;
int _b;
};
int main()
{
//对象实例化
Date d;
return 0;
}
调试结果:

可以看到,若我们没有显式定义构造函数,编译器也会自动生成⼀个无参的默认构造函数,编译不会报错
但是,在调试窗口可见,对象 d 中 a 和 b 的值是一个随机值(不确定,有些编译器是随机值,有些是0)
所以,若我们没有显式定义构造函数,编译器会自动生成⼀个无参的默认构造函数,但是数据具有=不确定性=
所以回到开头,构造函数就一句话:应写尽写,人机写的不放心!!!
特性6
- 不传实参就可以调⽤的构造就叫默认构造
⽆参构造函数、全缺省构造函数、我们不显示写编译器默认⽣成的构造函数,都叫做
默认构造函数
这三个函数有且只有⼀个存在,不能同时存在(⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义)
特性7
- 对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化(稍微了解即可)
我们不显示写编译器默认⽣成的构造函数,对内置类型成员变量的初始化没有要求
而对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化
没有默认构造函数,那么就会报错
我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我将会在以后的学习进行讲解
三、析构函数
1.简介
析构函数与构造函数功能相反,对象在销毁时会自动调用析构函数,完成对象中资源的清理释放⼯作
析构函数就类似于我们之前手搓栈
Stack实现的Destroy功能,用于销毁释放
2.特点
还是一样,下面我用代码来一一解释这些特性
特性1~4
由于前面几点较简单,所以这里简单概括下
- 析构函数名是在类名前加上字符
~- 构造函数和构造函数一样,无需传返回值
- ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数
- 对象⽣命周期结束时,系统会⾃动调⽤析构函数
如下代码就是一个简单的析构函数,当对象⽣命周期结束时,系统会⾃动调⽤析构函数进行清理
代码演示:
cpp
class Date
{
public:
//无参构造
Date()
{
_a = 1;
_b = 1;
}
//析构函数
~Date()
{
_a = 0;
_b = 0;
}
private:
int _a;
int _b;
};
如下代码就是稍微复杂一点的析构函数,是一个栈的类的析构
所以需要用 free 释放空间,还要置空指针等
代码演示:
cpp
class Stack
{
public:
//带参构造
Stack(int* a, int top, int capacity)
{
int* _a;
int _top;
int _capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
特性5
内置类型成员与自定类型成员的析构函数
我们不显示写析构函数,编译器⾃动⽣成的析构函数对内置类型成员不做处理
而⾃定类型成员会调⽤他的析构函数
如下代码就是我们用两个栈类来实现一个队列类的代码
此时编译器默认生成了 MyQueue 的析构和构造且都是调用我们写的 Stack 的成员函数
代码演示:
(内有注释)
cpp
//用两栈实现队列
class MyQueue
{
Stack pushst;
Stack popst;
//此时编译器默认生成了MyQueue的析构和构造
//且都是调用我们写的Stack的成员函数
};
特性6
注意事项
我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构
也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
就例如刚刚的MyQueue类,⽆论什么情况都会⾃动调⽤Stack的析构函数
特性7
写还是不写?
如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如前面的
Date
如果默认⽣成的析构函数可用时,也不需要显⽰写析构,如前面的MyQueue,可以使用Stack的析构
但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack
特性8
析构顺序
⼀个局部域的多个对象,C++规定 后定义的先析构
四、拷贝构造函数
1.简介
拷贝构造顾名思义就是
拷贝 + 构造确实如此,拷贝构造函数就是一个特殊的构造函数,属于构造函数的一个函数重载
2.特点
还是一样,下面我用代码来一一解释这些特性
特性1
拷贝构造函数的第⼀个参数必须是类类型对象的引用
使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值
下面我就直接写一个简易的拷贝构造函数给大家看看
代码演示:
cpp
class Date
{
public:
//带参构造
Date(int year, int month, int day)
{
int _year = year;
int _month = month;
int _day = day;
}
//简易拷贝构造函数
//const使得d的值不会被改变
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
可以看到,拷贝构造传的参数类型是类类型对象的引⽤,而不是我们所认为的传值传参这是为什么?
若使用传值传参,那实参传递给形参是不是也要进行一个拷贝,将实参的值拷贝给形参 ,再进行使用
那么可以想到,在实参的值拷贝给形参时,是不是又调用了拷贝构造函数
如此一来就是个无限循环,一直不停的拷贝拷贝,成为一个无穷递归直到栈溢出,程序崩掉
如下图,有注释:

所以,拷贝构造函数的第⼀个参数必须是类类型对象的引用,不然程序会崩溃
特性2
若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数
自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝)
而自动生成的拷贝构造对对自定义类型成员变量会调⽤他的拷贝构造
在这里大家可能不知道什么是深拷贝,什么是浅拷贝,我来跟大家讲解一下
浅拷贝**:**
⼀个字节⼀个字节的拷贝,将数据的值,数据的资源空间一起拷贝意思是若要拷贝一个动态开辟的栈,不单单是把数值拷贝过来,还把数据的空间地址也拷贝了,此时他们两组数据的字节一模一样,
其中的指针会指向同一个空间,析构会析构两次, push 也会进行两次
深拷贝**:**
深拷贝不仅仅会对数据进行拷贝,还会开辟一个相同大小的空间,将数据存放进去所以,当类中有指针指向了资源,开辟了空间时,此时就要用深拷贝,用
malloc&memcpy进行实现
总结**:**
像Date的类成员全是内置类型且未指向任何资源,编译器自动生成的拷贝构造就可以使用
像Stack的类中_a指向了空间资源,浅拷贝就不符合要求,这时就要我们自己实现深拷贝
小技巧: 如果一个类显示实现了析构函数并释放了资源,那就要显示实现拷贝构造,否则不用
3.调用
拷贝构造函数的调用其实很简单,有两种形式:
Date d2 (d1) ;构造 d2 并将 d1 的值拷贝到 d2 中去
Date d2 = d1 ;构造 d2 并将 d1 的值拷贝到 d2 中去其中,第二种在编译时会自动转换为第一种
五、赋值运算符重载
1.运算符重载
( 1 ) 概念
当运算符被用于类类型的对象时,C++允许我们通过运算符重载的形式指定新的含义
规定类类型对象使⽤运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则编译报错
( 2 ) operator关键字
operator关键字 和后⾯要定义的运算符 共同构成了该运算符重载的名字
例如:operator== 、operator+ . . . . .
( 3 ) 用法
代码演示:
如下就是一个运算符重载,在类中写了一个成员函数
operator==,将运算符==进行重载
当运算符==被用于类类型的对象时,就会自动转换成中间的代码并且之后返回一个bool类型的值
cpp
//运算符== 的重载
bool operator==(Date d2)
{
return _year == d2._year &&
_month == d2._month &&
_day == d2._day;
}
//调用时
if (d1 == d2)
{
printf("d1 == d2\n\n");
}
后面调用时,直接写
d1 == d2即可
第一个对象d1会传给函数一个隐式的this指针,函数通过这个隐式的this指针来找到第一个操作数
而第二个对象d2就直接传给函数的形参d2,然后直接在函数中用d2来调用出第二个操作数
( 4 ) 完整代码演示
如下代码,创建了一个日期类,包含年月日
在类中重载了一个成员函数
operator==,将运算符==进行重载当运算符
==被用于类类型的对象时,就会自动转换成中间的代码并且之后返回一个bool类型的值**之后接收该值来判断两日期是否相等
cpp
#include<iostream>
using namespace std;
class Date
{
public:
//带参构造
Date(int year, int month, int day)
{
int _year = year;
int _month = month;
int _day = day;
}
//运算符== 的重载
bool operator==(Date d2)
{
return _year == d2._year &&
_month == d2._month &&
_day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//对象实例化调用构造函数
Date d1(2007, 10, 23);
Date d2(2007, 8, 29);
//此时运算符== 已经被重载
if (d1 == d2)
{
printf("d1 == d2\n\n");
}
else
{
printf("d1 != d2\n\n");
}
return 0;
}
( 5 ) 不可重载的操作符
在C++中有几个不能进行重载的操作符,记得注意
.*(回调成员函数操作符)
::(域作用限定符)
? :(三目操作符)
.(访问操作符)
( 6 ) d++ 和 ++d 的区别
运算符重载也可只有一个类类型参数
就拿刚刚的日期类举例:
d1 ++ ;表示天数 + 1
但是注意 d++ 和 ++d 在重载时是有区别的
- d++ 的重载:
cpp
//运算符d++ 的重载
Date operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
- ++d 的重载:
cpp
//运算符++d 的重载
Date& Date::operator++()
{
*this += 1;
return *this;
}
2.赋值运算符重载
(1)简介
赋值运算符重载 是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值
这里要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象,而赋值运算符重载是两对象早已初始化,之后进行的拷⻉赋值
(2)代码演示
由于赋值运算符重载跟上面的运算符重载一样,所以不过多解释,直接来看看代码
其实赋值运算符重载本质上就是重载了赋值符
=的重载函数
cpp
// 赋值运算符重载
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
(3)特点
这里还是简单概括一下赋值运算符重载的特点,这里我就不一一讲解了,原理跟之前的运算符重载一样
-
赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数
-
有返回值,建议返回类型为类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景
例:d1 = d2 = d3 ; -
建议形参类型前加上一个
const防止对对象的值进行修改
当未显示实现赋值运算符重载时,编译器会自动生成
但与拷贝构造一致,自动生成的函数仅仅能完成浅拷贝,有时会与实际需求不符
所以,当遇到Stack这样的类时,其中指针a指向了资源,就需要我们自己写一个深拷贝
结语
本期资料来自于:

OK,本期的类的默认成员函数到这里就结束了
若内容对大家有所帮助,可以收藏慢慢看,感谢大家支持
本文有若有不足之处,希望各位兄弟们能给出宝贵的意见。谢谢大家!!!
新人,本期制作不易希望各位兄弟们能动动小手,三连走一走!!!
支持一下(三连必回QwQ)