【C++】揭秘类与对象的内在机制(核心卷之运算符重载、赋值重载与取址重载的奥秘)

【C++】揭秘类与对象的内在机制(核心卷之运算符重载、赋值重载与取址重载的奥秘)
在这里插入图片描述


文章目录

这篇博客是接着之前的博客写的,希望大家可以先看看之前的两篇博客,了解了解类的默认成员函数,并且还有深浅拷贝等学习赋值重载的预备知识,也在我的之前博客有详细讲解,这里给出链接,如下: 【C++】揭秘类与对象的内在机制(核心卷之深浅拷贝与拷贝构造函数的奥秘)

一、前置知识—运算符重载

在C++中,运算符可以像函数一样进行重载,因为运算符的本质类似于函数,比如加操作,相当于加就是那个函数,左右操作数就是它的参数,其中一元操作符只有一个操作数,那么这个函数相当于就只有一个参数,依次类推
所以在C++中,运算符可以像函数一样进行重载,让同一个运算符根据操作数的不同调用不同的运算符函数,实现多态的效果,但是语法内置的那些运算符我们不能重载,比如运算符为" + ",操作数为两个整型,这样的例子就不能重载,整数加整数是确定的,不能自己去更改
但是如果运算符同样是 “ + ”,操作数却是两个类类型相加,或者是一个类类型和一个整型相加,那么就可以重载,因为类类型是程序员自己定义的,属于自定义类型,语法本身就不会规定自定义类型的相加规则,同时编译器也不知道怎么加,所以这个时候就需要我们重载运算符
可能一些还没有学过运算符重载的同学就会问,这个重载有什么意义呢?什么类类型可以和整型相加并且有意义呢?答案很简单,我们平常写的日期类不就可以和整型相加吗,也就是日期加上一个整数,比如2025年1月1日 + 5 = 2025年1月6日,所以类和整型之间是可以进行相加操作的
那么上面我们大只知道了运算符重载是什么,有什么用,接下来我们就来学习怎么写运算符重载,首先我们先列出运算符重载的一些规则,可能有点多,如果不想看的话可以直接跳过,我会在下面举例讲解,到时候会按照举例的顺序让大家去看,因为这里光看概念可能看不懂,但是我们还是列举一下,方便后面举例的时候直接让大家查找相应的规则,如下:
1. 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错
2. 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成,例如日期类和整型加号的重载为Date operator+(Date& d1, int day),可以看到,它和其他函数⼀样,具有返回类型和参数列表以及函数体,返回值需要程序员自己根据逻辑确定
3. 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数
4. 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个,比如重载+运算符,如果是成员函数时,左操作数默认就是this,也就是当前对象,不需要写出来,只需要写和当前对象做运算的另一个对象,只有一个参数
5. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致,同时不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@
6. 有5个操作符不能重载,常常在选择题考到,如: .*(点星) 、 ::(域访问限定符) 、sizeof运算符 、 ?:(唯一的三目操作符) 、 .(成员访问运算符)
7. 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,否则就乱套了,如:int operator+(int x, int y)
8. ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,日期和日期相减就是日期相隔的天数,但是重载operator+就没有意义,日期和日期相加无意义
9. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分,所以C++规定,后置++重载时,增加⼀个int形参,而前置++则没有参数,最终跟前置++构成函数重载,⽅便区分
10. 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout,不符合使⽤习惯和可读性,重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象,我们后面举例讲解
以上基本上就是所有运算符重载相关的知识点了,实在有点多,这里我们举例子讲解,可以不用管上面的那些概念,我会让大家按照举例的顺序去查找,到时候看起来就要方便和顺畅多了
首先要强调的点是运算符重载怎么写,由于这个在第2点做了详细举例,所以可以光去看看第2点即可,其次要强调的就是,不能重载内置类型的运算符,也就是上面的第7点,至少要有一方是类类型,否则会报错,那么我们就结合运算符重载的规则写一写内置类型的运算符重载,如下:
#include<iostream>usingnamespace std;intoperator+(int x,int y){return x + y;}intmain(){return0;}
按照我们上面说的,这里重载了内置类型的运算符,最终应该会报错,我们来看看最终运行结果是不是这样,如下:
在这里插入图片描述
可以看到我们确实不能重载内置类型的运算符,那么正确的运算符重载我们该怎么写呢?这里我们还需要知道另一个规则,就是第3、4点,其中第3点中我们知道了,如果重载的运算符中有两个操作数,那么左边的那个参数就是左操作数,右边的那个参数就是右操作数
第4点告诉我们,如果重载的运算符是成员函数,由于成员函数的特殊性,默认它的第一个参数就是this指针,虽然没有显示写出来,但是运算符重载还是将它算进去了,所以如果重载的运算符是成员函数,默认第一个参数就是this指针,也就是当前对象,我们只需要传第二个参数即可
那么接下来我们知道了上面的知识,就可以真正来实现一个日期类的运算符重载了,这里我们选择日期类的相等运算符重载(不是赋值),我们来分析分析它的返回值和参数
首先返回值肯定是bool类型的,也就是最后返回是否相等,参数就是两个日期类对象,由于第一个参数是当前对象的this指针,不用传参,所以只需要传另一个日期类对象过来即可,判断两个日期相不相等也很简单,只要年月日都相等,那么两个日期就相等,否则不相等,那么我们接下来就按照这个思路来自己实现一下日期类的相等运算符重载,如下:
#include<iostream>usingnamespace std;classDate{public://为了方便理解,还是显示写出this指针//默认构造Date(int year =2025,int month =1,int day =1){this->_year = year;this->_month = month;this->_day = day;}//日期类的相等运算符重载函数//this指针占据了第一个操作数的位置,所以直接传第二个操作数即可//注意小细节,加引用可以减少拷贝booloperator==(Date& d){//this指向的对象是第一个操作数,d是第二个操作数returnthis->_year == d._year &&this->_month == d._month &&this->_day == d._day;}private:int _year;int _month;int _day;};
上面就是我们日期类的相等运算符的重载函数,是不是一点都不难呢?只需要按照逻辑上的需求写对应的代码即可,接下来又有一个难点,就是我们写好了对应的运算符重载函数,该怎么使用呢?首先是第一个方法,这运算符重载不是个成员函数吗?我们就按照成员函数的调用方法对它进行调用即可,如下:
intmain(){//调用默认构造初始化(25年1月1日) Date d1;//自己传参调用构造 Date d2(2025,1,9);bool ret = d1.operator==(d2);if(ret) cout <<"两个日期相等"<< endl;else cout <<"两个日期不相等"<< endl;return0;}
这里我们完全使用了成员函数的调用方法来调用这个运算符重载函数,我们来看看代码的运行结果,如下:
在这里插入图片描述
可以看到这个方法确实可行,但是还是存在很大的问题,就是这个方法太复杂了,太麻烦了,我们本身就是重载的运算符,希望像运算符一样使用它,那么是否可以这样用呢?我们来测试一下:
intmain(){//调用默认构造初始化(25年1月1日) Date d1;//自己传参调用构造 Date d2(2025,1,9);//bool ret = d1.operator==(d2);bool ret = d1 == d2;if(ret) cout <<"两个日期相等"<< endl;else cout <<"两个日期不相等"<< endl;return0;}
在上面的代码中,我们将之前的函数调用方式,直接换成了使用相等运算符的方式,我们看看代码是成功运行还是报错,如下:
在这里插入图片描述
可以代码居然成功运行了,效果和我们使用成员函数调用方式一样,没错,只要是我们重载的运算符,我们都可以按照原本运算符的方式直接使用,因为在汇编层这里的d1 == d2会被编译器自动转换成成员函数的调用,不信的话我们来看看
首先按f10进入调试模式,然后对着d1 == d2这条语句右击,在快捷菜单中选择转到反汇编,我们就可以看到这条语句其实被转化成了成员函数的调用,如下:
在这里插入图片描述


在这里插入图片描述
可以看到d1 == d2这条语句在汇编层变成了对应运算符重载函数的调用,我们方便了,是因为编译器悄悄帮我们做了很多事
那么上面就是运算符重载的一些基础知识,其实我们还有很多规则没有讲到,这些其它的规则都需要结合具体的实例来讲解,我后面会带大家完整地实现一下日期类的所有方法,到时候我们会涉及大量的运算符重载,基本上上面没讲过的规则都会讲到,敬请期待吧!
既然我们运算符重载的基础知识讲解完了,我们接下来还是继续回到我们类和对象的重点上,继续讲解赋值重载这个默认成员函数,这里再强调一下,赋值重载除了涉及到运算符重载,还涉及到深浅拷贝的相关知识,希望可以先看看我的上一篇文章再接着往下看

二、赋值重载

赋值重载函数也是一个类的默认成员函数,因为当我们不写赋值重载时,编译器会默认生成一个赋值重载给我们使用,在这个部分我们除了要搞清楚赋值重载的作用和写法,我们还要和拷贝构造以及相等运算符重载作区分
接下来我们还是按照编译器默认生成的赋值重载函数能干什么?怎么写赋值重载函数两个大方面来讲解赋值重载,最后再来讲讲怎么区分拷贝构造和赋值重载

默认生成的赋值重载函数能干什么?

这里我们直接说结论,对于内置类型来说,编译器默认生成的赋值重载函数会帮我们完成浅拷贝方式的赋值,将目标对象的值拷贝到另一个对象中去,可以认为内部是一个字节一个字节进行拷贝的,对于自定义类型来说,会调用它自己的赋值重载,跟拷贝构造差不多
深浅拷贝我们上一篇文章讲过,这里就不再赘述,我们主要是要知道什么情况下这个默认生成的赋值重载够用,什么时候需要自己写,还是两个技巧:
技巧一就是:看有没有写析构函数或者有没有写拷贝构造,如果写了就要写我们的赋值重载完成深拷贝,否则就不需要,例如Stack类需要自己写,而Date类不需要
技巧二就是:看有没有内置类型的成员变量指向堆上面的空间,不用看自定义类型,因为自定义类型有它自己的赋值重载,只需要看看有没有内置类型的成员变量指向堆上面的空间,如果有,那么我们就需要自己写赋值重载以完成深拷贝,否则就不需要,例如Stack类需要自己写,而Date类不需要
接下来我们就分别用Date类和Stack类来验证一下我们默认生成的赋值重载是否如我们上面所说,首先是Date类,如下:
classDate{public://默认构造Date(int year =2025,int month =1,int day =1){this->_year = year;this->_month = month;this->_day = day;}private:int _year;int _month;int _day;};intmain(){ Date d1(2025,1,5); Date d2; d2 = d1; d1.Print(); d2.Print();return0;}
上面我们就使用了默认的赋值重载将d1赋值给了d2,应该默认会实现浅拷贝,应该没有问题,我们来看看代码运行结果:
在这里插入图片描述
可以看到代码没有问题,原本的d2使用了默认构造,是25年1月1日,我们将d1赋值给d2后,d1就变成了25年1月5日,接下来我们来测试一下Stack类使用默认生成的赋值重载会发生什么,测试代码如下:
classstack{public://默认构造stack(int n =10){this->_arr =(int*)malloc(n *sizeof(int));if(this->_arr ==nullptr){perror("malloc");return;}this->_top =0;this->_capacity = n;}//析构~stack(){if(this->_arr)free(this->_arr);this->_top =this->_capacity =0;}//拷贝构造(深拷贝)stack(const stack& st){this->_arr =(int*)malloc(st._capacity *sizeof(int));if(this->_arr =nullptr){perror("malloc");return;}for(int i =0; i < st._top; i++){this->_arr[i]= st._arr[i];}this->_top = st._top;this->_capacity = st._capacity;}voidpush(int x){this->_arr[this->_top++]= x;}private:int* _arr;int _top;int _capacity;};intmain(){ stack st1; st1.push(2); st1.push(3); st1.push(4); stack st2; st2 = st1;return0;}
按照我们的分析,上面的Stack类的赋值重载需要深拷贝,而编译器默认生成的赋值重载是浅拷贝,会导致同一空间被析构两次,最终导致程序运行出错,那么我们就来运行这段代码试试,看看跟我们的预期是否一样,如下:
在这里插入图片描述
可以看到确实如我们所料,程序崩溃了,间接证明了我们确实需要自己写一个深拷贝的赋值重载,那么上面就是对编译器默认生成的赋值重载函数的介绍,接下来我们的重点还是怎么写一个赋值重载函数,其实也不难,只要学了深浅拷贝和拷贝构造就会发现很简单,我们一起来学习吧!

怎么写赋值重载函数

当我们要手动写赋值重载函数的时候一般都是深拷贝的场景,在例如Stack这种类中,如果没有深拷贝方式的赋值重载,只有浅拷贝就会导致两个问题,一个问题是两个对象的某个成员都指向同一块空间,一个修改会造成另一个也跟着修改,另一个问题就是这块空间会被析构两次,这在深浅拷贝部分说过,这里也就不再多说了
所以当我们碰到类似于Stack这种类时,必须手动写一个赋值重载函数来完成深拷贝,由于赋值重载也属于运算符重载函数,所以写起来的方式和我们上面讲的运算符重载函数差不多,内容也不难,就是完成深拷贝,新开一段空间,然后再进行数据的拷贝
但是我们在写赋值重载之前,最好还是再来学习一下赋值重载的两三个独有的特点,不是很难,但是很关键,需要我们记住,如下:
1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数,同时赋值运算重载的参数建议写成const 当前类类型引⽤,既可以避免权限的放大,又可以减少拷贝
2. 需要注意的是,stack类有返回值,千万不要忘记了这一点,并且建议写成当前类类型的引⽤,引⽤返回可以不生成临时变量,从而提⾼效率,有返回值⽬的是为了⽀持连续赋值场景
3. 要注意判断是否是给自己赋值,如果是给自己赋值,导致的问题有两个,首先第一个问题就是给自己的_arr重新开辟空间,此时_arr中存放的都是随机值,自己给自己赋值后还是随机值,第二个问题就是,之前_arr指向的空间还没有被释放_arr就指向另一块空间了,导致内存泄漏,所以需要特殊判断一下是不是给自己赋值
上面就是我们在写赋值重载时需要额外注意的点,接下来我们就按照上面的特点再次完善一下我们的stack类,写出赋值重载,如下:
classstack{public://默认构造stack(int n =10){this->_arr =(int*)malloc(n *sizeof(int));if(this->_arr ==nullptr){perror("malloc");return;}this->_top =0;this->_capacity = n;}//析构~stack(){if(this->_arr)free(this->_arr);this->_top =this->_capacity =0;}//拷贝构造(深拷贝)stack(const stack& st){this->_arr =(int*)malloc(st._capacity *sizeof(int));if(this->_arr ==nullptr){perror("malloc");return;}for(int i =0; i < st._top; i++){this->_arr[i]= st._arr[i];}this->_top = st._top;this->_capacity = st._capacity;}//赋值重载(深拷贝),返回当前对象的引用 stack&operator=(const stack& st){//如果this等于st的地址,说明是同一个对象,不用进行赋值操作if(this!=&st){this->_arr =(int*)malloc(st._capacity *sizeof(int));for(int i =0; i < st._top; i++){this->_arr[i]= st._arr[i];}this->_top = st._top;this->_capacity = st._capacity;}return*this;}voidpush(int x){this->_arr[this->_top++]= x;}private:int* _arr;int _top;int _capacity;};intmain(){ stack st1; st1.push(2); st1.push(3); st1.push(4); stack st2; st2 = st1;return0;}
在上面我们就自己写了一个赋值重载,内部的逻辑和拷贝构造差不多,外部的语法结构和运算符重载那里讲的一样,所以总体而言难的应该是上面列出的3点赋值重载的特点,那么我们接下来调试一下这段代码,看看我们写的赋值重载是否能解决深拷贝的问题,如下:
在这里插入图片描述
可以看到我们自己写的赋值重载起到作用了,成功完成了赋值的任务,这也就是我们赋值重载怎么写的全部内容了,是不是很简单呢?接下来我们来学习一下怎么区分拷贝构造和赋值重载,如果没有深刻的理解很容易绕进去

怎么区分拷贝构造和赋值重载

拷贝构造和赋值重载有时候长得很像,我们要学会区分它们,接下来给大家一段代码,回答注释里面的两个问题,如下:
intmain(){ stack st1; st1.push(2); st1.push(3); st1.push(4);//《1》这里是拷贝构造还是赋值重载? stack st2 = st1;//《2》这里是拷贝构造还是赋值重载? stack st3; st3 = st1;return0;}
我们先不着急说答案,先来分析分析拷贝构造和赋值重载就能轻易解决这个问题,首先我们要知道什么是拷贝构造,我们在拷贝构造部分讲过,拷贝构造是一种特殊的构造函数,而构造函数的作用就是在对象被创建时对它进行初始化,所以拷贝构造也是如此,它是在创建对象时被调用,所以很明显第一题答案是拷贝构造,因为我们在创建对象st1
那么对象的赋值又是什么呢?这里的赋值是针对一个已经创建好的对象而言的,我们将源对象赋值给一个已经创建出来的目标对象,例如上面第二问中,我们就是先创建好一个st3对象,然后将st1赋值给它,这里就会调用赋值重载,可以自己调试一下
所以综上,当我们在创建对象时,使用另一个对象对它进行初始化就是拷贝构造,当我们对已经创建好的一个对象进行赋值时,调用的就是赋值重载

二、取地址重载

两个取地址重载函数是所有默认成员函数中最简单的,取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,它们分别可以给普通对象和const对象取地址,关键是编译器默认生成的两个取地址重载函数已经够我们平常使用了,我们基本上不需要自己写,那么什么时候需要我们写呢?
它们运用在⼀些非常非常特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现取地址重载函数,返回空指针或者胡乱返回⼀个地址,如下例:
//取地址重载(普通类对象) stack*operator&(){//原本该返回当前对象的地址//但是现在我们不希望外部轻易拿到对象的地址,于是返回空指针returnnullptr;}//取地址重载(cosnt类对象) stack*operator&()const{returnnullptr;}intmain(){ stack st1; stack* ps =&st1;return0;}
接下来我们来测试一下这段代码是否会按照我们预期进行,也就是取不到当前对象的地址,只能取到空指针,如下:
在这里插入图片描述
可以看到确实如我们所料,现在其他人就不能轻易取到我们对象的地址了,这差不多就是取地址重载自己写的一种应用,但是又极为特殊,如果我们没有这种特殊需求,那么直接用编译器默认生成的取地址重载即可,这也是它们简单的原因
那么截止目前为止我们终于讲完了类的六大默认成员函数,整整三篇不低于8000字的博客,可见这几个默认成员函数有多难缠,这也是C++难入门的原因之一,但是好在我们已经挺过来了,下一篇文章我们就可以融汇贯通一下这几篇文章,实现一个自己的日期类,敬请期待吧!
bye~

Read more

DeepSeek-R1是真码农福音?我们问了100位开发者……

DeepSeek-R1是真码农福音?我们问了100位开发者……

从GitHub Copilot到DeepSeek-R1,AI编程工具正在引发一场"效率革命",开发者们对这些工具的期待与质疑并存。据Gartner预测,到2028年,将有75%的企业软件工程师使用AI代码助手。 眼看着今年国产选手DeepSeek-R1凭借“深度思考”能力杀入战场,它究竟是真码农福音还是需要打补丁的"潜力股"? ZEEKLOG问卷调研了社区内来自全栈开发、算法工程师、数据工程师、前端、后端等多个技术方向的100位开发者(截止到2月25日),聚焦DeepSeek-R1的代码生成效果、编写效率、语法支持、IDE集成、复杂代码处理等多个维度,一探DeepSeek-R1的开发提效能力。 代码生成效果:有成效但仍需提升 * 代码匹配比例差强人意 在代码生成与实际需求的匹配方面,大部分开发者(58人)遇到生成代码与实际需求完全匹配无需修改的比例在40%-70%区间,12人遇到代码匹配比例在70%-100%这样较高的区间。 然而,有30人代码匹配比例低于40%。这说明DeepSeek-R1在代码生成方面有一定效果,但在部分复杂或特定场景下,仍有很大的提升空间。

By Ne0inhk
AI+游戏开发:如何用 DeepSeek 打造高性能贪吃蛇游戏

AI+游戏开发:如何用 DeepSeek 打造高性能贪吃蛇游戏

文章目录 * 一、技术选型与准备 * 1.1 传统开发 vs AI生成 * 1.2 环境搭建与工具选择 * 1.3 DeepSeek API 初步体验 * 二、贪吃蛇游戏基础实现 * 2.1 游戏结构设计 * 2.2 初始化游戏 * 2.3 DeepSeek 生成核心逻辑 * 三、游戏功能扩展 * 3.1 多人联机模式 * 3.2 游戏难度动态调整 * 3.3 游戏本地保存与回放 * 3.4 跨平台移植 * 《Vue.js项目开发全程实录/软件项目开发全程实录》 * 编辑推荐 * 内容简介 * 作者简介 * 目录 一、

By Ne0inhk
[DeepSeek] 入门详细指南(上)

[DeepSeek] 入门详细指南(上)

前言 今天的是 zty 写DeepSeek的第1篇文章,这个系列我也不知道能更多久,大约是一周一更吧,然后跟C++的知识详解换着更。 来冲个100赞兄弟们 最近啊,浙江出现了一匹AI界的黑马——DeepSeek。这个名字可能对很多人来说还比较陌生,但它已经在全球范围内引发了巨大的关注,甚至让一些科技巨头感到了压力。简单来说这 DeepSeek足以改变世界格局                                                   先   赞   后   看    养   成   习   惯  众所周知,一篇文章需要一个头图                                                   先   赞   后   看    养   成   习   惯   上面那行字怎么读呢,让大家来跟我一起读一遍吧,先~赞~后~看~养~成~习~惯~ 想要 DeepSeek从入门到精通.pdf 文件的加这个企鹅群:953793685(

By Ne0inhk
DeepFace深度学习库+OpenCV实现——情绪分析器

DeepFace深度学习库+OpenCV实现——情绪分析器

目录 应用场景 实现组件 1. 硬件组件 2. 软件库与依赖 3. 功能模块 代码详解(实现思路) 导入必要的库 打开摄像头并初始化变量 主循环 FPS计算 情绪分析及结果展示 显示FPS和图像 退出条件 编辑 完整代码 效果展示 自然的 开心的 伤心的 恐惧的 惊讶的  效果展示 自然的 开心的 伤心的 恐惧的 惊讶的   应用场景         应用场景比较广泛,尤其是在需要了解和分析人类情感反应的场合。: 1. 心理健康评估:在心理健康领域,可以通过长期监控和分析一个人的情绪变化来辅助医生进行诊断或治疗效果评估。 2. 用户体验研究:在产品设计、广告制作或网站开发过程中,通过观察用户在使用过程中的情绪反应,来优化产品的用户体验。 3. 互动娱乐:在游戏或虚拟现实应用中,根据玩家的情绪状态动态调整游戏难度或故事情节,以增加沉浸感和互动性。

By Ne0inhk