c++(9·强制转换,回调)
强制转换
类型转换:将一种数据类型转换为另一种数据类型
隐式 vs. 显示
- 隐式会在编译器在编译阶段自动进行
- 显示可以避免意外转换,从而产生符合预期的转换方式
- 显示让代码更明确清晰,提高可维护性
C风格 vs. C++风格
- C++ 显示/强制 类型转换运算符,相比于C风格强制转换,它更加规范、清晰
C风格转换
1
2
3
4
5
double pi = 3.14159;
int truncatedPi = pi;//double 转换为 int
uintptr_t address = (uintptr_t)ptr;//将指针转换为整数类型
int *newPtr = (int*)address;//将整数转换回指针
展开
static_cast 静态转换
编译时转换,不会进行 类型检查(编译运行时检查合法性),可能造成运行时崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int num = 97;
char ch = static_cast<char>(num);
int value = 42;
int* intPtr = &value;
int* ptr = nullptr;
void* voidPtr = static_cast<void*>(intPtr);
int* newIntPtr = static_cast<int*>(voidPtr);
void* voidPtr = static_cast<void*>(ptr);
class Animal {
public:
virtual void sound() const {
std::cout << "Animal makes a sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() const override {
std::cout << "Dog barks" << std::endl;
}
void fetch() const {
std::cout << "Dog fetches the ball" << std::endl;
}
};
Dog myDog;
Animal* animalPtr = static_cast<Animal*>(&myDog); // 安全的上行转换
animalPtr->sound(); // 调用的是 Dog 的 sound
Animal* animalPtr = new Animal();
Dog* dogPtr = static_cast<Dog*>(animalPtr);//不安全
dogPtr->sound(); // 调用 Animal 的 sound 方法
dogPtr->fetch(); //未定义行为
Animal* animalPtr = new Dog(); // Animal指针指向Dog对象
Dog* dogPtr = static_cast<Dog*>(animalPtr); // 下行转换
dogPtr->sound(); // 调用 Dog 的 sound 方法
dogPtr->fetch(); // 调用 Dog 的 fetch 方法
展开
- C++中内置类型之间的相互转换
- 浮点 -> 整形, 整形 -> 浮点
- 整形/浮点不同精度
- 字符 -> 整形 , 整形 -> 字符
- 有类型的指针,无类型指针,相互转换
- 对有继承关系的类的指针或引用进行强制转换
dynamic_cast 动态转换
运行时转换,会进行运行时类型检查,专门用于处理继承体系间的转换,它是更安全的
多态类之间的向下转换,要求父类中至少有一个虚函数,并且父类指针实际指向子类对象,才会转换成功,如果转换失败返回nullptr指针
1
2
3
4
5
6
7
8
9
10
11
12
13
Dog myDog;
Animal* animalPtr = dynamic_cast<Animal*>(&myDog); // 上行转换(子类到父类)
animalPtr->sound();//调用子类方法
Animal* animalPtr = new Dog();
Dog* dogPtr = dynamic_cast<Dog*>(animalPtr);//父类指针转换为子类指针
if (dogPtr) { // 如果转换成功
dogPtr->sound(); // 输出: Dog barks
dogPtr->fetch(); // 输出: Dog fetches the ball
} else {
std::cout << "Conversion failed!" << std::endl;
}
展开
const_cast 常量转换
- 移除 指向常数对象的指针或引用的常量性 常量 -> 非常量
- 添加 指向非常数对象的指针或引用的常量性 非常量 -> 常量
- 不能用于在不同类型之间进行转换
- 必须谨慎使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void modify(int* p)
{
*p = 100; // 修改指针指向的值
}
const int a = 10;
const int * p = &a;
modify(const_cast<int*>(p));//试图修改常量对象,不安全,可能导致未定义行为
int b = 20;
const int* p = &b;
modify(const_cast<int*>(p));//试图修改非常量对象,安全修改 b = 100
void print(const int* p) {
std::cout << "Value: " << *p << std::endl;
}
int c = 50;
int* p = &c;
print(const_cast<const int*>(p));//非常量转换为常量是安全的
展开
reinterpret_cast 重新解释转换
不同类型之间进行低级别(二进制)的类型转换, 它有更强大的转换功能,但最不安全
1
2
3
4
5
6
7
8
9
10
A* objA;
B* bPtr = reinterpret_cast<B*>(objA);
bPtr->display();//强行转换不相关类型,可能导致内存错误和未定义行为
int x = 42;
uintptr_t* address = reinterpret_cast<uintptr_t*>(&x);//如果修改了x的值足够大,转换为int,可能会导致非法的内存访问
int newV = reinterpret_cast<int>(address);
float f = 3.14f;
int* intPtr = reinterpret_cast<int*>(&f);// 输出 f 的位模式所代表的整数,结果可能非常难以理解
展开
- 指针类型之间的转换:将一个指针类型转换为另一个指针类型
- 指针和整数之间的转换:允许将指针转换为整数类型,或将整数转换为指针类型
- 非相关类型之间的转换:在不相关的类型之间进行转换
回调(callback)
可调用对象:
可以直接通过调用运算符()调用,它们各自都有适用的使用场景
- 普通函数(成员/非成员, 静态/非静态, 模板/非模板)
函数指针
1 2 3 4 5 6 7 8 9 10 11
int test(int a) { return a; } int main(int argc, const char * argv[]) { int (*fp)(int a); fp = test; cout<<fp(2)<<endl; return 0; }
展开
- ReturnType (*PointerName)(ParameterTypes…);
- PointerName = FunctionName,将函数赋值给函数指针
- PointerName() == (*PointerName)()调用
- 作用:
- 实现回调机制
std::function 函数包装器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
int fun1(int a){ return a; } template<typename T> T fun2(T a){ return a + 2; } struct add{ int operator()(int x){ return x + 9; } }; auto fun3 = [](int a) {return a * 2;}; template <typename T> struct sub{ T operator()(T a){ return a - 8; } }; template <typename T> struct foo2{ static T foo(T a){ return a * 4; } }; struct foo1{ static int foo(int a){ return a * 3; } }; struct foo3{ int foo(int a){ return a * a; } }; template <typename T> struct foo4{ T foo(T a){ return a * 6; } }; int main(int argc, char *argv[]){ std::function<int(int)> callback; callback = fun1; std::cout << callback(42) << std::endl; callback = fun2<int>; std::cout << callback(42) << std::endl; callback = add(); std::cout << callback(42) << std::endl; callback = fun3; std::cout << callback(42) << std::endl; callback = sub<int>(); std::cout << callback(42) << std::endl; callback = foo2<int>::foo; std::cout << callback(42) << std::endl; callback = foo1::foo; std::cout << callback(42) << std::endl; foo3 test_foo1; callback = std::bind(&foo3::foo, test_foo1, std::placeholders::_1); std::cout << callback(42) << std::endl; foo4<int> test_foo2; callback = std::bind(&foo4<int>::foo, test_foo2, std::placeholders::_1); std::cout << callback(42) << std::endl; return 0; }
展开
- std::function< return type(ParameterTypes…) > name;
- 作用:
- 实现回调机制
- 更灵活,它可以包装所有可调用对象,而函数指针只可以指向普通函数,但性能次于函数指针
函数对象:
1 2 3 4 5 6 7 8 9 10
class FuncObjType { public: void operator() () { cout<<"Hello C++!"<<endl; } }; FuncObjType funcObj; funcObj();
展开
- operator重载了调用运算符() 的类
- 作用:
- 可以存储状态
例子:
- 学校要求你实现以下一些需求:
- 统计学生人数
- 把所有学生的年龄+1
- 删除学生中成绩不及格的人
- 可以一个共有的遍历函数,和实现4个函数,它们都依赖于共有函数,这符合回调机制,但是发现用函数指针实现时无法保存数据,比如统计学生个数,当调用遍历时,无法增加count,状态的保存可以用函数对象实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
class Iterator{ public: Iterator(const vector<className>& v) : vec(v){} void ForAll(){ for(auto& i : vec){ Func(i); } } virtual void Func(className obj) = 0; private: vector<className> vec; }; class Count : public Iterator{ public: Count(const vector<ClassName>& v) : Iterator(v) {} void Func(className obj) override{ …… } private: int count; }; class AddAge : public Iterator{ …… }; //……其他需求
展开
- 如上所示,4个类存储状态,在调用各自的ForAll内部,调用各自的Func实现,但是这样会造成类爆炸
- 学校要求你实现以下一些需求:
- 可以存储状态
lambda/匿名表达式:
- [capture list] (parameter list) -> return type { function body }
[capture list]:
- lambda默认情况不能像普通函数一样使用外部变量,可以通过capture list / 参数形式传递进来
- []:默认不捕获任何变量;
- [ x ]:仅以值捕获x,其它变量不捕获;并非像传参那样调用时才拷贝,而是lambda被创建时拷贝,因此x之后的修改都不会影响它在lambda中的值
1 2 3 4 5 6 7 8 9
auto createLambda() { int x = 10; return [&x]() { return x; }; } int main() { auto func = createLambda(); func(); return 0; }
展开
- [&x]:仅以引用捕获x,其它变量不捕获;会一直跟踪x的变化,但如果在引用捕获的变量生命周期已结束时调用lambda表达式, 就会出现垂悬引用问题
- [=]:以值方式捕获所有外部变量;
- [&]:以引用方式捕获所有外部变量;
- [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
- [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
- [this] 捕获当前类的 this 指针,允许访问当前类的成员
- (parameter list)可以省略
- -> return_type 可以省略,编译器会自动推断
- auto lambda = ……
- 函数指针 ptr = ……(仅对于空捕获)
- 对于全局/文件/命名空间作用域/static修饰的可以直接访问,无需通过捕获列表捕获,但对于局部非静态/类非静态成员,需要捕获才能使用(注意需要在同一作用域下才能捕获到)
在编译阶段,lambda表达式会自动转换为重载了()的匿名类
- 捕获列表 -> 作为成员并列表初始化
- 参数列表 -> ()重载中的参数列表
- 函数体 -> ()重载中的函数体
作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class Iterator{ public: Iterator(const vector<className>& v) : vec(v){} template<typename T> void ForAll(T&& callback){ for(auto& i : vec){ callback(i); } } private: vector<className> vec; }; int main(){ Iterator manager(v); int count = 0; manager.forEach([&count](Student&) { ++count; }); }
展开
- 捕获局部变量, 代替函数对象的状态保存功能,简化代码,防止类爆炸
- 在调用时直接定义,作为STL的谓词,简化函数定义和调用语法,匿名防止名称冲突
std::bind 函数适配器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
double callableFunc (double x, double y) {return x/y;} auto NewCallable = std::bind (callableFunc, std::placeholders::_1,2); std::cout << NewCallable (10) << '\n'; class Base { public: void display_sum(int a1, int a2) { std::cout << a1 + a2 << '\n'; } int m_data = 30; }; int main() { Base base; auto newiFunc = std::bind(&Base::display_sum, &base, 100, std::placeholders::_1); newiFunc(20); // should out put 120. }
展开
- 接受一个可调用对象,生成一个新的可调用对象
- 绑定普通函数:std::bind (callableFunc, ParameterList… / placeholders),普通函数做实参时,会隐式转换成函数指针
- 绑定成员函数:std::bind (&className::callableFunc, &callableFunc,ParameterList… / placeholders)
- 绑定带有引用的函数:要使用ref / cref ,防止拷贝
- 结果可以用std::function保存
- 作用:
- 减少参数数量,调整参数顺序,以便适配参数数量
回调
它有别于直接调用a,而通过调用b 间接调用a
回调函数:
实现回调函数的方式:
- 函数指针,作为形参,仅可以传入普通函数,空捕获的lambda 表达式
- std::function,作为形参,可以传入任何可调用对象
- 函数对象类型作为形参类型,将函数对象传入
作用:
- 解耦,增加灵活性,扩展性
使用场景
- 一个行为调用,触发其他行为(例如事件)
回调对象:
实现回调函数的方式:
- 纯虚函数(接口),派生类(接口实现),通过基类指针调用派生类(通过接口调用接口实现)
作用:
- 解耦,增加灵活性,扩展性,复用性
使用场景
- 通用行为封装为接口,有利于扩展行为,提高复用性
- 对于不同层级/插件/模块 之间不应直接依赖(使用),应依赖于接口,遵守依赖倒置原则
本文由作者按照 CC BY 4.0 进行授权