C++面经之继承|菱形继承和虚拟继承|一些关于继承的笔试面试题

文章目录

一、继承的概念及定义

1.继承定义

2.继承关系和访问限定符

二、基类和派生类对象赋值转换

三、继承中的作用域

四、派生类的默认成员函数

五、继承与友元

六、继承与静态成员

七、复杂的菱形继承及菱形虚拟继承

菱形继承:菱形继承是多继承的一种特殊情况。

解决菱形继承的问题:虚继承

扫描二维码关注公众号,回复: 16753484 查看本文章

八、继承的总结

九、关于继承的笔试面试题

总结



一、继承的概念及定义

继承机制是面向对象程序设计,使代码可以复用的最重要的手段,它允许程序员在保持原有特性的基础上进行扩展,增加功能,这样产生新的类称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用

//基类 person
class Person
{
    public:
        void Print()
        {
            cout<<"name:" <<_name<<endl;
            cout<<"age:"<<_age<<endl;
        }


    protected:
        string _name = "peter";
        int _age;
};


class Student::Person
{
    protectd:
        int _stuid; //学号;
};

int main()
{
    Student s;
    s.print();
    return 0;
}

1.继承定义

class Student::public Person 

2.继承关系和访问限定符

继承方式分为:public继承,protected继承,private继承

访问限定符:public访问,protected访问,private访问

类成员/继承方式 public继承 protected继承 private继承
基类的Public成员 派生类的public成员 派生类的protected成员 派生类的Private成员
基类的Protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

总结:

  1. 基类的private成员在派生类中无论以什么方式继承都是不可见的,这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象在类内或在类外都无法访问
  2. 基类private成员在派生类中不能被访问。如果基类成员不想在类外被直接访问,但需要在派生类中访问,就定义为protected,可以看出保护成员限定符是因为继承才出现的
  3. 使用关键字class时默认的继承方式是private,使用struct默认的继承方式是Public,最好显示的写出继承方式。
  4. 实际中应用最多的就是public继承

二、基类和派生类对象赋值转换

  • 派生类对象可以赋值给基类对象/基类指针/基类引用。这个叫做切片或者切割。意思就是派生类中父类的那部分赋值过去。
  • 基类对象不能赋值给派生类对象
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针是指向派生类对象时才是安全的。基类如果是多态类型,可以使用RTTI的dynamic_cast进行识别后进行安全转换。

三、继承中的作用域

  • 继承体系中基类和派生类都有独立的作用域
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义)
  • 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  • 在实际中最好不要使用同名成员

四、派生类的默认成员函数

默认成员函数就是程序员不写,编译器会自动生成。这6个默认成员函数是:

初始化和清理:构造函数完成初始化工作,析构函数完成清理工作

拷贝赋值:拷贝构造是使用同类对象初始化创建对象,赋值重载主要是把一个对象赋值给另一个对象

取地址重载:主要是普通对象和const对象取地址,一般很少自己实现

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  2. 派生类的拷贝构造必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 派生类的operator=必须调用基类的operator=完成基类的赋值
  4. 怕派生类的析构函数会在被调用完成后自动调用基类的析构清理基类成员,保证派生类对象先清理派生类成员,再清理基类成员
  5. 派生类对象初始化先调用基类构造再调用派生类构造
  6. 派生类对象析构先调用派生类析构再调用基类析构
  7. 析构函数需要构成重写,重写的条件之一是函数名相同。编译器会对析构函数名进行特殊处理,处理成析构,所以父类析构不加virtual时,子类析构和父类析构构成隐藏关系。

五、继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

六、继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例。

class Person
{
public :
 Person () {++ _count ;}
protected :
 string _name ; // 姓名
public :
 static int _count; // 统计人的个数。
};

int Person :: _count = 0;


class Student : public Person
{
protected :
 int _stuNum ; // 学号
};


class Graduate : public Student
{
protected :
 string _seminarCourse ; // 研究科目
};


void TestPerson()
{
 Student s1 ;
 Student s2 ;
 Student s3 ;
 Graduate s4 ;
 cout <<" 人数 :"<< Person ::_count << endl;  //4

}

七、复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类,这个继承关系称为单继承

多继承:一个子类有两个或者两个以上的父类,这个继承关系称为多继承。

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在assisstant对象中person成员变量会有两份。

class person
{
    public:
        string _name;
};


class Student:public person
{
    protected:
        int _num;
};

class Teacher:public person
{
    protectd: 
        int _id;
};

class Assisstant:public Stduent,public Teacher
{
    protected:
        string _majorCourse;
};

void Test()
{
    a._name = "peer" ; //数据的二义性,不知是student还是teacher的_name
    //解决数据的二义性
    a.Student::_name = "p1";
    a._Teacher::_name = "p2";
    //虽然解决了数据的二义性,使用作用域限定符,但是这样带来了数据冗余的问题,数据冗余的本质是空间浪费
}
class A
{

public:
 int _a;
};
// class B : public A
class B :  public A
{
public:
 int _b;
};
// class C : public A
class C : public A
{
public:
 int _c;
};
class D : public B, public C
{
public:
 int _d;
};
int main()
{
 D d;
 d.B::_a = 1;
 d.C::_a = 2;
 d._b = 3;
 d._c = 4;
 d._d = 5;
 return 0;
}

用内存窗口观察:

解决菱形继承的问题:虚继承

虚继承解决了数据冗余性和二义性的问题,用一个指针去存储一个地址,这个地址里存偏移量。

class A
{

public:
 int _a;
};
// class B : public A
class B : virtual public A
{
public:
 int _b;
};
// class C : public A
class C : virtual public A
{
public:
 int _c;
};
class D : public B, public C
{
public:
 int _d;
};
int main()
{
 D d;
 d.B::_a = 1;
 d.C::_a = 2;
 d._b = 3;
 d._c = 4;
 d._d = 5;
 return 0;
}
D 对象中将 A 放到的了对象组成的最下 面,这个 A 同时属于 B C ,那么 B C 如何去找到公共的 A 呢? 这里是通过了 B C 的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的 A。

八、继承和组合

1.继承和组合

//A和B  继承
class A {};

class B:public A
{};

//C和D 组合
class C
{
    public:
        void fun()
        {}
    protected:
        int _a1;
        int _a2;
};

class D
{
  private: 
        C _cc;
}
  • public继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象
  • 组合是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象
  • 优先使用对象组合,而不是类继承.
  • 继承的耦合度更高,B可以直接用A的成员,D可以直接用C的一个成员函数,间接用另外两个成员,如果修改了A中的成员,B中的都要改。C改了成员,D只需要改它用到的成员

九、关于继承的笔试面试题

1.什么是菱形继承,菱形继承的问题是什么

继承中存在着单继承和多继承,菱形继承属于多继承的一种,菱形继承的问题会导致数据的二义性和冗余性。

2.什么是菱形虚拟继承,如何解决冗余和二义性的

菱形虚拟继承就是在继承时语法加上virtual,通过虚继承,增加一个指针,这个指针是虚基表的地址,通过地址找到偏移量,再找到公共的成员。

3.继承和组合的区别,什么时候用继承,什么时候用组合


猜你喜欢

转载自blog.csdn.net/jolly0514/article/details/132131191
今日推荐