学习笔记

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::functionstd::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_tsize_tsocklen_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);

背后的真实流程:

  1. 你的程序在用户态执行open(),发现这是系统调用,触发 CPU 权限切换;
  2. CPU 从用户态切到内核态,执行内核的open()逻辑(检查文件权限、分配文件描述符、读取磁盘目录);
  3. 内核完成操作后,CPU 从内核态切回用户态,把fileFd返回给你的程序。

这个 “切来切去” 的过程有开销,而零拷贝不仅减少了拷贝,还减少了态切换次数(传统方式 2 次切换,零拷贝 1 次),因此性能更高。

③ 关键知识点

  1. 核心目的:用户态 / 内核态分离是为了「权限隔离 + 系统安全」,防止普通程序乱操作核心资源;
  2. 系统调用:普通程序访问核心资源的唯一合法方式,会触发「用户态↔内核态」切换;
  3. 跨态拷贝:数据在用户态和内核态之间的拷贝需要 CPU 参与,是性能瓶颈;
  4. 零拷贝的本质:让数据全程在内核态流转,跳过用户态,避免跨态拷贝和多余的态切换。

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 次用户态 / 内核态切换

  1. read():磁盘 → 内核缓冲区(DMA 拷贝,无 CPU 参与);
  2. read():内核缓冲区 → 用户缓冲区(CPU 拷贝);
  3. write():用户缓冲区 → Socket 内核缓冲区(CPU 拷贝);
  4. 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 个条件:

  1. 私有构造函数:禁止外部直接 new 该类对象(否则能创建多个实例);
  2. 私有拷贝构造 / 赋值运算符:禁止通过拷贝 / 赋值创建新实例;
  3. 静态公有接口:提供全局唯一的访问入口(如 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+2temp() 表达式执行完毕后立即销毁
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 全局变量
函数体本身 代码区 存储编译后的二进制指令,只读

③补充说明:

  1. 全局 / 静态区:独立于栈和堆,存储全局变量、static 变量,程序启动时分配,结束时释放;

  2. 常量区:存储字符串常量、const 全局常量,只读属性,不可修改;

  3. 代码区:存储函数的二进制执行代码,只读,与数据区分离;

  4. 堆内存的坑:

    • 忘记 delete 会导致内存泄漏(程序运行中占用内存不释放);
    • 重复 deletedelete 后使用指针会导致野指针(未定义行为);
    • C++11 后推荐用智能指针(unique_ptr/shared_ptr)管理堆内存,避免手动释放。
  5. 栈地址通常是 “高地址” 且连续;

    堆地址是 “低地址” 且分散;

    全局 / 静态区地址介于栈和堆之间;

    常量区地址与全局区接近但只读。

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(&ltm, &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(&ltm, &now_c);
	#else // GCC/Clang
		localtime_r(&now_c, &ltm); // 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(&ltm, "%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_guardstd::unique_lock:用于管理锁的获取和释放。
  • std::condition_variable:用于线程间的条件变量,协调线程间的等待和通知。
  • std::futurestd::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::threadstd::bindstd::function、算法模板)的参数传递默认是值拷贝—— 即使你写了引用符号 &,也会被拷贝语义覆盖,无法真正传递引用。

核心点 总结
作用 将变量包装为可拷贝的引用包装器,让拷贝传参场景能传递真正的引用
解决的问题 线程、bind、模板等场景默认拷贝传参,无法直接传递引用的问题
常用搭配 std::threadstd::bindstd::async
易错点 确保被包装变量的生命周期覆盖使用场景,避免悬空引用

场景强制拷贝” 的核心含义是:某些 C++ 语法 / 库函数的设计规则,会强制把你传入的参数做一次 “值拷贝”,哪怕你写了引用符号 &,也无法改变 “传拷贝而非传引用” 的结果

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐