面向对象的三大特点:封装、继承、多态。当然讲四大特点可以加上抽象。
定义:将数据和信息包装在单个单元中。在面向对象的编程中,封装被定义为将数据和操纵它们的功能绑定在一起。
比如,一家公司有销售部门、技术部门、财务部门,销售部门有对应销售数据,而如果财务部门想要查看销售部门的数据时,需要先通过销售部门的人员才能得到相应的数据。而这就是封装,外部人员访问相关部门时需要通过某种渠道、接口才能够获得数据。
而在代码中,具体体现在类的编写过程中,有private、public、protected修饰符。定义在private中的成员变量,外部只能够通过友元或类对外提供的接口访问。其中,友元函数、友元类会破会类的封装性,一般不使用。
定义:一个类(子类)从另一个类(父类)获得成员函数和成员变量的过程。
C++有三种继承:public、private、protected,如果不写参数默认为private。一般在工程中只采用public继承。
C++支持多继承,多继承功能强大,使得C++不需要像java、C#引入接口。接口:一个不能实例化,且只能拥有抽象方法的类型。接口为没有亲缘关系的类提供通用功能。当然多继承使用起来也比较麻烦,可能带来几个问题:
为了解决多继承时的命名冲突和冗余数据,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。
class A { protected: int m_a; }; class B : virtual public A //虚继承 { protected: int m_b; }; class C : virtual public A //虚继承 { protected: int m_c; }; class D : public B, public C { protected: void seta(int x) {m_a = x;} void setb(int x) {m_b = x;} void setc(int x) {m_c = x;} void setd(int x) {m_d = x;} private: int m_d; };
使用虚继承实现上述的菱形继承,就不会出现多份A的成员变量了,访问a时也不用指定某个类了。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。这个被共享的基类就称之为虚基类,本例中的A就是个虚基类。在这种机制下,不论虚基类在后面的继承中出现多少次,继承的类都只包含一份虚基类的成员。
多态是面向对象编程的一大特点,而虚函数是实现多态的机制。多态使得程序调用的函数是在运行时确定的,而不是编译时静态确定的。使用基类对象或引用指向子类对象,通过虚函数实现对子类的访问,就是实现多态的一个案例。
虚函数:在函数声明前面加上virtual,如:virtual void func();
纯虚函数:在虚函数的基础上加上=0,如:virtual void func() = 0;
虚函数,实现了运行时多态;重载,实现了编译时多态。
子类不实现虚函数,默认调用基类的虚函数。
class Base { public: virtual void vir_show() { cout << "vir_show Base" << endl; } void show() { cout << "show Base" << endl; } }; class A : public Base { public: virtual void vir_show() { cout << "vir_show A" << endl; } void show() { cout << "show A" << endl; } }; class B : public Base { public: virtual void vir_show() { cout << "vir_show B" << endl; } void show() { cout << "show B" << endl; } }; int main() { Base* base = new Base(); Base* a = new A(); Base* b = new B(); base->show(); a->show(); b->show(); cout << "###########################" << endl; base->vir_show(); a->vir_show(); b->vir_show(); cout << "###########################" << endl; ((A*)b)->show(); ((A*)b)->vir_show(); return 0; }
通过以上代码的运行结果,可以看到,当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定。将b强转为a,调用普通函数显示A,而虚函数仍然显示B。
这是因为,普通函数放在代码区,为该类所有对象共用,而成员变量则在堆区,为各个对象私有。在调用成员函数时,程序会根据类的类型,在对应的代码区找到对应的函数并调用。其实就是根据指针的类型调用对应的函数,如强转为A,就调用类A的函数。
那么虚函数的内存分配又是如何的?如以下代码:
class C { public: void fun1() {} void fun2() {} int x; }; class D { public: void fun1() {} virtual void fun2() {} int x; };
定义了C和D,其中D的函数fun2()是虚函数,这时候我们使用sizeof计算C和D的大小时会发现,D比C大四个字节。首先来看内存分配:
类D多出的四个字节是虚函数表指针vptr,这个指针指向一张名为“虚函数表”(vtbl)的表,表中的数据为函数指针,存储了虚函数fun2()具体实现对应函数的位置,即地址。
普通函数、虚函数、虚函数表都是同一个类的所有对象公有的。
成员变量、虚函数表指针是每个对象私有的。
sizeof()计算的值只包括成员变量和虚函数表指针,所以D有虚函数表指针所以比C多四个字节。
即使类有多个虚函数,也只有一个虚函数表指针,同时虚函数表里会有多个函数指针,指向对应的虚函数实现位置。
虚函数实现的过程是:通过对象内存中的虚函数指针vptr找到虚函数表vtbl,再通过vtbl中的函数指针找到对应虚函数的实现区域并进行调用。
所以,虚函数的调用不由指针的类型决定,而由虚函数指针指向的位置所决定。