C++中的那些零拷贝技术
目录
3.1.使用写时复制(Copy-on-Write, COW)
3.6.使用字符串视图std::string_view(C++17)
1.零拷贝技术概念
零拷贝(Zero-copy)是一种重要的优化技术,旨在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗。在C++中,零拷贝技术通过多种方式实现,包括引用语义、视图(view)类型、移动语义、内存映射等。如果按底层机制分类,可分为内核态零拷贝(针对文件 / 网络 IO,跳过用户态拷贝)和用户态零拷贝(针对内存操作,避免用户态内部拷贝)。
2.内核态零拷贝技术
内核态零拷贝是一种通过避免数据在用户态内存与内核态内存之间重复拷贝,减少 CPU 开销和内存带宽占用的高性能数据传输技术,核心目标是 “让数据只动一次,甚至不动”。数据直接在内核态流转,用户态仅操作 “数据地址” 而非 “数据本身”。这涉及到的技术有:
1.使用sendfile系统调用:sendfile可以在两个文件描述符之间直接传输数据,无需将数据拷贝到用户空间。这在内核版本2.4及以上可用。
2.使用splice系统调用:splice用于在两个文件描述符之间移动数据,其中一个必须是管道。它可以在内核中直接操作,避免用户空间和内核空间之间的数据拷贝。
3.使用mmap和munmap:通过内存映射,将文件或设备映射到内存中,应用程序可以直接通过指针访问数据,而无需显式读取或写入。
4.使用DMA(直接内存访问):DMA允许某些硬件子系统直接访问主内存,而无需CPU的介入,这可以减少CPU的负载并避免不必要的拷贝。
它们的原理和实现方式如下:
| 实现方式 | 核心原理 | 关键 API / 技术 | 适用场景 |
|---|---|---|---|
| mmap + write | 将文件 / 设备内存直接映射到用户态地址空间,用户态操作映射地址等同于操作内核数据,避免拷贝。 | mmap() / munmap()(POSIX)、Qt 的QFile::map() |
大文件读写(如日志、配置)、实时数据采集(如设备内存映射) |
| sendfile | 由内核直接将文件数据从 “文件描述符” 拷贝到 “socket 描述符”,全程不经过用户态。 | sendfile()(POSIX)、Windows 的TransmitFile() |
网络文件传输(如 HTTP 服务器静态资源分发)、本地进程间高速数据转发 |
| splice/tee | 用 “管道(pipe)” 作为中间缓冲区,内核通过管道在两个文件描述符间直接传输数据,零拷贝且不占用用户态内存。 | splice() / tee()(POSIX) |
高并发网络 IO(如代理服务器)、多通道数据分发(如实时数据流复制) |
2.1.mmap(内存映射)
mmap(内存映射) 是将内核态内存(如文件页缓存、设备内存)直接映射到用户态进程的虚拟地址空间,使用户态可通过指针直接读写内核态数据,彻底跳过 “内核态→用户态” 的数据拷贝,仅保留 “磁盘 / 设备→内核页缓存” 的一次必要拷贝(针对文件),从而实现零拷贝。
1.传统 IO 与 mmap 的流程对比(直观理解零拷贝)
1) 传统 IO(read + write)流程(3 次拷贝 + 2 次切换)
- 磁盘数据 → 内核页缓存(内核态拷贝)。
- 内核页缓存 → 用户态缓冲区(跨态拷贝,性能瓶颈)。
- 用户态缓冲区 → 内核 socket 缓存(跨态拷贝,性能瓶颈)。
- 内核 socket 缓存 → 网卡(内核态拷贝)。
- 伴随 2 次用户态 / 内核态切换(read、write 各 1 次)。
2) mmap + write 流程(2 次拷贝 + 1 次切换)
- 磁盘数据 → 内核页缓存(内核态拷贝)。
- mmap 映射:内核页缓存直接映射到用户态虚拟地址(无拷贝,仅建立地址映射关系)。
- write 调用时,内核直接将 “映射的内核页缓存” 数据拷贝到 socket 缓存(内核态内部拷贝,无跨态开销)。
- 内核 socket 缓存 → 网卡(内核态拷贝)。
- 仅 1 次用户态 / 内核态切换(write 时),彻底消除 “内核→用户” 的跨态拷贝。
2.mmap 实现零拷贝的示例代码
1) 标准 C++(POSIX)实现:文件映射读写(基础场景)
适用于大文件读写、日志存储等场景,避免跨态拷贝。
#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
int main() {
const char* filename = "large_data.bin";
const char* data = "Hello, mmap Zero-Copy!";
size_t data_len = strlen(data);
// 1. 打开文件(O_RDWR:读写模式,O_CREAT:文件不存在则创建)
int fd = open(filename, O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
return -1;
}
// 2. 调整文件大小(确保文件有足够空间容纳数据,否则映射后写会越界)
if (ftruncate(fd, data_len) == -1) {
perror("ftruncate failed");
close(fd);
return -1;
}
// 3. 建立内存映射:将文件fd映射到用户态虚拟地址
// 参数:addr(0=内核自动分配), length(映射长度), prot(权限), flags(映射类型), fd(文件描述符), offset(偏移量)
void* mapped_addr = mmap(nullptr, data_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_addr == MAP_FAILED) {
perror("mmap failed");
close(fd);
return -1;
}
// 4. 直接操作映射地址(相当于直接写内核页缓存,无跨态拷贝)
memcpy(mapped_addr, data, data_len);
std::cout << "Write via mmap: " << static_cast<char*>(mapped_addr) << std::endl;
// 5. (可选)强制同步映射区数据到磁盘(避免内核缓存延迟写入)
if (msync(mapped_addr, data_len, MS_SYNC) == -1) {
perror("msync failed");
}
// 6. 解除映射 + 关闭文件(必须释放,否则内存泄漏)
if (munmap(mapped_addr, data_len) == -1) {
perror("munmap failed");
}
close(fd);
return 0;
}
2) Qt 实现:QFile::map(跨平台简化版)
Qt 封装了 mmap 接口,更适合 Qt 项目,跨平台兼容性更好。
#include <QCoreApplication>
#include <QFile>
#include <QDebug>
#include <cstring>
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
const QString filename = "qt_mmap_data.bin";
const char* data = "Qt mmap Zero-Copy Example!";
const qint64 data_len = strlen(data);
// 1. 打开文件
QFile file(filename);
if (!file.open(QIODevice::ReadWrite | QIODevice::Create)) {
qCritical() << "File open failed:" << file.errorString();
return -1;
}
// 2. 调整文件大小
if (!file.resize(data_len)) {
qCritical() << "File resize failed:" << file.errorString();
file.close();
return -1;
}
// 3. 建立映射:QFile::map返回映射后的内存指针
uchar* mapped_addr = file.map(0, data_len, QFile::MapReadWrite);
if (!mapped_addr) {
qCritical() << "mmap failed:" << file.errorString();
file.close();
return -1;
}
// 4. 读写映射内存
memcpy(mapped_addr, data, data_len);
qDebug() << "Write via Qt mmap:" << reinterpret_cast<char*>(mapped_addr);
// 5. 解除映射(QFile析构时会自动解除,但手动解除更安全)
file.unmap(mapped_addr);
file.close();
return a.exec();
}
3) 设备内存映射(实时数据采集场景)
适用于工业控制、医疗设备等场景,直接映射硬件设备的内存(如 ADC 采集缓冲区),无需拷贝即可读取实时数据。
#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
// 设备文件(如Linux下的设备节点,需root权限)
const char* dev_path = "/dev/adc_device"; // 假设为ADC采集设备
const size_t dev_mem_size = 4096; // 设备内存大小(根据硬件手册确定)
// 1. 打开设备文件(O_RDWR:读写,O_SYNC:同步模式,确保实时性)
int fd = open(dev_path, O_RDWR | O_SYNC);
if (fd == -1) {
perror("Open device failed");
return -1;
}
// 2. 映射设备内存(MAP_SHARED:共享映射,进程间可见)
void* dev_mem = mmap(nullptr, dev_mem_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (dev_mem == MAP_FAILED) {
perror("mmap device failed");
close(fd);
return -1;
}
// 3. 直接读取设备内存中的实时采集数据(无拷贝,实时性极高)
uint16_t* adc_data = static_cast<uint16_t*>(dev_mem);
for (int i = 0; i < 4; ++i) { // 假设4通道采集
std::cout << "ADC Channel " << i << ": " << adc_data[i] << std::endl;
}
// 4. 解除映射 + 关闭设备
munmap(dev_mem, dev_mem_size);
close(fd);
return 0;
}
2.2.sendfile 系统调用
sendfile 是一种由内核直接操控数据传输的零拷贝技术,核心作用是在两个文件描述符之间直接传递数据(无需用户态参与),彻底跳过 “内核态→用户态” 和 “用户态→内核态” 的两次跨态拷贝,仅保留必要的内核态内部拷贝(甚至在支持硬件加速时可进一步省略)。
1.sendfile 与传统 IO 的流程对比(为什么高效?)
1) 传统文件→网络传输(read + write):4 次拷贝 + 4 次用户态 / 内核态切换
- 拷贝 1:磁盘 → 内核页缓存(DMA 拷贝)。
- 拷贝 2:内核页缓存 → 用户态缓冲区(CPU 拷贝,跨态开销)。
- 拷贝 3:用户态缓冲区 → 内核 socket 缓存(CPU 拷贝,跨态开销)。
- 拷贝 4:内核 socket 缓存 → 网卡(DMA 拷贝)。
- 切换:
read(用户态→内核态→用户态)、write(用户态→内核态→用户态),共 4 次切换。
2.sendfile 传输:2 次拷贝 + 2 次切换(甚至 1 次拷贝)
- 拷贝 1:磁盘 → 内核页缓存(DMA 拷贝)。
- 拷贝 2:内核页缓存 → 网卡(直接由内核操作,若支持 “分散 - 聚集 DMA”,则无需 CPU 参与,仅 1 次 DMA 拷贝)。
- 切换:仅
sendfile调用的 2 次切换(用户态→内核态→用户态),无中间用户态干预。
2.sendfile 适用场景
- 静态资源服务器:如 Nginx 传输 HTML/CSS/ 图片等文件(核心场景,Nginx 大量依赖 sendfile 提升性能)。
- 日志 / 大文件传输:无需用户态处理数据,直接从文件发送到网络(如分布式系统日志同步)。
- 本地文件转发:快速在两个文件描述符间复制数据(如备份工具)。
3.sendfile 示例代码(文件→Socket 传输)
模拟 Web 服务器发送静态文件的场景:客户端连接后,通过 sendfile 将本地文件直接发送到 socket。
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <cstring>
// 绑定端口并监听
int create_listen_socket(int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket failed");
return -1;
}
// 允许端口复用
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt failed");
close(sockfd);
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind failed");
close(sockfd);
return -1;
}
if (listen(sockfd, 5) == -1) {
perror("listen failed");
close(sockfd);
return -1;
}
return sockfd;
}
int main() {
const int port = 8080;
const char* filename = "static_file.html"; // 要传输的静态文件
// 1. 创建监听 socket
int listen_fd = create_listen_socket(port);
if (listen_fd == -1) return -1;
std::cout << "Listening on port " << port << "..." << std::endl;
// 2. 等待客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept failed");
close(listen_fd);
return -1;
}
std::cout << "Client connected" << std::endl;
// 3. 打开要传输的文件
int file_fd = open(filename, O_RDONLY);
if (file_fd == -1) {
perror("open file failed");
close(client_fd);
close(listen_fd);
return -1;
}
// 4. 获取文件大小(用于 sendfile 传输)
struct stat file_stat;
if (fstat(file_fd, &file_stat) == -1) {
perror("fstat failed");
close(file_fd);
close(client_fd);
close(listen_fd);
return -1;
}
size_t file_size = file_stat.st_size;
// 5. 发送 HTTP 响应头(简单模拟,实际需完整头信息)
const char* header = "HTTP/1.1 200 OK\r\nContent-Length: ";
std::string response = std::string(header) + std::to_string(file_size) + "\r\n\r\n";
send(client_fd, response.c_str(), response.size(), 0);
// 6. 核心:用 sendfile 直接将文件数据发送到客户端 socket(零拷贝)
off_t offset = 0; // 从文件起始位置传输
ssize_t sent = sendfile(client_fd, file_fd, &offset, file_size);
if (sent == -1) {
perror("sendfile failed");
} else {
std::cout << "Sent " << sent << " bytes via sendfile" << std::endl;
}
// 7. 清理资源
close(file_fd);
close(client_fd);
close(listen_fd);
return 0;
}
4.跨平台差异
- Linux 支持
sendfile。 - Windows 对应 API 为
TransmitFile(功能类似,参数不同)。 - FreeBSD 有
sendfile但参数格式不同(需注意兼容性)。
2.3.splice 系统调用
splice 是 Linux 特有的零拷贝技术,核心作用是在两个文件描述符之间通过 “管道(pipe)” 作为中间缓冲区传输数据,全程数据仅在内核态流转,完全避免用户态与内核态之间的拷贝,甚至可通过内核优化(如页面引用计数)实现 “零拷贝”(无实际数据复制,仅移动指针)。
与 sendfile 相比,splice 更灵活:它不限制输入 / 输出文件描述符的类型(可支持 socket、管道、普通文件等任意组合),尤其适合需要在 “非文件→socket” 或 “socket→socket” 之间转发数据的场景(如代理服务器)。
1.splice 与传统 IO 的流程对比
1) 传统转发(read + write):4 次拷贝 + 4 次切换
- 数据从输入 fd(如 socket A)→ 内核缓存(拷贝 1)。
- 内核缓存 → 用户态缓冲区(拷贝 2,跨态开销)。
- 用户态缓冲区 → 输出 fd(如 socket B)的内核缓存(拷贝 3,跨态开销)。
- 输出 fd 内核缓存 → 目标设备(拷贝 4)。
- 伴随
read和write的 4 次用户态 / 内核态切换。
2) splice 转发:0 次跨态拷贝 + 2 次切换
- 数据从输入 fd 内核缓存 → 管道内核缓存(内核态内部操作,无拷贝或仅移动指针)。
- 数据从管道内核缓存 → 输出 fd 内核缓存(内核态内部操作,同上)。
- 仅
splice调用的 2 次切换(用户态→内核态→用户态),无用户态参与数据处理。
2.splice 函数原型与参数解析(Linux 特有)
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
| 参数 | 含义 |
|---|---|
fd_in |
输入文件描述符(若为 -1,则从管道读取数据,需配合 off_in=NULL)。 |
off_in |
输入 fd 的读取偏移量(NULL 表示使用当前文件指针,且随传输自动更新)。 |
fd_out |
输出文件描述符(若为 -1,则向管道写入数据,需配合 off_out=NULL)。 |
off_out |
输出 fd 的写入偏移量(规则同 off_in)。 |
len |
要传输的字节数。 |
flags |
传输标志(关键控制参数,见下表)。 |
关键 flags 选项:
| 标志 | 作用 |
|---|---|
SPLICE_F_MOVE |
尝试通过 “移动页面” 而非拷贝数据(依赖内核支持,可能降级为拷贝)。 |
SPLICE_F_NONBLOCK |
非阻塞模式(若操作无法立即完成,返回 -1 并置 errno=EAGAIN)。 |
SPLICE_F_MORE |
提示内核后续还有更多数据传输,优化批量处理效率。 |
SPLICE_F_GIFT |
表示输入数据是 “内核赠品”(仅用于特定场景,如网络接收的页面)。 |
返回值:成功返回实际传输的字节数;失败返回 -1(通过 errno 查看错误,如 EBADF 表示 fd 不支持 splice)。
3.splice 的核心特性
- 双向灵活性:输入 / 输出 fd 可任意组合(文件→socket、socket→socket、socket→文件等),突破 sendfile 对输入 fd 必须是文件的限制。
- 管道依赖:必须至少有一个 fd 是管道(pipe),即 “输入是管道” 或 “输出是管道”(这是 splice 设计的核心约束)。
- 零拷贝潜力:若内核支持页面迁移(
SPLICE_F_MOVE),可完全避免数据拷贝,仅通过调整页表指针实现传输。
4.适用场景
- 代理服务器:如反向代理(Nginx 部分场景用 splice 转发请求),需在客户端 socket 与后端服务器 socket 之间转发数据。
- 多通道数据分发:如将一个 socket 的数据同时转发到多个文件或 socket(配合
tee系统调用,tee是 splice 的变体,用于复制数据)。 - 非文件源数据转发:如从标准输入(stdin)、网络 socket 读取数据并转发到其他设备(sendfile 无法处理此类输入)。
5.splice 示例代码(socket→socket 转发,模拟代理)
模拟代理服务器接收客户端数据后,通过 splice 转发到后端服务器(核心逻辑:用管道作为中间缓冲区,分两次 splice 完成 “客户端→管道” 和 “管道→后端” 的传输)。
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
// 创建并连接到后端服务器(模拟)
int connect_backend(const char* host, int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("backend socket failed");
return -1;
}
struct sockaddr_in backend_addr;
backend_addr.sin_family = AF_INET;
backend_addr.sin_port = htons(port);
backend_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 本地后端
if (connect(sockfd, (struct sockaddr*)&backend_addr, sizeof(backend_addr)) == -1) {
perror("connect backend failed");
close(sockfd);
return -1;
}
return sockfd;
}
// 创建监听 socket(客户端连接入口)
int create_listen_socket(int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("listen socket failed");
return -1;
}
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
return sockfd;
}
int main() {
const int proxy_port = 8080; // 代理服务器端口
const int backend_port = 9090; // 后端服务器端口
// 1. 创建监听 socket 并等待客户端连接
int listen_fd = create_listen_socket(proxy_port);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept client failed");
return -1;
}
std::cout << "Client connected" << std::endl;
// 2. 连接后端服务器
int backend_fd = connect_backend("localhost", backend_port);
if (backend_fd == -1) {
close(client_fd);
return -1;
}
std::cout << "Connected to backend" << std::endl;
// 3. 创建管道(splice 必须依赖管道作为中间缓冲区)
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe failed");
close(client_fd);
close(backend_fd);
return -1;
}
int pipe_read = pipefd[0]; // 管道读端
int pipe_write = pipefd[1]; // 管道写端
// 4. 用 splice 转发数据:客户端 → 管道 → 后端(零拷贝)
const size_t BUF_SIZE = 4096;
while (true) {
// 4.1 第一次 splice:客户端数据 → 管道(输入是 client_fd,输出是管道写端)
ssize_t n = splice(client_fd, NULL, pipe_write, NULL, BUF_SIZE, SPLICE_F_MOVE);
if (n <= 0) {
if (n == 0) break; // 客户端关闭连接
perror("splice client→pipe failed");
break;
}
// 4.2 第二次 splice:管道数据 → 后端(输入是管道读端,输出是 backend_fd)
ssize_t m = splice(pipe_read, NULL, backend_fd, NULL, n, SPLICE_F_MOVE);
if (m <= 0) {
perror("splice pipe→backend failed");
break;
}
std::cout << "Forwarded " << m << " bytes (zero-copy)" << std::endl;
}
// 5. 清理资源
close(client_fd);
close(backend_fd);
close(pipe_read);
close(pipe_write);
close(listen_fd);
return 0;
}
3.用户态零拷贝技术
用户态零拷贝的核心逻辑是通过 “共享缓冲区” 或 “只读引用”,避免用户态下数据的冗余拷贝。
3.1.使用写时复制(Copy-on-Write, COW)
COW技术允许多个进程共享同一块内存,直到某个进程需要修改数据时才会进行复制。这可以避免不必要的拷贝。
- 示例:C++11 前的
std::string(部分实现)、Qt 的QSharedData(自定义 COW 类) - 适用场景:大量只读数据的共享(如配置项分发)、对象频繁拷贝但修改较少的场景。
更多可参考:
3.2.使用移动语义(C++11及以上)
移动语义允许资源(如动态分配的内存)的所有权从一个对象转移到另一个对象,而无需复制资源。它的详细讲解可参考:
3.3.使用智能指针管理资源
智能指针(如std::unique_ptr和std::shared_ptr)可以帮助管理动态分配的内存,并通过移动语义避免不必要的拷贝。
#include <iostream>
#include <memory>
#include <vector>
void processData(std::shared_ptr<std::vector<int>> data) {
// 处理数据,不需要复制
for (int val : *data) {
// 处理逻辑
}
std::cout << "Processing data with use count: " << data.use_count() << std::endl;
}
int main() {
// 创建大数据
auto data = std::make_shared<std::vector<int>>(1000000, 42);
// 传递共享所有权,而不是复制数据
processData(data);
// 仍然可以从main函数访问原始数据
std::cout << "Data size: " << data->size() << std::endl;
std::cout << "Final use count: " << data.use_count() << std::endl;
return 0;
}
3.4.内存池 / 预分配缓冲区
提前分配固定大小的内存块,数据传输时直接复用缓冲区,避免频繁创建 / 销毁内存导致的拷贝和碎片。
- 示例:自定义
BufferPool、Qt 的QByteArray(内部预分配)、Boost 的pool - 适用场景:实时数据采集(如 10M 采样率的 4 通道数据缓存)、高并发网络数据包处理。
3.5.使用共享内存
通过共享内存,多个进程可以直接访问同一块内存区域,从而避免数据在进程间的拷贝。
共享内存(Shared Memory) 是一种高效的进程间通信(IPC)技术,它通过让多个进程直接访问同一块物理内存区域,实现数据的 “零拷贝” 共享(无需在进程间复制数据),是性能最高的 IPC 方式之一。
核心优势:彻底避免进程间数据拷贝,仅需一次 “映射” 操作,后续访问效率与访问本地内存一致,适合高频、大数据量的进程间通信(如实时数据采集、高频交易系统)。
windows、linux以及Qt平台的详细实现可参考:
3.6.使用字符串视图std::string_view(C++17)
std::string_view允许以引用的方式访问字符串,而不需要复制字符串数据。
std::string_view 是C++17引入的一个轻量级非拥有型字符串视图类,它提供了对字符串数据的只读访问,而不进行数据复制。它的核心优势在于:
-
不拥有字符串数据,仅存储指向数据的指针和长度
-
轻量级,通常只占用两个指针大小的内存空间
-
可以高效地与任何类似字符串的数据源交互
-
可以避免不必要的字符串复制操作
下面是一个简单的示例,展示了 std::string_view 的基本用法:
#include <iostream>
#include <string>
#include <string_view>
// 使用string_view作为参数,避免不必要的复制
void printStringView(std::string_view sv) {
std::cout << "String view: " << sv << ", length: " << sv.length() << std::endl;
}
int main() {
// 从std::string创建string_view
std::string str = "Hello, World!";
std::string_view sv1 = str;
// 从C风格字符串创建string_view
const char* cstr = "Hello, C++!";
std::string_view sv2 = cstr;
// 从字符串字面量创建string_view
std::string_view sv3 = "Hello, Zero-copy!";
// 使用string_view作为函数参数
printStringView(sv1);
printStringView(sv2);
printStringView(sv3);
// 注意:string_view不拥有数据,源数据必须保持有效
// 以下代码不安全,因为临时字符串在表达式结束后会被销毁
// std::string_view unsafe = std::string("Temporary"); // 危险!
return 0;
}
更多示例详情可参考:
3.7.使用数组视图std::span(C++20)
std::span允许以引用的方式访问连续存储的数据(如数组或vector),而不需要复制数据。
在 C++20中引入的 std::span 是一种轻量级的 “非拥有式视图(non-owning view)”,用于指代一段连续的内存序列(如数组、std::vector、std::array 或 C 风格数组等)。它不管理内存的生命周期,仅通过 “起始指针 + 长度” 的组合提供对底层内存的访问,核心目标是避免不必要的数据拷贝,同时简化对连续内存的操作。
更多的详情介绍可参考:
4.总结
零拷贝技术是C++中提高性能的重要手段,特别是在处理大量数据时。std::string_view 作为C++17引入的重要特性,提供了一种轻量级、高效的字符串处理方式,避免了不必要的数据复制。结合移动语义、智能指针和内存映射等技术,可以构建更加高效的数据处理系统。
更多推荐



所有评论(0)