C/C++小课之继承(一)

继承的概念

继承源于生物界,指后代能够传承前代的特征和行为。

基类和子类

继承使我们可以通过一个已有的类创建一个新类,新类自然获得已有类的全部功能。在某个继承关系中,已有的类在这个继承关系中作为基类超类父类,新类称为派生类子类

具体来讲,子类复制了基类的全体数据成员和成员函数。此外,子类还可以进行扩充、修改和重定义。

同一个类可以作为多个类的基类,一个子类也可以作为另一个类的基类。

基类和子类是继承关系中的概念,并不是本质的概念,而是关系上的概念。

继承与派生

A作为基类,B作为子类时,我们称:

  • B继承A
  • A派生B

单继承和多继承

只能从一个基类派生的继承称为单继承,即一个类一次最多继承一个基类。

可以从多个基类派生的继承称为多继承

C++支持单继承和多继承。

Java只支持单继承,需要“多继承”时使用接口实现。(接口是什么,学了Java就知道了)

类成员访问权限

C++类成员访问权限默认为private

public

语法正确的条件下,访问不受控制,在哪里都可以访问。

private

只能在类内友元函数访问。

protected

只能在类内子类内友元函数访问。

继承方式

注意:继承方式决定的是子类如何继承基类成员的访问权限

不同继承方式会不同程度地改变基类成员在派生类中的访问权限。(个人觉得这句话不好,易让人误解)

C++中继承方式默认为private

public

基类成员的访问权限在派生类中保持不变

基类的private成员、public成员、protected成员在子类中保持他们在基类中相同的访问权限。

private

基类成员的访问权限在派生类中全部变为private

protected

基类的public成员的访问权限在派生类中变为protected

private成员和protected成员的访问权限不变。

派生类对基类的扩展

派生类可以

  • 增加新的数据成员和成员函数
  • 重载从基类继承到的成员函数
  • 覆盖(重定义)从基类集成到的成员函数
  • 改变基类成员在派生类中的访问属性(通过继承方式实现)

派生类不能继承基类的以下内容:

  • 析构函数
  • 基类的友元函数
  • 静态成员
    • 静态数据成员
    • 静态成员函数

注意

静态成员为整个继承体系公有。

C++11之前,派生类不能继承基类的构造函数,C++11之后可以,通过using实现。

成员函数的重定义、重载及隐藏

派生类对继承到的基类成员函数的重定义重载都会影响它们在派生类中的可见性。

派生类对基类函数进行重定义或重载时,由于继承和类作用域的原因,导致了隐藏,解决隐藏有三种方法。

影响可见性

如果派生类重定义或者重载了基类中某个函数名为func的函数,则基类中的所有名字为func的函数都会被隐藏。(其中隐藏指的是在派生类中不能直接看到这个函数。)

解决

  1. 通过基类名称访问被派生类重定义重载隐藏的函数,

    基类::函数名(...)

  2. 重载基类的所有同名函数,这些重载函数的代码与基类完全相同。

  3. 使用using

前两种方法都很繁琐。

而C++11提供了using使基类中被隐藏的函数在派生类中可见,具体使用在后边的using部分讲解。

重定义

是什么

覆盖也称为重定义重写

重定义是指派生类定义与基类具有相同参数列表的同名成员函数。

(关于重定义的定义,我个人认为书上P147的说法是错的。)

隐藏及解决

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;

class Base {
public:
void print()
{
cout << "Base::print()" << endl;
}
void print(int i)
{
cout << "Base::print()......" << i << endl;
}
};

class Derived :public Base {
public:
int print() //重定义Base类的void print()
{
cout << "Derived::print()" << endl;
return 0;
}
};

int main()
{
Base b1;
Derived d1;

b1.print(); //Base类调用函数正常
b1.print(1);//Base类调用函数正常

d1.print(); //Base类中的print()被隐藏,调用Derived类的print()
d1.print(1);//该行报错,编译不通过。原因是Derived类的int print()重定义了Base类的void print(),导致Base中所有同名函数被隐藏,即print(int)和print()被隐藏。

d1.Base::print(); //调用Base类的print()
d1.Base::print(1); //调用Base类的print(int)
return 0;
}

将第34行注释后,输出结果如下:

1
2
3
4
5
Base::print()
Base::print()......1
Derived::print()
Base::print()
Base::print()......1

重载

是什么

重载与重定义不同,重载要求成员函数名称相同,但具有不同的参数列表

(关于重载的定义,我个人认为书上P147的说法是错的)

隐藏及解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;

class Base {
public:
void print()
{
cout << "Base::print()" << endl;
}
void print(int i)
{
cout << "Base::print()......" << i << endl;
}
};

class Derived :public Base {
public:
void print(char i) //重载Base类的两个print函数,即print()、print(int i)
{
cout << "Derived::print()......" << i << endl;
}
//int print(int i);//这个是重定义Base的void print(int i),而非重载
};

int main()
{
Base b1;
Derived d1;

b1.print(); //Base类调用函数正常
b1.print(1);//Base类调用函数正常

d1.print(); //该行报错,因为Derived的print(char i)重载了Base类的两个print函数,它们在Derived类中被隐藏了
d1.print('A');//调用Derived类的print(char i)

d1.Base::print(); //调用Base类的print()
d1.Base::print(1); //调用Base类的print(int)
return 0;
}

将第33行注释后,输出结果如下:

1
2
3
4
5
Base::print()
Base::print()......1
Derived::print()......A
Base::print()
Base::print()......1

using

using有三个用途:

  • 使隐藏的函数重现
  • 改变基类成员在派生类中的访问权限
  • 使子类继承基类构造函数(在继承第二讲中讲解如何使用)

使隐藏函数重现

方法:在派生类中用using 基类::函数名;声明基类的函数名,不需提供函数参数。

一条using可以把指定函数名的所有版本添加到派生类作用域中,它们的访问权限与using语句所在区域的访问权限相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

class Base {
public:
void print()
{
cout << "Base::print()" << endl;
}
void print(int i)
{
cout << "Base::print()......" << i << endl;
}
};

class Derived :public Base {
public:
void print(char i) //重载Base类的两个print函数,即print()、print(int i)
{
cout << "Derived::print()......" << i << endl;
}
using Base::print;
};

int main()
{

Derived d1;

d1.print(); //不加using的话该行报错
d1.print('A');//调用Derived类的print(char i)

return 0;
}

改变基类成员在派生类中的访问权限

在派生类的publicprotectedprivate权限区域内,使用using再次声明基类的非private成员,就可以重新设置它们在派生类中的权限为using语句所在区域的权限。

继承和类作用域

在存在继承关系时,派生类的作用域嵌套在基类作用域的内层。

因此,在解析类成员名称时,如果在本类的作用域内没有找到,编译器会接着在外层的基类作用域内继续寻找该成员名称的定义。

在这个寻找的过程中,一旦在某个作用域找到了,就停止查找,即使外层作用域内还有同名成员,也不找了。

形式如下:

1
2
3
4
5
6
7
8
Base {
int print(int){}
...
Derived {
void print()
...
}
}

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class Base {
public:
void print(int i)
{
cout << "Base::print()......" << i << endl;
}
};

class Derived1 :public Base {
public:
void print()
{
cout << "Derived::print()......" << endl;
}

};

class Derived2 :public Derived1 {

};

int main()
{
Derived2 d;
d.print(1); //该行代码报错,因为在Derived1中找到同名函数void print(),不会再向外层找,又因为参数列表不匹配,所以报错

return 0;
}

作者:@臭咸鱼

转载请注明出处:https://chouxianyu.github.io

欢迎讨论和交流!