【C++】C++ 重载深度解析:赋值运算与取地址运算符,你真的会了吗?

【C++】C++ 重载深度解析:赋值运算与取地址运算符,你真的会了吗?
在这里插入图片描述
🔭 个人主页:散峰而望

《C语言:从基础到进阶》《编程工具的下载和使用》《C语言刷题》
《C++》《算法竞赛从入门到获奖》《人工智能》《AI Agent》

愿为出海月,不做归山云


🎬博主简介

在这里插入图片描述
请添加图片描述

【C++】C++ 重载深度解析:赋值运算与取地址运算符,你真的会了吗?


前言

在C++中,运算符重载是一项强大的特性,允许开发者自定义运算符的行为,使其适用于用户定义的类型。赋值运算符(operator=)和取地址运算符(operator&)是其中两个关键的重载目标,但它们的实现细节和潜在陷阱往往容易被忽视。

赋值运算符的重载不仅涉及深拷贝与浅拷贝的问题,还需处理自赋值安全性及资源管理的正确性。而取地址运算符的重载则较少被讨论,但其在智能指针、代理类等高级场景中扮演重要角色,尤其是结合const成员函数的特性时,能够提供更灵活的语义控制。

本文将从基础语法到实际应用,深入解析这两种运算符的重载机制,帮助开发者规避常见错误并掌握其高效用法。

1. 赋值运算符重载

1.1 运算符重载

  • 当运算符被用于类类型的对象时,C++ 语言允许我们通过运算符重载的形式指定新的含义。C++ 规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
  • 运算符重载是具有特殊名字的函数,其的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致
  • 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@
  • .*::sizeof?:. 注意以上 5 个运算符不能重载
#include<iostream>usingnamespace std;// 编译报错:“operator +”必须至少有一个类类型的形参 //int operator+(int x, int y)//{// return x + y;//}classA{public:voidfunc(){ cout <<"A::func()"<< endl;}};typedefvoid(A::* PF)();//成员函数指针类型 intmain(){// C++规定成员函数要加&才能取到函数指针  PF pf =&A::func; A obj;//定义ob类对象temp // 对象调用成员函数指针时,使用.*运算符 (obj.*pf)();return0;}
  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}//private:int _year;int _month;int _day;};// 重载为全局的面临对象访问私有成员变量的问题 // 有几种方法可以解决: // 1、成员放公有 // 2、Date提供getxxx函数 // 3、友元函数 // 4、重载为成员函数 booloperator==(const Date& d1,const Date& d2)//参数要有类类型{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}intmain(){ Date d1(2026,3,10); Date d2(2026,3,11);// 运算符重载函数可以显示调用 operator==(d1, d2);// 编译器会转换成 operator==(d1, d2);  d1 == d2;return0;}
  • 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如 Date 类重载 operator- 就有意义,但是重载 operator+ 就没有意义。
  • 重载 ++ 运算符时,有前置 ++ 和后置 ++ ,运算符重载函数名都是operator++ ,无法很好的区分。C++ 规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分。
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}booloperator==(const Date& d){return _year == d._year && _month == d._month && _day == d._day;} Date&operator++(){ cout <<"前置++"<< endl;//...return*this;} Date operator++(int){ Date tmp; cout <<"后置++"<< endl;//...return tmp;}private:int _year;int _month;int _day;};intmain(){ Date d1(2026,3,10); Date d2(2026,3,11);// 运算符重载函数可以显示调用  d1.operator==(d2);// 编译器会转换成 d1.operator==(d2);  d1 == d2;// 编译器会转换成 d1.operator++(); ++d1;// 编译器会转换成 d1.operator++(0);  d1++;return0;}
  • 重载 << 和 >> 时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象 << cout ,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。
classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}private:int _year;int _month;int _day;};void Date::operator<<(ostream& out){ out << _year << _month << _day << endl;}intmain(){ d1 << cout; d1.operator<<(cout);return0;}
在这里插入图片描述

因为存在隐藏的 this 指针,即第一个参数(this指针)是左操作数,函数的显示参数是右操作数,将 operator<< 错误地实现为 Date 类的成员函数,因此需要改成全局函数。

voidoperator<<(ostream& out,const Date& d){ out << d._year << d._month << d._day << endl;}

1.2 赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象

Date d1(2026,3,10); Date d2(2026,3,11);//赋值重载 d1 = d2;//拷贝构造 Date d3(d2); Date d4 = d2;

赋值运算符重载的特点:

  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝;
//d1 = d2//d是d2的别名,d1用this指针voidoperator=(const Date& d){ _year = d._year; _month = d._month; _day = d._day;}
  1. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。有返回值主要是为了实现连续赋值。
Date&operator=(const Date& d){ _year = d._year; _month = d._month; _day = d._day;return*this;}
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}Date(const Date& d){ cout <<" Date(const Date& d)"<< endl; _year = d._year; _month = d._month; _day = d._day;}voidoperator=(const Date& d){ _year = d._year; _month = d._month; _day = d._day;}private:int _year;int _month;int _day;};intmain(){ Date d1(2026,3,10); Date d2(d1); Date d3(2026,3,11); Date d4 = d3; d1 = d2 = d3;//报错:E0349 没有与这些操作数匹配的 "=" 运算符 return0;}
  1. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
  2. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像 Stack 这样的类,虽然也都是内置类型,但是 _a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载,也不需要我们显示实现 MyQueue 的赋值运算符重载。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要
#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}Date(const Date& d){ cout <<" Date(const Date& d)"<< endl; _year = d._year; _month = d._month; _day = d._day;}// 传引用返回减少拷⻉ // d1 = d2; Date&operator=(const Date& d){// 不要检查自己给自己赋值的情况 if(this!=&d){ _year = d._year; _month = d._month; _day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是*this return*this;}voidPrint(){ cout << _year <<"-"<< _month <<"-"<< _day << endl;}private:int _year;int _month;int _day;};intmain(){ Date d1(2026,3,10); Date d2(d1); Date d3(2026,3,11); d1 = d3;// 需要注意这里是拷贝构造,不是赋值重载  Date d4 = d1;return0;}

2. 取地址运算符重载

2.1 const成员函数

将 const 修饰的成员函数称之为 const 成员函数,const 修饰成员函数放到成员函数参数列表的后面

const 实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改

const 修饰 Date 类的 Print 成员函数,Print 隐含的 this 指针由 Date* const this 变为 const Date* const this

#include<iostream>usingnamespace std;classDate{public:Date(int year =1,int month =1,int day =1){ _year = year; _month = month; _day = day;}// void Print(const Date* const this) constvoidPrint()const{ cout << _year <<"-"<< _month <<"-"<< _day << endl;}private:int _year;int _month;int _day;};intmain(){// 这里非const对象也可以调用const成员函数是一种权限的缩小  Date d1(2024,7,5); d1.Print();const Date d2(2024,8,5); d2.Print();return0;}
void Print(const Date* const this) const第一个 const(Date* 前) :修饰指针指向的对象,表示 this 指针指向的 Date 对象是常量。在函数内部,不能通过 this 指针修改该对象的成员变量;第二个 const(Date* 后) :修饰指针本身,表示 this 指针本身是常量指针,其指向不可修改,即 this 指针重新指向其他对象。(C++ 中 this 指针默认就是常量指针,这里显示强调其不可修改性);第三个 const(函数末尾) :修饰成员函数,表示这是一个常量成员函数。在该函数中,不能修改调用对象的任何成员变量,保证函数不会意外改变对象状态。

2.2 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和 const 取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

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

不想让别人取到当前类对象的地址时,可以返回空,也可以返回任意地址。

//返回空classDate{public: Date*operator&(){returnnullptr;}const Date*operator&()const{returnnullptr;}private:int _year ;// 年 int _month ;// 月 int _day ;// 日 };
//返回任意地址classDate{public: Date*operator&(){return(Date*)0x2631FF10;}const Date*operator&()const{return(Date*)0x2631FF20;}private:int _year ;// 年 int _month ;// 月 int _day ;// 日 };

结语

运算符重载是C++强大的特性之一,合理使用可以提升代码的可读性和灵活性。赋值运算符重载需特别注意资源管理和自赋值问题,而取地址运算符重载常用于特殊场景如代理模式或智能指针实现。理解其底层机制能帮助开发者编写更安全高效的代码。

愿诸君能一起共渡重重浪,终见缛彩遥分地,繁光远缀天



Read more

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true 引言 在开发过程中,我们常常使用集成开发环境(IDE)如 IntelliJ IDEA 或 JetBrains DataGrip 来与数据库进行交互。然而,有时可能会遇到无法连接数据库的情况,尤其是当使用新版的 IDEA 或 DataGrip 时。这种问题通常是由于网络配置或者 IDE 与数据库之间的兼容性问题引起的。 一种常见的解决办法是添加 JVM 参数 -Djava.net.preferIPv4Stack=true,以优先使用 IPv4 协议栈。这种方式能够有效解决因 IPv6 配置问题导致的数据库连接失败问题。本文将详细介绍如何通过修改 IDEA 或 DataGrip 的启动参数来解决这个问题。 文章目录 * 如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.

By Ne0inhk
Java Web 开发:JSON 基础 + @Test 测试 + Cookie/Session/ 请求处理

Java Web 开发:JSON 基础 + @Test 测试 + Cookie/Session/ 请求处理

个人主页:♡喜欢做梦 欢迎  👍点赞  ➕关注  ❤️收藏  💬评论 目录 编辑 🍍JSON的概念  🍐概念  🍐@Test注解 🍑什么是@Test? 🍑与JSON关联 🍑@Test标记的方法与main方法的区别  🍍JSON语法  🍐核心数据类型  🍐常见使用 🍑对象 🍑数组  🍑JSON字符串和Java对象的互转 🍑传递JSON 🍑获取URL中的参数 🍑上传文件:@RequestPart  🍍Cookie和Seeion  🍐Cookie 🍑什么是Cookie? 🍑Cookie的获取  🍐Session 🍑什么是Session?  🍐Cookie和Session之间的关系 🍑Session的存储 🍑Session的获取 🍍获取header 🍍JSON的概念  🍐概念 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。他基于JavaScript的一个子集,但采用了独立语言的文

By Ne0inhk
Java 部署:跨环境部署(开发 / 测试 / 生产配置隔离)

Java 部署:跨环境部署(开发 / 测试 / 生产配置隔离)

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Java部署这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * Java 部署:跨环境部署(开发 / 测试 / 生产配置隔离) 🚀 * 为什么需要跨环境配置隔离?🤔 * Spring Boot 中的 Profile 机制:最常用的隔离方案 🌈 * 1. 创建 Profile 特定的配置文件 * 示例:通用配置(application.yml) * 示例:开发环境配置(application-dev.yml) * 示例:生产环境配置(application-prod.yml) * 2. 激活 Profile * 方式一:启动参数 * 方式二:环境变量 * 方式三:application.

By Ne0inhk
Java 大视界 -- 基于 Java 的大数据分布式存储在科研数据管理与共享中的创新应用(418)

Java 大视界 -- 基于 Java 的大数据分布式存储在科研数据管理与共享中的创新应用(418)

Java 大视界 -- 基于 Java 的大数据分布式存储在科研数据管理与共享中的创新应用(418) * 引言: * 正文: * 一、科研数据管理的核心痛点与 Java 分布式存储的适配性 * 1.1 科研数据管理的四大核心痛点(2023 年科研机构调研数据) * 1.2 Java 大数据分布式存储的适配优势(对比 Python/Go 生态) * 1.3 核心技术选型:Java 生态下的三大分布式存储方案 * 二、Java 分布式存储在科研数据管理中的三大创新应用场景 * 2.1 场景一:生命科学领域 —— 基因测序数据的分布式存储与共享 * 2.1.1 架构设计 * 2.1.2 核心代码:基于 Java 的基因数据

By Ne0inhk