[Tech Note] Things you better know about object-oriented programming

本文深入探讨了C++面向对象编程的核心概念,包括构造函数与析构函数的特点、多重继承的问题及解决方案、虚拟继承的工作原理,以及名称隐藏等主题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Introduction

This article is to elaborate several valuable topics that a C++ programmer should be aware of regarding object-oriented programming. Surprisingly, many programmers who have been working for years cannot even grasp the details.

Topics

The topics will be introduced briefly assuming that you have known about the basics of OOP to some extent. If not, please refer to technical books like C++ Primer and Thinking in C++ for much more details. These lessons are in no particular order.

1. Initialization in constructor initializer list and constructor body

For some built-in types (e.g. int, float, double), the difference in efficiency and effect between them is negligible. There are mainly two cases we need to stress.

Initilization of members of class(struct) type.

Prior to executing any codes in the constructor body, every member of class/struct type should be initialized with their default constructor. Here is an example.

class A
{
public:
	A(){ cout<<"Default constructor of A"<<endl; }
	A(int) { cout<<"Constructor with single param of A"<<endl; }
	A(const A& src) { cout<<"Copy constructor of A"<<endl; }
	A& operator=(const A& src){ cout<<"Assignment operator of A"<<endl; return *this; }
};

class B
{
public:
	B(int varForB,int varForA)
	{
		cout<<"Constructor with two params of B"<<endl;
		var = varForB;
		a = A(varForA);
	}
private:
	A a;
	int var;
};

int main()
{
	B b(1,2);
}
 

The output:

Clearly before entering the constructor body of class B, the default constructor of class A is called to initialize member a. After that, member a, which has been initialized once already, is reassigned to the user's expectation. The problem is that if there would be a lot of time-consuming work to do in the constructors of class A, then the run time of this execution will roughly double, i.e. inefficient. If we rewrite the constructor of class B as the following, then member a is simply initialized once bypassing the default constructor, which in fact is what we want initially.

B(int varForB,int varForA):a(varForA)
{
	cout<<"Constructor with two params of B"<<endl;
	var = varForB;
}

The output:

More imprtantly, if we don't explicitly define the default constructor for class A while maintain the constructor with single parameter, we must initialize membera in the initializer list, otherwise it will cause compile error saying 'no default constructor exists for class A'. Why? Because the synthesized default constructor of class A is not available in this case meanwhile there is no explicit one. To conclude, member of class type must be initialized in the initializer list if it has no default constructor. 

Initialization of const and reference members

Const and reference variables must be initialized when defined. After that, you can not assign values to them any more. Hence, the only way to initialize const and reference members is initializing them in the initializer list.

Note that, class members are initialized not in the order of presence in initializer list but in order of their declarations. So you need to be careful about their orders when initializing one with another. Here is an example.

class A
{
public:
	A(int var):a(var),b(a) {} //error! to initialize b with non-initialized member a
private:
	int b;
	int a;
};

2. How does dynamic binding work?

Let's start with a simple example.

class Animal
{
public:
	virtual void Run(){ cout<<" Animal::Run"<<endl; }
	virtual void Drink(){ cout<<" Animal::Drink"<<endl; }
};

class Dog: public Animal
{
public:
	virtual void Run(){ cout<<" Dog::Run"<<endl; }
	virtual void Bark(){ cout<<" Dog::Bark"<<endl; }
};

class Husky: public Dog
{
public:
	virtual void Drink(){ cout<<" Husky::Run"<<endl; }
	virtual void Bark(){ cout<<" Husky::Bark"<<endl; }
};
 

When you define an Animal pointer that points to an object of type Dog as Animal *animal = new Dog(), animal->Run() will resolves to Dog::Run() rather than Animal::Run() at run time owing to dynamic binding. Normally, dynamic binding is enabled by prepending the virtual keyword to a method. Every class that either uses virtual functions or is derived from a class that uses virtual functions is given its own virtual table (abbreviated asVTABLE), which is a lookup table of functions used to resolve function calls at run time. A virtual table contains one entry for each virtual function that can be called by objects of the class. Each entry in this table is simply a function pointer that points to the most-derived function accessible by that class. This table is simply a static array that the compiler sets up at compile time, and meanwhile the compiler also adds to the base class a hidden virtual pointer indicating the starting address of VTABLE, abbreviated asVPTR. Unlike the *this pointer, which is actually a function parameter used by the compiler to resolve self-references, VPTR is a real pointer. Consequently, it makes each class object allocated bigger by the size of a void pointer. 

More importantly, VPTR is inherited by all derived classes. All Animal objects or objects derived from Animal have their VPTR in the same place (often at the beginning of the object), so the compiler can pick the VPTR out of the object. The VPTR points to the starting address of the VTABLE. All the VTABLE function addresses are laid out in the same order, regardless of the specific type of the object. 

For the above codes, there are three virtual tables: one for Animal, one for Dog and one for Husky. Moreover, VPTR is generated automatically for Animal and inherited by Dog and Husky. Remember that when these VTABLEs are filled out, each entry is filled out with the most-derived function an object of that class type can call. So, VTABLE for class Animal is filled with Animal::Run and Animal::Drink. Simple and straightforward, as Dog::Run has overriden Animal::Run in class Dog, Dog::Run becomes the most-derived one that a Dog object can access, making the entry for Run points to Dog::Run. As Drink function is not overrided in Dog but declared virtual in the base class Animal, the compiler uses the address of the base-class version in Dog. In addtion, Dog defines a new virtual method Bark which will be added to its VTABLE after all of those from Animal. In this way, the virtual table of class Dog contains in order Dog::Run, Animal::Drink and Dog::Bark. Similarly, the virtual table of class Husky contains Dog::Run, Husky::Drink and Husky::Bark. Here is what it looks like graphically.

Check the following codes (Classes Animal, Dog and Husky follow the definition above).

int main()
{
	Animal* animal = new Animal();
	animal->Drink(); // call Animal::Drink()
	animal->Run();   // call Animal::Run()
	delete animal;

	animal = new Dog();
	animal->Drink(); // call Animal::Drink()
	animal->Run();   // call Dog::Run()
	//animal->Bark();  // compile error, class Animal has no member 'Bark'
	delete animal;

	animal = new Husky();
	animal->Run();   // call Dog::Run()
	animal->Drink(); // call Husky::Drink()
	delete animal;

	Dog* dog = new Husky();
	dog->Run();      // call Dog::Run()
	dog->Drink();    // call Husky::Drink()
	dog->Bark();     // call Husky::Bark()
	delete dog;
}

Given that Animal *animal=new Dog(), what happens when we call animal->Run()? First, the program recognizes that Run() is a virtual function. Second, uses animal->VTPR to get to Dog′s VTABLE. Third, it looks up which version of Run() to call in Dog′s virtual table, which has been set to Dog::Run(). Therefore, animal->Run() resolves to Dog::Run(). All the VTABLE function addresses are laid out in the same order, regardless of the specific type of the object. Run( ) is first, Drink( ) is second, and Bark( ) is third. The compiler knows that regardless of the specific object type, the Drink() function is always at the location VPTR+1. Note that an Animal pointer can only access the members defined in class Animal, even though it points to a Dog object. Sometimes indeed you need to call Animal::Run rather than Dog::Run through animal pointer, in this case you take the advantage of scope operator (::).

int main()
{
	Animal *animal=new Dog();
	animal->Run();          // call Dog::Run()
	animal->Animal::Run();  // explicitly call Animal::Run()
}

Remember that dynamic binding takes effect when and only when you’re using a pointer or reference to a base class. Check what happens in the following codes.

int main()
{
	Dog dog;
	Animal animal(dog);
	animal.Drink(); // call Animal::Drink()
	animal.Run();   // call Animal::Run()
	animal.Bark();  // compile error, class Animal has no member 'Bark'
}

By Animal animal(dog), the system simply copies the values of common members from Dog to Animal and leave the hidden VTPR of Animal still pointing to its own virtual table.

Then consider what if I set all memory bytes to which a base class pointer points to zero? 

Dog *dog = new Husky();
memset(dog,0,sizeof(Dog));
dog->Bark();  // runtime error
First of all, dynamic binding won't work for sure because the VTABLE turns out missing by assigning VPTR to NULL. More than just disabling dynamic binding, the system will throw runtime error because it can not locate the definition of Bark().

Now, let's take a look at another interesting case on virtual method.

class Dog
{
public:
	virtual void Bark(const char* name = "dog"){ cout<<"Dog::Bark:"<<name<<endl; }
};

class Husky: public Dog
{
public:
	virtual void Bark(const char* name = "husky"){ cout<<"Husky::Bark:"<<name<<endl; }
};

int main()
{
	Dog *dog = new Husky();
	dog->Bark();  // displays 'Husky::Bark::dog'
}
See the difference? For all virtual methods Bark, there is a default argument, however they are configured differently in Dog and Husky. If the call dog->Bark() omits the argument, it resolves to Husky::Bark but uses the default argument of Dog::Bark instead. This is because normally default argument must be determined by the compiler and the only way that compiler determines it is through checking the type definition. At this stage of compiling, the dynamic binding has not started yet and thus dynamic assigned class type is still unknown for any usage. To conclude, which default argument of the virtual method is used is determined simply by the definition type of its owner pointer or reference.

3. Constructor and destructor: virtual or not?

The answer is that constructor is never allowed to be virtual and destructor especially in the base class of a hierarchy is suggested to be virtual even if it is empty.

Constructor is not allowed to be virtual

The reason is quite simple. When an object containing virtual functions is created, its VPTR (short for virtual pointer) must be initialized to point to the proper VTABLE (short for virtual table). This must be done before there’s any possibility of calling a virtual function. Because the constructor has the job of bringing an object into existence, it is also the constructor’s job to set up the VPTR. The compiler secretly inserts code into the beginning of the constructor that initializes the VPTR. If you don’t explicitly create a constructor for a class, the compiler will synthesize one for you. If the class has virtual functions, the synthesized constructor will include the proper VPTR initialization code. If the constructor is virtual, the program has to look it up in VTABLE upon calling the virtual constructor. Here comes the contradiction that the VPTR and VTABLE are not configured before the execution of constructor. So, class constructor must be non-virtual.

Destructor is suggested to be virtual

To start with, let's take a look at the following codes.
class Base
{
private:
	int* baseDataPtr;
public:
	Base(int capacity)
	{ 
		cout<<"Base::Constructor"<<endl;
		baseDataPtr = new int[capacity]; 
	}
	~Base()
	{
		cout<<"Base::Destructor"<<endl;
		delete[] baseDataPtr; 
	}
};

class Derived: public Base
{
private:
	int* derivedDataPtr;
public:
	Derived(int capacity):Base(capacity)
	{ 
		cout<<"Derived::Constructor"<<endl;
		derivedDataPtr = new int[capacity]; 
	}
	~Derived()
	{ 
		cout<<"Derived::Destructor"<<endl;
		delete []derivedDataPtr; 
	}
};
What happens if we execute the following codes?
Base *obj = new Derived(10);
delete obj;
The output:
Appearantly, the destructor of Derived is not executed, leading to memory leak by the dangling pointer derivedDataPtr. If the pointer is to the base class, the compiler can only know to call the base-class version of the destructor during delete. So if the base pointer points to a derived class object, deletion of it will miss the destructor of the derived class, which is absolutely dangerous. The solution to this problem is define virtual destructor for the base class of the hierarchy. Modify the destructor of Base as follows:
virtual ~Base()
{
	cout<<"Base::Destructor"<<endl;
	delete[] baseDataPtr; 
}

The output:


Problem solved! Once the destructor of the base class is declared to be virtual, all destructors down to the most derived class are virtual (explicitly defined as virtual or not). What makes the difference between virtual constructor and virtual destructor is that whenever destructor is called, the VPTR is always out there ready for use. Virtual functions work for destructors as they do for all other functions except constructors.

Pure virtual destructor

Pure virtual destructor s is quite strange and confusing. It is OK to define the base-class destructor as pure virtual, however you must provide a function body for it. You could even leave the function body empty. How can a virtual function be “pure” if it needs a function body? Seems wired, but just accept it. One possible explanation is that since the destructor is called from the derived to the base, you can not just leave the last one without definition. The consequence of doing so is that the class is abstract now and you cannot create an object of it any more.

class Base
{
public:
	virtual ~Base() = 0;
};

Base::~Base(){} // error thrown without this

class Derived: public Base
{
public:
	~Derived() { cout<<"Dereived"<<endl; }
};

int main()
{
	Base *obj = new Derived(); //OK
	delete obj; // OK

	Base a; // compile error! cannot create a object for an abstract class
}

Calling virtual functions in constructor

Now we are pretty sure that constructor must be non-virtual while destructor better be virtual. Then what happens if we call a virtual function inside the constructor? Inside an ordinary member function you can imagine what will happen – the virtual call is resolved at runtime because the object cannot know whether it belongs to the class the member function is in, or some class derived from it. However, you are totally wrong if you intuitively assume its consistency inside the constructor. Take a look at the following codes.

class Base
{
public:
	Base(){ Display(); }
	virtual void Display() { cout<<"Base::Diaplay"<<endl; }
};

class Derived: public Base
{
public:
	void Display() { cout<<"Dereived:Display"<<endl; }
};

int main()
{
	Base *obj = new Derived(); // display 'Base::Diaplay'
	delete obj;
}

This is quite not the case. If you call a virtual function inside a constructor, only the local version of the function is used. That is, the virtual mechanism doesn’t work within the constructor. Two reasons:

  1. The constructor will be called one by one down to the most derived class. So inside any constructor, the object may only be partially formed – you can only know that the base-class objects have been initialized, but its derived class still remain uninitialized. If you could do this inside a constructor, you’d be calling a function that might manipulate members that hadn’t been initialized yet. This is quite dangerous and should not be encouraged.
  2. When a constructor is called, one of the first things it does is initializing its VPTR. When the compiler generates code for that constructor, it generates code for a constructor of that class, not a base class and not a class derived from it. So the VPTR it uses must be for the VTABLE of that class. The VPTR remains initialized to that VTABLE for the rest of the object’s lifetime unless this isn’t the last constructor call. If a more-derived constructor is called afterwards, that constructor sets the VPTR to its VTABLE, and so on, until the last constructor finishes. While all this series of constructor calls is taking place, each constructor has set the VPTR to its own VTABLE. If it uses the virtual mechanism for function calls, it will produce only a call through its own VTABLE, not the most-derived VTABLE. Actually this is one of the reasons why the constructors are called in order from base to most-derived.

Calling virtual functions in destructor

This case is similar with that in constructor. As the destructors are called in order from most-derived to base, so inside the destructor, one thing is for sure that those more derived objects have already been destroyed. If virtual mechanism were used inside the destructor, the call of virtual functions will rely on proportion of destroyed objects, which is unaccepted. So if you call a virtual function inside a destructor, only the local version of the function is used. That is, the virtual mechanism doesn’t work within the destructor.

4. Multiple inheritance

Distinct from single inheritance, multiple inheritance is defined as deriving directly from more than one class. Multiple inheritance, as a good structuring tool, is an important feature in object-oriented C++ programming, however it will complicate the design and introduce ambiguity in many situations, making it a quite controversial topic. We will introduce the problem and solution step by step later, but before that let's consider the following example.

class Animal
{
public:
	int m_age;
	Animal(int age):m_age(age){ cout<<"Animal::(Age:"<<m_age<<")::Constructor"<<endl; }
	virtual void Run(){ cout<<"Animal::(Age:"<<m_age<<")::Run"<<endl; }
	virtual void Eat(){ cout<<"Animal::(Age:"<<m_age<<")::Eat"<<endl; } 
	virtual ~Animal(){ cout<<"Animal::(Age:"<<m_age<<")::Destructor"<<endl; }      
};

class Donkey: public Animal 
{
public:
	int m_height;
	Donkey(int age, int height):Animal(age),m_height(height){ cout<<"Donkey::(Age:"<<m_age<<",height:"<<m_height<<")::Constructor"<<endl; }
	void Run(){ cout<<"Donkey::(Age:"<<m_age<<",height:"<<m_height<<")::Run"<<endl; }
	~Donkey(){ cout<<"Donkey::(Age:"<<m_age<<",height:"<<m_height<<")::Destructor"<<endl; }      
};

class Horse: public Animal
{
public:
	int m_weight;
	Horse(int age, int weight):Animal(age),m_weight(weight){ cout<<"Horse::(Age:"<<m_age<<",weight:"<<m_weight<<")::Constructor"<<endl; }
	void Eat(){ cout<<"Horse::(Age:"<<m_age<<",weight:"<<m_weight<<")::Eat"<<endl; }
	~Horse(){ cout<<"Horse::(Age:"<<m_age<<",weight:"<<m_weight<<")::Constructor"<<endl; }
};

class Mule: public Horse, public Donkey
{
public:
	Mule(int age, int height, int weight):Donkey(age+1,height),Horse(age+2,weight){ cout<<"Mule::Constructor"<<endl; }
	~Mule(){ cout<<"Mule::Destructor"<<endl; }
};
 

The situation is: class Mule inherits from both Donkey and Horse, which happen to inherit from the same base class Animal. C++ doesn't impose any constraint on the number of classes that can be inherited, and we here use two in our example for simplicity. Intuitively, since both Donkey and Horse classes are using the method Run() from the base class, calling the method Run() from a Mule object will be ambiguous, i.e. the compiler can't know which implementation of Run() to use, the one from the Donkey class or the one from the Horse class. This is for sure a multiple inheritance case and we always call it the "diamond problem" owing to the diamond-shaped inheritance diagram. 

Memory layout for multiple inheritance

First, think about one question: what will sizeof outputs for each of the classes? The answer will be:

sizeof(Animal) = 8; sizeof(Donkey) = 12 ; sizeof(Horse) = 12; sizeof(Mule) = 24;

What confuses us is definitely the size of Mule. Inheritance simply puts the implementation of two objects one after another, but in this case Mule is both a Donkey and a Horse, so the Animal class gets duplicated inside the Mule object. The Memory layout for an Mule object should be like:

Mule
Mule::vptr for Horse      -> Animal::Run | Horse::Eat
Horse::Animal::m_age
Horse::m_weight
Mule::vptr for Donkey    -> Donkey::Run | Animal::Eat
Donkey::Animal::m_age
Donkey::m_height

The compiler will generate two pointers (vptr) to visual table, one covers the inheritance path through Horse and the other through Donkey. We will cover what vptr does inMemory Layout for virtual inheritance. When calling an ambiguous accessible function member or data member from a Mule object, you should use scope operator (::) to specify which base class is it from, Horse or Donkey, otherwise will cause compile error. Check out the following calls.

Mule mule(0,20,30);
mule.Eat();         // compile error
mule.m_age = 10;    // compile error
mule.m_height = 10; // OK, m_height is not ambiguous
mule.Horse::Eat();  // OK, Eat is specified to come from Horse

We can respectively point a Animal, Horse or Donkey pointer to a Mule object. However when doing so, we have to adjust the pointer value to make it point to the corresponding section of the Mule layout. Horse and Mule can point to the exact same address, and we can treat the Horse object as if it were a Mule object (and obviously a similar thing happens for Right). However, as two Animal duplicates are ambiguous, we need to first convert the Mule pointer into Horse either Horse or Donkey pointer and then convert further to desired Animal pointer.

Mule mule(0,20,30);
	
Horse* horse = (Horse*)(&mule);  // the same as Horse* horse = (Horse*)((int*)&mule);
horse->Run(); // Animal::(Age:2)::Run
horse->Eat(); // Horse::(Age:2,weight:30)::Eat

Donkey *donkey = (Donkey*)(&mule); // the same as Donkey *donkey = (Donkey*)(((int*)&mule)+3);
donkey->Run(); // Donkey::(Age:1,height:20)::Run
donkey->Eat(); // Animal::(Age:1)::Eat

Animal* animalOfHorse = (Animal*)((Horse*)&mule); //the same as Animal* animalOfHorse = (Animal*)((int*)&mule);
animalOfHorse->Run(); // Animal::(Age:2)::Run
animalOfHorse->Eat(); // Horse::(Age:2,weight:30)::Eat

Animal* animalOfDonkey = (Animal*)((Donkey*)&mule); // the same as Animal* animalOfDonkey = (Animal*)(((int*)&mule)+3);
animalOfDonkey->Run(); // Donkey::(Age:1,height:20)::Run
animalOfDonkey->Eat(); // Animal::(Age:1)::Eat

Using virtual inheritance

Besides the ambiguity in calling members, a second problem that can occur with the diamond pattern is that if the two classes derived from the same base class, and that base class has one or more members, then those members will be duplicated in the joining class. In our example codes, a Mule object will have two Animal duplicates. Fortunately, C++ allows us to solve this problem by usingvirtual inheritance. In order to prevent the compiler from giving an error we use the keywordvirtual when we inherit from the base class Animal in both derived classes:

class Animal
{...};

class Donkey: public virtual Animal
{...};

class Horse: public virtual Animal
{...};

class Mule: public Horse, public Donkey
{...};
 

Here, Animal is called the virtual base class. When we use virtual inheritance, we are guaranteed to get only a single instance of the common base class. In other words, the Mule class will have only a single instance of the Animal class, shared by both the Horse and Donkey classes. By having a single instance of Animal, we've resolved the compiler's immediate issue, the ambiguity, and the code will compile fine.

Because there is only a single instance of a virtual base class that is shared by multiple classes that inherit from it, the constructor for a virtual base class is not called by the class that inherits from it (which is how constructors are called, when each class has its own copy of its parent class) since that would mean the constructor would run multiple times. Instead, the constructor is called by the constructor of the concrete class. In the example above, the class Mule directly calls the constructor for Animal. If you need to pass any arguments to the Animal constructor, you would do so using an initialization list, as usual:

class Mule: public Horse, public Donkey
{
public:
	Mule(int age, int height, int weight): Animal(age),Donkey(age+1,height),Horse(age+2,weight){ cout<<"Mule::Constructor"<<endl; }
	~Mule(){ cout<<"Mule::Destructor"<<endl; }
};

With the above definition of Mule class, when you run Mule mule(0,20,30), the output will be:

Look, in the constructor initializer list of Mule, through it seems that Donkey and Horse could have both initilized Animal::m_age, the truth is that only Animal constructor worked. If Animal has default constructor and Mule did not directly initialize Animal in its initializer list, the default constructor of Animal will be called. If Animal contains no default constructor, then you must explicitly pass the argument(s) to Animal constructor in Mule initializer list. One more thing, how does the compiler determine the order of calling constructor function of Horse and Donkey? Deducing from the above codes and displays, obviously the answer is that it exactly follows the order you define them in the inheritance list (class Mule: public Horse, public Donkey). The destructors run in the opposite order of the constructors. Consider the following codes and what is the value of Animal::m_age finally?

class Animal
{
public:
	int m_age;
	Animal(int age):m_age(age){ cout<<"Animal("<<age<<")"<<endl; }
	Animal():m_age(-1){ cout<<"Animal()"<<endl;}
	virtual ~Animal(){}      
};

class Donkey: public virtual Animal 
{
public:
	Donkey(int age):Animal(age){ cout<<"Donkey("<<age<<")"<<endl;}     
};

class Horse: public virtual Animal
{
public:
	Horse(int age):Animal(age){ cout<<"Horse("<<age<<")"<<endl; }     
};

class Mule: public Horse, public Donkey
{
public:
	Mule(int age):Donkey(age+1),Horse(age+2){ cout<<"Mule("<<age<<")"<<endl;  }
};

Animal::m_age turns out  -1 and the output is

Clearly, the default constructor of Animal is called as no Animal constructors in Mule's initializer list is explicitly called. Both of Animal initialization part in Horse's and Donkey's initializer list are blocked. By the way, the constructors for virtual base classes(Animal) are always called before the constructors for non-virtual base classes(Horse, Donkey). This ensures that a class inheriting from a virtual base class can be sure the virtual base class is safe to use inside the inheriting class's constructor.

Cross delegation in virtual Inheritance

Let's start with the following example.

class Animal
{
public:
	virtual void Run(){ cout<<"Animal::Run"<<endl; }
	virtual void Jump() = 0;
};

class Donkey: public virtual Animal 
{
public:
	void Run(){ cout<<"Donkey::Run"<<endl; Jump(); }
};

class Horse: public virtual Animal
{
public:
	void Jump(){ cout<<"Horse::Jump"<<endl;} 
};

class Mule: public Horse, public Donkey {};

int main()
{ 
	Mule mule;
	mule.Run();
}

 

If a Mule object calls Run(), what will be the output? Or will the codes invoke any compile error because Donkey::Run calls Jump while it has not implemented pure virtual function Jump from Animal? The answer is that the codes are fine and output as follows:

A powerful technique that arises from using virtual inheritance is to delegate a method from a class in another class by using a common abstract base class. This is also called cross delegation. This is kind of a weird behavior, but let's take it for the sake of illustration. Since through virtual inheritance, Animal has only one instance in a Mule object and it's OK that either of the derived classes implement the abstract function from the virtual base class. However in the above hierarchy we can instantiate only the Mule and Horse class because Animal and Donkey are abstract. Then what if we add the following member function into Donkey that implement abstract function Jump as well?

void Jump(){ cout<<"Donkey::Jump"<<endl; }

As this moment, both of Horse and Donkey have implemented Jump, which will absolutely introduce ambiguity and raise compile error. Mule does not know which one to inherit Jump from. The same as if we override Animal::Run in Horse. However such ambiguity can be cleared if we override Jump in Mule as

class Animal
{
public:
	virtual void Run() = 0;
};

class Donkey: public virtual Animal 
{
public:
	void Run(){}
};

class Horse: public virtual Animal
{
public:
	void Run(){}
};

class Mule: public Horse, public Donkey
{
public:
	void Run(){} // here overrides Run, so no constrains on Donkey and Horse
};
 

This override is not ambiguous. Then the output changes into:

Memory Layout for virtual inheritance

We have illustrated on the memory layout of multiple inheritance. Since there is only one single instance of the visual base class owing to visual inheritance, intuitively the memory layout here is a little bit different. Try to figure out the size of a Mule object defined below:

<pre class="cpp" name="code">class Animal
{
public:
	int m_age;
	virtual void Run() { cout<<"Animal::Run"<<endl;}
};

class Donkey: public virtual Animal 
{
public:
	int m_height;
	void Run(){ cout<<"Donkey::Run"<<endl;}
};

class Horse: public virtual Animal
{
public:
	int m_weight;
	void Run(){ cout<<"Horse::Run"<<endl;}
	virtual void Eat(){ cout<<"Horse::Eat"<<endl;}
};

class Mule: public Horse, public Donkey
{
public:
	int m_color;
	void Run(){ cout<<"Mule::Run"<<endl;}
};

If we remove virtual in the inheritance list, the size of Mule is 28 and memory layout is like:

Mule (Diamond problem)
Mule::vptr for Horse         ->     Mule::Run  |   Horse::Eat
Horse::Animal::m_age
Horse::m_weight
Mule::vptr for Donkey       ->    Mule::Run
Donkey::Animal::m_age
Donkey::m_height
Mule::m_color

For visual inheritance case, the size of Mule is still 28, but the memory layout is altered. It becomes,
Mule (Visual inheritance)
Mule::vf_ptr for Horse            ->   Horse::Eat
Mule::offset_ptr for Horse      ->   offset to Animal: 20 bytes |  offset to top: 4 bytes
Horse::m_weight
Mule::offset_ptr for Donkey    ->  offset to Animal: 12 bytes |  offset to top: 12 bytes
Donkey::m_height
Mule::m_color
Mule::vf_ptr for Animal            ->  Mule::Run
Animal::m_age
Interestingly, the visual table (VTABLE) stores the offsets separately of Horse's and Donkey's entry pointers (referred to as offset_ptr) to Animal instance. As Horse contains a virtual function which is not an overriden version from its parent classes (Animal), a visual pointer is added to locate the function address entry in VTABLE. No vf_ptr is added for Donkey because the only virtual function Run overrides the Animal::Run and will be considered in Mule::vf_ptr for Animal. In fact, vf_ptr and offset_ptr points to the same VTABLE and the entries are stored in a reverse order. We distinguish between vf_ptr and offset_ptr by names only to show the different usages of corresponding VTABLE entries. To see how the virtual table ( vtable) is used, consider the following C++ code.
Mule mule;
Horse* horse = (Horse*)&mule;
Donkey* donkey = (Donkey*)&mule;
Now how does the runtime find horse->m_age and donkey->m_age? Since *horse points to Mule:vf_ptr for Horse, *horse first find the offset_ptr and then simply increase by the associated offset to right locate Animal instance. For *donkey, as it right points to Mule::offset_ptr for Donkey, just increase by the specified offset. Meanwhile, "offset to top" is used to address downcasting as in the following example, just think about it by yourself.
Donkey* donkey = new Mule; // *donkey points to Mule::offset_ptr for Donkey
Mule* newMule = (Mule*)donkey;  // decrease by the specified "offset to top"

Name Hiding

If you inherit a class and provide a new definition for one of its member functions, there are two possibilities. The first is that you provide the exact signature and return type in the derived class definition as in the base class definition. This is called redefining for ordinary member functions and overriding when the base class member function is a virtual function. But what happens if you change the member function argument list or return type in the derived class? Here’s an example:

class Base
{
public:
	virtual Base* A(){ cout<<"virtual Base* Base::A()"<<endl; return NULL;}
	void A(int arg) { cout<<"void Base::A(int)"<<endl; }
};

class Derived_1:public Base
{
public:
	// orverride virtual method
	Base* A(){ cout<<"virtual Base* Derived_1::A()"<<endl; return NULL;}  
};

class Derived_2: public Base
{
public:
	// different argument list
	Base* A(int arg){ cout<<"Base* Derived_2::A(int)"<<endl; return NULL; } 
};

class Derived_3: public Base
{
public:
	// const function
	Base* A() const { cout<<"Base* Derived_3::A() const"<<endl; return NULL; } 
};

class Derived_4: public Base
{
public:
	// diffferent return type for non-virtual functions
	int A(int arg){ cout<<"int Derived_4::A(int)"<<endl; return 0;}  
};

class Derived_5: public Base
{
public:
	// complile error, diffferent return type for virtual functions
	int A(){ cout<<"int Derived_5::A()"<<endl; }  
};

class Derived_6: public Base
{
public:
	// diffferent return type for virtual functions, return derived class pointer
	Derived_6* A() { cout<<"Derived_6* Derived_6::A()"<<endl; return NULL;}
};

void main()
{
	Derived_1 derived_1;
	derived_1.A();  // output "virtual Base* Derived_1::A()"
	derived_1.A(1); // compile error, cannot find A(int)

	Derived_2 derived_2;
	derived_2.A();  // compile error, cannot find A()
	derived_2.A(1); // output "Base* Derived_2::A(int)"

	Derived_3 derived_3;
	derived_3.A(1); //compile error, cannot find A(int)
	derived_3.A();  // output ""Base* Derived_3::A() const"

	Derived_4 derived_4;
	derived_4.A(); // compile error, 
	derived_4.A(1);  // output "int Derived_4::A(int)"

	Derived_6 derived_6;
	derived_6.A();  // output "Derived_6* Derived_6::A()"
}

With the codes and outputs above, we have the following conclusions:

  • In general, anytime you redefine an overloaded function name from the base class, all the other versions are automatically hidden inside the derived class. When attempting to call a function through a derived class instance, the compiler will locate it only by its name, regardless of neither its argument list nor its return type. If the compiler can not find any in the derived class, then it will continue to check in the base class.  Once the compiler find one or more in derived class, it will skip searching in the base class and try to locate the one with matched arguments and return type, otherwise compiler will throw a error. Derived_1 overrides the virtual function and the other non-virtual one (void A(int)) is hidden.
  • If you redefine a member function from the base class by altering its argument list, the Base class versions will be hidden. Check Derived_2.
  • If you redefine a member function from the base class by making it const, the Base class versions will be hidden. Check Derived_3. Meanwhile in this case, Derived_3 has not overriden the virtual function.

Base* a = &derived_3; 
a->A();  // output "virtual Base* Base::A()"

  • It is fine to redefine a non-virtual member function fromm the base class only by altering its return type, and the Base class versions will be hidden. Check Derived_4.
  • It will cause compile error to redefine a virtual member function fromm the base class only by altering its return type (check Derived_5), except when the new version returns a derived class pointrer or reference of that of the base class version (check Derived_6).

If you change the interface of the base class by modifying the signature and/or return type of a member function from the base class, then you’re using the class in a different way than inheritance is normally intended to support. It doesn’t necessarily mean you’re doing it wrong, it’s just that the ultimate goal of inheritance is to support polymorphism, and if you change the function signature or return type then you are actually changing the interface of the base class. If this is what you have intended to do then you are using inheritance primarily to reuse code, and not to maintain the common interface of the base class (which is an essential aspect of polymorphism). In general, when you use inheritance this way it means you’re taking a general-purpose class and specializing it for a particular need – which is usually, but not always, considered the realm of composition.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值