嵌入式现代C++教程:C++98------从C向C++的演化(3)
完整的仓库地址在Tutorial_AwesomeModernCPP中,您也可以光顾一下,喜欢的话给一个Star激励一下作者
10. 类型转换运算符
C++提供了四种专用的类型转换运算符,比C风格的强制转换更安全、更明确。
10.1 static_cast
static_cast用于编译时已知的类型转换:
cpp
// 基本类型转换
int i = 10;
float f = static_cast<float>(i); // int到float
// 指针类型转换(需要有继承关系或void*)
void* void_ptr = &i;
int* int_ptr = static_cast<int*>(void_ptr);
// 向上转换(派生类到基类,总是安全的)
class Base {};
class Derived : public Base {};
Derived d;
Base* base_ptr = static_cast<Base*>(&d); // 向上转换
// 向下转换(基类到派生类,程序员需确保安全)
Base b;
// Derived* derived_ptr = static_cast<Derived*>(&b); // 危险!可能不安全
在嵌入式开发中的典型用法:
cpp
// 寄存器地址转换
uint32_t address = 0x40020000;
volatile uint32_t* reg = static_cast<volatile uint32_t*>(
static_cast<void*>(address)
);
// 或使用reinterpret_cast(见下文)
10.2 dynamic_cast
dynamic_cast用于运行时类型检查,主要用于多态类型(包含虚函数的类):
cpp
class Base {
public:
virtual ~Base() {} // 必须有虚函数
};
class Derived : public Base {
public:
void derived_specific_method() {}
};
// 向下转换,带运行时检查
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
if (derived_ptr != nullptr) {
// 转换成功
derived_ptr->derived_specific_method();
} else {
// 转换失败,base_ptr不指向Derived对象
}
// 引用的dynamic_cast失败时抛出异常
Base& base_ref = *base_ptr;
try {
Derived& derived_ref = dynamic_cast<Derived&>(base_ref);
derived_ref.derived_specific_method();
} catch (std::bad_cast& e) {
// 转换失败
}
注意 :在嵌入式系统中,dynamic_cast需要RTTI(运行时类型信息)支持,会增加代码大小和运行时开销。许多嵌入式编译器默认禁用RTTI以节省资源。
10.3 reinterpret_cast
reinterpret_cast执行低级别的重新解释转换,常用于指针类型之间的转换:
cpp
// 整数到指针的转换(嵌入式中常见)
uint32_t address = 0x40020000;
volatile uint32_t* reg = reinterpret_cast<volatile uint32_t*>(address);
// 不同指针类型之间的转换
struct GPIO_TypeDef {
volatile uint32_t MODER;
volatile uint32_t OTYPER;
// ...
};
uint32_t gpio_base = 0x40020000;
GPIO_TypeDef* gpio = reinterpret_cast<GPIO_TypeDef*>(gpio_base);
// 函数指针转换(例如中断向量表)
typedef void (*ISR_Handler)(void);
void timer_isr() {
// 中断处理代码
}
uint32_t isr_address = reinterpret_cast<uint32_t>(timer_isr);
在嵌入式系统中,reinterpret_cast是访问硬件寄存器的标准方法:
cpp
// 定义外设基地址
#define PERIPH_BASE 0x40000000UL
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000UL)
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000UL)
// 定义寄存器结构
typedef struct {
volatile uint32_t MODER; // 模式寄存器
volatile uint32_t OTYPER; // 输出类型寄存器
volatile uint32_t OSPEEDR; // 输出速度寄存器
volatile uint32_t PUPDR; // 上拉/下拉寄存器
volatile uint32_t IDR; // 输入数据寄存器
volatile uint32_t ODR; // 输出数据寄存器
volatile uint32_t BSRR; // 位设置/复位寄存器
} GPIO_TypeDef;
// 创建指向硬件的指针
#define GPIOA (reinterpret_cast<GPIO_TypeDef*>(GPIOA_BASE))
// 使用
GPIOA->MODER |= 0x01; // 配置引脚模式
10.4 const_cast
const_cast用于添加或移除const属性:
cpp
// 移除const属性(应谨慎使用)
const int const_value = 100;
int* modifiable = const_cast<int*>(&const_value);
*modifiable = 200; // 危险!可能导致未定义行为
// 合法的用法:调用不接受const参数的遗留C API
void legacy_c_function(char* str); // 老的C函数,不接受const
const char* message = "Hello";
// legacy_c_function(message); // 错误:不能将const char*转换为char*
// 如果确定函数不会修改字符串,可以使用const_cast
legacy_c_function(const_cast<char*>(message));
在嵌入式开发中,const_cast偶尔用于与硬件或遗留代码接口:
cpp
// 某些硬件驱动库可能没有正确使用const
void hal_uart_send(uint8_t* data, size_t length); // 应该用const,但没有
class UARTWrapper {
public:
void send(const uint8_t* data, size_t length) {
// 我们知道hal_uart_send实际上不会修改数据
// 但它的签名错误
hal_uart_send(const_cast<uint8_t*>(data), length);
}
};
警告 :移除真正const对象的const属性并修改它会导致未定义行为。const_cast应该只用于移除意外添加的const属性。
11. 异常处理 (Exception Handling)
异常处理提供了一种结构化的错误处理机制,可以将错误处理代码与正常逻辑分离。至少看起来,代码能干净一些,后面会讲为什么很多情况下,我们会禁止使用异常处理。
11.1 基本异常处理
基本的异常处理范式是try catch throw办法------尝试的执行代码,遇到错误抛出异常,然后捕获住这个异常。
cpp
#include <exception>
#include <stdexcept>
// 抛出异常
void risky_function(int value) {
if (value < 0) {
throw std::invalid_argument("Value must be non-negative");
}
if (value > 100) {
throw std::out_of_range("Value exceeds maximum");
}
// 正常处理
}
// 捕获异常
void caller() {
try {
risky_function(-5);
} catch (const std::invalid_argument& e) {
// 处理invalid_argument异常
printf("Invalid argument: %s\n", e.what());
} catch (const std::out_of_range& e) {
// 处理out_of_range异常
printf("Out of range: %s\n", e.what());
} catch (const std::exception& e) {
// 捕获所有标准异常
printf("Exception: %s\n", e.what());
} catch (...) {
// 捕获所有其他异常
printf("Unknown exception\n");
}
}
11.2 自定义异常类
cpp
// 自定义异常类
class HardwareException : public std::exception {
private:
const char* message;
int error_code;
public:
HardwareException(const char* msg, int code)
: message(msg), error_code(code) {}
const char* what() const throw() override {
return message;
}
int get_error_code() const {
return error_code;
}
};
class SensorException : public HardwareException {
public:
SensorException(const char* msg, int code)
: HardwareException(msg, code) {}
};
// 使用自定义异常
void read_sensor() {
if (!sensor_initialized) {
throw SensorException("Sensor not initialized", 0x01);
}
if (sensor_timeout) {
throw SensorException("Sensor read timeout", 0x02);
}
// 读取传感器
}
// 捕获
try {
read_sensor();
} catch (const SensorException& e) {
printf("Sensor error: %s (code: 0x%02X)\n",
e.what(), e.get_error_code());
}
11.3 异常安全性
编写异常安全的代码需要考虑资源管理:
cpp
// 不安全的代码
void unsafe_function() {
int* data = new int[100];
risky_operation(); // 如果抛出异常,data不会被释放
delete[] data;
}
// 安全的代码(使用try-catch)
void safe_function_v1() {
int* data = new int[100];
try {
risky_operation();
delete[] data;
} catch (...) {
delete[] data;
throw; // 重新抛出异常
}
}
// 更好的做法:使用RAII(Resource Acquisition Is Initialization)
class AutoArray {
private:
int* data;
public:
AutoArray(size_t size) : data(new int[size]) {}
~AutoArray() { delete[] data; }
int& operator[](size_t index) { return data[index]; }
};
void safe_function_v2() {
AutoArray data(100);
risky_operation(); // 即使抛出异常,data也会自动释放
}
11.4 异常规格说明
C++98允许指定函数可能抛出的异常类型(C++11中已废弃):
cpp
// 声明函数不会抛出异常
void no_throw_function() throw() {
// 不应该抛出异常
}
// 声明函数可能抛出特定异常
void specific_throw(int value) throw(std::invalid_argument, std::out_of_range) {
if (value < 0) throw std::invalid_argument("negative");
if (value > 100) throw std::out_of_range("too large");
}
11.5 嵌入式系统中的异常处理
在嵌入式系统中使用异常需要谨慎考虑,第一点,在本来资源环境就很紧张的情况下,异常处理会增加显著的代码大小(异常表、展开代码等),致命的是------发生错误了,咱们处理异常的时间开销完全无法预测,在实时性看得极重的嵌入式实时系统里,这么玩就是玩火自焚。所以,许多嵌入式项目选择禁用异常(使用-fno-exceptions编译选项),而使用返回值或错误码进行错误处理。
现代C++中的optional和expected就是成熟得多的方案,笔者就在用这个。
cpp
// 推荐的嵌入式错误处理方式
enum ErrorCode {
ERROR_OK = 0,
ERROR_INVALID_PARAM,
ERROR_TIMEOUT,
ERROR_HARDWARE_FAULT
};
ErrorCode initialize_hardware() {
if (!check_hardware()) {
return ERROR_HARDWARE_FAULT;
}
if (!configure_registers()) {
return ERROR_TIMEOUT;
}
return ERROR_OK;
}
// 使用
ErrorCode result = initialize_hardware();
if (result != ERROR_OK) {
// 处理错误
}
12. C++98的其他重要特性
12.1 explicit关键字
explicit关键字防止隐式类型转换:
cpp
class Distance {
private:
int meters;
public:
// 没有explicit:允许隐式转换
Distance(int m) : meters(m) {}
int get_meters() const { return meters; }
};
void process_distance(Distance d) {
// 处理距离
}
// 隐式转换
process_distance(100); // OK:100被隐式转换为Distance(100)
// 使用explicit防止隐式转换
class SafeDistance {
private:
int meters;
public:
explicit SafeDistance(int m) : meters(m) {}
int get_meters() const { return meters; }
};
void process_safe_distance(SafeDistance d) {
// 处理距离
}
// process_safe_distance(100); // 错误:不能隐式转换
process_safe_distance(SafeDistance(100)); // OK:显式构造
在嵌入式开发中,使用explicit可以避免意外的类型转换错误:
cpp
class PWMChannel {
private:
int channel;
public:
explicit PWMChannel(int ch) : channel(ch) {
if (ch < 0 || ch >= MAX_CHANNELS) {
// 错误处理
}
}
void set_duty_cycle(int duty) {
// 设置占空比
}
};
// 使用
// pwm.set_duty_cycle(PWMChannel(50)); // 错误!防止意外传递通道号而非占空比
12.2 mutable关键字
mutable允许在const成员函数中修改成员变量:
cpp
class Cache {
private:
mutable int access_count; // 可以在const函数中修改
int data;
public:
Cache() : access_count(0), data(0) {}
int get_data() const {
access_count++; // OK:access_count是mutable
return data;
}
int get_access_count() const {
return access_count;
}
};
const Cache cache;
int value = cache.get_data(); // 增加access_count
在嵌入式系统中,mutable可用于实现缓存、统计信息等:
cpp
class Sensor {
private:
mutable bool cached_valid;
mutable float cached_value;
int pin;
public:
explicit Sensor(int p) : cached_valid(false), pin(p) {}
float read() const {
if (!cached_valid) {
cached_value = read_from_hardware();
cached_valid = true;
}
return cached_value;
}
private:
float read_from_hardware() const {
// 实际读取硬件
return 25.0f;
}
};
12.3 内联函数
内联函数在C++中得到了增强:
cpp
// 在头文件中定义内联函数
inline int max(int a, int b) {
return (a > b) ? a : b;
}
class Math {
public:
// 类内定义的成员函数隐式为inline
int add(int a, int b) {
return a + b;
}
// 显式inline
inline int multiply(int a, int b);
};
// 在类外定义时也需要inline
inline int Math::multiply(int a, int b) {
return a * b;
}
在嵌入式开发中,内联函数可以提高性能而不牺牲类型安全:
cpp
// 相比宏,内联函数更安全
inline void set_bit(volatile uint32_t& reg, int bit) {
reg |= (1UL << bit);
}
inline void clear_bit(volatile uint32_t& reg, int bit) {
reg &= ~(1UL << bit);
}
inline bool read_bit(volatile uint32_t& reg, int bit) {
return (reg >> bit) & 1UL;
}
12.4 类型别名
除了C的typedef,C++还可以使用更灵活的语法:
cpp
// 传统typedef
typedef unsigned int uint32;
typedef void (*ISR_Handler)(void);
// C++的typedef用于类型
typedef std::vector<int> IntVector;
// 为类模板创建别名,不过涉及到模板了,这里只是提一嘴
typedef std::map<std::string, int> StringIntMap;
12.5 作用域解析运算符
作用域解析运算符::用于访问全局作用域或命名空间:
cpp
int value = 100; // 全局变量
void function() {
int value = 50; // 局部变量
printf("Local: %d\n", value); // 50
printf("Global: %d\n", ::value); // 100:访问全局变量
}
// 访问命名空间成员
namespace math {
const double PI = 3.14159;
}
double circumference = 2.0 * math::PI * radius;