c++知识点5

异常

std::exception的派生类作为异常对象

在 C++ 中,将 std::exception 的派生类作为异常对象,是构建健壮、可维护异常处理体系的核心实践。这种做法不仅能清晰地传达错误意图,还能利用标准库提供的统一接口。

在实际开发中,标准异常可能无法满足所有业务需求。此时,最佳实践是继承一个合适的标准异常派生类来创建自己的异常类。通常,我们会选择继承 std::runtime_errorstd::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 主要在以下几种情况下被自动调用:

  1. 未捕获的异常 (Uncaught Exception)

    当一个异常被抛出,但在整个调用栈中都找不到匹配的 catch 块来处理它时,程序会调用 std::terminate。这是最常见的触发场景。

  2. 栈展开期间的二次异常 (Exception during Stack Unwinding)

    这是一个非常关键且危险的场景。当一个异常正在传播(即正在进行栈展开,调用沿途对象的析构函数)时,如果某个析构函数又抛出了一个新的、未被其自身捕获的异常,C++ 运行时会立即调用 std::terminate。这是因为运行时无法同时处理两个活跃的异常。

  3. noexcept 函数抛出异常

    如果一个函数被声明为 noexcept(表示承诺不抛出任何异常),但其内部实际抛出了异常,程序会立即调用 std::terminate

  4. 显式调用

    程序员也可以在代码中主动调用 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 引入的 noexceptthrow() 的现代替代品,它在编译期工作,更加高效和安全。

  1. 编译期检查(零运行时开销)

    • noexcept 是编译器的承诺。如果标记为 noexcept 的函数抛出了异常,编译器不需要生成运行时检查代码,而是直接调用 std::terminate() 终止程序。这消除了运行时开销。
  2. 助力编译器优化

    • 当编译器明确知道一个函数绝对不会抛出异常时,它可以大胆地进行优化(例如省略异常处理表、进行更激进的内联),显著提升代码性能。
  3. 移动语义的关键

    • 这是 noexcept 最重要的用途之一。标准库容器(如 std::vector)在扩容时,会检查元素的移动构造函数是否标记为 noexcept
    • 如果是 noexcept,容器会直接使用高效的移动操作;
    • 如果不是,为了保证强异常安全,容器会退而求其次使用拷贝操作,导致性能下降。

全捕获子句(形如catch (...) { ... })

在 C++ 中,catch (...) 被称为全捕获子句通配符捕获 。它的作用是捕获任何类型 的异常,无论该异常是标准库异常、自定义类对象,还是内置类型(如 intconst 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

简单来说,它的意思是:程序在使用 malloccallocnew 分配内存时,分配的大小直接由外部输入(如用户输入、网络数据包、文件内容)决定,而程序没有对这个大小进行严格的检查或限制。

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 = 0x40000001size = 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 charunsigned charchar 是三种独立的数据类型,而不仅仅是同一个类型的不同写法。

虽然它们通常都占用 1 个字节(8位) 的内存空间,但在数值范围符号性 以及跨平台行为上有着本质的区别。

特性 signed char unsigned char char
含义 明确有符号的 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:负数比较

cpp 复制代码
unsigned int a = 1;
int b = -1;
if (a > b) { 
    // 你以为是真的?其实是假的!
    // 因为 b (-1) 会被隐式转换为巨大的无符号整数
}

场景 2:循环陷阱

cpp 复制代码
std::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)**的函数。这类函数可以被信号中断,并在中断后安全地再次执行,而不会导致数据损坏或死锁。
  • 为什么 mallocstrdup 不安全?
    1. 非可重入性mallocstrdupstrdup 内部会调用 malloc)都不是异步信号安全的函数。它们在执行时会持有内部的锁。
    2. 死锁风险 :设想一个场景:主程序的线程正在执行 malloc,并持有了内部的锁。此时,一个信号到来,中断了主程序,转而执行信号处理函数。如果信号处理函数中也调用了 mallocstrdup,它会尝试去获取同一个锁。由于这个锁已经被当前线程持有(且不被支持递归获取),线程就会陷入自我等待,导致死锁,程序挂起。

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>

cpp 复制代码
errno_t strcpy_s(char *dest, rsize_t dest_size, const char *src);
  1. dest: 目标缓冲区指针。
  2. dest_size: 目标缓冲区的大小 (通常使用 sizeof(dest))。
  3. 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>

cpp 复制代码
int sprintf_s(char *buffer, size_t sizeOfBuffer, const char *format, ...);
  1. buffer: 目标缓冲区。
  2. sizeOfBuffer: 目标缓冲区的大小
  3. format: 格式化字符串(如 "%d", "%s")。
  4. ...: 可变参数。
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>

cpp 复制代码
errno_t memcpy_s(void *dest, rsize_t dest_size, const void *src, rsize_t count);

参数说明:

  1. dest: 目标内存地址。
  2. dest_size: 目标缓冲区的总大小(用于检查边界)。
  3. src: 源内存地址。
  4. 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>

cpp 复制代码
errno_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 的主要目的是提供一个可移植且安全的方式,将指针转换为整数,以便进行一些指针本身无法直接执行的运算。

  1. 安全的指针与整数转换

    你可以将一个 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;
    }
  2. 进行指针的算术运算

    指针不能直接进行取模(%)等算术运算。通过转换为 uintptr_t,就可以对地址值进行计算。一个典型的应用是内存对齐检查

    cpp 复制代码
    // 检查指针 ptr 是否按 16 字节对齐
    bool is_aligned = ((uintptr_t)ptr % 16 == 0);
  3. 用于哈希计算或调试输出

    将指针地址作为整数用于哈希表,或者以特定格式打印地址时,使用 uintptr_t 是一种标准做法。

与相关类型的区别

类型 描述 符号性
uintptr_t 用于存储指针转换后的整数值。 无符号
intptr_t uintptr_t 的有符号版本。 有符号
size_t 用于表示对象的大小或数组索引(如 sizeof 的返回类型)。 无符号

关键区别: uintptr_tintptr_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. 核心原则:显式优于隐式

  • 原则 :不要依赖编译器的默认推导(如 intdouble),应显式使用后缀来匹配目标变量的类型。
  • 目的:提高代码可读性,消除歧义,防止截断或溢出。

2. 整型后缀规范

整型后缀用于指定整数的符号性(有符号/无符号)和宽度(long, long long)。

常用后缀表

后缀 含义 推荐场景 示例
u / U unsigned 位运算、非负计数、防止负数溢出 100u
l / L long 32位系统上的大整数(较少用,易混淆) 100L
ll / LL long long 64位大整数,防止溢出 10000000000LL
z / Z std::size_t C++23 新特性,用于数组索引和大小 10z

最佳实践与避坑指南

  1. 优先使用大写 ULL

    • 原因 :小写的 l 极易与数字 1 混淆(如 100l 看起来像 1001)。
    • 规范 :始终写作 100U100LL
  2. 位运算必须加 u

    • 原因 :C++ 中整数字面量默认是 signed。对负数进行右移是"实现定义"的(可能补1),且左移溢出是未定义行为。
    • 示例
      • int mask = 1 << 31; (有符号溢出,UB)
      • unsigned mask = 1u << 31; (安全)
  3. 大整数必须加 LL

    • 原因 :如果一个十进制数超过了 int 的范围但未加后缀,编译器可能会报错或将其推导为 long(在 Windows 上 long 仍是 32 位)。
    • 示例long long val = 9000000000LL;
  4. C++23 的 z 后缀

    • 用于解决 size_tint 比较时的符号警告。
    • 示例for (auto i = 0z; i < vec.size(); ++i);

C++中的格式化占位符

在 C++ 中,格式化占位符主要出现在两类函数中:

  1. C 风格的 printf 系列函数 (如 printf, sprintf, snprintf 等)
  2. C++20 引入的 std::format

C 风格 printf 系列函数的占位符

printf 的格式化字符串由普通字符和转换说明符(Conversion Specifier) 组成。转换说明符以 % 开头,用于指定后续参数的输出格式。

一个完整的转换说明符结构如下:
%[flags][width][.precision][length]specifier

其中,只有 specifier 是必需的。

**核心格式说明符 (Specifier)**这是占位符的灵魂,决定了数据的解释方式。

占位符 描述 对应参数类型
di 有符号十进制整数 int
u 无符号十进制整数 unsigned int
x / X 无符号十六进制整数 (小写/大写) unsigned int
o 无符号八进制整数 unsigned int
f 浮点数 (小数形式) double
e / E 浮点数 (科学计数法) double
g / G 根据数值大小自动选择 %f%e double
c 单个字符 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) 等。
cpp 复制代码
int num = 42;
double pi = 3.14159;
char* str = "Hello";
void* ptr = &num;

// 基本用法
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); // 输出: 5

C++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()。它的核心优势在于增加了缓冲区大小检查,能有效防止缓冲区溢出这一常见的安全漏洞。

cpp 复制代码
int 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;
}
相关推荐
澈2071 小时前
C++内存管理:new/delete与内存泄漏实战
开发语言·c++·内存分区
星星码️1 小时前
LeetCode刷题简单篇之反转字母
c++·算法·leetcode
其实防守也摸鱼1 小时前
VS code怎么使用 Conda 安装预编译包
开发语言·网络·c++·vscode·安全·web安全·conda
默子昂1 小时前
langchain 基本使用
开发语言·python·langchain
yaoxin5211231 小时前
402. Java 文件操作基础 - 读取二进制文件
java·开发语言·python
Hello.Reader1 小时前
ds4.c 深度解析为 DeepSeek V4 Flash 打造的本地推理引擎
c语言·开发语言
naturerun2 小时前
螺旋形遍历奇数阶矩阵
c++·算法·矩阵
TopGames2 小时前
〖Unity GPU粒子插件〗ParticleSystem的终极性能优化方案 十倍百倍的显著提升 现有特效转GPU粒子 高性能特效方案
java·开发语言
Chase_______2 小时前
计算机数据存储全解:从底层进制转换到存储介质演进
java·开发语言·python