继承

来源:互联网 发布:重装系统无法安装软件 编辑:程序博客网 时间:2024/05/29 10:57
1.定义
面向对象的继承就是:
从一个类派生另外一个类,使父类的所有的特征在子类中自动可用。这个类可以声明一些类型,这些类型可以共享部分或全部以前所声明的类型,它也可以从超过一个的基类型共享一些特征。继承是面向对象复用的重要手段。通过继承定义一个类,继承类型之间的关系模型,共享共有的东西,实现各自本质的不同的东西。
2.分类
一.继承的关系及继承访问限定符
1.下面是三种继承关系下基类成员在派生类中的访问变化
2.保护继承与私有继承的区别
private和protected都是限定直接访问,他们有什么区别?
  1. #include<iostream>
  2. using namespace std;
  3. class person //父类/基类
  4. {
  5. public:
  6. person()
  7. {
  8. cout << "person" << endl;
  9. }
  10. protected:
  11. int id;
  12. private:
  13. int _age;
  14. };
  15. //class student:protected person
  16. //class student:private person
  17. class student :public person //class 子类/派生类: 继承关系 父类/基类
  18. {
  19. public:
  20. student()
  21. {
  22. cout<< person::id << endl;
  23. cout << _num << endl;
  24. }
  25. public:
  26. int _num;
  27. };
  28. int main()
  29. {
  30. person p;
  31. student s;
  32. return 0;
  33. }
下图发现类可以直接调用父类的id和person(),而不能调用父类中私有的_age成员

这里程序可以正常运行,但是子类中不可以输出cout<<person::_age<<endl;,因为_age是父类中的私有成员(private),不可在类外直接访问;而id是父类中的保护成员(protected),id可以被子类继承后直接访问,但不可以在子类和父类外的其它地方直接使用;从而可以看出来protected访问限定符是为继承而产的。
要想访问类中的私有成员,只能通过类中的其它访问权限的方法调用成员,然后在类外使用。
3.区别隐藏/重定义、重载、重写/覆盖
隐藏/重定义 :子类与父类的同名成员,子类隐藏父类的成员,在访问是按照就近原则。 想要指定访问,必须指定作用域
重载:(前提:在同一个作用域)函数名相同,参数不同或者返回值可以不同。
重写/覆盖:在子类定义一个与父类完全相同的虚函数,要求返回值、函数名、参数列表都相同。
构造是先构造父类后构造子类,析构要先析构子类后析构父类
总结
1.(protected)基类的私有成员在派生类中是不能被访问的.如果一些基类成员不想被基类对象直接访问.但需要在派生类中能访问.就定义为保护成员。可以看出保护成员限定符是因继承才出现的。
public继承 is-a
一个子类就是一个父类,子类继承的父类在子类里面访问限定符不变。 子类可以赋给父类对象/指针/引用,会发生切片/切割
private/rotectd继承 has-a
一个子类里面有一个父类,子类继承的父类在子类里面访问限定符发生变化。 子类就无法赋给父类。
2. (is-a)public继承是一个接口继承,保持is-a原则.每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
3. (has-a)protected / private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
4.不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员但是基类的私有成员存在但是在子类中不可见(不能访问)
5.使用关键字 class时默认的继承方式是 private.使用 struct时默认的继示方式是 public.不过最好显示的写出继承方式
6.在实际运用中一般使用都使用 public继承,极少场景下才会使用 protected和private继承




二.赋值兼容规则
看下面示例代码:
[html]view plaincopy
  1. #include<iostream>
  2. using namespace std;
  3. class person
  4. {
  5. public:
  6. void Display()
  7. {
  8. cout << "person()" << endl;
  9. }
  10. protected:
  11. string _name;
  12. private:
  13. int _age;
  14. };
  15. class student :public person
  16. {
  17. public:
  18. int _num;
  19. };
  20. int main()
  21. {
  22. person b;
  23. student a;
  24. //子类对象可以赋值给父类对象(切片/切割)
  25. b = a; //yes
  26. //父类对象不可以赋值给子类对象
  27. //a = b; //no
  28. //父类指针/引用可以指向子类对象
  29. person* p1 = &a;
  30. person& p1 = a;
  31. //子类指针/引用不能指向父类对象(可以通过强制类型转换来指向)
  32. //student* s1 = &b; //no
  33. student* s1 = (student*)&b; //yes
  34. //student& s2 = b; //no
  35. student& s2 = (student&)b; //yes
  36. return 0;
  37. }
可以总结:
子类对象可以赋值给父类对象(切片/切割)
父类对象不可以赋值给子类对象
父类指针 / 引用可以指向子类对象
子类指针 / 引用不能指向父类对象(可以通过强制类型转换来指向)
下面我们一条一条解释
那什么是切片/切割呢?看下图

子类可以赋值给父类,是因为子类里边包含父类里的全部成员变量,所以可以通过切片把父类的成员都赋予相应的值
父类赋给子类为什么不可以呢?是因为父类里面没有子类独有的那部分变量,所以无法给子类独有的成员变量赋值,因此不能用父类给子类赋值

父类指针 / 引用可以指向子类对象



现在我们知道子类指针 / 引用不能指向父类对象,因为子类比父类空间大,所以会越界,那强制类型转换会出现什么情况呢?
如果上面代码的main函数内加上以下这两句代码程序会是什么反应呢?
s1->_num = 10;
s2._num = 20;
解释如下图所示:

继承体系中的作用域:
1.在继承体系中基类和派生类都有独立的作用域。
2.子类和父类中有同名成员、子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用基类:基类成员访问)-隐藏-重定义
3.注意在实际中在继承体系里面最好不要定义同名的成员。


派生类中的默认成员函数
在继承关系里,如果派生类没有显示的定义6个默认成员函数(构造函数,拷贝构造,析构函数,赋值运算符重载,取地址操作符重载,const修饰的取地址操作符重载),编译系统则会默认的的合成这6个成员函数

#include<iostream>
using namespace std;

class A
{
public:
A(int x)
:_a(x)
{
cout << "A()" << endl;
}

请写一个不能被继承的类
A(const A& p)
:_a(p._a)
{
cout << "A(const A& p)" << endl;
}

A& operator=(const A& s)
{
cout << "A& operator=(const A& s)" << endl;
if (&s != this)
{
_a = s._a;
}

return *this;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int _a;
};

class B : public A
{
public:
B(int a, int b)
: _b(b)
, A(a)
{
cout << "B()" << endl;
}

B(const B& p)
:A(p)
, _b(p._b)
{
cout << "B(const B& p)" << endl;
}

B& operator=(const B& s)
{
cout << "B& operter = (const B& s)" << endl;
if (&s != this)
{
A::operator=(s);
_b = s._b;
}
return *this;
}

~B()
{
//A::~A();
cout << "~B()"<<endl;
}
protected:
int _b;
};
int main()
{
B bb(1, 2);
//B bb2(bb);

//B bb3(2, 4);
//bb2 = bb3;

return 0;
}


看如上代码总结出以下三点:
(1)当基类构造函数不带参数时, 派生类不一定需要定义构造函数, 系统会自动的调用基类的无参构造函数; 然而当基类的构造函数那怕只带有一个参数, 它所有的派生类都必须定义构造函数,
参数只是被传递给了要调用的基类构造函数

2)如果派生类的基类也是一个派生类, 每个派生类只需负责其直接基类数据成员的初始,依次上溯。

多继承与菱形继承会出现二义性和数据冗余现象:
[html]view plaincopy
  1. #include<iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6. int _a;
  7. };
  8. class B : virtual public A
  9. {
  10. public:
  11. int _b;
  12. };
  13. class C : virtual public A
  14. {
  15. public:
  16. int _c;
  17. };
  18. class D : public C, public B
  19. {
  20. public:
  21. int _d;
  22. };
  23. int main()
  24. {
  25. D dd;
  26. cout << sizeof(dd) << endl;
  27. dd.B::_a = 1;
  28. dd._b = 3;
  29. dd.C::_a = 2;
  30. dd._c = 4;
  31. dd._d = 5;
  32. B bb;
  33. C cc;
  34. cout << sizeof(bb) << endl;
  35. //bb = dd;
  36. ////cc = dd;
  37. //A* pa = ⅆⅆ
  38. //B* pb = ⅆ
  39. //C* pc = ⅆⅆ
  40. //D* pd = ⅆ
  41. return 0;
  42. }

为什么运行结果是这样?看完下面的对象的内存分布也就明白了
若要解决二义性和数据冗余就得使用虚继承(在继承基类时,使用关键字virtual,如上面代码所示)

从上图可以发现,当使用虚继承后,通过虚基表就解决了数据二义性的问题。多了虚基表的空间所以sizeof(dd)就变成24了。
虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虛继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损牦。

再看下面,如果main()里面运行此代码,各指针所指向的空间是什么?
[html]view plaincopy
  1. int main()
  2. {
  3. D dd;
  4. dd.B::_a = 1;
  5. dd._b = 3;
  6. dd.C::_a = 2;
  7. dd._c = 4;
  8. dd._d = 5;
  9. B bb;
  10. C cc;
  11. A* pa = ⅆ
  12. B* pb = ⅆ
  13. C* pc = ⅆ
  14. D* pd = ⅆ
  15. return 0;
  16. }




补充几点:
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例