目录
前言
1. 继承的概念
2. 继承的定义
3. 继承中基类与派生类的赋值转换
4. 继承中的作用域
5. 派生类的默认成员函数
6. 继承与友元、静态成员
7. 多继承与菱形继承
7.1 如何解决
前言
继承是面向对象编程中的一个重要概念,也是面向对象编程语言中普遍存在的特性,本文我将会深入的向大家介绍C++的继承以及继承中菱形继承的问题;
1. 继承的概念
继承是面向对象的程序设计,使代码可以复用的最重要的手段,在使用时我们可以保持原有类特性的基础上进行扩展,增加功能,产生新的类,我们称之为派生类;继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
之前我们在实现函数中,如果存在多个函数中重复相同的操作,我们就会把这个重复操作单独写成一个函数,以便于不同接口的调用;在类当中也存在这样的问题,两个或者多个类同时拥有相同属性,比如:
class Student
{
public:
//...
protected:
string _name;// 姓名
string _gender;// 性别
string _id; // 身份号
int age; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
//...
protected:
string _name;
string _gender;
string _id;
int age;
int _jobid; // 工号
};
他们都拥有一些相同的属性,这时我们就可以把这些属性提取出来,写成一个类Person,用的时候只需继承Person类即可;
2. 继承的定义
继承的定义规则:
前边我们在学习类的时候提到,类有三种访问限定符:
public、protected、private;
它的继承方式也是这三种,继承方式不同,派生类使用基类时的权限也不同:
继承基类成员访问方式的变化
小tips:
这张表我们可以分为两部分:
基类的private成员为一类,它在派生类中不可见(不可见不代表没有继承下来),派生类无法使用(类里边和类外边都无法使用)
基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private
在继承部分才能体现出private和protected的区别
如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected;
继承一个类时没有写继承方式,它们的默认继承方式会因为声明有些不同的,使用的是class,子类继承时默认是private,使用struct时默认的继承方式是public
class Person
{
public :
void Print ()
{
cout<<_name <<endl;
}
protected :
string _name ; // 姓名
string _gender;// 性别
string _id; // 身份号
private :
int _age ; // 年龄
};
//class Student : Person
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
int _stunum ; // 学号
};
3. 继承中基类与派生类的赋值转换
在继承体系中,派生类对象 可以赋值给 基类的对象,包括派生类的指针,引用都可以赋值给基类;这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去;
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//他们中间不会产生临时对象
父子继承是一种 is-a 的关系,子类是一个特殊的父类,子类包含一个父类,所以可以通过切片的方式将派生类对象赋值给父类但父类对象不能赋值给子类
4. 继承中的作用域
. 在继承体系中基类和派生类都有独立的作用域;子类创建的对象可以在类外部调用父类的public成员函数;
当子类和父类中有相同的成员时,子类对象调用时,优先调用自己的成员;子类成员会屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义;
class Person
{
public:
void fun(){
cout << "父类" << endl;
}
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
// 当子类和父类中有两个相同变量时,可以通过指定类域进行访问
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
void fun(){
cout << "子类" << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s;
s.Print();
//默认调用的是子类的成员
//可以通过指定类域的方式进行调用
s.Person::fun();
return 0;
}
但是我们也可以通过指定类域的方法去调用父类的成员; 继承中,同名的成员函数,函数名相同就构成隐藏,不管参数和返回值
注意:
在实际中应用中,继承体系里面最好不要定义同名的成员!
5. 派生类的默认成员函数
现在需要结合前边类的默认成员函数,这部分相对较为复杂;
class Person
{
public:
Person(const char* name = "peter")
: _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:
protected:
int _id;
};
int main()
{
Student s1;
return 0;
}
我们实现一个父类,Student继承Person,Student什么都不实现,编译器会默认生成的构造函数和析构函数;默认的构造函数和析构函数会去调父类的构造和析构;
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员;
- 派生类对象析构先调用派生类析构再调基类的析构(保证派生类对象先清理派生类成员再清理基类成员的顺序);
- 派生类对象初始化先调用基类构造再调派生类构造
要实现子类的构造函数,来对子类对象进行初始化:
Student(const char* name, int id)
:_id(id)
, Person(name) // _name(name)不可以
{
cout << "Student(const char* name, int id)" << endl;
}
Student s1("张三", 18);
- 对子类对象进行初始化,同时也需要传参数对父类进行初始化
- 对父类进行初始化时需要注意,子类是父类的衍生类,
- 在对父类部分进行初始化时,要把父类看作一个单独的类进行初始化(对父类整体进行初始化)
- 不可以单独对父类成员进行初始化
拷贝构造:
Student(const Student& s)
:Person(s)
, _id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
Student s2(s1);
使用s1进行初始化,对父类进行初始化时可以直接使用子类进行赋值,衍生类在向基类赋值时会有切片(切割),基类会截取对基类部分初始化的数据;这里也体现出了切片的意义;
赋值重载:
Student& operator=(const Student& s)
{
if (&s != this)
{
// 注意这里需要指定类域,不然会构成隐藏,一直调用子类的operator=
Person::operator=(s);
_id = s._id;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
Student s3("李四", 19);
s1 = s3;
派生类的operator=必须要调用基类的operator=完成基类的复制;
派生类的析构:
~Student()
{
//Person::~Person();
//父类的析构不可以显示的调用比如:~Person();
cout << "~Student()" << endl;
}
这是因为父子类的析构函数构成隐藏,由于多态的原因,析构函数统一会被处理成destructor,如果想要调用就必须指定类域;但是调用后又会很奇怪,每个派生类对象调用了两次基类的析构函数,父类析构函数不需要显示调用,子类析构函数结束时会自动调用父类析构,保证析构先子后父(构造函数先父后子),我们调用的父类析构函数是多余的;(如果子类显式调用就会造成析构先父后子,存在风险)
总结就是派生类中有一个基类,这个基类需要把它当成一个单独的类来处理;
6. 继承与友元、静态成员
继承与友元只有一个需要注意的点:
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
继承与静态成员:
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例
7. 多继承与菱形继承
前边我们玩的都是单继承,单继承:一个子类只有一个直接父类时称这个继承关系为单继
现在我们来聊一聊多继承,多继承可以说是C++的一个不好的设计,它又会引发菱形继承的问题;
什么是多继承?
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
菱形继承的问题:从下面的对象成员模型构造,课以看出他们的继承关系,Student和Teacher继承了Person(Student和Teacher内部都有一个Person),Postgraduate又继承了Student和Teacher,Postgraduate内部就会有两个Person;编译器到底用哪个Person?
从这里可以看出菱形继承有数据冗余和二义性的问题。
7.1 如何解决
C++有具体的应对措施,那就是虚拟继承(虚继承),在继承时加上virtual;
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 ()
{
//使用虚继承后他们用的就是同一Person
Assistant a ;
a._name = "peter";
}
注意virtual不是随便加的,它有具体的添加位置;virtual要添加在派生类对基类的继承声明中的类名之前;
C++的语法设计复杂,多继承就是一个体现,有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题;
8. 组合
继承一般不会单独的使用,因为继承的使用会增加程序的耦合度,程序设计要求的是高内聚,低耦合,就是减少代码之间的关联度;C++的继承都是配合多态一起使用;介绍了继承,那就不得不提一下组合;
- 继承是is - a的关系:每个派生类对象都是一个基类对象;
- 组合就是has - a的关系:假设B组合了A,每个B对象中都有一个A对象;;
class A
{
protected:
int data;
};
class B
{
protected:
double x;
A _a;
};
类之间的关系可以用继承,也可以用组合,但能用组合最好还是用组合,优先使用组合!
总结
继承是C++的一个重要的概念,继承几乎不会单独使用,更多的是配合多态使用,要实现多态,必须需要继承;下期我将会向大家介绍C++的多态;好了以上便是本文的全部内容,希望可以对你有所帮助,感谢阅读!