异常
std::exception的派生类作为异常对象
在 C++ 中,将
std::exception的派生类作为异常对象,是构建健壮、可维护异常处理体系的核心实践。这种做法不仅能清晰地传达错误意图,还能利用标准库提供的统一接口。在实际开发中,标准异常可能无法满足所有业务需求。此时,最佳实践是继承一个合适的标准异常派生类来创建自己的异常类。通常,我们会选择继承
std::runtime_error或std::logic_error,因为它们都接受一个std::string参数来初始化错误消息
cpp#include <stdexcept> // std::runtime_error #include <string> // 自定义异常类,继承自 std::runtime_error class MyException : public std::runtime_error { public: // 构造函数,传递消息给基类,并初始化自己的成员 explicit MyException(const std::string& msg, int error_code) : std::runtime_error(msg), error_code_(error_code) {} // 提供获取错误码的方法 int get_error_code() const { return error_code_; } private: int error_code_; };创建好自定义异常后,就可以在函数中抛出,并在调用处进行捕获和处理。由于自定义异常是标准异常的派生类,因此既可以精确捕获,也可以被基类引用捕获。
cpp#include <iostream> // 一个可能抛出异常的函数 void risky_function() { // ... 一些业务逻辑 ... throw MyException("文件打开失败", 1001); } int main() { try { risky_function(); } // 优先捕获具体的自定义异常,以获取详细信息 catch (const MyException& e) { std::cout << "捕获到自定义异常: " << e.what() << ", 错误码: " << e.get_error_code() << '\n'; } // 捕获其他所有继承自 std::exception 的异常 catch (const std::exception& e) { std::cout << "捕获到标准异常: " << e.what() << '\n'; } return 0; }
std::terminate函数是什么
std::terminate是 C++ 标准库提供的一个函数,用于在程序遇到无法恢复的严重错误时,立即终止程序的执行。它通常不是由程序员直接调用,而是由 C++ 运行时系统在特定致命条件下自动触发。当
std::terminate被调用时,程序会立即停止,不会执行任何栈展开(stack unwinding)操作,这意味着局部对象的析构函数不会被调用,可能导致资源无法被正常释放。
td::terminate主要在以下几种情况下被自动调用:
未捕获的异常 (Uncaught Exception)
当一个异常被抛出,但在整个调用栈中都找不到匹配的
catch块来处理它时,程序会调用std::terminate。这是最常见的触发场景。栈展开期间的二次异常 (Exception during Stack Unwinding)
这是一个非常关键且危险的场景。当一个异常正在传播(即正在进行栈展开,调用沿途对象的析构函数)时,如果某个析构函数又抛出了一个新的、未被其自身捕获的异常,C++ 运行时会立即调用
std::terminate。这是因为运行时无法同时处理两个活跃的异常。
noexcept函数抛出异常如果一个函数被声明为
noexcept(表示承诺不抛出任何异常),但其内部实际抛出了异常,程序会立即调用std::terminate。显式调用
程序员也可以在代码中主动调用
std::terminate(),例如在捕获到一个无法处理的异常后,决定终止程序。
抛异常方式是抛对象本身,而不是指向对象的指针。
在 C++ 中,抛出异常对象本身(By Value),而不是抛出指向对象的指针,是异常处理机制中最核心的原则之一。
为什么不能抛指针?
如果你抛出的是一个指针(例如
throw new MyException()或throw &local_obj),会带来极大的风险
指针类型 风险描述 后果 堆对象指针 throw new MyException()内存泄漏: catch块捕获到指针后,谁负责delete它?如果捕获者忘了delete,内存就会泄漏。资源管理混乱,违反 RAII 原则。 栈对象指针 throw &local_obj悬空指针:局部变量 local_obj在函数返回时会被销毁。异常传播到上层时,指针指向的内存已经无效。未定义行为,程序极大概率崩溃。 字面量/裸指针 throw "error"类型不安全:虽然语法允许,但这绕过了类型系统,无法携带结构化信息,且可能导致 std::terminate。无法统一处理错误。 抛出对象本身的优势
正如你所说,抛出对象本身(通常建议通过值抛出,通过常量引用捕获)有以下巨大优势:
- 避免对象切片 (Object Slicing) :
如果你抛出一个派生类对象(如FileNotFound),而用基类指针捕获,可能会丢失派生类特有的信息。但如果你抛出的是对象本身,并且catch使用引用(catch (const std::exception& e)),那么多态性会被完美保留,你可以调用虚函数(如what())获取详细信息。- 自动资源管理 (RAII) :
异常对象本身也是一个 C++ 对象。当它被销毁时(即catch块结束时),它的析构函数会被自动调用。这意味着你可以在异常类内部安全地管理资源(如字符串消息、错误码等),而无需担心内存泄漏。- 类型安全 :
catch块会根据类型精确匹配。抛出对象允许编译器进行类型检查,而抛出void*或int*则完全失去了这种保护。
cpp#include <iostream> #include <exception> class BadException : public std::exception { public: const char* what() const noexcept override { return "这是一个糟糕的异常"; } }; // ❌ 错误示范:抛出局部对象的指针 void dangerousFunction() { BadException localEx; // 局部对象,位于栈上 // ❌ 致命错误:抛出局部变量的地址 // 当函数返回时,localEx 会被销毁,抛出的指针将指向无效内存 throw &localEx; } int main() { try { dangerousFunction(); } catch (const std::exception* e) { // 这里访问 e->what() 可能会导致程序崩溃,因为 e 指向的内存已经无效 std::cout << "捕获到: " << e->what() << std::endl; } return 0; }
抛出方式 内存管理责任 潜在后果 抛对象 ( throw Ex())系统自动管理 安全,无内存泄漏,无悬空指针。 抛堆指针 ( throw new Ex())程序员手动管理 catch块必须delete,否则内存泄漏。抛栈指针 ( throw &ex)不可控 函数返回后对象销毁,导致悬空指针/程序崩溃。
禁止给函数加"throw"异常说明
这不仅仅是一个语法的变更,更是为了性能 、安全性 和编译器优化。
特性 throw()(旧式)noexcept(现代)检查时机 运行时 (Runtime) 编译期 (Compile-time) 违反后果 调用 std::unexpected()调用 std::terminate()性能影响 有额外开销,阻碍优化 零开销,辅助优化 标准状态 C++17 已移除 (动态说明) C++11 起推荐使用 移动语义 无特殊帮助 启用 std::vector 等的高效移动 ❌ 错误写法 (旧式):
cpp// 不推荐:不仅性能差,而且在 C++17 下可能报警告 void oldFunction() throw() { // ... }✅ 正确写法 (现代):
cpp// 推荐:明确承诺不抛异常,编译器可优化 void newFunction() noexcept { // ... } // 明确说明可能抛异常 (默认情况) void mayThrowFunction() noexcept(false) { throw std::runtime_error("Error"); }C++11 引入的
noexcept是throw()的现代替代品,它在编译期工作,更加高效和安全。
编译期检查(零运行时开销)
noexcept是编译器的承诺。如果标记为noexcept的函数抛出了异常,编译器不需要生成运行时检查代码,而是直接调用std::terminate()终止程序。这消除了运行时开销。助力编译器优化
- 当编译器明确知道一个函数绝对不会抛出异常时,它可以大胆地进行优化(例如省略异常处理表、进行更激进的内联),显著提升代码性能。
移动语义的关键
- 这是
noexcept最重要的用途之一。标准库容器(如std::vector)在扩容时,会检查元素的移动构造函数是否标记为noexcept。- 如果是
noexcept,容器会直接使用高效的移动操作;- 如果不是,为了保证强异常安全,容器会退而求其次使用拷贝操作,导致性能下降。
全捕获子句(形如catch (...) { ... })
在 C++ 中,
catch (...)被称为全捕获子句 或通配符捕获 。它的作用是捕获任何类型 的异常,无论该异常是标准库异常、自定义类对象,还是内置类型(如int或const char*)。
~A() noexcept; ~A(); ~A()noexcept(false) ;~A()noexcept(true)区别
核心结论是:
~A() noexcept;和~A() noexcept(true);是完全等价的,也是现代 C++ 的推荐写法 ;~A();在 C++11 及以后标准中默认也是noexcept的 ;而~A() noexcept(false);则是极其危险的"异类",明确表示析构函数可能抛出异常。
声明方式 含义 编译器默认行为 (C++11+) 风险等级 推荐程度 ~A() noexcept;承诺绝不抛异常 显式声明 🟢 安全 ⭐⭐⭐⭐⭐ (最佳实践) ~A();默认不抛异常 自动视为 noexcept🟢 安全 ⭐⭐⭐⭐ (默认即可) ~A() noexcept(true);承诺绝不抛异常 显式声明 🟢 安全 ⭐⭐⭐⭐⭐ (与第一种等价) ~A() noexcept(false);允许抛异常 显式覆盖默认行为 🔴 高危 ⭐ (极不推荐) A() noexcept;
A();
A()noexcept(false) ;
A()noexcept(true): 区别
这四个声明在 C++ 中有着非常明确的区别。与析构函数不同,普通成员函数(如构造函数
A())默认是允许抛出异常的。核心结论是:
A() noexcept;和A() noexcept(true);是完全等价的,用于承诺不抛异常 ;A();是默认状态,允许抛异常 ;而A() noexcept(false);则是显式地声明允许抛异常。
声明方式 含义 默认行为 风险与用途 A() noexcept;承诺绝不抛异常 显式声明 🟢 安全。用于移动构造等关键路径。 A();允许抛异常 默认就是 noexcept(false)🟡 正常。普通函数的默认行为。 A() noexcept(true);承诺绝不抛异常 显式声明 🟢 安全。与第一种完全等价。 A() noexcept(false);允许抛异常 显式声明 🟡 正常。用于明确意图或模板编程。
那些函数是默认加上noexcept
在 C++11 及以后的标准中,编译器会自动为某些特定的函数隐式添加
noexcept属性(即noexcept(true))。
函数类型 默认是否 noexcept?备注 析构函数 ✅ 是 极其重要,防止双重异常崩溃。 移动构造函数 ✅ 是 前提是成员变量也都支持 noexcept移动。移动赋值运算符 ✅ 是 同上。 默认构造函数 ✅ 是 前提是成员变量默认构造不抛异常。 operator delete✅ 是 内存释放不应失败。 普通成员函数 ❌ 否 默认为 noexcept(false)。复制构造函数 ❌ 否 通常涉及内存分配,默认为可能抛异常。 1. 析构函数
这是最重要的一条规则。
- 规则 :类的析构函数(无论是隐式生成的、
= default的,还是用户自定义的)默认都是noexcept的。- 例外 :如果该类的某个基类或成员变量的析构函数是
noexcept(false)的,那么该类的析构函数也会变成noexcept(false)(但在现代 C++ 中,几乎不应该让析构函数允许抛异常)。- 原因 :在栈展开(处理异常)过程中,如果析构函数又抛出了异常,程序会直接崩溃 (
std::terminate)。因此,C++ 强制析构函数默认不抛异常。2. 默认生成的特殊成员函数
当编译器隐式生成(或你显式使用
= default)以下函数时,它们默认是noexcept的,前提是 其所有成员变量和基类的对应操作也是noexcept的:
- 默认构造函数
- 移动构造函数
- 移动赋值运算符
- 复制构造函数 (注:虽然也是默认生成,但因为涉及资源分配,通常包含可能抛异常的操作,但在语法层面,如果成员都是
noexcept的,它也是noexcept的。不过通常大家更关注移动操作的noexcept属性)。关键点:
编译器会检查类中的所有成员。如果你的类包含一个
std::vector成员,而std::vector的移动构造函数是noexcept的,那么你的类的移动构造函数也会自动变成noexcept。如果成员中有某个类型的移动操作可能抛异常,你的类的移动操作也就不会是noexcept。3. 释放内存的
operator delete
- 规则 :所有的
operator delete函数(包括普通版本和数组版本)默认都是noexcept的。- 原因:内存释放操作不应该失败,如果释放内存时抛出异常,通常意味着严重的系统错误。
未受控的内存分配 是什么意思
"未受控的内存分配"是 C/C++ 编程中一种严重的安全漏洞,通常被称为 CWE-789。
简单来说,它的意思是:程序在使用
malloc、calloc或new分配内存时,分配的大小直接由外部输入(如用户输入、网络数据包、文件内容)决定,而程序没有对这个大小进行严格的检查或限制。1. 核心原理
攻击者通过控制程序的输入变量(通常是一个整数),来操纵内存分配函数的大小参数。
典型代码模式(存在漏洞):
cpp// 假设 length 是从网络包或文件中读取的变量 unsigned int length = get_input_from_user(); // 危险:直接使用外部输入作为分配大小 char *buffer = (char *)malloc(length); // 后续操作...如果攻击者能够控制
length的值,可能会引发以下几种灾难性后果:A. 拒绝服务
这是最常见的后果。
- 耗尽内存 :攻击者传入一个极大的数值(例如
0xFFFFFFFF或几个 GB)。程序尝试分配巨大的内存块,导致物理内存和交换空间被迅速耗尽。- 系统崩溃:操作系统可能会因为内存不足而杀死该进程,甚至导致整个系统卡死或重启。
B. 整数溢出导致缓冲区溢出
这是一个非常隐蔽且危险的连锁反应。
- 原理 :
malloc的参数通常是size_t(无符号)。如果攻击者传入的值在数学上很大,但在转换为size_t时发生了整数回绕。- 例子 :假设程序逻辑需要分配
count * size字节。
- 攻击者设置
count = 0x40000001,size = 0x20。- 两者相乘的结果在 32 位系统中会溢出,变成很小的值(例如 32 字节)。
malloc(32)成功分配了一小块内存。- 但程序后续逻辑认为已经分配了巨大的空间,开始向这块小内存中写入大量数据 -> 堆缓冲区溢出。
C. 绕过安全检查
某些程序可能会根据分配的大小来执行不同的逻辑。如果分配大小未受控,攻击者可能诱导程序进入未预期的代码路径。
假设你在写一个图片处理程序:
cpp// 从图片文件头读取宽度和高度 uint32_t width = read_header_width(); uint32_t height = read_header_height(); // 计算总像素大小 size_t size = width * height * 4; // 每个像素4字节 // 漏洞点:如果 width 和 height 都是攻击者构造的极大值 // size 可能会发生整数溢出,变成一个很小的数 char *image_buffer = malloc(size); // 随后程序尝试根据 width 和 height 填充像素 // 这会疯狂地覆盖 image_buffer 之后的内存 fill_pixels(image_buffer, width, height);未受控的内存分配 不仅仅是"忘了检查
malloc返回值",更核心的问题是**"让不可信的外部数据决定了系统资源的消耗量"**。在编写网络服务器、文件解析器等处理外部数据的程序时,这是必须严防死守的底线。
C++基础
signed char 和 unsigned char 和char的区别
在 C/C++ 中,
signed char、unsigned char和char是三种独立的数据类型,而不仅仅是同一个类型的不同写法。虽然它们通常都占用 1 个字节(8位) 的内存空间,但在数值范围 、符号性 以及跨平台行为上有着本质的区别。
特性 signed charunsigned charchar含义 明确有符号的 1 字节整数 明确无符号的 1 字节整数 独立类型,符号性由编译器决定 取值范围 -128 ~ 127 0 ~ 255 取决于平台(可能是 -128~127 或 0~255) 最高位 符号位 (1 代表负数) 数值位 (代表 128) 取决于编译器实现 主要用途 处理带负号的小整数 处理二进制数据、字节流、像素值 处理文本字符串 (ASCII) 跨平台性 高 (行为确定) 高 (行为确定) 低 (存在歧义) 通常 signed char 和 unsigned char 类型只能用于数值的存储和使用
有符号整型 ,无符号整型 ,size_t区别
特性 int(有符号整型)unsigned int(无符号整型)size_t(标准库定义类型)符号性 有符号 (可正可负) 无符号 (仅非负) 无符号 (仅非负) 大小 (32位系统) 通常 4 字节 通常 4 字节 通常 4 字节 大小 (64位系统) 通常 4 字节 (固定) 通常 4 字节 (固定) 通常 8 字节 (随平台变化) 主要用途 通用计算、循环计数、数学逻辑 需要非负数的场景、位运算 内存大小、数组索引、 sizeof返回值溢出/转换风险 与无符号混用时,负数会被转为极大正数 减法结果小于0时会回绕成极大值 赋值给 int可能导致截断或溢出类型转换规则
根据C++标准,当有符号整数类型(如
int)和无符号整数类型(如unsigned int)进行运算时,如果两者占用相同的内存大小,有符号操作数会被隐式转换为无符号类型。1.
int(有符号整型)这是我们最常用的整数类型。
- 特点 :它是有符号的,意味着最高位用于表示正负。
- 范围:在大多数现代编译器中是 32 位,范围约为 -21亿 到 +21亿 ( −231∼231−1−231∼231−1 )。
- 适用场景:一般的数学计算、需要表示负数的逻辑、普通的循环计数器。
- 注意:它的大小通常固定为 4 字节,不随系统是 32 位还是 64 位而改变。
2.
unsigned int(无符号整型)
- 特点 :它是无符号的,所有位都用于表示数值,不能存储负数。
- 范围 :范围是
int正数部分的两倍,约为 0 到 42亿 (0∼232−10∼232−1 )。- 适用场景:确定不会为负数的计数(如人数、物品个数)、位操作(如设置标志位)。
- 陷阱 :如果做减法运算(例如
0 - 1),结果不会变成 -1,而是会"回绕"成一个巨大的正数(42亿...)。3.
size_t(关键类型)这是一个为了跨平台安全而设计 的类型,定义在
<stddef.h>或<cstddef>中。
- 本质 :它是一个
typedef,具体是unsigned int还是unsigned long取决于编译器。- 为什么需要它?
- 可移植性 :在 32 位系统上,
size_t通常是 4 字节;但在 64 位系统上,为了能寻址更大的内存,size_t会变成 8 字节。- 语义明确 :它专门用来表示内存大小 、数组长度 或元素个数。
- 核心地位 :
sizeof运算符的返回类型就是size_t。- C++ 标准库容器(如
std::vector::size())返回的都是size_t。- 内存分配函数(如
malloc)的参数也是size_t。- 风险 :如果你用
int去接收size_t的值(例如在 64 位系统上),如果数值超过 21 亿,就会发生截断或溢出,导致程序崩溃容易出错的地方是将有符号类型 (
int) 和 无符号类型 (size_t,unsigned int) 混用。场景 1:负数比较
cppunsigned int a = 1; int b = -1; if (a > b) { // 你以为是真的?其实是假的! // 因为 b (-1) 会被隐式转换为巨大的无符号整数 }场景 2:循环陷阱
cppstd::vector<int> vec = {1, 2, 3}; // 错误写法:如果 vec 为空,vec.size() 为 0 // 0 - 1 会变成巨大的无符号数,导致死循环 for (size_t i = vec.size() - 1; i >= 0; --i) { ... }建议:
- 涉及内存大小、数组索引、
sizeof时,始终使用size_t。- 涉及通用计算、可能为负数时,使用
int。- 尽量避免两者直接比较或运算,必要时进行显式类型转换。
malloc/strdup函数
场景 malloc/strdup原因 多线程程序 安全 (线程安全) 库内部通过锁机制保护共享资源。 信号处理函数 不安全 (非异步信号安全) 可能导致死锁或破坏堆数据结构。
malloc函数被设计为**线程安全(Thread-Safe)**的。
- 实现机制 :glibc 的
malloc实现(如 ptmalloc2)通过使用互斥锁(mutex)和线程本地存储(TLS)来保护其内部的全局数据结构(如内存堆的元数据)。- 结果 :当多个线程同时调用
malloc时,这些锁机制会确保它们不会同时修改共享资源,从而避免了数据竞争,保证了多线程环境下的安全性。
- 信号处理函数需要调用**异步信号安全(Async-Signal-Safe)**的函数。这类函数可以被信号中断,并在中断后安全地再次执行,而不会导致数据损坏或死锁。
- 为什么
malloc和strdup不安全?
- 非可重入性 :
malloc和strdup(strdup内部会调用malloc)都不是异步信号安全的函数。它们在执行时会持有内部的锁。- 死锁风险 :设想一个场景:主程序的线程正在执行
malloc,并持有了内部的锁。此时,一个信号到来,中断了主程序,转而执行信号处理函数。如果信号处理函数中也调用了malloc或strdup,它会尝试去获取同一个锁。由于这个锁已经被当前线程持有(且不被支持递归获取),线程就会陷入自我等待,导致死锁,程序挂起。
strcpy_s() ,memmove_s() ,memcpy_s() ,sprintf_s() 函数的作用和区别
这四个函数都是 C/C++ 标准库中为了增强安全性 而引入的函数,通常被称为"安全版本"。它们的主要目的是为了解决传统函数(如
strcpy,memcpy,sprintf)容易导致的缓冲区溢出问题。
函数名 核心功能 关键安全机制 内存重叠处理 适用场景 strcpy_s复制字符串 检查目标缓冲区大小 N/A (字符串通常独立) 字符串赋值 memcpy_s复制内存块 检查目标缓冲区大小 不支持 (行为未定义) 高效复制非重叠内存 memmove_s移动/复制内存 检查目标缓冲区大小 支持 (正确处理) 数组内部移动、重叠内存复制 sprintf_s格式化输出 检查目标缓冲区大小 N/A 日志记录、数据转字符串 🛡️
strcpy_s(字符串复制)
- 作用:将一个字符串复制到另一个缓冲区中。
- 对比
strcpy:
strcpy不检查目标缓冲区大小,如果源字符串太长,就会发生溢出。strcpy_s强制要求传入目标缓冲区的大小 。如果源字符串(包括结束符\0)大于目标缓冲区,函数会报错并终止复制,从而避免溢出。- 原型 :
errno_t strcpy_s(char *dest, rsize_t destsz, const char *src);🛡️
memcpy_s(内存复制 - 不重叠)
- 作用:将一块内存中的数据复制到另一块内存中。
- 对比
memcpy:
memcpy假定两块内存不重叠,且不检查目标大小。memcpy_s增加了目标缓冲区大小 参数。如果count(要复制的字节数)大于destsz(目标大小),它会触发错误处理。- 注意 :和
memcpy一样,memcpy_s不能保证正确处理内存重叠的情况。- 原型 :
errno_t memcpy_s(void *dest, rsize_t destsz, const void *src, rsize_t count);🛡️
memmove_s(内存移动 - 可重叠)
- 作用 :将一块内存数据复制到另一块内存,支持重叠。
- 对比
memmove:
memmove能够处理源地址和目标地址重叠的情况(例如在数组内部移动数据),但不检查缓冲区边界。memmove_s既支持重叠 ,又增加了目标缓冲区大小检查。- 原型 :
errno_t memmove_s(void *dest, rsize_t destsz, const void *src, rsize_t count);🛡️
sprintf_s(格式化字符串)
- 作用:将格式化的数据输出到字符串缓冲区。
- 对比
sprintf:
sprintf不检查缓冲区边界,极易溢出。sprintf_s强制要求传入缓冲区大小。如果格式化后的字符串超过缓冲区大小,它会报错并将缓冲区置空(或仅包含 null),防止溢出。- 原型 :
int sprintf_s(char *buffer, size_t sizeOfBuffer, const char *format, ...);
strcpy_s() ,memmove_s() ,memcpy_s() ,sprintf_s() 怎么用的
这些带 _s 后缀的函数是 C 语言中的安全函数 ,主要目的是为了解决传统函数(如 strcpy, memcpy 等)容易导致的缓冲区溢出问题。它们通过增加一个参数来指定目标缓冲区的大小,从而在运行时进行边界检查。
1. strcpy_s (String Copy Safe)
用途: 替代
strcpy,用于复制字符串。
头文件:<string.h>
cpperrno_t strcpy_s(char *dest, rsize_t dest_size, const char *src);
dest: 目标缓冲区指针。dest_size: 目标缓冲区的大小 (通常使用sizeof(dest))。src: 源字符串。
cpp#include <stdio.h> #include <string.h> int main() { char dest[10]; char *src = "Hello"; // 正确用法:传入目标缓冲区大小 // 如果 src 长度 + 1 (结束符\0) > 10,程序会报错或终止 errno_t err = strcpy_s(dest, sizeof(dest), src); if (err == 0) { printf("复制成功: %s\n", dest); } else { printf("复制失败,错误代码: %d\n", err); } return 0; }2. sprintf_s (String Print Safe)
用途: 替代
sprintf,用于格式化输出到字符串。
头文件:<stdio.h>
cppint sprintf_s(char *buffer, size_t sizeOfBuffer, const char *format, ...);
buffer: 目标缓冲区。sizeOfBuffer: 目标缓冲区的大小。format: 格式化字符串(如"%d", "%s")。...: 可变参数。
cpp#include <stdio.h> int main() { char buffer[20]; int age = 25; // 传入 buffer 大小 20 // 如果格式化后的字符串超过 20 字节,会触发断言或返回错误 int result = sprintf_s(buffer, sizeof(buffer), "Age is %d", age); if (result != -1) { printf("%s\n", buffer); } return 0; }
3. memcpy_s (Memory Copy Safe)
用途: 替代
memcpy,用于内存块复制。
头文件:<string.h>或<memory.h>
cpperrno_t memcpy_s(void *dest, rsize_t dest_size, const void *src, rsize_t count);参数说明:
dest: 目标内存地址。dest_size: 目标缓冲区的总大小(用于检查边界)。src: 源内存地址。count: 想要复制的字节数。注意: 这里有两个大小参数。
dest_size是为了防止溢出,count是实际要拷多少数据。如果count > dest_size,函数会报错。
cpp#include <stdio.h> #include <string.h> int main() { char src[] = "Hello World"; char dest[20]; // 复制 12 个字节(包括 \0)到 dest // 检查 dest 是否有至少 12 字节的空间 errno_t err = memcpy_s(dest, sizeof(dest), src, 12); if (err == 0) { printf("内存复制成功: %s\n", dest); } return 0; }4. memmove_s (Memory Move Safe)
用途: 替代
memmove,用于处理内存重叠 的安全复制。
头文件:<string.h>
cpperrno_t memmove_s(void *dest, rsize_t dest_size, const void *src, rsize_t count);参数说明: 与
memcpy_s完全一致。区别点:
memcpy_s:如果内存重叠,行为是未定义的(通常报错)。memmove_s:专门设计用于处理源地址和目标地址重叠的情况(例如在一个数组内部移动数据)。
cpp#include <stdio.h> #include <string.h> int main() { char str[] = "abcdefghij"; // 将 "abc" 移动到 "def" 的位置 // 内存区域重叠:源是 str[0],目标是 str[3] // 使用 memmove_s 保证数据不被覆盖破坏 memmove_s(str + 3, 10 - 3, str, 3); // 结果应该是 "abcabcdefgh" -> "abcabcghij" (取决于具体移动逻辑) // 上例是将前3个字符拷贝到后面 printf("%s\n", str); return 0; }
#define P(x,y) x##y
- 这是一个带参数的宏。
##运算符 :它的作用是将两个参数"拼接"在一起,形成一个新的标识符。例如,P(a, b)会变成ab。
v是std::vector<int>类型变量
v.size()返回的是std::size_t(无符号整数)
uintptr_t这是什么类型
uintptr_t是 C99 和 C++11 标准引入的一种无符号整数类型 ,定义在<stdint.h>(C) 或<cstdint>(C++) 头文件中。它的核心特性是:保证足够大,可以容纳任何指针的值。
uintptr_t的主要目的是提供一个可移植且安全的方式,将指针转换为整数,以便进行一些指针本身无法直接执行的运算。
安全的指针与整数转换
你可以将一个
void*或任何其他类型的指针转换为uintptr_t,并且之后再将其转换回指针,而不会丢失信息。这在 64 位系统上尤其重要,因为普通的int类型通常只有 32 位,无法完整存储一个 64 位的指针地址,强行转换会导致数据丢失。
cpp#include <cstdint> #include <iostream> int main() { int value = 42; int* ptr = &value; // 1. 指针 -> 整数 uintptr_t int_value = (uintptr_t)ptr; std::cout << "Pointer as integer: " << int_value << std::endl; // 2. 整数 -> 指针 (完全复原) int* restored_ptr = (int*)int_value; std::cout << "Restored value: " << *restored_ptr << std::endl; // 输出 42 return 0; }进行指针的算术运算
指针不能直接进行取模(
%)等算术运算。通过转换为uintptr_t,就可以对地址值进行计算。一个典型的应用是内存对齐检查。
cpp// 检查指针 ptr 是否按 16 字节对齐 bool is_aligned = ((uintptr_t)ptr % 16 == 0);用于哈希计算或调试输出
将指针地址作为整数用于哈希表,或者以特定格式打印地址时,使用
uintptr_t是一种标准做法。与相关类型的区别
类型 描述 符号性 uintptr_t用于存储指针转换后的整数值。 无符号 intptr_tuintptr_t的有符号版本。有符号 size_t用于表示对象的大小或数组索引(如 sizeof的返回类型)。无符号 关键区别:
uintptr_t和intptr_t的设计初衷是存储指针的值 ,而size_t的设计初衷是表示大小。虽然在 32 位或 64 位系统上,它们的大小可能相同,但语义和用途是不同的。
为什么左移也要用无符号?
右移操作确实应该使用无符号类型,以避免"算术右移"还是"逻辑右移"的歧义。
对于左移操作 (
<<) ,结论同样是:强烈建议只使用无符号类型。对于有符号整数(
signed int),左移操作有两个主要的"坑",容易导致程序崩溃或逻辑错误:
陷阱一:符号位溢出(改变正负性)
如果你左移一个正数,导致最高位(符号位)变成了 1,这个数就会变成负数。虽然这在数学上看似合理,但在 C++ 标准中,如果结果无法在对应的无符号类型中表示(即溢出了),对于有符号数来说,这属于未定义行为。
- 例子 :
int a = 0x40000000; a << 1;可能会导致未定义行为,因为结果超出了int的正数范围。陷阱二:数值溢出
即使不考虑符号位,只要左移的结果超过了该类型能表示的最大值,对于有符号类型就是未定义行为。编译器可能会利用这一点进行激进的优化,导致你的代码在 Release 模式下运行出错,而在 Debug 模式下却看起来正常。
相比之下,无符号类型 (
unsigned int) 的左移是非常安全的:
- 低位永远补 0。
- 如果高位溢出(移出去了),直接丢弃(相当于对 2N2N 取模),这是定义明确的行为,绝不会导致程序崩溃。
特性 无符号类型 ( unsigned int)有符号类型 ( int)左移 ( <<)✅ 安全/推荐 低位补 0,高位溢出丢弃(取模)。 ⚠️ 危险/不推荐 低位补 0,但溢出或改变符号位属于未定义行为 (UB)。 右移 ( >>)✅ 安全/推荐 逻辑右移:高位补 0。 ⚠️ 依赖实现 通常是算术右移(高位补符号位),但标准未强制规定,移植性差。
字面量后缀的使用规范。
在 C++ 编程中,字面量后缀(Literal Suffixes) 不仅仅是为了节省打字,更是为了明确数据的类型、精度和语义。正确使用后缀可以避免隐式类型转换带来的精度丢失、编译器警告以及未定义行为。
以下是 C++ 字面量后缀的使用规范和最佳实践总结:
1. 核心原则:显式优于隐式
- 原则 :不要依赖编译器的默认推导(如
int或double),应显式使用后缀来匹配目标变量的类型。- 目的:提高代码可读性,消除歧义,防止截断或溢出。
2. 整型后缀规范
整型后缀用于指定整数的符号性(有符号/无符号)和宽度(
long,long long)。常用后缀表
后缀 含义 推荐场景 示例 u/Uunsigned位运算、非负计数、防止负数溢出 100ul/Llong32位系统上的大整数(较少用,易混淆) 100Lll/LLlong long64位大整数,防止溢出 10000000000LLz/Zstd::size_tC++23 新特性,用于数组索引和大小 10z最佳实践与避坑指南
优先使用大写
U和LL
- 原因 :小写的
l极易与数字1混淆(如100l看起来像1001)。- 规范 :始终写作
100U或100LL。位运算必须加
u
- 原因 :C++ 中整数字面量默认是
signed。对负数进行右移是"实现定义"的(可能补1),且左移溢出是未定义行为。- 示例 :
- ❌
int mask = 1 << 31;(有符号溢出,UB)- ✅
unsigned mask = 1u << 31;(安全)大整数必须加
LL
- 原因 :如果一个十进制数超过了
int的范围但未加后缀,编译器可能会报错或将其推导为long(在 Windows 上long仍是 32 位)。- 示例 :
long long val = 9000000000LL;C++23 的
z后缀
- 用于解决
size_t与int比较时的符号警告。- 示例 :
for (auto i = 0z; i < vec.size(); ++i);
C++中的格式化占位符
在 C++ 中,格式化占位符主要出现在两类函数中:
- C 风格的
printf系列函数 (如printf,sprintf,snprintf等)- C++20 引入的
std::format库C 风格
printf系列函数的占位符
printf的格式化字符串由普通字符和转换说明符(Conversion Specifier) 组成。转换说明符以%开头,用于指定后续参数的输出格式。一个完整的转换说明符结构如下:
%[flags][width][.precision][length]specifier其中,只有
specifier是必需的。**核心格式说明符 (Specifier)**这是占位符的灵魂,决定了数据的解释方式。
占位符 描述 对应参数类型 d或i有符号十进制整数 intu无符号十进制整数 unsigned intx/X无符号十六进制整数 (小写/大写) unsigned into无符号八进制整数 unsigned intf浮点数 (小数形式) doublee/E浮点数 (科学计数法) doubleg/G根据数值大小自动选择 %f或%edoublec单个字符 int(会被转换为char)s字符串 (以 \0结尾的字符数组)const char*p指针地址 (十六进制) void*n特殊! 不输出,而是将已输出的字符数写入对应的 int*参数中int*其他修饰符
- Flags (标志) :
-: 左对齐 (默认是右对齐)。+: 在数字前强制显示+或-号。- (空格): 如果数字为正,前面留一个空格。
#: 为x/X添加0x前缀,为o添加0前缀。0: 用前导零填充宽度,而不是空格。- Width (宽度) : 指定输出的最小字符数。如果实际输出短于此宽度,则用空格或
0填充。- Precision (.精度) :
- 对于浮点数 (
f,e): 指定小数点后的位数。- 对于字符串 (
s): 指定最多打印多少个字符。- 对于整数 (
d,x): 指定最少显示多少位数字,不足则前补零。- Length (长度) : 指定参数的实际大小,如
h(short),l(long),ll(long long),z(size_t) 等。
cppint num = 42; double pi = 3.14159; char* str = "Hello"; void* ptr = # // 基本用法 printf("%d\n", num); // 输出: 42 // 宽度和对齐 printf("%10d\n", num); // 输出: " 42" (右对齐,总宽10) printf("%-10d\n", num); // 输出: "42 " (左对齐,总宽10) printf("%010d\n", num); // 输出: "0000000042" (前导零填充) // 浮点数精度 printf("%.2f\n", pi); // 输出: 3.14 // 字符串精度 printf("%.3s\n", str); // 输出: Hel // 十六进制和指针 printf("%x\n", num); // 输出: 2a printf("%p\n", ptr); // 输出: 0x7ffd... (具体地址) // %n 的特殊用法 int count; printf("12345%n", &count); // 输出: 12345,并将5写入count变量 printf("\n已输出字符数: %d\n", count); // 输出: 5C++20
std::format的占位符C++20 引入了类型安全的
std::format库,它使用{}作为占位符,语法更现代、更安全,且功能强大。占位符是
{},可以通过位置索引或参数名来引用。
cpp#include <format> #include <iostream> int main() { std::string name = "Alice"; int age = 30; // 按位置引用 std::cout << std::format("Hello, {}! You are {} years old.\n", name, age); // 按索引引用 std::cout << std::format("{1}, {0}!\n", "World", "Hello"); // 输出: Hello, World! // 格式化选项 double pi = 3.1415926; std::cout << std::format("Pi is approximately {:.2f}\n", pi); // 输出: Pi is approximately 3.14 int num = 255; std::cout << std::format("Hex: {:#x}, Binary: {:#b}\n", num, num); // 输出: Hex: 0xff, Binary: 0b11111111 return 0; }主要优势
- 类型安全 : 编译器会在编译时检查类型是否匹配,避免了
printf中因类型不匹配导致的未定义行为。- 可扩展: 可以为自定义类型提供格式化支持。
- 语法清晰 :
{}占位符比%系列更易读,尤其是在复杂格式化字符串中。
++sprintf_s()用法++
sprintf_s()是 C/C++ 中用于格式化字符串的安全函数 ,主要用于替代传统的sprintf()。它的核心优势在于增加了缓冲区大小检查,能有效防止缓冲区溢出这一常见的安全漏洞。
cppint sprintf_s(char *buffer, size_t sizeOfBuffer, const char *format, ...);
参数 说明 buffer目标字符数组(缓冲区)的指针。 sizeOfBuffer目标缓冲区的总大小(字节数)。这是安全的关键,通常使用 sizeof(buffer)。format格式化字符串(如 "%d", "%s"),与printf相同。...可变参数,与格式化字符串中的占位符对应。
cpp#include <stdio.h> int main() { char buffer[20]; // 分配 20 字节空间 int age = 25; // 传入 sizeof(buffer) 让函数知道边界 int result = sprintf_s(buffer, sizeof(buffer), "Age is %d", age); if (result != -1) { printf("成功: %s\n", buffer); } else { printf("格式化失败\n"); } return 0; }