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;
};
- class Date {
-
// 使用场景
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));
}
- void WriteBinary(const ServerInfo& info, const char* filename) {
-
-
文本模式:
-
方法:使用 << 和 >>。
-
优点:人类可读,跨平台处理换行符。
-
-
字符串流 (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;
- 优点:延迟加载,不影响启动速度;可以控制多个单例的初始化顺序。
- 缺点:实现复杂,需要处理线程安全和资源释放。