当前位置: 首页 > article >正文

C++ Learning 函数重载•引用

函数重载

函数重载是 C++ 中的一种特性,它允许在同一个作用域中定义多个同名的函数,只要它们的参数列表(参数的数量或类型)不同。函数重载提供了根据不同的输入调用同一个函数名称的能力,从而使代码更加简洁和可读。

函数重载的基本概念

函数重载允许在同一个作用域中定义多个函数,这些函数具有相同的名称,但它们的参数列表不同。编译器会根据函数调用时传递的参数来决定调用哪个函数。

#include <iostream>

void print(int i) {
    std::cout << "Integer: " << i << std::endl;
}

void print(double d) {
    std::cout << "Double: " << d << std::endl;
}

void print(const std::string& str) {
    std::cout << "String: " << str << std::endl;
}

int main() {
    print(10);         // 调用 print(int)
    print(3.14);       // 调用 print(double)
    print("Hello");    // 调用 print(string)
    return 0;
}

在这个例子中,print 函数被重载了三次:一个接受整数、一个接受浮点数、一个接受字符串。根据传递的参数类型,编译器会选择适当的重载版本。

函数重载的规则

C++ 中函数重载的规则:

  • 参数数量不同:如果函数参数的数量不同,编译器可以根据参数的个数来决定使用哪个重载。
  • 参数类型不同:如果函数参数的类型不同,编译器可以根据参数类型来选择适当的重载。
  • 参数顺序不同:如果函数参数的类型相同,但顺序不同,也可以进行重载。
  • 返回类型不能作为重载的区分依据:C++ 不允许仅通过返回类型不同来重载函数,函数的返回类型不能作为重载的依据

如何实现函数重载

函数重载是由 C++ 编译器在编译时通过名称修饰(Name Mangling)来实现的。编译器会在内部对函数的名称进行修改,以便能够区分不同的重载版本。

为了了解编译器是如何处理这些重载函数的,我们反编译下上面函数重载示例的代码,看下汇编代码。

由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面使用了g++演示这个修饰后的名字。

名称修饰是 C++ 编译器对函数名称进行修改的过程,它会将函数的名称与其参数的类型信息结合起来,生成一个唯一的标识符。这样即使多个函数有相同的名称,编译器仍然能够区分它们。

  1. 编译 C++ 程序

先将 C++ 源代码编译成目标文件:

g++ -c overload.cpp

这将生成 overload.o

  1. 查看名称修饰

使用 nm 命令查看目标文件中的符号表,显示修饰后的函数名称:

nm overload.o

会看到以下内容
在这里插入图片描述

可以发现编译之后,重载函数的名字变了不再都是print!这样不存在命名冲突的问题了。

但又有新的问题了——变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?

第一反应是:函数名+参数列表,因为函数重载取决于参数的类型、个数,而跟返回类型无关。

具体解释如下:

  • _Z 是 GCC 中的前缀,表示这是一个 C++ 名称修饰符。
  • 5 是函数名 print 的长度。
  • print 是函数的名称。
  • d 表示参数的类型(d 代表 double

总结::在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。

详细解释见C++的函数重载

C语言为什么不支持函数重载

在 C 语言中,函数的调用是通过其符号名称来确定的。链接器通过函数的名称查找符号表,并将正确的函数与调用代码链接起来。

如果按照gcc编译上述文件会得到

在这里插入图片描述

结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。故无法支持函数重载、

extern "C"关键字

在 C++ 中,可以使用 extern "C" 关键字将函数按照 C 语言的方式来编译,这样 C++ 编译器就会使用 C 的名称修饰规则(也叫名称链接规范)。这可以确保 C++ 编译器不会对函数名称进行 C++ 的名称修饰,而是按照 C 语言的标准方式处理函数。

主要用于:

  • 与 C 代码互操作:例如,当 C++ 代码需要调用 C 库或者将 C++ 函数暴露给 C 代码时。
  • 避免名称修饰:C++ 会对函数名进行名称修饰(mangling),使用 extern "C" 可以让函数按照 C 的方式进行链接,保持函数名不变

使用 extern "C" 来声明和定义函数:

extern "C" {
    void foo(int a);
}

总结:C++ 可以通过 extern "C" 关键字将函数按照 C 的方式来编译和链接。extern "C" 会禁用 C++ 的名称修饰,并允许 C++代码与 C 代码进行兼容和互操作。

引用

引用是一个别名,它是某个变量的直接引用。在声明时,引用必须与一个已有的变量绑定,并且一旦绑定后就不能再改变指向其他对象。

语法:

type& reference_name = existing_variable;
  • type:引用所绑定的变量类型。
  • &:表示引用的符号。
  • reference_name:引用变量的名字。
  • existing_variable:要引用的现有变量。

引用特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体后,不能再引用其他实体

使用场景

传递引用作为参数

通过引用传递参数,函数可以直接操作原始数据,而不是它的副本,这样不仅提高了效率,还可以使函数修改调用者的变量。

void increment(int& num) {
    num += 1;  // 直接修改调用者的变量
}

int main() {
    int x = 5;
    increment(x);  // x 被修改为 6
    std::cout << x;  // 输出 6
}

传递常量引用

常量引用(const 引用)允许通过引用传递参数,但保证不会修改传入的对象。这对传递大的对象(如大数组或大类对象)很有用,因为常量引用避免了不必要的复制,同时保持对象不变。

void print(const std::string& str) {
    std::cout << str << std::endl;
}

int main() {
    std::string text = "Hello, World!";
    print(text);  // 通过引用传递,避免复制
}

引用作为函数返回值

引用可以作为函数的返回值,这样函数可以返回一个对象的别名,避免了复制操作。但是要注意返回的引用应该指向有效的对象,否则会导致未定义的行为。

int& getElement(int* arr, int index) {
    return arr[index];  // 返回数组元素的引用
}

int main() {
    int arr[] = {10, 20, 30};
    int& ref = getElement(arr, 1);
    ref = 40;  // 修改 arr[1] 的值为 40
    std::cout << arr[1];  // 输出 40
}

返回局部变量的引用是一个常见的错误,容易导致未定义行为。因为局部变量在函数结束时会被销毁,如果返回该变量的引用,那么返回的引用将指向一个已经被销毁的内存区域。

错误示例:

int& badFunction() {
    int localVar = 10;
    return localVar;  // 错误,返回一个局部变量的引用
}

int main() {
    int& ref = badFunction();  // 引用指向已销毁的变量
    std::cout << ref;  // 未定义行为,可能会出现一个随机值
}

为什么不行?

  • localVarbadFunction 函数的局部变量,它的生命周期只在函数内部有效。
  • badFunction 返回时,localVar 会被销毁,因此返回的引用指向一个已经无效的内存区域(悬空引用)。

正确做法

如果你需要返回一个引用,确保返回的是一个静态存储区的对象(例如,全局变量、静态变量或者堆上的对象)。

总结避免返回局部变量的引用:局部变量在函数退出时会被销毁,返回局部变量的引用是危险的。

传值、传引用效率比较

传值

传值是将函数参数的副本传递给函数。这意味着函数内部对参数的修改不会影响外部传入的值。

传值的特点

  • 内存开销:当传递的参数较大(如大数组、大对象)时,传值会创建该对象的副本,这会占用额外的内存。拷贝的过程可能会消耗较多的时间,尤其是当对象较大时。
  • 开销: 传值时需要进行 拷贝构造移动构造。如果拷贝构造函数比较复杂,传值就会比较慢,尤其是涉及大数据结构时。
  • 修改局限:由于传值只是传递副本,函数内部对参数的修改不会影响原始数据。

传引用

传引用是将参数的引用传递给函数,函数内部操作的就是原始对象而非副本。因此,传引用的方式通常比传值更高效,尤其是在传递大型对象时。

传引用的特点

  • 内存开销:传引用时,只传递一个指向原始对象的指针(引用实际上是一个隐式的指针)。这样不需要额外的内存分配,避免了拷贝的开销。
  • 效率高:传引用避免了拷贝构造的开销,特别适用于传递大型对象,如大型数组、容器、类等。
  • 修改影响:函数内部对引用参数的修改会直接影响到外部的原始对象。

总结

  • 小型数据类型:对于小型数据类型(如 intfloat 等),传值和传引用的性能差异不大。可以根据需求选择。
  • 大型对象:对于大型对象,传值会涉及昂贵的拷贝构造,而传引用不会。因此,传引用通常更高效,特别是当你传递的是大型数据结构或类对象时。

引用和指针的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

int main() {
	int a = 10;

	int& ra = a;
	ra = 20;

	int* pa = &a;
	*pa = 30;
	return 0;
}
	int& ra = a;
00007FF6D7472B14  lea         rax,[a]  
00007FF6D7472B18  mov         qword ptr [ra],rax  
	ra = 20;
00007FF6D7472B1C  mov         rax,qword ptr [ra]  
00007FF6D7472B20  mov         dword ptr [rax],14h  

	int* pa = &a;
00007FF6D7472B26  lea         rax,[a]  
00007FF6D7472B2A  mov         qword ptr [pa],rax  

由此可见引用和指针的底层实现是一样的。

对比

特性引用指针
实现原理引用通常通过指针实现明确地使用指针来存储地址
内存占用引用本身不占用额外的内存指针本身需要占用内存来存储地址
是否可以为空不能为空可以为空(nullptr
是否可以修改指向不可以修改引用所指向的对象可以随时修改指针指向的对象
语法语法简洁,直接使用引用需要使用解引用操作符 * 或者 &
能否指向临时对象可以,尤其是常量引用不可以

http://www.kler.cn/a/428185.html

相关文章:

  • Python设计模式 - 组合模式
  • Python练习(2)
  • 《HelloGitHub》第 106 期
  • MyBatis 框架:简化 Java 数据持久化的利器
  • 【go语言】结构体
  • Spring MVC 综合案例
  • PyTorch基本使用——张量的索引操作
  • opencv光流法推测物体的运动
  • Spring Boot日志:从Logger到@Slf4j的探秘
  • ChatGPT 最新推出的 Pro 订阅计划,具备哪些能力 ?
  • uniapp 微信小程序webview 和 h5数据通信
  • 【AWS re:Invent 2024】一文了解EKS新功能:Amazon EKS Auto Mode
  • Python实现BBS论坛自动签到【steamtools论坛】
  • Python 入门教程(2)搭建环境 | 2.4、VSCode配置Node.js运行环境
  • 如何利用DBeaver配置连接MongoDB和人大金仓数据库
  • django 实战(python 3.x/django 3/sqlite)
  • AI by Hand:手搓 AI 模型
  • git遇见冲突怎么解决?
  • Dell电脑安装Centos7问题处理
  • Python Virtualenv 虚拟环境迁移, 换新电脑后 Python 环境快速迁移 Virtualenv 环境配置管理,实测篇
  • 黑马程序员Java项目实战《苍穹外卖》Day12
  • 十六、大数据之Shell 编程
  • 第四十一天 ASP应用 HTTP.sys 漏洞 iis6文件解析漏洞和短文件漏洞 access数据库泄露漏洞
  • L-BFGS 方法实现
  • Hive 中 Order By、Sort By、Cluster By 和 Distribute By 的详细解析
  • 流量转发利器之Burpsuite概述(1)