C++中的多态分为静态多态和动态多态,也称为编译时多态和运行时多态。 静态多态包括:函数重载、模板; 动态多态包括:虚函数。
C++通过虚函数实现动态多态: 在基类的成员函数前加上virtual关键字,在派生类中重写该函数,运行时根据对象的实际类型调用相应的函数。 原理: 对于每个定义了虚函数的类和它的派生类,都会产生一个虚函数表,这个虚函数表是类共享的。每个对象里都有一个虚表指针vf_ptr,指向该类型的虚函数表vf_table(虚表存放在只读数据段.rodata)。动态联编在基类指针或引用指向不同的派生类对象时发生,若派生类对象重写了虚函数,则虚表对应项将被覆盖,实际调用中根据对象的虚表指针vf_ptr找到该类的虚表,调用对应的函数。
早绑定/晚绑定、静态绑定/动态绑定: 早绑定又称静态绑定:在程序编译期发生,即编译期就确定将要调用的函数地址。 晚绑定又称动态绑定:在程序运行期发生,即在程序运行中确定要调用的函数地址。
p->Show(); # 静态绑定 0x0139B5D8 push 0Ah # 参数压栈 0x0139B5DA mov ecx,dword ptr [p] 0x0139B5DD call Base::Show (013916BDh) # 调用函数 p->Show(); # 动态绑定 0x009AA858 mov esi,esp 0x009AA85A push 0Ah # 参数压栈 0x009AA85C mov eax,dword ptr [p] # 将对象地址放入eax 0x009AA85F mov edx,dword ptr [eax] # 将对象中的虚表指针放入寄存器 0x009AA861 mov ecx,dword ptr [p] 0x009AA864 mov eax,dword ptr [edx] # 查虚表得到虚函数地址 0x009AA866 call eax # 调用相应的虚函数 0x009AA868 cmp esi,esp 0x009AA86A call __RTC_CheckEsp (09A147Eh) # 调整栈平衡volatile关键字提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了,将出现不一致。
一般情况下,volatile用在如下几个地方: 1. 中断服务程序中修改的供其它程序检测的变量需要加volatile。 1. 多任务环境下,各任务间共享的标志应该加volatile。 1. 存储器映射的硬件寄存器通常要加volatile说明,因为每次对它的读写都可能有不同的意义。
多线程下的volatile(防止多线程对共享变量进行缓存): 当两个线程都要用到某个变量且该变量的值会被改变时,应该用volatile声明,防止编译器优化把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,会造成程序的错误执行。
函数重载: 是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。 优点: 减少了函数名的数量,避免了名字空间的污染。 编译器实现: 应用源码编译之后,编译器对函数进行签名(作用域+返回类型+函数名+参数列表),从而重载函数的名字变了。例如
void print(int i) // 全局函数编译后函数名:_Z5printi class test{ public: void print(int i); // 编译后函数名: _ZN4test5printEi void print(char c); // 编译后函数名: _ZN4test5printEc }函数名调用解析: 为了估计哪个重载函数最合适,需要依次按照规则来判断: 精确匹配: 参数匹配而不做转换,或者只做微小的转换,如数组名到指针、函数名到指向函数的指针。 提升匹配: 即整数提升(bool到int,char到int,short到int)。 使用标准转换匹配: 使用用户自定义匹配: 使用省略号匹配:
都是地址的概念: 指针指向一块内存,内容是所指内存的地址;引用是某块内存的别名。
VC、VS等编译器默认是 #pragma pack(8) (可选1,2,4,8,16),g++、clang++编译器默认是#pragma pack(4) (可选1,2,4)
注意: 1. 递归函数不能定义为内联函数; 2. 内联函数一般适合于不存在循环等复杂的结构且只有1-5条语句的小函数上,否则编译系统将该函数视为普通函数; 3. 内联函数只能先定义后使用; 4. 对内联函数不能进行异常的接口声明。
仅仅重新解释类型,但没有进行二进制的转换: 1. 转换的类型必须是一个指针、引用、算术类型、函数指针或成员指针; 2. 在比特位级别上进行转换。可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针; 3. 最普通的用途就是在函数指针类型之间进行转换; 4. 很难保持移植性。
类似于C风格的强制转换。无条件转换、静态类型转换。用于: 1. 基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。 2. 基本数据类型转换。 3. 把空指针转换成目标类型的空指针。 4. 把任何类型的表达式转换成void类型。 5. static_cast不能去掉类型的const、volatile属性。
有条件转换、动态类型转换,运行时类型安全检查(转换失败返回NULL); 1. 安全的基类和子类之间转换 2. 必须要有虚函数 3. 相同基类不同子类之间的交叉转换,但结果是NULL。
设置或去掉类型的const或volatile属性。
总结: 去const属性用const_cast; 基本类型转换用static_cast; 多态类之间的类型转换用dynamic_cast; 不同类型的指针类型转换用reinterpret_cast;
对于指针变量有以下四种情况: 1. 指向非const对象的指针 将非const对象的指针指向一个常量对象将引起编译错误 2. 指向const对象的指针 不能通过指针修改常量的值,但是指针本身可以修改 3. const指针 声明const指针时,必须同时对其进行初始化 4. 指向const对象的const指针 声明时必须初始化,指针指向的对象以及指针本身都不能修改
typedef是用来声明自定义数据类型,配合各种原有数据类型来达到简化编程的目的; #define是预处理指令 1. 首先,两者指向时间不同 typedef在编译阶段有效,有类型检查的功能。 #define是宏定义,发生在预处理阶段,不进行任何检查 2. 功能不同 typedef用来定义类型的别名,类型不仅包含内部类型,还包含自定义类型。定义机器无关的类型。 #define可以为类型取别名,还可以定义常量、变量、编译开关等。
作用域不同 typedef 有自己的作用域 #define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。
对指针的操作 二者修饰指针类型时,作用不同
编写C++程序有时需要调用其他语言编写的函数,比如C, Fortran等。C++使用链接指示(linkage directive)标注这种混合编程编写的函数。
声明一个非C++函数 链接指示可以有两种形式:单个或复合。链接指示不能出现在类定义或函数定义的内部。 单语句: extern "C" size_t strlen(const char*); 复合语句: extern "C"{ int strcmp(const char*, const char*); char *strcat(char*, const char*); }链接指示与头文件 复合语句: extern "C" { #include <string.h> }
指向extern “C”函数的指针 extern "C" void (*pf)(int); 当使用pf调用函数时,编译器认定当前调用的是一个C函数。
链接指示对整个声明都有效 当使用链接指示时,不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效。
导出C++函数到其他语言 通过使用链接指示对函数进行定义,可以令一个C++函数在其他语言编写的程序中可用。 对链接到C的预处理器的支持: 有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理器定义__cplusplus(两个下划线)。利用这个变量,我们可以在编译C++程序的时候有条件地包含进来一些代码:
#ifndef __cplusplus //正确:我们在编译C++程序 extern "C" #endif int strcmp( const char*, const char* ); 重载函数与链接指示 链接指示与重载依赖于目标语言,目标语言支持则支持,C不支持重载,所以不能extern “C”用于相同函数名类型。如果在一组重载函数中有一个是C函数,其余的必然都是C++函数。在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A *ptr = new A;这两种方式有区别: 静态建立类对象: 是由编译器为对象在栈空间中分配内存,然后在这片栈内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。 动态建立类对象: 使用new运算符将对象建立在堆空间中,这个过程分为两步:第一步是指针operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数,初始化这片堆内存空间。使用这种方法,间接调用类的构造函数。
类对象只能建立在堆上,就不能静态建立类对象,即不能直接调用类的构造函数。 编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性以及非静态函数。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就无法建立在栈上。例如:
class A{ public: A(){} void destory() {delete this;} private: ~A(){} };缺点: 1)无法解决继承问题,如果A作为其他类的基类,则析构函数通常设为virtual,然后在子类中重写,以实现多态。因此,析构函数不能设为private。可以将析构函数设为protected。 2)类的使用很不方便,使用new建立对象,却使用delete释放对象,而不是使用delete。为了统一,可以将构造函数设为protected,然后提供一个public的静态函数来完成构造,这样不使用new。
class A{ protected: A() {} ~A() {} public: static A* create(){ return new A(); } void destory(){ delete this; } }; int main(int argc, char* argv[]){ A *aptr = A::create(); return 0; }只有使用new运算符,对象才会建立在堆上。因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。
class A{ private: void *operator new(size_t) {} void operator delete(void *ptr) {} public: A(){} ~A(){} };static 对象属于类,不属于具体的对象; static const对象不能在初始化列表中初始化。
基类构造函数是在派生类之前执行的,在基类构造函数运行的时候对象的派生类的数据成员部分还没有初始化。如果在基类的构造过程中,对虚函数的调用传递到派生类,派生类对象可以参照引用局部的数据成员。但是数据成员此时未初始化,这会导致无休止的未定义行为。
如果派生类的对象进行析构,首先调用派生类的析构函数,然后在调用基类的析构时,遇到一个虚函数,由两种选择:1)调用虚函数的基类版本,那么虚函数则失去了运行时调用正确版本的意义;2)调用虚函数的派生类版本,此时对象的派生类部分已经完成析构,函数调用会导致未定义行为。 实际情况使用基类版本,如果虚函数的基类版本不是纯虚实现,不会有严重错误发生。