OOP3(继承中的类作用域/构造函数与拷贝控制/继承与容器)
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义
在编译时进行名字查找:
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型与动态类型不一致:
1 #include <iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 // A(); 7 // ~A(); 8 ostream& print(ostream& os) const { 9 os << x; 10 return os; 11 } 12 13 protected: 14 int x; 15 }; 16 17 class B : public A{ 18 public: 19 // B(); 20 // ~B(); 21 ostream& f(ostream &os) const { 22 os << y; 23 return os; 24 } 25 26 private: 27 int y; 28 }; 29 30 int main(void) { 31 B b; 32 33 b.f(cout) << endl;//正确,b的动态类型和静态类型都是B,B::f对b是可见的 34 35 A *a = &b; 36 // b->f(cout) << endl;//错误,静态类型是A,B::f对A的对象是不可见的 37 38 B *p = &b; 39 p->f(cout) << endl;//正确,静态类型是B,B::f对B的对象是可见的 40 41 return 0; 42 }
名字冲突与继承:
派生类的成员将隐藏同名的基类成员:
1 #include <iostream> 2 using namespace std; 3 4 struct Base{ 5 Base() : mem(0) {} 6 7 protected: 8 int mem; 9 }; 10 11 struct Derived : Base{ 12 Derived(int i) : mem(i) {} 13 14 int get_mem() { 15 return mem; 16 } 17 18 protected: 19 int mem;//隐藏基类中的mem 20 }; 21 22 int main(void) { 23 Derived d(42); 24 cout << d.get_mem() << endl;//42 25 26 return 0; 27 }
通过域作用符可以使用隐藏的成员:
1 #include <iostream> 2 using namespace std; 3 4 struct Base{ 5 Base() : mem(0) {} 6 7 protected: 8 int mem; 9 }; 10 11 struct Derived : Base{ 12 Derived(int i) : mem(i) {} 13 14 int get_mem() { 15 // return mem; 16 return Base::mem; 17 } 18 19 protected: 20 int mem;//隐藏基类中的mem 21 }; 22 23 int main(void) { 24 Derived d(42); 25 cout << d.get_mem() << endl;//0 26 27 return 0; 28 }
c++ 成员函数调用过程。假设我们调用 p_>mem()(或者 obj.mem()):
首先确定 p(或 obj) 的静态类型。
在 p(或 obj) 的静态类型对应的类中查找 mem。如果找不到,则依次在直接基类中不断查找直至达到继承链的顶端。如果仍然找不到则编译器报错
一旦找到了 mem,就进行常规的类型检查以确认对于当前找到的 mem,本次调用是否合法。
假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
——如果 mem 是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行哪个版本,依据是对象的动态类型
——反之,如果 mem 不少虚函数或者我们是通过对象(非指针或引用)进行的调用,则编译器将产生一个常规函数调用
名字查找先于类型检查:
1 #include <iostream> 2 using namespace std; 3 4 struct Base{ 5 int memfcn(); 6 }; 7 8 int Base::memfcn() { 9 // 10 } 11 12 struct Derived : Base{ 13 int memfcn(int);//隐藏基类的memfcn,即便形参不同 14 }; 15 16 int Derived::memfcn(int a) { 17 // 18 } 19 20 int main(void) { 21 Derived d; 22 Base b; 23 24 b.memfcn();//调用Base::memfcn 25 d.memfcn(10);//调用Derived::memfcn 26 27 // d.memfcn();//错误,参数列表为空的memfcn被隐藏了 28 d.Base::memfcn();//正确,调用Base::memfcn 29 30 return 0; 31 }
注意:如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员仍然会被隐藏
虚函数与作用域:
由上面这段话我们可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了。假如基类与派生类的虚函数形参列表不同,则基类的同名函数会在派生类中被隐藏,我们也就无法通过基类的引用或指针调用派生类的虚函数了:
1 #include <iostream> 2 using namespace std; 3 4 class Base{ 5 public: 6 virtual int fcn(); 7 }; 8 9 int Base::fcn() { 10 cout << "int Base::fcn" << endl; 11 } 12 13 class D1 : public Base{ 14 public: 15 // 隐藏基类的fcn,这个fcn不是虚函数 16 // D1继承了Base::fcn()的定义 17 int fcn(int);//形参列表与Base中的fcn不一致 18 virtual void f2(){//是一个新的虚函数,在Base中不存在 19 cout << "void D1::f2" << endl; 20 } 21 }; 22 23 int D1::fcn(int a) { 24 cout << "int D1::fcn int" << endl; 25 } 26 27 // void D1::f2() { 28 29 // } 30 31 class D2 : public D1{ 32 public: 33 int fcn(int);//是一个非虚函数,隐藏了D1::fcn(int) 34 int fcn();//覆盖了Base的虚函数fcn 35 void f2();//覆盖了D1的虚函数f2 36 }; 37 38 int D2::fcn(int a) { 39 cout << "int D2::fcn int" << endl; 40 } 41 42 int D2::fcn() { 43 cout << "int D2::fcn" << endl; 44 } 45 46 void D2::f2() { 47 cout << "void D2::f2" << endl; 48 } 49 50 int main(void) { 51 Base bobj; 52 D1 d1obj; 53 D2 d2obj; 54 55 Base *bp1 = &bobj; 56 Base *bp2 = &d1obj; 57 Base *bp3 = &d2obj; 58 bp1->fcn();//虚调用,将在运行时调用Base::fcn 59 bp2->fcn();//虚调用,将在运行时调用Base::fcn,因为在D1中没有覆盖Base::fcn 60 bp3->fcn();//虚调用,将在运行时调用D2::fcn,D2中覆盖了Base::fcn 61 cout << endl; 62 63 D1 *d1p = &d1obj; 64 D2 *d2p = &d2obj; 65 66 // bp2->f2();//错误,静态类型Base中没有名为f2的成员 67 68 d1p->f2();//虚调用,将在运行时调用D1::f2 69 d2p->f2();//虚调用,将在运行时调用D2::f2 70 cout << endl; 71 72 Base *p1 = &d2obj; 73 D1 *p2 = &d2obj; 74 D2 *p3 = &d2obj; 75 76 // p1->fcn(42);//错误,Base中没有接受一个int的fcn 77 p2->fcn(42);//静态类型D1中的fcn(int)是一个非虚函数,执行静态绑定,调用D1::fcn(int) 78 p3->fcn(42);//静态类型D2中的fcn(int)是一个非虚函数,执行静态绑定,调用D2::fcn(int) 79 80 // 输出: 81 // int Base::fcn 82 // int Base::fcn 83 // int D2::fcn 84 85 // void D1::f2 86 // void D2::f2 87 88 // int D1::fcn int 89 // int D2::fcn int 90 return 0; 91 }
注意:如果派生类中没有覆盖基类中的虚函数,则运行时解析为基类定义的版本
覆盖重载的函数:
成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的 0 个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有版本,或者一个也不覆盖。
我们可以为重载的成员提供一条 using 声明语句,这样我们就无需覆盖基类中的每一个版本。using 声明指定一个名字而不指定形参列表,所以一条基类成员函数的 suing 声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就可以了,而无需为继承而来的其它函数重新定义。
构造函数与拷贝控制:
虚析构函数:
如果基类的析构函数不是虚函数,则 delete 一个指向派生类对象的基类指针将产生未定义的行为。因此我们通常应该给基类定义一个虚析构函数。同时,定义了析构函数应该定义拷贝和赋值操作这条准则在这里不适用。还需要注意的是,定义了任何拷贝控制操作后编译器都不会再合成移动操作
合成拷贝控制与继承:
基类或派生类的合成拷贝控制成员的行为与其它合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员一次进行初始化、赋值或销毁操作。此外,这些合成的成员还负责适用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作
派生类中删除的拷贝控制与基类的关系:
就像其它任何类的情况一样,基类或派生类也能处于同样的原因将其合成默认构造函数或者任何一个拷贝控制成员被定义成删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员城外删除的函数:
如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或不可访问的,则派生类中对应的成员将是被删除的,原因是编译器不能适用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作
如果在基类中有一个不可访问或删除的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类的基类部分
编译器不会合成一个删除掉的移动操作。当我们使用 =default 请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的:
1 #include <iostream> 2 using namespace std; 3 4 class B{ 5 public: 6 B(){} 7 B(const B&) = delete; 8 // ~B(); 9 }; 10 11 class D : public B{ 12 public: 13 // D(); 14 // ~D(); 15 16 }; 17 18 19 int main(void) { 20 D d;//正确,D的合成默认构造函数使用B的默认构造函数 21 // D d2(d);//错误,D的合成拷贝构造函数是被删除的 22 // D d3(std::move(d));//错误,没有移动构造函数,所以会调用拷贝构造函数,但是D的合成拷贝构造函数是删除的 23 24 return 0; 25 }
移动操作与继承:
大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作(派生类的合成移动构造函数会调用基类的移动构造函数来完成继承自基类的数据成员的移动操作),所以当我们确实需要执行移动操作时应该首先在基类中定义:
1 class Quote{ 2 public: 3 Quote() = default; 4 Quote(const Quote&) = default; 5 Quote(Quote&&) = default; 6 Quote& operator=(const Quote&) = default; 7 Quote& operator=(Quote&&) = default; 8 ~Quote() = default; 9 10 };
注意:一旦基类定义了自己的移动操作,那么它必须同时显式地定义拷贝操作,否则拷贝操作成员将被默认合成为删除函数
派生类的拷贝控制成员:
移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。对象的成员是被隐式销毁的,类似的,派生类对象的基类部分也是自动销毁的:
1 class D : public Base{ 2 public: 3 //Base::~Base被自动调用 4 ~D(){ 5 // 该处由用户定义释放派生类资源的操作 6 } 7 8 };
对象销毁的顺序与创建的顺序相反
注意:在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(赋值或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(赋值或移动)构造函数
不要在构造函数和析构函数中调用虚函数:
如果构造函数或析构函数调用了某个虚函数,则执行与构造函数或析构函数所属类型相对应的虚函数版本(这可能不是我们所期望的)
详见:http://blog.csdn.net/xtzmm1215/article/details/45130929
继承的构造函数:
构造函数不能以常规的方法继承:
1 #include <iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 A(int a = 0, int b = 0) : x(a), y(b) {} 7 int get_x(void) const { 8 return x; 9 } 10 11 int get_y(void) const { 12 return y; 13 } 14 15 protected: 16 int x, y; 17 }; 18 19 class B : public A{ 20 // 没有使用 using 声明来继承构造函数,所以 B 没有继承 A(int a, int b) 21 // 由于我们没有在 B 中定义构造函数,所以 B 中会合成默认构造函数 22 }; 23 24 int main(void) { 25 // B b(1, 2);//错误,不能使用构造函数 26 B b;//使用 B 类中编译器合成的默认构造函数 27 cout << b.get_x() << " " << b.get_y() << endl;//0 0 28 // 派生类的合成默认构造函数会自动调用基类的默认构造函数来初始化基类的数据成员 29 30 return 0; 31 }
我们可以通过 using 声明来使派生类继承基类的构造函数:
1 #include <iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 A() : x(-1), y(-1) {} 7 8 A(int a, int b) : x(a), y(b) {} 9 int get_x(void) const { 10 return x; 11 } 12 13 int get_y(void) const { 14 return y; 15 } 16 17 protected: 18 int x, y; 19 }; 20 21 class B : public A{ 22 using A::A;//通过using说明,继承了 A 中定义的构造函数 23 // 对于基类的每个构造函数,编译器都在派生类中生成一个形参列表与之完全相同的构造函数 24 25 //派生类不会继承基类的默认构造函数, 由于我们没有在 B 中定义默认构造函数,所以 B 中会合成默认构造函数 26 }; 27 28 int main(void) { 29 B b(1, 2);//通过 using 声明,B 继承了 A 中定义的构造函数 30 cout << b.get_x() << " " << b.get_y() << endl;//1 2 31 32 B c;//使用合成的默认构造函数 33 cout << c.get_x() << " " << c.get_y() << endl;//-1 -1 34 // 派生类的合成默认构造函数会自动调用基类的构造函数来初始化基类的数据成员 35 36 return 0; 37 }
注意:通常情况下,using 声明只是令某个名字在当前作用域内可见。而当作用于构造函数时,using 声明语句将令编译器产生代码,但不会改变该构造函数的访问级别。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表与之完全相同的构造函数。
一个 using 声明不能指定 explicit 或 constexpr。如果基类的构造函数是 explicit 或者 constexpr 的,则其继承的构造函数也拥有相同的属性
派生类类不能继承默认,拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
1 #include <iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 A() : x(-1), y(-1) {//默认构造函数 7 cout << "ji lei mo ren gou zao han shu" << endl; 8 } 9 A(int a, int b) : x(a), y(b) {//构造函函数 10 cout << "ji lei gou zao han shu" << endl; 11 } 12 A(const A &a) : x(a.x), y(a.y) {//拷贝构造函数 13 cout << "ji lei kao bei gou zao han shu" << endl; 14 } 15 A(A &&a) : x(a.x), y(a.y) {//移动构造函数 16 cout << "ji lei yi dong gou zao han shu" << endl; 17 } 18 19 virtual A& operator=(const A &a) {//可以写成虚函数,说明拷贝赋值运算符会被派生类继承 20 this->x = a.x; 21 this->y = a.y; 22 cout << "ji lei kao bei fu zhi yun suan fu" << endl; 23 return *this; 24 } 25 26 virtual A& operator=(A &&a) {//可以写成虚函数,说明移动赋值运算符会被派生类继承 27 this->x = a.x; 28 this->y = a.y; 29 cout << "ji lei yi dong fu zhi yun suan fu" << endl; 30 return *this; 31 } 32 33 protected: 34 int x, y; 35 }; 36 37 class B : public A{ 38 using A::A;//通过using说明,继承了 A 中定义的构造函数 39 // 对于基类的每个构造函数,编译器都在派生类中生成一个形参列表与之完全相同的构造函数 40 41 //派生类不会继承基类的默认、拷贝、移动构造函数, 42 //由于我们没有在 B 中定义默认构造函数,所以 B 中会合成默认构造函数, 43 //又由于我们没有在派生类中定义任何拷贝控制成员,所以会合成拷、移动构造函数 44 }; 45 46 int main(void) { 47 B b(1, 2);//通过 using 声明,B 继承了 A 中定义的构造函数 48 cout << endl; 49 50 B c;//使用合成的默认构造函数 51 // 派生类的合成默认构造函数会自动调用基类的构造函数来初始化基类继承自部分的数据成员 52 cout << endl; 53 54 B d = c; 55 // 派生类的合成拷贝构造函数会自动调用基类的拷贝构造函数来拷贝继承自基类部分的数据成员 56 cout << endl; 57 58 d = c;//使用继承自基类的拷贝赋值运算符 59 cout << endl; 60 61 B e = std::move(b); 62 // 派生类的合成移动构造函数会自动调用基类的移动构造函数来移动继承自基类部分的数据成员 63 cout << endl; 64 65 e = std::move(b);//使用继承自基类的移动赋值运算符 66 cout << endl; 67 68 return 0; 69 }
注意:
派生类的合成默认构造函数、合成拷贝构造函数、合成移动构造函数中会自动使用基类的对应构造函数来操作派生类中继承自基类部分数据成员,而派生类的新成员执行默认初始化
定义派生类的默认、拷贝、移动构造函数时我们应该调用基类中的对应操作来完成继承自基类部分的数据成员的操作,否则我们可能无法完成继自基类的 private 数据成员的操作
当我们在派生类中覆盖拷贝、移动赋值运算符时,应该调用基类中的对应操作来完成继承自基类部分的数据成员的操作,否则我们可能无法完成继自基类的 private 数据成员的操作
当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参
容器与继承:
当我们希望在容器中存储具有继承关系的对象时,在容器中存放基类(智能)指针而非对象 ,因为其动态类型既可以是基类类型,也可以是派生类类型