C++高阶机制与通用技能

C++高阶机制与通用技能

C++ 异常处理

错误处理方式的演变

  • C语言传统方式

    • 终止程序:如 assert

      • 优点:简单直接。

      • 缺点:过于暴力,用户体验差(直接闪退),无法进行清理工作。

    • 返回错误码:如 errno 或函数返回值。

      • 优点:程序不会崩溃,可以层层传递。

      • 缺点:需要程序员手动检查返回值;在深层函数调用中,错误码需要层层往上传递,代码冗余且易出错。

  • C++ 异常机制

    • 定义:当函数发现无法处理的错误时,抛出(throw)异常,让调用者处理。

    • 核心关键字:try(监控)、throw(抛出)、catch(捕获)。

异常的使用规则原则

  • 抛出与匹配原则

    • 类型匹配:异常通过抛出对象引发,其类型决定激活哪个 catch。

    • 最近匹配:激活调用链中与对象类型匹配且离抛出位置最近的 catch。

    • 对象拷贝:throw 的对象是一个临时对象,会生成一个拷贝(类似传值返回),该拷贝在 catch 结束后销毁。

    • 捕获任意异常:catch(...) 可以捕获所有类型异常,通常用于防止程序崩溃,但无法得知具体错误信息。

    • 继承匹配(重要原则):基类可以捕获派生类抛出的异常对象。这是实际开发中最常用的方式。

  • 栈展开(Stack Unwinding)

    • 过程:

      • 检查 throw 是否在 try 内部。

      • 如果在,查找匹配的 catch。

      • 如果不在或没找到匹配,则退出当前函数栈,到上一层函数栈继续查找。

      • 如果到达 main 栈仍未匹配,程序终止(Terminate)。

    • 执行顺序:找到匹配的 catch 并处理后,程序会沿着该 catch 子句之后的代码继续执行。

异常的进阶操作

  • 异常的重新抛出(Rethrow)

    • 场景:当单个 catch 不能完全处理异常,或者需要在传递异常前先进行一些局部清理(如释放内存、解开互斥锁)。

    • 语法:throw; (不带任何参数,表示将当前捕获到的异常原样抛出)。

    • void Func() {

      int* array = new int[10];

      try {

      // 可能抛出异常的逻辑

      DoSomething();

      } catch (...) {

      // 这里必须先释放资源,否则会内存泄漏

      cout << "释放资源: " << array << endl;

      delete[] array;

      throw; // 重新抛出,交给外层处理

      }

      delete[] array;

      }

  • 异常规范(Exception Specifications)

    • C++98:void func() throw(A, B); 表示可能抛出A或B类型异常;void func() throw(); 表示不抛出异常。

    • C++11 (推荐):使用 noexcept。

      • void func() noexcept; 明确声明函数不会抛出异常。

      • 如果标了 noexcept 却抛出了异常,程序会直接调用 std::terminate 终止。

异常安全与RAII(难点)

  • 构造函数安全:尽量不要在构造函数中抛出异常,否则可能导致对象初始化不完整。

  • 析构函数安全:绝对不要在析构函数中抛出异常。因为异常抛出过程中的栈展开会自动调用析构函数,如果析构函数再抛异常,会导致程序崩溃。

  • 资源泄漏问题:由于异常会导致执行流乱跳,极易导致 new 后的 delete 没被执行。

    • 解决方案:使用 RAII (Resource Acquisition Is Initialization) 思想(如智能指针)来管理资源。

自定义异常体系(生产环境实践)

  • 实际项目中,通常定义一个基类,所有业务异常继承该基类。

    • // 异常基类
      class Exception {
      public:
      Exception(const string& errmsg, int id) : _errmsg(errmsg), _id(id) {}
      virtual string what() const { return _errmsg; } // 多态支持
      protected:
      string _errmsg;
      int _id;
      };

// 派生类:数据库异常

class SqlException : public Exception {

public:

SqlException(const string& msg, int id, const string& sql)

: Exception(msg, id), _sql(sql) {}

virtual string what() const {

return "SqlException: " + _errmsg + " -> SQL: " + _sql;

}

private:

string _sql;

};

// 使用场景

try {

throw SqlException("权限不足", 100, "select * from user");

} catch (const Exception& e) { // 捕获基类引用,利用多态

cout << e.what() << endl;

}

标准库异常体系 (std::exception)

  • C++提供了一系列标准异常,组织结构如下:

    • std::exception:所有标准异常的父类。

      • logic_error:逻辑错误(如 invalid_argument, out_of_range)。

      • runtime_error:运行时错误(如 overflow_error, range_error)。

      • bad_alloc:new 失败时抛出。

      • bad_cast:dynamic_cast 失败时抛出。

C++ 类型转换

C语言中的类型转换

  • 转换形式

    • 隐式类型转换:编译器在编译阶段自动进行。能转就转,不能转就报错。

      • 场景:赋值运算符左右类型不同、形参与实参不匹配、返回值类型与接收类型不一致。
    • 显式类型转换:由用户强制指定转换类型。

      • 语法:(Type)variable
  • void TestCStyle() {

    int i = 1;

    // 1. 隐式类型转换(意义相近的类型:如整型与浮点型)

    double d = i;

    printf("%d, %.2f\n", i, d);

    int* p = &i;

    // 2. 显式强制类型转换(意义不相近的类型:如指针与整型)

    int address = (int)p;

    printf("%p, %d\n", p, address);

    }

  • C风格转换的缺陷

    • 隐式转换的隐患:数据精度容易丢失(如双精度转整型),且由于是自动发生的,错误难以发现。

    • 显式转换的混杂:将所有转换情况(如无关指针转换、去掉const等)混合在一起,代码意图不清晰。

    • 可视性差:转换代码在程序中不够显眼,难以进行全局搜索和审计。

C++ 强制类型转换的四种操作符

  • static_cast (静态转换)

    • 用途:用于非多态类型的转换(静态转换)。

    • 适用场景:

      • 相关类型之间的转换(如 int 与 double)。

      • 编译器隐式执行的任何类型转换。

    • 限制:不能用于两个不相关的类型进行转换(如指针转整型)。

    • double d = 12.34;

      int a = static_cast(d); // OK:相关类型转换

      // int* p = static_cast<int*>(a); // Error:不相关类型

  • reinterpret_cast (重新解释转换)

    • 用途:对操作数的位模式进行低层次的重新解释。

    • 适用场景:

      • 将一种类型转换为另一种截然不同的类型。

      • 例如:将指针转为整数,或将一种指针转为另一种无关指针。

    • 优缺点:极其灵活但也极其危险,完全依赖程序员保证转换的逻辑正确。

    • int a = 10;

      int* p = reinterpret_cast<int*>(a); // OK:将整数解释为地址

  • const_cast (常量转换)

    • 用途:删除或增加变量的 const 或 volatile 属性。

    • 适用场景:方便对原被声明为 const 的内存进行赋值(前提是该内存本身不是只读的)。

    • 注意:如果原始对象本身是在只读存储区的常量,通过 const_cast 修改它会导致未定义行为。

    • void TestConstCast() {

      const int a = 2;

      int* p = const_cast<int*>(&a);
      p = 3;
      // 注意:打印a时,由于编译器优化可能仍显示2(从寄存器读取),
      // 但通过
      p访问内存,值可能已经改成了3。

      cout << a << " " << *p << endl;

      }

  • dynamic_cast (动态转换)

    • 用途:将父类对象的指针或引用安全地转换为子类对象的指针或引用。

    • 特性:

      • 向上转型:子类 -> 父类(不需要强制转换,符合赋值兼容规则)。

      • 向下转型:父类 -> 子类(由 dynamic_cast 负责,运行时检查)。

    • 必要条件:父类必须含有虚函数(因为它是靠RTTI来识别类型的)。

    • 安全性:

      • 指针转换失败:返回 nullptr。

      • 引用转换失败:抛出 std::bad_cast 异常。

    • class A { public: virtual void f(){} };

      class B : public A {};

void fun(A* pa) {

// static_cast向下转型是不安全的,无论pa指向谁都会转成功

B* pb1 = static_cast<B*>(pa);

复制代码
// dynamic_cast是安全的,如果pa指向的确实是B,则转换成功;否则返回0
B* pb2 = dynamic_cast<B*>(pa);

cout << "pb1: " << pb1 << endl;
cout << "pb2: " << pb2 << endl;

}

RTTI (运行时类型识别)

  • C++ 提供了 RTTI 机制来在程序运行阶段确定对象的真实类型:

    • typeid 运算符:获取表达式的类型信息(返回 type_info 对象)。

    • dynamic_cast 运算符:利用 RTTI 在运行时安全地转换多态指针。

    • decltype (C++11):在编译时推导表达式的类型。

C++ IO流

IO流的基本概念

  • 什么是"流"?

    • 定义:流(Stream)是对一种有序、连续、具有方向性的数据(单位可以是bit, byte, packet)的抽象描述。

    • 方向性:

      • 输入流:信息从外部设备(键盘、磁盘)向计算机内部(内存)流动。

      • 输出流:信息从计算机内部(内存)向外部设备(显示器、磁盘)流动。

    • 特性:有序连续、方向明确。

  • C语言的IO机制

    • 核心函数:scanf() 和 printf()。

    • 缓冲区概念:C语言借助缓冲区进行IO。

      • 屏蔽底层差异:屏蔽不同OS内核低级I/O的实现差异,提高可移植性。

      • 行读取实现:解析缓冲区内容,通过换行符等标识定义"行"的概念。

    • 缺点:不安全(易溢出)、不支持自定义类型、类型识别需手动指定(如%d)。

C++ 标准IO流体系

  • 类层次结构

    • 基类:ios_base -> ios。

    • 核心派生类:

      • istream (cin)

      • ostream (cout, cerr, clog)

      • iostream (标准输入输出流)

      • fstream (文件流)

      • stringstream (字符串流)

  • C++ IO的优势

    • 类型安全:自动识别内置类型(通过运算符重载)。

    • 可扩展性:支持通过重载 << 和 >> 来支持自定义类型。

深入理解 cin 的运行逻辑

  • 状态位与循环读取

    • 原理:while(cin >> a) 能够运行,是因为 operator>> 返回的是 istream& 引用,而 istream 对象重载了 operator bool(或在旧标准中是 void*)。

    • 模拟实现 operator bool:

      • class Date {
        public:
        Date(int y = 0) : _year(y) {}
        // 模拟实现:将对象转换为逻辑判断值
        explicit operator bool() const {
        // 如果年份为0,认为流结束或出错,返回false
        return _year != 0;
        }
        private:
        int _year;
        };

// 使用场景

Date d;

while(cin >> d_year) { // 假设重载了>>

// 处理逻辑

}

  • 细节注意

    • 分隔符:空格和回车默认作为数据分隔符,无法直接通过 cin >> 读入空格。

    • 类型一致性:输入类型必须与变量一致,否则流的状态位(state)会置1,导致后续读取失效。

文件IO流 (fstream)

  • 文件操作步骤

    • 定义流对象(ifstream读 / ofstream写 / fstream读写)。

    • 打开文件:建立流对象与物理文件的联系(可指定二进制或文本模式)。

    • 读写操作:使用 << / >>(文本)或 read() / write()(二进制)。

    • 关闭文件:释放资源。

  • 二进制 vs 文本模式

    • 二进制模式:

      • 方法:write((char*)&info, sizeof(info))。

      • 优点:读写速度快,数据在文件中与内存中完全一致。

      • 代码补充:

        • void WriteBinary(const ServerInfo& info, const char* filename) {
          ofstream ofs(filename, ios::out | ios::binary);
          ofs.write((const char*)&info, sizeof(info));
          }
    • 文本模式:

      • 方法:使用 << 和 >>。

      • 优点:人类可读,跨平台处理换行符。

字符串流 (stringstream)

  • 三大类

    • istringstream(从字符串输入)、ostringstream(向字符串输出)、stringstream(双向)。
  • 核心用途与对比

    • 用途:

      • 数据类型转换:将数值转为字符串,替代 itoa / sprintf。

      • 字符串拼接:底层维护一个 string 对象,避免字符数组溢出。

      • 序列化与反序列化:将结构化数据转为长字符串或反向解析。

    • 对比 C 语言 (sprintf):

      • 优点:无需担心缓冲区溢出,自动类型推导,更安全。

      • 缺点:效率略低于 sprintf。

  • 重要操作:状态重置与内容清空

    • s.clear():重置流的状态位(如 eofbit、failbit)。在多次循环转换类型时必须调用。

    • s.str(""):真正清空底层存储的字符串内容。

    • stringstream ss;

      int a = 123;

      ss << a;

      string s = ss.str(); // s = "123"

// 再次使用前必须:

ss.str(""); // 清空内容

ss.clear(); // 重置状态

如何选择 IO 方式?

  • 追求极致效率:在某些算法竞赛中,C 语言的 scanf/printf 或自定义快读快出更快。

  • 追求安全性与开发效率:优先选择 C++ IO 流,利用其类型安全和对自定义类型的支持。

  • 处理内存格式化:首选 stringstream。

  • 网络传输或持久化:

    • 简单结构:二进制读写。

    • 复杂协议:序列化为字符串或使用 JSON/XML 等工具。

C++ 特殊类设计

设计目标:不能被拷贝的类

  • 核心原理

    • 拷贝行为主要发生在两个场景:拷贝构造函数和赋值运算符重载。禁止这两个函数即可实现目标。
  • 实现方式

    • C++98 方式:

      • 做法:将拷贝构造和赋值重载声明为 private,且只声明不定义。

      • 原理:

        • 声明为私有:防止外部调用。

        • 只声明不定义:防止成员函数或友元函数内部调用。如果不慎调用,会在链接阶段报错。

    • C++11 方式(推荐):

      • 做法:在默认成员函数后加上 = delete。

      • 原理:明确告诉编译器删除该函数,任何形式的调用(包括内部)都会在编译阶段直接报错。

  • class CopyBan {

    public:

    CopyBan() {}

    private:

    // C++98: 私有且不实现

    // CopyBan(const CopyBan&);

    // CopyBan& operator=(const CopyBan&);

    // C++11: 直接删除

    CopyBan(const CopyBan&) = delete;

    CopyBan& operator=(const CopyBan&) = delete;

    };

设计目标:只能在堆上创建对象的类

  • 实现方案:构造函数私有化 + 静态工厂

    • 做法:

      • 将构造函数设为私有。

      • 提供一个静态成员函数来调用 new 并返回对象指针。

      • 关键点:必须同时禁止拷贝构造(或设为私有),否则可以通过拷贝构造在栈上生成对象。

  • class HeapOnly {

    public:

    static HeapOnly* CreateObject() {

    return new HeapOnly;

    }

    // 释放对象的接口

    void Destroy() {

    delete this;

    }

    private:

    HeapOnly() {} // 构造函数私有

    HeapOnly(const HeapOnly&) = delete; // 禁止拷贝

    };

设计目标:只能在栈上创建对象的类

  • 实现方案:禁用 new 运算符

    • 做法:重载并禁用 operator new。由于 new 在创建对象时会先调用 operator new 分配内存,禁用后即无法在堆上创建。
  • class StackOnly {

    public:

    static StackOnly CreateObj() {

    return StackOnly();

    }

    // 也可以将构造函数公开,只要禁掉new即可

    private:

    StackOnly() : _a(0) {}

    // 禁用 new 和 delete(防止在堆上申请空间)

    void* operator new(size_t size) = delete;

    void operator delete(void* p) = delete;

    private:

    int _a;

    };

设计目标:不能被继承的类

  • 实现方式

    • C++98 方式:将构造函数设为私有。

      • 原理:派生类创建对象时必须调用基类的构造函数。如果基类构造函数私有,派生类无法调用,从而导致无法继承。
    • C++11 方式(推荐):使用 final 关键字。

      • 语法:class A final { ... };

      • 原理:编译器强制检查,如果发现有类尝试继承 final 类,直接报错。

单例模式(Singleton Pattern)

  • 饿汉模式(Hungry Mode)

    • 原理:在程序启动阶段(main 函数执行前)就初始化唯一的实例。

    • class Singleton {

      public:

      static Singleton* GetInstance() {

      return &m_instance;

      }

      private:

      Singleton() {} // 构造私有

      Singleton(const Singleton&) = delete; // 禁拷贝

      static Singleton m_instance; // 静态实例

      };

      // 在类外初始化静态成员

      Singleton Singleton::m_instance;

    • 优点:实现简单,无线程安全问题(静态成员初始化由编译器保证线程安全)。

    • 缺点:若单例资源大且暂不需要,会浪费资源并拖慢启动速度;多个饿汉单例的初始化顺序无法确定。

  • 懒汉模式(Lazy Mode)

    • 原理:第一次调用 GetInstance() 时才初始化对象。

    • 代码实现(含双检锁与自动释放):

      • #include

class Singleton {

public:

static Singleton* GetInstance() {

// 双检锁 (Double-Check Locking) 保证效率与安全

if (m_pInstance == nullptr) {

std::unique_lockstd::mutex lock(m_mtx);

if (m_pInstance == nullptr) {

m_pInstance = new Singleton();

}

}

return m_pInstance;

}

复制代码
// 内部类用于垃圾回收
class CGarbo {
public:
    ~CGarbo() {
        if (Singleton::m_pInstance)
            delete Singleton::m_pInstance;
    }
};

private:

Singleton() {}

Singleton(const Singleton&) = delete;

复制代码
static Singleton* m_pInstance;
static std::mutex m_mtx;
static CGarbo m_garbo; // 静态回收对象

};

// 静态成员初始化

Singleton* Singleton::m_pInstance = nullptr;

std::mutex Singleton::m_mtx;

Singleton::CGarbo Singleton::m_garbo;

复制代码
- 优点:延迟加载,不影响启动速度;可以控制多个单例的初始化顺序。

- 缺点:实现复杂,需要处理线程安全和资源释放。
相关推荐
白太岁1 小时前
Muduo:(1) 文件描述符及其事件与回调的封装 (Channel)
c++
我命由我123452 小时前
Visual Studio 文件的编码格式不一致问题:错误 C2001 常量中有换行符
c语言·开发语言·c++·ide·学习·学习方法·visual studio
MR_Promethus2 小时前
【C++类型转换】static_cast、dynamic_cast、const_cast、reinterpret_cast
开发语言·c++
Trouvaille ~2 小时前
【Linux】epoll 深度剖析:高性能 IO 多路复用的终极方案
linux·运维·服务器·c++·epoll·多路复用·io模型
mjhcsp2 小时前
C++数位 DP解析
开发语言·c++·动态规划
小龙报2 小时前
【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列
c语言·开发语言·数据结构·c++·算法·贪心算法·动态规划
仰泳的熊猫3 小时前
题目1529:蓝桥杯算法提高VIP-摆花
数据结构·c++·算法·蓝桥杯
小糯米6013 小时前
C++ 树
数据结构·c++·算法
掘根3 小时前
【C++STL】红黑树(RBTree)
数据结构·c++·算法