现代c++第二章 字符串

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 起)

一句话总结

内联函数 = 编译器在调用点"复制粘贴"函数体,省去跳转开销,适合短小且频繁调用的函数,是宏的安全替代品。

相关推荐
idealzouhu2 小时前
现代 CMake 依赖管理详解:从构建树、Target 封装到 find_package 工作原理
c++·cmake
四代水门3 小时前
游戏客户端——算法题
c++
Xia__Quan3 小时前
centos 共享文件夹
c++
hi_ro_a4 小时前
基于正倒排索引的boost搜索引擎
linux·c++·搜索引擎·项目
神仙别闹4 小时前
基于 C++ MFC 实现的个人通讯录管理系统
c++
GG-_-Bond4 小时前
9.1kv存储持久化模拟面试
linux·c语言·数据结构·c++
郝学胜-神的一滴5 小时前
C++20模板元编程 04:变量模板 别名模板与模板Lambda实战
服务器·开发语言·数据结构·c++·程序人生
qq_344920565 小时前
Qt事件过滤器
开发语言·c++·qt
码匠许师傅6 小时前
【C++三方组件】fmt:C++字符串格式化的前世今生
c++