n the previous lesson on pointers and references to the base class of derived objects, we took a look at a number of examples where using pointers or references to a base class had the potential to simplify code. However, in every case, we ran up against the problem that the base pointer or reference was only able to call the base version of a function, not a derived version.
Here’s a simple example of this behavior:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class
Base
{
protected
:
public
:
const
char
* GetName() {
return
"Base"
; }
};
class
Derived:
public
Base
{
public
:
const
char
* GetName() {
return
"Derived"
; }
};
int
main()
{
Derived cDerived;
Base &rBase = cDerived;
cout <<
"rBase is a "
<< rBase.GetName() << endl;
}
|
This example prints the result:
Because rBase is a Base pointer, it calls Base::GetName(), even though it’s actually pointing to the Base portion of a Derived object.
In this lesson, we will address this issue using virtual functions.
Virtual functions
A virtual function is a special type of function that resolves to the most-derived version of the function with the same signature. To make a function virtual, simply place the “virtual” keyword before the function declaration.
Note that virtual functions and virtual base classes are two entirely different concepts, even though they share the same keyword.
Here’s the above example with a virtual function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class
Base
{
protected
:
public
:
virtual
const
char
* GetName() {
return
"Base"
; }
};
class
Derived:
public
Base
{
public
:
virtual
const
char
* GetName() {
return
"Derived"
; }
};
int
main()
{
Derived cDerived;
Base &rBase = &cDerived;
cout <<
"rBase is a "
<< rBase.GetName() << endl;
return
0;
}
|
This example prints the result:
Because rBase is a pointer to the Base portion of a Derived object, when rBase.GetName() is evaluated, it would normally resolve to Base::GetName(). However, Base::GetName() is virtual, which tells the program to go look and see if there are any more-derived versions of the function available. Because the Base object that rBase is pointing to is actually part of a Derived object, the program will check every inherited class between Base and Derived and use the most-derived version of the function that it finds. In this case, that is Derived::GetName()!
Let’s take a look at a slightly more complex example:
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
|
class
A
{
public
:
virtual
const
char
* GetName() {
return
"A"
; }
};
class
B:
public
A
{
public
:
virtual
const
char
* GetName() {
return
"B"
; }
};
class
C:
public
B
{
public
:
virtual
const
char
* GetName() {
return
"C"
; }
};
class
D:
public
C
{
public
:
virtual
const
char
* GetName() {
return
"D"
; }
};
int
main()
{
C cClass;
A &rBase = cClass;
cout <<
"rBase is a "
<< rBase.GetName() << endl;
return
0;
}
|
What do you think this program will output?
Let’s look at how this works. First, we instantiate a C class object. rBase is an A pointer, which we set to point to the A portion of the C object. Finally, we call rBase.GetName(). rBase.GetName() evaluates to A::GetName(). However, A::GetName() is virtual, so the compiler will check all the classes between A and C to see if it can find a more-derived match. First, it checks B::GetName(), and finds a match. Then it checks C::GetName() and finds a better match. It does not check D::GetName() because our original object was a C, not a D. Consequently, rBase.GetName() resolves to C::GetName().
As a result, our program outputs:
A more complex example
Let’s take another look at the Animal example we were working with in the previous lesson. Here’s the original class:
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
40
|
#include <string>
class
Animal
{
protected
:
std::string m_strName;
// We're making this constructor protected because
// we don't want people creating Animal objects directly,
// but we still want derived classes to be able to use it.
Animal(std::string strName)
: m_strName(strName)
{
}
public
:
std::string GetName() {
return
m_strName; }
const
char
* Speak() {
return
"???"
; }
};
class
Cat:
public
Animal
{
public
:
Cat(std::string strName)
: Animal(strName)
{
}
const
char
* Speak() {
return
"Meow"
; }
};
class
Dog:
public
Animal
{
public
:
Dog(std::string strName)
: Animal(strName)
{
}
const
char
* Speak() {
return
"Woof"
; }
}
|
And here’s the class with virtual functions:
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
40
|
#include <string>
class
Animal
{
protected
:
std::string m_strName;
// We're making this constructor protected because
// we don't want people creating Animal objects directly,
// but we still want derived classes to be able to use it.
Animal(std::string strName)
: m_strName(strName)
{
}
public
:
std::string GetName() {
return
m_strName; }
virtual
const
char
* Speak() {
return
"???"
; }
};
class
Cat:
public
Animal
{
public
:
Cat(std::string strName)
: Animal(strName)
{
}
virtual
const
char
* Speak() {
return
"Meow"
; }
};
class
Dog:
public
Animal
{
public
:
Dog(std::string strName)
: Animal(strName)
{
}
virtual
const
char
* Speak() {
return
"Woof"
; }
}
|