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 数据成员的操作

当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参

容器与继承:

当我们希望在容器中存储具有继承关系的对象时,在容器中存放基类(智能)指针而非对象 ,因为其动态类型既可以是基类类型,也可以是派生类类型

原文地址:https://www.cnblogs.com/geloutingyu/p/8453270.html

时间: 2024-10-14 23:48:06

OOP3(继承中的类作用域/构造函数与拷贝控制/继承与容器)的相关文章

面向对象程序设计——抽象基类,访问控制与继承,继承中的类作用域,拷贝函数与拷贝控制

一.抽象基类 1)纯虚函数 和普通的虚函数不同,一个纯虚函数无须定义.我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数.其中,=0只能出现在类内部的虚函数声明语句处. 值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部.也就是说,我们不能在类的内部为一个=0的函数提供函数体. 2)含有纯虚函数的类是抽象基类 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类.抽象基类负责定义接口,而后续的其他类可以覆盖该接口.我们不能直接创建一个抽象

【c++】继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内. 一个对象.引用或指针的静态类型决定了该对象的哪些成员是可见的.即使静态类型与动态类型可能不一致,但我们使用哪些成员仍然是由静态类型决定的.基类指针(引用)即使指向派生类对象,仍然不能通过该指针(引用)来访问派生类中定义的成员,即使是public. 名字查找与继承: 假设调用p->mem() 1.首先确定p的静态类型 2.在p的静态类型对应的类中查找,如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端,如果找遍了该类及其基类仍然找

c++--类的构造函数与拷贝控制

类(class)与结构体(struct)的位移区别在于:默认情况下,类的派生方式和访问权限是private的,struct的派生方式和访问权限是public的. 构造函数 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数. 特点1:不同于其他成员函数,构造函数不能被声明为const的(参见7.1.2,P231). 当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其"常量属性".因此,构造函数在const对象的构造过程中

【足迹C++primer】54、继承类的范围,构造函数和拷贝控制

继承类的范围,构造函数和拷贝控制 当用派生类执行函数的时候,首先会在当前的类里面找 如果找不到就一级一级地往上找. Name Lookup Happens at Compile Time class Quote { public: Quote()=default; Quote(const string &book, double sales_price):bookNo(book), price(sales_price) {cout<<"Quote gouzhao functi

c++学习笔记5,多重继承中派生类的构造函数与析构函数的调用顺序(二)

现在来测试一下在多重继承,虚继承,MI继承中虚继承中构造函数的调用情况. 先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include <iostream> using namespace std; class base { public: base() { cout<<"base created!"<<endl; }

【编程题】编写String类的构造函数、拷贝构造函数、析构函数和赋值函数

[编程题]编写String类的构造函数.拷贝构造函数.析构函数和赋值函数 [题目]:请编写如下4个函数 1 class String 2 { 3 public: 4 String(const char *str = NULL);// 普通构造函数 5 String(const String &other); // 拷贝构造函数 6 ~ String(void); // 析构函数 7 String & operate =(const String &other);// 赋值函数 8

对C++中派生类的构造函数和析构函数的认识

一:构造函数 形式:派生类名::派生类名:基类名1(参数1),基类名2(参数2),--基类名n(参数n),数据成员1(参数1),数据成员2(参数2),--数据成员n(参数n){ 各种操作的说明 } 执行过程:先执行基类的构造函数,再进行数据成员的赋值,最后执行函数体. 其中基类名和数据成员的顺序是由在派生类的定义中声明的顺序决定执行的顺序的,因此它们的顺序是任意的,但为了可读性,还是最好按顺序写. 如果基类只有默认构造函数,则基类名和参数表可以不用写出来. 二:复制构造函数 派生类的构造函数的形

c++中派生类的构造函数

Son(char *n, int *b, int g, char *add, int *gir, double s) : Father(n, b, g) 派生类构造函数名(总参数列表):基类构造函数名(参数列表) {派生类中新增数据成员初始化语句} 冒号前面的部分是派生类构造函数的主干,基类的构造函数想同,但它的总参数列表中包括基类构造函数所需要的参数和对派生类新增的数据成员初始化所需参数 冒号后面的部分是要调用的基类构造函数及其参数

基类和派生类中构造函数和拷贝控制

15.26 定义Quote和Bulk_quote的拷贝控制成员,令其与合成的版本行为一致.为这些成员以及其他构造函数添加打印状态的语句,使得我们能够知道正在运行哪个程序.使用这些类编写程序,预测程序将创建和销毁哪些对象.重复实验,不断比较你的预测和实际输出结果是否相同,直到预测完全准确再结束. Quote.h #ifndef QUOTE_H #define QUOTE_H #include<iostream> #include<string> using namespace std