class Date
{
};
可以看到,上面那个类没有任何成员,是一个空类,但是它真的什么都没有吗?其实一个类在我们不写的情况下,都会生成6个默认的成员函数
分别是构造函数,析构函数,拷贝构造函数,赋值运算符重载,取地址运算符重载,对const对象取地址运算符的重载
构造函数
构造函数是特殊的成员函数,它的主要功能就是初始化对象。和我们之前c语言中自己实现的init函数类似。但是有一点不同的是,init是在我们创建完后才自己调用,而构造函数是创建对象的时候由编译器自动调用,并且在生命周期中只调用这一次。
特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
因为构造函数可以重载,所以我们可以根据不同的需求和场景,创建多个构造函数。
同时,如果我们没有显式创建构造函数,编译器会自动构建一个,但如果我们已经显式定义了,则编译器不会再生成。
显示定义后则编译器不会再默认生成。
class Date
{
public:
//无参构造函数
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
//有参构造函数
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 d3();
//如果要调用无参的后面不能加上括号,加上了则变成了函数声明
Date d2(2020, 4, 19);
//调用有参数的
return 0;
}
可以使用一个有参的和一个无参的构造函数
也可以将两者合起来,写一个全缺省的构造函数
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2020, 4, 19);
return 0;
}
但是使用全缺省就不能再使用无参的,因为编译器会无法识别此时到底该调用哪一个。
然后还有一个令人好奇的点,因为我们不实现构造函数的时候编译器会代替我们实现,但是对于上面那个类,那个构造函数并没有做什么事,那些成员变量还是处于没有初始化的状态,那默认的构造函数有什么用呢?
这也就是默认构造函数被称为双标的原因
其实默认构造函数并不是什么都没有做
class Date
{
int _year;
int _month;
int _day;
A a1;
};
假设成员变量中有一个类A
当调用默认构造函数的时候,我们会发现Date的默认构造函数会去调用A的构造函数,来帮助a1初始化,所以我们可以发现,对于内置的类型,默认构造函数并不会帮助它初始化,而自定义类型则会。
析构函数
析构函数也是一个特殊的成员函数,它的功能时清理对象,和之前一些自定义类型所写的destroy函数类似
特征:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
class A
{
A(char* str = "hello world", int num = 0)
{
_str = (char*)malloc(sizeof(str));
strcpy(_str, str);
_num = num;
}
~A()
{
free(_str);
_str = nullptr;
_num = 0;
}
char* _str;
int _num;
};
同时,默认的析构函数和默认的构造函数效果一样,会去调用自定义类型的析构函数,而不会处理内置类型。
拷贝构造函数
C++也为我们准备了两种能够通过其他对象的值来初始化一个对象的默认成员函数,他们分别是拷贝构造函数和赋值运算符重载
拷贝构造函数
拷贝构造函数是构造函数的一个重载形式。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
调用方法
int main()
{
Date d1;
Date d2 = d1;
Date d3(d1);
//这两种等价,都是拷贝构造函数,并且d2不是赋值运算符重载
}
这是刚刚那个日期类的拷贝构造函数,即用别的对象来为本对象赋值,同时因为不需要修改引用的对象则为它加上const属性。
注意:拷贝构造函数的参数必须是引用,否则会引发无限递归调用
当我们要用d1来初始化d2的时候,需要将d1先传递给形参d,再用形参d进行赋值,但是d1传递给d的时候又会再次引发一个拷贝构造函数,这个d又会给它的拷贝构造函数的形参d传参,这又会引发新的,就导致了一个无限循环,所以要加上引用
如果我们不去定义一个拷贝构造函数,编译器也会默认创建一个,并且对于这个类,他们所实现的功能是一模一样的。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。 如果是上面这个日期类,当然没问题,但如果涉及到了动态开辟的数据,就会有问题了。
假设在堆上开辟了某个大小的一个数据,默认的拷贝构造函数会按照字节序来拷贝这个数据,这一步其实没问题,问题就出在析构函数上。因为析构函数会类的生命周期结束后将类中所有成员变量释放,但是这种浅拷贝的数据就会存在问题,因为他们指向的是同一块空间,而原对象和拷贝的对象会分别对它释放一次,就导致了重复释放,引发错误。
所以对于动态开辟的数据,我们还需要使用深拷贝。
运算符重载
前几章我写过了函数重载,在C++中,还支持对自定义的类类型或者枚举类型的运算符的重载。
函数原型:返回值类型 operator操作符(参数列表)
例如两个日期类是否相等
//成员函数的操作符重载
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
//如果写成普通的函数
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main
{
bool isSame = (d1 == d2)
//调用时等价于 isSame = d1.operator==(d2);
//或者 isSame = operator==(d1, d2);
}
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
- * 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
赋值运算符的重载
上面说过,有两种方法能够实现用其他类来拷贝一个类,一个是拷贝构造函数,一个是赋值运算符
int main()
{
Date d;
Date d1(d);
Date d2 = d;
//拷贝构造函数
d1 = d2;
//这才是赋值运算符重载,因为之前的都是在声明的时候初始化,
//都自动调用了拷贝构造函数,只有不是声明阶段的=才是赋值运算符重载
return 0;
}
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
这里简单的实现了一个,需要注意的有几点
- 返回*this,因为可能存在连续赋值
- 检测是否是自己给自己赋值,如果是则忽略
- 因为不需要修改任何参数,所以参数都需要加上const,并且为了不花费多余的空间去拷贝数据,都采取引用
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
如果我们没有实现的话,编译器也会默认生成一个,但是这个默认的赋值重载运算符和上面的拷贝构造函数存在着同样的问题,它们都是浅拷贝。
默认的取地址运算符
取地址运算符也有两个默认的成员函数,编译器默认生成,不需要我们定义,一般只有想让别人获取指定内容的时候才自己定义一个。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//默认取地址重载
Date* operator&()
{
return this;
}
//const取地址重载
const Date* operator&()const
{
return this;
}
int _year;
int _month;
int _day;
};