一:类的六大默认成员函数
- 上一次我们认识了如何创建一个类,这次我们再来看看类里面还有些什么
- 只要我们创建了一个类,即使我们不编写,编译器也会代替我们生成六个函数,所以我们把这六个函数称为类的六大默认成员函数
- 构造函数:用于初始化对象
- 析构函数:用于清理对象(比如delete释放申请的空间)
- 拷贝构造函数:用同类对象来初始化对象
- 赋值操作符重载:将一个对象赋值给另一个对象
- 取地址操作符重载:取得对象的地址
- const对象的取地址操作符重载:取得const对象的地址
二:构造函数
- 我们之前在使用C语言创建一个结构体的时候,比如链表,想要初始化它都需要写一个初始化函数,像这样:
- 这样每次我们创建一个对象就要调用一次初始化函数,好麻烦啊,能不能两步并一步呢?
- 当然可以,构造函数应运而生,它能够在创建一个对象的时候自动就把它初始化了
- 它有以下几个特点
- 函数名与类名相同
- 无返回值(不是返回void,是直接不写返回值类型)
- 对象实例化的时候编译器自动调用对应的构造函数
- 构造函数可以重载(保证了多种初始化方式,拷贝构造其实就是构造函数的一个重载)
- 光说不练假把式,来看看它和DateInit函数有什么区别
- 当我们构造了一个无参或者全缺省的构造函数(上图就是全缺省的)时,我们不传参数时是不需要加()的,会被编译器当成一个函数的声明
- 构造函数可以有很多,但是推荐使用全缺省的,不传参的时候可以用默认参数,想要指定数值也可以传参
- 之前说过,我们不写构造函数,编译器会自动生成一个,那么我们为什么要写呢?
- 因为编译器自动生成的构造函数对内置类型什么都不做
- 可以看到d1中的值还是随机值,但是它又不是完全没做事,对于内置类型,它会调用它的构造函数
- 可以看到Date类中有个我们自己创建的类型Tmp,我们用Date类实例化一个对象时Tmp中的a被修改成1了,说明编译器自动生成的构造函数会调用自定义类型的构造函数
- 还有一点,可以看到图中所有的成员变量前面都对了个_,这是为了在初始化时便于区分
- 我们可以不在成员变量前加_或者其它符号做区分,但不推荐
- 最后一点,如何理解默认构造函数?
- 我常误以为编译器自动生成的就是默认构造函数,这么说没错,但是不全面
- 默认构造函数其实有三种
- 我们不写,编译器自动生成的
- 我们自己写的无参构造函数
- 我们自己写的全缺省的构造函数
三:析构函数
- 和构造函数设计的初衷相似,我们之前经常使用完一个自定义的结构体后,还需要调用destroy函数
- 而有了析构函数,它能完成和destroy函数相同的功能,但是我们不需要自己去显示的调用它,当对象被销毁时编译器会自动调用析构函数
- 它有如下的特点:
- 函数名为类名前加上~
- 无参数无返回值
- 一个类有且只有一个析构函数(无法重载,没参数当然无法重载啦)
- 对象生命周期结束时,C++会自动调用析构函数
- 可以看到,我们没有显示调用析构函数,编译器还是调用了,我们打印一下证明它被调用了
- 编译器默认生成的析构函数和构造函数相同,对内置类型不做处理,对自定义类型会调用其析构函数
四:拷贝构造函数
- 拷贝构造函数其实就是构造函数的一种重载,它的参数是类类型的引用
- 特点:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
- 我们来看看拷贝构造函数的实现
- 如果不传引用,那么会发生无限循环中,因为这时函数传参时就要调用拷贝构造了
- 这个函数通常情况下我们是可以不写的,它和构造和析构不同,编译器自动生成的拷贝构造它不区别对待了,对于内置类型它也会干事了
- 对内置类型它会直接拷贝,对于自定义类型调用它的拷贝构造函数
- 可以看到我们没有写拷贝构造函数,但是还是完成了拷贝构造,这就是使用的编译器默认生成的拷贝构造
- 那么编译器生成的够用了,我们还要自己写吗?
- 答案是大多数情况是不用的,但是对于某些类我们直接使用类自己生成的拷贝构造会出问题
- 拷贝构造s2时是值拷贝,说明s2的_a和s1的_a指向的是同一块空间,之后调用析构函数时_a指向的空间会被free两次,就出问题了
- 所以,有需要的时候我们还是需要自己写拷贝构造函数
五:赋值操作符的重载
- 在说赋值操作符的重载之前,我们需要先了解一下运算符重载
- 运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名为:关键字operator后面接需要重载的运算符符号。
- 函数原型:返回值类型 operator操作符(参数列表)
- 注意:
- 作为类成员的重载函数时,其形参看起来比操作数数目少1,第一个形参为this指针,被编译器限定,我们虽然看不到它,但是实际存在
-
- 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载
- 运算符重载和函数重载虽然都使用了重载一词,但无关联。函数重载是为了支持定义同名函数,运算符重载是为了让自定义类型可以像内置类型一样使用
- 前置知识就说到这里,现在来举个例子,比如我们都知道两个类无法直接用==比较是否相等
- 但是现在我们重载这个==操作符就可以实现
- 我们使用的时候只用使用==就可以了
- 实际上编译器是这么调用的
operator==(&d1,d2)
- 函数真正其实长这样
bool operator==(const Date*const this,const Date&d)
- 言归正传,现在来讲讲赋值操作符的重载
- 赋值操作符的重载也是实现的拷贝行为,但是和拷贝构造不同的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝
- 而赋值操作符的拷贝是对象已经存在,初始化完成了,想把一个对象的值赋值给另一个对象
- 以日期类举例,函数原型大概长这样
Date& operator=(Date*const this,const Date&d)
- 返回类型为Date&是为了支持一行连续赋值,this之前没加const是因为变量要被修改了,不能加const
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
- 注意:
- 返回类型为类类型的引用
- 检查是否给自己赋值
- 返回*this
- 与拷贝构造函数同理,我们不写编译器也会自动生成,并且对内置类型直接拷贝,对自定义类型调用其自己的赋值运算符重载
- 可以看到我们没有重载=但是还是完成了拷贝
- 与拷贝构造一样,我们大多数时候不用自己写,但是遇到如stack这样的类,我们还是需要自己手动重载=的
六:const成员函数
- 将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
void Print() const
->void Print(const Date* const this)
- 相当于const修饰*this,this指向的对象不能被修改
- const对象和非const对象都能调用const成员函数
- const对象无法调用非const成员函数
七: 取地址和const取地址操作符的重载
- 这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
- 说白了几乎不需要我们自己写,去掉这俩把默认成员函数当4个我觉得问题也不大
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}