关于对象
封装的布局成本
- 普通函数没有增加成本
- 成员函数虽然含在类的声明之内,却不出现在对象之中
- 虚机制会增加成本
- 虚函数机制用来支持一个有效率的
运行期绑定
- 虚基类用来实现
多次出现在机场体系中的基类,有一个单一而被共享的实例
- 虚函数机制用来支持一个有效率的
类成员概述
- 类数据成员
- 静态
- 非静态
- 类成员函数
- 静态
- 非静态
- 虚函数
简单对象模型
- 这个模型中,一个对象是一系列的槽,每个槽指向一个成员
- 成员按声明顺序
- 每个数据成员或成员函数都有一个自己的槽
表格驱动模型
- 类对象包含指向两个表格的指针
- 数据成员表存放所有数据成员相关的信息
- 成员函数表存放所有成员函数的地址
- 每个表是一系列的槽
C++对象模型
- 非静态的数据成员被配置于每一个对象之内
- 静态数据成员被存放在类对象之外
- 静态和非静态的成员函数也被放到了类对象之外
- 至于虚函数:
- 每个类产生出一堆指向虚函数的指针,放在表格之中,表格叫
virtual table
- 每个类对象被安插一个指针(
vptr
),执行相关的virtual table
vptr
的设定和重置都由每个类的构造、析构和拷贝赋值运算符自动完成
- 每个类产生出一堆指向虚函数的指针,放在表格之中,表格叫
RTTI相关
- 运行时类型识别
RTTI
是C++
提供的一种机制,用于在运行时识别对象的类型RTTI
主要包括typeid
操作符和dynamic_cast
typeid
返回的是一个type_info
对象的引用,这个对象包含了类型信息
type_info
对象type_info
对象用于存储与某个类型相关的RTTI
信息,包括类型的名称等- 在
RTTI
机制中,type_info
对象的地址被用来唯一标识一个类型
- 虚函数表(
Virtual Table
)和RTTI
- 每个带有虚函数的类会有一个虚函数表(
vtable
),该表包含虚函数的地址以及RTTI
相关信息 - 对于一个带有虚函数的类,其对象会在内存中包含一个指向虚函数表的指针(
vptr
)
- 每个带有虚函数的类会有一个虚函数表(
type_info
对象的位置:gcc
实现- 具体还没研究
msvc
实现msvc
中RTTI
信息,保存在保存了虚函数表指针的前一个地址*((void***)(&dd))
就是拿到了指向了虚函数表指针的指针*((void***)(&dd)) - 1
就是从执行虚函数表指针的指针位置,向前偏移一个位置
这个位置,在msvc
的实现中,存放的是RTTI
相关的信息(void**)(*((void***)(&dd)) - 1)
进一步把偏移后的地址转成void**
拿到RTTI Complete Object Locator
信息
1 |
*((void**)(*((void***)(&dd)) - 1)) |
msvc
里面RTTI Complete Object Locator
内部Signature
一个4
字节的整数,用于标识RTTI
的版本(通常为0
)Offset
一个4
字节的整数,表示对象相对于某个基类的偏移CDOffset
一个4
字节的整数,表示对象相对于构造时的偏移(虚继承相关)pTypeDescriptor
指向type_info
的指针,这就是实际的type_info
信息pClassHierarchyDescriptor
指向类层次结构描述符的指针,包含类继承关系的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Signature *((int*)(*(void**)(*((void***)(&dd)) - 1))) // 1 // Offset *((int*)(*(void**)(*((void***)(&dd)) - 1)) + 1) // 0 // CDOffset *((int*)(*(void**)(*((void***)(&dd)) - 1)) + 2) // 0 // pTypeDescriptor *((void**)((char*)(*(void**)(*((void***)(&dd)) - 1)) + 12)) // pClassHierarchyDescriptor *((void**)((char*)(*(void**)(*((void***)(&dd)) - 1)) + 16)) |
构造函数语意学
默认构造函数
- 什么时候编译器会合成出一个默认构造呢?
- 当编译器需要它的时候
- 关于合成的默认构造,
- 是属于被隐式声明出来的
- 是没啥用的
默认构造函数通常无法满足需要特定初始化的类
- 什么时候编译器会合成出默认的构造函数?
- 一个类没有任何构造函数,但是含有带默认构造的类对象成员
如果成员没有默认构造函数,编译器会报错,因为无法生成合法的默认构造函数 - 一个类没有任何构造函数,但是它的基类有默认构造函数
- 一个类没有任何构造函数,但是它声明或继承了一个虚函数
- 一个类没有任何构造函数,但是它派生自一个继承串链,其中有一个或更多的虚基类
- 一个类没有任何构造函数,但是含有带默认构造的类对象成员
拷贝构造函数
- 以一个对象的内容作为另一个对象的初值的情况:
- 对一个对象做显式的初始化操作
- 对象被当做参数传给某个函数
- 函数返回一个对象
- 如果一个类显式定义了拷贝构造函数,那么大部分情况下,以一个对象内容作为另一个同类对象的初值,会触发拷贝构造函数的调用
1 |
X::X(const X& x); |
- 如果一个类没有提供一个显式定义的拷贝构造,当以一个对象的内容作为另一个对象的初值时:
- 如果这个类展现了位逐次拷贝语意,那么编译器会隐式声明默认的拷贝构造函数
- 但是,这个默认的拷贝构造函数执行的是浅拷贝(即位逐次拷贝)
- 那么,什么时候一个类不展示出
位逐次拷贝
呢- 情况一:当类内含一个对象成员,并且这个对象成员的类声明有一个拷贝构造函数(不管是显式声明的还是被编译器合成的)
- 情况二:当类继承自一个基类,而这个基类存在一个拷贝构造函数时(不管是显式声明的还是被编译器合成的)
- 情况三:当类声明了一个或多个虚函数
- 情况四:当类派生自一个继承串链,其中有一个或多个虚基类
- 对于情况一和情况二:
- 编译器必须将成员或基类的拷贝构造函数的调用操作安插到合成的拷贝构造函数中
- 对于情况三:
- 编译器合成基类的拷贝构造函数时,会显式指定
vptr
指向基类的虚函数表
- 编译器合成基类的拷贝构造函数时,会显式指定
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 |
class base { public: b(); virtual ~b(); virtual void test1(); virtual void test2(); private: // data }; class derive : public base { public: derive(); void test1() override; void test2() override; private: // data }; derive d; // 这里发生了切割 // 并且,base的拷贝构造函数,会把b的vptr设置为base的虚函数表 base b = d; |
- 对于情况四:
- 这是因为涉及虚基类时,拷贝构造函数需要处理虚基类的复杂初始化逻辑,而不能简单地逐位复制
1 2 3 4 5 6 7 8 |
class test : public virtual base { public: test() {} test(int value) {} private: // data } |
拷贝构造函数-位逐次拷贝(浅拷贝)
-
位逐次拷贝是对内存中的二进制数据进行逐位拷贝,不关心数据的实际类型和意义
-
对于内建类型(如
int
、char
、指针等),这通常是安全的- 因为这些类型的拷贝就是简单的内存内容复制
-
对于类对象成员
- 默认拷贝构造函数不会直接进行位逐次拷贝,而是递归调用该成员的拷贝构造函数
- 这意味着,如果类对象成员有自己的拷贝构造函数(无论是默认的还是显式定义的),那么这个拷贝构造函数会被调用
拷贝构造函数-深拷贝
- 在浅拷贝的基础上,针对指针指向的内存,深拷贝会单独分配新空间,并复制原对象指针指向的数据
- 而不是简单地复制指针地址
拷贝构造函数-对象切割
- 直接用派生类对象去初始化基类对象时,会发生对象切割,导致派生类的特性丢失
- 为了避免对象切割,最常见的解决方法是
- 使用指针或引用来传递对象,而不是按值传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } }; void callFunc(Base& obj) { // 使用引用避免切割 obj.func(); // 调用派生类的函数 } int main() { Derived d; callFunc(d); // 不会发生切割,调用 Derived::func() return 0; } |
- 智能指针
- 使用
std::shared_ptr
或std::unique_ptr
等智能指针不仅可以避免对象切割,还能有效管理对象的生命周期,防止内存泄漏
- 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> #include <memory> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } }; void callFunc(std::shared_ptr<Base> obj) { // 使用智能指针避免切割 obj->func(); // 调用派生类的函数 } int main() { auto d = std::make_shared<Derived>(); callFunc(d); // 不会发生切割,调用 Derived::func() return 0; } |
- 使用多态工厂方法或多态接口
- 有时,可以通过工厂方法或多态接口来确保在不发生对象切割的情况下获取完整的派生类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <memory> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } virtual ~Base() = default; // 确保派生类可以正确析构 }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } }; // 工厂方法 std::unique_ptr<Base> createObject() { return std::make_unique<Derived>(); // 返回派生类的对象 } int main() { auto obj = createObject(); // 获取派生类对象,避免切割 obj->func(); // 调用 Derived::func() return 0; } |
std::variant
或std::any
- 在
C++17
及以上,可以使用std::variant
或std::any
来存储派生类对象,这些类型可以在运行时保持对象的类型信息,避免对象切割 std::variant<Base, Derived>
是一个能够存储Base
或Derived
对象的联合类型std::variant
:类型安全,适用于已知的多种类型,通过std::visit
调用正确类型的函数std::any
:适用于需要存储任意类型的场景,通过std::any_cast
恢复类型信息
std::any
可以存储几乎任何类型的对象,包括智能指针
- 在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> #include <variant> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } virtual ~Base() = default; // 确保派生类正确析构 }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } }; void callFunc(std::variant<Base, Derived>& obj) { std::visit([](auto& obj) { obj.func(); }, obj); // 调用正确的虚函数 } int main() { std::variant<Base, Derived> obj = Derived(); // 存储派生类对象 callFunc(obj); // 调用 Derived::func() return 0; } |
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 |
#include <iostream> #include <any> #include <memory> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } virtual ~Base() = default; // 确保派生类正确析构 }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } }; void callFunc(std::any& obj) { try { // 尝试将 std::any 恢复为 std::shared_ptr<Derived> auto derived = std::any_cast<std::shared_ptr<Derived>>(obj); derived->func(); } catch (const std::bad_any_cast&) { std::cout << "Failed to cast to Derived" << std::endl; } } int main() { std::shared_ptr<Derived> d = std::make_shared<Derived>(); std::any obj = d; // 使用 std::any 存储 Derived 对象 callFunc(obj); // 调用 Derived::func() return 0; } |
拷贝构造-场景
- 对象被复制时
- 类中管理动态资源时(如动态内存、文件句柄、网络连接等)
- 当类内部有指针、动态内存或其他需要特殊管理的资源时,编译器合成的默认拷贝构造函数执行的是浅拷贝
- 可能导致以下问题:
两个对象共享同一块内存,修改一个对象会影响另一个
当多个对象共享同一资源时,析构时会导致重复释放内存或资源
如果不正确管理资源生命周期,可能会导致资源泄漏
- 类中包含指针或指针成员变量
- 当类包含指针成员时,默认拷贝构造函数只会复制指针的地址,而不会复制指针所指向的数据
- 这在需要独立的数据副本时,会引发严重问题
- 禁止复制的场景
- 有时我们需要禁止对象的复制行为,可以显式删除拷贝构造函数(
C++11
及以上),防止意外发生拷贝
- 有时我们需要禁止对象的复制行为,可以显式删除拷贝构造函数(
1 2 3 4 5 6 |
class NonCopyable { public: NonCopyable() = default; NonCopyable(const NonCopyable&) = delete; // 禁止拷贝 NonCopyable& operator=(const NonCopyable&) = delete; // 禁止赋值 }; |
- 类包含其他需要深度复制的对象成员
- 如果类中包含的成员是其他需要特殊管理的对象(如智能指针、其他复杂类型对象),也需要显式定义拷贝构造函数以确保对象的完整复制
- 性能优化或日志需求
- 有时需要自定义拷贝构造函数来添加日志或做额外的性能优化
- 例如,为了减少不必要的复制操作,可以在拷贝过程中实现某些特殊优化逻辑
初始化列表
- 必须使用初始化列表的情况:
- 初始化一个引用成员时
- 初始化一个常量成员时
- 调用基类的构造,而该构造有一组参数
- 调用类成员的构造,而该构造有一组参数
数据语意学
空类
- 空类有一个隐藏的
1
字节大小,因为那是被编译器安插进去的一个char
- 这使得这个类的两个对象在内存中配置独一无二的地址
1 |
class X {}; |
内存布局
C++
对象在内存中存储数据成员时,编译器会按照定义的顺序进行存储- 在继承的场景中,基类的成员会先被布局,然后是派生类的成员
访问控制
- 为了提高数据成员的访问效率,编译器在内存布局时可能会进行对齐优化
静态数据成员
- 静态数据成员属于类本身,而不是某个特定对象,因此它们不被存储在对象内部,而是保存在全局数据区
- 静态数据成员可以通过类名或者对象来访问,且所有对象共享同一个静态成员
const 数据成员
const
数据成员表示对象状态的不可变性,编译器会对const
成员施加只读限制,确保它们在对象的生命周期内不会被修改const
数据成员必须通过初始化列表来赋值,无法在构造函数体内赋值,这与普通成员的初始化方式不同
对象的大小与对齐
- 对象的大小由其成员的大小和对齐要求决定
- 编译器可能会在成员之间插入填充字节,以满足对齐要求
- 引入虚函数或虚基类会增加对象的大小,因为这些特性需要额外的指针来支持多态性和虚继承机制
- 如虚函数表指针、虚基类表指针
继承中的数据
- 单继承时,派生类对象在内存中首先存储基类部分,然后存储派生类新增部分
- 虚继承通过虚基类表(
vbptr
)来共享虚基类,避免多继承时重复构造虚基类
函数语意学
非静态成员函数
- 与对象关联
- 非静态成员函数必须通过对象调用,因为它们操作的是特定对象的数据成员
- 调用非静态成员函数时,编译器会隐式传递对象的指针(即
this
指针)给函数,使得函数能够访问调用它的那个对象的成员
- 隐含的
this
指针- 每个非静态成员函数都有一个隐式的
this
指针,指向调用该函数的对象实例 this
指针的类型为ClassName* const
,指向对象本身,因此可以在成员函数内访问对象的成员变量和其他成员函数
- 每个非静态成员函数都有一个隐式的
- 函数指针的使用
- 非静态成员函数的指针和普通函数指针不同,成员函数指针需要绑定对象才能被调用
虚拟成员函数
- 实现动态多态性
- 虚函数是通过基类指针或引用调用派生类的重写函数,实现动态多态性
virtual
关键字声明- 在基类中使用
virtual
关键字声明的成员函数即为虚函数。派生类中的相同函数可以选择是否重写(override
)该虚函数 - 一旦基类的函数被声明为虚函数,派生类中相同函数即使不显式加上
virtual
关键字,依然是虚函数
- 在基类中使用
- 虚函数表(
vtable
)和虚指针(vptr
)- 编译器为包含虚函数的类创建一个虚函数表(
vtable
),表中存储了虚函数的地址 - 每个包含虚函数的对象都有一个虚指针(
vptr
),指向这个虚函数表,决定调用哪一个函数实现
- 编译器为包含虚函数的类创建一个虚函数表(
- 析构函数
- 如果一个类有虚函数,通常建议将析构函数也定义为虚函数
- 因为如果一个对象通过基类指针被删除,只有虚析构函数才能确保派生类的析构函数被正确调用,防止资源泄漏
静态成员函数
- 与类本身关联,不与对象实例关联
- 静态成员函数属于类本身,而不是某个具体的对象
- 它们可以在没有创建类对象的情况下直接通过类名调用
- 没有
this
指针- 静态成员函数不能访问非静态成员,因为它们没有隐式的
this
指针 this
指针是指向对象本身的指针,而静态成员函数与对象无关,所以无法访问对象的非静态数据成员或非静态成员函数
- 静态成员函数不能访问非静态成员,因为它们没有隐式的
- 只能访问静态成员
- 静态成员函数只能访问类的静态数据成员和静态成员函数,不能直接访问类的非静态数据成员
- 这是因为非静态成员需要依赖于具体的对象,而静态成员函数不依赖于任何对象
- 可以通过类名或对象调用
- 静态成员函数既可以通过类名调用,如
ClassName::staticFunction()
,也可以通过对象调用,如object.staticFunction()
- 尽管可以通过对象调用,编译器实际上仍然把它看作类的函数,而不是某个对象的函数
- 静态成员函数既可以通过类名调用,如
- 适用于需要共享数据的场景
- 静态成员函数通常用于那些需要在类的所有对象之间共享数据或实现类级别的操作的场景
多重继承下的虚函数
- 如果两个基类有同名的虚函数
test
,那么类A
调用了test
,是调用哪个基类的test
?- 如果类 A 没有重写同名虚函数
test
,并且尝试直接调用test
,编译器会报错,因为它不知道应该调用哪个基类的版本。这种情况称为二义性 - 解决方法如下
- 如果类 A 没有重写同名虚函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 方式一:在类 A 中重写 test #include <iostream> class Base1 { public: virtual void test() { std::cout << "Base1::test" << std::endl; } }; class Base2 { public: virtual void test() { std::cout << "Base2::test" << std::endl; } }; class A : public Base1, public Base2 { public: void test() override { std::cout << "A::test" << std::endl; } // 重写 test }; int main() { A a; a.test(); // 调用 A::test return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 方式二:通过基类作用域指定调用 #include <iostream> class Base1 { public: virtual void test() { std::cout << "Base1::test" << std::endl; } }; class Base2 { public: virtual void test() { std::cout << "Base2::test" << std::endl; } }; class A : public Base1, public Base2 { // A 没有重写 test }; int main() { A a; a.Base1::test(); // 显式调用 Base1 的 test a.Base2::test(); // 显式调用 Base2 的 test return 0; } |
- 如果类
A
重写了test
函数,在类A
对象的虚函数表中,test
在哪里- 类
A
有两个虚函数表,test
在这两个虚函数表中是同一个地址
- 类
- 如果类
A
有自己的虚函数aaa
,aaa
会存放在哪张虚函数表里- 类
A
自己的虚函数通常会被放置在它的主继承路径的虚函数表中 - 主继承路径通常是第一个基类的继承路径,也就是在多重继承中,虚函数会优先添加到第一个基类的虚函数表中
- 类
1 2 3 4 5 6 |
class A : public Base1, public Base2 { public: void func1() override { std::cout << "A::func1" << std::endl; } void func2() override { std::cout << "A::func2" << std::endl; } virtual void aaa() { std::cout << "A::aaa" << std::endl; } // 类A自己定义的虚函数 }; |
虚继承下的虚函数
- 虚继承的目的
- 虚继承用于解决多重继承时的二义性和重复继承问题,确保虚基类只被继承一次
- 通过虚继承,多个派生类共享虚基类的同一份实例,避免重复构造和重复存储虚基类的数据成员
- 虚继承下的虚函数表(
vtable
)和虚基类表(vbtable
)- 虚继承引入了一个虚基类表(
vbtable
),用于记录虚基类在内存中的偏移量,确保派生类可以正确访问共享的虚基类 - 当类同时包含虚继承和虚函数时,类的虚函数表不仅要管理虚函数指针,还可能包含对虚基类表的引用,以正确处理虚继承
- 虚继承引入了一个虚基类表(
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 |
#include <iostream> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } virtual ~Base() = default; }; class Derived1 : virtual public Base { public: void func() override { std::cout << "Derived1::func" << std::endl; } }; class Derived2 : virtual public Base { public: void func() override { std::cout << "Derived2::func" << std::endl; } }; class Final : public Derived1, public Derived2 { public: void func() override { std::cout << "Final::func" << std::endl; } }; int main() { Final f; Base* b = &f; Derived1* d1 = &f; Derived2* d2 = &f; b->func(); // 调用 Final::func() d1->func(); // 调用 Final::func() d2->func(); // 调用 Final::func() return 0; } |
- 关于虚函数表
- 虚函数表
1
里面有Base
里的所有虚函数,以及Derived1
重写了的虚函数,以及被Final
重写了的虚函数,以及Derived1
自己独有的虚函数,以及Final
自己独有的虚函数 - 虚函数表
2
里面保存了Base
的所有虚函数,以及Derived2
重写了的虚函数,以及Final
重写了的虚函数
- 虚函数表
- 关于虚基类表
- 虚基类表(
vbtable
)并不直接保存虚基类Base
的地址,而是保存指向虚基类的内存偏移量 - 当
Derived1
和Derived2
通过虚继承继承Base
时,编译器为它们各自生成了虚基类表(vbtable
),用来记录从当前派生类对象到Base
的内存偏移量 - 对于
Final
类,编译器会为Final
生成一个自己的虚基类表,并根据Derived1
和Derived2
中各自的虚基类表偏移量,计算出Final
类自己的虚基类表中应该保存的关于Base
的偏移量 - 在虚继承的情况下,
Derived1
、Derived2
和Final
在访问虚基类Base
的成员(包括虚函数)时,都需要通过虚基类表(vbtable
)中的偏移量来定位虚基类Base
的实际位置,然后再根据定位到的虚基类位置来访问它的虚函数表(vtable
)和其他成员
- 虚基类表(
指向成员函数的指针
- 概述
- 指向类成员函数的指针是
C++
中一种特殊的指针类型,用于指向类的成员函数 - 与普通函数指针不同,指向成员函数的指针包含成员函数在类中的位置,而不包含对象实例的信息
- 因此,在使用成员函数指针时,必须结合具体的对象实例进行调用
- 指向类成员函数的指针是
- 语法格式
1 |
返回类型 (类名::*指针名)(参数类型列表) = &类名::成员函数名; |
1 |
void (ClassName::*funcPtr)() = &ClassName::memberFunction; |
1 2 |
(对象.*指针名)(参数); // 对象调用 (对象指针->*指针名)(参数); // 对象指针调用 |
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 |
#include <iostream> class MyClass { public: void display() { std::cout << "Display function called" << std::endl; } void show(int x) { std::cout << "Show function called with value: " << x << std::endl; } }; int main() { MyClass obj; // 声明一个指向 MyClass 类成员函数的指针,并赋值给 display 函数 void (MyClass::*funcPtr1)() = &MyClass::display; // 声明另一个指向 MyClass 类成员函数的指针,并赋值给 show 函数 void (MyClass::*funcPtr2)(int) = &MyClass::show; // 使用对象调用成员函数指针 (obj.*funcPtr1)(); // 输出:Display function called // 使用对象调用另一个成员函数指针 (obj.*funcPtr2)(42); // 输出:Show function called with value: 42 // 也可以通过对象指针调用成员函数指针 MyClass* objPtr = &obj; (objPtr->*funcPtr1)(); // 输出:Display function called (objPtr->*funcPtr2)(99); // 输出:Show function called with value: 99 return 0; } |
- 使用场景1:实现回调机制
- 使用场景2:实现策略模式或行为模式
- 使用场景3:事件驱动系统
- 使用场景4:封装行为或实现多态
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 |
#include <iostream> class Robot { public: // 设置行为的接口 void setAction(void (Robot::*action)()) { this->action = action; } // 执行行为的统一接口 void performAction() { if (action) { (this->*action)(); // 统一通过这个接口执行当前的行为 } } private: // 行为函数被设置为私有,禁止直接调用 void walk() { std::cout << "Robot walking..." << std::endl; } void talk() { std::cout << "Robot talking..." << std::endl; } void dance() { std::cout << "Robot dancing..." << std::endl; } // 指向成员函数的指针,用于动态设置行为 void (Robot::*action)() = nullptr; }; int main() { Robot robot; // 假设这些设置是根据某些逻辑决定的 int command = 1; // 例如,这个变量可以由用户输入、传感器数据或其他逻辑决定 // 根据不同的条件设置行为 if (command == 1) { robot.setAction(&Robot::walk); } else if (command == 2) { robot.setAction(&Robot::talk); } else if (command == 3) { robot.setAction(&Robot::dance); } else { std::cout << "Unknown command!" << std::endl; } // 通过统一接口执行当前设置的行为 robot.performAction(); return 0; } |
指向虚拟成员函数的指针
- 概述
- 指向虚拟成员函数的指针(即指向虚函数的指针)是指向类中虚函数的成员函数指针
- 虚函数通过虚指针(
vptr
)和虚函数表(vtable
)实现动态绑定,这意味着当你调用一个虚函数时,实际调用的函数版本取决于对象的实际类型,而不是指针或引用的类型 - 指向虚函数的成员函数指针与普通成员函数指针的定义方式相同,只是当该指针指向虚函数时,它会通过对象的虚函数表来找到函数的地址
- 调用指向虚函数的指针时,系统会通过虚函数表来查找函数的实际地址。这意味着即使指针指向基类,调用的也可能是派生类重写的版本
- 语法格式
1 |
返回类型 (类名::*指针名)(参数类型列表) = &类名::虚函数名; |
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 |
#include <iostream> class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } virtual void show(int x) { std::cout << "Base::show called with " << x << std::endl; } virtual ~Base() = default; }; class Derived : public Base { public: void func() override { std::cout << "Derived::func" << std::endl; } void show(int x) override { std::cout << "Derived::show called with " << x << std::endl; } }; int main() { Derived d; Base* bPtr = &d; // 指向派生类对象的基类指针 // 声明指向虚函数的成员函数指针,并指向 Base::func void (Base::*funcPtr)() = &Base::func; // 声明另一个指向虚函数的成员函数指针,并指向 Base::show void (Base::*showPtr)(int) = &Base::show; // 使用指向虚函数的指针调用,通过虚函数表动态绑定到 Derived 的实现 (bPtr->*funcPtr)(); // 输出:Derived::func (bPtr->*showPtr)(42); // 输出:Derived::show called with 42 return 0; } |
- 使用场景1:实现回调机制
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 |
#include <iostream> #include <vector> class Button { public: virtual void onClick() = 0; // 定义一个虚函数作为回调接口 }; class SaveButton : public Button { public: void onClick() override { std::cout << "Save operation performed." << std::endl; } }; class CancelButton : public Button { public: void onClick() override { std::cout << "Cancel operation performed." << std::endl; } }; void triggerCallback(Button* btn, void (Button::*callback)()) { (btn->*callback)(); // 动态调用传入的回调函数 } int main() { SaveButton save; CancelButton cancel; // 定义指向虚函数的指针 void (Button::*callback)() = &Button::onClick; // 触发回调 triggerCallback(&save, callback); // 输出: Save operation performed. triggerCallback(&cancel, callback); // 输出: Cancel operation performed. return 0; } |
- 使用场景2:实现策略模式或行为模式
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 |
#include <iostream> class SortStrategy { public: virtual void sort() = 0; }; class QuickSort : public SortStrategy { public: void sort() override { std::cout << "QuickSort algorithm executed." << std::endl; } }; class MergeSort : public SortStrategy { public: void sort() override { std::cout << "MergeSort algorithm executed." << std::endl; } }; void executeSort(SortStrategy* strategy, void (SortStrategy::*sortFunc)()) { (strategy->*sortFunc)(); } int main() { QuickSort quick; MergeSort merge; void (SortStrategy::*sortFunc)() = &SortStrategy::sort; // 执行不同的排序算法 executeSort(&quick, sortFunc); // 输出: QuickSort algorithm executed. executeSort(&merge, sortFunc); // 输出: MergeSort algorithm executed. return 0; } |
- 使用场景3:事件驱动系统
- 在事件驱动的编程中,使用指向虚函数的指针可以实现事件响应的动态绑定,比如
GUI
程序中按钮点击事件的处理,或者游戏引擎中对不同对象的响应
- 在事件驱动的编程中,使用指向虚函数的指针可以实现事件响应的动态绑定,比如
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 |
#include <iostream> class EventListener { public: virtual void onEvent() = 0; }; class Player : public EventListener { public: void onEvent() override { std::cout << "Player responds to event." << std::endl; } }; class Enemy : public EventListener { public: void onEvent() override { std::cout << "Enemy responds to event." << std::endl; } }; void triggerEvent(EventListener* listener, void (EventListener::*eventFunc)()) { (listener->*eventFunc)(); } int main() { Player player; Enemy enemy; void (EventListener::*eventFunc)() = &EventListener::onEvent; triggerEvent(&player, eventFunc); // 输出: Player responds to event. triggerEvent(&enemy, eventFunc); // 输出: Enemy responds to event. return 0; } |
多重继承下,指向成员函数的指针
内联函数
构造析构拷贝语意学
纯虚函数
虚继承
- 在虚继承的情况下,虚基类
Base
的构造函数由最终派生类负责调用,而不是由中间派生类(如Derived
)直接调用 - 这意味着在构造一个最终派生类(如
Final
)对象时,Final
类的构造函数会先调用Base
的构造函数,再调用Derived
的构造函数- 种顺序确保了
Base
只被构造一次,并且由最派生类来控制其初始化
- 种顺序确保了
初始化语意学
- 当类有虚函数时,编译器会在构造函数的开头自动插入对
vptr
的初始化操作 - 这一步是在构造函数体开始执行之前完成的,是为了确保在对象构造过程中,虚函数的调用是正确的
- 构造函数的初始化列表用于初始化成员变量和基类部分
- 初始化列表的内容在进入构造函数体之前执行,但它的执行顺序在
vptr
的初始化之后
- 初始化列表的内容在进入构造函数体之前执行,但它的执行顺序在
构造语意学
- 需要对数据成员进行初始化
- 如果类中包含需要特定初始值的数据成员,如内建类型、类对象或指针等,构造函数是必要的
- 特别是对于
const
成员、引用成员,必须在构造函数初始化列表中进行初始化,因为它们不能在构造函数体中赋值
- 需要分配或管理资源
- 如果类中需要分配资源(如动态内存、文件句柄、网络连接等),构造函数可以用于执行这些资源的初始化和分配操作
- 构造函数可以帮助确保资源分配成功,并将对象设置为一个可用状态
- 类具有复杂的初始化逻辑
- 当类的初始化过程需要特定的逻辑(例如依赖于参数的初始化、调用特定的初始化函数),构造函数提供了一个执行这些逻辑的地方
- 类需要接受参数进行初始化
- 当一个类的对象需要通过参数来指定初始状态时,自定义构造函数是必要的
- 需要确保对象的状态一致性
析构语意学
- 析构函数的核心职责是确保对象被销毁时,所占用的资源被正确释放
- 释放动态分配的资源
- 关闭文件或网络连接
- 管理系统资源
- 自定义清理逻辑
- 类使用了
RAII
(Resource Acquisition Is Initialization
)模式 - 基类需要一个虚析构函数
- 如果类是作为基类使用的,特别是有可能通过基类指针或引用指向派生类对象,析构函数应该定义为虚函数,以确保派生类对象能够正确地调用自己的析构函数
- 类需要在销毁时执行特殊操作
- 类可能需要在销毁时执行特定的逻辑,如日志记录、通知系统状态改变等,这些操作应在析构函数中完成
执行期语意学
全局对象
- 全局对象是在
main
函数调用之前完成构造的 - 构造顺序:
- 全局对象的构造在程序启动时进行,即在进入
main
函数之前由运行时环境负责构造
它们的构造顺序依赖于它们在文件中的定义顺序,以及在不同编译单元中的链接顺序 - 全局对象的析构在
main
函数结束(包括正常结束和异常退出)之后进行,析构顺序与构造顺序相反,即最后构造的对象最先析构
- 全局对象的构造在程序启动时进行,即在进入
局部静态对象
- 概述
- 局部静态对象是在函数内部声明的具有
static
关键字的对象 - 它的生命周期贯穿程序运行的整个过程
- 但它的构造只会在第一次进入函数时执行一次,并且在后续调用中不会再次构造
- 局部静态对象是在函数内部声明的具有
- 析构时机
- 局部静态对象的析构是在程序结束时(通常在
main
函数结束后)由运行时环境自动处理,析构顺序与构造顺序相反
- 局部静态对象的析构是在程序结束时(通常在
默认构造和数组
- 定义一个类的数组,类必须满足以下要求:
- 类必须有可用的默认构造函数
- 当定义一个类的数组时,编译器会为数组中的每个元素调用默认构造函数
- 如果类没有默认构造函数,或者默认构造函数被删除,编译器将无法为数组元素自动构造对象
- 类的构造函数不应引发异常(推荐)
- 在数组构造过程中,如果任何一个元素的构造函数抛出异常,可能会导致部分对象被构造,而其他对象未被正确构造,造成资源泄漏或未定义行为
- 类必须有可用的析构函数
- 数组的析构过程会对每个元素调用析构函数,确保对象的资源能够被正确释放
- 可用的拷贝或移动构造函数(根据情况)
- 如果数组元素需要被复制或者从另一个数组移动,需要确保类提供合适的拷贝构造函数或移动构造函数
- 总结:
- 必须要有可用的默认构造函数和析构函数
- 最好能保证构造函数不抛出异常
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++编程规范101规则、准则与最佳实践 二01/07
- ♥ Effective C++_第三篇07/01
- ♥ Soui四05/23
- ♥ Boost程序库完全开发指南:时间与内存08/21
- ♥ C++数据库_Sqlite306/23
- ♥ C++17_第三篇06/29