小言_互联网的博客

大厂面试重要C++知识(一)—— C++面向对象特性

318人阅读  评论(0)

一、面向对象

面向对象的三大特点:封装继承多态。当然讲四大特点可以加上抽象

二、封装

定义:将数据和信息包装在单个单元中。在面向对象的编程中,封装被定义为将数据和操纵它们的功能绑定在一起。

比如,一家公司有销售部门、技术部门、财务部门,销售部门有对应销售数据,而如果财务部门想要查看销售部门的数据时,需要先通过销售部门的人员才能得到相应的数据。而这就是封装,外部人员访问相关部门时需要通过某种渠道、接口才能够获得数据。

而在代码中,具体体现在的编写过程中,有private、public、protected修饰符。定义在private中的成员变量,外部只能够通过友元或类对外提供的接口访问。其中,友元函数、友元类会破会类的封装性,一般不使用。

三、继承

  • 定义:一个类(子类)从另一个类(父类)获得成员函数和成员变量的过程。

  • C++有三种继承:public、private、protected,如果不写参数默认为private。一般在工程中只采用public继承。

  • C++支持多继承,多继承功能强大,使得C++不需要像java、C#引入接口。接口:一个不能实例化,且只能拥有抽象方法的类型。接口为没有亲缘关系的类提供通用功能。当然多继承使用起来也比较麻烦,可能带来几个问题:

    • 菱形继承。D继承B和C,而B和C同时继承A。如果A中有个成员变量a,那么D无法知道是从A->B->D来的a还是A->C->D来的a。当然可在前面用域运算符(::)指明是哪个元素:B::a,C::a。

  • 为了解决多继承时的命名冲突冗余数据,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中的函数指针找到对应虚函数的实现区域并进行调用。

  所以,虚函数的调用不由指针的类型决定,而由虚函数指针指向的位置所决定。

纯虚函数

  • 纯虚函数可以看做一个抽象的个体,例如动物可能有猫、狗、老虎等,但动物本身就是一个抽象的概念,不存在诸如吃、叫等方法的实现,所以可以将吃、叫等方法定义为纯虚函数,基类不实现其方法,由派生类实现。
  • 含有纯虚函数的类称之为抽象类,它不能生成对象(创建实例),只能创建它的派生类的实例。
  • 子类实现了纯虚函数,该纯虚函数在子类中就变成了虚函数
  • 如果派生类中没有重新定义实现纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。
  • 纯虚函数可以理解为提供一个接口,基类无法实现该接口,由继承的派生类实现对应的操作。

五、参考文章


转载:https://blog.csdn.net/aruewds/article/details/117091442
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场