如果一个类中什么成员都没有,则简称为空类,但空类中并非什么都没有,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
class Date
{
};
在开始之前先做一个补充:C++把类型分为两类:内置类型(基本类型)和自定义类型。
内置类型:int / char / double / 指针 / 内置类型数组 等等。
自定义类型:struct / class定义的类型。
一、构造函数(特殊的成员函数)
1.主要任务:在创建对象时完成初始化工作。
2.特征:
1)函数名与类名相同。
2)无返回值。
3)对象实例化时编译器自动调用对应的构造函数。
4)构造函数可以重载。
形式:
类名(参数列表)
{
// ...
}
比如:
Date (int year = 2022, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
默认构造函数有三种:全缺省的构造函数、无参的构造函数、用户不写编译器自动生成的构造函数。(注意:必须传参的构造函数并不是默认构造函数)
1)对内置类型的成员变量不做初始化处理。
2)对自定义类型的成员变量会去调用它的默认构造函数初始化。
用户不写编译器自动生成的构造函数:
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的构造函数。一旦用户显式定义,编译器将不再生成。
// 像这两种类,我们就需要自己实现构造函数(对应第1小点)
class Date
{
int _year;
int _month;
int _day;
};
class Stack
{
int* _pData;
int _top;
int _capacity;
};
// 像这种类,我们不需要自己实现构造函数,默认生成的就可以(对应第2小点)
class MyQueue
{
Stack _pushST;
Stack _popST;
};
二、析构函数(特殊的成员函数)
1.主要任务:与构造函数相对应的,在对象被销毁时,完成对象的一些资源清理工作。
2.特征:
1)析构函数名是在类名前加~。
2)无参数无返回值。
3)一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4)对象的生命周期结束时,C++编译系统自动调用析构函数。
形式:
~类名()
{
// ...
}
比如:
~Stack()
{
if (_pData)
{
free(_pData); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_top = 0;
_capacity = 0;
}
}
默认生成的析构函数:
1)对内置类型的成员变量不做处理。
2)对自定义类型的成员变量会去调用它的析构函数。
// 像这种类,没有资源需要清理,不用自己实现析构函数
class Date
{
int _year;
int _month;
int _day;
};
// 像这种类,有资源需要清理,需要自己实现析构函数(对应第1小点)
class Stack
{
int* _pData;
int _top;
int _capacity;
};
// 像这种类,我们不需要自己实现析构函数,默认生成的就可以(对应第2小点)
class MyQueue
{
Stack _pushST;
Stack _popST;
};
三、拷贝构造函数(特殊的成员函数)
1.主要任务:使用已存在的同类对象来初始化创建对象。
2.特征:
1)是构造函数的一个重载形式。
2)参数只有一个且必须使用引用(常用const修饰)。
3)若未显示定义,系统会自动生成一个默认的拷贝构造函数。
形式:
类名(const 类名& 对象)
{
// ...
}
比如:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
默认生成的拷贝构造函数:
1)对内置类型的成员变量,会完成按字节序的值拷贝(浅拷贝)。
2)对自定义类型的成员变量,会调用它的拷贝构造函数。
问:为什么我们必须使用引用,而不是使用传值的方式呢?
答:在使用传值的方式下,当调用拷贝构造函数时,需要先传参数,而形参是实参的一份拷贝,在拷贝形参的时候又会出现拷贝构造,从而引发了无穷递归调用。因此,我们必须使用引用,而非传值。
// 像这种类,默认生成的拷贝构造函数完成浅拷贝就可以满足需求(对应第1小点)
class Date
{
int _year;
int _month;
int _day;
};
// 像这种类,因为默认生成的浅拷贝不能满足需求,所以需要我们自己实现深拷贝的拷贝构造函数
class Stack
{
int* _pData;
int _top;
int _capacity;
};
// 像这种类,我们不需要自己实现拷贝构造函数,默认生成的就可以(对应第2小点)
class MyQueue
{
Stack _pushST;
Stack _popST;
};
四、赋值运算符重载函数
C++为了增强代码的可读性引入了运算符重载,而赋值运算符重载就是运算符重载中的一种。
问:赋值运算符重载如何增强代码的可读性呢?
答:比如我们把一个对象赋值给另一个对象时,如果不写赋值运算符重载函数的话就要用一个普通函数来实现我们的赋值,而且这个函数在调用时的可读性并没有 “ = ” 高,所以实现赋值运算符重载函数是非常有必要的。
1.主要任务:两个已经存在的同类对象之间进行赋值拷贝。(把一个对象赋值给另一个对象)
2.特征:
1)函数名是 operator=,参数类型是引用(常用const修饰)。
2)函数返回值是引用(因为在内置类型中是可以连续赋值的,比如:i = j = k,所以最好沿用这个习惯)。
3)返回 *this。
4)若未显式定义,编译器会自动生成一个默认的赋值运算符重载函数。
形式:
类名& operator=(const 类名& 对象)
{
// ...
}
比如:
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
默认生成的赋值运算符重载函数:
1)对内置类型的成员变量,会完成按字节序的值拷贝(浅拷贝)。
2)对自定义类型的成员变量,会调用它的赋值运算符重载函数。
// 像这种类,默认生成的赋值重载函数完成浅拷贝就可以满足需求(对应第1小点)
class Date
{
int _year;
int _month;
int _day;
};
// 像这种类,因为默认生成的浅拷贝不能满足需求,所以需要我们自己实现深拷贝的赋值重载函数
class Stack
{
int* _pData;
int _top;
int _capacity;
};
// 像这种类,我们不需要自己实现赋值重载函数,默认生成的就可以(对应第2小点)
class MyQueue
{
Stack _pushST;
Stack _popST;
};
区分拷贝构造和赋值拷贝:
int main()
{
// 拷贝构造是用一个已经存在的对象去初始化一个马上创建实例化的对象
Date d4(d1); // 拷贝构造
Date d5 = d1; // 拷贝构造
// 赋值拷贝是两个已经存在的对象之间进行赋值拷贝
d2 = d1 = d3; // 赋值拷贝
d4 = d5; // 赋值拷贝
return 0;
}
五、取地址操作符重载 和 const修饰的取地址操作符重载
1.主要任务:都是取出对象的 this指针,即对象的地址。
2.特征:
1)若未显式定义,编译器会自动生成。
形式:
// 取地址操作符重载
类名* operator&()
{
return this;
}
// const修饰的取地址操作符重载
const 类名* operator&()const
{
return this;
}
比如:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址重载即可,只有特殊情况才需要重载,比如想让别人获取指定的内容!
总结一下:
1.构造函数主要完成初始化工作。
2.析构函数主要完成资源清理工作。
3.拷贝构造函数是使用同类对象初始化创建对象。
4.赋值运算符重载函数是把一个对象赋值给另一个对象。
5.取地址操作符重载 和 const修饰的取地址操作符重载 分别对普通对象和 const对象 取地址,这两个很少会要求用户显式定义,一般用编译器生成的。
相似性:
构造函数和析构函数的处理机制基本类似,
拷贝构造函数和赋值运算符重载函数的处理机制基本类似。