C++中的继承/虚继承原理

简介: C++中的继承/虚继承原理

C++中的继承

1.继承的概念和定义

继承是一种提高代码复用率的重要方式,它允许程序员保持原有类的特性的基础上去增加其他特性、功能,这样的类叫做派生类继承是类设计层次的复用

class Person
{
public:
  void Print()
  {
    cout << "name: " << _name << endl;
    cout << "age: " << _age << endl;
  }
protected:
  string _name = "牡丹";
  int _age = 18;
};
class Student :public Person
{
protected:
  int _stuid;
};
class Teacher :public Person
{
protected:
  int _jobid;
};
int main(void)
{
  Student s;
  Teacher t;
  s.Print();
  t.Print();
  return 0;
}

运行结果为:

明明Student和Teacher当中都没有Print()函数啊,为什么还是正常运行呢?因为它们都继承了Person类。

1.1 继承定义

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。

1.12 继承关系和访问限定符

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见(隐藏) 在派生类中不可见(隐藏) 在派生类中不可见(隐藏)

总结

1.基类的private成员在派生类当中无论是什么继承方式,都是不可见的,是隐藏的,在类外,在派生类当中都是无法调用到private的成员,都是隐藏的。

2.private成员在派生类中是不能被访问的,但是如果想要在类外不能被访问,但是在派生类当中被访问到就可以使用protected

3.public > protected > private,基类的其他成员在子类的访问方式 == 权限小的那个

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的

写出继承方式

5.在实际运用中一般使用都是publicb继承,几乎很少使用protetced/private继承,也不提倡使用

protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中

扩展维护性不强。

2.基类和派生类对象的复制转换

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。有一种很形象的说法叫做切片或者切割。就是把派生类当中父类的那部分切来赋值过去。
  • 基类的对象不能赋值给派生类
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才
    是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来
    进行识别后进行安全转换。

class Person
{
protected:
  string _name; // 姓名
  string _sex; // 性别
  int _age; // 年龄
};
class Student : public Person
{
public:
  int _No; // 学号
};
void Test()
{
  Student sobj;
  // 1.子类对象可以赋值给父类对象/指针/引用
  Person pobj = sobj;//Person = Student
  Person* pp = &sobj;//Person = Student
  Person& rp = sobj;//Person = Student
  //2.基类对象不能赋值给派生类对象
  sobj = pobj;//Student = Person
  // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
  pp = &sobj;
  Student * ps1 = (Student*)pp; // 这种情况转换时可以的。Student* = (Student*)Person*
  ps1->_No = 10;
  pp = &pobj;//Person =Person
  Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
  ps2->_No = 10;
}

3.继承中的作用域

1.在继承体系中基类派生类都有独立的作用域

2.子类和父类中有同名的成员,子类成员和父类成员的关系叫做隐藏,如果非要访问父类的成员,可以添加访问限定符显示访问

3.函数只需要函数名相同就能构成隐藏,参数什么的无关。

4.最好不要定义同名的成员

class Person
{
public:
protected:
  string _name="牡丹";
  int _num=18;
};
class Student :public Person
{
public:
  void Print()
  {
    cout << "姓名:" << _name << endl;
    cout << "学号:" << _num << endl;
    cout << "学号:" << Person::_num << endl;
  }
protected:
  int _num = 520;
};
int main(void)
{
  Student s1;
  s1.Print();
  return 0;
}

运行结果:

class Person
{
public:
  void Print(int a)
  {
    cout << "姓名:" << _name << endl;
    cout << "学号:" << _num << endl;
  }
protected:
  string _name="牡丹";
  int _num=18;
};
class Student :public Person
{
public:
  void Print()
  {
    Person::Print(1);
    cout << "姓名:" << _name << endl;
    cout << "学号:" << _num << endl;
    cout << "学号:" << Person::_num << endl;
  }
protected:
  int _num = 520;
};
int main(void)
{
  Student s1;
  s1.Print();
  return 0;
}

这里构成的是函数的隐藏而不是重载,因为不在同一个作用域

4.派生类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个

成员函数是如何生成的呢?

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

2.派生类的拷贝构造必须调用基类的拷贝构造完成基类的初始化。

3.派生类的operator=必须要调用基类的operator=完成基类的复制。

4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类

对象先清理派生类成员再清理基类成员的顺序。

5.派生类对象初始化先调用基类构造再调派生类构造。

6.派生类对象析构清理先调用派生类析构再调基类的析构。

class Person
{
public:
  Person(const char* name = "牡丹")
    :_name(name)
  {
    cout << "Person()" << endl;
  }
  Person(const Person& p)
    :_name(p._name)
  {
    cout << "Person(const Person& p)" << endl;
  }
  Person& operator=(const Person& p)
  {
    cout << "Person& operator=(const Person& p)" << endl;
    if (this != &p)
    {
      _name = p._name;
    }
    return *this;
  }
  ~Person()
  {
    cout << "~Person()" << endl;
  }
protected:
  string _name;
};
class Student : public Person
{
public:
  Student(const char* name, int num)
    : Person(name)
    , _num(num)
  {
    cout << "Student()" << endl;
  }
  Student(const Student& s)
    : Person(s)
    , _num(s._num)
  {
    cout << "Student(const Student& s)" << endl;
  }
  Student& operator = (const Student& s)
  {
    cout << "Student& operator= (const Student& s)" << endl;
    if (this != &s)
    {
      Person::operator =(s);
      _num = s._num;
    }
    return *this;
  }
  ~Student()
  {
    cout << "~Student()" << endl;
  }
protected:
  int _num; //学号
};
void Test()
{
  Student s1("jack", 18);
  Student s2(s1);
  Student s3("rose", 17);
  s1 = s3;
}
int main(void)
{
  Test();
  return 0;
}

这段代码:

实例化对象s1的时候会先去调用Student的构造函数,然后再Student的构造函数当中显示的调用了父类的构造函数。
实例化对象s2的时候先调用Student的拷贝构造,在拷贝构造的初始化列表当中显示的调用父类的拷贝构造
实例化s3的时候和s1一样
s1赋值给s3的时候先去调用子类的operator=,但是在子类当中会调用父类的operator=

继承与友元

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

Display是基类Person的友元,假如Student把Person当中的友元关系也给继承了,那么这里的s._stuNum是可以访问的,但是结果的不可访问,所以友元关系是不能被继承的

6.继承与静态成员

基类定义了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;
 Student ::_count = 0;
 cout <<" 人数 :"<< Person ::_count << endl;
}

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

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

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

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

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

class Person
{
public:
  string _name; // 姓名
};
class Student : public Person
{
protected:
  int _num; //学号
};
class Teacher : public Person
{
protected:
  int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
  string _majorCourse; // 主修课程
};
void Test()
{
  // 这样会有二义性无法明确知道访问的是哪一个
  Assistant a;
  a._name = "peter";
  // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
  a.Student::_name = "xxx";
  a.Teacher::_name = "yyy";
}

虚继承可以解决菱形继承的二义性和数据冗余问题。

class Person
{
public:
  string _name; // 姓名
};
class Student :virtual public Person
{
protected:
  int _num; //学号
};
class Teacher : virtual public Person
{
protected:
  int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
  string _majorCourse; // 主修课程
};
void Test()
{
  // 这样会有二义性无法明确知道访问的是哪一个
  Assistant a;
  a._name = "peter";
  // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
  a.Student::_name = "xxx";
  a.Teacher::_name = "yyy";
}
int main(void)
{
  Test();
  return 0;
}

这样,指向的_name就都是同一个_name了。

7.虚继承解决数据冗余和二义性的原理

可以看到,虚继承的成员都放在公共区了,但是这里又多了两个地址,看起来像是两个指针

可以看到,这里有一个140c,14的16进制就是20,c就是12,刚好记录了当前类到公共区域的距离。

目录
相关文章
|
8天前
|
C++
C++(二十)继承
本文介绍了C++中的继承特性,包括公有、保护和私有继承,并解释了虚继承的作用。通过示例展示了派生类如何从基类继承属性和方法,并保持自身的独特性。此外,还详细说明了派生类构造函数的语法格式及构造顺序,提供了具体的代码示例帮助理解。
|
22天前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
33 0
|
2月前
|
Java 编译器 程序员
C++中的语法知识虚继承和虚基类
**C++中的多继承可能导致命名冲突和数据冗余,尤其在菱形继承中。为解决这一问题,C++引入了虚继承(virtual inheritance),确保派生类只保留虚基类的一份实例,消除二义性。虚继承通过`virtual`关键字指定,允许明确访问特定路径上的成员,如`B::m_a`或`C::m_a`。这样,即使基类在继承链中多次出现,也只有一份成员副本,简化了内存布局并避免冲突。虚继承应在需要时提前在继承声明中指定,影响到从虚基类派生的所有后代类。**
51 7
|
30天前
|
安全 Java 编译器
|
2月前
|
存储 Java 程序员
【c++】继承深度解剖
【c++】继承深度解剖
25 1
|
2月前
|
存储 编译器 数据安全/隐私保护
|
2月前
|
Java C++ 运维
开发与运维函数问题之C++中有哪些继承方式如何解决
开发与运维函数问题之C++中有哪些继承方式如何解决
21 0
|
2月前
|
存储 编译器 C++
C++基础知识(六:继承)
多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。
|
8天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)
|
8天前
|
C++
C++(十六)类之间转化
在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。