章节目录

调用继承的函数与重写行为

本节阅读量:

默认情况下,派生类会继承基类中定义的所有行为。在本课中,我们将更详细地了解成员函数是如何被选择的,以及如何更改派生类中的行为。

在派生类对象上调用成员函数时,编译器首先查看派生类中是否存在同名函数。如果存在,则考虑所有同名重载函数,并使用函数重载解析过程确定是否存在最佳匹配。如果不存在,编译器将沿着继承链向上查找,并以相同方式依次检查每个层级中的父类。

换句话说,编译器会选择继承链中层级最低且最匹配的函数。


调用基类函数

首先,让我们研究当派生类没有匹配函数,但基类有匹配函数时会发生什么:

 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
#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

这将打印:

1
2
Base::identify()
Base::identify()

当调用base.identify()时,编译器会查看名为identify()的函数是否已在Base类中定义。有,因此编译器会继续检查它是否匹配。匹配,所以它会被调用。

当调用derived.identify()时,编译器会查看Derived类中是否定义了名为identify()的函数。没有。因此,它会移动到父类(在本例中为Base),并在那里重新查找。Base定义了identify()函数,因此会使用该函数。换句话说,使用Base::identify()是因为Derived::identify()不存在。

这意味着,如果基类提供的行为已经足够,就可以直接使用基类行为。


重新定义行为

然而,如果我们在Derived类中定义了Derived::identify(),则会改用它。

这意味着,通过在派生类中重新定义函数,我们可以让函数在派生类中以不同方式工作!

例如,假设我们希望derived.identify()打印Derived::identify(),可以直接在Derived类中添加函数identify(),以便在使用Derived对象调用identify()时返回正确的响应。

要修改基类中定义的函数在派生类中的工作方式,只需在派生类内重新定义该函数。

 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
#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const { std::cout << "Derived::identify()\n"; }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

这将打印:

1
2
Base::identify()
Derived::identify()

请注意,在派生类中重新定义函数时,派生类中的函数不会继承基类同名函数的访问说明符。它会使用派生类中定义时所在的访问说明符。因此,在基类中定义为private的函数可以在派生类中重新定义为public,反之亦然!

 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
#include <iostream>

class Base
{
private:
	void print() const 
	{
		std::cout << "Base";
	}
};
 
class Derived : public Base
{
public:
	void print() const 
	{
		std::cout << "Derived ";
	}
};
 
 
int main()
{
	Derived derived {};
	derived.print(); // 调用 derived::print(), 它是 public
	return 0;
}

使用部分现有功能

有时,我们不想完全替换基类函数,而是希望在使用派生对象调用时,为它添加额外功能。在上面的示例中,请注意Derived::identify()完全覆盖了Base::identify()!这可能不是我们想要的。可以让派生函数先调用基类版本(以复用代码),然后再添加其它功能。

要让派生函数调用同名的基类函数,只需执行普通函数调用,但在函数前面加上基类的作用域限定符。例如:

 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
#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const
    {
        std::cout << "Derived::identify()\n";
        Base::identify(); // 注 这里调用 Base::identify()
    }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

这将打印:

1
2
3
Base::identify()
Derived::identify()
Base::identify()

当执行derived.identify()时,它会解析为Derived::identify()。在打印“Derived::identify()”之后,会调用Base::identify(),后者打印“Base::identify()”。

这应该相当简单。为什么需要使用作用域限定符(::)?如果我们这样定义Derived::identify():

 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
#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const
    {
        std::cout << "Derived::identify()\n";
        identify(); // 不使用作用域限定符,会调用Derived的identify(),会无限循环调用自身
    }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

在没有作用域限定符的情况下调用identify(),会默认解析为当前类中的identify(),也就是Derived::identify()。这会导致Derived::identify()调用自身,从而产生无限递归!

在试图调用基类的友元函数(如 operator«)时,可能需要一点技巧。由于基类的友元函数实际上不是基类的一部分,因此使用作用域限定符不会起作用。相反,我们需要一种方法,让Derived类暂时看起来像基类,以便调用函数的正确版本。

幸运的是,使用static_cast很容易做到这一点。下面是一个示例:

 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
#include <iostream>

class Base
{
public:
    Base() { }

	friend std::ostream& operator<< (std::ostream& out, const Base&)
	{
		out << "In Base\n";
		return out;
	}
};

class Derived: public Base
{
public:
    Derived() { }

 	friend std::ostream& operator<< (std::ostream& out, const Derived& d)
	{
		out << "In Derived\n";
		// static_cast 将 Derived 转换为 Base 对象, 所以可以调用正确的 operator<< 版本
		out << static_cast<const Base&>(d); 
		return out;
    }
};

int main()
{
    Derived derived {};

    std::cout << derived << '\n';

    return 0;
}

因为Derived是一个Base,所以我们可以将Derived对象static_cast为Base的引用,以便调用适用于Base的 operator« 版本。

这将打印:

1
2
In Derived
In Base

派生类中的重载解析

如课程顶部所述,编译器将选择最匹配的继承链最下层的函数。

首先,让我们看一个包含重载成员函数的简单例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
};


int main()
{
    Derived d{};
    d.print(5); // 调用 Base::print(int)

    return 0;
}

对于d.print(5)这个调用,编译器在Derived中找不到名为print()的函数,因此会检查Base,并在那里找到两个同名函数。它使用函数重载解析过程,确定Base::print(int)比Base::print(double)更匹配。因此,Base::print(int)会被调用,就像我们预期的那样。

现在,让我们来看一个行为不符合直觉的情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    void print(double) { std::cout << "Derived::print(double)"; } // 新增的函数
};


int main()
{
    Derived d{};
    d.print(5); // 调用 Derived::print(double), 而不是 Base::print(int)

    return 0;
}

对于d.print(5)这个调用,编译器在Derived中找到一个名为print()的函数,因此在尝试确定要解析到哪个函数时,它会先只考虑Derived中的函数。对于此函数调用,Derived::print(double)也是Derived中唯一可见的匹配函数。因此,这将调用Derived::print(double)。

由于Base::print(int)的参数与int参数5的匹配程度高于Derived::print(double),您可能会期望此函数调用解析为Base::print(int)。但由于d是Derived对象,Derived中至少有一个print()函数,并且Derived位于Base继承链的更下层,因此Base中的函数甚至不会被考虑。

那么,如果我们确实希望d.print(5)解析为Base::print(int),该怎么办?一种不太好的方法是定义Derived::print(int):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    void print(int n) { Base::print(n); } // 可以,但不是最优方式
    void print(double) { std::cout << "Derived::print(double)"; }
};

int main()
{
    Derived d{};
    d.print(5); // 调用 Derived::print(int), 其中调用 Base::print(int)

    return 0;
}

虽然这是可行的,但并不理想,因为我们必须为每个希望转发到Base的函数重载,在Derived中额外添加一个函数。这可能会产生许多额外函数,而它们本质上只是把调用转发给Base。

更好的选择是在Derived中声明需要使用的Base函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    using Base::print; // 让 Base::print() 函数可以在Derived被重载解析
    void print(double) { std::cout << "Derived::print(double)"; }
};


int main()
{
    Derived d{};
    d.print(5); // 调用 Base::print(int), 这是 Derived 中可以看到的最优匹配

    return 0;
}

通过在Derived中放置 using Base::print; 声明,我们告诉编译器,所有名为print的基类函数都应该在Derived中可见,这会使它们参与重载解析。因此,Base::print(int) 会被选中,而不是 Derived::print(double)。


24.5 向派生类添加新功能

上一节

24.7 隐藏继承的功能

下一节