本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:boolan_cpp:oop_b_week2 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:oop_b_week2 [2024/01/14 13:47] (当前版本) – ↷ 链接因页面移动而自动修正 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======C++面向对象高级编程(下)第二周====== | ||
+ | 本页内容是作为 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | ====虚指针和虚函数表==== | ||
+ | |||
+ | 上节课的作业和第三周的内容都探讨了与虚函数相关的问题,今天我们可以着重来分析一下虚函数是怎么运作的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 来考虑一下如下继承情况: | ||
+ | <WRAP group> | ||
+ | <WRAP half column> | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | <WRAP half column> | ||
+ | <code cpp linenums: | ||
+ | Class Base { | ||
+ | public: | ||
+ | virtual void vfunc1(); | ||
+ | virtual void vfunc2(); | ||
+ | void func1(); | ||
+ | void func2(); | ||
+ | private: | ||
+ | int data1, data2; | ||
+ | }; | ||
+ | Class Derived: | ||
+ | public: | ||
+ | virtual void vfunc1(); | ||
+ | void func2(); | ||
+ | private: | ||
+ | int data3; | ||
+ | }; | ||
+ | Class SubDerived: | ||
+ | public: | ||
+ | virtual void vfunc1(); | ||
+ | | ||
+ | void func2(); | ||
+ | private: | ||
+ | int data1, data2; | ||
+ | }; | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | 这三个类中, '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 在叙述过程之前,我们需要明白:和普通的函数调用不一样,虚函数的调用是**通过指针查询一个表(数组)来实现的**;这个表就像是虚函数的地址簿一样。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 当一个类中有虚函数的声明时候,该类的对象里就会多一个指针,这个指针称为**虚指针**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | 在调用的时候,编译器首会根据对象的具体类型初始化一个指向当前类的虚指针。通过这个虚指针,我们就能在虚表里找到对应的虚函数的地址,从而可以调用正确的虚函数。比如上图的 '' | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | 这是单个类调用虚函数的情况。不过我们都知道,虚函数主要应用于类的继承关系中去实现多态;因此我们必须考虑继承的情况。 | ||
+ | |||
+ | ===继承中虚函数的调用=== | ||
+ | |||
+ | 当继承关系确定的时候,子类会**构造一个与父类相同的虚表**;也就是说,虚表可以继承。\\ | ||
+ | \\ | ||
+ | 打个比方,如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址)。而派生类的虚表中也至少有三个地址。如果重写了相应的虚函数,那么派生类虚表中对应的虚函数地址就会改变,转而指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,可以知道子类的虚函数调用分为两种情况: | ||
+ | * 如果子类对父类的虚函数进行了重定义,那么子类虚表中对应的虚函数地址将会被子类的虚函数地址覆盖。 | ||
+ | * 如果子类对父类的虚函数没有进行重写,那么子类虚表将沿用父类虚函数的地址。 | ||
+ | \\ | ||
+ | 本节开头的例子中 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 此时,'' | ||
+ | |||
+ | ==多重继承中的虚函数调用== | ||
+ | |||
+ | 如果子类同时继承多个父类,那么该子类将继承所有父类中的虚函数表,然后根据子类中重写的情况覆盖对应虚表中的虚函数地址。 | ||
+ | |||
+ | ===动态联编和静态联编=== | ||
+ | |||
+ | 一个源程序需要经过编译、连接才能形成可执行文件,在这个过程中要把调用函数的名称和对应函数在内存中的区域关联在一起,这个过程就是**绑定**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | 绑定分为**静态绑定**和**动态绑定**。这两者的根本目的都是在为被调用函数寻找其所处内存的位置,只是**寻址的方式有所不同**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 静态绑定又称静态联编,是指在编译程序时候,被调用函数的信息中就包含了函数在内存中所处的位置。如果用汇编语言来表示静态绑定,可以直接写成:'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 动态绑定又称动态联编,也就是虚函数绑定其函数在内存中的地址的过程。跟静态绑定不同的是,虚函数无法在编译的时候确定我们需要具体调用哪个虚函数;因为虚函数的调用存在于对象里,而对象必须要在 run-time 中才能建立。因此,我们不能在编译的时候直接得到对应虚函数的地址,而是在程序运行中通过对象的创建,才能通过虚指针去找到对应的虚函数。对于虚函数的绑定来说,这个过程是动态的,因此称为动态绑定。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 虚函数进行动态绑定的过程大概如下: | ||
+ | * 建立一个指向对象的指针 '' | ||
+ | * 因为虚指针的位置位于对象地址的开头,所以有 指针'' | ||
+ | * 通过 '' | ||
+ | * 通过 '' | ||
+ | 整个过程可以写成如下代码: | ||
+ | <code cpp linenums: | ||
+ | (*(p-> | ||
+ | </ | ||
+ | 我们把这个整个过程称为**虚机制**。其中'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 值得注意的是,动态绑定的判定有三个条件: | ||
+ | - 需要用指针调用函数。所有对象直接调用函数(比如 '' | ||
+ | - 调用函数的指针要完成 '' | ||
+ | - 调用的函数一定是虚函数。 | ||
+ | |||
+ | ===子类对象调用父类函数=== | ||
+ | |||
+ | 子类对象调用父类函数是通过 '' | ||
+ | * 当新建一个子类对象,并用这个对象调用父类函数的时候, '' | ||
+ | * 如果调用的是父类普通函数,那么调用方法为 '' | ||
+ | * 如果调用的是虚函数,那么 '' | ||
+ | |||
+ | ====Const和成员函数==== | ||
+ | |||
+ | '' | ||
+ | <code cpp linenums: | ||
+ | void func() const {} // member function | ||
+ | </ | ||
+ | 我们知道,'' | ||
+ | - // | ||
+ | - // | ||
+ | - //const// 对象调用// | ||
+ | - //const// 对象调用 //const// 成员函数 | ||
+ | 纵观这四个组合,我们发现只有第三条有问题:我们声明了一个 '' | ||
+ | |||
+ | ===额外的规则=== | ||
+ | |||
+ | 除开第三种组合,其余的组合从 '' | ||
+ | <code cpp linenums: | ||
+ | operator[](sizetype pos) const {...} | ||
+ | operator[](sizetype pos) {...} | ||
+ | </ | ||
+ | 在 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 而我们发现,如果使用非常量字符串去调用常量成员函数,那么从函数的角度来看是不用考虑 //COW// 的,但从对象的角度上看是需要考虑 //COW// 的,这样也矛盾了。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,C++ 对此提出了额外的一个规则:**成员函数中如果同时存在常量和非常量版本,那么常量对象只能调用常量版本,非常量对象只能调用非常量版本**。 | ||
+ | |||
+ | ====New / Detete 中的相关函数重载==== | ||
+ | |||
+ | 前面我们学习 '' | ||
+ | |||
+ | ===为何要重载 new / delete=== | ||
+ | |||
+ | 对于我们来说,'' | ||
+ | |||
+ | ===重载方式:operator new / delete=== | ||
+ | |||
+ | 一种重载 '' | ||
+ | |||
+ | ==Operator 全局重载== | ||
+ | |||
+ | 全局重载的示例如下: | ||
+ | <code cpp linenums: | ||
+ | inline void * operator new(size_t size) { return myAlloc(size); | ||
+ | inline void * operator new[] (size_t size) { return myAlloc(size); | ||
+ | inline void * operator delete(size_t size) { myFree(ptr); | ||
+ | inline void * operator delete[] size_t size) { myFree(ptr); | ||
+ | </ | ||
+ | |||
+ | ==Operator 类成员重载== | ||
+ | |||
+ | 类成员重载的实例如下: | ||
+ | <code cpp linenums: | ||
+ | class Foo { | ||
+ | .... | ||
+ | static void* operator new(size_t size); | ||
+ | static void operator delete(void* pdead, size_t size); | ||
+ | /*array version*/ | ||
+ | static void* operator new[](size_t size); | ||
+ | static void operator delete[](void* pdead, size_t size); | ||
+ | }; | ||
+ | </ | ||
+ | 调用的方法如下: | ||
+ | <code cpp linenums: | ||
+ | Foo*p = new Foo; | ||
+ | delete p; | ||
+ | /*array version*/ | ||
+ | Foo*pa = new Foo[n]; | ||
+ | delete [] pa; | ||
+ | </ | ||
+ | 类中重载 '' | ||
+ | * 参数中的 '' | ||
+ | * 如果没有重载,默认调用全局函数;如果有重载,则使用类重载版本。 | ||
+ | * 可以通过 ''::'' | ||
+ | * '' | ||
+ | |||
+ | ===重载方式:new() / delete()=== | ||
+ | |||
+ | C++ 中还提供了另外一种形式对 '' | ||
+ | <code cpp linenums: | ||
+ | Foo* p = new (size_t, placement_arg) Foo; | ||
+ | </ | ||
+ | 重载的写法示例如下: | ||
+ | <code cpp linenums: | ||
+ | void* operator new(size_t size, void* start) { return start; }; | ||
+ | void* operator new(size_t size, long extra) { return malloc(size + extra); }; | ||
+ | </ | ||
+ | 而我们也需要针对这些不同的版本写出相应的 '' | ||
+ | <code cpp linenums: | ||
+ | void operator delete(void*, | ||
+ | void operator delete(void*, | ||
+ | </ | ||
+ | 这类的 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 重载中有几点需要注意的是: | ||
+ | * 重载的第一参数类型必须是 '' | ||
+ | * 各个重载参数的参数列表必须不同。 | ||
+ | * 重载的 // | ||
+ | * 如果没有对应的 '' | ||