• C++ 六个默认成员函数 + this指针


    目录

    this指针

    引出:

    概念:

    示例:

    注意点:

    this指针存储在哪里?

    一个经典题

    六个默认成员函数👇

    一、构造函数

    简介与作用

    基本语法与一些规则

    默认构造函数

    初始化列表

    类成员变量声明处的默认值/缺省值

    编译器自动生成的默认构造函数做了什么工作

    二、析构函数

    作用与简介:

    基本语法:

    默认析构函数

    什么时候需要主动实现析构函数

    析构函数调用顺序

    三、拷贝构造函数

    作用:

    拷贝是什么操作?

    语法与细节:

    为什么参数必须是类对象的&,和const

    默认拷贝构造函数的操作:

    什么时候需要自己编写拷贝构造函数:

    拷贝构造函数使用情景

    四、重载拷贝赋值运算符函数

    重载运算符函数

    理解:

    语法与规则:

    重载拷贝赋值运算符函数

    简介与功能

    什么时候是拷贝构造,什么时候是赋值?

    语法与细节:

    编译器默认生成的重载赋值运算符函数

    什么时候需要自己实现重载赋值运算符函数

    重载<<   >>运算符

    前言

    重载<< >>函数必须定义为全局的,不能定义在类内。

    注意点:

    五,六 重载&运算符函数


    this指针

    this指针是C++类和对象中的一个关键,后面的运算符重载,以及很多知识都涉及到this指针。

    引出:

    1. bool Date::Change(int year, int month, int day) {
    2. if(CheckDate(year,month,day)) {
    3. _year = year;
    4. _month = month;
    5. _day = day;
    6. return true;
    7. }else{
    8. return false;
    9. }
    10. }

    如上是一个日期类的修改日期的函数,如果date1 和 date2分别调用此函数,那么这个函数是如何正确修改对应对象的数据成员_year _month _ day的呢? 就是因为this指针。

    概念:

    C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

    示例:

    所以上述示例代码编译器会处理为:

    1. bool Date::Change(Date* const this, int year, int month, int day) {
    2. if(this->CheckDate(year,month,day)) {
    3. this->_year = year;
    4. this->_month = month;
    5. this->_day = day;
    6. return true;
    7. }else{
    8. return false;
    9. }
    10. }

    而我们的调用语句,编译器会处理为

    1. Date d1(2022,8,10);
    2. Date d2(2021,3,1);
    3. d1.Print(&d1);
    4. d1.Change(&d1,2022,2,22);
    5. d2.Change(&d2,2021,1,1);

    由上可知,任何一个非静态成员函数,调用时必须由某个类实例化对象调用,而调用后,就会隐式地在第一个参数位置传递这个对象的地址。而形参列表的第一个也是一个隐式的Date* const this。由此才能对某个对象的数据成员进行准确的读写操作!

    注意点:

    1. 实参和形参部分,不能显式地传递和接收this指针,这是编译器隐式进行的操作。
    2. 在函数内可以显式地使用this指针,去调用其数据成员或者成员函数,如果不显式写,在每个数据成员和成员函数前,编译器会隐式处理为this-> 
    3. this指针只能在成员函数内部使用
    4. this指针默认为常量指针,也就是this的指向不可以改变。
    5. const对象的this指针为const X* const this,代表此this指向的对象的数据不可以被改变,即指向常量对象的常量指针。 这就会引出const成员函数等一系列问题。后面再详细说
    6. 如果成员函数被定义为const成员函数,则表示此函数不会改变this指向的对象的数据,则this指针变为const X* const this ,而对应第5点,可知,const对象只能调用const成员函数,因为指向常量的this指针如果传递给一个非const成员函数,就属于权限的方法,是非法的。(后面再详细说const成员函数,其实内容并不多)
    7. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

    this指针存储在哪里?

    this指针并不存储在对象内部,因为this指针是作为参数传递的,所以在函数栈帧内,也就是栈区。但是这个也不绝对,因为this指针是一个使用频繁的参数,有些编译器会将使用频繁的变量进行优化,将其存储在寄存器中,不过这是属于编译器的优化行为。

    一个经典题

    1. class A
    2. {
    3. public:
    4. void Print()
    5. {
    6. printf("%p\n",this);
    7. // cout<
    8. cout<<"Print()"<
    9. }
    10. void PrintA()
    11. {
    12. cout<<_a<
    13. }
    14. private:
    15. int _a;
    16. };
    17. int main()
    18. {
    19. A* p = nullptr;
    20. p->Print();
    21. p->PrintA();
    22. return 0;
    23. }

    问,如果单独运行p->Print();  和 p->PrintA(); 分别是什么结果?

    答: Print()函数会正常运行,因为这里的p虽然是空指针,但是我们调用Print函数时,并没有解引用它,因为函数代码根本不存在对象内,是存储在公共代码区里的,通过指针得知其指向A类型对象,然后到对应的区域调用Print()函数,打印出00000000 和 Print()   (这里也会把this指针作为第一个参数隐式传递过去)

            PrintA();函数会运行崩溃,因为执行函数内代码cout<<_a<_a ,会解引用this指针,因为_a是存储在对象内的。后发觉这是一个空指针,就会崩溃。(这并不是编译时报错,而是运行时)        

    六个默认成员函数👇

    默认成员函数:用户没有显式实现时,编译器自动生成的成员函数称为默认成员函数。

    下面介绍六个默认成员函数,默认成员函数即每个类默认都会有的成员函数,它们各自有不同的作用,如果你不显式实现,那么编译器会自动实现一个默认的。分别是:构造函数,析构函数,拷贝构造函数,拷贝赋值运算符重载函数,&运算符重载函数,const版&运算符重载函数。其中前四个为重点,最后两个了解即可。  

     学习这些函数时,需要针对几个点进行学习
    这些函数各自的作用,什么时候调用,编译器默认生成的能够完成什么工作,什么时候需要我们自己实现。   暂时想不到其他的了,剩下的就是一些细节的点了

    一、构造函数

    简介与作用

    构造函数的存在的意义是什么呢? 比如我们用C语言写一个Stack,那么这个Stack创建出变量/对象之后,要调用Init进行初始化。其实构造函数的作用就是,帮助我们进行更方便地初始化对象。因为每个类实例化对象时,都会由编译器自动调用配对的构造函数,(无论是编译器自己生成的还是我们写的,无论是有参还是无参)这样就保证了对象的初始化工作,并且更加的方便。(一个类如果没有构造函数,则无法创建对象)

    所以说,构造函数并不是用来构造/创建对象的,而是初始化对象的!

    基本语法与一些规则

    1. 构造函数无返回值,函数名与类名相同(不想说了)
    2. 构造函数在创建对象时由编译器自动调用,且必须调用一个,无论哪个版本
    3. 构造函数可以重载,以适应不同的初始化情形。

    默认构造函数

    默认构造函数只能有一个,(默认构造函数是不传参时调用的那个构造函数),为了避免歧义,所以只能有一个。

    默认构造函数包括:

    a、我们不实现任意构造函数时,编译器自动创建的那个

    b、我们写的无参构造函数 Date() {} 

    c、我们实现的,所有参数都有默认值/缺省值的构造函数 Date(int year = 1, int month = 1, int day = 1) {}   

    这三个,一个类中只能存在一个。 (一旦用户显式定义任何一个构造函数,编译器都不会再生成那个默认构造函数)建议每个类都实现一个默认构造函数,原因暂时略了。


    构造函数还有几个需要注意的点,如初始化列表,成员变量声明处的默认值。编译器自动生成的默认构造函数完成了什么工作。


    Date类的初步实现,主要关注其中的两个构造函数

    1. class Date
    2. {
    3. public:
    4. // 我们主动实现的全缺省默认构造函数
    5. Date(int year = 1, int month = 1, int day =1)
    6. : _year(year),_month(month),_day(day){
    7. assert(CheckDate(_year,_month,_day)); // 断言此检查为真,如果为假则报错。
    8. }
    9. // 构造函数重载
    10. Date(int month,int day)
    11. :_month(month),_day(day) {
    12. _year = 2022;
    13. }
    14. ~Date() = default;
    15. Date(const Date& d) = default;
    16. Date& operator=(const Date& d) = default;
    17. void Print()const;
    18. static bool CheckDate(int year,int month,int day);
    19. bool Change(int year,int month, int day);
    20. static int GetDayOfMonth(int year,int month); // 这里设置为静态的,主要是因为函数内部没有使用this
    21. private:
    22. int _year = 1;
    23. int _month = 1;
    24. int _day = 1;
    25. };

    初始化列表

    初始化列表对于理解构造函数非常重要。 理解初始化列表的作用,它和构造函数函数体的功能的区别!!

    1. 每个构造函数都存在初始化列表,如果我们不显式写,就会有一个默认的。且每次执行构造函数都会执行初始化列表

    2. 初始化列表的作用是初始化数据成员,构造函数函数体的作用是给数据成员赋值。他们是有本质区别的

    3. 如果我们不写初始化列表,它会执行默认的。默认的对于基本类型会初始化为随机值,自定义类型会调用其默认构造函数进行初始化。 这里和编译器自动形成的默认构造函数的作用也有关系,因为那个构造函数的作用就是靠它的初始化列表实现的!!

    4. 由上我们可以知道,能使用初始化列表就使用,除非一些特定的操作必须函数体内执行。

    (有点烦)

    类成员变量声明处的默认值/缺省值

    之前有过一个疑惑,就是,这里的默认值,和构造函数参数的默认值,初始化列表,还有函数体内的语句优先级,最终会采用哪个来作为数据成员的值。

    首先需要认识到一点:这里是成员变量的声明,而不是定义,数据成员的定义发生在创建对象时。

    引:C++默认对于内置类型不处理,初始化列表对其初始化为随机值,这算是C++的一个缺陷,后来为了弥补这个缺陷,在C++11中有了这里的默认值,即数据成员声明处的默认值。 

    结论:其实这里的默认值,就是在初始化列表中,如果你没有对内置类型数据成员进行显式初始化,就会采用这里的默认值对其初始化。 

    所以,上方的疑惑可以解决了,这里的值就是初始化列表对内置类型数据成员的初始值,如果自己没有对内置类型显式初始化,就会用这里的默认值进行初始化。而构造函数函数体中的语句为赋值,也就是初始化工作之后的行为。  则:声明处的缺省值 < 初始化列表显式初始化 < 构造函数函数体内的赋值

    编译器自动生成的默认构造函数做了什么工作

    有了上面的铺垫,了解这个就不难了。 

    联想初始化列表,它对数据成员进行了分类:内置类型初始化为随机值(除非声明处有默认值)。自定义类型数据成员,调用其默认构造函数进行初始化!  

    所以,如果一个类的某自定义类型数据成员没有默认构造函数,则这个类也无法生成默认构造函数!

    二、析构函数

    学习析构函数的作用,默认析构函数做了哪些,什么时候我们需要自己实现析构函数。

    作用与简介:

    析构函数,就是在对象销毁时调用的一个函数,目的是对对象申请的资源进行回收,防止内存泄漏。联想到C语言实现的各种数据结构,其中的Destroy就是完成的这个工作。而C++设计了析构函数,可以保证在每个对象生命周期结束时,自动调用,很方便省心。

    基本语法:

    1. 函数名为~ 加 类名,无返回值,无参数,不能重载

    2. 如果我们不自己定义析构函数,则每个类都会有一个默认析构函数。

    3. 每个对象的生命周期结束时,都会调用析构函数。如果一个类的析构函数是delete的,则根本无法创建其对象

    4. 析构函数的作用并不是销毁这个对象,而是回收清理对象申请的资源。(比如函数内的局部对象是在函数栈帧内创建的,函数栈帧结束,对象自动销毁。 静态对象,在创建之后直到程序结束都存在,程序结束时销毁。 new/malloc创建的堆区对象,delete/free时销毁。  这些都不是析构函数的作用)

    默认析构函数

    如果我们没有自己定义析构函数,则编译器自动生成一个默认的析构函数。但是问题是,我们什么时候需要自己写析构函数呢?所以需要了解默认析构函数的作用。

    对于内置类型,不处理。 对于自定义类型数据成员,调用它的析构函数。

    什么时候需要主动实现析构函数

    1. 比如上方的Date类,它的数据成员都是内置类型,并且只用于存储数据,没有申请额外的空间,所以,默认析构函数就可以。

    2. 如下,C++实现了一个简单的Stack(大部分功能还没实现)

    1. #define DateType int
    2. class Stack {
    3. public:
    4. Stack(int capacity = 4): _capacity(capacity), _top(0){
    5. _array = (DateType*)malloc(sizeof(DateType)*capacity);
    6. assert(_array!= nullptr);
    7. }
    8. ~Stack() {
    9. if(_array != nullptr) {
    10. free(_array);
    11. }
    12. }
    13. void Push(DateType x) {
    14. CheckCapacity();
    15. _array[_top++] = x;
    16. }
    17. void CheckCapacity() {
    18. if(_top == _capacity) {
    19. DateType* tmp = (DateType*)realloc(_array,sizeof(DateType)*(_capacity!=0?2*_capacity:4));
    20. if(tmp == nullptr) {
    21. perror("malloc failed");
    22. return;
    23. }else {
    24. _array = tmp;
    25. _capacity = _capacity!=0?_capacity*2:4;
    26. cout<<"增容成功"<
    27. }
    28. }
    29. }
    30. void Print()const{
    31. for(int i = 0; i < _top;++i) {
    32. cout<<_array[i]<<" ";
    33. }
    34. cout<
    35. }
    36. private:
    37. DateType* _array;
    38. int _capacity;
    39. int _top;
    40. };

    可以看到,这个Stack的数据成员分别是指针,int。都属于内置类型,析构函数不会对其进行处理。但是,可以看到这个类在堆区申请了空间,并由_array指向了这块空间,如果我们不主动实现析构函数,则默认析构函数无法完成堆区资源回收的功能,会造成资源浪费。 所以,诸如此类情况,我们需要自己实现析构函数。

    3. 两个Stack实现Queue

    1. class QueueByStack
    2. {
    3. public:
    4. // .....
    5. private:
    6. Stack _st1;
    7. Stack _st2;
    8. int _size;
    9. };

    如上,如果我们用两个栈来实现一个队列,则此时我们是否需要主动编写析构函数呢?

    答:因为对于自定义类型,默认析构函数会去调用其析构函数,所以,Stack的两个实例化数据成员,会去调用~Stack完成资源清理,所以我们不需要自己编写析构函数,因为默认的即可。

    综上:如果类自己申请了某些资源(尤其是堆区资源),则,需要我们在析构函数中主动回收。 这样,每个类只管理自己申请的资源,就会使得QueueByStack这样的类不需要做额外的工作。

    析构函数调用顺序

    1. Test t4(4);
    2. static Test t5(5);
    3. Test t6(6);
    4. static Test t7(7);
    5. void func() {
    6. static Test t0(0);
    7. Test t1(1);
    8. Test t2(2);
    9. static Test t3(3);
    10. }
    11. int main()
    12. {
    13. func();
    14. func();
    15. return 0;
    16. }

    对于上述代码,可以看到,有全局对象,静态全局对象,局部对象,局部静态对象。 我们研究的是,析构函数的调用顺序与对象的创建顺序的关系。

    首先,对于静态全局对象,和全局对象。大致介绍:静态全局对象是只在此文件内全局可见,在其他文件中extern此静态全局变量/对象是无法访问的。 但是全局对象,在其他文件中extern是可以访问的。  它们都存储在静态区。包括局部静态对象。

    函数调用,会在栈区内开辟函数栈帧,而栈区空间由上向下使用。且函数调用遵循栈的特性:先调用的后销毁,后调用的先销毁。 同样,在一个函数栈帧内创建的局部对象,也遵循先创建的后析构,后创建的先析构。   (局部静态对象存储在静态区,不随着函数栈帧的销毁而销毁,从对象创建的语句开始到程序结束一直存在)

    如下,为构造函数和析构函数的调用顺序。

    1. Test(int n)4
    2. Test(int n)5
    3. Test(int n)6
    4. Test(int n)7
    5. Test(int n)0
    6. Test(int n)1
    7. Test(int n)2
    8. Test(int n)3
    9. ~Test()2
    10. ~Test()1
    11. Test(int n)1
    12. Test(int n)2
    13. ~Test()2
    14. ~Test()1
    15. ~Test()3
    16. ~Test()0
    17. ~Test()7
    18. ~Test()6
    19. ~Test()5
    20. ~Test()4

    三、拷贝构造函数

    理解拷贝构造函数和构造函数的关系,作用是什么及什么时候会调用,默认拷贝构造函数的行为,什么时候需要自己定义。

    作用:

    拷贝构造函数,属于构造函数的一种,作用是用来拷贝生成新的对象。当用已有的对象去拷贝生成一个新的对象时,编译器默认调用此函数。

    拷贝是什么操作?

    1. Date d1(2022,8,11);
    2. Date d2(d1);
    3. Date d3 = d1;
    4. int i = 0;
    5. int i2(i); // C++
    6. int i3 = i; // C

    如上下面两种都是拷贝的操作,都会调用拷贝构造函数。都是拷贝操作。

    语法与细节:

    1. 拷贝构造函数是构造函数的一种重载形式,只是参数上有些特殊,语法上遵循构造函数。
    2. 拷贝构造函数只有一个形参,即类类型对象的const &,即 (const X& x)   (结合拷贝构造的操作去理解这里的参数。
    3. 在用已存在的类类型对象拷贝构造新对象时,由编译器自动调用,如果拷贝构造函数设为delete,则拷贝操作将无法编译通过。

    为什么参数必须是类对象的&,和const

    1. void func1(Date d) {
    2. d.Print();
    3. }
    4. void func2(Date& d2) {
    5. d2.Print();
    6. }
    7. int main()
    8. {
    9. // TestDate1();
    10. //TestDate2();
    11. Date d1(1,2,3);
    12. func1(d1);
    13. return 0;
    14. }

    这段代码中,func1是传值传参,而func2为传引用传参。 调用func1时会进行拷贝构造,用实参d1拷贝构造d,而func2传引用传参,不会进行拷贝构造,因为d2是实参的别名。

    那么,如果拷贝构造函数的参数是传值调用,即Date(const Date d),那么拷贝构造新对象时,就需要先将实参拷贝给拷贝构造函数的形参d,这又需要调用拷贝构造函数,就会陷入无限递归。所以!拷贝构造函数的参数必须为&类型,并且这样做还更高效!   

    我们只是需要被拷贝对象的值,来创建一个新的对象,并不会对其进行修改,所以,一般会将其定为const。   即 Date(const Date& d) {}   这样可以防止修改被引用的对象。

    默认拷贝构造函数的操作:

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

    内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

    所以,如上Date类型,默认拷贝构造函数即可。

    而对于Stack类型,需要我们主动编写。

    1. Stack(const Stack& s) :_capacity(s._capacity),_top(s._top)
    2. {
    3. this->_array = (DateType*)malloc(sizeof(DateType)*_capacity);
    4. memcpy(this->_array,s._array,sizeof(DateType)*_top);
    5. cout<<"Stack(const Stack& s)"<
    6. }

    如果不主动编写,则对于数据成员DateType*  _array  来说,它本身指向的是堆区开辟的空间,默认拷贝构造函数对于指针这种内置类型来说,进行字节序的直接值拷贝,也就是浅拷贝,使得新创建对象的_array也指向这块堆区内存。 

    那么,1. 改变st1 就会影响st2 因为指向的是同一块内存    2. 当两个对象销毁时析构函数会对同一块堆区内存free/delete两次,直接导致程序崩溃。

    1. class QueueByStack
    2. {
    3. public:
    4. // .....
    5. private:
    6. Stack _st1;
    7. Stack _st2;
    8. int _size;
    9. };

    再比如QueueByStack ,成员Stack完成了拷贝构造函数,则它的默认拷贝构造函数拷贝Stack类型数据成员时,就会调用Stack的拷贝构造函数,所以默认的就可以完成工作。

    什么时候需要自己编写拷贝构造函数:

    综上,类一旦自己申请资源时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 (还要看类具体的操作)

    拷贝构造函数使用情景

    1. Date func(Date d) {
    2. Date ret = d;
    3. return ret;
    4. }

    这个函数,对于传参时,实参会拷贝构造出形参d, 然后形参拷贝构造出ret,ret返回时,并不是将ret这个对象返回回去,而是拷贝构造出一个临时对象,作为返回值。  这些地方都会有拷贝构造函数调用。

    所以,为了避免拷贝构造函数调用而影响效率,传参时能传引用就传引用。 对于函数内的堆区对象,能传引用返回就传引用返回。  (上方的ret是局部对象,不能传引用返回,因为函数调用结束后,ret就随之销毁了)


    这里还有一些知识点和细节,比如 : 传值传参和传引用传参 ,传值返回和传引用返回,还有编译器对于拷贝构造函数和赋值运算符的优化问题。   最后这个问题需要重载赋值运算符的知识。还有关于深拷贝如何操作,等之后再写新的blog吧。

    四、重载拷贝赋值运算符函数

    重载赋值运算符需要先学习重载运算符


    重载运算符函数

    理解:

    重载运算符是为了提高代码的可读性,可以让类的使用者像使用内置类型一样使用类对象。
    重载哪些运算符需要看类的实际情况和需求,看这个运算符在类对象之间使用是否有意义。
    这根本上也是一个函数,只是函数名有些特殊而已。

    语法与规则:

    1. 函数名为operator+运算符,返回值参数列表都和普通函数类似
    2. 重载运算符函数必须有一个类类型参数

    3. 不能重载不存在的参数,比如@
    4. .*  ::  sizeof  ?:  . 这五个运算符不能重载
    5. (重要)重载运算符函数可以作为类的成员函数,也可以定义在类外,作为全局函数。如果重载运算符函数作为类的成员函数,则会多一个隐藏的this指针,则这个运算符的左操作数默认为类对象。 而如果作为全局函数,就需要全部的操作数,比如+运算符就需要两个参数。

    如,Date类重载+ 运算符

    1. Date operator+(int day) const; // 作为Date类的成员函数时
    2. Date operator+(const Date& d, int day); // 作为全局函数时
    3. // 定义省略了
    1. void TestDate5() {
    2. Date d1(2022,8,11);
    3. Date d2 = d1+2;
    4. Date d3 = d1.operator+(2); // 等同于
    5. d2.Print();
    6. d3.Print();
    7. }
    1. void TestDate6() {
    2. Date d1(2022,8,11);
    3. Date d2 = d1 + 2;
    4. Date d3 = operator+(d1,2); // 等同于
    5. d2.Print();
    6. d3.Print();
    7. }

    上面两个函数分别为,operator+ 作为Date的成员函数和全局函数时的情况。都是可以直接date + x的,只是编译器转换之后的函数形式略有不同而已。因为无论全局函数,还是成员函数,编译器都可以正确转换+运算符为对应函数。

    后面紧跟的是编译器处理后的,也就是直接函数调用。

    (补一句话,函数定义不能在.h文件中,因为在多个cpp文件中展开会造成重定义,所以应该声明在.h  定义在.cpp  否则就在.h中声明为inline,如果声明为inline函数,则在cpp文件中展开之后,inline函数在函数调用处直接转换为函数代码,不会重定义。

    重载拷贝赋值运算符函数

    理解功能,与拷贝构造的区别,什么时候需要自己实现。 细节就是参数和返回值啥的。重点是如何实现细节,最后这个重点日后再说。

    简介与功能

    重载赋值运算符函数,属于默认函数的一种,也就是我们不实现时,编译器自动实现默认版本。
    作用是:进行类对象的赋值操作,比如

    1. Date d1(2022,8,11);
    2. Date d2(2003,3,1);
    3. Date d3(d1); // 拷贝构造
    4. Date d4 = d1; // 拷贝构造
    5. d2 = d1; // 赋值,调用重载赋值运算符函数

    什么时候是拷贝构造,什么时候是赋值?

    拷贝构造是从无到有,创建对象的情况,只是以另一个对象作为一个基准。
    赋值是已存在的对象赋值给已存在的另一个对象。   
    主要区别是:是否创建新的对象。

    语法与细节:

    1. 参数类型:const T&,传递引用可以提高传参效率。(const加强保护,传值也可以,但是低效)

    2. 返回类型:T& ,  提高效率,且内置类型的赋值也是返回左值。

    3. 函数内,需要检测是否是自赋值的情况。

    4. 赋值运算符函数必须定义为成员函数,因为如果定义为全局函数,则编译器会自动给类定义一个默认重载赋值运算符函数,进行赋值时就会产生歧义。

    编译器默认生成的重载赋值运算符函数

    以值的形式逐字节拷贝。

    对于内置类型,是直接赋值的。 对于自定义类型,会去调用它的重载赋值运算符函数。

    什么时候需要自己实现重载赋值运算符函数

    a.  Date类,不需要,因为数据成员都是内置类型,且并非指针指向申请的空间,而是存储数据。默认重载赋值运算符函数就可以完成浅拷贝。

    b. Stack类,需要,因为指针数据成员指向堆区申请的空间,如果浅拷贝,则会发生多个对象的指针数据成员指向同一块内存,这里的情况类似于拷贝构造函数。

    1. Stack& operator=(const Stack& s) {
    2. if(&s!=this) {
    3. free(this->_array);
    4. this->_array = (DateType*)malloc(sizeof(DateType)*s._capacity);
    5. memcpy(this->_array,s._array,sizeof(DateType)*s._top);
    6. this->_capacity = s._capacity;
    7. this->_top = s._top;
    8. }
    9. cout<<"Stack& operator=(const Stack& s)"<
    10. return *this;
    11. }

    注意这里需要进行自赋值的检验,否则自赋值时将会出错。(测试过了,函数是正确的) 

    c.  QueueByStack类,不需要,因为自定义类型数据成员已经实现了赋值运算符重载。自己的默认函数会去调用其自定义类型成员的赋值运算符函数。

    我记得Primer里面有句话,是如果一个类需要自定义析构函数,则一般都需要定义拷贝构造和赋值运算符。  这些是相关联的。主要是看类有没有在堆区申请空间。

    重载<<   >>运算符

    前言

    我们的cout << int1 << double2 << endl;   其实就是cout对象所属的ostream类重载了<<运算符。   而cin >> i 是因为cin所属的类istream重载了>>运算符。(istream 和 ostream 是类型,cin cout分别是他俩类型的对象,定义在iostream头文件中,是一个全局对象。

     可以看到,ostream类对于各种基本类型都做了重载,才可以利用cout方便地输出内置类型。

    那么,我们如何让自己定义的类也可以用<< >>运算符进行输入输出呢? 我们无法去ostream类成员函数中加一个参数为Date对象的重载函数,所以,我们只能在我们这里进行运算符重载。

    重载<< >>函数必须定义为全局的,不能定义在类内。

    如果定义在类内:

    1. ostream operator<<(ostream& os) {
    2. os << this->_year <<"/"<<this->_month <<"/"<<this->_day<
    3. return os;
    4. }
    1. void TestDate7() {
    2. Date d1(2022,8,11);
    3. // cout<
    4. d1<// right
    5. d1.operator<<(cout); // right
    6. }

    所以 ,你应该明白为什么不能定义为成员函数吧? 就是因为this指针,使得类对象默认是左操作数

    全局重载<<  >>运算符函数:

    1. inline ostream& operator<<(ostream& os, const Date& d) {
    2. os<"年"<"月"<"日"<
    3. return os;
    4. }
    5. inline istream& operator>>(istream& is, Date& d) {
    6. is >> d._year >>d._month >>d._day;
    7. assert(Date::CheckDate(d._year,d._month,d._day));
    8. return is;
    9. }

    这样才可以 cout << d1 << endl;   有关这里的inline,是需要理解的,是因为这个函数使用频繁,且定义在了.h文件中,防止链接时重定义!

    注意点:

    1. 参数为ostream& 是因为cout 和 cin 不能拷贝, 不加const是因为,要改变cout 和 cin 

    2. 返回值为ostream& istream& 是因为进行连续输出或输入。 即 cout << d1 << i <

    五,六 重载&运算符函数

    最后的两个默认函数,不写编译器会自动添加

    1. Date* operator&() {
    2. return this;
    3. }
    4. const Date* operator&() const {
    5. return this;
    6. }

    所以,我们之前的&x 的操作其实都隐式地使用了运算符重载,因为这些运算符根本上,类对象是不可以使用的。只是因为编译器帮助我们实现了。  (不是重点

  • 相关阅读:
    搭建自己的搜索引擎之二
    通信网络从4G升级到5G,核心网融合至关重要
    dll动态链接库及ocx activex 控件regsvr32注册失败 解决方法(Win10)
    国内主机整车EEA架构汇总
    微信朋友圈全新玩法,轻松互动,引爆你的社交圈
    如何加快发明专利的审查时间
    ubuntu云服务器怎么做好初始安全设置
    OSG第三方库编译之三十五:libzip编译(Windows、Linux、Macos环境下编译)
    laravel+vue2 element 一套项目级医院手术麻醉信息系统源码
    【机器学习】机器学习创建算法第3篇:K-近邻算法,学习目标【附代码文档】
  • 原文地址:https://blog.csdn.net/i777777777777777/article/details/126269467