In the past two lessons, we’ve explored some basics around inheritance in C++ and the order that derived classes are initialized. In this lesson, we’ll take a closer look at the role of constructors in the initialization of derived classes. To do so, we will continue to use the simple Base and Derived classes we developed in the previous lesson:
在本节中,我们要更仔细的研究构造函数在派生类对象构造过程中扮演的角色。我们会继续使用我们之前写出来的最简单的Base和Derived类。
class Base
{
public:
int m_id;
Base(int id=0)
: m_id{ id }
{
}
int getId() const { return m_id; }
};
class Derived: public Base
{
public:
double m_cost;
Derived(double cost=0.0)
: m_cost{ cost } //没有指定构造函数
{
}
double getCost() const { return m_cost; }
};
With non-derived classes, constructors only have to worry about their own members. For example, consider Base. We can create a Base object like this:
对于非派生类而言,构造函数只需要关心他自己的成员。
int main()
{
Base base{ 5 }; // use Base(int) constructor
return 0;
}
Here’s what actually happens when base is instantiated:
以下是在这个实例化过程中发生的事情
-
Memory for base is set aside
分配了内存
-
The appropriate Base constructor is called
合适的构造函数被调用
-
The initialization list initializes variables
初始化列表的初始化变量。就是函数参数定义的右括号后面的那些东西,还记得吗?
-
The body of the constructor executes
构造函数的函数体被执行
-
Control is returned to the caller
CPU控制交还给调用函数
This is pretty straightforward. With derived classes, things are slightly more complex:
这个事情非常简单,但是如果是派生类的话,就变得复杂了一点。
int main()
{
Derived derived{ 1.3 }; // use Derived(double) constructor
return 0;
}
Here’s what actually happens when derived is instantiated:
派生类对象在实例化的时候发生了一下的事情
-
Memory for derived is set aside (enough for both the Base and Derived portions)
足够基类和派生类使用的内存空间首先被分配
-
The appropriate Derived constructor is called
合适的派生类对象构造函数被调用
-
The Base object is constructed first using the appropriate Base constructor. If no base constructor is specified, the default constructor will be used.
基类对象首先通过合适的构造函数被构造。如果没有指定基类的构造函数的话,就会使用默认的构造函数
-
The initialization list initializes variables
初始化列表初始化变量。
-
The body of the constructor executes
构造函数的函数体执行
-
Control is returned to the caller
控制流返回给调用函数
The only real difference between this case and the non-inherited case is that before the Derived constructor can do anything substantial, the Base constructor is called first. The Base constructor sets up the Base portion of the object, control is returned to the Derived constructor, and the Derived constructor is allowed to finish up its job.
这个例子和前面的例子的不同区别在于在派生类对象做后续的操作之前,要基类的构造函数先完成。(构造函数在成员变量的初始化之后才进行构造,构造函数会符合程序员的想象,会在程序员定义的成员变量完成构造之后再使用或者暂时不使用这些成员变量)基类的构造函数完成之后,把控制流交给派生类的构造函数,然后派生类的构造函数现在就能去完成他自己的事情了。
Initializing base class members
One of the current shortcomings of our Derived class as written is that there is no way to initialize m_id when we create a Derived object. What if we want to set both m_cost (from the Derived portion of the object) and m_id (from the Base portion of the object) when we create a Derived object?
我们目前的派生类的一个缺点是在我们创建一个派生类对象的时候没有办法去初始化我们的id成员变量。要是我们想在构造派生类对象的时候既设置派生类的部分,也设置基类的部分的话,怎么做?
New programmers often attempt to solve this problem as follows:
新的程序员可能会这样去尝试
class Derived: public Base
{
public:
double m_cost;
Derived(double cost=0.0, int id=0)
// does not work
: m_cost{ cost }, m_id{ id }
{
}
double getCost() const { return m_cost; }
};
This is a good attempt, and is almost the right idea. We definitely need to add another parameter to our constructor, otherwise C++ will have no way of knowing what value we want to initialize m_id to.
这样做是一个很合理的尝试,已经接近正确答案,但是编译器还不让你这么干。我们确实需要添加一个额外的参数给我们的构造函数,否则的话C++编译器就不知道我们想用什么值来初始化这个变量了。
However, C++ prevents classes from initializing inherited member variables in the initialization list of a constructor. In other words, the value of a member variable can only be set in an initialization list of a constructor belonging to the same class as the variable.
然而,C++不允许在初始化列表里面初始化基类的成员。换句话说在初始化列表里面可以设置的只能是当前这个类里面的定义的成员变量。
Why does C++ do this? The answer has to do with const and reference variables. Consider what would happen if m_id were const. Because const variables must be initialized with a value at the time of creation, the base class constructor must set its value when the variable is created. However, when the base class constructor finishes, the derived class constructors initialization lists are then executed. Each derived class would then have the opportunity to initialize that variable, potentially changing its value! By restricting the initialization of variables to the constructor of the class those variables belong to, C++ ensures that all variables are initialized only once.
为什么C++要这么做呢。思考一下如果id是一个const的话,因为const变量必须在被创建的时候初始化,基类的构造函数要给它的成员变量设置值。但是只有在基类的构造函数完成之后,派生类的初始化列表才开始工作,每一个派生类然后才有机会去初始化变量,潜在地,是有可能修改id的值的。所以做了这样的限制之后(初始化列表只能初始化那些它自己这个类内定义的成员变量),C++保证所有的变量都只被初始化一次。
The end result is that the above example does not work because m_id was inherited from Base, and only non-inherited variables can be initialized in the initialization list.
结果就是上面的例子不能行。因为id是从基类继承的,只有不是继承下来的那些成员变量才能在初始化列表里面进行初始化。
However, inherited variables can still have their values changed in the body of the constructor using an assignment. Consequently, new programmers often also try this:
然而,继承下来的变量仍然可以在构造函数的函数体里面进行访问,因此新的程序员经常做下面这样的尝试
class Derived: public Base
{
public:
double m_cost;
Derived(double cost=0.0, int id=0)
: m_cost{ cost }
{
m_id = id;
}
double getCost() const { return m_cost; }
};
While this actually works in this case, it wouldn’t work if m_id were a const or a reference (because const values and references have to be initialized in the initialization list of the constructor). It’s also inefficient because m_id gets assigned a value twice: once in the initialization list of the Base class constructor, and then again in the body of the Derived class constructor. And finally, what if the Base class needed access to this value during construction? It has no way to access it, since it’s not set until the Derived constructor is executed (which pretty much happens last).
这样做虽然能行,但是如果id是一个const的变量或者是一个引用的话(引用也只能在创建的时候被初始化,还记得吗因为这个毛病引用不能做vector的模板实参)。而且这样做效率也很低,因为id被赋值了两次。最后,如果基类在整个构造过程中想访问这个值的话,由于这个变量在派生类执行前还没有完成它的两次初始化,所以不能这么干。
So how do we properly initialize m_id when creating a Derived class object?
所以应该怎么去适当的初始化这个id呢?
In all of the examples so far, when we instantiate a Derived class object, the Base class portion has been created using the default Base constructor. Why does it always use the default Base constructor? Because we never told it to do otherwise!
在到现在为止的所有例子中,我们实例化一个派生类对象的时候,基类部分已经被基类的默认构造函数所创建。干嘛要每次都用默认构造函数呢?其实是因为我们没有告诉他你去用别的默认构造函数完成基类的构造。(笑哭)
Fortunately, C++ gives us the ability to explicitly choose which Base class constructor will be called! To do this, simply add a call to the base class Constructor in the initialization list of the derived class:
幸运的是,C++给了我们明确选择基类使用的构造函数的能力。为了达到这个目的,只需要在初始化列表里面调用基类特定的构造函数就行。
class Derived: public Base
{
public:
double m_cost;
Derived(double cost=0.0, int id=0)
: Base{ id }, // Call Base(int) constructor with value id!
m_cost{ cost }
{
}
double getCost() const { return m_cost; }
};
Now, when we execute this code:
int main()
{
Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
std::cout << "Id: " << derived.getId() << '\n';
std::cout << "Cost: " << derived.getCost() << '\n';
return 0;
}
The base class constructor Base(int) will be used to initialize m_id to 5, and the derived class constructor will be used to initialize m_cost to 1.3!
Thus, the program will print:
Id: 5
Cost: 1.3
In more detail, here’s what happens:
以下是关于此更详细的说明
-
Memory for derived is allocated.
内存空间首先被分配
-
The Derived(double, int) constructor is called, where cost = 1.3, and id = 5
派生类的构造函数被调用
-
The compiler looks to see if we’ve asked for a particular Base class constructor. We have! So it calls Base(int) with id = 5.
编译器看一下我们是不是要一个特定的基类构造函数。在这里我们指定了一个构造函数。所以它调用了Base(int)
-
The base class constructor initialization list sets m_id to 5
基类的构造函数初始化列表把id设置成5
-
The base class constructor body executes, which does nothing
基类的构造函数体开始执行,什么都不干。
-
The base class constructor returns
基类的构造函数返回
-
The derived class constructor initialization list sets m_cost to 1.3
派生类的构造函数的初始化列表初始化其成员
-
The derived class constructor body executes, which does nothing
派生类构造函数体开始执行
-
The derived class constructor returns
派生类构造函数返回
This may seem somewhat complex, but it’s actually very simple. All that’s happening is that the Derived constructor is calling a specific Base constructor to initialize the Base portion of the object. Because m_id lives in the Base portion of the object, the Base constructor is the only constructor that can initialize that value.
看起来复杂,其实非常简单。发生的一切不过是派生类构造函数调用了一个特定的基类构造函数来初始化基类部分。因为id在基类的部分里面,所以积累的构造函数是唯一一个可以初始化其值的函数。
Note that it doesn’t matter where in the Derived constructor initialization list the Base constructor is called -- it will always execute first.
注意:不管基类的构造函数存在于派生类的构造函数的初始化列表的什么位置,它总会被首先调用。
Now we can make our members private
Now that you know how to initialize base class members, there’s no need to keep our member variables public. We make our member variables private again, as they should be.
现在你知道怎么初始化基类的成员了。没必要让我们的所有的变量保持public了,我们要把它们变回来,变成它们该有的private的样子。
As a quick refresher, public members can be accessed by anybody. Private members can only be accessed by member functions of the same class. Note that this means derived classes can not access private members of the base class directly! Derived classes will need to use access functions to access private members of the base class.
复习一下,public成员总是可以给任何地方访问。private成员只能被同一个类的成员函数访问。注意,这意味着派生类不能直接访问基类的私有成员。派生类需要使用接口函数来访问基类的私有成员。
Consider:
#include <iostream>
class Base
{
private: // our member is now private
int m_id;
public:
Base(int id=0)
: m_id{ id }
{
}
int getId() const { return m_id; }
};
class Derived: public Base
{
private: // our member is now private
double m_cost;
public:
Derived(double cost=0.0, int id=0)
: Base{ id }, // Call Base(int) constructor with value id!
m_cost{ cost }
{
}
double getCost() const { return m_cost; }
};
int main()
{
Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
std::cout << "Id: " << derived.getId() << '\n';
std::cout << "Cost: " << derived.getCost() << '\n';
return 0;
}
In the above code, we’ve made m_id and m_cost private. This is fine, since we use the relevant constructors to initialize them, and use a public accessor to get the values.
在上面的例子中,我们已经把id改成private了。因为我们使用构造函数来初始化它,用一个访问性函数来允许别人拿到其值。
This prints, as expected:
Id: 5
Cost: 1.3
We’ll talk more about access specifiers in the next lesson.
我们会在下一节讨论关于访问限定符的事情。
Another example
Let’s take a look at another pair of classes we’ve previously worked with:
#include <string>
class Person
{
public:
std::string m_name;
int m_age;
Person(const std::string& name = "", int age = 0)
: m_name{ name }, m_age{ age }
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
double m_battingAverage;
int m_homeRuns;
BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
: m_battingAverage{ battingAverage },
m_homeRuns{ homeRuns }
{
}
};
As we’d previously written it, BaseballPlayer only initializes its own members and does not specify a Person constructor to use. This means every BaseballPlayer we create is going to use the default Person constructor, which will initialize the name to blank and age to 0. Because it makes sense to give our BaseballPlayer a name and age when we create them, we should modify this constructor to add those parameters.
棒球运动员只初始化他自己的成员,也没有指定哪个基类的构造函数去使用,所以就使用了默认的构造函数,让name为空,age为0.因为让运动员有一个名字,有一个年龄很有意义。在我们创建他们的时候,我们应该修改这个构造函数,给他们添加两个参数。
Here’s our updated classes that use private members, with the BaseballPlayer class calling the appropriate Person constructor to initialize the inherited Person member variables:
下面的例子中我们更新了两个类,使用了private成员,运动员类调用了合适的Person基类的构造函数来初始化从Person类中继承的变量。
#include <iostream>
#include <string>
class Person
{
private:
std::string m_name;
int m_age;
public:
Person(const std::string& name = "", int age = 0)
: m_name{ name }, m_age{ age }
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
private:
double m_battingAverage;
int m_homeRuns;
public:
BaseballPlayer(const std::string& name = "", int age = 0,
double battingAverage = 0.0, int homeRuns = 0)
: Person{ name, age }, // call Person(const std::string&, int) to initialize these fields
m_battingAverage{ battingAverage }, m_homeRuns{ homeRuns }
{
}
double getBattingAverage() const { return m_battingAverage; }
int getHomeRuns() const { return m_homeRuns; }
};
Now we can create baseball players like this:
int main()
{
BaseballPlayer pedro{ "Pedro Cerrano", 32, 0.342, 42 };
std::cout << pedro.getName() << '\n';
std::cout << pedro.getAge() << '\n';
std::cout << pedro.getHomeRuns() << '\n';
return 0;
}
This outputs:
Pedro Cerrano
32
42
As you can see, the name and age from the base class were properly initialized, as was the number of home runs and batting average from the derived class.
正如你所看到的那样,基类已经被合适的初始化,而且就像全垒打的次数和击打次数一样。
Inheritance chains
Classes in an inheritance chain work in exactly the same way.
一条继承链上的许多类也是这样被构造的
#include <iostream>
class A
{
public:
A(int a)
{
std::cout << "A: " << a << '\n';
}
};
class B: public A
{
public:
B(int a, double b)
: A{ a }
{
std::cout << "B: " << b << '\n';
}
};
class C: public B
{
public:
C(int a , double b , char c)
: B{ a, b }
{
std::cout << "C: " << c << '\n';
}
};
int main()
{
C c{ 5, 4.3, 'R' };
return 0;
}
In this example, class C is derived from class B, which is derived from class A. So what happens when we instantiate an object of class C?
First, main() calls C(int, double, char). The C constructor calls B(int, double). The B constructor calls A(int). Because A does not inherit from anybody, this is the first class we’ll construct. A is constructed, prints the value 5, and returns control to B. B is constructed, prints the value 4.3, and returns control to C. C is constructed, prints the value ‘R’, and returns control to main(). And we’re done!
首先main函数调用C(int,double,char),然后C的构造函数调用B的构造函数,B的构造函数调用A的构造函数,A的构造完成之后开始执行B的构造函数体,B的构造函数体完成之后,再开始执行C的(初始化列表初始化其各自的成员变量在其中)。
Thus, this program prints:
A: 5
B: 4.3
C: R
It is worth mentioning that constructors can only call constructors from their immediate parent/base class. Consequently, the C constructor could not call or pass parameters to the A constructor directly. The C constructor can only call the B constructor (which has the responsibility of calling the A constructor).
值得一提的是:构造函数只能调用其直接父类的构造函数。C的构造函数不能直接地调用或者传递参数给A。C只能通过先给B,然后B再给A的方式。
Destructors
When a derived class is destroyed, each destructor is called in the reverse order of construction. In the above example, when c is destroyed, the C destructor is called first, then the B destructor, then the A destructor.
当派生类被销毁的时候,每一个析构函数以相反的方向进行,在上面的例子中,当c被析构的时候,C的析构函数先被调用,然后是B的析构函数,然后是A的析构函数。
Summary
When constructing a derived class, the derived class constructor is responsible for determining which base class constructor is called. If no base class constructor is specified, the default base class constructor will be used. In that case, if no default base class constructor can be found (or created by default), the compiler will display an error. The classes are then constructed in order from most base to most derived.
在构造一个派生类的时候,派生类的构造函数负责决定哪一个基类的构造函数去使用。如果没有指定基类的构造函数。默认的基类构造函数就会被使用。在那种情况下,如果没有默认的基类构造函数被找到(或者因为你写了带参的构造函数,而没有编译器没有帮你产生默认构造函数),编译器就会报错。类会从继承链的最上面往下挨个的构造。
At this point, you now understand enough about C++ inheritance to create your own inherited classes!
到这儿的话,你就已经知道足够多关于C++继承的能支撑你自己去写继承类的知识了。