写C++函数的时候,一旦参数类型是string这种"重量级"对象,怎么传参就成了一个绕不开的问题。很多人一开始都是"用起来能跑就行",但传参方式其实直接影响程序的性能和安全性。这篇文章把string、string&、const string、const string&这四种写法掰开揉碎讲清楚。
先看一个例子
csharp
void func(string s); // 值传递
void func(string& s); // 引用传递
void func(const string s); // const值传递
void func(const string& s); // const引用传递
int main() {
string name = "hello";
func(name);
}
调用func(name)时,这四种写法在底层做的事情完全不一样,接下来一个一个拆开看。
一、string s:值传递,拷贝一份新的
typescript
void func(string s) {
s += " world"; // 修改的是拷贝,不影响外面的name
}//这里的拷贝就是新创建了一个变量赋上s的值,这新的变量就叫拷贝
这种写法在调用时,会重新开辟一块内存空间 ,把传进来的name完整拷贝一份给形参s。函数内部对s的任何修改,都只是改了这个"复制品",函数结束后s被销毁,外面的name完全不受影响。
优点:
- 安全,函数内部怎么折腾都不会影响外面的变量,不用担心"意外修改了别人的东西"。
- 语义清晰,调用者一看就知道这个函数不会改自己传进去的变量。
缺点:
- 性能开销大。
string内部管理的是一块堆内存,拷贝意味着要重新申请内存、把字符内容整个复制一遍。如果字符串很长,或者这个函数被频繁调用(比如在循环里调用几万次),这个开销会非常明显。
适用场景:
- 字符串很短(比如短单词、短标识符),拷贝开销可以忽略。
- 你确实需要在函数内部得到一份独立的、可以随意修改的副本,且不想影响原变量。
- 追求接口的"绝对安全",宁可牺牲一点性能。
实际情况: 这种写法在生产代码里其实不算常用,因为大多数场景下没必要付出拷贝的代价,除非你明确需要一份独立副本。
二、string& s:引用传递,直接操作原变量
typescript
void func(string& s) {
s += " world"; // 直接修改了外面的name!
}//直接修改了调用函数时传进去的这个参数
这种写法不会开辟新的空间 。s只是name的一个"别名",它们指向同一块内存。函数内部对s做任何修改,实际上就是在修改外面的name本身。函数调用结束后,外面的name已经被改变了。
优点:
- 没有拷贝开销,性能好。
- 如果你的目的就是要修改传进来的变量(比如一个
trim(string& s)去掉字符串首尾空格的函数),引用传递是最直接、最高效的方式。
缺点:
- 危险。调用者传进去一个变量,函数内部却可能悄悄把它改了,如果调用者不知道这个函数会修改参数,很容易踩坑,导致意料之外的bug。
- 不能传常量或临时对象。比如
func("hello")这种字面量、或者func(a + b)这种临时结果,是不能绑定到非const引用上的,编译器会报错。
适用场景:
- 函数的目的本来就是要修改 传入的变量,比如
swap(string& a, string& b)、toUpper(string& s)这类"就地修改"的函数。 - 需要在函数内部返回多个"输出结果",用引用参数当作输出参数。
实际情况: 常用,但只在"确实需要修改原变量"的场景下用,用的时候最好在函数名或文档里明确说明会修改参数,避免调用者踩坑。
三、const string s:const值传递,拷贝一份不能改的常量
typescript
void func(const string s) {
// s += " world"; // 编译报错!s是const的,不能修改
}
这种写法同样会开辟新的内存空间 ,把传进来的变量拷贝一份,但拷贝出来的这份被标记成了const,也就是说函数内部不能修改这个拷贝。
优点:
- 既有值传递的安全性(不影响外部变量),又多了一层保护------防止你自己在函数内部不小心改了这个变量(比如写了
s = s + "x"却忘了这行代码本来不该出现)。
缺点:
- 该有的拷贝开销一分不少,性能上跟普通值传递一样差。
- 而且这个"不能修改"的限制只在函数内部实现中有意义,对调用者毫无影响------调用者根本感知不到形参是不是const的,因为函数声明和定义中的顶层const在函数签名里其实是没有区分作用的(这也是为什么很多编译器/规范建议不要在函数声明里写顶层const形参)。
适用场景:
- 说实话,这种写法非常不常用 。因为它既没有省掉拷贝的开销,又没有给调用者带来任何额外的接口保证,唯一的作用只是给函数内部实现者提个醒:"这个变量在函数体内不该被改"。
- 如果确实想要这种"防止自己手滑改错"的保护,通常直接在函数体内部用
const局部变量重新声明会更清晰,而不是写在参数列表里。
四、const string& s:const引用传递,既高效又安全
c
void func(const string& s) {
cout << s << endl;
// s += " world"; // 编译报错,不能修改
}
这是最值得展开讲的一种。const string& s传进去的本质上是原变量的地址 (引用的底层实现通常就是指针),没有开辟任何新的空间 去存字符串内容。函数内部用到的s,实际上操作的还是外面那个原始变量,只是加了一层"只读"的限制------你可以读它、用它,但不能通过s去修改它。
这也是它和string&最大的区别: string&是"直接修改了传进去的变量",而const string&是"用的还是原来的变量,但不让你改它"------既拿到了引用传递不拷贝的性能优势,又靠const守住了值传递的安全性。可以说是把前面三种写法的优点捏在了一起。
优点:
- 零拷贝开销,无论字符串多长,传参的成本都只是传一个引用(本质上类似传一个指针),跟传int差不多快。
- 安全,函数内部不可能修改传进来的原变量,调用者可以放心传参,不用担心变量被"偷偷改掉"。
- 可以接收临时对象和常量字符串 ,比如
func("hello")、func(a + b)这种字面量和临时结果都可以传给const string&(因为const引用可以绑定到临时对象上,这是C++语言规则里的一个特殊许可),而普通的string&是做不到这一点的。
缺点:
- 几乎没有明显缺点,唯一要注意的是:如果函数确实需要修改参数或者需要一份独立副本,这种写法就不适用了,硬套上去反而受限。
适用场景:
- 只读、不修改参数的函数,这是C++里最常见的场景,比如打印、比较、计算长度、查找子串、拼接生成新字符串(返回值是新的,参数本身不变)等。
- 几乎所有"传入一个字符串,函数只是使用它而不修改它"的场景,都应该优先用这个。
实际情况: 这是最常用 的写法,可以说是C++里传递string(以及绝大多数非基本类型对象,比如vector、自定义类)的默认首选方案。
总结对比
| 写法 | 是否拷贝 | 能否修改原变量 | 能否接收临时对象 | 常用程度 |
| string s | 是,开辟新空间 | 不能 | 能 | 一般,仅在需要独立副本时用 |
| string& s | 否 | 能,直接修改 | 不能 | 常用,仅在需要修改原变量时用 |
| const string s | 是,开辟新空间 | 不能(对内部也限制) | 能 | 很少用,收益不明显 |
| const string& s | 否 | 不能 | 能 | 最常用,默认首选 |
一句话选择原则
- 需要修改 传进来的变量 → 用
string& - 只需要读取 传进来的变量,不修改 → 用
const string&(默认首选) - 需要在函数内部得到一份完全独立、可以随意改造的副本 → 用
string(按值传递,甚至可以考虑传入后用std::move) const string(值传递)基本可以从你的工具箱里划掉,几乎没有它明显优于其他三种的场景
理解了这四种传参方式背后"到底有没有拷贝、拷贝的是谁、能不能改"的本质区别,以后写函数签名的时候就不会再纠结了------大部分时候直接甩一个const string&就是最优解,只有在明确需要修改原变量或者需要独立副本时,才切换成string&或string。