【C++】-- 继承(二)

简介: 【C++】-- 继承

三、继承中的作用域

1.父类和子类都有独立的作用域

如下:子类中初始化的变量的值不受父类中初始化的变量的值的影响。

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. class Shape
6. {
7. public:
8.  void setWidth(float w)
9.  {
10.     _width = w;
11.   }
12. 
13.   void setHeight(float h)
14.   {
15.     _height = h;
16.   }
17. 
18.   void ShapePrintf()
19.   {
20.     cout << "Shape : _width" << ":" << _width << endl;
21.     cout << "Shape : _height" << ":" << _height << endl;
22.   }
23. 
24. public:
25.   float _width = 10;
26.   float _height = 10;
27. };
28. 
29. class Rectangle : public Shape
30. {
31. public:
32.   float Area()
33.   {
34.     return _width * _height;
35.   }
36. 
37.   void RectanglePrintf()
38.   {
39.     _width = 20;
40.     _height = 30;
41.     cout << "Rectangle : _width" << ":" << _width << endl;
42.     cout << "Rectangle : _height" << ":" << _height << endl;
43.   }
44. 
45. };
46. 
47. int main()
48. {
49.   Shape s;
50.   s.ShapePrintf();
51. 
52.   Rectangle r;
53.   r.RectanglePrintf();
54. 
55. return 0;
56. }

父类的_width和_height是父类中初始化的值,子类的_width和_height是子类中初始化的值:

2.隐藏

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 父类::父类成员 显式访问父类的同名成员)

在父类和子类中定义同名成员_width:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. class Shape
6. {
7. public:
8.  void setWidth(float w)
9.  {
10.     _width = w;
11.   }
12. 
13.   void setHeight(float h)
14.   {
15.     _height = h;
16.   }
17. 
18.   void ShapePrintf()
19.   {
20.     cout << "Shape : _width" << ":" << _width << endl;
21.     cout << "Shape : _height" << ":" << _height << endl;
22.   }
23. 
24. public:
25.   float _width = 10;
26.   float _height = 10;
27. };
28. 
29. class Rectangle : public Shape
30. {
31. public:
32.   float Area()
33.   {
34.     return _width * _height;
35.   }
36. 
37.   void RectanglePrintf()
38.   {
39.     cout << "Rectangle : _width" << ":" << _width << endl;
40.     cout << "Rectangle : _height" << ":" << _height << endl;
41.   }
42. public:
43.   float _width = 50;//子类重定义同名成员
44. };
45. 
46. int main()
47. {
48.   Shape s;
49.   s.ShapePrintf();
50. 
51.   Rectangle r;
52.   r.RectanglePrintf();
53. 
54. return 0;
55. 
56. }

发现子类打印的_width的值是子类初始化的值,这是因为子类和父类的成员同名时,构成了隐藏,子类成员屏蔽了父类对同名成员的访问:

那么子类对象r中到底有一个_width还是有两个_width呢?F10-监视:发现r中有两个_width,根据刚才的打印发现_width访问的是子类的_width 50。那么说明代码根据就近原则,在子类中优先访问了子类的同名成员。

如果就想在子类中访问父类的同名成员呢?需要用::访问限定符指定访问的是父类的同名成员,即父类::父类成员

将上述代码中RectanglePrintf()函数中的

cout << "Rectangle : _width" << ":" << _width << endl;

改为显式访问父类成员:

cout << "Shape : _width" << ":" << Shape::_width << endl;

这就打印了父类的同名成员:

3.如果是成员函数的隐藏,只需要函数名相同就构成隐藏

由于子类继承父类的所有成员,因此,子类对象可以直接访问父类的成员,那么子类对象r可以访问父类Shape类的成员setHeight():

1. int main()
2. {
3.  Rectangle r;
4.  r.setHeight(100);
5.  cout << "r._height : " << r._height << endl;
6. 
7.  return 0;
8. }

访问成功:

如果子类成员函数名和父类成员函数名相同,不管参数相不相同,都会构成隐藏,即子类成员屏蔽父类同名函数的访问,子类对象不可以访问父类的同名称成员函数:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. class Shape
6. {
7. public:
8.  void setWidth(float w)
9.  {
10.     _width = w;
11.   }
12. 
13.   void setHeight(float h)
14.   {
15.     _height = h;
16.   }
17. 
18.   //和子类成员函数同名
19.   void printf()
20.   {
21.     cout << "Shape : _width" << ":" << _width << endl;
22.     cout << "Shape : _height" << ":" << _height << endl;
23.   }
24. 
25. public:
26.   float _width = 10;
27.   float _height = 10;
28. };
29. 
30. class Rectangle : public Shape
31. {
32. public:
33.   float Area()
34.   {
35.     return _width * _height;
36.   }
37. 
38.   //和父类成员函数同名,不论参数和父类同名函数的参数相不相同
39.   void printf(float width,float height)
40.   {
41.     cout << "Rectangle : _width" << ":" << width << endl;
42.     cout << "Rectangle : _height" << ":" << height << endl;
43.   }
44. 
45. public:
46.   float _width = 50;
47. };
48. 
49. int main()
50. {
51. 
52.   Rectangle r;
53.   r.printf();
54. 
55.   return 0;
56. }

r.printf()这一行会报错:

这是因为,只要父类函数和子类函数同名,都会构成隐藏,不管参数相不相同。隐藏了父类同名函数之后,子类对象就不能访问父类同名函数了。

4.继承中最好不要定义同名成员

四、子类的默认成员函数

以前学过类的默认成员函数,父类遵从类的默认成员函数规则,那么子类的默认成员函数如何生成呢?

6个默认成员函数中,取地址操作符重载和const修饰的取地址操作符重载不常用,只需要看前4种即可。

写上父类的默认成员函数:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. class Shape
6. {
7. public:
8.  Shape(float width = 10,float height = 10)
9.    :_width(width)
10.     ,_height(height)
11.   {
12.     cout << "Shape()" << endl;
13.   }
14. 
15.   Shape(const Shape& s)
16.     :_width(s._width)
17.     ,_height(s._height)
18.   {
19.     cout << "Shape(const Shape& s)" << endl;
20.   }
21. 
22.   Shape& operator=(const Shape& p)
23.   {
24.     cout << "Shape& operator=(const Shape& p)" << endl;
25.     if (&p != this)
26.     {
27.       _width = p._width;
28.       _height = p._height;
29.     }
30. 
31.     return *this;
32.   }
33. 
34.   ~Shape()
35.   {
36.     cout << "~Shape()" << endl;
37.   }
38. 
39.   void setWidth(float w)
40.   {
41.     _width = w;
42.   }
43. 
44.   void setHeight(float h)
45.   {
46.     _height = h;
47.   }
48. 
49.   //和子类成员函数同名
50.   void ShapePrintf()
51.   {
52.     cout << "Shape : _width" << ":" << _width << endl;
53.     cout << "Shape : _height" << ":" << _height << endl;
54.   }
55. 
56. public:
57.   float _width;//声明成员变量时不给默认值
58.   float _height;//声明成员变量时不给默认值
59. };
60. 
61. class Rectangle : public Shape
62. {
63. public:
64.   float Area()
65.   {
66.     return _width * _height;
67.   }
68. 
69.   void RectanglePrintf(float width,float height)
70.   {
71.     cout << "Rectangle : _width" << ":" << width << endl;
72.     cout << "Rectangle : _height" << ":" << height << endl;
73.   }
74. 
75. public:
76.   int _rightAngleCounts;//直角个数,声明成员变量时不给默认值
77. };

这段代码中, 子类和父类各自有各自的打印函数,并且子类的成员变量变成了直角个数_rightAngleCounts,目的是为了方便理解后面的子类默认成员函数的生成规则。

子类和普通类的默认成员函数不同点:

① 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。

② 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

③ 子类的operator=必须要调用父类的operator=完成父类的赋值。

④ 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。

⑤ 子类对象初始化先调用父类构造再调子类构造。

⑥ 子类对象析构清理先调用子类析构再调父类的析构。

1.规则①

规则① 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。

由于Ranctangle继承了Shape类的成员变量_width和_height,再加上自己的成员变量_rightAngleCounts,子类一共有3个成员变量,现在写子类的构造函数:

1. Rectangle(float width = 20, float height = 20, int rightAngleCounts = 4)
2.    :_width(width),
3.    _height(height),
4.    _rightAngleCounts(rightAngleCounts)
5.  {
6.    cout << "Rectangle(float width = 20, float height = 20, int rightAngleCounts = 4)" << endl;
7.  }

但是编译报错:

这是因为父类有默认构造函数,子类必须调用父类的默认构造函数去初始化父类的那一部分成员,所以当父类有默认构造函数时,子类不用在自己的构造函数内初始化父类的那一部分成员,只初始化自己的成员就可以了:

1. Rectangle(int rightAngleCounts = 4)
2.         :_rightAngleCounts(rightAngleCounts)
3.  {
4.    cout << "Rectangle()" << endl;
5.  }

编译通过。我们知道,默认构造函数分为3种:

(1)我们没写,编译器默认自动生成

(2)我们写的无参默认构造函数

(3)我们写的带参全缺省默认构造函数

假如父类的构造函数不是以上3种呢?也就是父类没有默认的构造函数呢?子类如何初始化?

父类构造函数写成这样就不是默认构造函数:

1. Shape(float width, float height)
2.    :_width(width)
3.    ,_height(height)
4.  {
5.    cout << "Shape()" << endl;
6.  }

那么子类构造函数只初始化子类的成员还可行吗?发现编译出错:

当父类没有默认构造函数时,子类的初始化列表必须显式调用父类的默认构造函数:

1. Rectangle(float width = 10,float height = 10,int rightAngleCounts = 4)
2.    :Shape(width, height)
3.    ,_rightAngleCounts(rightAngleCounts)
4.  {
5.    cout << "Rectangle()" << endl;
6.  }

这时候,显式调用父类默认构造函数,是把当作一个整体去调父类的构造函数

1. int main()
2. {
3.  Rectangle r1(6,6,4);
4. 
5.  return 0;
6. }

监视:都初始化成功了

总结:

(1)父类有默认构造函数,子类只需要在构造函数中初始化子类自己的成员

(2)父类没有默认构造函数,子类要在构造函数初始化列表显式调用父类的构造函数,再初始化子类自己的成员

2.规则②

规则② 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

子类的拷贝构造函数该如何写呢?

调用父类拷贝构造函数时,将子类对象作为参数传给父类拷贝构造函数,这时会发生切片行为,将子类对象中父类成员部分切片赋值给父类成员,完成父类拷贝构造;再将子类自己的成员初始化为拷贝对象的子类成员值:

1.  Rectangle(const Rectangle& r)
2.    :Shape(r)//切片,子类对象赋值给父类引用
3.    , _rightAngleCounts(r._rightAngleCounts)//子类自己的成员初始化为拷贝对象的子类成员值
4.  {
5.    cout << "Rectangle(const Rectangle& r)" << endl;
6.  }
1. int main()
2. {
3.  Rectangle r1(6,6,4);
4.  Rectangle r2(r1);//拷贝构造
5. 
6.  return 0;
7. }

3.规则

规则子类的operator=必须要调用父类的operator=完成父类的赋值

如果子类的赋值运算符重载函数里只调用子类的operator=,那么会发生栈溢出:

1.  Rectangle& operator=(const Rectangle& r)
2.  {
3.    if (this != &r)
4.    {
5.      Shape::operator=(r);
6.      _rightAngleCounts = r._rightAngleCounts;
7.    }
8.    cout << "Rectangle& operator=(const Rectangle& r)" << endl;
9. 
10.     return *this;
11.   }
1. int main()
2. {
3.  Rectangle r1(6,6,4);
4.  Rectangle r3(9,9,4);
5.  r3 = r1;//赋值运算符重载
6. 
7.  return 0;
8. }

这是因为子类的operator=和父类的operator=同名,构成隐藏,调不到父类的operator=,只会不断调用自己的operator=,就会发生栈溢出。那如何才能访问到父类的operator=呢?加父类作用域,指定访问的是父类的operator=即可:

1.  Rectangle& operator=(const Rectangle& r)
2.  {
3.    if (this != &r)
4.    {
5.      Shape::operator=(r);//加父类作用域,指定访问的是父类的operato=
6.      _rightAngleCounts = r._rightAngleCounts;
7.    }
8. 
9.         cout << "Rectangle& operator=(const Rectangle& r)" << endl;
10.     return *this;
11.   }

4.规则④

规则④ 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序

如果按照前面的思路,先显式调用父类的析构函数,会报错:

1.  ~Rectangle()
2.  {
3.    ~Shape();
4.  }

这是由于父类的析构函数和子类的析构函数构成隐藏,奇不奇怪?

按照隐藏的定义,父类和子类的同名函数才会构成隐藏,为什么父类和子类的析构函数不同名也会构成隐藏呢?

这是因为多态的缘故,任何类的析构函数名都会被统一处理成destructor( )。所以父类和子类析构函数同名,会构成隐藏。解决方法就是加父类作用域,指定访问的是父类的析构函数就可以了:

1.  ~Rectangle()
2.  {
3.    Shape::~Shape();
4. 
5.    //清理子类自己的空间
6.    //delete ptr;
7. 
8.    cout << "~Rectangle()" << endl;
9.  }
1. int main()
2. {
3.  Rectangle r1(6,6,4);
4. 
5.  return 0;
6. }

但是,会发现,竟然调用了2次父类的析构函数:(这里调了两次父类析构函数没有崩的原因是父类的析构函数啥也没干)

这是因为,构造子类对象时,先调用父类构造函数,再调用子类构造函数,即规则⑤,如果按照正确的析构顺序,那么就得先调用子类的析构函数,再调用父类的析构函数。为了保证析构时,先调用子类的析构函数,再调用父类的析构函数的顺序,会在调用完子类析构函数后,自动再调用一次父类的析构函数。所以就出现了上面调用了两次父类析构函数的情况。

其实析构函数比较特殊,不需要显式调用父类析构函数,因为子类析构函数调用完毕后,会自动调用父类的析构函数:

1.  ~Rectangle()
2.  {
3.    //清理自己的空间
4.    //delete ptr;
5. 
6.    cout << "~Rectangle()" << endl;
7.  }

这就符合先构造的后析构的顺序,即规则⑥

子类对象构造函数和析构函数调用顺序:

相关文章
|
2天前
|
编译器 数据安全/隐私保护 C++
c++primer plus 6 读书笔记 第十三章 类继承
c++primer plus 6 读书笔记 第十三章 类继承
|
16天前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
24 1
|
20天前
|
C++
c++继承
c++继承
25 0
|
26天前
|
C++
C++程序中的继承与组合
C++程序中的继承与组合
31 1
|
1月前
|
安全 前端开发 Java
【C++】从零开始认识继承二)
在我们日常的编程中,继承的应用场景有很多。它可以帮助我们节省大量的时间和精力,避免重复造轮子的尴尬。同时,它也让我们的代码更加模块化,易于维护和扩展。可以说,继承技术是C++的灵魂。
19 1
|
1月前
|
安全 程序员 编译器
【C++】从零开始认识继承(一)
在我们日常的编程中,继承的应用场景有很多。它可以帮助我们节省大量的时间和精力,避免重复造轮子的尴尬。同时,它也让我们的代码更加模块化,易于维护和扩展。可以说,继承技术是C++的灵魂。
27 3
【C++】从零开始认识继承(一)
|
1天前
|
存储 编译器 程序员
【C++高阶】C++继承学习手册:全面解析继承的各个方面
【C++高阶】C++继承学习手册:全面解析继承的各个方面
|
2天前
|
程序员 C语言 C++
【C++语言】继承:类特性的扩展,重要的类复用!
【C++语言】继承:类特性的扩展,重要的类复用!
|
9天前
|
C++
C++继承的相关知识点
C++继承的相关知识点
5 0
|
15天前
|
数据安全/隐私保护 C++
C++中类的继承技术详解
C++中类的继承技术详解
30 1