跳到主要内容C++ 核心学习笔记:指针、内存与零拷贝实战 | 极客日志C++算法
C++ 核心学习笔记:指针、内存与零拷贝实战
涵盖 C++ 指针本质、内存管理(栈堆)、系统调用态切换、零拷贝优化原理及实现、单例模式、多线程基础等核心知识点。重点解析 sendfile 零拷贝机制对比传统读写性能差异,以及 C++11 线程安全单例与局部静态变量用法。适合后端开发深入理解底层资源调度。
DebugKing24 浏览 学习笔记
1 指针的数组下标语法本质
指针的数组下标本质是'指针偏移 + 解引用'
int* array_index = static_cast<int*>(ptr);
2 memset
memset 是 C/C++ 标准库(<cstring>/<string.h>)中的函数,按字节为单位初始化内存块。它的核心作用是把一块连续内存的每个字节都设置为指定的值。注意必须把结束位置设为 \0,否则会越界访问到脏数据。
函数原型
void* memset(void* ptr, int value, size_t num);
ptr:指向要初始化的内存块起始地址(void* 兼容任意类型指针);
value:要设置的字节值(虽为 int 类型,但实际只取低 8 位,范围 0~255);
num:要初始化的字节数;
- 返回值:指向内存块的指针(通常忽略,仅用于链式调用)。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char str[10];
memset(str, 'a', 5);
memset(str + 5, '\0', 1);
cout << << str << endl;
cout << << ()str[] << endl;
;
}
"str = "
"str[0] 字节值:"
int
0
return
0
| 操作场景 | 推荐用法 | 原因 |
|---|
| char 数组设指定字符 | memset | 按字节操作,完美匹配 char 占 1 字节的特性 |
| 任意类型内存清零 | memset (ptr, 0, 字节数) | 高效、简洁,比循环赋值快 |
| int/float 设非 0 值 | 直接赋值 / 循环赋值 | memset 按字节赋值会导致数值错误 |
| C++ 类对象初始化 | 构造函数 / 定位 new | memset 会跳过析构函数,破坏对象语义(如覆盖 string 的指针成员) |
3 noexcept
| 维度 | 无 noexcept | 有 noexcept |
|---|
| STL 容器兼容性 | 容器优先拷贝(低效深拷贝) | 容器优先移动(高效浅拷贝) |
| 异常安全 | 可能触发未定义行为 | 异常时直接终止,避免数据损坏 |
| 性能 | 保留异常处理开销 | 编译器优化,执行更快 |
| 代码语义 | 调用者需预判异常 | 明确无异常,可读性更高 |
简单来说:移动构造 / 赋值加 noexcept,是让 MyString 既能享受'移动语义'的高性能,又能保证异常安全,同时兼容 STL 容器的设计逻辑——这也是 C++ 实现'零开销抽象'的关键细节。
#include <iostream>
#include <cstring>
#include <utility>
using namespace std;
class MyString {
private:
char* _data;
size_t _size;
size_t _capacity;
void allocateAndCopy(const char* str, size_t len) {
_size = len;
_capacity = len + 1;
_data = new char[_capacity];
strcpy_s(_data, _capacity, str);
}
public:
MyString(const char* str = "") {
size_t len = strlen(str);
allocateAndCopy(str, len);
cout << "默认构造函数:" << _data << endl;
}
MyString(const MyString& other) {
allocateAndCopy(other._data, other._size);
cout << "拷贝构造函数:" << _data << endl;
}
MyString& operator=(const MyString& other) {
allocateAndCopy(other._data, other._size);
cout << "拷贝赋值函数:" << _data << endl;
return *this;
}
MyString(MyString&& other) noexcept {
_data = other._data;
_size = other._size;
_capacity = other._capacity;
other._data = nullptr;
other._size = 0;
other._capacity = 0;
cout << "移动构造函数:" << _data << endl;
}
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] _data;
_data = other._data;
_size = other._size;
_capacity = other._capacity;
other._data = nullptr;
other._size = 0;
other._capacity = 0;
}
cout << "移动赋值运算符:" << _data << endl;
return *this;
}
~MyString() {
cout << "析构:" << (_data ? _data : "nullptr") << endl;
delete[] _data;
}
const char* c_str() const { return _data; }
size_t size() const { return _size; }
};

4 常见头文件
① C++ 标准库头文件
| 头文件 | 核心作用 | 典型使用场景 |
|---|
<functional> | 提供 C++ 函数对象相关工具:std::function、std::bind、lambda 支持等 | 回调函数、函数封装、异步操作(比如网络回调、事件处理器) |
<string> | C++ 标准字符串类 std::string,替代 C 风格 char*,支持安全的字符串操作 | 处理网络地址(如 IP 字符串)、协议数据、错误信息等 |
② Linux 系统错误处理头文件
| 头文件 | 核心作用 | 典型使用场景 |
|---|
<errno.h> | 定义全局错误码 errno,以及错误码对应的宏(如 EINVAL、EAGAIN、EPIPE 等) | 系统调用(如 socket、send、recv、open)失败时,通过 errno 定位具体错误原因 |
③ Socket 网络编程核心头文件
| 头文件 | 核心作用 | 典型使用场景 |
|---|
<sys/types.h> | 定义系统基本数据类型(如 pid_t、size_t、socklen_t 等) | 所有系统调用的基础类型支撑(比如 socket 函数的参数类型) |
<sys/socket.h> | 核心 Socket 函数 / 结构体:socket()、bind()、listen()、accept()、connect()、send()、recv()、sockaddr_in 等 | TCP/UDP 套接字的创建、绑定、监听、连接、数据收发 |
<netinet/tcp.h> | 定义 TCP 协议专属选项:如 TCP_NODELAY(禁用 Nagle 算法)、TCP_CORK 等 | 调优 TCP 连接(比如低延迟场景关闭 Nagle 算法,提升实时性) |
④ 文件操作 / 零拷贝相关头文件
| 头文件 | 核心作用 | 典型使用场景 |
|---|
<sys/sendfile.h> | 提供 sendfile() 函数:实现'零拷贝'文件传输(内核态直接拷贝,无需用户态) | 高性能文件传输(如 HTTP 服务器发送静态文件,避免 read+write 的内存拷贝) |
<fcntl.h> | 文件控制函数:open()、fcntl()、O_RDONLY/O_WRONLY 等文件打开标志、F_SETFL(设置非阻塞)等 | 文件打开 / 关闭、设置文件状态(如非阻塞 socket)、文件权限控制 |
<unistd.h> | 通用系统调用:read()、write()、close()、lseek()、dup()、fork()、sleep() 等 | 读写文件 / 套接字、关闭文件描述符、进程控制、文件偏移量调整 |
5 用户态和内核态
操作系统为了安全和权限管控,把 CPU 的执行权限分成了两个等级——普通程序只能在「用户态」运行(低权限),只有操作系统内核能在「内核态」运行(高权限)。
① 为什么要分用户态 / 内核态?
计算机的核心资源(CPU、内存、磁盘、网卡、显卡等)必须由操作系统统一管理,不能让普通程序直接操作——否则会出大问题:
- 比如一个普通程序直接写磁盘,可能覆盖系统文件;
- 一个程序直接操作网卡,可能窃取其他程序的网络数据;
- 多个程序同时申请内存,可能导致内存混乱。
| 状态 | 权限 | 能做的事 | 不能做的事 |
|---|
| 用户态 | 低权限 | 执行普通代码(如加减乘除、变量赋值、调用 C++ 标准库) | 直接操作硬件、访问内核内存、执行系统调用(如 open/socket) |
| 内核态 | 高权限 | 操作硬件、管理内存、执行所有系统调用(如读写磁盘、创建 Socket、分配内存) | 无(操作系统内核独占) |
普通程序(比如 C++ 代码)默认跑在用户态,想要操作硬件 / 核心资源,必须「请求内核帮忙」——这个请求过程就是「系统调用(syscall)」,比如 open()/socket()/sendfile() 都是系统调用。
② 一个极简例子:直观理解态切换
比如代码 open() 函数(打开文件),看似简单,实际执行流程是:
int fileFd = open("/tmp/test.txt", O_RDONLY);
- 你的程序在用户态执行
open(),发现这是系统调用,触发 CPU 权限切换;
- CPU 从用户态切到内核态,执行内核的
open() 逻辑(检查文件权限、分配文件描述符、读取磁盘目录);
- 内核完成操作后,CPU 从内核态切回用户态,把
fileFd 返回给你的程序。
这个'切来切去'的过程有开销,而零拷贝不仅减少了拷贝,还减少了态切换次数(传统方式 2 次切换,零拷贝 1 次),因此性能更高。
③ 关键知识点
- 核心目的:用户态 / 内核态分离是为了「权限隔离 + 系统安全」,防止普通程序乱操作核心资源;
- 系统调用:普通程序访问核心资源的唯一合法方式,会触发「用户态↔内核态」切换;
- 跨态拷贝:数据在用户态和内核态之间的拷贝需要 CPU 参与,是性能瓶颈;
- 零拷贝的本质:让数据全程在内核态流转,跳过用户态,避免跨态拷贝和多余的态切换。
6 零拷贝
零拷贝是操作系统 / 编程层面的优化技术,核心目标是:减少数据在「用户态内存」和「内核态内存」之间的拷贝次数,甚至完全避免不必要的拷贝,从而提升文件 / 网络传输的性能(降低 CPU 开销、减少内存占用、提高吞吐量)。
以「Linux 下从文件读取数据并通过 Socket 发送」为例,传统方式(read + write)的流程是:
磁盘 → 内核态缓冲区(kernel buffer)→ 用户态缓冲区(user buffer)→ 内核态 Socket 缓冲区 → 网卡
这个过程包含 4 次数据拷贝 + 2 次用户态 / 内核态切换:
read():磁盘 → 内核缓冲区(DMA 拷贝,无 CPU 参与);
read():内核缓冲区 → 用户缓冲区(CPU 拷贝);
write():用户缓冲区 → Socket 内核缓冲区(CPU 拷贝);
write():Socket 内核缓冲区 → 网卡(DMA 拷贝,无 CPU 参与)。
其中,第 2、3 步的 CPU 拷贝是完全冗余的——数据只是'路过'用户态,没有任何修改,却消耗了 CPU 和内存带宽。
① 零拷贝的核心优化:跳过用户态拷贝
零拷贝的核心思路是:让数据直接在内核态流转,不进入用户态,常见实现方式有 2 种(Linux 下):
a. sendfile() 系统调用(最常用)
代码中 <sys/sendfile.h> 就是为这个函数服务的,它的流程:
磁盘 → 内核态缓冲区 → 内核态 Socket 缓冲区 → 网卡
- 拷贝次数:3 次(2 DMA 拷贝,1 CPU);
- 切换次数:2 次(用户态→内核态→用户态);
- 核心逻辑:
sendfile() 直接在内核中完成'内核缓冲区 → Socket 缓冲区'的转发,数据全程不进入用户态。
b. splice()/tee() 系统调用(更极致)
进一步优化:连'内核缓冲区 → Socket 缓冲区'的拷贝都跳过,通过内存地址映射让两个内核缓冲区共享数据,流程:
磁盘 → 内核态缓冲区(映射到 Socket 缓冲区)→ 网卡
- 拷贝次数:1 次(磁盘→内核缓冲区,DMA 拷贝);
- 核心逻辑:数据始终只存在于内核缓冲区,Socket 直接'引用'该缓冲区,无任何实际拷贝。
② 极简示例(对比传统方式 vs 零拷贝)
a. 传统方式(read + write,冗余拷贝)
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <iostream>
void sendFile_traditional(int clientFd, const char* filePath) {
int fileFd = open(filePath, O_RDONLY);
if (fileFd < 0) {
std::cerr << "open failed: " << strerror(errno) << std::endl;
return;
}
char buf[4096];
ssize_t n;
while ((n = read(fileFd, buf, sizeof(buf))) > 0) {
write(clientFd, buf, n);
}
close(fileFd);
}
b. 零拷贝方式(sendfile(),无用户态拷贝)
#include <sys/sendfile.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <iostream>
void sendFile_zerocopy(int clientFd, const char* filePath) {
int fileFd = open(filePath, O_RDONLY);
if (fileFd < 0) {
std::cerr << "open failed: " << strerror(errno) << std::endl;
return;
}
off_t offset = 0;
ssize_t sent = sendfile(clientFd, fileFd, &offset, 1024 * 1024);
if (sent < 0) {
std::cerr << "sendfile failed: " << strerror(errno) << std::endl;
} else {
std::cout << "发送 " << sent << " 字节(零拷贝)" << std::endl;
}
close(fileFd);
}
③ 零拷贝的核心价值(为什么要用)
| 维度 | 传统方式(read+write) | 零拷贝(sendfile) |
|---|
| CPU 开销 | 高(2 次 CPU 拷贝) | 低(1 次 CPU 拷贝) |
| 内存占用 | 高(用户态 + 内核态各一份) | 低(仅内核态一份) |
| 系统调用次数 | 多(多次 read/write) | 少(1 次 sendfile) |
| 吞吐量 | 低 | 高(典型提升 30%+) |
④ 关键知识点
a '零拷贝'不是真的零拷贝
- 是'零 CPU 拷贝',DMA 拷贝(硬件直接操作内存)仍存在,只是 DMA 不消耗 CPU;
- 核心是跳过'用户态缓冲区'这个中间环节。
b 适用场景
- 大文件传输(如视频、日志、静态资源);
- 高性能网络服务(如 Web 服务器、文件服务器);
- 数据无需修改的场景(若要修改数据,必须进入用户态,零拷贝不适用)。
c 核心 API
- Linux:
sendfile()、splice()、tee();
- Java:
FileChannel.transferTo()(底层封装 sendfile);
- Windows:
TransmitFile()。
7 单例模式
① 单例模式定义
单例模式是**设计模式中'创建型模式'**的一种,核心目标是:保证一个类在程序运行期间,全局只创建一个实例对象,且提供一个全局唯一的访问入口。
② 单例模式的核心要求
- 私有构造函数:禁止外部直接
new 该类对象(否则能创建多个实例);
- 私有拷贝构造 / 赋值运算符:禁止通过拷贝 / 赋值创建新实例;
- 静态公有接口:提供全局唯一的访问入口(如
instance()),负责创建 / 返回唯一实例。
③ C++ 单例模式的常见实现
a. 饿汉式单例(最简单,推荐小型程序)
核心逻辑:程序启动时(静态变量初始化阶段)就创建实例,线程安全(C++11 后静态变量初始化是线程安全的)
#include <iostream>
using namespace std;
class Logger : noncopyable {
private:
Logger() { cout << "Logger 构造(饿汉式)" << endl; }
static Logger instance_;
public:
static Logger& instance() {
return instance_;
}
void log(const string& msg) { cout << "日志:" << msg << endl; }
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
Logger Logger::instance_;
int main() {
Logger& logger1 = Logger::instance();
logger1.log("第一次调用");
Logger& logger2 = Logger::instance();
logger2.log("第二次调用");
cout << "logger1 地址:" << &logger1 << endl;
cout << "logger2 地址:" << &logger2 << endl;
return 0;
}
- 优点:实现简单、线程安全(无需加锁)、无内存泄漏风险;
- 缺点:实例在程序启动时就创建,即使没用到也会占用内存(比如日志类如果全程没调用,也会初始化)。
b. 懒汉式单例(懒加载,推荐大型程序)
核心逻辑:第一次调用 instance() 时才创建实例(懒加载),节省内存;C++11 后用'局部静态变量'实现,天然线程安全。
class Logger {
private:
Logger() { cout << "Logger 构造(懒汉式)" << endl; }
public:
static Logger& instance() {
static Logger instance_;
return instance_;
}
void log(const string& msg) { cout << "日志:" << msg << endl; }
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
int main() {
cout << "程序启动,还没创建 Logger 实例" << endl;
Logger& logger1 = Logger::instance();
logger1.log("第一次调用");
Logger& logger2 = Logger::instance();
logger2.log("第二次调用");
return 0;
}
- 优点:懒加载(不用不创建)、线程安全(C++11 局部静态变量初始化是原子操作)、实现简洁;
- 缺点:C++11 前需手动加锁(如
std::mutex),否则多线程调用会创建多个实例。
多线程下单例的核心问题:懒汉式初始化时,多个线程同时创建实例,破坏唯一性;
C++11 及以上优先用'局部静态变量懒汉式'(更简洁),兼容旧版本用 call_once + once_flag。
8 private protect public
| 修饰符 | 类内访问 | 子类(派生类)访问 | 类外(全局 / 其他类)访问 |
|---|
public | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ |
private | ✅ | ❌ | ❌ |
子类继承父类时(public/protected/private 继承),会改变父类成员在子类中的访问权限,核心规则:
| 父类成员权限 | public 继承 | protected 继承 | private 继承 |
|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
9 C++ 变量作用域
- 局部作用域:在函数内部声明的变量具有局部作用域,它们只能在函数内部访问。局部变量在函数每次被调用时被创建,在函数执行完后被销毁。
- 全局作用域:在所有函数和代码块之外声明的变量具有全局作用域,它们可以被程序中的任何函数访问。全局变量在程序开始时被创建,在程序结束时被销毁。
- 块作用域:在代码块内部声明的变量具有块作用域,它们只能在代码块内部访问。块作用域变量在代码块每次被执行时被创建,在代码块执行完后被销毁。
- 类作用域:在类内部声明的变量具有类作用域,它们可以被类的所有成员函数访问。类作用域变量的生命周期与类的生命周期相同。
在程序中,局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。
当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化为下列值:
| 数据类型 | 初始化默认值 |
|---|
| int | 0 |
| char | '\0' |
| float | 0 |
| double | 0 |
| pointer | NULL |
10 栈 vs 堆
① 概念对比
| 特性 | 栈(Stack) | 堆(Heap) |
|---|
| 管理方 | 编译器自动管理 | 程序员手动管理(new/delete) |
| 内存分配方式 | 连续内存,栈帧自动增长 / 收缩 | 非连续内存,按需动态分配 |
| 大小限制 | 较小(通常几 MB,系统固定) | 较大(接近系统可用内存,GB 级) |
| 访问速度 | 极快(直接访问栈指针) | 较慢(需通过指针间接访问) |
| 生命周期 | 随作用域结束销毁(如函数执行完毕) | 直到手动释放(delete)或程序结束 |
| 碎片化 | 无(栈帧连续分配 / 释放) | 有(频繁分配 / 释放易产生内存碎片) |
| 溢出风险 | 有(递归过深 / 局部变量过多触发栈溢出) | 无(但分配失败返回 nullptr/ 抛异常) |
| 示例 | int a = 10;(局部变量) | int* p = new int(10);(动态内存) |
② C++ 不同数据的存储位置汇总表
| 数据类型 / 场景 | 存储位置 | 关键说明 |
|---|
| 局部变量(函数 / 代码块内) | 栈 | 作用域结束自动销毁,如 void func() { int x = 5; } 中的 x |
| 函数参数 | 栈 | 随函数调用入栈,函数结束出栈,如 func(int a) 中的 a |
临时变量(如 1+2、temp()) | 栈 | 表达式执行完毕后立即销毁 |
static 局部变量 | 全局 / 静态区 | 虽定义在函数内,但生命周期贯穿程序,仅初始化一次(如 func(){static int x;}) |
| 全局变量 / 全局常量 | 全局 / 静态区 | 程序启动时分配,结束时释放,所有作用域可见 |
static 全局变量 | 全局 / 静态区 | 仅当前编译单元(.cpp)可见,避免跨文件冲突 |
动态分配的内存(new/malloc) | 堆 | 需手动 delete/free,否则内存泄漏,如 int* p = new int; 中的 *p |
| 类 / 结构体实例(局部) | 栈 | Person p;(栈上对象,析构函数自动调用) |
| 类 / 结构体实例(动态) | 堆 | Person* p = new Person;(堆上对象,需 delete p 调用析构函数) |
| 数组(局部) | 栈 | int arr[10];(栈上数组,大小固定) |
| 动态数组 | 堆 | int* arr = new int[10];(堆上数组,需 delete[] arr 释放) |
字符串常量(如 "hello") | 常量区 | 只读内存,程序结束释放,不可修改(修改会触发未定义行为) |
| 匿名命名空间内的变量 | 全局 / 静态区 | 仅当前编译单元可见,等价于 static 全局变量 |
| 函数体本身 | 代码区 | 存储编译后的二进制指令,只读 |
③ 补充说明:
- 全局 / 静态区:独立于栈和堆,存储全局变量、
static 变量,程序启动时分配,结束时释放;
- 常量区:存储字符串常量、
const 全局常量,只读属性,不可修改;
- 代码区:存储函数的二进制执行代码,只读,与数据区分离;
- 堆内存的坑:
- 忘记
delete 会导致内存泄漏(程序运行中占用内存不释放);
- 重复
delete 或 delete 后使用指针会导致野指针(未定义行为);
- C++11 后推荐用智能指针(
unique_ptr/shared_ptr)管理堆内存,避免手动释放。
- 栈地址通常是'高地址'且连续;堆地址是'低地址'且分散;全局 / 静态区地址介于栈和堆之间;常量区地址与全局区接近但只读。
11 static VS const
| 维度 | const | static |
|---|
| 核心目标 | 限制修改(只读) | 控制作用域 / 生命周期(存储 / 可见性) |
| 存储位置 | 由变量作用域决定:- 局部 const → 栈- 全局 const → 常量区- 类成员 const → 每个对象(除非加 static) | 固定存储在全局 / 静态区:- 局部 static → 全局区(生命周期全程)- 全局 static → 全局区(可见性仅当前文件)- 类 static → 全局区(类共享) |
| 可修改性 | 不可修改(只读) | 可修改(除非叠加 const,如 static const) |
| 初始化要求 | 必须初始化(局部 / 全局 const 均需) | - 局部 static:可延迟初始化(首次调用时)- 全局 static:可默认初始化(如 static int x;)- 类 static:需类外初始化(C++17 前) |
| 作用域(局部变量) | 作用域随代码块结束,但值不可改 | 作用域仍为局部,但生命周期贯穿程序 |
| 作用域(全局变量) | 可见性为整个程序(跨文件),但不可改 | 可见性限制为当前编译单元(.cpp),可修改 |
| 类成员场景 | 修饰类成员:每个对象有独立的只读副本;修饰成员函数:保证函数不修改类成员 | 修饰类成员:所有对象共享一个副本,属于类;修饰成员函数:无 this 指针,只能访问静态成员 |
| 示例 | const int a = 10; // 只读``const int* p = &a; // 指针指向只读数据 | static int b = 20; // 全局区,当前文件可见``void func(){static int c=30;} // 仅初始化一次 |
12 c++ 存储类
存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C++ 程序中可用的存储类:
- auto:这是默认的存储类说明符,通常可以省略不写。auto 指定的变量具有自动存储期,即它们的生命周期仅限于定义它们的块(block)。auto 变量通常在栈上分配。
- register:用于建议编译器将变量存储在 CPU 寄存器中以提高访问速度。在 C++11 及以后的版本中,register 已经是一个废弃的特性,不再具有实际作用。
- static:用于定义具有静态存储期的变量或函数,它们的生命周期贯穿整个程序的运行期。在函数内部,static 变量的值在函数调用之间保持不变。在文件内部或全局作用域,static 变量具有内部链接,只能在定义它们的文件中访问。
- extern:用于声明具有外部链接的变量或函数,它们可以在多个文件之间共享。默认情况下,全局变量和函数具有 extern 存储类。在一个文件中使用 extern 声明另一个文件中定义的全局变量或函数,可以实现跨文件共享。
- mutable (C++11):用于修饰类中的成员变量,允许在 const 成员函数中修改这些变量的值。通常用于缓存或计数器等需要在 const 上下文中修改的数据。
#include <iostream>
class Example {
public:
Example() : value(0), cachedValue(0) {}
int getValue() const {
return value;
}
void increment() {
++value;
cachedValue = value * 2;
}
int getCachedValue() const {
return cachedValue;
}
private:
int value;
mutable int cachedValue;
};
int main() {
const Example ex;
std::cout << "Value: " << ex.getValue() << std::endl;
std::cout << "Cached Value: " << ex.getCachedValue() << std::endl;
return 0;
}
- thread_local (C++11):用于定义具有线程局部存储期的变量,每个线程都有自己的独立副本。线程局部变量的生命周期与线程的生命周期相同。在操作系统中,线程的执行由 CPU 调度器决定,而调度器采用'抢占式'策略:
- 即使
t1先被创建(std::thread t1(...)),也不代表t1会先获得 CPU 执行权——调度器可能先调度t2,导致t2先进入thread_func、先加锁、先输出。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
thread_local int tl_id;
void thread_func(int id) {
std::lock_guard<std::mutex> lock(mtx);
tl_id = id + 3;
std::cout << "thread:" << id << " tl_id = " << tl_id << std::endl;
}
int main() {
std::thread t1(thread_func, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
std::thread t2(thread_func, 2);
t1.join();
t2.join();
return 0;
}
13 c++ 获取时间
ctime, localtime 是 C 标准库中不安全的函数(返回指向全局静态缓冲区的指针,线程不安全 + 缓冲区溢出风险),VS 编译器推荐改用微软扩展的安全版本 ctime_s, localtime_s。
#include <iostream>
#include <ctime>
#include <iomanip>
#include <chrono>
#include <cstring>
void getTime01() {
time_t now = time(0);
char dt[26];
errno_t err = ctime_s(dt, sizeof(dt), &now);
if (err == 0) {
std::cout << "本地时间和日期:" << dt << std::endl;
} else {
std::cerr << "获取时间失败,错误码:" << err << std::endl;
}
}
void getTime02() {
time_t now = time(0);
std::cout << "1900 年到现在进过秒数:" << now << std::endl;
tm ltm{};
errno_t err = localtime_s(<m, &now);
std::cout << "年:" << 1900 + ltm.tm_year << std::endl;
std::cout << "月:" << 1 + ltm.tm_mon << std::endl;
std::cout << "日:" << ltm.tm_mday << std::endl;
std::cout << "星期:" << ltm.tm_wday << std::endl;
std::cout << "小时:" << ltm.tm_hour << std::endl;
std::cout << "分钟:" << ltm.tm_min << std::endl;
std::cout << "秒:" << ltm.tm_sec << std::endl;
}
void getTime03() {
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
std::cout << now_c << std::endl;
std::tm ltm{};
#ifdef _MSC_VER
localtime_s(<m, &now_c);
#else
localtime_r(&now_c, <m);
#endif
std::cout << "1970 年到现在经过秒数:" << now_c << std::endl;
std::cout << "年:" << 1900 + ltm.tm_year << std::endl;
std::cout << "月:" << 1 + ltm.tm_mon << std::endl;
std::cout << "日:" << ltm.tm_mday << std::endl;
std::cout << "星期:" << ltm.tm_wday << std::endl;
std::cout << "小时:" << ltm.tm_hour << std::endl;
std::cout << "分钟:" << ltm.tm_min << std::endl;
std::cout << "秒:" << ltm.tm_sec << std::endl;
std::cout << "完整时间(put_time)" << std::put_time(<m, "%Y-%m-%d %H:%M:%S") << std::endl;
}
int main() {
getTime01();
getTime02();
getTime03();
}
14 c++ 文件和流
| 数据类型 | 描述 |
|---|
| ofstream | 该数据类型表示输出文件流,用于创建文件并向文件写入信息。 |
| ifstream | 该数据类型表示输入文件流,用于从文件读取信息。 |
| fstream | 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。 |
| 模式标志 | 描述 |
|---|
| ios::app | 追加模式。所有写入都追加到文件末尾。 |
| ios::ate | 文件打开后定位到文件末尾。 |
| ios::in | 打开文件用于读取。 |
| ios::out | 打开文件用于写入。 |
| ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
可以把以上两种或两种以上的模式结合使用。例如,如果想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么可以使用下面的语法:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc);
类似地,如果想要打开一个文件用于读写,可以使用下面的语法:
ifstream afile;
afile.open("file.dat", ios::out | ios::in);
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ofstream ofs;
ofs.open("text.txt", std::ios::out);
std::string data;
std::cout << "请输入名字";
std::getline(std::cin, data);
ofs << data << std::endl;
std::cout << "输入年龄:";
std::cin >> data;
ofs << data << std::endl;
ofs.close();
std::ifstream ifs;
ifs.open("text.txt", std::ios::in);
std::cout << "Reading from the file" << std::endl;
while (std::getline(ifs, data))
{
std::cout << data << std::endl;
}
ifs.close();
return 0;
}
15 c++ 信号
#include <iostream>
#include <csignal>
#include <Windows.h>
void signalHandler(int signum) {
std::cout << "Interrupt siganl(" << signum << ") received" << std::endl;
exit(signum);
}
int main() {
int i = 0;
signal(SIGINT, signalHandler);
while (++i) {
std::cout << "Going to sleep...." << std::endl;
if (i == 4) {
raise(SIGINT);
}
Sleep(2000);
}
return 0;
}
16 c++ 多线程
线程是程序中的轻量级执行单元,允许程序同时执行多个任务。
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。
一般情况下,两种类型的多任务处理:基于进程和基于线程。
- 基于进程的多任务处理是程序的并发执行。
- 基于线程的多任务处理是同一程序的片段的并发执行。
C++ 多线程编程涉及在一个程序中创建和管理多个并发执行的线程。
① 概念
- 线程是程序执行中的单一顺序控制流,多个线程可以在同一个进程中独立运行。
- 线程共享进程的地址空间、文件描述符、堆和全局变量等资源,但每个线程有自己的栈、寄存器和程序计数器。
并发 (Concurrency) 与并行 (Parallelism)
- 并发:多个任务在时间片段内交替执行,表现出同时进行的效果。
- 并行:多个任务在多个处理器或处理器核上同时执行。
C++11 及以后的标准提供了多线程支持,核心组件包括:
- std::thread:用于创建和管理线程。
- std::mutex:用于线程之间的互斥,防止多个线程同时访问共享资源。
- std::lock_guard 和 std::unique_lock:用于管理锁的获取和释放。
- std::condition_variable:用于线程间的条件变量,协调线程间的等待和通知。
- std::future 和 std::promise:用于实现线程间的值传递和任务同步。
② 三种创建线程的方式
a 函数指针
b 函数对象
c lambda 表达式
#include <iostream>
#include <thread>
void func01(int num) {
for (int i = 0; i < num; i++) {
std::cout << "create thread by ptr: thread(" << i << ")" << std::endl;
}
}
class func02 {
public:
void operator()(int num) const {
for (int i = 0; i < num; i++) {
std::cout << "create thread by object: thread(" << i << ")" << std::endl;
}
}
};
int main() {
std::thread thread01(func01, 5);
std::thread thread02(func02(), 5);
std::thread thread03([](int num) {
for (int i = 0; i < num; i++) {
std::cout << "create thread by lambda: thread(" << i << ")" << std::endl;
}
}, 5);
thread01.join();
thread02.join();
thread03.join();
return 0;
}
③ 线程传参
std::thread t(func, arg1, arg2);
如果需要传递引用参数,需要使用 std::ref:
#include <iostream>
#include <thread>
void increment(int& num) {
num += 1;
}
int main() {
int a = 6;
std::thread thread01(increment, std::ref(a));
thread01.join();
std::cout << a << std::endl;
}
在 C++ 中,std::ref() 是定义在 <functional> 头文件中的工具函数,核心作用是将变量包装为'引用包装器(std::reference_wrapper)',解决'值传递场景下无法传递真正引用'的问题(比如向线程、函数模板、绑定函数传参时)。
C++ 中很多场景(如 std::thread、std::bind、std::function、算法模板)的参数传递默认是值拷贝——即使你写了引用符号 &,也会被拷贝语义覆盖,无法真正传递引用。
| 核心点 | 总结 |
|---|
| 作用 | 将变量包装为可拷贝的引用包装器,让拷贝传参场景能传递真正的引用 |
| 解决的问题 | 线程、bind、模板等场景默认拷贝传参,无法直接传递引用的问题 |
| 常用搭配 | std::thread、std::bind、std::async |
| 易错点 | 确保被包装变量的生命周期覆盖使用场景,避免悬空引用 |
场景强制拷贝'的核心含义是:某些 C++ 语法 / 库函数的设计规则,会强制把你传入的参数做一次'值拷贝',哪怕你写了引用符号 &,也无法改变'传拷贝而非传引用'的结果。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online