前面我们讲到了类的成员变量、成员函数,以及怎么计算一个类的大小等。我们知道如果是一个空类,那么在内存空间中会用一个字节来标识这个类。那么空类真的是什么都没有吗?答案是否定的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数。
6个默认成员函数
1、构造函数
首先我们先创建一个日期类
class Date{
public:
void SetDate(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
void Display(){
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;
Date d2;
d1.SetDate(2022,2,7);
d2.SetDate(2021, 1, 6);
return 0;
}
可以看到,我们在用类实例化对象时,并没又给成员变量赋值,而且成员变量都是私有的,在类外无法访问。所以如果我们要给对象的成员变量赋值只有用对象去调用它的SetDate()
函数,这样就做未免有些麻烦,所以就有了构造函数。
构造函数是特殊的成员函数,需要注意的是,构造函数的名字虽然叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
构造函数的特点
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
接下来我们写一个带构造函数的日期类
class Date{
public:
// 无参构造函数
Date(){}
// 带参数的构造函数
Date(int year, int month, int day){ // 构造函数是不需要返回值的,在函数名前面也不需要加void
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用无参构造函数
Date d2(2022, 2, 7); // 调用带参的构造函数
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
return 0;
}
如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成
class Date{
public:
// 无参构造函数
// Date(){}
// 带参数的构造函数
/* Date(int year, int month, int day){ // 构造函数是不需要返回值的,在函数名前面也不需要加void
_year = year;
_month = month;
_day = day;
}*/
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用默认构造函数
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
return 0;
}
这就是为什么我们没写构造函数,依然可以实例化对象的原因,因为编译器会自动生成一个默认构造函数。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
PS:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
// 默认构造函数
class Date{
public:
Date(){
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 这样可以实例化对象吗
Date d1;
return 0;
}
答案是不行,因为这两种构造函数都是默认构造函数,这样去实例化对象的时候就编译器会不知道该调用哪一个构造函数。
C++把类型分为内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:int、char、float…,自定义类型就是我们使用class、struct、union自己定义的类型,在C++中,编译器生成的默认的构造函数,对于基本类型的成员变量不做处理,而对自定义类型的成员变量,它会去调用该成员的构造函数。
class A {
public:
A() {
cout << "我是A的构造函数" << endl;
}
};
// 构造函数
class Date {
public:
// 无参构造函数
Date(){
cout << "我是Date的构造函数" << endl;
}
// 这里我们把成员变量的权限打开
int _year;
int _month;
int _day;
A a;
};
int main() {
Date d1;
cout << "_year =" << d1._year << endl;
cout << "_month =" << d1._month << endl;
cout << "_day =" << d1._day << endl;
return 0;
}
从上面的代码可以看出,构造函数在初始成员变量时,对于基本类型的成员变量是不做处理的,所以基本成员打印出来是乱码,而对与自定义类型的成员变量,在调用构造函数时,会去调用该自定义类型的构造方法来初始化自定义类型的成员变量。
成员变量在命名时,一般在变量名的前面加一个下划线,来表示这是一个成员变量。
2、析构函数
前面通过构造函数的学习,我们知道了一个对象是怎么来的,那么当我们使用完这个对象后,怎么让它消失呢?
析构函数:与构造函数的功能相反,析构函数是完成对象的销毁,局部对象销毁工作是由编译器完成。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数的特点
- 析构函数的函数名是在类名前加上一个字符~
- 无参数、无返回值
- 一个类有且只有一个析构函数,若未显示定义,系统会自动生成默认的析构函数
- 对象声明周期结束时,C++编译系统自动调用析构函数
typedef int DataType;
class SeqList{
public:
// 构造函数
SeqList(int capacity = 10){
_data = (DateType*)malloc(sizeof(DataType) * capacity);
assert(_data);
_size = 0;
_capacity = capacity;
}
// 析构函数
~SeqList(){
if(_data){
free(_data); // 释放掉堆上的空间
_data = NULL;
_capacity = 0;
_size = 0;
}
cout << "我是析构函数" << endl;
}
private:
int* _data;
size_t _size;
size_t _capacity;
};
可以看出来,析构函数是编译器在对象生命周期结束自动调用的函数,同样的,如果我们没有写析构函数,编译器会自动生成一个默认的析构函数,默认的析构函数对自定义类型的成员变量会去调用该成员的析构函数。
4、拷贝构造函数
我们有时候在创建对象时,需要创建一个与对象一模一样的新对象,这就需要用到拷贝构造函数
拷贝构造函数:只有单个形参,该形参时对本类类型对象的引用(一般使用const修饰),再用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数的特征
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数有只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
这里对拷贝函数如果参数是传值的话会引发无穷递归调用做一个解释,我们知道,如果参数是传值传参的话就是将实参拷贝给形参,那么拷贝构造在传值的时候又会调用拷贝构造,所以就会一直递归下去。
// 默认构造函数
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
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); // 会去调用拷贝构造函数
return 0;
}
若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
那么既然编译器能生成默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需去实现拷贝构造函数吗?看看下面的例子:
我们看到在调试运行这段代码的时候报错了,就是在释放空间的时候报错了,这是为什么呢?
经过调试,我们发现s1和s2的地址是一样的,而在对象生命周期结束的时候会自动调用它的析构函数,而编译器自动生成的默认拷贝构造函数是浅拷贝,拷贝得到的对象和原对象地址是一样,所以析构函数中的free释放的是同一块空间,就会报错。
5、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型、函数名字以及参数列表,其返回值类型与参数列表和普通的的函数类似。
函数名为:关键字operator
后面接需要重载的运算符号,比如:加法的重载,函数名就是operator+
函数原型:返回值类型 operator操作符(参数列表)
PS:
- 不能通过连接其他符号类创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能变成乘法这样的
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参。
.*
::
sizeof
?:
.
以上5个运算符不能重载。切记切记
在本文中,我们以Date类来实现运算符的重载
class Date {
private:
int _year;
int _month;
int _day;
public:
// 缺省行构造函数
Date(int year = 1, int month = 1, int day = 1);
// 实现拷贝构造(const通吃)
Date(const Date& d);
// 析构函数
~Date();
// 重载 == 运算符
bool operator==(Date d){
// ==符号,是只要年月日都相等,就相等
return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
// 重载 + 运算符
Date operator+(int day) {
// 加法需要使用传值返回
Date ret = *this;
if (day < 0) {
day *= -1;
ret -= day;
return ret;
}
ret._day += day;
while (ret._day > ret.GetMonthDay(ret._year, ret._month)) {
if (ret._day > GetMonthDay(ret._year, ret._month)) {
ret._day = ret._day - GetMonthDay(ret._year, ret._month);
++(ret._month);
if (ret._month > 12) {
++(ret._year);
ret._month = 1;
}
}
else {
ret._day += day;
}
}
return ret;
}
private:
// 获取某年某月的天数
int GetMonthDay(int year, int month) {
int monthDay[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int i = 0;
// 判断是否闰年
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
if (month == 2) {
return monthDay[month] + 1;
}
else {
return monthDay[month];
}
}
else {
return monthDay[month];
}
}
};
赋值运算符重载
class Date {
private:
int _year;
int _month;
int _day;
public:
// 缺省行构造函数
Date(int year = 1, int month = 1, int day = 1);
// 实现拷贝构造(const通吃)
Date(const Date& d);
// 析构函数
~Date();
// 重载 = 运算符
Date& operator=(const Date d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
};
赋值运算符主要有四点:
- 参数类型是const修饰的
- 返回值是一个引用
- 检测是否是自己给自己赋值
- 返回*this
- 一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
6、const成员
将const
修饰的类成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
7、取地址及const取地址操作符重载
class Date{
public:
Date* operator&(){
return this;
}
const Date* operator&()const{
return this;
}
private:
int _year;
int _month;
int _day;
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况才要重载。