• c++之类与对象


    目录

    一 类的引入

    类的声明和定义

    类的实例化

    类的对象的存储模型

    this指针

    关于类的成员变量的命名规范

    访问限定符

    类的作用域

    二 默认成员函数

    1 构造函数

    2 析构函数

    3 拷贝构造函数

    4 赋值重载

    5 取地址运算符的重载


    一 类的引入

    之前提到c++的三大特征:封装,继承和多态。

    c++中的封装主要是通过类与对象来实现的。因此通过类与对象的了解,我们可以深刻地体会c++中的封装的特性。

    c++是对c语言的改进,其实类也是如此,是针对结构体的改进。结构体只能定义变量,如果要定义相关函数的实现的话,需要定义在结构体外面,单独写成函数,并且命名的时候需要指明操作的对象。

    但是不同于c语言,c++中的类还可以定义函数。并且习惯用class来标识一个类。类主要由成员函数和成员变量构成。这样把函数定义在类的体内,就和类产生了联系。相关的操作也只是针对这个类来进行操作的。

    类的声明和定义:

    主要有两种主要的方式,一种是声明和定义分离,分别写在.h与.cpp文件中,这样做的好处是方便阅读代码,了解概况。但是需要注意的是,如果这样操作的话,定义函数的时候需要声明类的作用域

    对于.h文件

    1. void Date::Init(int year = 1, int month = 1, int day = 1)
    2. {
    3. _year = year;
    4. _month = month;
    5. _day = day;
    6. }

    对于.cpp文件

    1. #include
    2. using namespace std;
    3. class Date
    4. {
    5. int _year;
    6. int _month;
    7. int _day;
    8. public:
    9. void Init(int year=1, int month=1, int day=1);
    10. };

    还有一种方式是声明和定义写在一起。如果这样子处理,对于符合内联函数要求的函数,编译器会自动当成内联函数来处理。

    1. #include
    2. using namespace std;
    3. class Date
    4. {
    5. int _year;
    6. int _month;
    7. int _day;
    8. public:
    9. void Init(int year=1,int month=1,int day=1)
    10. {
    11. _year = year;
    12. _month = month;
    13. _day = day;
    14. }
    15. };

    一般情况下是采用第一种的。但是两种可以结合。比如对于需要被频繁调用的函数可以定义在类的体内提高效率。

    类的实例化:

    需要注意的是,区分类内成员函数和变量的声明和定义主要是根据是否给对应的函数或者变量开辟了空间来确定的。

    举个例子,上述Date类的函数和变量都是声明,只有用类实例化一个对象的时候,才算是在内存上开辟了相应的空间成为了定义。

    比如下面这段代码才算是定义:

    1. int main()
    2. {
    3. Date d1;
    4. Date d2;
    5. }

     类和对象的区别:

    类是对对象抽象的描述,相当于模型,定义类的时候并没有分配实际的内存空间。

    实例化的对象则占用了实际的物理空间,并且存储类成员变量。

    类的对象的存储模型

    这里有一个误区:可能有人会以为只有当类被定义了,在内存上开辟了相应的空间了,才能去计算大小。

    但是即使只有类的定义,也能够计算出类的大小。类类比于结构体,有一套内存对其的规则,可以预先设计大概的大小,来计算大小。

    对于成员变量,根据内存对齐规则来处理就行了。主要是成员函数该怎么计算呢?他会不会被存储在类内?

    我们一起来验证一下吧

     

     

    通过结果我们很容易就发现,类中的函数是不会在存储在类内的。如果对应的成员函数存储在类内,包含成员函数的类的大小至少要比没有对应成员函数的类大4个字节。因为要通过指针类存储对应的类。但是实际上通过编译器运行的结果,有没有成员函数,类的大小都不会改变。

    那么类中成员和函数是怎么进行存储的呢?

    不存储成员函数也不存储对应的指针,而是把对应的成员函数都存放在公共代码区(代码段)

    类内只存储对应的成员变量。

     

    此时对象和函数之间没有关联了,那么调用的时候我是如何去找到对应的函数?

    编译链接的时候已经生成了对应的函数,并且根据函数名去公共代码区确定了函数的地址,不是在运行的时候创建的。

    我们在调用这个函数的时候,在编译链接的时候找到对应的函数,函数直接call函数地址找到对应的函数。

     

    那么为什么还需要对象来调用成员函数?

    1 成员函数属于对象的域。相当于告诉编译器,这个函数是属于类的成员函数,寻找对应的函数的时候,去对应的类的公共代码区里面找。

    2 与后文提到的this指针有关系。

     

    所以这样的一段代码既不会运行崩溃,也不会编译报错,而是正常运行的。

    调用func的时候,没有解引用,并没有去对象里面寻找对应的函数。

    是存在公共代码区的。直接去公共代码区里面寻找,并不会引起空指针的问题。

    了解了类的存储模型是这样的。那我们不妨思考一下,为什么类没有采取其他的存储模式呢?

    比如把成员函数和成员变量都存在类中。

    对于这个我认为,如果成员函数也存储在类中的话,之后去实例化不同的对象,来调用对应的成员函数。那么每个对象中都会保存一份对应的函数代码,相同的代码被多次实现,造成了空间的浪费。所以不会采取这种方式。

    那么我们不禁又思考:空类和类中只存储成员函数的类,他们的大小是多少呢?

     

    通过程序执行的结果可以知道,二者都是1。实际上是为了占位,在内存中存1字节大小,来唯一标识这一块空间,实现区分二者的目的。

    this指针

    就上述的类的对象的存储模型来看,由于函数都被保存在公共代码区。那么通过对象来调用函数的时候,就产生了一个问题:怎么区分类中相应的成员函数到底作用于哪个对象呢?

    1. Date d1;
    2. Date d2;
    3. d1.Init(2022, 7, 26);
    4. d2.Init();

    在思考这个问题的时候,我们需要了解:对应的成员函数传入的参数首先不可能是类中的成员变量。因为他仅仅是个声明,甚至都没有存储在对应的物理空间里。那么如何初始化?完成初始化肯定是需要已经定义的变量来完成的。

    这时就引入了this指针的概念。

    this指针是用来标识传入的对应的对象的地址的。由于对象的成员函数存储在公共代码区,因此去调用不同的对象的函数的时候,都是在同一块内存空间上调用的。但是有一个this指针指向这个实例化的对象,用这个实例化的对象来分别初始化对应的变量。就可以实现区分了。

     

     

    对于普通的非静态的成员函数来说,第一个接收变量的位置是用来存储this指针的。拿上面的代码举例子,上述代码定义类的成员函数的时候会被编译器处理成:

    void Init(int year = 1, int month = 1, int day = 1)//void Init(Date* const this,int year = 1, int month = 1, int day = 1)

    对于这个指针的属性,他是由const来修饰的,就代表这个指针所指向的对象不能被修改。举个形象的例子,对于d1的初始化,他只能指向d1;对于d2,他也只能指向d2.但是指针所指向的对象的内容并没有被const修饰,是可以修改的哦!

    对于调用该函数的地方会被处理成:

    1. Date d1;
    2. Date d2;
    3. d1.Init(2022, 7, 26);//(&d1,2022,7,26)
    4. d2.Init();//(&d2,1,1,1)

     

    注意:语法上规定,this指针不能显示地传递和接收,也就是不能在定义函数或者调用函数的时候直接写出。因为它是由编译器自动确定和生成的。但是你可以在函数内部使用它。(即使你不使用,编译器也会自动带上就是了)

    那么this指针存储在哪里呢?

    一般情况下是存储在栈区的,因为他是一个形参。但是对于vs这个编译器来说,当this指针被经常访问的时候,编译器会自动优化,把this指针放在寄存器中,来提高效率。是否优化取决于编译器。

    了解了this指针的原理之后,也就能解释下面程序为什么运行崩溃了。

    1. class Date
    2. {
    3. int _year;
    4. int _month;
    5. int _day;
    6. public:
    7. void Init(int year = 1, int month = 1, int day = 1)//void Init(Date* const this,int year = 1, int month = 1, int day = 1)
    8. {
    9. _year = year;
    10. _month = month;
    11. _day = day;
    12. }
    13. void PrintYear() { cout << _year << endl; }
    14. };

    1. int main()
    2. {
    3. Date* p = nullptr;
    4. p->PrintYear();
    5. }

     因为访问_year这个成员变量他是要通过this指针解引用来访问的。而此时this指针是个空指针,对空指针怎么能解引用呢?自然而然导致运行崩溃了。

    关于类的成员变量的命名规范

    通过上述的代码段,我们发现在定义类的成员变量的时候,我添加了_。

    其实这样做主要是为了初始化的时候区分。

    C++中成员变量怎么命名呢?

    1 驼峰法

    单词和单词之间首字母单词大写间隔法 GetYear

    函数名类名等所有单词首字母大写

    变量首字母小写,后面的单词首字母大写

    成员变量首单词前面+_

    2 _

    单词全部小写 单词之间用下划线分割

     Get_year

    3 m_

    m是member的意思。意思是成员变量。

    个人命名比较倾向于第一种方式

    访问限定符

    基于上述的给出的一个日期类的声明和定义,我们发现在完成这个类的时候添加了public这样的访问限定符。

    访问限定符是通过权限选择性的访问来提供接口供给外部的用户来使用。

    也就是说,访问限定符主要限定类外对类内成员或者函数进行访问,对类访问内不管限定符是什么都没有影响。这也是类的封装性的体现。

    访问限定符的分类:private(私有),public(共有),public(保护)。类内默认的访问权限是private。

    私有和保护在类外不能直接被访问,除非通过使用成员函数的方式来返回值。

    共有在类外是可以被使用的。

    访问限定符的访问权限到下一个限定符或者类的定义结束的位置}处结束。

    类的作用域

    作用域限定符::

    作用域限定的跟编译器的搜索规则有关。其实是告诉编译器去哪里寻找对应的函数。

    类内的成员函数如果在类外定义的话一定要加上类作用域限定符。因为在类外编译器默认先去局部域寻找对应函数,再去全局域寻找对应的函数,最后去命名空间域里面寻找。如果不加类名限定符,编译器默认会把他当成普通的函数。

    所以如果有类内类外同名的函数,但是由于在不同的内存区域中,编译链接是能够通过的。

     与生命周期相区分

    一个函数或者变量的生命周期主要是和他在内存中存储的位置有关系的。

    二 默认成员函数

    C++基于类衍生出了六个默认成员函数。

    见其名,明其义。默认的意思就是,即使你不显示地在类内定义。编译器也会自动生成。

    看到这里你可能会说:既然我不写,编译器也会自动生成。那我写它有啥意义啊?

    针对不同的场景,默认构造函数有不同的用途,有些场景下,不写也不会有太大的问题。但是针对特定的场景,你就必须显示的给出了。怎么理解这些场景呢?接下来我会给大家介绍一下对应的默认成员函数的定义功能用途,让大家更好的理解。

     

    1 构造函数

    功能:完成对象的初始化

    注意:并不是在内存上开辟空间。在内存上开辟空间是由编译器来完成的。

    如何初始化:在类实例化对象的时候,自动实现初始化。

    特征:

    ①函数名和类名相同

    ②无返回值。即使是void也没有。因为void返回值是空,仍然是有返回值的。但是构造函数本身就不需要返回值。因为构造函数主要是完成对应的初始化工作,在类实例化的时候自动调用已经完成了对其的初始化,返回值此时没有什么意义。

    ③对象实例化的时候编译器自动调用对应的的构造函数。从汇编的角度来理解,对象实例化的时候,对应的对象会直接call相关的函数,来完成实例化。

    如下图:

    ④可以重载

    1. class Date
    2. {
    3. int _year;
    4. int _month;
    5. int _day;
    6. public:
    7. Date()
    8. {
    9. }//无参构造函数
    10. Date(int year, int month, int day)
    11. {
    12. _year = year;
    13. _month = month;
    14. _day = day;
    15. }//带参构造函数
    16. Date(int year=1, int month=1, int day=1)
    17. {
    18. _year = year;
    19. _month = month;
    20. _day = day;
    21. }//全缺省构造函数
    22. };

    但是需要注意的是,虽然无参构造函数和全缺省构造函数在语法上是可以同时存在的,但是实际调用过程中,如果实例化对象的时候都没有进行传参,那么编译器就无法判断到底调用哪个了,引起歧义。也就是所谓的二义性。所以这两个只能写一个。个人比较全缺省的构造函数,因为他十分好用。传参的时候非常灵活。

    默认构造函数

    特点:不用传参数就可以被自动调用

    类型:1 无参构造函数 2 全缺省构造函数 3 当我们没有显示定义的时候编译器自动生成的构造函数

    由于会存在调用时候的歧义 因此上述的默认构造函数只能存在一个。当我们显示写出的时候,编译器就不会自动生成默认构造函数了,因此不用考虑1或者2与3同时出现的场景。

    主要是考虑12同时出现,并且调用的时候都不传参数时导致的二义性。

     

    如何理解编译器自动生成的默认构造函数呢?

      调用不用传入参数。实例化的时候编译器就可以自动调用

      对于内置类型不作处理:int double char float 指针等(即使是自定义类型的指针,因为在编译器看来都是四个或者八个字节的地址本质上没有什么区别)

      对于自定义构造函数,当满足条件的时候,去调用自定义构造函数的默认构造函数,完成对自定义成员变量的初始化。

    需要满足的条件:默认构造函数只有当我们没有显示定义或者定义了相对应的无参或者全缺省的构造函数才会被调用。如果我们写了有参构造函数,那么此时是调不动编译器自动生成的对应的默认构造函数的。因为已经显示写出了对应的有参构造函数,就需要传参进行初始化。

    比如你写了一个这个构造函数,那么对应的你就调用不动对应的默认构造函数了。

    1. Date(int year)
    2. {
    3. _year = 1;
    4. _month = 1;
    5. _day = 1;
    6. }

    由于上述c++语法中对于内置类型不作处理,实际上是不太好的,受到了诟病。因此c++11中打了对应的补丁,即:如果你想通过编译器自动生成的默认构造函数来实现对内置类型的初始化,那么定义的时候带上对应的初识化值,即可在调用的时候被初始化了。

    1. class Date
    2. {
    3. int _year=1;
    4. int _month=1;
    5. int _day=1;
    6. public:
    7. Date(int year)
    8. {
    9. _year = 1;
    10. _month = 1;
    11. _day = 1;
    12. }
    13. };

    注意:此时是定义,不是初始化。本质上是对默认构造函数自定义类型进行的处理

    由于编译器自动生成的默认构造函数比较特殊,只对自定义类型做处理,并且如果想对内置类型同时处理仍然需要写出对应的值。

    因此我们日常在使用的时候,一般是显示写出默认构造函数来完成初始化的。写全缺省的默认构造函数。

    由于构造函数被频繁调用,因此在类内定义比较好。由编译器处理成inline类型的函数。

    2 析构函数

    析构函数的功能:完成对资源的清理。与构造函数相反。

    1. class Stack
    2. {
    3. int* _array;
    4. int _capacity;
    5. int _size;
    6. public:
    7. Stack()
    8. {
    9. malloc(……)
    10. }
    11. ~Stack()
    12. {
    13. free(_array);
    14. _array = nullptr;
    15. _capacity = _size = 0;
    16. }
    17. };

    理解对资源的清理:如上图。当我malloc一个空间,是在堆上开辟的一块空间。而main函数中的对象是在栈上的。如果函数生命周期结束,完成对对象栈空间的销毁后,堆空间仍然被占用。只能借助析构函数来完成对资源的清理。

    其实上述析构函数只用写第一行,后面两行主要是为了避免野指针问题,但是栈空间已经被销毁了,所以是访问不到对应的变量的。所以不用写也没关系。

    需要注意的是:析构函数不是完成对应空间的销毁。因为对应空间的销毁和函数的生命周期有关。对于局部变量来说,出了函数的作用域,函数对应的栈帧就会被操作系统回收,在栈帧上创建的变量也会被操作系统回收。静态变量也是同样的,在程序结束后销毁。因此对应对象地销毁是和函数的生命周期有关。生命周期结束后,由编译器自动销毁。

    特征:

    ①~表示与构造函数作用相反。

    ②无参数无返回值。和构造函数一样,无返回值同时也不是void的返回值。无参数就不能构成重载了。因为重载必须要求参数列表不同才可以构成重载。所以一个类的构造函数只能有一个。

    ③也可以不显示定义。那么当对象的生命周期结束,编译器自动调用析构函数完成资源的清理。

    系统调用的默认的析构函数做了什么呢?

    对于内置类型,不作处理。对于自定义类型,调用自定义类型的构造函数。

    那么什么情况下需要写出析构函数,什么情况又不需要呢?

    1 不需要:

    ①没有资源需要清理。即只涉及在栈上开辟对应的成员变量的空间。比如Date类实例化的对象。

    ②自定义类型,我们不显示写,编译器自动调用。比如用队列实现栈。如果已经写出了队列对应的析构函数了,那么栈的析构函数就不用显示去写了。因为对于栈来说,队列是一个自定义类型的成员变量。栈实现析构的时候,会去自动调用队列的已经写好的析构函数。

    2 需要写:当内置类型的成员变量在堆上开辟了空间,需要完成资源清理,比如malloc,new等。

     

     

    关于析构函数的顺序问题

    1. int main()
    2. {
    3. Date d1;
    4. Date d2;
    5. }

     

     

    1. Date d3;
    2. int main()
    3. {
    4. static Date d0;
    5. Date d1;
    6. Date d2;
    7. static Date d4;
    8. }

     

     

     

    1. Date d3;
    2. void test()
    3. {
    4. static Date d0;
    5. Date d1;
    6. Date d2;
    7. static Date d4;
    8. }
    9. int main()
    10. {
    11. test();
    12. test();
    13. }

     

     

    析构函数是在对象对应的空间被销毁的时候完成的资源清理工作。因此对象什么时候被销毁,析构函数什么时候发生作用。

    ①对于栈来说。他是自顶向下生长的。符合先进后出的性质。因此会先析构后入栈的对象。

    其实对于堆也是和栈差不多的

    ②对于构造函数,函数是一句一句往下执行的,上述程序先构造全局的变量,之后进入main函数体内依次执行对应的操作,构造0124;析构的时候,由于main中的12存储在栈中,栈被销毁了,21也依次被销毁。04存储在是存储在静态区的局部变量,3是存储在静态区的全局变量。只不过一个是局部的一个是全局的。析构的时候也和栈一样的顺序,后进先出,因此403依次析构。

    ③对于涉及main中再继续调用其他的函数

    出了test()函数,栈帧也被销毁,因此21也被销毁,但是04是在静态区的,第一次被定义的时候就在静态区存储着了。因此构造的时候不会重复去构造。构造函数的顺序就是上述顺序。

    同理,析构的时候也是先销毁对应的栈上的变量,2121,出了main函数,结束程序的时候再依次销毁静态区上的变量。

    以上涉及main函数中再调用函数,局部变量与静态变量与全局变量的析构。

    3 拷贝构造函数

    拷贝构造函数是构造函数的一种特殊形式。只不过初始化的形式是通过对对象的拷贝。他是构造函数的重载。

    特点:

    1 只有一个形参,并且这个形参应该是类的引用。如果是传值调用会引发无穷递归。

    举个例子:

    1. int main()
    2. {
    3. Date d1;
    4. Date d2(d1);
    5. return 0;
    6. }

     

    传值调用:本质上形参接收到的是实参的一份临时拷贝。对于对象来说,临时拷贝传的就是拷贝构造函数。如果用传值调用,那么需要接收一个拷贝构造函数,而这个拷贝构造函数的形成又需要接收一个拷贝构造函数。由此引发了无穷的递归。

    因此,传参的时候,要保证接收的参数要马上就能被使用,而不需要再额外的传参。那么可以有两种解决方法。

    • 传引用。传入的是一个别名。d1d2共用一块空间。
    • 传指针。使用的时候很奇怪。d2(&d1)

    所以拷贝构造函数一般要传引用

    由于传引用的话,会导致传入的参数也可能会被修改。可能会让本来需要接收d1的d2;来修改了原来d1的值,使他变成随机值。因此需要加上const修饰。权限缩小,确保传入的d1不会被修改

    除此之外,传引用对于较大的对象来说,可以节省资源的消耗,提高效率。

    2 如果我们不显示写出拷贝构造函数,那么编译器会默认自动生成一个拷贝构造函数。

    他会做什么?

    当我们不显示写出拷贝构造函数的时候

    对于内置类型,会实现值拷贝。按字节的方式,将需要拷贝的d1中的成员变量依次拷贝给d2.

    对于自定义类型,会去调用自定义类型的拷贝构造函数来完成拷贝。

    因此对于像日期类这样的函数,拷贝构造函数本质上是没有意义的。

    那么什么样的情况下需要我们去写一个拷贝构造函数呢?

    涉及malloc new fopen等的深拷贝

     

    浅拷贝是值拷贝。当拷贝的对象中含有指针对象的时候会引发两个问题;

    1 由于拷贝的本质上是引用,因此拷贝和本体并不是独立的,一个值的修改,会影响另一个值

    2 析构的时候 虽然capacity size array都是独立拷贝出的一份 但是array指向同一块空间,那么被析构的时候会析构两次,造成程序崩溃

    对于这种成员变量有指针类型的,需要进行深拷贝

    对于拷贝构造函数传值返回和传引用返回:

    • 对于传值返回来说,需要返回的是这个对象的拷贝构造函数,析构的时候也是去析构这一块的拷贝构造函数。这样一来,不仅需要额外开辟空间而且需要进行拷贝,产生消耗
    • 传引用返回直接返回

    综合上述因素考虑,当返回的是不会被销毁的变量,比如被static修饰的,那么可以提升效率,比传值返回好。

    4 赋值重载

    了解赋值重载之前,我们先了解一下运算符重载。因为赋值重载运用到了运算符重载的相关知识。

    其实运算符重载是一个函数,用来完成相关的功能的。

    运算符重载

    为什么会有运算符重载呢?

    对于内置类型,可以直接使用运算符运算,编译器知道要如何运算。

    但是自定义类型无法直接使用运算符,因为编译器不知道如何运算。

    因此对于类的对象的运算,需要自己对运算符赋予新的意义,来实现对应的功能。即运算符重载。

    运算符重载的声明和定义

    返回值类型+operator+需要重载的操作符+参数

    返回值的类型根据所实现的功能的实际意义来判断。参数顺序和运算符操作数的顺序是一致的。

    注意:

    ①不能用@来创建新的操作符

    ②必须有一个类类型参数

    ③用于内置类型的运算符不能改变对应的含义。因为运算符重载是在内置类型能够使用的基础上新增入类。所以要确保原先的内置类型是能够使用的。

    ④.*     ::     sizeof()    ? :     .不能重载

    如果定义在类外,由于需要访问到类中的私有成员变量来进行相关的操作,那么就需要GetYear();GetMonth();GetDay();等函数,通过函数的调用来实现类中私有成员的访问。比较麻烦,因此一般在类中定义相关的运算符重载。

    写一个关于判断日期相等的运算符重载函数。

    1. bool operator==(const Date& date)
    2. {
    3. return _year == date._year
    4. && _month == date._month
    5. && _day == date._day;
    6. }

    由于定义在类内部,自带了一个this类型的指针。所以参数会看上去少了一个。 

    1. cout << (d1 == d2) << endl;
    2. //cout<

    调用的时候,编译器是这样进行处理的。

    关于优先级:

    由于<< >>等的优先级特别高,因此编译器会认为你要先去输出d1的结果,再用这个结果和d2进行比较。

    但是实际上我们是要输出d1和d2比较的结果,因此需要带上括号。

    由于运算符重载本质上是这样子编译的,相当于是通过一段函数的代码来实现某种功能。如果使用函数的话,可读性比较差,受到命名规范等的影响。直接使用对应的运算符会增强可读性,因为运算符本身就具有某种含义。

    1. bool operator==(const Date& date1, const Date& date2)
    2. {
    3. return date1._year == date2._year
    4. && date1._month == date2._month
    5. && date1._day == date2._day;
    6. }
    7. int main()
    8. {
    9. cout << (d1 == d2) << endl;
    10. //cout<
    11. }

    运算符重载传入对象的话,会因为栈帧的创建和销毁造成效率上的降低。因此我们一般传引用。并且传引用就要考虑到可能会被修改的风险,因此需要带上const。

    所以运算符重载处理,编译器会自动认为是去调用相关的函数

    赋值运算符的重载

    与拷贝构造相区分。在d2被创建的时候,将d1的值拷贝给对完成初始化工作。这是拷贝构造。

    赋值运算。完成和拷贝类似的事情。不同点是此时d1,d3两个对象都是已经存在的,此时可以进行相互赋值。但是由于是针对类,不是内置类型,因此需要运算符的重载。

    1. int main()
    2. {
    3. Date d1(2022, 7, 26);
    4. Date d2=d1;//拷贝构造
    5. Date d3(2022, 7, 27);
    6. d1 = d3;//赋值重载
    7. }

    那我们来实现一下赋值函数的重载吧。

    返回值:由于要实现连续赋值,因此返回值应该是同对象的类型。比如日期类就是日期类型的。

    参数:如果传值会调用很多次的拷贝构造函数。因此传参的时候要用引用传参。

    其次,需要考虑考虑自己给自己赋值这种情况怎么解决。虽然没有实际意义,但是语法上是允许这样做的,因此我们也需要考虑。所以可以添加相应的条件判断。如果是不同对象分别赋值;如果是同一个对象,直接返回。 

    1. Date& operator=(const Date& date)
    2. {
    3. if (this != &date)
    4. {
    5. _year = date._year;
    6. _month = date._month;
    7. _day = date._day;
    8. }
    9. return *this;
    10. }

    注意:

    ①负值重载运算符只能写在类的内部,作为成员函数

    ②因为当不显式写出负值重载运算符,编译器会自动生成,完成的工作和拷贝构造函数类似。

    ③有时可能尽管使用了=但是仍然是拷贝构造

    ④只有两个都已经被初始化出来的对象使用=才是赋值。

    5 取地址运算符的重载

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

    由于const修饰的类的对象和普通的对象返回值上有差异。因此写成两个,构成函数的重载。

    但是我们不写的话,编译器也会自动生成,够用了

    一般情况下不需要自己写。

    只有这样的场景才要自己实现:

    不想让别人去到这个类型对象的地址,用const修饰,或者返回空指针,或者设置成私有。

    关于const的修饰问题

    this指针的类型是date*const 那么代表对应的对象不能被修改。但是有的时候,我们不希望对象的内容被修改。那么需要的是const Date* this的类型。这时候就可以在括号后面带上const来表示。就如同上面的代码一样。

    再举个例子:

    1. int main()
    2. {
    3. Date d1(2022, 7, 26);
    4. const Date d2(2022, 7, 26);
    5. d1.Print();//Date*
    6. d2.Print();//const Date*
    7. return 0;
    8. }

     上述这段代码,我们在传参的时候,打印d2是不能通过的。因为Print()是类中定义的成员函数,默认第一个参数的类型时 Date* const。上述这样子操作相当于把只读的权限放大成了可读可写的权限,是不可以的。

     

     

  • 相关阅读:
    mysql 死锁详细分析(三)
    MyBatis-Plus
    【java_wxid项目】【第十三章】【Elasticsearch集成】
    Android开发笔记——快速入门 (延迟初始与Viewbinding)
    Ubuntu server 24 (Linux) 普通用户不能sudo 也不能使用root登录 忘记root密码 修复解决方案
    开发中如何克服tomcat热部署弱的缺陷?看这篇文章就够了
    智汀智能家居知识普及篇——家居智能控制方式及控制系统的特点,你知道几个?
    记一次利用工具升级 redhat 7.5到bclinux8.2操作
    git分布式版本控制系统
    我的电子萝卜刀火了吗?
  • 原文地址:https://blog.csdn.net/zhengyawen666/article/details/125998575