C++程序中函数的地址是什么?

39

函数是一组存储在连续内存块中的指令。

函数的地址(入口点)是函数中第一条指令的地址。(据我所知)

因此,我们可以说函数的地址和函数中第一条指令的地址将是相同的(在这种情况下,第一条指令是变量的初始化)。

但下面的程序与上述说法相矛盾。

代码:

#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
char ** fun()
{
    static char * z = (char*)"Merry Christmas :)";
    return &z;
}
int main()
{
    char ** ptr = NULL;

    char ** (*fun_ptr)(); //declaration of pointer to the function
    fun_ptr = &fun;

    ptr = fun();

    printf("\n %s \n Address of function = [%p]", *ptr, fun_ptr);
    printf("\n Address of first variable created in fun() = [%p]", (void*)ptr);
    cout<<endl;
    return 0;
}

一个输出示例是:

 Merry Christmas :) 
 Address of function = [0x400816]
 Address of first variable created in fun() = [0x600e10]

所以,这里函数的地址和函数中第一个变量的地址不相同。为什么会这样?

我在谷歌上搜索,但找不到确切的答案,由于对这种语言还很陌生,因此无法完全理解网络上的一些内容。


14
为什么函数的地址和函数内第一个变量的地址不同?-它为什么应该相同?这不是一条指令。 - Ed Heal
4
请看所创建的组装件。 - Ed Heal
4
函数中的变量并不总是与函数一起存储。这些变量被存储在其他地方,比如上。这样函数就可以利用推入和弹出操作。 - Greg M
10
@AmitUpadhyay 要么你之前评论中提到的那本书是错得离谱的,要么你误解了它所说的内容。你声称那本书说的话是无用的。 - JSF
2
@MarianSpanik 这会导致 error: invalid conversion from ‘const char**’ to ‘char**’ [-fpermissive] - Amit Upadhyay
显示剩余12条评论
11个回答

57
因此,函数的地址和函数内第一个变量的地址不同。为什么会这样呢?函数指针是指向函数的指针,而不是指向函数内部的第一个变量。
详细地说,函数(或子程序)是一组指令(包括变量定义和不同的语句/操作),执行特定的工作,通常多次执行。它不仅仅是指向函数内部元素的指针。
在函数内定义的变量不存储在与可执行机器代码相同的存储区域中。根据存储类型,存在于函数内部的变量位于执行程序的某个其他内存部分。
当程序被构建(编译成对象文件)时,程序的不同部分以不同的方式组织。
  • 通常,函数(可执行代码)驻留在一个名为代码段的单独段中,通常是只读内存位置。

  • 而编译时分配的变量则存储在数据段中。

  • 函数局部变量通常会根据需要填充到堆栈内存中。

因此,函数指针不会产生源代码中存在的指向函数中第一个变量的地址的关系。

在这方面,引用wikipedia文章中的话:

函数指针指向内存中的可执行代码,而不是数据值。

因此,简而言之,函数的地址是位于代码(文本)段内的内存位置,其中包含可执行指令。


好的,我在一本书中读到过一个函数没有自己的地址,但它的地址只是内存堆栈开头的地址,据我所知,堆栈的开头从变量(或指令)的初始化开始。 - Amit Upadhyay
@AmitUpadhyay:我想你搞反了。 在大多数架构上,堆栈帧的第一个值都是当前调用的返回地址(我猜你可以将其视为函数指针,尽管在C ++中技术上没有高级等效项),而不是相反的情况。 - BlueRaja - Danny Pflughoeft
函数内定义的变量不一定存储在可执行部分的相同内存地址中。你所说的“可执行代码与变量不一定在相同的内存地址中”是什么意思? - hyde
@hyde 这涉及到数据和代码段部分。我尝试澄清,但如果您觉得可以通过使用任何替代措辞来改进它,请随意编辑。谢谢。 :) - Sourav Ghosh
亲爱的点踩者,你的沉默并不能帮助提高这篇文章的质量。如果你有什么建议,请告诉我,这样这篇文章就可以得到改进。谢谢 :) - Sourav Ghosh
显示剩余3条评论

17

一个函数的地址只是一种象征性的方式来传递这个函数, 比如在函数调用时传递它之类的。潜在地,你获取到的函数地址的值甚至不是指向内存的指针。

函数地址有两个好处:

  1. 用于比较相等性 p==q,

  2. 可进行解引用和调用 (*p)()

其它任何你尝试做的事情都是未定义的,可能会或可能不会工作,并且取决于编译器的决定。


4
特别是,你可以很容易地拥有一个符合标准的C++实现,它被设计为一个用面向对象语言(例如Java)编写的解释器。在这个实现中,函数指针可能是一个整数索引,指向一个不透明的字节码对象数组,与机器代码(或甚至解释器代码)没有任何关联。 - Alex Celeste

13

好的,这会很有趣。我们将从C++中函数指针的极其抽象的概念一直深入到汇编代码层面,并且由于我们遇到了一些特定的混淆,我们甚至能够讨论堆栈!

让我们从高度抽象的角度开始,因为显然你是从这方面开始的。你有一个函数 char** fun(),正在与它交互。现在,在这个抽象级别上,我们可以看看哪些操作可以在函数指针上执行:

  • 我们可以测试两个函数指针是否相等。如果它们指向同一个函数,则两个函数指针是相等的。
  • 我们可以在这些指针上进行不等式测试,从而允许我们对这样的指针进行排序。
  • 我们可以解引用函数指针,这会产生一个“函数”类型,但它实际上很难理解,我选择暂时忽略它。
  • 我们可以使用你使用的符号调用函数指针:fun_ptr()。这个的含义与调用被指向的任何函数相同。

这就是它们在抽象级别上所做的全部。在此之下,编译器可以自由地按照他们认为合适的方式进行实现。如果编译器希望有一个 FunctionPtrType,它实际上是程序中每个函数的一个大表的索引,那么他们可以这样做。

然而,通常情况下并不是这样实现的。当将C++编译成汇编/机器代码时,我们倾向于利用尽可能多的特定于体系结构的技巧,以节省运行时间。在现实计算机上,几乎总会有一种“间接跳转”操作,它读取一个变量(通常是寄存器),并跳转到开始执行存储在该内存地址处的代码。几乎所有的函数都被编译为连续的指令块,因此如果您跳转到块中的第一个指令,它会逻辑地调用该函数。第一个指令的地址恰好满足C ++的函数指针抽象概念所需的每个比较,并且它恰好是硬件需要使用间接跳转来调用函数的值!这是如此方便,以至于几乎每个编译器都选择以这种方式进行实现!

但是,当我们开始谈论为什么你认为你正在查看的指针与函数指针相同时,我们必须深入讨论一些更加微妙的内容:段。

静态变量与代码存储在不同的内存空间。这么做有几个原因。首先,您希望代码尽可能紧凑。您不希望代码中充斥着用于存储变量的内存空间,否则会效率低下。您必须跳过各种内容,而不是直接执行。其次,还有一个更现代的原因:大多数计算机可以将某些内存标记为“可执行”的,而某些标记为“可写”的。这样做有助于防范一些非常恶意的黑客技巧。我们试图永远不要同时将某些东西标记为可执行和可写,以防黑客巧妙地找到方法来欺骗我们的程序,从而用自己的程序覆盖我们的一些函数!

因此,通常有一个名为.code的段(使用这种点号表示法只是因为它是许多体系结构中常用的表示方法)。在这个段中,您可以找到所有的代码。静态数据将存储在类似.bss的地方。所以您可能会发现您的静态字符串存储在远离操作它的代码的地方(通常至少相隔4kb,因为大多数现代硬件允许您在页面级别上设置执行或写入权限:在许多现代系统中,页面大小为4kb)。

现在是最后一部分...栈。您提到以令人困惑的方式在堆栈上存储数据,这表明可能有必要快速了解一下它。让我编写一个快速递归函数,因为它们更有效地演示了堆栈中发生的事情。

int fib(int x) {
    if (x == 0)
        return 0;

    if (x == 1)
        return 1;

    return fib(x-1)+fib(x-2);
}

这个函数使用了一种相当低效但清晰易懂的方法来计算斐波那契数列。

我们有一个名为fib的函数。这意味着&fib始终是指向同一位置的指针,但显然我们正在多次调用fib,因此每个调用都需要自己的空间,对吗?

在堆栈上,我们有所谓的“帧”。“帧”并不是函数本身,而是允许该特定函数调用使用的内存部分。每次调用函数(比如fib),都会为其帧在堆栈上分配一些空间(或更严谨地说,在调用后它将在您进行调用之后分配)。

在我们的情况下,fib(x)显然需要在执行fib(x-2)时存储fib(x-1)的结果。它不能将其存储在函数本身中,甚至不能将其存储在.bss段中,因为我们不知道递归多少次。相反,它在堆栈上分配空间来存储其自己的fib(x-1)结果的副本,而fib(x-2)则在其自己的帧中(使用完全相同的函数和相同的函数地址)进行操作。当fib(x-2)返回时,fib(x)只需加载那个旧值(它肯定没有被任何其他人触摸过),加上结果并返回!

它是如何做到这一点的?实际上,几乎所有处理器都有一些硬件支持栈。在x86上,这称为ESP寄存器(扩展堆栈指针)。程序通常将其视为指向下一个可以开始存储数据的堆栈位置的指针。您可以随意移动此指针以建立自己的帧空间,并移动。执行完成后,您应该将所有内容移回。

事实上,在大多数平台上,您的函数中的第一条指令不是最终编译版本中的第一条指令。编译器注入了一些额外的操作来为您管理这个堆栈指针,使您甚至无需担心它。在某些平台上(如x86_64),这种行为甚至经常是强制性的,并在ABI中指定!

总之,我们有:

  • .code段——存储函数指令的地方。函数指针将指向其中的第一条指令。这个段通常标记为“只执行/只读”,在它被加载后,防止程序对其进行写入。
  • .bss段——存储您的静态数据的地方,因为如果要成为数据,它不能是“只执行”.code段的一部分。
  • 堆栈——您的函数可以在此存储帧,在帧中仅跟踪当前实例所需的数据,而不需要更多。 (大多数平台也使用此功能来存储有关在函数完成后返回何处的信息)
  • 堆——此答案中没有出现此项,因为您的问题不包括任何堆活动。但是,为了完整起见,我将在此留下它,以便以后不会让您感到惊

10
这是一段关于C ++程序中函数地址的内容。与其他变量一样,函数的地址也是为其分配的空间,换句话说,函数执行操作的机器码指令存储的内存位置。要理解这一点,请深入了解程序的内存布局。程序的变量和可执行代码/指令存储在不同的内存(RAM)段中。变量可以存储在STACK、HEAP、DATA和BSS段中,而可执行代码可以存储在CODE段中。请参考程序的通用内存布局。

enter image description here

现在你可以看到变量和指令有不同的内存段。它们存储在不同的内存位置。函数地址是位于CODE段的地址。
所以,您将术语“第一条语句”与“第一条可执行指令”混淆了。当函数调用被调用时,程序计数器会更新为函数的地址。因此,函数指针指向存储在内存中的函数的第一条指令。

enter image description here


9
在你的问题中,您提到:

因此,我们可以说函数的地址和函数中第一条指令的地址是相同的(在这种情况下,第一条指令是变量的初始化)。

但在代码中,您并没有得到函数中第一条指令的地址,而是得到了在函数中声明的某些局部变量的地址。

函数是代码,变量是数据。它们存储在不同的内存区域中,甚至不在同一内存块中。由于现代操作系统所施加的安全限制,代码存储在被标记为只读的内存块中。

据我所知,C语言没有提供任何方法来获取内存中语句的地址。即使它提供了这样的机制,函数的开始(函数在内存中的地址)也与从第一个C语句生成的机器代码的地址不同。

在从第一个C语句生成的代码之前,编译器会生成一个 函数序言,该序言(至少)保存了栈指针的当前值,并为函数的本地变量腾出空间。这意味着在C函数的任何代码生成之前需要多个汇编指令。


1
这似乎是目前为止唯一直接回答OP困惑的观点:代码和数据是两个不同的东西... - hyde
此外,甚至不一定是函数(正如提问者所说)“存储在一个连续的内存块中的指令集”。一旦优化器完成,函数就不需要是连续的(例如,常见代码可以从两个函数中提取并共享,尽管这很少见),它们可能由其他东西组成,除了指令(例如常量池或调试信息)。 - Steve Jessop

7

正如你所说,函数的地址可能是(这将取决于系统)该函数第一条指令的地址。

这就是答案。在典型环境中,指令不会与变量共享地址,因为同一地址空间用于指令和数据。

如果它们共享相同的地址,指令将被赋值给变量而被销毁!


4
其他答案已经解释了函数指针的是什么和不是什么。我会具体说明为什么您的测试并没有测试您认为它所测试的内容。
一个函数(入口点)的地址是该函数中第一条指令的地址。(据我所知)
这不是必需的(正如其他答案所解释的那样),但这是常见的,通常也是一个很好的直觉。
(在这种情况下,第一条指令是变量的初始化。)
好的。
printf("\n Address of first variable created in fun() = [%p]", (void*)ptr);
你正在打印的是变量的地址,而不是设置该变量的指令的地址。
它们不同。事实上, 它们不能相同。
变量的地址存在于函数的特定运行中。如果程序执行期间多次调用该函数,则变量每次可能在不同的地址上。如果函数递归调用自身,或者更一般地说,如果函数调用另一个函数,然后调用原始函数,那么每次调用函数都有自己的变量,具有自己的地址。在多线程程序中,如果多个线程在特定时间内调用该函数,则情况也是如此。
相反,函数的地址始终相同。无论是否当前正在调用函数,它都存在: 毕竟,使用函数指针的点通常是调用函数。多次调用函数不会改变其地址: 当您调用函数时,您不需要担心它是否已被调用。
由于函数的地址和其第一个变量的地址具有矛盾的属性,因此它们不能相同。
(注意: 可能存在系统,使得该程序可以打印相同的两个数字,尽管您可以很容易地通过编程生涯而不遇到这种情况。有哈佛体系结构,其中代码和数据存储在不同的存储器中。在这样的机器上,打印函数指针时的数字是代码存储器中的地址,而打印数据指针时的数字是数据存储器中的地址。两个数字可能相同,但这只是巧合,在对同一函数的另一个调用中,函数指针将是相同的,但变量的地址可能会发生改变。)

如果在程序执行期间多次调用该函数,则变量每次可能位于不同的地址。但如果该变量是“静态”的,则不会发生这种情况。 - glglgl

4

普通函数的地址指的是指令开始的位置(如果没有涉及vTable)。

对于变量,情况各异:

  • 静态变量存储在另一个地方。
  • 参数被推送到堆栈中或者保存在寄存器中。
  • 局部变量也被推送到堆栈中或者保存在寄存器中。

除非函数被内联或优化掉。


4

如果我没记错的话,程序会加载到内存的两个位置。第一个是编译后的可执行文件,包括预定义的函数和变量。这将从应用程序占用的最低内存开始。在一些现代操作系统中,这个地址可能是0x00000,因为内存管理器会根据需要进行转换。代码的第二部分是应用程序堆,其中存放运行时分配的数据,例如指针等。因此,任何运行时内存都会在不同的内存位置上。


2

函数内声明的变量并不是在代码中看到的位置分配的。 自动变量(在函数内部定义的变量)在函数调用时会被分配到栈内存的适当位置, 这是编译器在编译时完成的, 因此第一条指令的地址与变量无关。 这与可执行指令有关。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接