C++类和对象-中篇(探究六大默认成员函数的奥妙)

文章目录

  • 前言
  • 六大成员函数
  • 一、构造函数
    • 1. 为什么要有构造函数
    • 2. 构造函数的概念与用法
      • (1)概念
      • (2)用法
    • 3. 构造函数的特性
    • 4.默认构造函数
    • 5. 总结
  • 二、析构函数
    • 1. 引入
    • 2. 外在特征
    • 3. 内在特征
    • 4. 总结
  • 三、拷贝构造函数
    • 1. 引入
    • 2. 什么是拷贝构造函数?
    • 3. 什么情况下使用拷贝构造函数?
    • 4. 使用拷贝构造函数需要注意什么?
    • 5. 深拷贝和浅拷贝
      • (1)浅拷贝
      • (2)深拷贝
  • 四、赋值运算符重载函数
    • 1. 运算符重载回忆
    • 2. 赋值重载
  • 五、取地址及const取地址操作符重载
  • 六、const成员


前言

深入剖析类和对象中六大成员函数不为人知的一面。
如果觉得有所收获,或者觉得作者写的还行,希望给作者点点赞,能评论几句就更好了,欢迎各位大佬对我的文章提出建议。
如果文章有错误,还请各位指出,感谢。
C++类和对象-中篇(探究六大默认成员函数的奥妙)_第1张图片


六大成员函数

如果一个类中什么成员都没有,简称为空类。
但是空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数
默认成员函数就是:如果用户没有自己去编写如何实现这几个函数,那么编译器就会自动帮你生成。如果用户写了,那编译器就不会自动生成了。
在这里插入图片描述

一、构造函数

1. 为什么要有构造函数

我们先实现一个日期类,用这个日期类来说明。


#include 
using namespace std;

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;

};

int main() 
{
    Date t1;
    t1.Init(2024, 1, 21);
    t1.Print();
    Date t2;
    t2.Init(2024, 1, 23);
    t2.Print();
    return 0;
}

我们可以看到,每设置一个对象比如t1、t2,想要初始化对象中的内容是每次都得自己手动调用成员函数Init(),是不是很烦呢?那有没有什么方法可以让事情变得简单一点,比如在对象创建时,就将信息设置进去呢?
当然有,那就是接下来要说的构造函数

2. 构造函数的概念与用法

(1)概念

构造函数是一个特殊的成员函数

  • 函数名与类名相同;
  • 创建类类型对象时由编译器自动调用自动挡,非手动挡);
  • 保证每个数据成员都有一个合适的初始值,并且对象整个生命周期内只调用一次

构造函数的意义:能够保证对象被 初始化 (init) 。
构造函数是特殊的成员函数,主要任务是初始化,而不是开空间(虽然构造函数的名字叫构造)。
可以看到我们完成了初始化工作
在这里插入图片描述

(2)用法

构造函数的定义

类名(形参)
{
	内容
}

形参可以有好几个也可以没有,会构成函数重载
在这里插入图片描述
上图中我们定义了两个构造函数一个无参数一个三个参数,它们之间构成函数重载。

使用方法
由上图可得,我们想要调用无参的构造函数那就直接实例化对象就行,啥也不用管。如果你想要调用三参数的构造函数,那就在实例化对象之后加个(),里面填上你想要的数据即可。

3. 构造函数的特性

  1. 函数名与类名相同。(比如类名叫Date,那么构造函数名就叫Date)
  2. 无返回值。(返回值啥都没有,可不是void啊)
  • 这里要说明一下,无返回值是根本没有返回值这个东西,不是返回值为空(void)
    在这里插入图片描述
  1. 对象实例化时编译器自动调用对应的构造函数。
  • 就是这个构造函数会在对象实例化时编译器自动调用,不用你自己调用,甚至自己调用会报错。
    在这里插入图片描述
  • 可以看到,我们并没有调用构造函数,但它却输出了“调用了构造函数Date”说明了编译器自动帮我们调用了。是自动挡,不是手动挡。如果你非要手动调用,那是会报错的哦。
    在这里插入图片描述
  1. 构造函数可以重载。(无参构造函数和有参构造函数构成函数重载)
  2. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  • 这里我们把构造函数全部注释掉之后,编译器会自动生成一个无参构造函数,在实例化对象之后,就会调用该构造函数。(在下图中可以看到成员变量是随机值,即编译器自己生成的构造函数对于内置类型是不会有任何操作的,下一点中会说到。)
    在这里插入图片描述
  1. 关于编译器生成的默认成员函数,很多同学会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?
  • 解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
#include 
using namespace std;

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

我们可以看到,我们在Date类中是没有写构造函数的,这里运行代码使用过的是编译器生成的默认构造函数,可见这个默认构造函数调用的_t的构造函数而_t的数据类型是自定义类型。而对内置类型没有任何处理,这里的内置类型是随机值。
在这里插入图片描述
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
这样就解决了调用编译器生成的构造函数内置类型是随机值的问题。
在这里插入图片描述

总结:

  1. C++中将数据类型分为内置类型如:int/char…,和使用class/struct/union等自己定义的类型。
  2. 对于编译器生产的默认构造函数来说,它会调用自定义类型的构造函数,但不会对内置类型进行处理,这也是内置类型是随机值的原因。
  3. C++11中内置类型成员变量在类中声明时可以给默认值来解决针对内置类型成员不初始化的缺陷
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

4.默认构造函数

构造函数是可以全缺省的。意思就是在函数形参中给定一个初始值,在调用这个函数是,不过不传参数,那就使用默认的缺省值,如果传参了,那就使用传的参数。
如图我们实现了一个全缺省的构造函数,如果没有传参,那就默认year=2000, month=2, day=2,如果传了参数t2(2024,1,23)那就使用传的参数而不是缺省值。
在这里插入图片描述
在C++中类的默认构造函数有三个。

  1. 无参数的构造函数
  2. 全缺省的构造函数
  3. 编译器自动生成的构造函数
  • 这里有一个问题就是无参数的构造函数和全缺省的构造函数是不能同时写出来的,因为这俩构造函数调用方法都是Date t1,就会导致不知道应该调用哪个构造函数,是无参的呢?还是全缺省的呢?因此会报错。
    在这里插入图片描述
    删掉一个就好了。

5. 总结

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

  1. C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
  2. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

二、析构函数

1. 引入

通过构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么销毁的呢?
比如在构造函数中malloc了一段内存,最后要释放的话,我们还得专门写一个Destroy函数来free它,还得记得调用这个函数。
**有没有一个函数像构造函数一样编译器会自动调用来帮助我们实现清理资源的工作呢?**当然有,那就是析构函数。

2. 外在特征

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

外貌特征:

  1. 析构函数名是在类名前加上字符 ~。(如Date类对象的析构函数就叫 ~Date())
  2. 无参数无返回值类型。(没有返回值类型,不是返回值为空,也没有参数)
    C++类和对象-中篇(探究六大默认成员函数的奥妙)_第2张图片

3. 内在特征

  1. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  2. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

为了验证在对象生命周期结束时,编译器会自动调用析构函数,我们可以让对象叫一声即在析构函数中写个输出语句,只要析构函数被调用了,那就会在控制台上打印出一段话。


#include 
using namespace std;

class Date
{
public:

    Date(int year=2024, int month=1, int day=24)//全缺省的构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }

    ~Date()// 析构函数
    {
        //Date类没有要清理的资源,所以可以什么都不写
        cout << "调用了析构函数" << endl;//叫一声
    }
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year = 2024;
    int _month = 1;
    int _day = 23;

};
int main() 
{
    Date t1;
    return 0;
}

C++类和对象-中篇(探究六大默认成员函数的奥妙)_第3张图片

用Date类没办法向大家清晰的展示析构函数的清理工作,这里我们使用Stack来展示析构函数是如何清理资源的。

因为Stack要用malloc在堆区开空间的,所以最后就一定要将这部分空间释放掉,这就要靠析构函数来实现了。

如此一来就保证了初始化有构造函数清理资源有析构函数,再也不用担心最后因为没有手动调用destroy函数释放资源了,析构函数会被编译器自动调用完成这件事。

代码演示

#include
#include
using namespace std;
 
typedef int StackDataType;
class Stack {
public:
    // 构造函数 - StackInit
    Stack(int capacity = 4) {  // 这里只需要一个capacity就够了,默认给4(利用缺省参数)
        _array = (StackDataType*)malloc(sizeof(StackDateType) * capacity);
        if (_array == NULL) {
            cout << "Malloc Failed!" << endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
 
    // 析构函数 - StackDestroy 
    ~Stack() {   
    // 这里就用的上析构函数了,我们需要清理开辟的内存空间(防止内存泄漏)
        free(_array); //释放内存空间
        _array = nullptr;  //置空
        _top = _capacity = 0;
    }
 
private:
    int* _array;
    size_t _top;
    size_t _capacity;
};
 
int main(void)
{
    Stack s1;
    Stack s2(20); // s2 栈 初始capacity给的是20
 
    return 0;
}
  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
    C++类和对象-中篇(探究六大默认成员函数的奥妙)_第4张图片

注释:

在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类生成的默认析构函数

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

总结:
一、如果不自己写构造函数,让编译器自动生成,那么这个自动生成的 默认构造函数:

  1. 对于 “内置类型” 的成员变量:不会做初始化处理。
  2. 对于 “自定义类型” 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!

二、如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:

  1. 对于 “内置类型” 的成员变量:不作处理 (不会帮你清理的.)
  2. 对于 “自定义类型” 的成员变量:会调用它对应的析构函数。
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类

4. 总结

  1. 析构函数名是在类名前加上字符 ~。(如Date类对象的析构函数就叫 ~Date())
  2. 无参数无返回值类型。(没有返回值类型,不是返回值为空,也没有参数)
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,==编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
  6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类

三、拷贝构造函数

1. 引入

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
在这里插入图片描述
相当于就是把自己复制一遍,内置类型如int,char这些要实现复制很简单只需要:

int a = 10;
int b = a;

那在创建类对象时,如何创建一个与已存在对象一摸一样的新对象呢?
答案是拷贝构造

class Date
{
public:
    Date(int year = 2024, int month = 1, int day = 25)//全缺省构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date(const Date& d)//拷贝构造
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    ~Date()//析构函数
    {
        //cout << "~Time()" << endl;
    }
    void show()//普通函数
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};


int main()
{
    Date a(2000, 1, 1);
    Date b = a; //注意这里的对象初始化要调用拷贝构造函数,而非赋值
    b.show();
    return 0;
}

在这里插入图片描述
从以上代码可以看出系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
具体过程是当编译器执行到Date b = a这一行代码时会自动调用b的拷贝构造函数,拷贝构造的参数为对象a,并在函数中完成赋值操作。


2. 什么是拷贝构造函数?

同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的。==这个拷贝过程只需要拷贝数据成员,==而函数成员是共用的(只有一份拷贝)。在建立对象时可用同一类的另一个对象来初始化该对象的存储空间,这时所用的构造函数称为拷贝构造函数。

拷贝构造函数本质上来说也是构造函数是构造函数的一个重载。

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


3. 什么情况下使用拷贝构造函数?

  • 使用已存在对象创建新对象(用旧对象去初始化新对象)
Date a;
Date b=a;//用已存在的对象去创建一个新对象,这里是拷贝构造不是复制重载
Date c(a);//拷贝构造
//下面这个是复制重载为了你们区分就都写出来对比一下
Date d;
d=a;
  • 函数参数类型为类类型对象(从实参传递给形参的过程,是用实参去构造形参)
  • 函数返回值类型为类类型对象(用局部对象去构造临时对象调用拷贝构造)
class Date
{
public:
    Date(int year = 2024, int month = 1, int day = 25)//全缺省构造函数
    {
        cout << "Date(int,int,int):" << this << endl;
    }
    Date(const Date& d)//我们自定义的拷贝构造
    {
        cout << "Date(const Date& d):" << this << endl;
    }
    ~Date()//析构函数
    {
        cout << "~Date():" << this << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
Date Test(Date d)
{
    Date temp(d);
    return temp;
}
int main()
{
    Date d1(2022, 1, 13);
    Test(d1);
    return 0;
}

在这里插入图片描述
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
在这里插入图片描述


4. 使用拷贝构造函数需要注意什么?

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  • 上面我们知道,实参去构造形参的时候会调用拷贝构造的,如果构造函数没有用引用,那么在调用构造函数进行实参构造形参时就会调用构造函数,调用构造函数就又得用实参构造形参,又有得调用构造函数,这样下去就会导致一直调用构造函数,引发无穷递归调用。
    C++类和对象-中篇(探究六大默认成员函数的奥妙)_第5张图片
  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

5. 深拷贝和浅拷贝

(1)浅拷贝

浅拷贝就是在对象复制时**,仅仅只对对象中的数据成员进行简单的赋值**,默认的拷贝构造就是浅拷贝,虽然大多数情况下浅拷贝就已经够用了,但如果出现资源申请的情况(申请内存),浅拷贝就会出现一些问题。

以下程序是我们自定义实现了一个栈,其中有个成员变量是指针DataType* _array;,在构造函数中我们在堆区申请了内存,让指针_array指向了内块内存。我们在主函数中写了Stack s2(s1);利用构造函数构造了一个s2,但默认的构造函数时是浅拷贝,在s2的构造函数中相当于有这么一句代码_array=s1._array;。这造成了两个指针指向了同一块内存空间,但这俩指针在不同的对象s1,s2中,在俩对象析构的时候会导致同一块内存空间释放两次,导致错误。

typedef int DataType;
class Stack
{
public:
    Stack(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++;
    }
    ~Stack()
    {
        if (_array)

        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType* _array;
    size_t _size;
    size_t _capacity;
};
int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2(s1);
    return 0;
}

在这里插入图片描述


(2)深拷贝

为了解决上述问题 我们就需要给s2中的_array也开辟和s1中的_array一样大小的空间,所以我们就需要深拷贝
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
手动写一个深拷贝的拷贝构造,各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

    Stack(const Stack& s)
    {
        //s1空间有多大就申请多大的空间
        _array= (DataType*)malloc(s._capacity * sizeof(DataType));
        if (_array == NULL)
        {
            exit(-1);
        }
        for (int i = 0; i < s._size; i++)
        {
            _array[i] = s._array[i];
            _size++;
        }
        _capacity = s._capacity;
    }

四、赋值运算符重载函数

1. 运算符重载回忆

运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

//函数原型:返回值类型 operator操作符(参数列表)
返回值 operator 操作符(参数列表)
{

}

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

2. 赋值重载

在讲拷贝构造的时候,我们说明了初始化和赋值的区别:在定义的同时进行赋值叫做初始化(Initialization),定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值(Assignment)。初始化只能有一次,赋值可以有多次。

//Date为一个类
Date a;
Date b(a);//拷贝构造
Date c=a;//拷贝构造
Date d;
d=a;//复制重载

当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符

1. 赋值运算符重载格式

    1. 参数类型:const T&,传递引用可以提高传参效率
    1. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
//Date& operator=(const Date& d)a、b、c、d都是Date类对象
a=b=c=d;//由于有返回值Date&,所以能连续赋值
    1. 检测是否自己给自己赋值
    1. 返回*this :要复合连续赋值的含义
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;
    }
    Date& operator=(const Date& d)
    {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
private:
    int _year;
    int _month;
    int _day;
};

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

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    int _year;
    int _month;
    int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
    if (&left != &right)
    {
        left._year = right._year;
        left._month = right._month;
        left._day = right._day;
    }
    return left;
}
// 编译失败:
//  “operator =”必须是成员函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

class Time
{
public:
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time& operator=(const Time& t)
    {
        if (this != &t)
        {
            _hour = t._hour;
            _minute = t._minute;
            _second = t._second;
        }
        return *this;
    }
private:
    int _hour;
    int _minute;
    int _second;
};
class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;
};
int main()
{
    Date d1;
    Date d2;
    d1 = d2;
    return 0;
}

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

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
    Stack(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++;
    }
    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType* _array;
    size_t _size;
    size_t _capacity;
};
int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2;
    s2 = s1;
    return 0;
}

C++类和对象-中篇(探究六大默认成员函数的奥妙)_第6张图片

对于简单的类,默认的赋值运算符一般就够用了,我们也没有必要再显式地重载它。但是当类持有其它资源时,例如动态分配的内存、打开的文件、指向其他数据的指针、网络连接等,默认的赋值运算符就不能处理了,我们必须显式地重载它,这样才能将原有对象的所有数据都赋值给新对象。

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{
public :
Date* operator&()
{
	return this ;
}
const Date* operator&()const
{
	return this ;
}
private :
	int _year ; // 年
	int _month ; // 月
	int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

六、const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
C++类和对象-中篇(探究六大默认成员函数的奥妙)_第7张图片

你可能感兴趣的:(C++,c++)