C++类和对象(二)—— 类的6个默认成员函数及日期类的实现

类的6个默认成员函数

如果一个类什么构造都没有,简称空类,这个空类会被系统产生6个默认成员函数

这里的“默认”和“缺省”的意思差不多,也就是你不写这6个函数,编译器会自动生成,你若是写了任意一个默认成员函数,则编译器就不生成了。

构造函数

构造函数:名字和类名一样,对象实例化的时候,编译器自动调用,保证每个对象都能获取数据,它的生命周期只有一次

例如,以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)// 构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
这个构造会对成员变量中的_year,_month,_day进行初始化,当写出构造函数以后,系统不会在默认生成了,构造函数的主要任务并不是开空间创建对象,而是初始化对象。

构造函数的特性

一、构造函数的函数名与类名相同

二、构造函数无返回值

三、对象实例化时编译器自动调用对应的构造函数

四、构造函数支持重载(析构不行,因为不能析构两次)

五、无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个

六、如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成

#include <iostream>
using namespace std;
class Date
{
public:
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1; // 编译器将调用自动生成的默认构造函数对d1进行初始化
    d1.Print();
    return 0;
}
代码最终的打印结果:

编译器自动生成的构造函数机制:

1、编译器自动生成的构造函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认构造函数。

析构函数

析构函数:与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

析构的优势就是在第一次构建类的时候就保证了这段类一定会被析构掉,防止了我们像以前c语言一样malloc以后总是忘记free掉申请的空间,造成空间浪费

析构函数的特性

class Date
{
public:
    Date()// 构造函数
    {}
    ~Date()// 析构函数
    {}
private:
    int _year;
    int _month;
    int _day;
};


二、析构函数无参数,无返回值

析构函数所谓的无返回值也是真的无返回值,而不是返回值为void。

三、对象生命周期结束时,C++编译器会自动调用析构函数 

这就大大降低了C语言中栈空间忘记释放问题的发生,因为当栈对象生命周期结束时,C++编译器会自动调用析构函数对其栈空间进行释放。

四、一个类有且只有一个析构函数。

若未显示定义系统会自动生成默认的析构函数
编译器自动生成的析构函数机制:

1、编译器自动生成的析构函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。

五、先构造的后析构,后构造的先析构

因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则。

  拷贝构造函数

拷贝构造函数的概念

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用从const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 0, 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(2021, 5, 31);
    Date d2(d1); // 用已存在的对象d1创建对象d2

    return 0;
}

拷贝构造函数的特性


一、拷贝构造函数是构造函数的一个重载形式

 因为拷贝构造函数的函数名也与类名相同。

二、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用

要调用拷贝构造函数就需要先传参,若传参使用传值传参,那么在传参过程中又需要进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用。

拷贝构造又分浅拷贝和深拷贝

浅拷贝实际上就是将d1的内容完完全全的复制了一份拷贝给d2,所以说浅拷贝也叫做值拷贝。

Date d2(d1);// 用已存在的对象d1创建对象d2

如果需要开空间的类进行拷贝的话就必须要深拷贝,系统不会生成深拷贝,所以需要我们自己写一个

Stack s1;
Stack s2(s1);// 用已存在的对象s1创建对象s2
代码中,我们的本意是用已存在的对象s1创建对象s2,但编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝出来的对象s2将不能满足我们的要求。
举个例子,现有以下栈(Stack)类:

class Stack
{
public:
    Stack(int capacity = 4)
    {
        _ps = (int*)malloc(sizeof(int)* capacity);
        _size = 0;
        _capacity = capacity;
    }
    void Print()
    {
        cout << _ps << endl;// 打印栈空间地址
    }
private:
    int* _ps;
    int _size;
    int _capacity;
};
我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。看看以下代码运行结果:

int main()
{
    Stack s1;
    s1.Print();// 打印s1栈空间的地址
    Stack s2(s1);// 用已存在的对象s1创建对象s2
    s2.Print();// 打印s2栈空间的地址
    return 0;
}
结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈。

赋值运算符重载

运算符重载

 C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。

d1 == d2;// 可读性高(书写简单)
IsSame(d1, d2);// 可读性差(书写麻烦)

运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
 运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表)

1.不能通过连接其他符号来创建新的操作符:比如operator@。
 2.重载操作符必须有一个类类型或枚举类型的操作数。
 3.用于内置类型的操作符,重载后其含义不能改变。
 4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
 5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。这里以重载 == 运算符作为例子:

 我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
    bool operator==(const Date& d)// 运算符重载函数
    {
        return _year == d._year
            &&_month == d._month
            &&_day == d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};
我们也可以将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
    int _year;
    int _month;
    int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数
{
    return d1._year == d2._year
        &&d1._month == d2._month
        &&d1._day == d2._day;
}

赋值运算符重载

这里以重载 = 运算符作为例子:

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)// 构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date& operator=(const Date& d)// 赋值运算符重载函数,隐藏了一个this指针 {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
    void Print()// 打印函数
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

重载赋值运算符需要注意以下几点:
一、参数类型设置为引用,并用const进行修饰
 赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。
 由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。
 其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。
二、函数的返回值使用引用返回
 实际上,我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。
 和使用引用传参的道理一样,为了避免不必要的拷贝,我们最好还是使用引用返回,因为此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回。
三、赋值前检查是否是给自己赋值
 若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。
四、引用返回的是*this
 赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。
五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
 没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
 对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是得自己动手写赋值运算符函数的。

    Date d1(2021, 6, 1);
    Date d2(d1);
    Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?
 其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
 拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
 赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

const成员

const修饰类的成员函数
 我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

    void Print()const// cosnt修饰的打印函数
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
 

思考下面几个问题(经典面试题)
1.const对象可以调用非const成员函数吗?
2.非const对象可以调用const成员函数吗?
3.const成员函数内可以调用其他的非const成员函数吗?
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗?

答案是:不可以、可以、不可以、可以

解释:权限可以放大,但是不能缩小

取地址及const取地址操作符重载

取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了:

class Date
{
public:
    Date* operator&()// 取地址操作符重载
    {
        return this;
    }
    const Date* operator&()const// const取地址操作符重载
    {
        return this;
    }
private:
    int _year;
    int _month;
    int _day;
};
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值