前言
首先,我们知道C++中有空类(Empty Class),空类是指在C++中没有任何成员变量和成员函数的类。也就是说,空类不包含任何数据成员和成员函数。
而实际上并非如此。
C++默认成员函数是在没有显式定义的情况下由编译器自动生成的成员函数。C++中有6个默认成员函数,也就是说,一个空类仍含有这6个默认成员函数。
如下:
下面介绍这6种默认成员函数
一. 默认构造函数
默认构造函数(Default Constructor)是C++中的一种特殊的成员函数,如果在一个类中没有显式定义任何构造函数,编译器就会自动为该类生成一个默认的无参数构造函数。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数,并且只能有一个。
➡️默认构造函数的主要作用是完成对象的默认初始化,即给对象的成员变量赋予默认值。
它的特征如下:
1. 无参数,无返回值,函数名与类名相同:默认构造函数不接受任何参数。
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用无参构造函数
Date d2(2024,3,26); // 调用带参构造函数
Date d3; //警告:C4930“Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
return 0;
}
2.自动生成:如果类中没有显式定义任何构造函数,编译器就会自动生成一个默认构造函数。
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
Date d;
return 0;
}
3. 初始化:默认构造函数会调用每个非静态数据成员的默认构造函数来初始化对象。
4. 可重载:默认构造函数可以被其他构造函数重载,也可以被用户显式定义。
二. 析构函数
析构函数(Destructor)是C++中的一种特殊的成员函数,它在对象的生命周期结束时自动被调用,用于执行必要的清理工作,如释放对象在运行时获取的所有资源。
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,而析构函数就是针对自定义类型调用的函数
➡️它与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
它的特征如下:
1. 无参数,无返回值,函数名与类名相同
2. 析构函数名是在类名前加上字符 “ ~ ”
// 如下
class Myclass
{
public:
//...
~Myclass() // 析构函数
{
//...
}
};
3. 隐式调用:只能在对象生命周期结束时自动被调用
4. 不可重载:一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。这意味着如果你显式定义了析构函数,你需要自己编写析构函数来实现这些功能。
// 顺序栈
class Stack
{
public:
// ...
// 显式定义析构函数
~Stack()
{
if (array)
{
free(array);
array = NULL;
capacity = 0;
size = 0;
}
}
private:
int* array;
int capacity;
int size;
};
三. 拷贝构造函数
拷贝构造函数(copy constructor)是C++中一个特殊的构造函数,它接受一个与类同类型的实参(通常用 const 修饰,以防止不小心修改原对象),并使用这个实参来初始化新创建的对象。
C++规定自定义的类型都会调用拷贝构造函数。
它的特征如下:
1. 无参数,无返回值,函数名与类名相同
2. 参数是同类型的引用 :拷贝构造函数接受一个同类型对象的引用作为参数。通常,这个参数是一个常量引用,以避免对原对象的修改。
class Myclass
{
public:
//...
Myclass(); // 默认构造
Myclass(const Myclass& d); // 常量引用拷贝构造
};
如果我们不使用引用传参,则会引发无穷递归调用,如下图所示:
3. 隐式调用:如果程序员没有显式定义拷贝构造函数,编译器会自动为类生成一个默认的拷贝构造函数。
4. 浅拷贝与深拷贝:默认的拷贝构造函数执行的是浅拷贝,即只拷贝对象的数据成员。如果一个类包含指针成员,并且需要拷贝指针所指向的数据,就需要自定义拷贝构造函数来实现深拷贝。
四. 赋值操作符重载函数
重载运算符本质上是函数,其名字由 operator 关键字后接表示要定义的运算符的符号组成。
类似于其他函数,它也有一个返回类型和一个参数列表。
函数原型:
有几点需要注意:
- 不能通过连接其他符号来创建新的操作符:比如就没有 @ 这样的操作符,所以不能写成operator@
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型 “+”,不 能改变其含义。
- 重载操作符针对的是自定义类型
- 作为类成员的重载函数时,其形参看起来比操作数数目少一个成员,因为有一个隐藏的 this 指针
- .*,::,sizeof,?:,。这五个运算符不能重载。
分为两个,一个是赋值运算符重载,另一个是运算符重载。
1. 运算符重载
如下,我们定义一个整形变量 i,对他进行一些运算符操作,比如加减乘除,这一点是大家很熟悉的,并且编译也直到该怎么做。
int main()
{
int i = 1;
// 编译器知道这样做的意义
i += 1;
i -= 1;
return 0;
}
但是,对于自定义类型就不是这样了。比如用一个日期类,去实例化一个对象,如果你拿上这个对象进行上述来操作,编译器就不知道它的意义何在。
int main()
{
Date d;
// 编译器不知道这里是什么意思,或许是加一天?加一个月?还是其他意思
d += 1;
d -= 1;
return 0;
}
于是引入了运算符重载。
运算符重载(Operator Overloading)是C++中一种特性,允许您重新定义或扩展原有的操作符的功能。通过运算符重载,您可以为自定义的类或类型定义特定的操作符行为,使其能够像内置类型一样进行操作。
➡️在C++中,您可以重载大部分的运算符,例如算术运算符(+、-、*、/)、关系运算符 (==、!=、<、>)、赋值运算符(=)、递增递减运算符(++、--)等。
例如,可以重载 == 运算符,以便在两个对象之间进行关系运算:
class MyNumber {
public:
MyNumber(int value = 5) {
_value = value;
}
// 重载 == 运算符
bool operator==(const MyNumber& other) {
return _value == other._value; // 相等返回true,否则返回false。
}
void print() {
cout << "Value: " << _value << endl;
}
private:
int _value;
};
2. 赋值运算符重载
赋值运算符重载是一种特殊的运算符重载,它允许我们为自定义的类型定义赋值操作的行为。在C++中,赋值运算符是 = 。
class func{
public:
func& operator=(const func&); //赋值运算符
// ...
};
下面是一个简单的示例:
class MyNumber {
public:
MyNumber(int value = 5){
_value = value;
}
// 重载赋值运算符
MyNumber& operator=(const MyNumber& d) {
if (*this != d) {
_value = d._value;
}
return *this;
}
void print() {
cout << "Value: " << _value << endl;
}
private:
int _value;
};
注意:
- 如果用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
- 赋值运算符只能重载成类的成员函数不能重载成全局函数,如下:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right) {
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
这是因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
五. 普通对象取地址重载函数
普通对象取地址重载函数是指通过重载operator&运算符,实现对普通对象的取地址操作。重载后的operator&运算符可以返回一个指向对象的指针,或者是一个包含对象地址的自定义类对象。
例如,我们可以这样重载operator&运算符:
class Date
{
public:
Date* operator&() {
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date p;
Date* ptr = &p; //调用重载的operator&运算符
return 0;
}
这个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
六. const 修饰的对象据地址重载函数
class Date
{
public:
const Date* operator&()const {
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date p;
Date* ptr = &p; //调用重载的operator&运算符
return 0;
}
和普通对象的一样,这个运算符一般也不需要重载,使用编译器生成的默认取地址的重载即可。