c++不同于c语言 ,其官方为我们实现了一套字符串类型 string
2.1 动态字符串
string类型有自动扩从功能,比较智能
2.1.1 c风格的字符串
在c语言中,字符串表示装有字符的数组,它的最后一个字符是null字符(\0),这就导致像"hello"这个字符串需要6个char空间才能存储
c++包含一些含c语言的字符串操作函数,它们在中定义
cpp
char* copyString(const char* str)
{
char* result{ new char[std::strlen(str)] };
std::strcpy(result, str);
return result;
}
这个代码是错误的,strlen不会计算末尾\0的大小
cpp
char* copyString(const char* str)
{
char* result{ new char[std::strlen(str)+1] };
std::strcpy(result, str);
return result;
}
补充
strcat:可以将字符串联在一起
cpp
void fun1(const char* str)
{
char* funstring{ copyString(str) };
std::strcat(funstring, str);
}
请注意,sizeof会直接返回变量体所占空间的大小,但是传入c风格的字符串,出来的结果就是指针的大小,32位是4,64位是8
2.1.2 字符串的字面量
在c语言中的printf函数,printf("hello,pmps")这个函数即使被调用114514次,也只会生成一个实例。也就是说char *p="pmps";
每个量其实是一个const char 类型,不能被改变,使用const char *p="pmps";会更加保险;
原始字符串的自变量
println("hello "world"!"); //编译器会报错
println("hello "world"!'");
println(R"("hello "world"!")");
2.1.3 std::string
部分内容在算法竞赛篇学到过,这里只讲一些常见的操作
substr(pos,len);返回从起始位置到终点位置的字符串
find(str);如果找到了给定的子串,返回位置,不然返回nops(表示极大的数)
replace(pos,len,str);表示将字符串的一部分进行替换
starts_with(str)/ends_with(str):是否是以给定的字符串作为开头或是结尾
contains(str) 表示一个字符串是否包含了某个字符
std::string 的字面量:
一句话解释:
auto a{"hello"}; const char*
auto& b{"hello"}; const char6;
auto c{"hello"s};std::string;
补充:内联命名空间
即在父命名空间下的子命名空间加上inline,是的通过访问父命名空间可以直接访问内容,而不用像剥洋葱那样一层一层的访问
vector的内容推导可不以直接推导出string的类型,需要像前面那样加一个s
vector name {"aaaa","bbb","ccc"s};
2.1.4 数值转换
to_string(t var);t类型
字符串转变为数值
stoi stol stoll stoull (const string&str,size_t pos,int base=10);
2.1.5 std::string_view 类
std::string_view 是 C++17 引入的一个轻量级、非拥有(non-owning)、只读的字符串视图类。
你可以把它理解为一本书的"目录索引"------它只记录"从哪一页开始、一共多少页",但不会把整本书复印一份。它本质上只存了两个东西:一个 const char* 指针 + 一个 size_t 长度,总共通常只有 16 字节(64位系统)。
为什么需要它?
在 string_view 出现之前,C++ 处理字符串主要靠 std::string 和 C 风格的 const char*,但它们都有痛点:
std::string:虽然功能强大,但每次拷贝、传参、截取子串时都可能触发堆内存分配和数据拷贝,对于只读操作来说很浪费。
const char*:需要手动管理内存,依赖 0 结尾,没有长度信息,容易越界。
string_view 就是为了解决这些问题而生的------零拷贝、零内存分配,只提供一个"窗口"去观察已有的字符串数据。
核心特性
特性 说明
非拥有 不分配、不释放内存,仅引用现有数据
只读 无法修改底层字符串内容
轻量 仅含指针+长度,复制开销极低,适合按值传递
零开销切片 substr() 返回新视图,时间复杂度 O(1),不拷贝数据
不依赖 0 长度由 size 记录,可指向字符串中间的片段
基本用法
include
include
include <string_view>
int main() {
// 从字符串字面量构造
std::string_view sv1 = "Hello";
// 从 std::string 构造(不拷贝数据!)
std::string str = "World";
std::string_view sv2 = str;
// 零开销截取子串
std::string_view sub = sv2.substr(1, 3); // "orl",O(1) 复杂度
std::cout << sv1 << " " << sv2 << " " << sub << std::endl;
return 0;
}
最典型的应用场景:函数参数
这是 string_view 最核心、最推荐的用法。把只读字符串参数统一写成 std::string_view,可以同时接受 std::string、const char*、字符串字面量,且不会产生任何拷贝。
// ✅ 推荐:用 string_view 接收只读字符串参数
void process_text(std::string_view text) {
if (text.starts_with("https://")) {
// 处理逻辑...
}
}
process_text("https://example.com"); // 字符串字面量,零开销
std::string url = "https://test.com";
process_text(url); // std::string,零开销
相比之下,如果写成 const std::string&,传入字符串字面量时编译器会隐式创建一个临时 std::string 对象,涉及一次堆分配和一次内存拷贝,完全是浪费。
⚠️ 最大的坑:悬垂引用(Dangling Reference)
string_view 不拥有数据,所以你必须确保被引用的字符串在 string_view 使用期间一直有效。这是它最容易出 Bug 的地方。
// ❌ 错误示范:返回局部变量的视图
std::string_view create_view() {
std::string tmp = "hello world";
return tmp; // tmp 销毁后,视图悬空!未定义行为!
}
// ❌ 错误示范:把视图存入容器,但原字符串已析构
std::vectorstd::string_view bag;
{
std::string s = "abc";
bag.push_back(s); // s 即将析构,bag 里的视图悬垂
}
cout << bag0; // 💥 未定义行为
原则:需要长期存储字符串时,用 std::string;只在短期只读访问时用 string_view。
其他注意事项
没有 0 保证:string_view 的 data() 返回的指针不一定以 0 结尾,不能直接传给 printf("%s", ...) 或需要 C 风格字符串的 API。需要时可以用 std::string(sv) 转换。
不能修改内容:它是只读的,需要修改时还是得用 std::string。
竞赛中非常实用:在 CSP/NOIP 等算法竞赛中,处理大文本解析、字符串切片时,string_view 全程零拷贝、O(1) 切片,能显著提升性能。
总结:什么时候用哪个?
场景 选择
函数参数(只读) ✅ std::string_view
字符串解析/切片 ✅ std::string_view
需要长期存储 ✅ std::string
需要修改内容 ✅ std::string
与 C API 交互 ✅ std::string(用 c_str())
一句话概括:string 是"房主",拥有并管理数据;string_view 是"租客看房",只看不拥有。
补充:(10)
内联函数(inline)是 C++ 中一种用空间换时间的编译期优化机制 ,核心目的是消除函数调用的开销。
核心原理
普通函数调用时,程序需要执行一系列操作:
- 压入返回地址到栈
- 压入参数
- 跳转到函数地址执行
- 函数执行完后返回并平衡栈
这些操作虽然单次开销很小,但对于频繁调用的小函数来说,累积起来就很可观。
内联函数在编译阶段 直接将函数体代码"原地展开"到每一个调用处,不生成 call 指令,从而彻底消除上述开销。
cpp
// 普通函数
int add(int a, int b) { return a + b; }
// 调用 add(1, 2) 会生成 call 指令,有跳转开销
// 内联函数
inline int add(int a, int b) { return a + b; }
// 调用 add(1, 2) 被编译器直接替换为 1 + 2,无跳转
关键特性
| 特性 | 说明 |
|---|---|
inline 只是建议 |
编译器有权决定是否真正内联,并非强制 |
| 空间换时间 | 代码被复制多份,目标文件体积会变大 |
| 必须定义在头文件中 | 编译器需要在每个调用点看到完整函数体才能展开 |
| 遵循 ODR 规则 | 允许在多个翻译单元中定义,但所有定义必须完全相同,链接器会去重 |
| 类内定义的成员函数隐式为 inline | 写在类体内部的函数自动具有内联属性,无需显式加 inline |
什么时候适合用内联?
✅ 适合:
- 函数体短小(通常 1~5 行)
- 调用非常频繁
- 简单运算,如 getter/setter、数学计算
❌ 不适合(编译器通常会忽略):
- 递归函数
- 包含循环、
switch等复杂控制流 - 函数体过大
- 虚函数(除非编译器能去虚化)
- 函数地址被取用(如通过函数指针调用)
和 C 语言宏的对比
inline 的设计初衷之一就是替代 C 语言的宏函数,它比宏更安全:
| 对比项 | inline 函数 |
#define 宏 |
|---|---|---|
| 类型检查 | ✅ 有完整类型检查 | ❌ 纯文本替换,无类型检查 |
| 作用域 | ✅ 遵循 C++ 作用域规则 | ❌ 全局替换,易命名冲突 |
| 参数求值 | ✅ 只求值一次 | ❌ 可能多次求值,产生副作用 |
| 调试 | ✅ 可正常调试 | ❌ 展开后难以调试 |
cpp
// 宏的坑
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 1, y = 2;
MAX(x++, y); // x 可能被求值多次,结果不可预期
// inline 函数没有这个问题
inline int max(int a, int b) { return a > b ? a : b; }
max(x++, y); // 安全,参数只求值一次
隐式内联的情况
除了手动加 inline,以下情况函数也自动具有内联属性:
- 类体内定义的成员函数 :
class Foo { void bar() { /* 隐式 inline */ } }; constexpr函数(C++11 起)consteval函数(C++20 起)
一句话总结
内联函数 = 编译器在调用点"复制粘贴"函数体,省去跳转开销,适合短小且频繁调用的函数,是宏的安全替代品。