目录
继承
1.继承的概念及定义
概念:
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(或叫做‘子类’)。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
先看代码示例子(后面有详细讲解)test:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
// 这里体现出了 Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,
// 可以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person //继承Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person //继承Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
输出:
定义
继承基类成员访问方式的变化:
类成员/继承方式 | PUBLIC继承 | PROTECTED继承 | PRIVATE 7* |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的protected 成员 | 派生类的protected 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的private成 员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可 见 |
代码示例:
定义父类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl<<endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
Student、Teacher对父类(Person)进行继承:
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
继承的总结:
-
基类private成员无论以什么方式继承到派生类中都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
-
基类private成员在派生类中不能被访问,如果基类成员不想在派生类外直接被访问,但需要在派生类中访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
-
基类的私有成员在子类都是不可见;基类的其他成员在子类的访问方式就是访问限定符和继承方式中权限更小的那个(权限排序:public>protected>private)。
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,但最好显式地写出继承方式。
2.子类和父类(基类和派生类)
先看代码:
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl<<endl;
}
protected:
string _name = "father"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
int main()
{
Student s;
Person p;
cout<<'s'<<endl;
s.Print();
p=s;
p.Print();
return 0;
}
我们对代码进行监视:
为什么我们进行了'p=s'的操作而p中却不存在_studi 这是因为发生了'切片'
因为父类中没有_studi,所以父类接收子类传过来的 _name和 _age之后,多余的 _studi就不管了。但是如果父类传给子类,少传一个,所以会报错。
正确的赋值方式:
-
Student s; Person p; p=s;
-
//引用 Student s; Person& p=s;
-
//指针 Student s; Person* p=&s;
Person指向子类的首元素地址,因为是父类,所以也只能操作父类的_name和 _age
3.继承中的作用域
-
在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
-
注意在实际中在继承体系里面最好不要定义同名的成员。
同名成员变量:
同名成员函数:
这就构成了隐藏。(函数重载是在同一个作用域,这里父类和子类是两个作用域)
虽然成员函数的隐藏,只需要函数名相同就构成隐藏,对参数列表没有要求。
4.派生类的默认成员函数
4.1构造函数
编译器会默认先调用父类的构造函数,再调用子类的构造函数,如下
class Person
{
public:
Person(string name = "Father") // 先调用:父类默认构造调用一个print打印name
: _name(name)
{
cout << name << endl;
}
protected:
string _name;
};
class student : public Person
{ // 后调用:子类默认构造调用一个print打印name和age
public:
student(string name, int age)
: _age(age)
{
cout << name << " "
<< age << endl;
}
protected:
int _age;
};
int main()
{
student st("Son", 18);
return 0;
}
输出结果:
假如父类失效
Person(string name)//你这里不传值,那么就不能完成初始化,相当于父类失效
:_name(name)
{
cout << name << endl;
}
那么就必须在子类中给父类构造赋值
student(string name,int age)
:_age(age)
, human(name)//新增,子类以自己的name给父类的析构中的name赋值,age和name的顺序随意变动
4.2析构函数
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(). 所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。(为了防止子类进行析构时出现内存泄漏的情况)
class Person
{
public:
Person(string name = "Father") // 先调用:父类默认构造调用一个print打印name
: _name(name)
{
cout << name << endl;
}
~Person()
{
cout << "~Person" << endl;
}
protected:
string _name;
};
class student : public Person
{ // 后调用:子类默认构造调用一个print打印name和age
public:
student(string name, int age)
: _age(age)
{
cout << name << " "
<< age << endl;
}
~student()
{
cout << "~student" << endl;
}
protected:
int _age;
};
不要在子类中调用父类的析构,如果是指针类型,那么同一块区域被析构两次就会造成野指针的问题
4.3拷贝构造
子类中调用父类的拷贝构造时,直接传入子类对象即可,父类的拷贝构造会通过“切片”拿到父类的那一部分。
student(student &s)
: Person(s) // 直接将st传过来通过切片拿到父类中的值
,
_age(s._age) // 拿除了父类之外的值
{
cout << s._age << endl << s._name << endl;
}
4.4赋值运算符重载
子类的operator=必须要显式调用父类的operator=完成父类的赋值。
因为子类和父类的运算符,编译器默认给与了同一个名字,所以构成了隐藏,所以每次调用'='这个赋值运算符都会一直调用子类,会造成循环,所以这里的赋值要直接修饰限定父类
class Person
{
public:
Person(string name = "Father")
: _name(name)
{ }
Person &operator=(const Person &p)
{
if (this != &p)
{
cout << "调用父类" << endl;
_name = p._name;
}
return *this;
}
protected:
string _name;
};
class student : public Person
{
public:
student(string name, int age)
: _age(age)
{ }
student &operator=(const student &s)
{
if (this != &s)
{
cout << "调用子类" << endl;
Person::operator=(s); // 必须调用父类运算符
_age = s._age;
_name = s._name;
}
return *this;
}
protected:
int _age;
};
int main()
{
student st("小红", 18); student st3("小刚", 16);
st = st3;
return 0;
}
5.继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
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;
cout<<"人数 :"<<Person::_count<<" ";
Student s2;
cout<<Person::_count<<" ";
Student s3;
cout<<Person::_count<<" ";
Graduate s4;
cout<<Person::_count<<" ";
Student ::_count = 0;
cout <<endl<< "人数 :" << Person ::_count << endl;
}
输出:
7.菱形继承及菱形虚拟继承
单继承
一个子类只有一个直接父类时称这个继承关系为单继承
多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Graduate的对象中Person成员会有两份。
class A {
public:
string name;
};
class B :public A {
public:
int age;
};
class C :public A {
public:
string sex;
};
class D :public B, public C {
public:
int id;
};
int main()
{
D student;
student.name = "小明";
student.age = 18;
student.sex = "男";
student.id = 666;
return 0;
}
解决办法:
1.加修饰符限定
2.虚继承:在继承方式前加上virtual
class B :virtual public A {
public:
int age;
};
class C :virtual public A {
public:
string sex;
};
单继承和多继承的总结:
多继承是C++复杂的一个体现。有了多继承,就存在菱形继承,为了解决菱形继承,又出现了菱形虚拟继承,其底层实现又很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。
8.虚继承
什么是虚继承?
所谓虚继承(virtual)就是子类中只有一份间接父类的数据。该技术用于解决多继承中的父类为非虚基类时出现的数据冗余问题,即菱形继承问题。
此时的内存对象模型:数据冗余
而当我们使用虚继承时,结构是下图这样,D中只有一份父类A,当我们调用A中数据时,并不会发生冗余。
虚继承原理
在上图中,父类数据并不存放在虚继承的子类中,那么子类怎么找到父类数据呢?
——在虚继承的类中,会定义一个虚基表指针vbptr,指向虚基表。
而虚基表中会存在偏移量,这个量就是表的地址到父类数据地址的距离。
我们可以通过调试,找到虚基表指针和虚基表
首先,我们为每一个数据赋值,以便观察:
class A {public:int _a;};
class B : virtual public A {public: int _b;};
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对象地址和d中a数据地址:
看d中内存分布:
由此我们可以分析得到对象d及其内部父类的内存布局:
在此可以看到分别在class B和 class C中存放了一串并不是自己输入的数字,这就是虚基表指针
我们进行监视两个地址(这里以b中虚基表为例):
问:为什么bptr和cptr能够找到并不位于自己内部的变量a?
因为bptr和cptr都对d进行了切片,当各自寻找变量a时,会从自身的虚基表指针中找到虚基表,通过虚基表的偏移量找到变量a的地址,从而找到了变量a。
9.虚继承使用注意事项
当使用虚继承的时候,需要注意,虚继承只有在多继承时才有用。也就是说如果只有一层继承关系或者是单继承都将不起作用。
因为虚继承是保证子类中只有一个间接父类,说简单一点就是虚继承只能在隔代继承中起作用。
比如下面两种情况即便虚继承也没有意义:
(1)是因为虽然虚继承产生了虚基表和指针,但是class B并没有子类,而虚继承是用以保证子类只有一个间接父类class A。当然话说回来,就算有子类、哪怕多个子类,也都体现不出虚继承,因为虚继承要求同一个子类的多个父类继承自同一个间接父类,而该例只有一个父类class B。
(2)是因为虽然class C虚继承了class B,但是class B是class A的非虚继承,那么B中就会有一份A。而class D对A是虚继承,就导致E在实例化时会存放一个对D而言公共的A。这样E中还是存放了两个A。调用变量时还是会混淆。
这样说可能还有些难懂,那换个说法,class B中没有虚基表指针,而D中有虚基表指针,当E从D调用int a时会从虚基表指针找到公共区域的A,而E从B中找只会在B
正确的继承关系应该是当class A的子类继承时,都是虚继承,这才能保证当有像class E这样的间接子类定义时,class在其中都只会在公共区域有一份。对本例来说即class B是虚继承。