Python vs C++ 函数完整对照详解
🎯 核心总纲
在正式开始之前,我想先把这两门语言在"函数"这件事上的根本差异亮出来,因为理解了这层本质,后面所有细节都是顺理成章的。
- C++ 函数:编译期确定签名;区分传值 / 传引用 / 传指针;栈上分配栈帧;支持重载;函数是独立代码块,不是对象
- Python 函数:运行期解析;全部传对象引用;函数本身是一等堆对象;无重载;灵活可变参数
在我个人看来,这两者的差异本质上不是"语法不同",而是两种完全不同的语言哲学在函数这个切面上的投影。C++ 关心的是"编译期我能不能把一切都定死",Python 关心的是"运行期我怎么把一切变得足够灵活"。
目录
- 1. 函数定义语法
- 2. 参数传递(最容易混淆,重中之重)
- 3. 默认参数
- 4. 可变参数
- 5. 函数重载
- 6. 返回值
- 7. 函数是一等公民(巨大差异)
- 8. 嵌套函数 & 闭包
- 9. 作用域
- 10. 栈帧底层对比(结合之前内存模型)
- 极简核心总结表
1. 函数定义语法
Python
def add(a: int, b: int) -> int:
return a + b
Python 的定义有几个值得注意的地方:
def是关键字,但它不像 C++ 那样在编译期生成代码,而是运行时才创建函数对象,函数本身存放在堆上- 可以写类型注解
a: int、-> int,但它仅起到提示作用,运行时并不校验,传一个字符串进去照样能跑(直到运算报错) - 如果没有写
return,函数默认返回None - 函数可以嵌套定义,这点在后面的闭包章节还会展开
C++
int add(int a, int b) {
return a + b;
}
C++ 的写法则是另一番气象:
- 返回类型写在最前面,编译期就把函数编译成机器指令
- 没有返回值时必须写
void,且不存在隐式返回——如果你漏了 return,编译器会给你警告甚至报错 - C++ 的普通函数不能嵌套定义(这一点和 Python 差异很大),想要局部定义函数只能借助 lambda
💡 个人的看法:这里最值得玩味的是"类型注解"。很多初学者以为 Python 有了
-> int就安全了,其实那只是"写给维护者看的说明书",编译器根本不看。这既是 Python 灵活性的来源,也是它容易在大型项目里埋雷的地方——而 C++ 恰恰相反,它把所有的约束都压在了编译期,宁可让你编译慢一点,也不让错误混到运行期。
2. 参数传递(最容易混淆,重中之重)
在我看来,这一节是整个"函数对照"里最值得花时间的部分,因为太多人在 Python 和 C++ 之间切换时,在这里栽过跟头。
Python:只有一种传递方式 —— 传对象引用(传递指针语义)
⚠️ 请先记住一句话:Python 没有真正的传值拷贝! 它不会拷贝对象本体,而是复制对象的引用(指针)。
理解了这一点,下面的两种现象就都说得通了。
① 可变对象(list、dict):函数内部修改对象内容,外部可见
def modify_list(data):
data.append(100)
lst = [1, 2, 3]
modify_list(lst)
print(lst) # [1,2,3] → [1,2,3,100],外部被改动
因为 data 和 lst 指向的是同一个 list 对象,所以你在函数里 append,外部自然能看到变化。
② 不可变对象(int、str、tuple):函数内部重新赋值,外部不受影响
def modify_num(x):
x = 999 # 只是把局部引用 x 指向新对象,外部变量不受影响
num = 10
modify_num(num)
print(num) # 输出 10,不变
这里要特别澄清一个常见的误解:x = 999 并不是在修改原来的对象(int 本身就是不可变的,想改也改不了),而是把局部变量 x 这个引用重新绑定到了一个新的堆对象 999 上。外部的 num 还稳稳地指着原来的 10。
💡
x = 999不是修改旧对象,而是局部引用重新绑定新堆对象。
C++:三种传参模式,编译期决定
C++ 这边就直白得多,它在编译期就把"你要怎么传"定死了,常见的有四种。
① 传值(拷贝)
void modify(int x) {
x = 999;
}
int a = 10;
modify(a); // 拷贝一份副本,外部 a 不变
传值时,参数 x 是 a 的一份完整拷贝,你在函数里改的只是副本,外面的 a 纹丝不动。
② 左值引用 &,别名,直接操作原内存
void modify(int &x) {
x = 999;
}
int a = 10;
modify(a); // a 变成 999
引用本质上是原对象的别名,x 和 a 操作的是同一块内存,所以修改会直接作用于外部变量。
③ const 左值引用,只读,大对象优先使用,避免拷贝开销
void show(const std::vector<int>& v) {
// v 不能修改
}
这是我在 C++ 里最推荐的一种传参方式:既避免了传值带来的拷贝开销,又通过 const 保证了只读,安全性最高。
④ 指针传参 *
void modify(int *p) {
*p = 999;
}
int a = 10;
modify(&a);
指针和引用在效果上很接近,但指针可以指向 nullptr、可以被重新赋值,语义上比引用更"自由"也更危险,需要自己保证非空。
对比小结
| 维度 | Python | C++ |
|---|---|---|
| 传参本质 | 永远复制引用,无拷贝对象本体 | 传值会完整复制对象;引用 / 指针直接操作原内存 |
| 想要拷贝对象 | 手动 copy / deepcopy | 传值即可 |
| 大对象处理 | 无拷贝开销问题 | 优先 const 引用 避免拷贝 |
📝 个人总结:用一句话概括——Python 里"传参"实际上传的是"对象引用",而 C++ 里你得自己决定"传值、传引用还是传指针"。Python 把这种选择替你包了起来,换来的是简单;C++ 把选择交还给你,换来的是精确的控制。这里没有谁绝对更好,关键看你追求什么。
3. 默认参数
Python —— 大坑:默认参数不能用可变对象
# ❌ 大坑:默认参数不能用可变对象
def bad_func(items=[]):
items.append(1)
return items
# ✅ 正确写法
def good_func(items=None):
if items is None:
items = []
items.append(1)
return items
⚠️ 坑点:默认参数对象在函数定义的时候就已经创建好了,而不是每次调用时重新创建。如果默认值是
[]这样的可变对象,那么所有调用共享这同一个 list,状态会跨调用累积,这是无数 Python 新手(甚至老手)都踩过的经典陷阱。
在我看来,这个坑是 Python 设计里最"反直觉"的地方之一。解决方式也很简单:默认值一律写 None,在函数体内部再去创建新的可变对象。这不是什么高深技巧,但值得形成习惯。
C++ 默认参数
void func(int a, int b = 10) {
}
- 默认参数在编译期确定;而且有个约定俗成的规矩——在声明处写默认值,实现处不要再重复写,否则容易产生歧义
- C++ 不存在 Python 那个坑,因为每次调用都会按类型构造对应的临时对象
📝 个人看法:同样是"默认参数",Python 因"对象在定义时创建"而埋下坑,C++ 因"每次调用构造"而天然安全。这恰恰印证了我们在第 1 节说的那句话——Python 的运行时灵活性,往往要付出"隐式共享状态"的代价。
4. 可变参数
Python
def demo(a, *args, **kwargs):
print(a)
print(args) # 元组,接收多余位置参数
print(kwargs) # 字典,接收多余关键字参数
demo(1, 2, 3, name="abc")
*args把多余的位置参数打包成一个元组;**kwargs把多余的关键字参数打包成一个字典- 这种写法在 Python 里几乎是开箱即用的,非常灵活
C++
C++ 没有 Python 这种开箱即用的可变参数,需要绕一下:
- C++11 之后可以使用可变模板参数(在编译期展开,类型安全)
- 也可以用 C 风格的
va_list,但它不安全、不推荐在业务代码中使用
💡 C++ 不能像 Python 那样随便接收任意多参数,这也是两者在设计理念上的一大差异。
📝 个人看法:
*args和**kwargs是 Python 里极其强大的工具,尤其在写框架、装饰器、接口封装时几乎离不开。但我想提醒的是,灵活是一把双刃剑——过度使用会让函数签名变得模糊、难以为调用者提供清晰的提示。建议是:能写明确参数就别滥用可变参数,它更适合做"透传"而非"日常"。C++ 的可变模板参数虽然写起来繁琐,但类型安全,这也再次体现了 C++"宁可麻烦也要安全"的哲学。
5. 函数重载
Python:没有函数重载
def calc(x):
pass
def calc(x, y):
pass
# 后面直接覆盖前面,只有第二个 calc 生效
- 在 Python 里,后面定义的函数会直接覆盖前面同名函数,只有最后一个生效
- 想要实现"多参数版本",只能用默认参数、
*args在函数内部手动判断
C++:支持函数重载
int calc(int a);
int calc(int a, int b);
// 编译期根据实参匹配不同函数
- C++ 允许函数名相同、参数列表不同,编译器会根据实参自动匹配最合适的那个
- 注意:返回值不能用来区分重载,只看参数列表
📝 个人看法:很多人会感慨"Python 居然不支持重载"。但其实换个角度想,Python 的动态类型让"重载"变得没那么必要——因为参数本身可以是任何类型,一个函数就能覆盖很多情况,配合默认参数和
*args已经够用了。而 C++ 是静态类型语言,没有运行时的灵活性,所以它把"多态"下沉到了编译期,用重载来补偿。语言不给你某样东西,往往是因为它用别的方式给了你等价的能力。
6. 返回值
Python
return可以返回任意对象,几乎没有限制- 更妙的是,它支持一次性返回多个值,本质上是返回了一个元组
def get_pos():
return 10, 20 # 等价 return (10, 20)
x, y = get_pos()
- 这里
return 10, 20实际返回的是元组(10, 20),再通过解包x, y = ...分别接收
C++
- 返回值可以传值、引用或指针,选择权在你自己
- 但有一条铁律需要牢记:千万不要返回局部栈对象的引用或指针。因为局部变量在函数结束时就被销毁了,返回的引用会变成悬垂引用,访问它是未定义行为(UB)
// ⚠️ 危险!返回局部变量引用
int& bad() {
int x = 10;
return x; // x 在栈上,函数结束销毁,悬垂引用 UB 未定义行为
}
📝 个人看法:多返回值这个特性,是 Python 日常开发里非常舒服的地方,省去了 C++ 里定义结构体或者传引用参数去"带出"结果的麻烦。而在 C++ 里,返回引用的诱惑(省一次拷贝)往往伴随着悬垂的风险,因此:没有十足把握,就老老实实按值返回,现代编译器有 RVO / 移动语义,性能损失远没有想象中那么大。安全永远排在"省一次拷贝"前面。
7. 函数是一等公民(巨大差异)
一等公民:函数可以赋值给变量,作为参数传入,作为返回值返回。
这一节的差异,实质上是两门语言"气质"上最大的分水岭之一。
Python
函数是堆上的普通对象,可以赋值、传参、返回,闭包正是基于这个特性才得以成立。
def hello():
return "hi"
f = hello # 把函数对象赋值给 f
print(f()) # hi
# 函数作为参数
def run(func):
return func()
run(hello)
这种"把函数当数据传来传去"的能力,让 Python 在写回调、装饰器、策略模式时行云流水。
C++
C++ 的普通函数本身并不是对象,不能直接赋给变量,但可以用几种方式"曲线救国":
- 函数指针:
int (*fp)() = hello; std::function包装器:可以包装任意可调用对象- lambda:局部定义匿名函数对象
#include <functional>
#include <iostream>
int hello() {
return 100;
}
int main() {
// 函数指针
int (*fp)() = hello;
std::cout << fp();
// std::function 包装器
std::function<int()> f = hello;
return 0;
}
📝 个人看法:Python 是一等公民的"原生"支持者,函数用起来就像变量一样自然;C++ 则把这个能力"外包"给了函数指针、
std::function和 lambda 这三驾马车,能用,但需要你多一层心智负担。从现代工程实践看,C++ 我更推荐优先用std::function或 lambda,函数指针的语法可读性确实差一些。这里没有对错,只是"语言给了你多直接的路径"的区别。
8. 嵌套函数 & 闭包
Python
Python 允许在函数内部再定义函数,从而形成闭包——内层函数可以捕获并使用外层函数的变量;如果要在内层修改外层变量,需要借助 nonlocal。
def outer(x):
def inner():
return x + 1
return inner
f = outer(10)
print(f()) # 11
这里 inner 捕获了 outer 的参数 x,即使 outer 已经返回,inner 依然能访问到 x——这正是闭包的精髓。
C++
C++ 的普通函数不能嵌套,闭包只能靠 lambda 实现。lambda 捕获外部变量有两种方式:值捕获 [=](捕获一份拷贝)和引用捕获 [&](捕获引用)。
#include <functional>
#include <iostream>
std::function<int()> outer(int x) {
return [x]() { // 值捕获 x
return x + 1;
};
}
int main() {
auto f = outer(10);
std::cout << f();
return 0;
}
📝 个人看法:闭包是一个非常强大的抽象,但我想提醒的是——捕获要谨慎。在 C++ 里,如果 lambda 用
[&]捕获了一个已经销毁的局部变量,同样会产生悬垂引用;而 Python 里如果用闭包捕获循环变量,也容易踩到"延迟绑定"的坑。闭包是把双刃剑,用好了是利器,用不好就是隐蔽的 bug 源头。个人习惯是:C++ 优先值捕获[=],只在确有必要时才用引用捕获。
9. 作用域
Python —— LEGB 规则
Python 的变量查找遵循一个明确的顺序,用一个口诀记它:
Local 局部 → Enclosed 闭包外层 → Global 全局 → Builtin 内置
- 修改全局变量:用
global声明 - 修改外层嵌套函数里的局部变量:用
nonlocal声明
这套规则解释了为什么 Python 里"在函数里给全局变量赋值"会报错——因为赋值默认被当作创建了一个新的局部变量,除非你用 global 明确声明。
C++
- 局部作用域:变量在函数(或
{}块)内声明,离开作用域即销毁 - 全局作用域:整个程序可见,但应尽量少用,容易造成耦合
- 命名空间
namespace用来隔离符号;C++ **没有"闭包作用域"**这一说
📝 个人看法:Python 的 LEGB 和
global/nonlocal初看有点绕,但它换来的是"隐式的词法作用域",写起来很省心;C++ 则把一切规则都显式化、块级化,作用域清晰但需要你时时记着。我个人认为,作用域的本质都是"可见性和生命周期"的管理,理解了这个抽象,换哪种语言都不会慌。
10. 栈帧底层对比(结合之前内存模型)
这一节如果只看语法可能觉得抽象,但它是理解两门语言"性能气质"的钥匙。
① Python 函数调用
- Python 在解释器层面创建"栈帧对象",这个栈帧对象其实是放在堆上的;真正的操作系统栈上只存了少量指针
- 函数结束时,栈帧对象解除引用,回收时机要等 GC 决定,不确定
② C++ 函数调用
- C++ 在操作系统原生栈上开辟栈帧;局部变量直接放在 CPU 栈上
- 函数执行完毕,栈帧立即弹出销毁,生命周期确定
💡 C++ 局部对象的生命周期是确定性的、可预测的;而 Python 的栈帧、对象回收时机不确定,受 GC 支配。这也是为什么同样一个高频调用的函数,C++ 能跑得远比 Python 快的重要原因之一——它省去了对象管理和 GC 的额外开销。
📝 个人思考:理解了这一点,你就明白为什么"性能敏感"的场景几乎都选 C++,而"开发效率优先"的场景更适合 Python。这不是谁比谁"好",而是两者在"确定性"和"灵活性"上做了不同的取舍。C++ 把栈帧管理、对象生命周期全部握在自己手里,换来了可预测的性能;Python 把这一切交给解释器和 GC,换来了极高的开发效率。
极简核心总结表
把上面十节浓缩成一张表,方便你随时回顾:
| 特性 | Python 函数 | C++ 函数 |
|---|---|---|
| 定义 | def,运行期生成堆函数对象 | 返回类型 + 函数名,编译成机器码 |
| 传参 | 全部传对象引用;无拷贝,需手动 copy | 传值(拷贝)/ & 引用 / * 指针 / const 引用 |
| 默认参数 | ⚠️ 可变对象陷阱,定义时实例化 | 编译期确定,无此坑 |
| 可变参数 | *args **kwargs 原生支持 | 模板可变参数 / va_list |
| 重载 | 不支持,后定义覆盖前面 | 支持,参数列表区分 |
| 一等公民 | ✅ 函数是对象,可赋值传参返回 | 靠函数指针 / std::function / lambda |
| 嵌套闭包 | def 嵌套 + nonlocal | 普通函数不能嵌套,lambda 实现闭包 |
| 栈帧 | 解释器栈帧(堆),回收看 GC | 操作系统原生栈,调用完立刻弹出销毁 |
📌 一句话总结:Python 函数是运行时的一等堆对象,靠引用传递、灵活但开销大、回收不确定,适合追求开发效率的场景;C++ 函数是编译期的机器代码块,靠栈帧 / 拷贝 / 引用精确控制生命周期与内存,效率高但更繁琐,适合追求性能与确定的场景。没有哪门语言更好,只有哪门语言更适合你当下的问题——理解它们的差异,正是选择与驾驭它们的前提。
希望这篇对照能帮你建立起对两门语言函数的整体认知。如果哪里讲得不够清楚,欢迎留言交流。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/m0_54450246/article/details/163979407



