- 函数签名(比如
void f(???);
)就是函数的“契约”或“承诺”。
- 它告诉调用者(caller)和被调用者(callee)双方,函数的输入是什么,函数该如何使用。
参数类型的重要性:
- 不同的参数类型代表不同的含义和用途。
- 它定义了函数需要什么样的数据,如何使用这些数据。
从两个角度来看:
- 调用者(Caller)的角度:
- 我要传入什么数据?
- 传入的参数类型决定了调用这个函数时,提供的数据格式和约束。
- 合理的参数类型设计让调用更简单、安全。
- 被调用者(Callee)的角度:
- 我接收到了什么样的数据?
- 我如何使用和处理这些参数?
- 函数的实现依赖于参数的类型来完成它的逻辑。
举个例子
void processData(const std::string& input);
- 调用者:我需要传入一个字符串的引用,且函数不会修改它。
- 被调用者:我接收到了一个字符串的常量引用,保证不修改。
如果改成:
void processData(std::string input);
- 调用者:我传入一个字符串的副本,函数修改不会影响我这边的原数据。
- 被调用者:我可以自由修改这个字符串副本。
总结
- 函数签名就像是一篇论述的中心论点,明确了双方的“契约”。
- 合理设计参数类型就是精心构造你的论点,决定了后续代码的逻辑和调用方式。
总结这个“Best Practices(最佳实践)”关于函数参数设计的要点:
函数参数设计最佳实践
void f(???);
1. 通过类型表达参数的用途
- 参数类型不仅是数据的载体,还应该传达这个参数在函数里的角色和用途。
- 例如,传递
const std::string&
表示函数不会修改这个字符串,传递指针可能意味着允许为空等。
- 类型本身就是一种“说明书”,让调用者一看就明白这个参数的含义。
2. 尽可能使用最“泛化”的类型
- 尽量用抽象程度高、通用的类型作为参数,比如用接口、基类或者模板参数,避免用具体实现类型。
- 这样你的函数更加灵活,能接受更多类型的参数,增强复用性。
- 比如,不要写死用
std::vector
,如果只需要遍历,可以用泛型迭代器或 std::span
。
3. 不要让调用者感到意外
- 参数的设计要符合直觉,避免隐藏副作用或让调用者猜测。
- 如果函数会修改参数,最好让调用者知道,比如传非
const
引用或者指针。
- 保证参数的行为符合常规约定,不要让调用者“踩坑”。
总结
- 类型即语义,通过类型告诉别人“我需要什么”和“我能做什么”。
- 选择“最通用”的类型,让代码更灵活易用。
- 设计清晰、符合预期的参数,避免调用者意外。
你说的内容是关于函数参数传递的不同方式,特别是针对类型 T
的各种传递方法。用中文简单整理并解释一下:
函数参数传递的几种典型方式
函数签名示例:
void f(???);
1. 按值传递(By Value)
void f(T param);
- 参数会被拷贝(或移动)一份,调用者传入的对象和函数内的对象是两个独立实体。
- 优点:简单,函数内可以自由修改
param
,不影响调用者。
- 缺点:如果
T
很大或拷贝成本高,性能开销大。
2. 按左值引用传递(By L-value Reference)
void f(T& param);
- 传入的是参数的引用,函数直接操作调用者传入的对象。
- 优点:避免拷贝,性能好。
- 缺点:调用者传入的对象必须是左值(具名变量),且函数可能修改调用者对象。
3. 按常量左值引用传递(By Const L-value Reference)
void f(const T& param);
- 传入对象的引用,但函数不能修改该对象。
- 优点:避免拷贝,且保证函数不会修改传入对象。
- 适合传递大对象且不需要修改。
4. 按右值引用传递(By R-value Reference)
void f(T&& param);
- 传入的是右值引用,专门接收临时对象(匿名对象)。
- 适用于移动语义,函数可以“窃取”资源。
- 例如,移动构造函数或移动赋值函数通常用这个。
5. 按指针传递(By Pointer)
void f(T* param);
- 传入对象的地址,函数通过指针操作对象。
- 可以传空指针(nullptr),表示无对象。
- 函数可能修改调用者对象。
- 适合需要表示“可能为空”或多态场景。
总结:对于类型 T,常见传参方式有
传递方式 |
示例 |
说明 |
传值(By Value) |
void f(T param) |
拷贝,独立对象 |
左值引用(By L-value Ref) |
void f(T& param) |
引用,可修改,必须是左值 |
常量左值引用 |
void f(const T& param) |
引用,不可修改,避免拷贝 |
右值引用(By R-value Ref) |
void f(T&& param) |
绑定右值,支持移动语义 |
指针传递 |
void f(T* param) |
传指针,可为空,可能修改对象 |
列出来的这些都是函数参数可能的写法,表示不同的传参方式和语义
函数参数的不同写法及含义
参数类型 |
含义与特点 |
T |
按值传递,函数会拷贝一份参数,调用者和函数内部是两个独立对象。 |
T& |
按左值引用传递,传入一个可修改的左值引用,函数内部修改会影响调用者。 |
T const |
这是个常量值传递(按值传递,但是常量),函数内部不能修改这个参数。 |
T const & |
按常量左值引用传递,避免拷贝,函数不能修改参数。通常用于传递大对象且不修改它。 |
T&& |
右值引用,接受右值(临时对象),适合移动语义,函数可“偷取”资源。 |
T* |
指针传递,函数可以通过指针访问或修改对象,也可以传空指针表示无对象。 |
T const && |
右值常量引用,接收右值但不允许修改它,比较少用。 |
T * const |
常量指针(指针本身不可变,但指向的对象可以变),参数是一个指针,函数不能改变指针的值。 |
举个例子
void f1(T param);
void f2(T& param);
void f3(const T param);
void f4(const T& param);
void f5(T&& param);
void f6(T* param);
void f7(const T&& param);
void f8(T* const param);
总结
- 值传递(T,const T):调用成本大,参数安全独立。
- 引用传递(T&,const T&):避免拷贝,左值引用可修改,常量引用不可修改。
- 右值引用(T&&,const T&&):支持移动语义,通常用来接收临时对象。
- 指针传递(T,T const)**:适合可能传空,或需要显式指针操作。
这段是围绕智能指针(unique_ptr
和 shared_ptr
)以及原始指针和普通类型 T
的各种传递方式的全方位罗列。
我帮你按类别梳理,并简要解释,方便理解所有可能的函数参数传递方式。
1. unique_ptr
及其相关参数
写法 |
说明 |
unique_ptr |
按值传递,通常不建议,因为 unique_ptr 是独占所有权,拷贝会被禁用 |
unique_ptr const |
按值传递且常量,拷贝依然不可行 |
unique_ptr& |
左值引用,可以修改原始的 unique_ptr (如移动它) |
unique_ptr const& |
常量左值引用,不允许修改,但可以访问(查看指针等) |
unique_ptr&& |
右值引用,转移所有权的标准用法 |
unique_ptr const&& |
常量右值引用,比较少见,一般没有太大用处 |
2. unique_ptr
和相关
unique_ptr
指向不可变对象的智能指针。
写法 |
说明 |
unique_ptr |
按值传递,不可拷贝 |
unique_ptr const |
按值传递且常量 |
unique_ptr& |
左值引用,可以修改指针本身(如移动它) |
unique_ptr const& |
常量左值引用 |
unique_ptr&& |
右值引用,用于移动 |
unique_ptr const&& |
常量右值引用 |
3. 普通类型 T
及其修饰
写法 |
说明 |
T |
传值 |
T const |
常量传值 |
T& |
左值引用,可修改 |
T const& |
常量左值引用 |
T&& |
右值引用,移动语义 |
T const&& |
常量右值引用,少用 |
4. 指针相关 T*
系列
写法 |
说明 |
T* |
指向可修改的对象 |
T* const |
常量指针,指针本身不可变 |
T*& |
指针的左值引用 |
T* const& |
常量指针的左值引用 |
T*&& |
指针的右值引用 |
T* const&& |
常量指针的右值引用 |
T const* |
指向常量对象的指针 |
T const* const |
常量指针指向常量对象 |
T const*& |
指向常量的指针的左值引用 |
T const* const& |
常量指针的左值引用 |
T const*&& |
指向常量的指针的右值引用 |
T const* const&& |
常量指针的右值引用 |
5. shared_ptr
及相关
写法 |
说明 |
shared_ptr |
按值传递,拷贝引用计数,轻量操作 |
shared_ptr const |
按值传递常量版本 |
shared_ptr& |
左值引用,可以修改智能指针本身 |
shared_ptr const& |
常量左值引用,不可修改指针本身但可读取 |
shared_ptr&& |
右值引用,移动所有权 |
shared_ptr const&& |
常量右值引用 |
shared_ptr |
指向不可变对象的 shared_ptr |
shared_ptr const |
按值传递常量版本的指向不可变对象的 shared_ptr |
shared_ptr& |
指向常量对象的 shared_ptr 左值引用 |
shared_ptr const& |
指向常量对象的 shared_ptr 常量左值引用 |
shared_ptr&& |
指向常量对象的 shared_ptr 右值引用 |
shared_ptr const&& |
指向常量对象的 shared_ptr 常量右值引用 |
总结
unique_ptr
传递时,最好用 unique_ptr&&
(右值引用) 来移动所有权。
shared_ptr
传递可以用按值传递(拷贝计数),或者用引用(左值引用/右值引用)来避免拷贝或移动。
T*
指针参数建议用指针或引用传递,区分是否指向 const 对象。
- 普通类型
T
传参方式根据性能和语义选择值传、引用或右值引用。
const
修饰通常表达“不修改”的语义。
传值参数(pass by value)时:
void foo(T);
和
void foo(T const);
两者在调用者看来是一样的,因为传值时,函数得到的是参数的一个拷贝。
- 传入的实参会被拷贝到函数的参数中。
const
修饰放在传值参数上,对调用者无影响,只告诉函数内部不能修改这个拷贝。
所以:
- 对调用者来说,两者没有区别。
- 编译器会把它们视作同一签名。
你如果想表达“调用者传进来之后,函数内部不能修改它”,写 T const
是可以的,但一般不写,因为不影响接口,反而容易让人迷惑。
这就是“const
放在传值参数上是给callee看的,不是caller”。
总结一下“传 const 引用” 和 “传值” 的区别:
传值:void foo(T);
- 调用时,参数会被拷贝一份。
- 函数内操作的是拷贝的副本,原始变量不受影响。
- 拷贝代价较大时(比如大对象),会影响性能。
传 const 引用:void foo(T const &);
- 参数是对原对象的只读引用。
- 函数内不能修改参数(
const
限制)。
- 没有拷贝开销,效率更高。
- 调用时传递的其实是对象地址,函数内通过解引用访问对象。
- 适合大对象或复杂类型。
汇编层面的区别
- 传值会把参数的值直接放到寄存器或栈里。
- 传 const 引用实际上传的是参数的地址(指针),函数通过地址去访问实际数据(需解引用)。
- 所以汇编里可以看到不同的指令和操作。
总结
方式 |
代价 |
修改参数 |
适用场景 |
传值 (T) |
拷贝开销 |
函数内修改不影响外部 |
小型内置类型(int, char等) |
传 const 引用 |
仅传地址,避免拷贝 |
不能修改 |
大对象或复杂类型,性能敏感时 |
代码和汇编对应关系,体现了一个带条件调用函数的示例,我帮你拆解说明:
C++代码示例(简化版)
void foo();
int do_work(int input) {
if (input > 0) {
foo();
}
return input;
}
汇编关键步骤解析
test edi, edi
:检测 input
(在寄存器 edi 中)是否为0。
jle .L2
:如果 input <= 0
,跳转到标签 .L2
,跳过调用 foo()
。
call foo
:调用函数 foo()
。
.L2:
标签后继续执行,返回 input
。
重要点理解
test
+ jle
是条件判断的典型指令组合。
- 调用
foo()
只有当 input > 0
时。
- 最后返回输入参数,没有修改。
- 汇编中保存/恢复栈(
push
和 pop
)确保调用前后环境一致。
你理解的要点
- C++代码如何映射为条件判断和函数调用的汇编。
- 条件跳转指令(
jle
)控制代码路径。
- 函数调用和返回的流程。
这段 C++ 代码及对应的汇编,展示了通过引用传参的函数do_work
,包含条件判断和函数调用的实现细节。下面我帮你逐步解释,方便你理解:
C++ 代码:
void foo();
auto do_work(int const& input) {
if (input > 0) {
foo();
return input;
}
return input;
}
汇编关键步骤分析:
do_work(int const&):
PTR [rdi] ; 读取 input 引用指向的值 (rdi 存放参数地址)
DWORD mov eax, [rdi] ; 将 input 的值加载到 eax 寄存器
test eax, eax ; 检测 eax 是否为0(等价于 if (input > 0) 判断)
jle .L4 ; 如果 eax <= 0,跳转到标签 .L4(跳过调用 foo)
push rbx ; 保存 rbx 寄存器
mov rbx, rdi ; 复制 rdi(input 的地址)到 rbx,为后续使用
call foo() ; 调用 foo 函数
DWORD mov eax, [rbx] ; 调用后重新加载 input 的值到 eax
pop rbx ; 恢复 rbx 寄存器
ret ; 返回,eax 里存 input 的值
.L4:
rep ret ; 直接返回,跳过调用 foo()
关键点解释:
int const& input
是传入参数的引用(地址在 rdi
),所以通过 [rdi]
访问其值。
test eax, eax
用来判断 input
是否大于 0。
jle .L4
是条件跳转:input <= 0
就跳过调用 foo()
。
push/pop rbx
是为了保护寄存器,调用函数前后要保证寄存器状态一致。
- 调用
foo()
后,再次读取 input
的值返回。
rep ret
是一种优化写法,表示快速返回。
你可以这么理解:
- 函数是通过引用传参的,所以要先解引用读取值。
- 条件判断通过测试寄存器实现。
- 函数调用被包裹在条件判断中。
- 返回值放在
eax
寄存器里。
如果需要,我能帮你画图解释流程,或者针对某条汇编指令展开讲解,或者帮你用其他例子演示。你想怎么学更有效?
这段代码和对应的汇编让我帮你整理下理解点:
C++代码简述:
void foo();
auto do_work(int const& input) {
if (input > 0) {
foo();
return input;
}
return input;
}
- 参数
input
是通过常量引用传递的。
- 如果
input
大于0,则调用 foo()
,然后返回 input
。
- 否则直接返回
input
。
汇编对应步骤:
do_work(int const&):
mov eax, DWORD PTR [rdi] ; 从 rdi(input 地址)解引用取值放入 eax
test eax, eax ; 测试 eax 是否为0(相当于判断 input 是否大于0)
jle .L4 ; 如果 eax <= 0,跳转到.L4(跳过调用foo)
push rbx ; 保存 rbx 寄存器(调用约定)
mov rbx, rdi ; 将 input 地址复制到 rbx 备用
call foo ; 调用 foo()
mov eax, DWORD PTR [rbx] ; 调用后重新加载 input 的值(作为返回值)
pop rbx ; 恢复 rbx 寄存器
ret ; 返回
.L4:
rep ret ; 直接返回,不调用 foo
理解要点:
- 参数传递:
input
是int const&
,传入时是地址,所以用[rdi]
解引用取值。
- 条件判断:
test eax, eax
是判断是否大于0,jle
表示如果不大于0就跳过调用。
- 保护寄存器:调用函数前保存和调用后恢复
rbx
寄存器,符合调用约定。
- 返回值:函数返回
input
的值,放在eax
寄存器。
总结:
这个示例展示了通过引用传参时的内存访问和条件跳转调用函数的编译器生成汇编代码。很典型的 if
+ 函数调用的低级实现。
梳理一下这些传参方式和它们的意义:
代码示例理解
int a;
void foo() {
++a;
}
int do_work(int const& input) {
if (input > 0) {
foo();
return input;
}
return input;
}
int bar() {
return do_work(a);
}
a
是全局变量,foo()
会对它自增。
do_work
接收一个 int const&
(常量引用),根据条件调用 foo()
并返回 input
。
bar()
调用 do_work
,传入的是变量 a
的引用。
传参最佳实践简述
传参方式 |
作用/含义 |
适用情况 |
void foo(T); |
传值。对参数做快照,调用者的变量不会被改动。 |
小型数据类型(如 int 、float )优选传值。 |
void foo(T const &); |
传常量引用。不修改参数,避免拷贝开销。 |
大型对象,且不需要修改,推荐使用。 |
void foo(T &); |
传引用。可以修改传入的参数。 |
需要修改参数,且不希望拷贝时使用。 |
void foo(T &&); |
传右值引用。用于“偷取”资源,实现移动语义。 |
移动语义中使用,避免不必要的拷贝。 |
void foo(T const &&); |
常量右值引用,没有实际意义,因为右值应该是可以修改的(可“移动”)。 |
不推荐使用,一般没有意义。 |
你提到的几条总结:
- Pass by value when you can, pass by const reference when you must.
尽量传值(简单小型数据),复杂对象且不需要修改时用 const &
。
- Pass by reference (
T &
) when you want to modify the argument.
需要修改参数时用非常量引用。
- Right value references (
T &&
) 是用来“偷取”资源(移动语义),const &&
没啥意义。
你的代码示例和最佳实践的联系
do_work(int const &input)
使用了常量引用传递,避免了传递大型对象的拷贝,同时保证了 input
不会被修改,符合最佳实践。
foo()
通过修改全局变量 a
,体现了修改状态的场景,这和传参方式无关,而是作用于全局变量。
你这部分讲的是指针参数传递和 std::optional
的区别,还有指针的常量性和引用,下面我帮你总结并详细解释:
传指针参数(Pass by pointer)
void foo(T *);
void foo(T const *);
- 指针参数传递 表示“可能有对象,也可能没有”(optional)。
- 传入指针可以是
nullptr
,表示“没有对象”。
- 但指针一般不适合用作输出参数的值,比如不建议用指针参数直接表达函数结果或生命周期管理。
- 不要用指针做生命周期管理,避免悬挂指针、内存泄漏等问题。
指针 vs std::optional
void foo(T *)
和 void foo(std::optional)
都能表达“参数可能有值也可能没有值”的语义,但实现和语义不同:
| 指针传递 | std::optional
|
| ----------------- | ----------------------------- |
| 传递对象地址或 nullptr
| 传递对象或空(std::nullopt
) |
| 可以直接修改传入对象 | std::optional
是值语义,修改的是副本 |
| nullptr
明确表示无对象 | 需要显式传递 std::nullopt
|
| 调用时要传地址:foo(&a)
| 传值调用:foo(a)
或 foo({})
表示空 |
| 可以用作输出参数(不推荐) | 设计用来表达有无值,不推荐作为输出参数 |
例子比较
void foo(int * t) {
*t = 3;
}
int i = 1;
foo(&i);
assert(i == 3);
void foo(std::optional<int> t) {
*t = 3;
}
int i = 1;
foo(i);
assert(i == 3);
传递 const
指针
void foo(std::mutex const*);
std::mutex m;
foo(&m);
void foo(std::optional<std::mutex const>);
std::mutex m;
foo(m);
传递指针的引用?
void foo(T * const &);
void foo(T const * const &);
- 对指针本身的常量引用(
T * const &
)很少用,也没有太大意义。
- 指针一般作为传入参数,不需要对指针变量本身做引用传递。
- 直接传指针就够了,没必要传指针的引用。
总结
传递方式 |
含义/用法 |
建议 |
T * |
可选参数,传递对象地址或 nullptr |
推荐用于表达“可选对象”的指针 |
T const * |
传递只读指针,不允许修改指向对象 |
保护不被修改的对象 |
std::optional |
可选值,非指针语义,修改的是副本 |
适合表达“可能有值”,但不适合修改调用者数据 |
T * const & |
指针本身的常量引用,一般没必要 |
不建议使用 |
这段内容讲的是用引用(l-value 或 r-value 引用)来传递指针参数时的注意点和常见误区
传递指向指针的右值引用(R-value reference to pointer)
void foo(T* &&);
void foo(T const* &&);
- 指针类型
T*
本质是一个“原始值”(primitive value),就是一个地址(数值)。
- 对原始值传递右值引用没啥意义,因为右值引用通常用于可移动对象的“移动语义”。
- 指针的右值引用其实就是拷贝一个指针地址,没什么额外好处。
- 所以传指针的右值引用在实践中没有实际意义,不推荐使用。
传递指向指针的左值引用(L-value reference to pointer)
void foo(T* &);
void foo(T const* &);
- 这会传递“指针的引用”,也就是说你可以修改指针本身指向的地址(“reseating a pointer”,重新指向其他地址)。
- 这相当于函数能“修改调用者指针的指向”,非常危险:
- 容易造成指针悬挂(悬挂指针)
- 破坏对象生命周期管理,导致内存问题
- 因此,绝对不要这样做,除非你很清楚自己在干什么。
- 这种做法极易引发管理和安全问题,实践中应避免。
总结
传递方式 |
说明 |
建议 |
T* && |
指针的右值引用,没意义 |
不要用 |
T* & |
指针的左值引用,能修改指针指向 |
非常危险,避免使用 |
如果你想管理指针生命周期,或者表达“可选”含义,用 智能指针(unique_ptr ,shared_ptr )或者 std::optional 会更安全且语义明确。 |
|
|
关于函数参数中传递unique_ptr
及相关类型时所有可能的方式。下面帮你梳理和解释这些参数传递的种类,方便理解:
函数参数中传递 unique_ptr
及相关类型的所有方式
假设有函数:
void f(???);
你想知道 ???
可以有哪些类型涉及 T
、unique_ptr
、shared_ptr
、指针等。
1. 传递普通类型 T
T
— 按值传递,拷贝
T &
— 按左值引用传递,可修改
T &&
— 按右值引用传递,可移动或转发
T const
— 按值传递常量(和T
等价)
T const &
— 按常量左值引用传递,避免拷贝且不可修改
T const &&
— 按常量右值引用传递,较少用
2. 传递智能指针相关类型
unique_ptr:
unique_ptr
— 按值传递,转移所有权(move only)
unique_ptr &
— 左值引用,修改所有权(通常不推荐)
unique_ptr &&
— 右值引用,移动所有权
unique_ptr const
— 常量对象(基本不用,因为unique_ptr通常非拷贝且非常态)
unique_ptr const &
— 常量左值引用,不转移所有权,观察
unique_ptr const &&
— 常量右值引用,没太大用
unique_ptr
— 管理指向常量的unique_ptr
unique_ptr &
— 左值引用,管理指向常量
unique_ptr const
— 常量指向常量
unique_ptr const &
— 常量左值引用,观察指向常量
unique_ptr const &&
— 常量右值引用
unique_ptr &&
— 右值引用,移动指向常量
shared_ptr:
shared_ptr
— 共享所有权,按值传递增加引用计数
shared_ptr &
— 左值引用,修改shared_ptr
shared_ptr &&
— 右值引用,移动所有权
shared_ptr const
— 常量shared_ptr对象
shared_ptr const &
— 常量左值引用,观察共享所有权
shared_ptr const &&
— 常量右值引用
shared_ptr
— 管理指向常量
shared_ptr &
— 左值引用,管理指向常量
shared_ptr &&
— 右值引用,移动指向常量
shared_ptr const
— 常量指向常量
shared_ptr const &
— 常量左值引用观察
shared_ptr const &&
— 常量右值引用观察
3. 传递裸指针相关
T *
— 普通裸指针,可选传递指向可修改对象
T * &
— 指向裸指针的左值引用,可修改指针本身(不推荐)
T * &&
— 指向裸指针的右值引用(没啥意义)
T const *
— 指向常量的裸指针
T const * &
— 指向常量的裸指针的左值引用
T * const
— 常量指针(指针本身不可变,但指向的可变)
T * const &
— 常量指针的左值引用
T * const &&
— 常量指针的右值引用
T const * const
— 指向常量的常量指针
T const * const &
— 指向常量的常量指针的左值引用
T const * &&
— 指向常量的裸指针右值引用(少用)
T const * const &&
— 指向常量的常量指针右值引用(更少用)
4. 其他(比如 unique_ptr
等)
unique_ptr
— unique_ptr管理的对象是常量
shared_ptr
— shared_ptr管理常量对象
总结
- 基本类型
T
和引用(左值、右值) 是最常见的。
unique_ptr
通常按值传递(移动所有权)或按右值引用传递。
shared_ptr
支持拷贝传递,按值传递时会增加引用计数。
- 裸指针参数常用于表示可选或非拥有指针,尽量避免传裸指针引用修改指针本身。
- 带
const
修饰的类型表示只读(不可修改)语义。
你这部分内容主要讲的是智能指针(尤其是 unique_ptr
)的传递和生命周期管理,结合新旧写法的对比,特别关注了所有权转移和观察的语义。下面帮你整理理解:
智能指针与生命周期管理 理解
1. 智能指针类型
unique_ptr
- 表示 唯一所有权,拥有对象生命周期。
- 传递时通常需要 转移所有权(move语义)。
shared_ptr
- 表示 共享所有权,对象生命周期由多个所有者共同管理。
- 传递时可以按值拷贝,增加引用计数。
2. unique_ptr
的传递方式
- 按值传递(转移所有权)
void foo(unique_ptr<T>);
void foo(unique_ptr<T const>);
- 按常量左值引用传递(观察,不转移所有权)
void foo(unique_ptr<T> const &);
void foo(unique_ptr<T const> const &);
- 按右值引用传递(移动所有权)
void foo(unique_ptr<T> &&);
void foo(unique_ptr<T const> &&);
3. 旧 C++98 写法 vs 新 C++11 写法
- 旧写法:返回裸指针,使用者负责释放
struct Bar {
static Bar* make_Bar(std::string);
};
Bar* my_b = Bar::make_Bar("cat");
log_stats(my_b);
- 新写法:返回
unique_ptr
,自动管理生命周期struct Bar {
static std::unique_ptr<Bar> make_Bar(std::string);
};
auto my_b = Bar::make_Bar("cat");
log_stats(my_b.get());
- 也可以传
unique_ptr
的 const 引用,表示观察:void log_stats(std::unique_ptr<Bar> const &);
log_stats(my_b);
4. 关于裸指针 T*
5. 总结建议
- 转移所有权时用
unique_ptr
按值传递或右值引用传递。
- 仅观察时用
unique_ptr const &
或裸指针 T*
。
- 避免传递裸指针管理生命周期。
shared_ptr
用于需要共享所有权的场景。
unique_ptr&&
代表“移动”,但 unique_ptr&&
不适合作为常规传参(只能接受右值)。
这段内容讲的是 shared_ptr
的传递方式和语义,结合了智能指针的所有权管理。帮你整理理解如下:
传递 shared_ptr
相关参数的理解
1. 按值传递 shared_ptr
void foo(std::shared_ptr<T>);
void foo(std::shared_ptr<T const>);
- 含义:共享所有权
- 函数调用时会 增加引用计数,函数内部持有该对象的所有权
- 使用场景:当函数需要 延长生命周期 或持有资源时
2. 按常量左值引用传递 shared_ptr const &
void foo(std::shared_ptr<T> const &);
void foo(std::shared_ptr<T const> const &);
- 含义:选择性共享所有权(观察者)
- 不会增加引用计数,只是观察(访问)已有的共享所有权
- 用于避免不必要的引用计数开销
- 例子:
void do_work(std::shared_ptr<Foo> const & input) {
if (run_async) {
start_background_calc(input);
} else {
}
}
3. 按右值引用传递 shared_ptr &&
void foo(std::shared_ptr<T> &&);
void foo(std::shared_ptr<T const> &&);
- 一般不推荐
- 有些人称其为“Sink”,意即接收临时的共享指针
- 但实际中更建议用按值传递来“拿走”所有权
- 右值引用会增加代码复杂度,不如直接用按值传递方便
4. 按左值引用传递智能指针
void foo(std::unique_ptr<T> &);
void foo(std::shared_ptr<T> &);
- 含义:允许重新绑定智能指针(reseat)
- 很少用,可能用于改变指针所指向的对象
- 不常见,使用需谨慎
5. 总结建议
传递方式 |
语义 |
适用场景 |
备注 |
shared_ptr |
传值,增加引用计数,转移共享所有权 |
需要延长生命周期 |
最常用 |
shared_ptr const & |
观察者,不增加计数 |
只需要访问不想增加计数时 |
性能更好,避免不必要拷贝 |
shared_ptr && |
接收右值临时对象 |
不推荐,复杂度高 |
用得少 |
shared_ptr & |
重新绑定智能指针 |
很少用 |
需要谨慎 |
你给的这组内容是在总结不同类型参数传递方式,及其语义(快照、观察、修改、所有权转移等),并且结合智能指针(unique_ptr
和 shared_ptr
)的传递特性。帮你归纳成一个清晰的表格和要点,方便理解:
参数传递方式与语义总结
类型 |
传递语义 |
说明 |
使用频率 |
T |
Snapshot (快照) |
传值,拷贝一份 |
高 |
T const & |
Observe (观察) |
传递只读引用,避免拷贝 |
高 |
**T const *** |
Observe (观察,optional) |
指针,可空,指向只读对象 |
中等 |
T & |
Modify (修改) |
传递可修改的引用 |
中等 |
**T *** |
Modify (修改,optional) |
指针,可空,指向可修改对象 |
中等 |
T && |
Snapshot, Sink |
传递右值,允许移动 |
低 |
unique_ptr |
Transfer Ownership (转移所有权) |
只能通过右值传递,转移独占所有权 |
中等 |
unique_ptr |
Transfer Ownership |
指向const的唯一所有权 |
低 |
unique_ptr & |
Reseat (重新绑定) |
罕见,修改智能指针本身 |
低 |
unique_ptr & |
Reseat |
罕见 |
低 |
shared_ptr |
Share Ownership (共享所有权) |
传值增加引用计数 |
高 |
shared_ptr |
Share Ownership |
传值共享指向const对象 |
中等 |
shared_ptr const & |
Optionally Transfer Ownership (可选共享) |
只观察,不增加计数 |
高 |
shared_ptr const & |
Optionally Transfer Ownership |
只观察,只读 |
中等 |
shared_ptr & |
Reseat |
罕见,修改智能指针 |
低 |
shared_ptr & |
Reseat |
罕见 |
低 |
语义解释
- Snapshot(快照)
传值,函数内部得到参数的副本,对外部无影响。
- Observe(观察)
传递const引用或const指针,函数只能读,不能改。
- Modify(修改)
传递非const引用或指针,允许函数修改调用者对象。
- Transfer Ownership(转移所有权)
对 unique_ptr
通过右值传递,实现所有权转移。
- Share Ownership(共享所有权)
对 shared_ptr
传值或传引用,实现共享所有权。
- Optionally Transfer Ownership(可选共享)
传递 shared_ptr
的const引用,只观察共享指针,无增加引用计数。
- Reseat(重新绑定)
传递智能指针的非const引用,允许修改智能指针本身指向。
简单总结
- 普通类型,尽量用传值或传const引用。
- 需要修改,用非const引用。
- 指针类型用于可空,optional语义,或者需要修改指向内容。
unique_ptr
用右值传递转移所有权,用const引用观察,非const引用罕见。
shared_ptr
传值实现共享所有权,传const引用观察,非const引用罕见。
避免“意外”共享(Surprising Sharing) 的问题,尤其是当引用或指针被保存并在函数调用外使用时,可能导致悬空引用或生命周期错误。
关键点总结:
1. 直接传引用,容易产生“意外”共享
void foo(Bar& input) {
global = std::make_unique<Oops>(input);
}
void some_work() {
Bar my_b;
foo(my_b);
}
foo
接收了一个 Bar&
,并把它传给 Oops
保存。
some_work
中的 my_b
生命周期结束后,global
持有的引用变成了悬空引用。
- 这是“意外共享”:引用被保存到函数外,但原对象生命周期却结束了。
2. 使用智能指针(shared_ptr + weak_ptr)管理生命周期
class Oops {
std::weak_ptr<Bar> b;
};
std::unique_ptr<Oops> global;
void foo(std::shared_ptr<Bar> input) {
global = std::make_unique<Oops>(input);
}
void some_work() {
auto my_b = std::make_shared<Bar>();
foo(my_b);
}
- 使用
shared_ptr
传递共享所有权,保证对象不会提前销毁。
Oops
中用 weak_ptr
观察,不拥有对象,避免循环引用。
- 避免了悬空引用问题,实现了安全的生命周期管理。
3. 总结传参选择
作用 |
推荐用法 |
说明 |
不会在函数外保存参数 |
void foo(T&) ,void foo(T*) |
简单传递,生命周期由调用者管理 |
可能在函数外保存参数 |
void foo(std::shared_ptr) |
共享所有权,保证生命周期安全 |
不允许修改 |
void foo(T const&) ,void foo(T const*) |
只读传递 |
你的代码理解示范
- 避免直接用裸引用或裸指针保存状态,容易悬空。
- 使用智能指针表达所有权和观察关系,明确生命周期。
理解了,这部分总结了传参和共享的设计原则,以及各种参数类型的用途和意义,帮你理清调用约定和对象生命周期管理的“套路”。
重点总结:
1. 明确参数类型的含义
- 传值 (T):适合简单类型或能轻松复制的类型,传递“快照”。
- 传常量引用 (T const&):避免复制,观察参数,不修改它。
- 传引用 (T&):允许修改参数。
- 传指针 (T, T const)**:表示“可选”传递(可以传nullptr),但不管理生命周期。
2. 智能指针的传递方式
unique_ptr
:表示“所有权转移”,参数通常传 右值引用 unique_ptr&&
,表明函数接管资源。
shared_ptr
:表示“共享所有权”,函数通常传 值传递 shared_ptr
或 常量引用 shared_ptr const&
,取决于是否需要复制智能指针。
- 避免裸指针或引用用于共享对象的生命周期管理,用智能指针明确表达。
3. 不要通过裸指针(*
)或引用(&
)共享所有权
- 共享对象时,要用
shared_ptr
来表达共享所有权。
- 传递裸指针或引用只表示“访问”,不承担生命周期管理。
4. 返回值
- 函数应尽量通过返回值来传递结果,而不是通过修改参数。
5. 频率与场景
参数类型 |
含义 |
典型用例 |
T |
Snapshot(传值快照) |
简单数据类型,复制代价低 |
T const & |
观察(只读) |
复杂类型,避免复制 |
T & |
修改 |
函数需要修改传入参数 |
T * / T const * |
可选参数(可能为空) |
指针传递,标识参数可选 |
unique_ptr && |
所有权转移(Sink) |
资源的唯一所有权转移 |
shared_ptr / shared_ptr const & |
共享所有权 |
多方共享资源 |
6. 典型函数签名示例
void foo(T);
void foo(T const &);
void foo(T &);
void foo(T *);
void foo(unique_ptr<T> &&);
void foo(shared_ptr<T>);
void foo(shared_ptr<T> const &);
总结一句:
当你设计函数的参数时,要想清楚:你是在传递一个快照?观察它?修改它?还是共享或转移资源的所有权?用合适的类型表达这个语义,避免“隐藏”的副作用。