问题

C/C++中按值传递比按地址传递更快吗, 引用呢?

回答
在 C/C++ 中,我们经常听到“按值传递”、“按地址传递”(也叫指针传递)和“引用传递”这几种参数传递方式。那么,它们在性能上到底有什么区别呢?哪种方式更快呢?这个问题看似简单,但深入分析起来,会涉及到内存、CPU 缓存、编译器优化等多个层面。

咱们先一个一个来聊聊。

1. 按值传递 (Pass by Value)

按值传递是最直观的传递方式。当你将一个变量按值传递给函数时,函数会为这个变量创建一个独立的副本。函数内部对这个副本的任何修改都不会影响到原始变量。

例子:

```c++
void func_by_value(int x) {
x = x 2; // 只修改了副本
}

int main() {
int a = 10;
func_by_value(a);
// a 的值仍然是 10
return 0;
}
```

性能分析:

开销: 当按值传递时,最直接的开销是需要将原始变量的值复制到函数的参数副本中。这个复制过程涉及内存读写操作。
数据类型大小的影响:
小数据类型(如 `int`, `float`, `char` 等基本类型): 这些类型通常占用很小的内存空间(通常是 4 或 8 字节)。复制它们非常快,开销几乎可以忽略不计。
大数据类型(如大型 `struct`、`class` 对象、数组、字符串等): 如果你传递的是一个非常大的对象,复制整个对象就需要更多的时间和内存。这可能会成为一个性能瓶颈,尤其是在频繁调用的函数中。
CPU 缓存: 即使是复制,如果数据很小,也可能很快地在 CPU 缓存中完成,速度也很快。但如果数据很大,超出缓存大小,就需要访问主内存,速度会慢一些。

结论(按值传递): 对于基本类型和小型数据结构,按值传递的性能非常好,开销极小。但对于大型数据结构,它可能不是最优选择,因为复制的开销会比较大。

2. 按地址传递 (Pass by Address / Pointer)

按地址传递,通常是通过传递指针来实现的。你传递的是变量的内存地址,函数通过这个地址来访问原始变量。这意味着函数可以修改原始变量的值。

例子:

```c++
void func_by_pointer(int ptr) {
if (ptr) { // 检查指针是否为空
ptr = ptr 2; // 通过指针修改原始变量
}
}

int main() {
int a = 10;
func_by_pointer(&a); // 传递变量的地址
// a 的值现在是 20
return 0;
}
```

性能分析:

开销:
传递地址: 传递一个指针的开销实际上是复制一个地址(通常是 4 或 8 字节,与指针的大小相同)。这个开销通常比复制一个大型对象要小得多。
解引用: 在函数内部,当你通过指针访问或修改值时,需要进行一次“解引用”操作(`ptr`)。这个操作涉及一次内存访问。
数据类型大小的影响: 按地址传递时,不论原始变量有多大,传递的都是一个固定大小的地址。因此,对于大型数据结构,按地址传递的开销(传递地址本身)通常远小于按值传递复制整个对象所需的开销。
潜在风险:
空指针: 如果传递了空指针(`nullptr`),解引用会导致程序崩溃。
野指针: 如果指针指向无效内存区域,也会导致问题。
可读性: 相比按值传递,使用指针的代码可读性可能稍差一些,需要时刻注意指针是否有效以及解引用操作。

结论(按地址传递): 对于大型数据结构,按地址传递通常比按值传递更快,因为它避免了昂贵的复制操作,只传递了一个地址。对于基本类型,传递地址和解引用也可能比按值传递多一点点开销(主要是解引用那一步),但差别通常非常微小。

3. 引用传递 (Pass by Reference)

引用传递是一种 C++ 特有的方式,它为原始变量创建了一个别名。函数内部对引用的操作实际上就是对原始变量的操作。引用在初始化时就必须绑定到一个变量,并且不能改变绑定的对象。

例子:

```c++
void func_by_reference(int& ref) {
ref = ref 2; // 直接修改原始变量
}

int main() {
int a = 10;
func_by_reference(a); // 传递变量的引用
// a 的值现在是 20
return 0;
}
```

性能分析:

底层实现: 在绝大多数情况下,编译器在底层会将引用传递“优化”成指针传递。也就是说,当你在函数签名中声明一个引用参数时,编译器实际上是在生成相应的汇编代码时使用指针来传递地址。
开销: 因此,引用的传递开销与按地址传递非常相似:传递一个地址,然后在函数内部通过地址进行操作。
与指针的区别:
语法糖: 引用提供了一种更简洁、更安全的语法。你不需要显式地去取地址(`&`)或解引用(``),代码看起来更像是在直接操作原始变量。
安全性: 引用在初始化时就必须绑定到有效的对象,不能为 `nullptr`。这避免了指针可能带来的空指针问题。
不能重新绑定: 一旦引用被初始化,它就永久地绑定到那个对象,不能像指针那样指向其他对象。
数据类型大小的影响: 与按地址传递一样,引用传递的开销与数据的大小无关,因为它传递的是地址。

结论(引用传递): 引用传递在性能上与按地址传递几乎没有差别,因为底层通常被实现为指针传递。它提供了一种更安全、更简洁的接口来修改或访问原始变量,尤其适合传递大型对象,因为避免了复制。

那么,到底谁更快?综合对比!

现在我们来总结一下,并回答最核心的问题:按值传递比按地址传递更快吗?引用呢?

1. 对于基本类型(如 `int`, `double`, `char` 等):
按值传递: 复制一个很小的数据(4 或 8 字节)。
按地址传递/引用传递: 传递一个地址(4 或 8 字节),然后在函数内解引用一次。
结论: 在大多数现代处理器上,这三者之间的性能差异 微乎其微,几乎可以忽略不计。甚至可能因为按值传递避免了额外的解引用操作,在某些极度微小的场景下略微快一点点。但这种差异通常会被编译器优化、CPU 缓存等因素所掩盖,你不太可能通过这种微小的差异来感知到性能的提升。按值传递通常是可读性最好且最安全的,所以对于基本类型,首选按值传递。

2. 对于大型数据结构(如 `struct`, `class`, `std::string`, 容器等):
按值传递: 复制整个对象。如果对象非常大(例如,包含大数组的对象,或者是一个大型字符串对象),复制的成本会非常高。这会占用更多的 CPU 时间和内存带宽,可能导致性能下降。
按地址传递/引用传递: 只复制一个地址(指针的大小)。在函数内部通过地址访问对象。
结论: 按地址传递和引用传递远比按值传递更快,并且更高效。 因为它们避免了昂贵的对象复制。在 C++ 中,通常推荐使用 常量引用(`const Type&`)来传递大型对象,如果函数不需要修改它们。这既能保证性能(避免复制),又能保证原始对象不被意外修改。如果函数确实需要修改原始对象,那么使用非常量引用(`Type&`)或指针(`Type`)会更合适,其中引用通常在语法上更简洁。

编译器优化和ABI (Application Binary Interface)

需要强调的是,编译器是性能优化的强大工具。

寄存器传递: 对于非常小的参数(通常是基本类型或指针大小的参数),现代编译器可能会直接将它们放到 CPU 的寄存器中传递,而不是进行实际的内存复制。这使得按值传递(特别是对于基本类型)和按地址传递/引用传递在寄存器级别的开销非常接近,甚至可能因为按值传递少一个解引用指令而略有优势。
返回值优化 (RVO / NRVO): 对于函数返回值,编译器也会进行优化,避免不必要的复制。
ABI 的影响: 不同平台、不同编译器的 ABI(应用程序二进制接口)可能会影响实际的参数传递方式(例如,是全部用寄存器传递,还是部分参数会放到栈上)。但总体趋势是相似的。

总结性的建议

基本类型: 使用按值传递。代码清晰,性能足够好。
大型对象:
如果函数不修改对象:使用常量引用(`const Type&`)。这是性能最优且安全的标准做法。
如果函数需要修改对象:使用引用(`Type&`)。它提供更简洁的语法,并且同样高效。
在某些特定情况下(例如处理 C 风格的 API 或需要传递 `nullptr` 的场景),可以使用指针(`Type`)。但一般情况下,引用是更现代、更安全的选择。

所以,“按值传递比按地址传递更快吗?” 的答案是:对于基本类型,它们差不多快,按值传递可能略有优势但差异极小。但对于大型对象,按值传递会明显慢于按地址传递(或引用传递)。

“引用呢?” 引用在性能上与按地址传递非常相似,并且在安全性和语法上通常优于指针。

最终选择哪种传递方式,除了考虑性能,更要考虑代码的可读性、安全性以及清晰地表达函数意图。对于大型对象的非修改性访问,常量引用几乎是无可争议的首选。

网友意见

user avatar

写在前面,题主问的是内置类型,那么,基本上就是int char long ptr这类数据,我的回答基于这个大前提来讨论,所以不要再提什么构造函数之类的内容了。

引用和按指针传递,在汇编级别上应该是一样的,所以你的代码里f2和f3在汇编层面上应该是一样的,所以性能上应该没有差别。

至于值传递和引用传递的性能差别,我觉得不应该是写C++代码该关注的地方。


如果真要深入分析这个问题,在不开任何优化的情况下:

mov和lea指令速度是有可能有差异的,现代的x86架构的CPU里,开启流水线不开启HT的情况下,lea可能比mov要快,或者基本上差不多,我查到的信息里:

Ryzen架构下:

MOV r,m的latency是3,throughput是1/2;
LEA r32/64,[m]的latency是2,throughput是1/4;

Haswell架构下:

MOV r32/64,m的latency是2,throughput是1/2;
LEA r32/64,m的latency是1,throughput是1/2;

整体上LEA是要快一些的。

但这里只是考虑到调用者的情况,如果是传引用或者指针,那么意味着被调用函数的内部需要做地址转换,才能获得实际的参数的值。

对于32位系统来说,参数是在栈上的,要先通过ebp获得参数值(指针或者引用),再通过参数值获得实际的指向或者引用的值,要进行两次取地址的操作。

对于64位系统来说,参数是通过寄存器来传递的,仍然需要一次取地址操作才能获得实际的值。

而如果是值传递的话,就没那么复杂了,参数就在栈上甚至在寄存器里,直接用就可以。因此整体来看,如果按值传递,被调用者的开销确实更小。

所以,大致的结论是:对于内置类型来说,值传递确实比指针(和引用)效率要高

以上只是不开优化的结论,开优化以后,性能就不好说了,而且代码的复杂度也会影响优化的效果。

虽然结论是值传递更快,但我仍然觉得如果没有特别的需求,写代码的人不太建议关注它。

-------------------------

更进一步的思考一下:用值传递,那么肯定是不关注这个参数在被调函数内的变化,而用指针或者引用,那么一定是关注这种变化,两种方式的用途是不一样的,效率上有差异是必然的,但多数情况下,不需要人为的去优化,编译器的优化已经足够用了。

类似的话题

  • 回答
    在 C/C++ 中,我们经常听到“按值传递”、“按地址传递”(也叫指针传递)和“引用传递”这几种参数传递方式。那么,它们在性能上到底有什么区别呢?哪种方式更快呢?这个问题看似简单,但深入分析起来,会涉及到内存、CPU 缓存、编译器优化等多个层面。咱们先一个一个来聊聊。 1. 按值传递 (Pass b.............
  • 回答
    你问了个非常实际且关键的问题,尤其是在C语言这种需要手动管理内存的语言里。简单来说,是的,用 `%d` 格式化打印一个 `char` 类型的数据,如果那个 `char` 变量紧挨着其他内存中的数据,并且你没有对打印的范围进行限制,那么理论上存在“把相邻内存也打印出来”的可能性,但这并不是 `%d` .............
  • 回答
    在C/C++中,当您声明一个 `int a = 15;` 这样的局部变量时,它通常存储在 栈 (Stack) 上。下面我们来详细解释一下,并涉及一些相关的概念:1. 变量的生命周期与存储区域在C/C++中,变量的存储位置取决于它们的生命周期和作用域。主要有以下几个存储区域: 栈 (Stack):.............
  • 回答
    为何C/C++中字符和字符串要用引号包裹?在C/C++的世界里,我们经常会看到单引号 `' '` 包裹着一个字符,双引号 `""` 包裹着一串字符(也就是字符串)。这不仅仅是语言的规定,背后有着深刻的设计哲学和实际考量。今天我们就来好好掰扯掰扯,为啥它们需要这些“外衣”。 先聊聊字符(char)和它.............
  • 回答
    在C/C++中,关于数组的定义与赋值,确实存在一个常见的误解,认为“必须在定义后立即在一行内完成赋值”。这其实是一种简化的说法,更准确地理解是:C/C++中的数组初始化,如果要在定义时进行,必须写在同一条声明语句中;而如果要在定义之后进行赋值,则需要分步操作,并且不能使用初始化列表的方式。让我们一步.............
  • 回答
    在C/C++的世界里,指针和结构体(或类)的组合使用是再常见不过的了。当你有一个指向结构体或类的指针,想要访问其中的成员时,你会发现有两种方式可以做到:`(p).member` 和 `p>member`。很多人会疑惑,既然它们的作用完全一样,为什么语言设计者要提供两种写法呢?这背后其实有其历史原因和.............
  • 回答
    const 的守护之剑:编译器如何雕琢 C/C++ 中的不变之道在C/C++的世界里,`const` 并非只是一个简单的关键字,它更像一把锋利的守护之剑,承诺着数据的不可变性,为程序的稳定性和可维护性筑起一道坚实的壁垒。那么,这把剑究竟是如何被铸造和挥舞的呢?这背后,是编译器一系列精巧的设计和严密的.............
  • 回答
    在 C/C++ 项目中,将函数的声明和实现(也就是函数体)直接写在同一个头文件里,看似方便快捷,实际上隐藏着不少潜在的麻烦。这种做法就像是把家里的厨房和卧室直接打通,虽然一开始可能觉得省事,但长远来看,带来的问题会远超于那一点点便利。首先,最直接也是最普遍的问题是 重复定义错误 (Multiple .............
  • 回答
    .......
  • 回答
    好的,我来详细解释一下 C 和 C++ 中 `malloc` 和 `free` 函数的设计理念,以及为什么一个需要大小,一个不需要。想象一下,你需要在一个储物空间里存放物品。`malloc`:告诉空间管理员你要多大的箱子当你调用 `malloc(size_t size)` 时,你就是在对内存的“管理.............
  • 回答
    C++ 中将内存划分为 堆(Heap) 和 栈(Stack) 是计算机科学中一个非常重要的概念,它关乎程序的内存管理、变量的生命周期、性能以及程序的灵活性。理解这两者的区别对于编写高效、健壮的 C++ 程序至关重要。下面我将详细阐述为什么需要将内存划分为堆和栈: 核心原因:不同的内存管理需求和生命周.............
  • 回答
    在C++开发中,我们习惯将函数的声明放在头文件里,而函数的定义放在源文件里。而对于一个包含函数声明的头文件,将其包含在定义该函数的源文件(也就是实现文件)中,这似乎有点多此一举。但实际上,这么做是出于非常重要的考虑,它不仅有助于代码的清晰和组织,更能避免不少潜在的麻烦。咱们先从根本上说起。C++的编.............
  • 回答
    在C++中,`?:` 是 条件运算符(ternary operator),也被称为 三元运算符。它是C++中最简洁的条件判断结构之一,用于根据一个布尔条件的真假,返回两个表达式中的一个。以下是详细解释: 1. 语法结构条件运算符的语法如下:```条件表达式 ? 表达式1 : 表达式2``` 条件表达.............
  • 回答
    一些C++程序员在循环中偏爱使用前缀自增运算符`++i`,而不是后缀自增运算符`i++`,这背后并非简单的个人喜好,而是基于一些实际的考量和性能上的微妙区别。虽然在现代编译器优化下,这种区别在很多情况下几乎可以忽略不计,但理解其根源有助于我们更深入地理解C++的运算符机制。要详细解释这个问题,我们需.............
  • 回答
    好,咱们不绕弯子,直接切入正题。在C++里,说到函数,离不开实参和形参这两个概念,它们就像是函数的“输入口”和“占位符”。理解它们俩的区别,是掌握函数传值、传址等核心机制的关键。咱们先从最直观的来说,把它们想象成我们在生活中接收信息和处理信息的过程。形参(Formal Parameter):函数的“.............
  • 回答
    在C++的世界里,对指针类型的“判断”这个说法,得看我们具体指的是什么。如果你的意思是像某些动态类型语言那样,在运行时能直接问一个变量是不是“指向一个int的指针”或者“指向一个字符串的指针”,那么答案是:不直接支持这种“运行时类型查询”(RTTI,Runtime Type Information).............
  • 回答
    在 C++ 编程中,`long` 整数类型的使用,确实是一个值得探讨的问题,尤其是在现代 C++ 的语境下。你问我它是否还有意义,我得说,它依然有其存在的理由,尽管它的必要性相比过去有所下降,并且需要更谨慎地理解和使用。在我看来,与其说 `long` 失去了意义,不如说它的 “角色定位”变得更加微妙.............
  • 回答
    在C++的世界里,“virtual”这个词被翻译成“虚函数”,这可不是随意为之,而是因为它精确地抓住了这种函数在继承和多态机制中的核心特征。理解“虚”这个字的关键,在于它暗示了一种“不确定性”,或者说是一种“在运行时才确定”的行为。设想一下,你有一系列动物,比如猫、狗,它们都属于一个更大的“动物”类.............
  • 回答
    在C++的世界里,链表的重要性,绝非“重要”二字能够轻易概括。它更像是一门关于“组织”与“流动”的艺术,是数据结构中最基础却也最富生命力的存在之一。我们不妨从最核心的用途说起:内存的动态分配与管理。当你编写C++程序时,你几乎无法避免地要跟内存打交道。数组,作为最直观的连续内存存储方式,在声明时就需.............
  • 回答
    .......

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有