C++ string 和string_view的区别和用法

核心区别

一句话先记住:

  1. string 是"拥有数据"的字符串。
  2. string_view 是"只看不拥有"的字符串视图。

也就是说:

  1. string 自己管理字符内存,像"房主"。
  2. string_view 只是指向一段字符范围,像"租客看房",它不负责这段内存的生死。

1. string 是什么

string 的特点:

  1. 自己持有字符串内容。
  2. 可以修改内容。
  3. 通常会分配和管理内存。
  4. 生命周期由对象自己控制。

例子:

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    std::string s = "hello";
    s += " world";
    std::cout << s << std::endl;
}

这里 s 真正保存了 hello world 这段数据。


2. string_view 是什么

string_view 是 C++17 引入的,表示"对一段字符串的只读观察"。

它的特点:

  1. 不拥有字符串。
  2. 不负责分配和释放内存。
  3. 一般只保存两个东西:
    • 一个指针
    • 一个长度
  4. 不能直接修改字符内容。

例子:

cpp 复制代码
#include <iostream>
#include <string_view>

int main() {
    std::string_view sv = "hello world";
    std::cout << sv << std::endl;
}

这里 sv 并没有拷贝 hello world,它只是指向这段字符数据。


3. 最本质的区别

可以用这个表来记:

对比项 string string_view
是否拥有数据
是否管理内存
是否可修改内容
是否可能发生拷贝 通常不会
适合长期保存吗 适合 要看原数据是否还活着
适合做只读参数吗 可以 更合适

4. 一个直观类比

假设有一本书:

  1. string 像你复印了一整本书,副本归你自己管。
  2. string_view 像你拿着目录卡,只记录"从第几页到第几页"。

所以:

  1. string 更安全,因为数据归自己。
  2. string_view 更轻量,因为不用复制。

5. 用法上的最大区别:生命周期

这部分最重要。

string_view 不拥有数据,所以它依赖原字符串继续存在。

安全例子:

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string s = "hello";
    std::string_view sv = s;

    std::cout << sv << std::endl;
}

这里安全,因为 s 还活着,sv 指向的内容有效。

危险例子:

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>

std::string_view bad() {
    std::string s = "hello";
    return s;
}

int main() {
    std::string_view sv = bad();
    std::cout << sv << std::endl;
}

这里有问题,因为:

  1. bad 里的 s 是局部变量。
  2. 函数返回后 s 已经销毁。
  3. sv 变成悬空视图。

所以 string_view 最大的坑就是:

它自己不保活数据。


6. 什么时候用 string

适合这些场景:

  1. 你需要拥有这段字符串。
  2. 你要修改字符串内容。
  3. 你要长期保存字符串。
  4. 你要拼接、插入、删除。
  5. 你不想操心原数据生命周期。

例如:

cpp 复制代码
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names;
    std::string name = "alice";
    names.push_back(name);
}

这里放进容器里,通常应该存 string,而不是存 string_view。

因为容器需要自己拥有数据,不能赌外部字符串一直有效。


7. 什么时候用 string_view

适合这些场景:

  1. 函数参数只读,不需要修改。
  2. 不想为传参产生额外拷贝。
  3. 只想看某一段字符串。
  4. 做解析、切片、查找时追求轻量。

最典型的是函数参数:

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>

void print(std::string_view sv) {
    std::cout << sv << std::endl;
}

int main() {
    std::string s = "hello";
    print(s);
    print("world");
}

这个接口很好,因为它既能接:

  1. std::string
  2. 字符串字面量
  3. std::string_view

而且一般不需要额外拷贝。

这也是现代 C++ 里 string_view 最常见的用法。


8. 作为函数参数时怎么选

这是实战里最有价值的地方。

如果函数只是读取字符串内容,优先考虑 string_view:

void log(std::string_view msg);

优点是:

  1. 调用方传 string 可以。
  2. 传字面量也可以。
  3. 不需要额外构造一个新的 string。

如果函数需要拥有或保存参数,应该用 string:

cpp 复制代码
class User {
public:
    void setName(std::string name) {
        name_ = std::move(name);
    }

private:
    std::string name_;
};

因为这里类成员要长期保存名字,所以必须自己拥有数据。


9. 子串操作的区别

string 的 substr 会构造新字符串,通常会拷贝。

例子:

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    std::string s = "hello world";
    std::string sub = s.substr(0, 5);
    std::cout << sub << std::endl;
}

这里 sub 是新的字符串对象。

string_view 的 substr 只是生成一个新的视图,一般不拷贝:

cpp 复制代码
#include <iostream>
#include <string_view>

int main() {
    std::string_view sv = "hello world";
    std::string_view sub = sv.substr(0, 5);
    std::cout << sub << std::endl;
}

这里 sub 只是"继续指向原来的那块数据中的前 5 个字符"。

所以做解析、切片时,string_view 很高效。


10. 性能上的区别

一般来说:

  1. string 复制时可能拷贝字符数据。
  2. string_view 复制时通常只复制指针和长度,代价很小。

例如:

cpp 复制代码
#include <string>
#include <string_view>

void f1(std::string s) {
}

void f2(std::string_view sv) {
}

如果你频繁调用只读函数:

  1. f1 可能产生字符串拷贝。
  2. f2 通常只是传一个轻量视图。

所以在"只读入参"场景里,string_view 常常更高效。

但要注意:

性能提升的前提是你真的只是读,不是存,不是改,不是跨生命周期保留。


11. string_view 不是 C 风格字符串

这是一个很容易踩坑的点。

string_view 记录的是:

  1. 指针
  2. 长度

它不保证末尾一定有 '\0',尤其是看某个子串时更明显。

例如:

cpp 复制代码
#include <iostream>
#include <string_view>

int main() {
    std::string_view sv = "hello world";
    std::string_view sub = sv.substr(0, 5);

    std::cout << sub << std::endl;
}

sub 表示 hello,长度是 5。

但它的 data() 后面不一定正好就是字符串结尾。

所以如果你把 string_view 直接丢给只接受 C 字符串的接口,就可能出问题。

比如这类接口通常要求:

  1. 数据以 '\0' 结尾
  2. 并且读到 '\0' 为止

而 string_view 只是"知道长度",不是"保证以 '\0' 截止"。

如果必须传给 C 接口,通常要先转成 string:

cpp 复制代码
std::string tmp(sub);
c_api(tmp.c_str());

12. string_view 不能修改内容

它是只读视图。

下面这种不行:

cpp 复制代码
std::string_view sv = "hello";
// sv[0] = 'H';   // 不允许

如果你需要改内容,就该用 string。


13. 返回值怎么选

这个点也很重要。

如果函数返回的是"新构造的数据",通常返回 string:

std::string makeName() {

return "alice";

}

如果函数返回的是"对已有长生命周期数据的视图",可以返回 string_view:

std::string_view getTag() {

return "INFO";

}

这个例子安全,因为字符串字面量的生命周期很长。

但是不要返回指向局部 string 的 string_view:

std::string_view bad() {

std::string s = "abc";

return s;

}

这一定有风险。

所以简单规则是:

  1. 返回新数据,用 string。
  2. 返回已有稳定数据的只读观察,用 string_view。
  3. 只要生命周期不确定,就别轻易返回 string_view。

14. 实战建议

可以直接记这几条:

  1. 读参数时优先考虑 string_view。
  2. 需要拥有、修改、保存时用 string。
  3. 不要把 string_view 存起来,除非你非常确定原数据活得更久。
  4. 不要随便把 string_view.data() 当成 C 字符串。
  5. 解析文本、切片、查找时,string_view 很有优势。

15. 一个综合示例

下面这个例子能把两者的典型用法串起来:

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>

void printPrefix(std::string_view text) {
    std::string_view prefix = text.substr(0, 5);
    std::cout << prefix << std::endl;
}

class Person {
public:
    void setName(std::string name) {
        name_ = std::move(name);
    }

    void show() const {
        std::cout << name_ << std::endl;
    }

private:
    std::string name_;
};

int main() {
    std::string s = "hello world";

    printPrefix(s);
    printPrefix("abcdefg");

    Person p;
    p.setName(s);
    p.show();
}

这里:

  1. printPrefix 只是读字符串,所以参数用 string_view。
  2. Person 要保存名字,所以成员用 string。
  3. 这是两者最典型的职责分工。

一句话总结

string 解决"我要拥有这段字符串"的问题。

string_view 解决"我只想高效地看这段字符串"的问题。

函数传参时,const string& 和string_view的区别的效率

如果函数只是"读字符串、不保存、不修改",在 C++17 以后一般优先用 std::string_view。

但"效率"要分场景看,不是所有情况下 string_view 都明显更快。

一、两者本质区别

  1. const std::string&

    表示"引用一个 std::string 对象"。

  2. std::string_view

    表示"查看一段字符串范围",通常内部就是:

    1. 一个字符指针
    2. 一个长度

所以它不要求调用方一定传 std::string,也可以传:

  1. std::string
  2. 字符串字面量
  3. const char*
  4. 某段子串视图

二、效率对比的关键结论

场景 1:传入的本来就是 std::string

cpp 复制代码
void f1(const std::string& s);
void f2(std::string_view s);

std::string name = "hello";
f1(name);
f2(name);

这种情况下,两者效率通常非常接近。

原因:

  1. const std::string& 本质上是传一个引用,成本很低。
  2. std::string_view 会从 name 构造一个小视图,通常只是取出 data 和 size,成本也很低。

实际工程里,这个差距通常小到可以忽略。

可以粗略理解为:

  1. const std::string& 是"传一个地址"
  2. std::string_view 是"传一个指针 + 一个长度"

在 64 位平台上,string_view 往往就是 16 字节左右,也很轻。

所以:

对现成 std::string 来说,两者性能差不多,不要指望这里有数量级差异。


场景 2:传入的是字符串字面量或 const char*

cpp 复制代码
f1("hello");
f2("hello");

这里 string_view 通常更高效。

原因是:

  1. 传给 const std::string& 时,往往需要先构造一个临时 std::string
  2. 这个临时 string 要做长度计算,还可能分配内存并复制字符
  3. 传给 string_view 时,只需要形成一个视图,不需要复制字符

所以这类调用里,string_view 的优势很明显。

不过有一个细节:

  1. string_view 从 const char* 构造时,通常也要扫一遍找到结尾的 '\0'
  2. 但它不分配内存、不复制整段数据

因此总体上仍然通常比构造 std::string 更省。


场景 3:传子串、切片、解析文本

这是 string_view 优势最大的地方之一。

看下面两种写法:

cpp 复制代码
std::string s = "hello world";

auto a = s.substr(0, 5);   // a 是新的 std::string,通常会拷贝
std::string_view b(s.data(), 5);  // b 只是个视图,不拷贝

如果你的函数参数是:

cpp 复制代码
void g(const std::string& s);

那你想传一个子串时,很多时候得先构造一个新的 std::string。

如果你的函数参数是:

cpp 复制代码
void g(std::string_view s);

那就可以直接传某段视图,通常零拷贝。

所以在"字符串切片、协议解析、日志解析、命令行解析"这类场景里,string_view 往往更高效。


三、为什么很多人说 string_view 更适合做只读参数

因为它的接受范围更广,而且通常避免不必要构造。

例如:

cpp 复制代码
void print(std::string_view s);

这个接口可以很自然地接收:

  1. std::string
  2. "hello"
  3. const char*
  4. 某个子串视图

如果你写成:

cpp 复制代码
void print(const std::string& s);

那么调用方如果传字面量,通常就要走一次临时 string 构造。

所以从"接口通用性 + 避免额外分配"两点看,string_view 常常更适合作为只读入参。


四、const std::string& 什么时候并不差

不要误以为 const std::string& 就"落后"了。

如果你的调用方几乎总是传现成的 std::string,比如:

  1. 业务代码里早就把数据存在 string 里
  2. 调用链上所有函数都处理 string
  3. 你也不需要接收字面量、子串视图、char*

那么 const std::string& 的性能完全可以接受,而且语义也直接。

也就是说:

如果输入几乎总是 std::string,本身没有什么额外构造,const std::string& 和 string_view 在效率上通常没什么本质差距。


五、真正的差异不只在效率,还在语义

这是实战里更重要的一点。

const std::string& 的语义更像:

"我接收的是一个字符串对象。"

std::string_view 的语义更像:

"我接收的是一段只读字符序列。"

所以 string_view 更像"泛化的只读文本参数"。


六、string_view 的效率优势,换来的代价是什么

代价就是:它不拥有数据。

这意味着它更容易踩生命周期问题。

例如:

cpp 复制代码
std::string_view bad() {
    std::string s = "hello";
    return s;
}

这是错的,因为 s 已经销毁了,返回的 view 悬空。

但如果参数只是函数调用期间临时使用,一般没问题:

cpp 复制代码
void print(std::string_view s) {
    std::cout << s << '\n';
}

print(std::string("hello"));

这次调用是安全的,因为那个临时 string 会活到整个函数调用结束。

所以要记住:

  1. string_view 很适合"只读参数"
  2. 不适合"长期保存,除非你非常确定原数据生命周期"

七、什么时候反而更适合 const std::string&

这几种情况里,const std::string& 往往更稳一些:

  1. 你的接口明确只接受 std::string
  2. 你内部需要依赖它是一个真正的 string 对象
  3. 你后面要把它传给要求以 '\0' 结尾的 C 接口,而且希望语义更直接
  4. 团队对 string_view 生命周期坑不熟,接口希望更保守

补一个细节:

string_view 的 data() 不等于"这是一个安全的 C 字符串"。

因为 view 只保证"指针 + 长度",不保证这段视图末尾正好是 '\0'。


八、面试或实战可以怎么回答"效率区别"

可以直接这样答:

如果调用方本来就传 std::string,那么 const std::string& 和 std::string_view 的传参成本都很低,性能通常接近;但如果调用方传的是字符串字面量、const char* 或子串,std::string_view 通常更高效,因为它不需要构造临时 std::string,也避免了额外的内存分配和字符拷贝。

所以在只读参数场景下,string_view 的优势主要体现在"减少不必要构造和拷贝",而不是在"引用传参"这一步本身比 const std::string& 快很多。


九、实战建议

可以直接按这个规则用:

  1. 只读、不保存、不修改:优先 std::string_view
  2. 需要拥有、保存、修改:用 std::string
  3. 输入几乎总是 std::string,而且团队更看重稳妥简单:const std::string& 也完全可以
  4. 做文本解析、切片、协议处理:优先 string_view,收益通常更明显

一句话总结

const std::string& 和 std::string_view 在"传现成 std::string"时效率通常差不多;

string_view 真正的效率优势在于:它能避免把字面量、char*、子串先转成临时 std::string 的那一步。

string_view 常见生命周期坑点示例版

最常见的坑就一句话:

string_view 不拥有数据,所以只要底层字符先死了,或者底层存储位置变了,view 就悬空了。

下面把最容易踩的几类坑直接列出来,每类都给你一个错误示例和正确写法。

1. 返回局部 string 的 string_view

错误:

cpp 复制代码
#include <string>
#include <string_view>

std::string_view bad() {
    std::string s = "hello";
    return s;
}

原因:

  1. s 是局部变量。
  2. 函数结束后 s 已经析构。
  3. 返回的 string_view 指向无效内存。

正确做法一:直接返回 string

cpp 复制代码
std::string good() {
    std::string s = "hello";
    return s;
}

正确做法二:只返回长生命周期数据的 view

cpp 复制代码
std::string_view good() {
    return "hello";
}

因为字符串字面量生命周期足够长。


2. 从临时 string 构造 string_view

错误:

cpp 复制代码
#include <string>
#include <string_view>

int main() {
    std::string_view sv = std::string("hello");
}

原因:

  1. 右边这个临时 string 在这一行结束后就销毁。
  2. sv 立刻变成悬空视图。

类似错误还有:

cpp 复制代码
std::string_view sv = std::to_string(123);
std::string_view sv = a + b;

因为 to_string 和字符串拼接都会产生临时 string。

正确做法:

cpp 复制代码
std::string s = std::string("hello");
std::string_view sv = s;

或者如果只是想临时传参,直接传给函数,不要存起来:

cpp 复制代码
void print(std::string_view sv);

print(std::string("hello"));

这个是安全的,因为临时 string 会活到整个函数调用结束。

关键区别是:

  1. 直接传参,通常安全
  2. 保存到变量里长期用,通常危险

3. 对 std::string 调 substr 后再赋给 string_view

这是很经典的坑。

错误:

cpp 复制代码
#include <string>
#include <string_view>

int main() {
    std::string s = "hello world";
    std::string_view sv = s.substr(0, 5);
}

很多人以为 sv 指向 s 的前 5 个字符,其实不是。

原因:

  1. s.substr(0, 5) 返回的是一个新的 std::string 临时对象。
  2. 这个临时对象在语句结束后销毁。
  3. sv 悬空。

正确写法一:先有 view,再对 view 做 substr

cpp 复制代码
std::string s = "hello world";
std::string_view sv = std::string_view(s).substr(0, 5);

正确写法二:

cpp 复制代码
std::string s = "hello world";
std::string_view sv(s.data(), 5);

前提是你确定长度没问题。


4. 把 string_view 存进容器,但底层 string 已失效

错误:

cpp 复制代码
#include <string>
#include <string_view>
#include <vector>

int main() {
    std::vector<std::string_view> vec;

    {
        std::string s = "hello";
        vec.push_back(s);
    }

    // 这里 vec[0] 已经悬空
}

原因:

  1. vec 里存的不是字符串副本。
  2. 它只是记住了 s 的地址和长度。
  3. s 离开作用域就没了。

正确做法一:容器里存 string

cpp 复制代码
std::vector<std::string> vec;
std::string s = "hello";
vec.push_back(s);

正确做法二:只有在你能保证原字符串长期有效时,才存 string_view。

例如原数据来自全局配置、静态字符串池、长生命周期缓存。


5. 原 string 修改后导致 view 失效

错误:

cpp 复制代码
#include <string>
#include <string_view>
#include <iostream>

int main() {
    std::string s = "hello";
    std::string_view sv = s;

    s += " world";

    std::cout << sv << '\n';
}

为什么危险:

  1. string 扩容后可能重新分配内存。
  2. sv 还指向旧地址。
  3. 旧地址可能已经失效。

要注意,不只是拼接,下面这些操作也可能让 view 失效:

  1. append
  2. push_back
  3. reserve
  4. assign
  5. 非小范围修改导致重分配

更稳妥的做法:

  1. 如果后面还要改 string,就不要长期保留它的 string_view。
  2. 修改完 string 之后,重新生成 view。

例如:

cpp 复制代码
std::string s = "hello";
s += " world";
std::string_view sv = s;

6. 把 string_view 传到异步任务里

这和回调、线程那类生命周期问题是一样的。

错误:

cpp 复制代码
#include <string>
#include <string_view>
#include <thread>
#include <chrono>
#include <iostream>

void asyncPrint(std::string_view sv) {
    std::thread([sv] {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << sv << '\n';
    }).detach();
}

int main() {
    std::string s = "hello";
    asyncPrint(s);
}

如果 main 很快结束,或者 s 先销毁,线程里的 sv 就悬空。

更安全的做法是在线程里持有 string:

cpp 复制代码
void asyncPrint(std::string s) {
    std::thread([s = std::move(s)] {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << s << '\n';
    }).detach();
}

经验规则很简单:

  1. 同步只读参数,string_view 很适合
  2. 异步保存、延迟执行,优先自己持有 string

7. 误把 data() 当 C 字符串

错误思路不是语法错,而是语义错。

看这个例子:

cpp 复制代码
#include <string_view>
#include <cstdio>

int main() {
    std::string_view sv = "hello world";
    std::string_view sub = sv.substr(0, 5);

    std::printf("%s\n", sub.data());
}

你想打印 hello,但这段代码不可靠。

原因:

  1. string_view 只保证一段字符范围。
  2. sub.data() 指向的是 h 的位置。
  3. 但 sub 只"长度为 5",并不保证第 6 个字符就是字符串结束符。
  4. printf 按 C 字符串规则会一直读到遇到 0 为止。

所以它可能打印成 hello world,而不是 hello。

正确做法:

cpp 复制代码
std::string tmp(sub);
std::printf("%s\n", tmp.c_str());

或者使用支持长度的接口。


8. 指向容器内部字符数据,容器变化后失效

例如:

cpp 复制代码
std::string s = "abcdef";
std::string_view sv = s;

s.clear();

此时 sv 也不该再用了。

或者:

s.resize(1000);

如果发生重分配,sv 同样可能失效。

你可以把 string_view 理解成:

它只是借住在别人家里,房东搬家、拆房、清空房间,你这张地址卡就没用了。


9. 一个容易误判但其实安全的场景

这个很多人会误会。

下面是安全的:

cpp 复制代码
void print(std::string_view sv) {
    std::cout << sv << '\n';
}

int main() {
    print(std::string("hello"));
}

原因:

  1. 临时 string 的生命周期会延续到整个函数调用结束。
  2. print 里面立刻用完 sv,没有保存它。
  3. 所以没问题。

但如果你在 print 里把 sv 存到全局变量、成员变量、异步任务里,就又危险了。

所以关键不在"参数是不是临时对象",而在"你有没有把 view 留到调用结束之后"。


10. 最实用的判断规则

每次想用 string_view,都先问自己 3 个问题:

  1. 这段字符是谁拥有的?
  2. 在我用完之前,这块内存一定还活着吗?
  3. 底层 string 在这期间会不会被改到重分配?

只要有一个答案不确定,就别长期保存 string_view。


一组实战建议

  1. 函数只读参数优先用 string_view。
  2. 需要保存、缓存、异步使用时优先用 string。
  3. 不要返回指向局部 string 的 string_view。
  4. 不要把临时 string 的结果存进 string_view 变量里。
  5. 对原 string 进行可能重分配的修改后,旧 view 一律视为失效。
  6. 传给 C 接口前,不要默认 string_view.data() 就是安全的 C 字符串。

一句话总结

string_view 最适合"当前这次调用里只读使用";一旦跨作用域、跨线程、跨生命周期保存,它的风险就会迅速升高。

相关推荐
宏笋1 小时前
C++ 回调函数详解和常用场景
开发语言·c++
WBluuue1 小时前
Codeforces 1095 Div2(ABCDE)
c++·算法
咩咦1 小时前
C++学习笔记07:引用做返回值
c++·学习笔记·引用·static·引用返回
郭涤生1 小时前
C++ 20联合体(Union)
开发语言·c++
Fanfanaas1 小时前
Linux 系统编程 文件篇 (一)
linux·运维·服务器·c++·学习
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之字符串 --【回文字符串】:判断字符串是否为回文
c++·字符串·csp·高频考点·信奥赛·回文字符串·判断字符串是否为回文
Emberone1 小时前
C++ 模板进阶详解:从非类型参数到特化、偏特化与分离编译
开发语言·c++
凤凰院凶涛QAQ1 小时前
《C++转Java快速入手系列》实践篇:图书系统
java·开发语言·c++
大大杰哥1 小时前
2025ccpc南昌补题笔记(前六题)
c++·笔记·算法