C++语言特征:构造函数,析构函数,虚函数,内联函数,静态成员函数,重载,覆盖,隐藏
C++语言特性:构造函数,析构函数,虚函数,内联函数,静态成员函数,重载,覆盖,隐藏
构造函数:
1.初始化对象成员;
2.创建类对象;
由于虚函数是建立在对象的基础上的,因此构造函数不能声明为虚函数;虚函数是在执行的时候才识别,根据具体对象进行动态绑定.
每个类对象都有一个默认构造函数.当一个对象被在堆上创建的时候,第一步先执行new操作,第二步才会执行构造函数体,因此尽可能不要在构造函数内部动态申请太多的资源,以免引起内存泄露.
详情如下:
拷贝构造函数[复制构造函数]:
如果要动态地为本地C++类的成员分配空间,则必须实现复制构造函数.
下面是拷贝构造函数的实现模版:
1.当一个对象以值传递的方式传入函数体;
2.当一个对象以值传递的方式作为函数的返回值;
3.当一个对象通过另外一个对象进行初始化;
为什么需要实现拷贝构造函数?
上面的现象也被OOP中称为浅拷贝.
那对应的肯定有深拷贝, Object0拥有资源,当上面的Object1在复制的过程中重新分配了资源,那么这个过程就是深拷贝.
在传递对象cName之前,编译器需要安排创建该对象的副本.因此,编译器为了处理复制构造函数的这条语句,需要调用复制构造函数来创建实参的副本。由于是按值传递,第二次调用同样需要创建实参的副本,因此还得调用复制构造函数,产生无穷调用.
看下面的一段代码:
实参cName是作为传值的方式传递的,那么Object0将导致默认构造函数被调用:
1.创建cName对象;
2.由于cName是传值方式,因此导致Object0将要创建一个副本,而且该副本同时指向原来对象所指向的数据成员.
3.当退出testDefaultConstructor函数的时候, Object0超出了其作用域,那么Object0的析构函数将要被调用,Object0指向的数据成员将要被释放.
4.当函数从testDefaultConstructor返回的时候, cName对象依然指向之前Object0指向的数据区域.异常发生!
析构函数:
1.给对象提供释放资源的机会;
2.销毁对象,销毁不再需要或超出其作用域的对象.当对象超出其作用域时,程序将自动调用类的析构函数.
如果想要防止对像被创建在堆上,可以私有化析构函数,不过这样以来该类不能被继承.
如果需要调用私有析构函数,则需要实现一个成员函数,在该成员函数内部调用
虚拟析构函数:
如果当前类作为接口基类,那么需要声明该类的析构函数为虚拟析构函数.
因为如果,当我们使用基类的指针去删除派生类的对象的时候,如果基类的析构函数不是虚拟析构函数,那么派生类的析构函数将会不被执行.
但是当生命了虚函数之后,在对象中就会生成一个虚函数表,这样会增加类对象的存储空间开销.
下面部分是基于基类虚拟析构函数的条件下反汇编出来的:
虚函数是以virtual关键字声明的基类函数.如果在基类中将某个函数指定为virtual,并且派生类中有该函数的另外一个定义,则编译器将知道我们不想静态链接该函数.
当基类中声明了虚函数,那么根据调用该函数的当前对象的类型,选择派生类中出现的该函数的其他定义.
纯虚函数, 通过在函数声明最后添加=0,可以将本地C++基类中的虚函数定义为纯虚函数. 那么该类就称为不能创建任何对象的抽象类,在任何派生类中,都必须定义所有纯虚函数.如果不是,则该派生类也将称为抽象类.
根据虚函数定义后的虚函数表,基类指针既可以指向派生类的成员,也可以指向基类的成员.
虚函数表,
下面部分内容参考陈皓专栏
虚函数表地址:002FF8D8
虚函数表第一个函数地址:00EF7868
baseClass function a()
请按任意键继续. . .
下面是虚函数表的详细情况:

图1
(int*)(&cBase) //强制转换cBase对象在内存中的内容为int*[0x0031fce4],它指向cBase对象的第一个成员的地址,即虚函数表的地址[0x011e7868]
(int*)*(int*)(&cBase) //对虚函数表的地址进行解引用,*(int*)(&cBase)指向的地址就是虚函数表的地址,然后转换为int*,它指向虚函数表的第一个成员,即_vfptr[0] ,地址为[0x011e121c]
C++中的重载、覆盖、隐藏
1.重载从最简单的角度来讲只发生在对象内部,对象内部同名的函数,但是参数个数或参数类型不同;
关键字inline,功能类似宏替换,具有函数的结构,在编译时刻根据函数名来替换成对应的代码,在代码量小的重复次数多的情况下,比较高效.不是适合复杂代码,同时是否能够实现内联的功能,具体要依赖编译器,有可能编译器根据实际情况当成普通函数来处理.
静态成员函数:
静态成员在同类对象中只有一个实例,因此可以用来统计同类对象的计数;
静态成员函数独立于类对象,因此即使类对象不存在,静态成员函数依然可以被调用,静态成员函数只能调用静态成员变量在这样的情况下.
构造函数:
1.初始化对象成员;
2.创建类对象;
由于虚函数是建立在对象的基础上的,因此构造函数不能声明为虚函数;虚函数是在执行的时候才识别,根据具体对象进行动态绑定.
每个类对象都有一个默认构造函数.当一个对象被在堆上创建的时候,第一步先执行new操作,第二步才会执行构造函数体,因此尽可能不要在构造函数内部动态申请太多的资源,以免引起内存泄露.
详情如下:
cppBaseClass *base = new cppBaseClass(); 012A1FDD push 0Ch 012A1FDF call operator new (12A11E5h) 012A1FE4 add esp,4 012A1FE7 mov dword ptr [ebp-11Ch],eax 012A1FED mov dword ptr [ebp-4],0 012A1FF4 cmp dword ptr [ebp-11Ch],0 012A1FFB je main+70h (12A2010h) 012A1FFD mov ecx,dword ptr [ebp-11Ch] 012A2003 call cppBaseClass::cppBaseClass (12A1023h) 012A2008 mov dword ptr [ebp-130h],eax 012A200E jmp main+7Ah (12A201Ah) 012A2010 mov dword ptr [ebp-130h],0 012A201A mov eax,dword ptr [ebp-130h] 012A2020 mov dword ptr [ebp-128h],eax 012A2026 mov dword ptr [ebp-4],0FFFFFFFFh 012A202D mov ecx,dword ptr [ebp-128h] 012A2033 mov dword ptr [ebp-14h],ecx
拷贝构造函数[复制构造函数]:
如果要动态地为本地C++类的成员分配空间,则必须实现复制构造函数.
下面是拷贝构造函数的实现模版:
ConstructorFunctionClassName(const ConstructorFunctionClassName& ObjectType) { }拷贝构造函数的调用条件:
1.当一个对象以值传递的方式传入函数体;
2.当一个对象以值传递的方式作为函数的返回值;
3.当一个对象通过另外一个对象进行初始化;
为什么需要实现拷贝构造函数?
首先这里需要描述一下C++中默认拷贝构造函数,参考下面代码:
className Object0("Test the constructor function"); className Object1 = Object0; //默认构造函数被调用默认构造函数会把类对象Object0的指针成员存储地址复制到Object1中,这个时候Object1和Object0同时指向同一块内存区域, 那么如果Oject1被销毁了,那么Object0中的内容就会发生改变.
上面的现象也被OOP中称为浅拷贝.
那对应的肯定有深拷贝, Object0拥有资源,当上面的Object1在复制的过程中重新分配了资源,那么这个过程就是深拷贝.
className cName; className::className(cName);
在传递对象cName之前,编译器需要安排创建该对象的副本.因此,编译器为了处理复制构造函数的这条语句,需要调用复制构造函数来创建实参的副本。由于是按值传递,第二次调用同样需要创建实参的副本,因此还得调用复制构造函数,产生无穷调用.
看下面的一段代码:
className cName("Test default constructor"); testDefaultConstructor(cName); void testDefaultConstructor( className Object0) { Object0.PrintString(); }
实参cName是作为传值的方式传递的,那么Object0将导致默认构造函数被调用:
1.创建cName对象;
2.由于cName是传值方式,因此导致Object0将要创建一个副本,而且该副本同时指向原来对象所指向的数据成员.
3.当退出testDefaultConstructor函数的时候, Object0超出了其作用域,那么Object0的析构函数将要被调用,Object0指向的数据成员将要被释放.
4.当函数从testDefaultConstructor返回的时候, cName对象依然指向之前Object0指向的数据区域.异常发生!
析构函数:
1.给对象提供释放资源的机会;
2.销毁对象,销毁不再需要或超出其作用域的对象.当对象超出其作用域时,程序将自动调用类的析构函数.
如果想要防止对像被创建在堆上,可以私有化析构函数,不过这样以来该类不能被继承.
如果需要调用私有析构函数,则需要实现一个成员函数,在该成员函数内部调用
delete this;
虚拟析构函数:
如果当前类作为接口基类,那么需要声明该类的析构函数为虚拟析构函数.
因为如果,当我们使用基类的指针去删除派生类的对象的时候,如果基类的析构函数不是虚拟析构函数,那么派生类的析构函数将会不被执行.
但是当生命了虚函数之后,在对象中就会生成一个虚函数表,这样会增加类对象的存储空间开销.
下面部分是基于基类虚拟析构函数的条件下反汇编出来的:
cppBaseClass *base = new cppDeriveClass(); 002D235D push 10h 002D235F call operator new (2D121Ch) 002D2364 add esp,4 002D2367 mov dword ptr [ebp-104h],eax 002D236D mov dword ptr [ebp-4],0 002D2374 cmp dword ptr [ebp-104h],0 002D237B je main+70h (2D2390h) 002D237D mov ecx,dword ptr [ebp-104h] 002D2383 call cppDeriveClass::cppDeriveClass (2D1069h) 002D2388 mov dword ptr [ebp-118h],eax 002D238E jmp main+7Ah (2D239Ah) 002D2390 mov dword ptr [ebp-118h],0 002D239A mov eax,dword ptr [ebp-118h] 002D23A0 mov dword ptr [ebp-110h],eax 002D23A6 mov dword ptr [ebp-4],0FFFFFFFFh 002D23AD mov ecx,dword ptr [ebp-110h] 002D23B3 mov dword ptr [ebp-14h],ecx delete base; 002D23B6 mov eax,dword ptr [ebp-14h] 002D23B9 mov dword ptr [ebp-0ECh],eax 002D23BF mov ecx,dword ptr [ebp-0ECh] 002D23C5 mov dword ptr [ebp-0F8h],ecx 002D23CB cmp dword ptr [ebp-0F8h],0 002D23D2 je main+0D9h (2D23F9h) 002D23D4 mov esi,esp 002D23D6 push 1 002D23D8 mov edx,dword ptr [ebp-0F8h] 002D23DE mov eax,dword ptr [edx] 002D23E0 mov ecx,dword ptr [ebp-0F8h] 002D23E6 mov edx,dword ptr [eax] 002D23E8 call edx 002D23EA cmp esi,esp 002D23EC call @ILT+445(__RTC_CheckEsp) (2D11C2h) 002D23F1 mov dword ptr [ebp-118h],eax 002D23F7 jmp main+0E3h (2D2403h) 002D23F9 mov dword ptr [ebp-118h],0 return 0; 002D2403 xor eax,eax }
虚函数:
虚函数主要是实现了面向对象中的多态的作用.通俗的讲就是通过基类的指针指向派生类的对象,用基类的指针来调用派生类的成员函数. 这种技术可以让派生类拥有“多种形态”,这是一种泛型技术, 通过使用不变的代码来实现可变的算法.比如:模版技术, RTTI[Run-Time Type Identification], 虚函数技术.虚函数是以virtual关键字声明的基类函数.如果在基类中将某个函数指定为virtual,并且派生类中有该函数的另外一个定义,则编译器将知道我们不想静态链接该函数.
当基类中声明了虚函数,那么根据调用该函数的当前对象的类型,选择派生类中出现的该函数的其他定义.
纯虚函数, 通过在函数声明最后添加=0,可以将本地C++基类中的虚函数定义为纯虚函数. 那么该类就称为不能创建任何对象的抽象类,在任何派生类中,都必须定义所有纯虚函数.如果不是,则该派生类也将称为抽象类.
根据虚函数定义后的虚函数表,基类指针既可以指向派生类的成员,也可以指向基类的成员.
虚函数表,
下面部分内容参考陈皓专栏
http://blog.****.net/haoel/article/details/1948051
cppVirtualFunctionTable.cpp
#include <iostream> using std::cout; using std::endl; class baseClass { public: virtual void a(){ cout<<"baseClass function a()"<<endl;} virtual void b(){ cout<<"baseClass function b()"<<endl;} virtual void c(){ cout<<"baseClass function c()"<<endl;} }; class deriveClass: public baseClass { public: void a(){ cout<<"deriveClass function a()"<<endl;} void b1(){ cout<<"deriveClass function b1()"<<endl;} void c1(){ cout<<"deriveClass function c1()"<<endl;} };
main.cpp
#include "cppVirtualFunctionTable.cpp" int main(int argc, char** argv) { // define the Func is an alias for the function pointer. typedef void(*Func)(void); baseClass cBase; Func pFunc = NULL; cout<<"虚函数表地址:"<<(int*)(&cBase)<<endl; cout<<"虚函数表第一个函数地址:"<<(int*)*(int*)(&cBase)<<endl; // Invoke first virtual function pFunc = (Func)*((int*)*(int*)(&cBase)); pFunc(); return 0; }
环境:Microsoft Visual Studio 2010, Window 7
输出结果:虚函数表地址:002FF8D8
虚函数表第一个函数地址:00EF7868
baseClass function a()
请按任意键继续. . .
下面是虚函数表的详细情况:
图1
(int*)(&cBase) //强制转换cBase对象在内存中的内容为int*[0x0031fce4],它指向cBase对象的第一个成员的地址,即虚函数表的地址[0x011e7868]
(int*)*(int*)(&cBase) //对虚函数表的地址进行解引用,*(int*)(&cBase)指向的地址就是虚函数表的地址,然后转换为int*,它指向虚函数表的第一个成员,即_vfptr[0] ,地址为[0x011e121c]
下面是关于基类地址,以及虚函数表、基类成员、派生类成员的详细分布:
图2
从上面的图中我们可以看到,派生类的成员a()覆盖了基类的成员a()C++中的重载、覆盖、隐藏
1.重载从最简单的角度来讲只发生在对象内部,对象内部同名的函数,但是参数个数或参数类型不同;
2.覆盖就是上面图中标示的那种情况;
3.当派生类和基类的函数同名,而且基类同名函数前virtual修饰符,基类的同名函数被隐藏;
关键字inline,功能类似宏替换,具有函数的结构,在编译时刻根据函数名来替换成对应的代码,在代码量小的重复次数多的情况下,比较高效.不是适合复杂代码,同时是否能够实现内联的功能,具体要依赖编译器,有可能编译器根据实际情况当成普通函数来处理.
静态成员函数:
静态成员在同类对象中只有一个实例,因此可以用来统计同类对象的计数;
静态成员函数独立于类对象,因此即使类对象不存在,静态成员函数依然可以被调用,静态成员函数只能调用静态成员变量在这样的情况下.
实例:Singleton模式,保证一个类只有一个实例对象,同时使该实例只有一个全局访问点.
C++对象属性:public, protected, private
共有继承,基类的公共成员和保护成员可以作为其派生类的公共成员和保护成员.
私有继承,基类的共有成员和私有成员都作为其派生类的私有成员.基类的成员只能由直接派生类来访问,不能再往下继承。
保护继承,基类的共有成员和保护成员都作为其派生类的保护成员