C语言函数指针

一、Q:函数体void x(); 把x赋值给函数指针p,p=x,当使用p指针时,是把函数体x从内存中复制一份,然后运行这个被复制的这段内存吗

answer:

不是的,绝对不会复制函数体。

在C/C++中,通过函数指针调用函数时,程序直接跳转到原函数所在的内存地址去执行,完全不会发生任何代码的复制。

可以从以下几个层面来看看底层到底发生了什么:

1. 函数在内存中的真实形态

当你写下 void x() { ... } 并且编译程序后,这个函数的代码(即大括号里的逻辑)会被转换成一系列机器指令

这些机器指令在程序运行时,会被统一加载到内存的一个特定区域,叫做代码段(Code Segment / Text Segment)

  • 代码段通常是只读的(防止程序意外篡改自己的代码)。
  • 函数名 x 在底层其实就是一个内存地址 (比如 0x00401000),这个地址就是 x 函数第一条机器指令所在的起始位置。

2. p = x 到底做了什么?

当你声明了一个函数指针 p,并执行 p = x 时:

  • 程序只是把 x 的入口地址(比如 0x00401000)赋值给了变量 p
  • 现在的 p 里面保存的仅仅是一个地址数字,并没有把 x 的代码搬运到 p 里面

3. 调用 p() 时发生了什么?

当你执行 p() 时,CPU 会进行以下操作:

  1. 读取指针 p 里面保存的地址(0x00401000)。
  2. 执行一条跳转指令(通常是汇编语言里的 CALL 指令),让 CPU 的**指令指针寄存器(PC / EIP / RIP)**直接指向 0x00401000
  3. CPU 开始顺着这个地址,逐条执行原来那份机器指令。
  4. 执行完毕后,通过 RET 指令返回到调用 p() 的地方继续往下运行。

4. 为什么不能(也不会)复制?

  • 性能极差:如果在每次调用时都去复制一段内存,会消耗大量的 CPU 时间和内存空间。
  • 安全限制 :现代操作系统都有安全机制(如 DEP / NX Bit),通常禁止在数据区(如栈或堆,也就是你复制出来的内存可能存放的地方)执行代码,以防止黑客利用缓冲区溢出进行攻击。因此,临时复制出来的代码往往是无法被执行的,操作系统会直接报错崩溃(Segmentation Fault)。

通俗的比喻

  • 函数 x 就像是一家建在固定位置的实体餐厅
  • 函数名 x 就像是这家餐厅的物理地址(例如:中山路100号)。
  • 函数指针 p 是一张小纸条
  • p = x 就是你在小纸条上写下了"中山路100号"。
  • 调用 p() 就是你按照纸条上的地址,亲自走过去(跳转)在那家实体餐厅里吃饭。

一个人绝对不会按照纸条上的地址,去把原来的餐厅"复制/克隆"一座到自己的后院里再进去吃饭。程序也是一样,它只是顺着指针找到了原函数的位置并执行它。

二、底层逻辑是《计算机组成原理》和《操作系统》

触及了计算机科学的"内功"层面。

第一部分:这些过程属于哪门课?

刚才说到的"函数在内存中的位置、CPU跳转、寄存器、内存分段"等知识,通常分布在计算机科学(CS)专业的以下几门核心课程中:

  1. 《C/C++程序设计》 (Programming in C/C++)
    • 学到什么: 表面语法。会学到如何定义函数指针、如何赋值(p = x)、如何调用。
    • 深度: 停留在语言应用层面,通常老师只会告诉"指针存的是地址",不会深入讲 CPU 是怎么跑的。
  2. 《计算机组成原理》 (Computer Organization and Architecture) ------ 核心相关
    • 学到什么: CPU 是如何工作的。会学到程序计数器(PC寄存器)、CPU 如何读取内存中的机器指令、CALL(调用)和 RET(返回)指令在硬件电路上是如何实现的。
    • 深度: 解释了为什么只需要一个地址,CPU就能跳转过去执行代码。
  3. 《操作系统》 (Operating Systems) ------ 核心相关
    • 学到什么: 程序是如何加载到内存中的。会学到"进程的内存模型"(代码段、数据段、堆、栈),以及内存保护机制(为什么不能在数据区执行代码,为什么代码段是只读的)。
    • 深度: 解释了为什么函数体不会被复制,以及复制了为什么会引发安全报错。
  4. 《汇编语言》 (Assembly Language)
    • 学到什么: C语言底层的样子。会亲手写出跳转到某个内存地址的指令,真正看到函数名是如何变成一串十六进制地址的。
  5. 《编译原理》 (Compilers)
    • 学到什么: 编译器(如 GCC/Clang)是如何把写的英文单词 void x() 翻译成 CPU 能看懂的二进制机器码,并为其分配内存地址的。

搞懂这些底层逻辑,《计算机组成原理》《操作系统》 是必须要跨过的两座大山。


三、函数指针在编程中使用极其多!非常重要!

是高级编程的核心机制之一。

虽然在普通的业务逻辑(比如写个简单的计算器)中不常直接写,但在系统级开发、框架设计、以及现代编程语言的底层,函数指针无处不在。

以下是它最核心的几个应用场景:

1. 回调函数 (Callback) ------ 最常见的用法

假设写了一个下载器,想在"下载完成"时播放一段音乐。但下载器是底层的代码,它不知道你要播什么音乐。

  • 做法: 可以把"播放音乐"的函数指针传给下载器。下载器在下完文件后,直接调用这个指针。
  • C标准库例子: C语言的排序函数 qsort。它不知道程序员想给什么数据排序(整数、小数还是结构体),所以必须传一个"比较规则"的函数指针给它,它在排序时通过指针来调用程序员的规则。
2. 实现面向对象 (C语言写出C++的效果)

C语言没有"类(Class)"和"对象(Object)",但 Linux 内核完全是用 C 语言写的,里面却充满了面向对象的设计。

  • 做法: 把函数指针塞进结构体(struct)里。
c 复制代码
struct File {
    void (*read)(int bytes);  // 读文件的函数指针
    void (*write)(char* data); // 写文件的函数指针
};

通过这种方式,Linux 实现了各种文件系统和硬件驱动的统一接口。这就是大名鼎鼎的 VFS(虚拟文件系统) 的底层原理。

3. 硬件中断与状态机(嵌入式开发必学)
  • 中断向量表: 当一个人按下键盘,CPU 会立刻停止手头工作,去内存的一个固定表格里查找"键盘处理函数"的地址。这个表格就是由一堆函数指针组成的。
  • 状态机: 游戏引擎或设备控制中,根据不同状态执行不同逻辑。不用写一堆 if-elseswitch,直接建一个函数指针数组,根据状态编号直接调用对应下标的函数,性能极高。
4. 它是现代高级语言中诸多特性的"老祖宗"

如果用 Java, Python, JavaScript 或 C#,可能很少听到"函数指针"这个词,但一直在用它的变体:

  • C++ 中的虚函数表 (vtable): C++ 实现多态(Polymorphism)的底层逻辑,就是偷偷在对象里藏了一个包含多个函数指针的数组。
  • JavaScript / Python 中的把函数当变量传(First-class functions):
    在 JS 中写 setTimeout(myFunction, 1000);,本质上就是把 myFunction函数指针传给了系统。
  • C# 的委托 (Delegate) 和现代语言的 Lambda 表达式: 在最底层,它们在编译后,依然是指向内存中某段代码地址的指针。

四、总结

函数指针就是"让代码去调用未知的代码"的唯一桥梁。

掌握了函数指针,就从"写死代码的人"晋升为了"写灵活框架的人"。

相关推荐
不写八个5 小时前
PHP教程006:ThinkPHP项目入门
开发语言·php
_MyFavorite_5 小时前
JAVA重点基础、进阶知识及易错点总结(31)设计模式基础(单例、工厂)
java·开发语言·设计模式
A.A呐5 小时前
【C++第二十三章】C++11
开发语言·c++
智算菩萨5 小时前
【Pygame】第8章 文字渲染与字体系统(支持中文字体)
开发语言·python·pygame
014-code5 小时前
Java SPI 实战:ServiceLoader 的正确打开方式(含类加载器坑)
java·开发语言
lifewange5 小时前
Go语言-开源编程语言
开发语言·后端·golang
白毛大侠6 小时前
深入理解 Go:用户态和内核态
开发语言·后端·golang
数据的世界016 小时前
C#4.0权威指南第12章:接口
开发语言·c#
¥-oriented6 小时前
【Python桌面应用开发环境搭建指南】
开发语言·python