类与对象中的六大默认成员函数万字详解

目录

1、类的6个默认成员函数

2、构造函数

2.1、概念

2.2、特性

3、析构函数

3.1、概念

3.2、特性

4、拷贝构造函数

4.1、概念

4.2、特性

5、赋值运算符重载

5.1、运算符重载

5.2、赋值运算符重载

5.3、前置++和后置++重载


1、类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
这6个默认成员函数的作用:
1.初始化和清理
   构造函数 主要完成初始化的工作。
   析构函数 主要完成清理工作。
2.拷贝赋值
   拷贝构造是使用同类对象初始化创建对象。
   赋值重载主要是把一个对象赋值给另一个对象。
3.取地址重载
   主要是普通对象和const对象取地址,这两个很少会自己实现。

类与对象中的六大默认成员函数万字详解_第1张图片

2、构造函数

2.1、概念

对于以下Date类:

class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};
void Test1()
{
    Date d1;
    d1.Init(2022, 7, 5);
    d1.Print();
    Date d2;
    d2.Init(2022, 7, 6);
    d2.Print();
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,(就是有时候可能会忘记初始化,然后就会出现一堆麻烦)
那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2、特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:(前四个特性属于语法定义)

1.函数名与类名相同。

2.无返回值。-- 这里的无返回值不是void的意思,而是压根就是不需要,就是没有。

3.对象实例化时编译器自动调用对应的构造函数。-- 一般情况下不能去显示调用

4.构造函数可以重载

ex:

class Date2
{
public:
    // 1.无参构造函数
    /*Date2()
    {
        _year=2024;
        _month=2;
        _day=10;
    }*/
    // 2.带参构造函数
    /*Date2(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }*/
    // 3.以上两种构造函数可以用一个全缺省的构造函数来完全替代
    Date2(int year = 2024, int month = 2, int day = 10)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // 但要注意:有了这个全缺省的构造函数,最好就不要再写上面两种了,因为全缺省的构造函数和上面两种的任意一种构造函数同时写,在main函数里调用时可能会出错(语法上是可以构成函数重载的,只是对函数重载的调用不明确这种错误)
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};
void TestDate()
{
    Date2 d1;   // 调用无参构造函数
    d1.Print(); // 打印的就是2024-2-10

    Date2 d2(2024, 2, 10); // 调用带参的构造函数
    d2.Print();            // 打印的就是2024-2-10

    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。
    //       以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    // Date2 d3(); // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
}

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

ex:

class Date3
{
public:
    // 注:如果用户显式定义了构造函数,编译器将不再生成
    Date3(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};
void Test2()
{
    // 注:
    // 1、将Date类中构造函数屏蔽后,这段代码可以通过编译,因为编译器生成了一个无参的默认构造函数。
    // 2、将Date类中构造函数放开,这段代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参构造函数。
    // Date3 d1; // 放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
}

6.(构造函数最重要的一个特性)

关于编译器生成的默认成员函数,可能会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。
但是看起来默认构造函数又没什么用?
d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。-- 就是感觉这个默认生成的无参构造函数啥也没做
也就说在这里编译器生成的默认构造函数并没有什么用?
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,
如:int/char/任何类型的指针...,自定义类型就是我们使用class/struct/union等自己定义的类型。
看看下面的程序,就会发现编译器默认生成的构造函数对于内置类型不做处理,对自定义类型(_t)则会去调用它的默认构造函数。

class Time
{
public:
    Time() // 这是个构造函数
    {
        cout << "Time()" << endl;
        _hour = 0;
        _minute = 0;
        _second = 0;
    }

private:
    int _hour;
    int _minute;
    int _second;
};
class Date4
{
private:
    // 基本类型(内置类型)
    int _year;
    int _month;
    int _day;
    // 自定义类型
    Time _t;
};
void test()
{
    Date4 d;
}

补充:C++98规定是默认生成的构造函数不对这些内置类型做处理,但有些编译器会自作主张对内置类型进行处理。
补充:C++11(2011年)对这个语法进行了打补丁,允许内置类型在声明时给缺省值。
ex:(注意这里还是声明,不是定义,这叫做声明给缺省值)

private:
    int _year=1;
    int _month=1;
    int _day;

总结:就是你不给缺省值,那最后这些内置类型就是随机值,你给了缺省值,那么默认生成的无参构造函数就会用你给的这些缺省值去初始化这些内置类型。
结论:绝大多数场景下都需要我们自己实现构造函数。

7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
总结:就是不需要传参就可以调用的构造函数,都可以叫做默认构造函数。

class Date5
{
public:
    /*Date5()
    {
        _year = 1900;
        _month = 1;
        _day = 1;
    }*/
    Date5(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};
// 以下测试函数能通过编译吗?
void Test3()
{
    Date5 d1;
}
// 答:可以正常运行。
// 建议:一般情况下,默认构造函数就写全缺省构造函数。 

 补充一个问题:内置类型有构造函数吗?
 答:理论上来讲内置类型是没有构造函数的。
 但是:有了模板之后,内置类型需要有构造函数。
 所以:也可以认为内置类型有构造函数。
 举个栗子:

int i = int();
int j = int(1);

3、析构函数

注意:它不是销毁对象,就像构造函数做的是初始化,而不是创建对象。

3.1、概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理(free)工作。

3.2、特性

析构函数是特殊的成员函数,其特征如下:
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载(因为它被规定了不能有参数(虽然有this指针(隐含的参数)),所以没法重载)
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。-- 当然你也可以显示调用(但一般不要这样做,不然有些对象(例如Stack),多次析构会出错的)

typedef int DataType;
class Date6
{
public:
    /*Date6()
    {
        _year = 1900;
        _month = 1;
        _day = 1;
    }*/
    Date6(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    ~Date6()
    {
        cout << "~Date" << endl; // 这里可以用个打印来证明它是否自动调用了
    }

private:
    int _year;
    int _month;
    int _day;
};
class Stack
{
public:
    Stack(size_t capacity = 3) // 这是一个全缺省的构造函数,会自动调用对栈进行初始化
    {
        _array = (DataType *)malloc(sizeof(DataType) * capacity);
        if (NULL == _array)
        {
            perror("malloc fail");
            return;
        }
        _capacity = capacity;
        _size = 0;
    }
    void Push(DataType data)
    {
        // CheckCapacity(); //正常push要先检查一下容量,这里懒得写了
        _array[_size] = data;
        _size++;
    }

    ~Stack() // 用这个析构函数比用Destory更方便,因为这个它会自动调用,但你写的Destory需要你自己记得调用,不然就会发生内存泄漏!!
    {
        cout << "~Stack" << endl; // 这里可以用个打印来证明它是否自动调用了
        if (_array)
        {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    DataType *_array;
    size_t _capacity;
    int _size;
};
void Test5()
{
    Date6 d1;
    Stack st1;
}
// 补充:上面main函数结束时会先打印"~Stack"再打印"~Date",因为main函数是在栈上开辟的,它要符合后入先出的原则,st1后创建,就要先销毁。

5.关于编译器自动生成的析构函数,是否会完成一些事情呢?

下面的程序我们会看到,编译器生成的默认析构函数,对内置类型不做处理,对于自定义类型(_t)则会去调用它自己的析构函数。

class Time1
{
public:
    ~Time1()
    {
        cout << "~Time()" << endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};
class Date7
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time1 _t;
};
void Test6()
{
    Date7 d;
}

上面的程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
答:
main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是,main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。

注:main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。

注意:创建哪个类的对象就调用哪个类的析构函数,销毁那个类的对象就调用哪个类的析构函数。

6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,而有资源申请时,一定要写,否则会造成资源泄漏。

补充:说明静态局部对象、全局对象、静态全局对象的生命周期谁先结束的一段代码。

class Date8
{
public:
    Date8(int year = 0)
        : _year(year)
    {
    }

    ~Date8()
    {
        cout << _year << endl;
    }

private:
    int _year;
};
static Date8 d6(6); // 最后销毁的
Date8 d5(5);        // 第5个销毁的
void func1()
{
    Date8(3);        // 最先销毁
    static Date8(4); // 第4个销毁的
}
void Test7()
{
    Date8 d1(1); // 第3个销毁的
    Date8 d2(2); // 第2个销毁的
    func1();
}

上述程序的运行结果是:打印321456

总结:局部对象最先析构(要满足后定义先析构的原则,就是栈的后入先出)-->再析构局部静态变量-->最后析构全局(静态)变量(也是满足后定义先析构)

4、拷贝构造函数

C++规定自定义的类型都会调用拷贝构造函数。

拷贝构造函数也算构造函数,如果你显示定义了一个拷贝构造函数,那么编译器就不会自动生成默认构造函数了,你得自己写一个,否则程序会报错。

4.1、概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

4.2、特性

拷贝构造函数也是特殊的成员函数,其特性如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类的类型的对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

ex:

class Date9
{
public:
    Date9(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // Date9(const Date d)   // 错误写法:编译报错,会引发无穷递归 -- 原因看截图:拷贝构造函数错误写法导致无限递归的原因
    //{
    //     _year = d._year;
    //     _month = d._month;
    //     _day = d._day;
    // }
    Date9(const Date9 &d) // 正确写法:加上引用 -- 用指针也是不好的,首先就是写起来麻烦,其次就是下面调用func1()函数的时候,传参不好写 -- 这里也体现了C++中引用的价值
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

private:
    int _year;
    int _month;
    int _day;
};
void func2(Date9 d)
{}
void func3(Date9 &d)
{}
void Test8()
{
    Date9 d1(2024, 2, 11);
    Date9 d2(d1);
    func2(d1); // 传值调用 -- 通过调试可发现程序在执行到该函数时,按f11(逐步调试)不是直接进到这个func1()函数中,而是先调用拷贝构造函数进行拷贝,拷贝完才会进入这个func1()函数
    func3(d1); // 通过引用的方式 -- 通过调试可发现程序在执行到该函数时,按f11(逐步调试)是直接进到这个func2()函数中,因为func2()的参数d是d1别名,它不会再去调用拷贝构造函数了。
}

3.若未显式定义,编译器会生成默认的拷贝构造函数。

注:默认的拷贝构造在拷贝自定义类型的成员时,会去调用该成员的拷贝构造。

默认的拷贝构造函数对象按内置类型成员内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

class Time20
{
public:
    Time20() // 这边必须自己写一个构造函数,因为拷贝构造函数也是构造函数,你写了拷贝构造函数,编译器就不会自动生成默认的构造函数了
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }

    // 补充一个关键字default,可以强制编译器生成一个默认的构造函数
    // Time20()=default;

    Time20(const Time20 &t)
    {
        _hour = t._hour;
        _minute = t._minute;
        _second = t._second;
        cout << "Time20::Time20(const Time20& t)" << endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};
class Date10
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;

    // 自定义类型
    Time20 _t;
};
void Test9()
{
    Date10 d1;
    // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数。
    // 若Date类没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数。
    Date10 d2(d1);
}

注意:编译器生成的默认拷贝构造函数,对于内置类型是按照字节方式直接拷贝的,而对自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?这里会发现下面的程序会发生运行崩溃(但是编译能过),这里就需要深拷贝去解决。

typedef int DataType;
class Stack1
{
public:
    Stack1(size_t capacity = 10)
    {
        _array = (DataType *)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }

    // 这里自己写个深拷贝就不会运行崩溃了 -- 注:深拷贝实际上是很复杂的,要依据不同的实际情况去写
    Stack1(const Stack1 &s)
    {
        DataType *tmp = (DataType *)malloc(sizeof(DataType) * s._capacity);
        if (!tmp)
        {
            perror("malloc fail");
            exit(-1);
        }

        // 这里也可以选择使用memcpy,也可以使用下面的 for 循环
        memcpy(tmp, s._array, sizeof(DataType) * s._size);

        _array = tmp;

        // for (int i = 0; i < s._size; i++)
        // {
        //     _array[i] = s._array[i]; 
        // }

        _capacity = s._capacity;
        _size = s._size;
    }

    void Push(const DataType &data)
    {
        // CheckCapacity(); // 懒得写
        _array[_size] = data;
        _size++;
    }
    ~Stack1()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    DataType *_array;
    size_t _size;
    size_t _capacity;
};
void Test10()
{
    Stack1 s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack1 s2(s1);
}

运行崩溃的原因:这里的浅拷贝会导致s2对象中的_array和s1对象中的_array的地址相同,就是两个对象中的两个数组共用同一块空间,然后main函数结束时s2对象会先进行析构(s2的析构也只是把s2中的那个数组给置空了,但不影响s1中的数组),就把那块空间清理了,这时候s1对象接着析构,就又会去清理那块空间,一块空间不能被清理两次!!

注意:这跟(不是)free(NULL)没关系。

注:一块空间被free后,这块空间就还给操作系统了,系统有可能马上就将它分配给了其他程序,你再free这块空间就相当于在对任意内存进行free。(这样子程序肯定要崩溃的)

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时(这时候就是需要深拷贝),则拷贝构造函数是一定要写的,否则就是浅拷贝。

5. 拷贝构造函数典型调用场景:

使用已存在对象创建新对象。
函数参数类型为类类型对象。
函数返回值类型为类类型对象。

class Date11
{
public:
    Date11(int year, int minute, int day)
    {
        cout << "Date11(int,int,int):" << this << endl;
    }
    Date11(const Date &d)
    {
        cout << "Date11(const Date& d):" << this << endl;
    }
    ~Date11()
    {
        cout << "~Date11():" << this << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};
Date11 Test11(Date11 d)
{
    Date11 temp(d);
    return temp;
}
void Test12()
{
    Date11 d1(2022, 1, 13);
    Test11(d1);
}

注:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

拷贝构造的定义:拷贝构造就是用一个同类型的已存在的对象去进行初始化另一个要创建的对象。-- 一个已存在,一个要创建。-- ex:Date d1(d2);

补充:Date d2 = d1; -- 这属于是拷贝构造,不是赋值(套定义)

5、赋值运算符重载

赋值运算符重载就是两个已存在的对象之间,其中一个拷贝赋值给另一个对象。-- 两个都存在 -- ex: d1 = d2;

注:这里的重载和函数重载的重载的意思不一样,这里的重载的意思是:对运算符的行为重新定义,重新控制,按照我自己实际的需求来重新控制 -- 目的是让自定义类型也可以用运算符

5.1、运算符重载

C++为了增强代码的可读性引入了运算符重载。
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

ex:

如果想要比较两个自定义类型的对象的大小要怎么比?(比如就比较日期类的对象)

在没有这个运算符重载之前,可能是这样做的,写一个函数DateCompare()来实现比较。
但是这又有新的问题,比如要判断是否相等,是否大于,是否小于,那岂不是要一口气写三个比较函数?
首先这就有些麻烦,其次取名方面不好取,尤其是英语还不是很好的情况下,取名取的乱七八糟,代码可读性会很差。
所以:C++有了运算符重载就能很好的解决这类问题。

ex:

bool operator==(const Date& d1,const Date& d2) // 就算你不懂英语,你也能很好的明白这是在判断两个自定义类型是否相等
{
    return d1._year == d2._year;
       && d1._month == d2._month;
       && d1._day == d2._day;
}
int main()
{
    cout << operator==(d1,d2) << endl; // 你可以显示调用这个运算符重载函数。
    cout << (d1==d2) << endl; // 也可以这样子直接使用重载后的运算符,因为它会帮你去调用相应的运算符重载函数。 // 注意:这里要记得加(),因为 << 流插入运算符的优先级高于 ==
    return 0;
}

这实现了自定义类型也可以使用各种运算符。

注:不是所有的运算符都需要重载,有的运算符重载是没有意义的。

注意:

1、不能通过连接其他符号来创建新的操作符:比如operator@  -- 注:只能连接C/C++语法中存在的操作符
2、重载操作符必须有一个类类型参数(自定义类型的参数)
3、用于内置类型的运算符,其含义不要去改变。例如:内置的整型+,不要去通过重载运算符的方式改变该运算符(+)原本的含义(实际上是不建议这样,你真要这样也行)
4、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5、 .*  ::  sizeof  ?:(三目操作符)  .  注意以上5个运算符不能重载。这个经常在笔试选择题中出现。-- 注:第一个不是*,*是可以重载的。第一个基本用不到,了解即可,记住它不能重载就差不多了

补充:.* 的例子

class ob
{
public: // 这里要搞成public,不然下面访问不到这个函数
    void func()
    {
        cout << "void func()" << endl;
    }
};

// typedef void(*)() pobfunc; // 注意:函数指针的typedef不是这样子定义的
// typedef void(*pobfunc)(); // 而是这样子定义的,重命名的名字一定要放到(*)里
// 这里要定义是这个:
typedef void (ob::*pobfunc)(); // 这里要表达的意思是这个函数指针是这个ob类里面的 -- 是个成员函数指针

void Test17()
{
    pobfunc p = &ob::func; // 正常的函数,函数名就是地址,但成员函数你要在其函数名前面加个&才能取到它的地址
    ob temp;
    (temp.*p)();
}
// 程序成功运行,打印 void func()

补充:

全局的operator==

class Date12
{
public:
    Date12(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    int _year;
    int _month;
    int _day;
};
bool operator==(const Date12 &d1, const Date12 &d2)
{
    return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
void Test13()
{
    Date12 d1(2018, 9, 26);
    Date12 d2(2018, 9, 27);
    cout << (d1 == d2) << endl;
}
// 这里会发现运算符重载成全局的就需要成员变量要是公有的才行(上面的代码无法编译通过),那么问题来了,封装性如何保证?
// 这里其实可以用友元解决(不推荐),或者干脆重载成成员函数或者写一个Get函数去获取相应的你需要的私有的成员变量。
class Date13
{
public:
    Date13(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // 方法一,Java里面很爱用,C++也可以这样弄
    int GetYear()
    {
        return _year;
    }

    // 方法二,直接把它放到类里面,重载成成员函数,但这时候要注意它的形参个数看起来要比实际的操作数数目少1(因为隐藏了一个this) --运算符需要几个操作数就只能有几个形参,不能多也不能少
    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this,指向调用函数的对象
    bool operator==(const Date13 &d2)
    {
        return _year == d2._year && _month == d2._month && _day == d2._day;
    }

private:
    int _year;
    int _month;
    int _day;
};
// bool operator==(const Date13& d1, const Date13& d2)
//{
//     return d1.GetYear() == d2.GetYear(); //方法一
// }
void Test14()
{
    Date13 d1;
    Date13 d2;
    cout << d1.operator==(d2) << endl; // 你可以显示调用这个运算符重载函数
    cout << (d1 == d2) << endl;        // 也可以这样子使用重载后的运算符,因为编译器会帮你去调用相应的运算符重载函数(有全局就调全局,没有全局的就去类里找) //注意:这里要记得加(),因为 << 流插入运算符的优先级高于 ==
}
// 补充:编译器会自动识别运算符中的操作数是自定义类型(自定义类型则是它会自动去找相应的重载运算符函数,找不到就报错)还是内置类型(内置类型会直接转换为指令去进行操作)

5.2、赋值运算符重载

1.赋值运算符重载格式

参数类型:const 类&,传递引用可以提高传参效率

返回值类型:类&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值           

返回*this :要符合连续赋值的含义  

ex:

class Date14
{
public:
    Date14(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // 注:这里的引用也不能替换成指针
    Date14& operator=(const Date14 &d) // 注意:赋值运算符的返回值不要加const,它没有被要求说返回值不能被修改 // 补充:int i = 0, j = 1; (i = j) = 10; -- 这是可行的,(i = j)的返回值就是 i
    { 
        if (this != &d) // 注:这是为了避免d1=d1;这种情况(虽然这也没关系)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this; // this是个指针,是传给它的那个对象的地址
    }

private:
    int _year;
    int _month;
    int _day;
};
void Test15()
{
    Date14 d1;
    Date14 d2;
    Date14 d3;

    d1 = d2 = d3; // 这就是连续赋值
}

2.赋值运算符只能重载成类的成员函数不能重载成全局函数

ex:

class Date15
{
public:
    Date15(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    int _year;
    int _month;
    int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
// Date15& operator=(Date15& left, const Date15& right)
// {
//      if (&left != &right)
//      {
//          left._year = right._year;
//          left._month = right._month;
//          left._day = right._day;
//      }
//      return left;
// }
// 编译失败:
// error C2801: “operator =”必须是非静态成员
// 原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

补充:《C++ prime》第5版中(p500页)说到:我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
注:只有赋值运算符的重载比较特殊,必须要是成员函数。

3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。-- 就是浅拷贝

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要去调用其对应类的赋值运算符重载完成赋值。

ex:

class Time6
{
public:
    Time6()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time6 &operator=(const Time6 &t)
    {
        if (this != &t)
        {
            _hour = t._hour;
            _minute = t._minute;
            _second = t._second;
        }
        return *this;
    }

private:
    int _hour;
    int _minute;
    int _second;
};
class Date16
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time6 _t;
};
void Test16()
{
    Date16 d1;
    Date16 d2;
    d1 = d2;
}

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?
当然像日期类这样的类是没必要的。
那么下面的类呢?验证一下试试?
这里会发现下面的程序会崩溃掉?这里就需要深拷贝去解决了。

typedef int DataType;
class Stack3
{
public:
    Stack3(size_t capacity = 10)
    {
        _array = (DataType *)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }
    void Push(const DataType &data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    ~Stack3()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    DataType *_array;
    size_t _size;
    size_t _capacity;
};
void Test18()
{
    Stack3 s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack3 s2;
    s2 = s1;
}

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

总结:就是跟拷贝构造函数差不多,有的像栈那种就需要深拷贝的,那么就要自己写一个函数,至于像日期类的那种,不需要深拷贝的则可以不写,用它默认生成的就好。
 

5.3、前置++和后置++重载

注:为了区分前置++和后置++,C++做了特殊处理:解决语法逻辑不自洽,自相矛盾的问题。

ex:

class Date17
{
public:
    Date17(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // 前置++:返回+1之后的结果
    // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
    Date17 &operator++()
    {
        _day += 1;
        return *this;
    }

    // 后置++:
    // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
    // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
    // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1
    // 而temp是临时对象,因此只能以值的方式返回,不能返回引用
    Date17 operator++(int)
    {
        Date17 temp(*this);
        _day += 1;
        return temp;
    }

private:
    int _year;
    int _month;
    int _day;
};

void Test20()
{
    Date17 d;
    Date17 d1(2022, 1, 13);
    d = d1++; // d: 2022,1,13  d1:2022,1,14
    d = ++d1; // d: 2022,1,15  d1:2022,1,15
}

你可能感兴趣的:(c++,开发语言)