五、继承和友元
(1)友元关系不能被继承
(2)根据友元性质,父类友元可以直接访问父类的所有成员,也可以访问子类的共有成员,但不能访问子类私有成员和保护成员
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #include<iostream> 3. using namespace std; 4. 5. class Rectangle;//前置声明Rectangle类 6. class Shape 7. { 8. public: 9. friend void fun(Shape& s, Rectangle& r);//声明fun是Shape类的友元 10. 11. //构造函数 12. Shape(float width, float height) 13. :_width(width), 14. _height(height) 15. { 16. cout << "Shape()" << endl; 17. } 18. 19. //拷贝构造函数 20. Shape(const Shape& s) 21. :_width(s._width), 22. _height(s._height) 23. { 24. cout << "Shape(const Shape& s)" << endl; 25. } 26. 27. //赋值运算符重载函数 28. Shape& operator=(const Shape& p) 29. { 30. cout << "Shape& operator=(const Shape& p)" << endl; 31. if (&p != this) 32. { 33. _width = p._width; 34. _height = p._height; 35. } 36. 37. return *this; 38. } 39. 40. //析构函数 41. ~Shape() 42. { 43. cout << "~Shape()" << endl; 44. } 45. 46. void setWidth(float w) 47. { 48. _width = w; 49. } 50. 51. void setHeight(float h) 52. { 53. _height = h; 54. } 55. 56. void ShapePrintf() 57. { 58. cout << "Shape : _width" << ":" << _width << endl; 59. cout << "Shape : _height" << ":" << _height << endl; 60. } 61. 62. public: 63. float _width; 64. float _height; 65. }; 66. 67. class Rectangle : public Shape 68. { 69. public: 70. 71. //构造函数 72. Rectangle(float width = 10, float height = 10, int rightAngleCounts = 4) 73. :Shape(width, height) 74. , _rightAngleCounts(rightAngleCounts) 75. { 76. cout << "Rectangle()" << endl; 77. } 78. 79. //拷贝构造函数 80. Rectangle(const Rectangle& r) 81. :Shape(r)//切片,子类对象赋值给父类引用 82. , _rightAngleCounts(r._rightAngleCounts)//子类自己的成员初始化为拷贝对象的子类成员值 83. { 84. cout << "Rectangle(const Rectangle& r)" << endl; 85. } 86. 87. //赋值运算符重载函数 88. Rectangle& operator=(const Rectangle& r) 89. { 90. if (this != &r) 91. { 92. Shape::operator=(r); 93. _rightAngleCounts = r._rightAngleCounts; 94. } 95. cout << "Rectangle& operator=(const Rectangle& r)" << endl; 96. 97. return *this; 98. } 99. 100. //析构函数 101. ~Rectangle() 102. { 103. //Shape::~Shape(); 104. 105. //清理自己的空间 106. //delete ptr; 107. 108. cout << "~Rectangle()" << endl; 109. } 110. 111. float Area() 112. { 113. return _width * _height; 114. } 115. 116. void RectanglePrintf(float width, float height) 117. { 118. cout << "Rectangle : _width" << ":" << width << endl; 119. cout << "Rectangle : _height" << ":" << height << endl; 120. } 121. 122. public://将子类自己的成员变量定义为公有 123. int _rightAngleCounts;//直角个数 124. }; 125. 126. //友元函数 127. void fun(Shape& s, Rectangle& r) 128. { 129. cout << "fun:s._width = " << s._width << endl; 130. cout << "fun:r._rightAngleCounts = " << r._rightAngleCounts << endl;//子类自己的成员变量为公有时,友元可以访问 131. } 132. 133. int main() 134. { 135. Shape s(10, 10); 136. Rectangle r(6, 6, 4); 137. 138. fun(s, r); 139. 140. return 0; 141. }
将子类自己的成员变量定义为公有时,友元可以访问:
但是将子类自己的成员变量定义为保护或私有时,友元不可以访问,将上述代码中的子类自己的成员变量的访问限定符:
1. public://将子类自己的成员变量定义为公有 2. int _rightAngleCounts;//直角个数
由公有改为保护:
1. private://将子类自己的成员变量定义为保护 2. int _rightAngleCounts;//直角个数
编译就会报错:
六、继承和静态成员
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #include<iostream> 3. using namespace std; 4. 5. class Shape 6. { 7. public: 8. //构造函数 9. Shape(float width = 10, float height = 10) 10. :_width(width), 11. _height(height) 12. { 13. ++_count; 14. } 15. 16. public: 17. float _width; 18. float _height; 19. static int _count; 20. }; 21. 22. int Shape::_count = 0; 23. 24. //长方形 25. class Rectangle : public Shape 26. { 27. public: 28. int _rightAngleCounts;//直角个数 29. }; 30. 31. //三角形 32. class Triangle : public Shape 33. { 34. public: 35. float _acuteAngleCounts;//锐角个数 36. }; 37. 38. 39. int main() 40. { 41. Shape s; 42. Rectangle r; 43. Triangle c; 44. 45. cout << Shape::_count << endl; 46. cout << Rectangle::_count << endl; 47. cout << Triangle::_count << endl; 48. 49. Triangle::_count = 20; 50. 51. cout << Shape::_count << endl; 52. cout << Rectangle::_count << endl; 53. cout << Triangle::_count << endl; 54. 55. return 0; 56. }
_count在整份代码中只有一份:
定义了3个类对象,_count就会自增3次变为3;重置了Circle类中的_count,父类中的_count也就被重置了:
静态成员可以统计整个代码一共产生了多少个对象,由于子类无论构造还是拷贝构造都会调用父类的构造函数(父类的拷贝构造对象没有被统计在内,因为父类的拷贝构造函数没有调用父类的构造函数)。
七、菱形继承
继承关系分为:单继承、多继承、菱形继承。
1.单继承
一个子类只有一个直接父类时称这个继承关系为单继承
2.多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承
多继承会导致菱形继承。
3.菱形继承
是多继承的一种特殊情况:
菱形继承会存在两个问题:
(1)数据冗余
(2)二义性
如上面的TriangleEmbeddedRectangle对象中会存在两份Shape成员:
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #include<iostream> 3. using namespace std; 4. 5. class Shape 6. { 7. public: 8. //构造函数 9. Shape(float width = 10, float height = 10) 10. :_width(width), 11. _height(height) 12. {} 13. 14. public: 15. float _width; 16. float _height; 17. }; 18. 19. 20. //长方形 21. class Rectangle : public Shape 22. { 23. public: 24. int _rightAngleCounts;//直角个数 25. }; 26. 27. //三角形 28. class Triangle : public Shape 29. { 30. public: 31. int _acuteAngleCounts;//锐角个数 32. }; 33. 34. //三角形内嵌于长方形 35. class TriangleEmbeddedRectangle : public Rectangle, public Triangle 36. { 37. public: 38. int _sideCounts;//边数 39. }; 40. int main() 41. { 42. TriangleEmbeddedRectangle t; 43. t._width = 60;//报错,指向不明 44. 45. return 0; 46. }
数据冗余:Rectangle和Triangle都有_width和_height,TriangleEmbeddedRectangle也只有一个,_width和_height,但是编译错误却表示有两个_width:
二义性:访问_width时,不知道要访问谁的_width:
指定作用域可以解决二义性的问题,即指定访问谁的_width:
1. int main() 2. { 3. TriangleEmbeddedRectangle t; 4. t.Triangle::_width = 60;//指定访问Triangle的_width 5. 6. return 0; 7. }
编译OK。
但是这只解决了二义性的问题,并不能解决数据冗余的问题,TriangleEmbeddedRectangle的对象t中还是有两份_width和_height。菱形虚拟继承既可以解决数据冗余也可以解决二义性的问题。
八、菱形虚拟继承
1.虚继承
在两个直接父类的继承方式访问限定符前加vitual:
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #include<iostream> 3. using namespace std; 4. 5. class Shape 6. { 7. public: 8. //构造函数 9. Shape(float width = 10, float height = 10) 10. :_width(width), 11. _height(height) 12. {} 13. 14. public: 15. float _width; 16. float _height; 17. }; 18. 19. 20. //长方形 21. class Rectangle : virtual public Shape//虚继承 22. { 23. public: 24. int _rightAngleCounts;//直角个数 25. }; 26. 27. //三角形 28. class Triangle : virtual public Shape//虚继承 29. { 30. public: 31. int _acuteAngleCounts;//锐角个数 32. }; 33. 34. //三角形内嵌于长方形 35. class TriangleEmbeddedRectangle : public Rectangle, public Triangle 36. { 37. public: 38. int _sideCounts;//边数 39. }; 40. 41. int main() 42. { 43. TriangleEmbeddedRectangle t; 44. t.Triangle::_width = 60; 45. 46. return 0; 47. }
监视发现现在改变的是同一个_width,Rectangle、Triangle和Shape中的_width是同一个_width:
现在可以不用指定作用域了。
那么C++编译器是如何通过虚继承解决数据冗余和二义性的问题的呢?
由于监视窗口被编译器处理过,看不到真实过程。可以使用内存窗口来查看。
2.虚继承的解决数据冗余和二义性的原理
(1)普通继承
对于Shape、Rectangle、Triangle、TriangleEmbeddedRectangle类,如果没有虚继承,仅仅只是菱形继承
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #define _CRT_SECURE_NO_WARNINGS 1 3. #include<iostream> 4. using namespace std; 5. 6. class Shape 7. { 8. public: 9. //构造函数 10. Shape(float width = 10,float height = 10) 11. :_width(width) 12. ,_height(height) 13. {} 14. 15. public: 16. float _width; 17. float _height; 18. }; 19. 20. 21. //长方形 22. class Rectangle : public Shape//非虚继承 23. { 24. public: 25. int _rightAngleCounts;//直角个数 26. }; 27. 28. //三角形 29. class Triangle : public Shape//非虚继承 30. { 31. public: 32. int _acuteAngleCounts;//锐角个数 33. }; 34. 35. //三角形内嵌于长方形 36. class TriangleEmbeddedRectangle : public Rectangle, public Triangle 37. { 38. public: 39. int _sideCounts;//边数 40. }; 41. 42. int main() 43. { 44. TriangleEmbeddedRectangle t; 45. t.Rectangle::_width = 1; 46. t.Rectangle::_height = 2; 47. t.Triangle::_width = 3; 48. t.Triangle::_height = 4; 49. t._rightAngleCounts = 5; 50. t._acuteAngleCounts = 6; 51. t._sideCounts = 7; 52. 53. return 0; 54. }
F10-调试-窗口-内存,输入&t,enter,将列改为4:
由于_width和_height的类型为float型,因此内存中存放的是浮点数的十六进制表示形式,VS编译器内存以小端形式存储。F10走到return 0:
可以看到内存中,蓝色为Rectangle的成员,绿色为Triangle的成员,Rectangle中存了一份Shape的成员,Triangle中存了一份Shape的成员,这就存在数据冗余,本来只需要存一份Shape就够了,但是内存中存了两份。
(2)虚继承
如果改成虚继承
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #define _CRT_SECURE_NO_WARNINGS 1 3. #include<iostream> 4. using namespace std; 5. 6. class Shape 7. { 8. public: 9. //构造函数 10. Shape(float width = 10,float height = 10) 11. :_width(width) 12. ,_height(height) 13. {} 14. 15. public: 16. float _width; 17. float _height; 18. }; 19. 20. 21. //长方形 22. class Rectangle : virtual public Shape//虚继承 23. { 24. public: 25. int _rightAngleCounts;//直角个数 26. }; 27. 28. //三角形 29. class Triangle : virtual public Shape//虚继承 30. { 31. public: 32. int _acuteAngleCounts;//锐角个数 33. }; 34. 35. //三角形内嵌于长方形 36. class TriangleEmbeddedRectangle : public Rectangle, public Triangle 37. { 38. public: 39. int _sideCounts;//边数 40. }; 41. 42. int main() 43. { 44. TriangleEmbeddedRectangle t; 45. t.Rectangle::_width = 1; 46. t.Rectangle::_height = 2; 47. t.Triangle::_width = 3; 48. t.Triangle::_height = 4; 49. t._rightAngleCounts = 5; 50. t._acuteAngleCounts = 6; 51. t._sideCounts = 7; 52. 53. return 0; 54. }
会发现Rectangle指针中存了地址0x00D39B54,Triangle的指针中也存了地址0x00D39B30,这两个地址是用来干嘛的呢?
新开两个内存分别输入0x00D39B54和0x00D39B30,在它们的下一个位置分别存放了0x14和0x0C,这两个都是偏移量,0x00B9FCDC+0x14=0x00B9FCF0,0x00B9FCE4+0x0C=0x00B9FCF0,都指向了同一地址0x00B9FCF0:这个地址是Shape成员的起始地址:
普通继承存放两份Shape成员,而虚继承只需要存在一份Shape成员即可,通过指针找到虚基表,通过虚基表指针的偏移量计算出Shape成员的起始地址。
菱形虚拟继承原理图:
虚拟菱形继承相比较于菱形继承,TriangleEmbeddedRectangle对象t的直接父类Rectangle和Triangle中存储的不再是Shape的成员,而是Shape的偏移量地址,通过该指针找到虚基表之后,计算偏移,就能知道Shape成员存放的地址,位于Rectangle和Triangle成员的下边,不再像普通继承那样位于Rectangle和Triangle成员的上边,这时候Shape既不属于Rectangle也不属于Triangle。
无论Shape类有多大,付出的代价只有2个指针,即8个字节,解决了数据冗余问题;整个内存中只需要存一份Shape的成员,解决了二义性。
一般不建议设计多继承,这样就不会出现菱形继承,也就不会有数据冗余和二义性的问题。
九.继承和组合
(1) public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。如Shape和Rectangle。他们之间是强关联关系。
(2)组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。如Room和Door。
他们之间是弱关联关系
1. #include<iostream> 2. using namespace std; 3. 4. class Room 5. { 6. public: 7. float _roomHegiht;//房间高度 8. int layers;//层数 9. }; 10. 11. class Door 12. { 13. public: 14. float _doorHegiht;//门高度 15. string brand = "梦天木门";//品牌 16. };
(3)优先使用对象组合,而不是类继承 。为了降低对象之间的关联度
(4)继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。父类所有非私有成员对子类都可见,父类的改变会影响子类,父类的封装对子类不太起作用。
(5)继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
(6)对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装。子类只能使用父类的共有成员 ,子类和父类关联度低。
(7)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。