继承
继承是面向对象复用的重要手段。通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同
的东
西。
下面是一个最简单的继承关系
:
继承是一种复用手段,在继承关系里基类继承派生类的成员,由此达到复用的目的。
继承分为3种继承方式分别为:公有继承,私有继承,保护继承。
三种继承关系下基类成员的在派生类的访问关系变化:
这些都是基础的东西,这些东西一定要牢牢的记住,后面的可能会后一点绕~先不说那么多了,对
概念总结一下吧。
基本概念
1.基类的私有成员在派生类中是不能被访问的,如果一些基类成员不想被基类对象直接访问,但需要在派生类中能访问,就定义为
保护成员。可以看出保护成员限定符是因继承才出现的。
2.public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
3.protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a 的关系原则,所以非特殊情
况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
4.不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不
能访问)
5.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
6.在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.
接下来我们需要开始进入继承有点难的地方了,首先是切片。
1.子类对象可以赋值给父类对象(切割/切片)
2.父类对象不能赋值给子类对象
3.父类的指针/引用可以指向子类对象
4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
对于这几种情况我们来进行代码演示:
首先是第一,二条赋值问题的演示:
class Person
{
public:
void Display()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num; // 学号
};
void Test()
{
Person p;
Student s;
// 1.子类对象可以赋值给父类对象(切割 /切片)
p = s;
// 2.父类对象不能赋值给子类对象
//s = p;
}
int main()
{
Test();
system("pause");
return 0;
}
接下来是对第三条的演示:
class Person
{
public:
void Display()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num; // 学号
};
void Test()
{
Person p;
Student s;
// 3.父类的指针/引用可以指向子类对象
Person* p1 = &s;
Person& r1 = s;
}
int main()
{
Test();
system("pause");
return 0;
}
对第4个进行验证:
首先我们应该很容易知道子类的指针不能够指向父类,指向派生类的指针,因为内存空间比基类长,
会导致严重了后果(内存越界),所以不允许派生类的指针指向基类。
但是我们可以强制类型转换将父类强制类型转化为子类,然后让子类的指针指向父类。
然后你觉得下面代码运行出来是什么东西?
class Person
{
public:
void Display()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num; // 学号
};
void Test()
{
Person p;
Student s;
// 4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
Student* p2 = (Student*)& p;
Student& r2 = (Student&)p;
p2->_num = 10;
r2._num = 20;
}
int main()
{
Test();
system("pause");
return 0;
}
继承体系中的作用域
1.在继承体系中基类和派生类都有独立的作用域。
2.子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数
中,可以使用基类::基类成员 访
问
)
--隐藏 --重定义
3.注意在实际中在继承体系里面最好不要定义同名的成员。
注意这里的隐藏,他是只要名字一样就会被隐藏,并不会是什么返回值还有参数都相等的时候,他才隐藏他就是只要
名字一样,就
被隐藏。
class Person
{
public :
Person(const char* name = "", int id = 0)
: _name(name )
, _num(id)
{}
protected :
string _name ; // 姓名
int _num ; // 身份证号
};
class Student : public Person
{
public :
Student(const char* name, int id, int stuNum )
: Person(name , id )
, _num(stuNum)
{}
void DisplayNum ()
{
cout<<" 身份证号: "<<Person ::_num<< endl;
cout<<" 学号"<< _num<<endl ;
}
protected :
int _num ; // 学号
};
void Test()
{
Student s1 ("paul", 110, 1);
s1.DisplayNum ();
};
派生类的默认构造函数
单继承和多继承
现在这个就是C++与java的不同了,java并没有多继承,C++的多继承也是他的诟病之一,这一块也是非常难
理解的。多继承
平
时使
用起来也是非常复杂,所以不建议使用,菱形继承你用的就要更少。
那么下来我们来看何为单继承,多继承,菱形继承。
前两种可能容易理解一点,但是菱形继承这里的问题就很大,为什么这样说呢?
现在我们先看看这个例子:
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.Student::_name = "听风";
a.Teacher::_name = "听檒";
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
这个编译可以通过的所以讲,所以编译器认可一个人拥有两个名字,这个就是菱形继承 的诟病。
也就是我们即将要说的
二义性
和
数据冗余
。
遇到问题解决问题,那么C++如何解决这个问题的呢?
这也就引出虚继承的概念:
何为虚继承,虚继承--解决菱形继承的二义性和数据冗余的问题。
1.虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2.虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万
不得已都不要定义菱形结构的
虚继承
体系结构,因为使用虚继承解决数据冗余问题也带来了
性能
上的损耗
。
我们现在看一个新的代码,我们现在使用虚继承来看看。
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public C, public B
{
public:
int _d;
};
int main()
{
D dd;
cout << sizeof(dd) << endl;
dd.B::_a = 1;
dd._b = 3;
dd.C::_a = 2;
dd._c = 4;
dd._d = 5;
system("pause");
return 0;
}
现在看他的监视窗口:
我们现在发现只要更改一个值,所有_a都改变了,二义性也就算是解决了,这是为什么呢?
虚继承到底做了什么?
现在我们来看虚继承的对象模型:
我们具体为什么不直接用指针而用用的是偏移量呢? 这个以后会说到,以后多态的时候会讲到。。。
有的人可能不明白这里二义性是解决了但是呢数据冗余并没有解决啊,_a耗费的空间好像比以前多了啊
这怎么反倒是使用空
间浪费
了呢?,但是你想一想如果_a是一个double类型呢并且该对象中
有3个 _a 呢?
简单算一下如果
不使用虚继承_a总共
耗费8+8+8=24个
字节,但是你使用了虚继承那就是那3个_a都存
的是
地址,也就是
4
个字节,
4+4+4+8=20个字节,所以这个方
法节约空间是相对的,
所
以当你的模型够大时解决
数据冗余才会有效。
现在我们针对菱形继承,还有继承的特性来做一道题:
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public C, public B
{
public:
int _d;
};
int main()
{
D dd;
cout << sizeof(dd) << endl;
dd.B::_a = 1;
dd._b = 3;
dd.C::_a = 2;
dd._c = 4;
dd._d = 5;
B bb;
C cc;
cout<<sizeof(bb)<<endl;
bb = dd; // 切片
cc = dd;
A* pa = ⅆ
B* pb = ⅆ
C* pc = ⅆ
D* pd = ⅆ
cout << pa << endl;
cout << pb << endl;
cout << pc << endl;
cout << pd << endl;
system("pause");
return 0;
}
通过打印出来的内容,我们看到我们对指针的分布应该没有任何问题,由pd到pa地址一次增大。
我们还看到D类型和B类型的长
度可
以看到分别为 24 和 12,我们看看上图D类型的模型,我们
来算一算对不对?
4(address)+ 4 + 4(
address
)+ 4 + 4 + 4=24. B
类型就不算了,都很简单~。
最后再补充两点
1.友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
2.基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一
个static成员实例。