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,刚好记录了当前类到公共区域的距离。

目录
相关文章
|
28天前
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
32 1
【C++】继承
|
2月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
63 1
|
2月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
46 1
|
2月前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
21 0
|
2月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
39 0
|
26天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
42 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
84 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
81 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
89 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4