c++_学习笔记【01】
目录
学习笔记
1 指针的数组下标语法本质
1 指针的数组下标语法本质是 “指针偏移 + 解引用”
int* array_index = static_cast<int*>(ptr); // ptr指向operator new[]分配的连续内存
array_index[i] → *(array_index + i) // 等价写法
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> // memset 头文件
using namespace std;
int main() {
char str[10];
// 把 str 的前5个字节设为 'a'(ASCII码97),剩余字节不变
memset(str, 'a', 5);
// 把第6个字节设为 '\0'(字符串结束符)
memset(str + 5, '\0', 1);
cout << "str = " << str << endl; // 输出:aaaaa
cout << "str[0] 字节值:" << (int)str[0] << endl; // 输出:97('a'的ASCII)
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++ 实现 “零开销抽象” 的关键细节。
例子:重写MyString
#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缓冲区 → 网卡

图片转载于小林code公众:https://xiaolincoding.com/os/8_network_system/zero_copy.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89-dma-%E6%8A%80%E6%9C%AF
这个过程包含 4 次数据拷贝 + 2 次用户态 / 内核态切换:
read():磁盘 → 内核缓冲区(DMA 拷贝,无 CPU 参与);read():内核缓冲区 → 用户缓冲区(CPU 拷贝);write():用户缓冲区 → Socket 内核缓冲区(CPU 拷贝);write():Socket 内核缓冲区 → 网卡(DMA 拷贝,无 CPU 参与)。
其中,第 2、3 步的 CPU 拷贝是完全冗余的—— 数据只是 “路过” 用户态,没有任何修改,却消耗了 CPU 和内存带宽。
① 零拷贝的核心优化:跳过用户态拷贝
零拷贝的核心思路是:让数据直接在内核态流转,不进入用户态,常见实现方式有 2 种(Linux 下):
a. sendfile() 系统调用(最常用)

图片转载于小林code公众:https://xiaolincoding.com/os/8_network_system/zero_copy.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89-dma-%E6%8A%80%E6%9C%AF
代码中<sys/sendfile.h>就是为这个函数服务的,它的流程:
plaintext
磁盘 → 内核态缓冲区 → 内核态Socket缓冲区 → 网卡
- 拷贝次数:3 次(2 DMA 拷贝, 1 CPU );
- 切换次数:2 次(用户态→内核态→用户态);
- 核心逻辑:
sendfile()直接在内核中完成 “内核缓冲区 → Socket 缓冲区” 的转发,数据全程不进入用户态。
b. splice()/tee() 系统调用(更极致)
进一步优化:连 “内核缓冲区 → Socket 缓冲区” 的拷贝都跳过,通过内存地址映射让两个内核缓冲区共享数据,流程:
plaintext
磁盘 → 内核态缓冲区(映射到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); // 每次都要CPU拷贝
}
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 单例模式
①单例模式定义
单例模式是设计模式中 “创建型模式” 的一种,核心目标是:保证一个类在程序运行期间,全局只创建一个实例对象,且提供一个全局唯一的访问入口。
② 单例模式的核心要求
要实现单例,必须满足 3 个条件:
- 私有构造函数:禁止外部直接
new该类对象(否则能创建多个实例); - 私有拷贝构造 / 赋值运算符:禁止通过拷贝 / 赋值创建新实例;
- 静态公有接口:提供全局唯一的访问入口(如
instance()),负责创建 / 返回唯一实例。
③C++ 单例模式的常见实现
a. 饿汉式单例(最简单,推荐小型程序)
核心逻辑:程序启动时(静态变量初始化阶段)就创建实例,线程安全(C++11 后静态变量初始化是线程安全的)
#include <iostream>
using namespace std;
class Logger : noncopyable {
private:
// 1. 私有构造函数:外部不能new
Logger() {
cout << "Logger 构造(饿汉式)" << endl;
}
// 2. 静态私有实例:程序启动时就初始化(全局唯一)
static Logger instance_;
public:
// 3. 静态公有接口:返回唯一实例
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() {
// 多次调用instance(),拿到的是同一个对象
Logger& logger1 = Logger::instance();
logger1.log("第一次调用");
Logger& logger2 = Logger::instance();
logger2.log("第二次调用");
cout << "logger1 地址:" << &logger1 << endl;
cout << "logger2 地址:" << &logger2 << endl; // 和logger1地址相同
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; // 读取常量成员
}
// 修改 mutable 成员
void increment() {
++value;
cachedValue = value * 2; // 修改 mutable 成员
}
int getCachedValue() const {
return cachedValue; // 读取 mutable 成员
}
private:
int value; // 常规成员,不能在 const 函数中修改
mutable int cachedValue; // 可修改成员,可以在 const 函数中修改
};
int main() {
const Example ex;
// ex.increment(); // 错误:无法在 const 对象上调用非 const 函数
// ex.value = 10; // 错误:无法修改 const 对象的成员
std::cout << "Value: " << ex.getValue() << std::endl;
std::cout << "Cached Value: " << ex.getCachedValue() << std::endl; // 输出为 0
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)
{
//mtx.lock();
std::lock_guard<std::mutex> lock(mtx);
tl_id = id+3;
std::cout<< "thread:" << id << " tl_id = " << tl_id << std::endl;
//mtx.unlock();
}
int main()
{
std::thread t1(thread_func, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(2000));//强制t1先加入调度区
std::thread t2(thread_func, 2);
t1.join();
t2.join();
return 0;
}
13 c++获取时间
ctime,localtime 是 C 标准库中不安全的函数(返回指向全局静态缓冲区的指针,线程不安全 + 缓冲区溢出风险),VS 编译器推荐改用微软扩展的安全版本ctiem_s localtime_s。
#include<iostream>
#include<ctime> // std::tm、std::localtime
#include<iomanip> //std::put_time(核心!缺一不可)
#include<chrono> //C++11时间库
#include<cstring>
void getTime01()
{
time_t now = time(0);
//char* dt = ctime(&now); 存在风险
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 = localtime(&now); 不安全
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 // VS编译器
localtime_s(<m, &now_c);
#else // GCC/Clang
localtime_r(&now_c, <m); // Linux/macOS的安全版本
#endif
// 4. 输出各个时间字段(修正了tm_wday的含义)
std::cout << "1970年到现在经过秒数:" << now_c << std::endl;
std::cout << "年:" << 1900 + ltm.tm_year << std::endl; // 注意:tm_year是从1900开始,不是1970!
std::cout << "月:" << 1 + ltm.tm_mon << std::endl; // tm_mon=0(1月)~11(12月)
std::cout << "日:" << ltm.tm_mday << std::endl; // 日期(1~31)
std::cout << "星期:" << ltm.tm_wday << std::endl; // 0(周日)~6(周六)
std::cout << "小时:" << ltm.tm_hour << std::endl; // 0~23
std::cout << "分钟:" << ltm.tm_min << std::endl; // 0~59
std::cout << "秒:" << ltm.tm_sec << std::endl; // 0~60(闰秒)
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);
//char data[100];
std::string data;
std::cout << "请输入名字";
//std::cin.getline(data, 100); 不可以读空格
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)) // 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++ 多线程编程涉及在一个程序中创建和管理多个并发执行的线程。
①概念
线程 (Thread)
- 线程是程序执行中的单一顺序控制流,多个线程可以在同一个进程中独立运行。
- 线程共享进程的地址空间、文件描述符、堆和全局变量等资源,但每个线程有自己的栈、寄存器和程序计数器。
并发 (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++ 语法 / 库函数的设计规则,会强制把你传入的参数做一次 “值拷贝”,哪怕你写了引用符号 &,也无法改变 “传拷贝而非传引用” 的结果。
更多推荐


所有评论(0)