恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C/C++函数参数传递:传值、传指针与传引用详解

  • 首页
  • 资讯中心
  • /
  • C/C++函数参数传递:传值、传指针与传引用详解

相关资讯

再也不用花冤枉钱了!TaoToken 统一 Key 接入 OpenAI/Claude/Gemini 的 API 配置指南 2026/10/15 20:01:42
Windows部署openclaw解决code 78问题:TaoToken统一Key接入与排错实录 2026/10/15 20:01:42
Apollo-11制导计算机代码深度拆解:2KB内存如何实现登月 2026/10/15 20:01:42

最新资讯

神经网络架构搜索(NAS)实战指南:搜索空间、策略与评估三要素
PyTorch深度学习实战:从张量自动微分到GPU训练与模型调试
Linux架构师千页文档实战指南:从内核到容器调优与避坑
动手学深度学习2.0 PyTorch版:从环境搭建到模型训练实战指南
TensorFlow本质:计算图驱动的工业级AI工程化框架
PyTorch底层原理:dtype/device/grad/no_grad/DataLoader四大核心契约解析

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

C/C++函数参数传递:传值、传指针与传引用详解

发布时间:2026/10/15 20:01:42
C/C++函数参数传递:传值、传指针与传引用详解 1. 从一个经典的内存踩坑说起很多写了几年C/C的人在面试或者code review的时候依然会在函数参数传递这件事上翻车。我见过太多这样的场景一个函数明明在内部修改了参数调用方拿到的却还是老值或者更隐蔽的函数内部修改成功了但程序跑着跑着就崩溃查了半天发现是野指针。这些问题的根源十有八九都指向同一个知识点——函数参数传递方式。C和C在参数传递上有三种基本方式传值pass by value、传指针pass by pointer、传引用pass by reference。其中传引用是C独有的C语言从语法层面就不支持。这个区别看起来简单但实际用起来每种方式背后的内存模型、拷贝开销、生命周期管理、以及适用场景都有不少门道。我打算把这三者彻底拆开讲清楚从底层内存布局到实际代码怎么写再到常见坑怎么排尽量让刚入门的朋友也能看懂同时给有一定经验的同行提供一些可以直接抄的实践方案。这篇文章适合谁看如果你正在学C/C搞不清楚什么时候该用指针什么时候该用引用或者你已经在写代码但经常被参数传递的问题困扰那这篇内容应该能帮到你。我会尽量用生活化的类比来解释同时给出可以直接复现的代码示例和参数选择依据。2. 三种传递方式的本质区别2.1 传值复制一份互不干扰传值是最好理解的一种方式。当你调用一个函数并传入一个参数时编译器会在函数栈帧中为这个参数创建一份独立的拷贝。函数内部操作的是这份拷贝跟外面的原始变量没有任何关系。用生活类比来说这就像你把一份文件的复印件交给同事同事在复印件上怎么涂改你手里的原件都不会变。void modifyByValue(int x) { x 100; // 修改的是拷贝 } int main() { int a 10; modifyByValue(a); // a 仍然是 10 return 0; }这段代码里modifyByValue接收的x是a的一份拷贝函数内部把x改成100a不受影响。这个行为是确定的也是C语言默认的参数传递方式。传值的核心开销在于拷贝成本。对于int、char、float这种小类型拷贝几乎不花时间。但如果传入的是一个几百字节的结构体每次调用都要完整复制一遍这个开销就不能忽略了。typedef struct { char name[256]; char address[512]; int data[1024]; } LargeStruct; void processByValue(LargeStruct s) { // 每次调用都会拷贝整个结构体 }上面这个结构体大概有 256 512 4096 4864 字节接近5KB。如果在一个循环里调用这个函数上万次光是拷贝就消耗了大量CPU周期。所以传值适合小类型大对象要慎重。2.2 传指针给一把钥匙能改原物传指针的本质仍然是传值只不过传的这个“值”是一个内存地址。函数接收到地址后可以通过解引用操作符*去访问和修改那个地址上存放的数据。继续用类比传值是给复印件传指针是把原件的存放位置比如保险柜编号告诉同事同事拿着这个编号就能找到原件并直接修改。void modifyByPointer(int *p) { *p 100; // 通过地址修改原始变量 } int main() { int a 10; modifyByPointer(a); // a 变成了 100 return 0; }这里p本身也是一份拷贝但拷贝的是一个地址通常4或8字节开销很小。通过*p解引用就能操作到main函数里的a。传指针的关键优势有两个一是避免大对象拷贝只传地址就行二是允许函数修改调用方的数据。但代价是引入了指针的安全问题——空指针、野指针、悬垂指针这些都是传指针时常见的坑。void unsafe(int *p) { *p 42; // 如果p是NULL直接崩溃 } int main() { int *ptr NULL; unsafe(ptr); // 运行时崩溃 return 0; }所以用指针的时候函数内部第一件事通常就是检查p ! NULL。这是基本纪律。2.3 传引用C的语法糖本质还是地址传引用是C引入的特性C语言不支持。从语法上看引用就是给变量起了个别名操作引用就等于操作原变量不需要用*解引用写起来更自然。void modifyByReference(int ref) { ref 100; // 直接修改无需解引用 } int main() { int a 10; modifyByReference(a); // 调用时也不需要取地址符 // a 变成了 100 return 0; }从编译器实现的角度看引用在底层通常就是用指针实现的但语言层面把它包装成了“别名”的语义。这意味着引用必须在定义时初始化而且一旦绑定就不能再指向别的对象。这比指针更安全因为不存在“空引用”这种合法状态虽然通过某些手段可以制造出悬垂引用但那是未定义行为。传引用在C中的典型应用场景是运算符重载和大对象传递。比如重载操作符时几乎总是用引用std::ostream operator(std::ostream os, const MyClass obj) { os obj.toString(); return os; }这里os用引用是因为流对象不允许拷贝obj用const引用是为了避免拷贝同时保证不修改。2.4 三者的内存模型对比为了更直观地理解我用一个表格把三种方式的关键特性列出来特性传值传指针传引用C传递内容变量的拷贝变量地址的拷贝变量的别名能否修改原变量否是通过解引用是是否有拷贝开销有整个对象小仅地址无语义上是否可为空不适用可以是NULL不应为空是否可重新绑定不适用可以指向别处不可重新绑定C语言支持是是否语法简洁度高中需和*高这张表建议刚入门的朋友存下来写代码拿不准的时候对照看一眼能避免很多低级错误。3. 什么时候该用哪种方式3.1 小类型优先传值对于int、char、float、double、bool以及小尺寸的结构体比如两个int组成的坐标点传值是最自然的选择。拷贝成本极低代码可读性最好而且函数内部随便改参数都不会影响外面减少了副作用。typedef struct { int x; int y; } Point; // 传值就够了8字节的拷贝几乎无开销 double distance(Point a, Point b) { int dx a.x - b.x; int dy a.y - b.y; return sqrt(dx * dx dy * dy); }这里Point只有8字节传值的拷贝开销和传指针差不多但代码更干净不需要处理空指针。3.2 大对象用指针或引用当对象尺寸超过几个机器字比如超过16字节传值的拷贝开销就开始显现了。这时候应该用指针C风格或引用C风格。在C语言里只能选指针void processLarge(LargeStruct *s) { if (s NULL) return; // 必须检查 // 通过 s-field 访问成员 }在C里如果不需要修改对象用const引用如果需要修改用非const引用void readLarge(const LargeStruct s) { // 只读不拷贝不能修改 } void writeLarge(LargeStruct s) { // 可修改不拷贝 }注意C中传递大对象时优先考虑const引用而不是指针。引用语义上不能为空调用方不需要检查代码更安全。只有在需要表达“参数可选”或“可能为空”的语义时才用指针。3.3 需要修改调用方数据时的选择如果函数需要修改调用方的变量C语言只能用指针C可以用指针也可以用引用。这时候怎么选我的经验是如果参数是必须存在的用引用如果参数可能为空用指针。这个原则能帮你在大多数场景下做出合理选择。// 必须存在用引用 void swap(int a, int b) { int tmp a; a b; b tmp; } // 可能为空用指针 void destroyIfNotNull(Object *obj) { if (obj ! NULL) { obj-cleanup(); } }3.4 数组参数的特殊处理数组作为参数传递时情况稍微特殊。在C/C中数组名作为参数会退化为指针所以下面两种写法是等价的void func(int arr[]); // 等价于 void func(int *arr); // 这个这意味着函数内部无法通过sizeof(arr)获取数组长度必须额外传一个长度参数void processArray(int *arr, size_t len) { for (size_t i 0; i len; i) { // 处理 arr[i] } }C中可以用引用来传递数组的引用保留数组类型信息template size_t N void processArray(int (arr)[N]) { // N 是编译期常量可以直接用 for (size_t i 0; i N; i) { // 处理 arr[i] } }这种方式在模板编程中很常见能避免数组退化为指针带来的长度信息丢失。4. 实操中的关键细节与避坑指南4.1 指针参数的空值检查传指针时函数内部第一件事应该是检查指针是否为空。这不是可选项是必须做的。我见过太多因为漏掉空指针检查导致的线上崩溃。int safeOperation(int *p) { if (p NULL) { return -1; // 或者返回错误码 } *p 42; return 0; }如果函数内部不打算处理空指针至少要用assert在调试阶段暴露问题#include assert.h void mustNotBeNull(int *p) { assert(p ! NULL); // 调试阶段崩溃release阶段被优化掉 *p 42; }实操心得在团队协作中我建议统一约定——所有接受指针参数的函数要么在文档注释中明确说明“参数不可为空”要么在函数入口做检查。最怕的是有的函数检查有的不检查调用方搞不清楚该不该传NULL。4.2 引用不能重新绑定带来的设计约束C引用一旦绑定就不能改指向这个特性在某些场景下是优势保证语义清晰但在另一些场景下是限制。比如你需要在一个循环中让引用依次指向数组的不同元素这是做不到的只能用指针。int arr[5] {1, 2, 3, 4, 5}; // 错误引用不能重新绑定 // int ref arr[0]; // ref arr[1]; // 这是赋值不是重新绑定 // 正确用指针 int *p arr; for (int i 0; i 5; i) { p arr[i]; // 指针可以重新指向 // 处理 *p }这个区别在设计API时很重要。如果你的函数需要在内部遍历一个容器并让参数依次指向不同元素那参数类型必须是指针而不是引用。4.3 const正确性从第一天就养成习惯C中const引用是传递只读大对象的标准方式。但很多人写代码时懒得加const导致后面想加的时候发现牵一发而动全身。// 好习惯只读参数加const void printInfo(const std::string name, const std::vectorint data) { // 不能修改 name 和 data } // 坏习惯不加const调用方无法确认函数是否会修改参数 void printInfo(std::string name, std::vectorint data) { // 鬼知道你会不会改 }const引用还有一个好处它能绑定临时对象。非const引用不能绑定临时对象这会导致一些看似奇怪的编译错误。void func(const std::string s) {} func(hello); // OK临时string对象绑定到const引用 void func2(std::string s) {} // func2(hello); // 编译错误非const引用不能绑定临时对象4.4 传值返回与传引用返回的陷阱参数传递讲完了顺便提一下返回值。函数返回时如果返回的是局部变量的引用或指针调用方拿到的是悬垂引用/指针使用它就是未定义行为。// 严重错误返回局部变量的引用 int badReturn() { int local 42; return local; // local在函数返回后销毁 } // 严重错误返回局部变量的指针 int* badReturnPtr() { int local 42; return local; // 同上 }这两种写法编译器通常会给出警告但不会报错运行时可能“看起来正常”也可能直接崩溃。我踩过这个坑调试了大半天才发现是返回了局部变量的地址。正确的做法是返回值拷贝或移动或者返回动态分配的内存但调用方要负责释放或者返回静态变量/全局变量的引用但要考虑线程安全。4.5 结构体传值的性能实测为了让大家对拷贝开销有个直观感受我写了一个简单的测试分别用传值和传指针的方式调用一个处理大结构体的函数一千万次对比耗时。#include stdio.h #include time.h typedef struct { int data[1024]; // 4KB } BigStruct; void byValue(BigStruct s) { s.data[0] 1; } void byPointer(BigStruct *s) { s-data[0] 1; } int main() { BigStruct s; clock_t start, end; start clock(); for (int i 0; i 10000000; i) { byValue(s); } end clock(); printf(By value: %f seconds\n, (double)(end - start) / CLOCKS_PER_SEC); start clock(); for (int i 0; i 10000000; i) { byPointer(s); } end clock(); printf(By pointer: %f seconds\n, (double)(end - start) / CLOCKS_PER_SEC); return 0; }在我自己的机器上编译器开启-O2优化传值版本大约耗时0.8秒传指针版本大约0.02秒差距接近40倍。当然实际项目中不会这么极端但这个测试说明了一个问题大对象传值的拷贝开销是真实存在的不能忽视。注意编译器优化可能会消除一些拷贝比如RVO、移动语义但不要依赖编译器帮你擦屁股。该用指针/引用的时候就用这是写出高效代码的基本功。5. 常见问题排查与速查表5.1 函数内修改了参数但外面没变这是最典型的问题原因通常是用了传值方式但期望修改原变量。void tryToModify(int x) { x 100; // 改的是拷贝 } int main() { int a 10; tryToModify(a); printf(%d\n, a); // 输出10不是100 return 0; }排查思路检查函数参数类型。如果是普通类型非指针非引用那函数内部改的就是拷贝。解决办法是把参数改成指针C/C或引用C。5.2 程序在解引用时崩溃通常是传入了空指针或野指针。void crash(int *p) { *p 42; // 如果p是NULL或野指针崩溃 }排查思路在函数入口加assert(p ! NULL)或空指针检查用调试器查看调用时传入的指针值检查指针是否指向了已经释放的内存5.3 传引用时编译报错“不能绑定临时对象”void func(int x) {} func(42); // 编译错误原因非const引用不能绑定右值临时对象。解决办法是加constvoid func(const int x) {} // OK func(42); // 现在可以了5.4 数组参数丢失长度信息void process(int arr[]) { // sizeof(arr) 是指针大小不是数组大小 }排查思路数组作为参数会退化为指针必须额外传长度参数。或者改用C的std::array、std::vector它们自带长度信息。5.5 常见问题速查表问题现象可能原因解决方法函数内修改参数外面没变用了传值改用指针或引用解引用时崩溃空指针/野指针加空值检查检查生命周期编译错误“不能绑定临时对象”非const引用绑定右值加const数组长度计算错误数组退化为指针额外传长度参数返回局部变量引用/指针生命周期错误返回值或动态分配大对象传值性能差拷贝开销大改用const引用或指针5.6 一个容易忽略的细节默认参数与引用C中引用参数可以有默认值但默认值必须是左值int global 0; void func(int x global) { // OK x 42; } // void func2(int x 42) {} // 错误42是右值这个细节在写库代码时可能会遇到知道就行实际项目中不常用。6. 从汇编角度看参数传递如果你对底层感兴趣可以看看编译器生成的汇编代码能更深刻地理解三种传递方式的区别。我用一个简单的例子来演示int addByValue(int a, int b) { return a b; } int addByPointer(int *a, int *b) { return *a *b; }在x86-64架构下传值时参数通过寄存器edi和esi传递函数内部直接相加。传指针时rdi和rsi存放的是地址函数内部需要先mov指令从地址加载值再相加。; addByValue addByValue: lea eax, [rdi rsi] ; 直接相加 ret ; addByPointer addByPointer: mov eax, [rdi] ; 从地址加载 add eax, [rsi] ; 从地址加载并相加 ret可以看到传指针多了一次内存访问解引用但省去了大对象的拷贝。对于小类型传值反而更快因为不需要额外的内存访问。这也是为什么我说“小类型优先传值”的原因之一。C的引用在汇编层面和指针几乎一样都是传地址只是语法层面更安全。所以传引用的性能和传指针基本一致但代码更干净。7. 工程实践中的选择策略在实际项目中我通常遵循以下策略来选择参数传递方式第一优先级看类型大小。小于等于16字节的POD类型直接传值。大于16字节的用const引用C或const指针C。第二优先级看是否需要修改。需要修改调用方数据的C语言用指针C优先用引用。不需要修改的C用const引用C用const指针。第三优先级看是否可为空。参数语义上可能为空的用指针。必须存在的C用引用C用指针加文档说明。第四优先级看团队约定。如果团队有统一的编码规范遵循规范比个人偏好更重要。我待过的团队里有的规定“所有输出参数必须用指针”有的规定“能用引用就不用指针”两种风格都能写出好代码关键是统一。实操心得在跨语言接口比如C调用C库的场景下只能用指针因为C不支持引用。这时候要特别注意指针的生命周期管理确保在C函数执行期间指针指向的对象一直有效。还有一个容易被忽略的点多线程环境下的参数传递。如果函数会在另一个线程中执行传入的指针或引用必须保证在那个线程执行期间一直有效。传值在这种情况下反而更安全因为拷贝是独立的不依赖调用方的生命周期。// 危险传递局部变量的引用到另一个线程 void startThread() { int local 42; std::thread t([local]() { // local 可能已经销毁 std::cout local std::endl; }); t.detach(); // local 在这里销毁线程可能还在运行 } // 安全传值 void startThreadSafe() { int local 42; std::thread t([local]() { // 拷贝 std::cout local std::endl; }); t.detach(); }这个例子说明参数传递方式的选择不仅要考虑性能和语义还要考虑生命周期和并发安全。传值虽然简单但在某些场景下反而是最安全的选择。8. 一些值得记住的经验法则写了这么多年C/C关于参数传递我总结了几个可以直接用的经验法则法则一默认用传值除非有明确理由不用。传值最简单、最安全没有空指针问题没有生命周期问题。只有在性能或语义需要时才改用其他方式。法则二C中传递只读大对象用const T。这是标准做法既避免拷贝又保证不修改还能绑定临时对象。法则三需要修改调用方数据时C用T*C用T。C中用引用比指针更安全因为引用不能为空调用方不需要检查。法则四参数可能为空时用指针必须存在时用引用。这个原则能帮你在指针和引用之间快速做出选择。法则五数组参数必须额外传长度。不要试图在函数内部用sizeof获取数组长度那是指针大小。法则六永远不要返回局部变量的指针或引用。这是未定义行为编译器可能不报错但运行时一定出问题。法则七多线程场景下传值往往比传引用/指针更安全。拷贝是独立的不依赖调用方的生命周期。这些法则不是绝对的但在大多数场景下能帮你做出合理的选择。真正重要的是理解每种方式背后的内存模型和生命周期语义这样遇到特殊情况时你才知道怎么权衡。最后分享一个我常用的调试技巧当你怀疑参数传递出了问题可以在函数入口和出口分别打印参数的地址和值。如果地址一样但值不对说明是修改没生效如果地址不一样说明发生了拷贝。这个简单的办法能快速定位大部分参数传递相关的问题。void debugFunc(int *p) { printf(Entry: ptr%p, value%d\n, (void*)p, p ? *p : -1); // ... 函数逻辑 ... printf(Exit: ptr%p, value%d\n, (void*)p, p ? *p : -1); }这个技巧看起来简单但在排查“为什么改了没生效”这类问题时特别管用。我靠它定位过好几次因为传值导致的逻辑错误。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号