当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义

在编译时进行名字查找:

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型与动态类型不一致:

 #include <iostream>
using namespace std; class A{
public:
// A();
// ~A();
ostream& print(ostream& os) const {
os << x;
return os;
} protected:
int x;
}; class B : public A{
public:
// B();
// ~B();
ostream& f(ostream &os) const {
os << y;
return os;
} private:
int y;
}; int main(void) {
B b; b.f(cout) << endl;//正确,b的动态类型和静态类型都是B,B::f对b是可见的 A *a = &b;
// b->f(cout) << endl;//错误,静态类型是A,B::f对A的对象是不可见的 B *p = &b;
p->f(cout) << endl;//正确,静态类型是B,B::f对B的对象是可见的 return ;
}

名字冲突与继承:

派生类的成员将隐藏同名的基类成员:

 #include <iostream>
using namespace std; struct Base{
Base() : mem() {} protected:
int mem;
}; struct Derived : Base{
Derived(int i) : mem(i) {} int get_mem() {
return mem;
} protected:
int mem;//隐藏基类中的mem
}; int main(void) {
Derived d();
cout << d.get_mem() << endl;// return ;
}

通过域作用符可以使用隐藏的成员:

 #include <iostream>
using namespace std; struct Base{
Base() : mem() {} protected:
int mem;
}; struct Derived : Base{
Derived(int i) : mem(i) {} int get_mem() {
// return mem;
return Base::mem;
} protected:
int mem;//隐藏基类中的mem
}; int main(void) {
Derived d();
cout << d.get_mem() << endl;// return ;
}

c++ 成员函数调用过程。假设我们调用 p_>mem()(或者 obj.mem()):

首先确定 p(或 obj) 的静态类型。

在 p(或 obj) 的静态类型对应的类中查找 mem。如果找不到,则依次在直接基类中不断查找直至达到继承链的顶端。如果仍然找不到则编译器报错

一旦找到了 mem,就进行常规的类型检查以确认对于当前找到的 mem,本次调用是否合法。

假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

——如果 mem 是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行哪个版本,依据是对象的动态类型

——反之,如果 mem 不少虚函数或者我们是通过对象(非指针或引用)进行的调用,则编译器将产生一个常规函数调用

名字查找先于类型检查:

 #include <iostream>
using namespace std; struct Base{
int memfcn();
}; int Base::memfcn() {
//
} struct Derived : Base{
int memfcn(int);//隐藏基类的memfcn,即便形参不同
}; int Derived::memfcn(int a) {
//
} int main(void) {
Derived d;
Base b; b.memfcn();//调用Base::memfcn
d.memfcn();//调用Derived::memfcn // d.memfcn();//错误,参数列表为空的memfcn被隐藏了
d.Base::memfcn();//正确,调用Base::memfcn return ;
}

注意:如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员仍然会被隐藏

虚函数与作用域:

由上面这段话我们可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了。假如基类与派生类的虚函数形参列表不同,则基类的同名函数会在派生类中被隐藏,我们也就无法通过基类的引用或指针调用派生类的虚函数了:

 #include <iostream>
using namespace std; class Base{
public:
virtual int fcn();
}; int Base::fcn() {
cout << "int Base::fcn" << endl;
} class D1 : public Base{
public:
// 隐藏基类的fcn,这个fcn不是虚函数
// D1继承了Base::fcn()的定义
int fcn(int);//形参列表与Base中的fcn不一致
virtual void f2(){//是一个新的虚函数,在Base中不存在
cout << "void D1::f2" << endl;
}
}; int D1::fcn(int a) {
cout << "int D1::fcn int" << endl;
} // void D1::f2() { // } class D2 : public D1{
public:
int fcn(int);//是一个非虚函数,隐藏了D1::fcn(int)
int fcn();//覆盖了Base的虚函数fcn
void f2();//覆盖了D1的虚函数f2
}; int D2::fcn(int a) {
cout << "int D2::fcn int" << endl;
} int D2::fcn() {
cout << "int D2::fcn" << endl;
} void D2::f2() {
cout << "void D2::f2" << endl;
} int main(void) {
Base bobj;
D1 d1obj;
D2 d2obj; Base *bp1 = &bobj;
Base *bp2 = &d1obj;
Base *bp3 = &d2obj;
bp1->fcn();//虚调用,将在运行时调用Base::fcn
bp2->fcn();//虚调用,将在运行时调用Base::fcn,因为在D1中没有覆盖Base::fcn
bp3->fcn();//虚调用,将在运行时调用D2::fcn,D2中覆盖了Base::fcn
cout << endl; D1 *d1p = &d1obj;
D2 *d2p = &d2obj; // bp2->f2();//错误,静态类型Base中没有名为f2的成员 d1p->f2();//虚调用,将在运行时调用D1::f2
d2p->f2();//虚调用,将在运行时调用D2::f2
cout << endl; Base *p1 = &d2obj;
D1 *p2 = &d2obj;
D2 *p3 = &d2obj; // p1->fcn(42);//错误,Base中没有接受一个int的fcn
p2->fcn();//静态类型D1中的fcn(int)是一个非虚函数,执行静态绑定,调用D1::fcn(int)
p3->fcn();//静态类型D2中的fcn(int)是一个非虚函数,执行静态绑定,调用D2::fcn(int) // 输出:
// int Base::fcn
// int Base::fcn
// int D2::fcn // void D1::f2
// void D2::f2 // int D1::fcn int
// int D2::fcn int
return ;
}

注意:如果派生类中没有覆盖基类中的虚函数,则运行时解析为基类定义的版本

覆盖重载的函数:

成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的 0 个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有版本,或者一个也不覆盖。

我们可以为重载的成员提供一条 using 声明语句,这样我们就无需覆盖基类中的每一个版本。using 声明指定一个名字而不指定形参列表,所以一条基类成员函数的 suing 声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就可以了,而无需为继承而来的其它函数重新定义。

构造函数与拷贝控制:

虚析构函数:

如果基类的析构函数不是虚函数,则 delete 一个指向派生类对象的基类指针将产生未定义的行为。因此我们通常应该给基类定义一个虚析构函数。同时,定义了析构函数应该定义拷贝和赋值操作这条准则在这里不适用。还需要注意的是,定义了任何拷贝控制操作后编译器都不会再合成移动操作

合成拷贝控制与继承:

基类或派生类的合成拷贝控制成员的行为与其它合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员一次进行初始化、赋值或销毁操作。此外,这些合成的成员还负责适用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作

派生类中删除的拷贝控制与基类的关系:

就像其它任何类的情况一样,基类或派生类也能处于同样的原因将其合成默认构造函数或者任何一个拷贝控制成员被定义成删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员城外删除的函数:

如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或不可访问的,则派生类中对应的成员将是被删除的,原因是编译器不能适用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作

如果在基类中有一个不可访问或删除的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类的基类部分

编译器不会合成一个删除掉的移动操作。当我们使用 =default 请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的:

 #include <iostream>
using namespace std; class B{
public:
B(){}
B(const B&) = delete;
// ~B();
}; class D : public B{
public:
// D();
// ~D(); }; int main(void) {
D d;//正确,D的合成默认构造函数使用B的默认构造函数
// D d2(d);//错误,D的合成拷贝构造函数是被删除的
// D d3(std::move(d));//错误,没有移动构造函数,所以会调用拷贝构造函数,但是D的合成拷贝构造函数是删除的 return ;
}

移动操作与继承:

大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作(派生类的合成移动构造函数会调用基类的移动构造函数来完成继承自基类的数据成员的移动操作),所以当我们确实需要执行移动操作时应该首先在基类中定义:

 class Quote{
public:
Quote() = default;
Quote(const Quote&) = default;
Quote(Quote&&) = default;
Quote& operator=(const Quote&) = default;
Quote& operator=(Quote&&) = default;
~Quote() = default; };

注意:一旦基类定义了自己的移动操作,那么它必须同时显式地定义拷贝操作,否则拷贝操作成员将被默认合成为删除函数

派生类的拷贝控制成员:

移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。对象的成员是被隐式销毁的,类似的,派生类对象的基类部分也是自动销毁的:

 class D : public Base{
public:
//Base::~Base被自动调用
~D(){
// 该处由用户定义释放派生类资源的操作
} };

对象销毁的顺序与创建的顺序相反

注意:在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(赋值或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(赋值或移动)构造函数

不要在构造函数和析构函数中调用虚函数:

如果构造函数或析构函数调用了某个虚函数,则执行与构造函数或析构函数所属类型相对应的虚函数版本(这可能不是我们所期望的)

详见:http://blog.csdn.net/xtzmm1215/article/details/45130929

继承的构造函数:

构造函数不能以常规的方法继承:

 #include <iostream>
using namespace std; class A{
public:
A(int a = , int b = ) : x(a), y(b) {}
int get_x(void) const {
return x;
} int get_y(void) const {
return y;
} protected:
int x, y;
}; class B : public A{
// 没有使用 using 声明来继承构造函数,所以 B 没有继承 A(int a, int b)
// 由于我们没有在 B 中定义构造函数,所以 B 中会合成默认构造函数
}; int main(void) {
// B b(1, 2);//错误,不能使用构造函数
B b;//使用 B 类中编译器合成的默认构造函数
cout << b.get_x() << " " << b.get_y() << endl;//0 0
// 派生类的合成默认构造函数会自动调用基类的默认构造函数来初始化基类的数据成员 return ;
}

我们可以通过 using 声明来使派生类继承基类的构造函数:

 #include <iostream>
using namespace std; class A{
public:
A() : x(-), y(-) {} A(int a, int b) : x(a), y(b) {}
int get_x(void) const {
return x;
} int get_y(void) const {
return y;
} protected:
int x, y;
}; class B : public A{
using A::A;//通过using说明,继承了 A 中定义的构造函数
// 对于基类的每个构造函数,编译器都在派生类中生成一个形参列表与之完全相同的构造函数 //派生类不会继承基类的默认构造函数, 由于我们没有在 B 中定义默认构造函数,所以 B 中会合成默认构造函数
}; int main(void) {
B b(, );//通过 using 声明,B 继承了 A 中定义的构造函数
cout << b.get_x() << " " << b.get_y() << endl;//1 2 B c;//使用合成的默认构造函数
cout << c.get_x() << " " << c.get_y() << endl;//-1 -1
// 派生类的合成默认构造函数会自动调用基类的构造函数来初始化基类的数据成员 return ;
}

注意:通常情况下,using 声明只是令某个名字在当前作用域内可见。而当作用于构造函数时,using 声明语句将令编译器产生代码,但不会改变该构造函数的访问级别。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表与之完全相同的构造函数。

一个 using 声明不能指定 explicit 或 constexpr。如果基类的构造函数是 explicit 或者 constexpr 的,则其继承的构造函数也拥有相同的属性

派生类类不能继承默认,拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

 #include <iostream>
using namespace std; class A{
public:
A() : x(-), y(-) {//默认构造函数
cout << "ji lei mo ren gou zao han shu" << endl;
}
A(int a, int b) : x(a), y(b) {//构造函函数
cout << "ji lei gou zao han shu" << endl;
}
A(const A &a) : x(a.x), y(a.y) {//拷贝构造函数
cout << "ji lei kao bei gou zao han shu" << endl;
}
A(A &&a) : x(a.x), y(a.y) {//移动构造函数
cout << "ji lei yi dong gou zao han shu" << endl;
} virtual A& operator=(const A &a) {//可以写成虚函数,说明拷贝赋值运算符会被派生类继承
this->x = a.x;
this->y = a.y;
cout << "ji lei kao bei fu zhi yun suan fu" << endl;
return *this;
} virtual A& operator=(A &&a) {//可以写成虚函数,说明移动赋值运算符会被派生类继承
this->x = a.x;
this->y = a.y;
cout << "ji lei yi dong fu zhi yun suan fu" << endl;
return *this;
} protected:
int x, y;
}; class B : public A{
using A::A;//通过using说明,继承了 A 中定义的构造函数
// 对于基类的每个构造函数,编译器都在派生类中生成一个形参列表与之完全相同的构造函数 //派生类不会继承基类的默认、拷贝、移动构造函数,
//由于我们没有在 B 中定义默认构造函数,所以 B 中会合成默认构造函数,
//又由于我们没有在派生类中定义任何拷贝控制成员,所以会合成拷、移动构造函数
}; int main(void) {
B b(, );//通过 using 声明,B 继承了 A 中定义的构造函数
cout << endl; B c;//使用合成的默认构造函数
// 派生类的合成默认构造函数会自动调用基类的构造函数来初始化基类继承自部分的数据成员
cout << endl; B d = c;
// 派生类的合成拷贝构造函数会自动调用基类的拷贝构造函数来拷贝继承自基类部分的数据成员
cout << endl; d = c;//使用继承自基类的拷贝赋值运算符
cout << endl; B e = std::move(b);
// 派生类的合成移动构造函数会自动调用基类的移动构造函数来移动继承自基类部分的数据成员
cout << endl; e = std::move(b);//使用继承自基类的移动赋值运算符
cout << endl; return ;
}

注意:

派生类的合成默认构造函数、合成拷贝构造函数、合成移动构造函数中会自动使用基类的对应构造函数来操作派生类中继承自基类部分数据成员,而派生类的新成员执行默认初始化

定义派生类的默认、拷贝、移动构造函数时我们应该调用基类中的对应操作来完成继承自基类部分的数据成员的操作,否则我们可能无法完成继自基类的 private 数据成员的操作

当我们在派生类中覆盖拷贝、移动赋值运算符时,应该调用基类中的对应操作来完成继承自基类部分的数据成员的操作,否则我们可能无法完成继自基类的 private 数据成员的操作

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

容器与继承:

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

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

  1. 【C++ Primer | 15】访问控制与继承、继承中的类作用域

    1. 只有D继承B的方式是public时,用户代码才能使用派生类向基类的转换:如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换. 2. 不论D以什么方式继承B,D的成员函数和友员函数都 ...

  2. 【c++】面向对象程序设计之继承中的类作用域

    当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内. 一个对象.引用或指针的静态类型决定了该对象的哪些成员是可见的.即使静态类型与动态类型可能不一致,但我们使用哪些成员仍然是由静态类型决定的.基 ...

  3. 不可或缺 Windows Native (21) - C++: 继承, 组合, 派生类的构造函数和析构函数, 基类与派生类的转换, 子对象的实例化, 基类成员的隐藏(派生类成员覆盖基类成员)

    [源码下载] 不可或缺 Windows Native (21) - C++: 继承, 组合, 派生类的构造函数和析构函数, 基类与派生类的转换, 子对象的实例化, 基类成员的隐藏(派生类成员覆盖基类成 ...

  4. c++中的类(构造函数,析构函数的执行顺序)

    类对象的初始化顺序 新对象的生成经历初始化阶段(初始化列表显式或者隐式的完成<这部分有点像java里面的初始化块>)——> 构造函数体赋值两个阶段 1,类对象初始化的顺序(对于没有父 ...

  5. python中的类,对象,实例,继承,多态

    ------------恢复内容开始------------ 类 (通俗来讲是 属性和方法的集合) 用来描述具有相同的属性和方法的对象的集合.它定义了该集合中每个对象所共有的属性和方法. 对象,即为类 ...

  6. C++//菱形继承 //俩个派生类继承同一个基类 //又有某个类同时继承俩个派生类 //成为 菱形继承 或者 钻石 继承//+解决

    1 //菱形继承 2 //俩个派生类继承同一个基类 3 //又有某个类同时继承俩个派生类 4 //成为 菱形继承 或者 钻石 继承 5 6 #include <iostream> 7 #i ...

  7. 【C++ Primer | 15】构造函数与拷贝控制

    合成拷贝控制与继承 #include <iostream> using namespace std; class Base { public: Base() { cout << ...

  8. c++中的类之构造函数

    一.构造函数的缘由 本文我们主要来讲解c++中类的构造函数,其中涉及了深拷贝和浅拷贝的问题,这也是在面试笔试中经常会碰到的问题.如果您是第一次听说构造函数,可能会觉得这个名字有点高大上,而它却和实际中 ...

  9. 关于c++11中static类对象构造函数线程安全的验证

    在c++11中,static静态类对象在执行构造函数进行初始化的过程是线程安全的,有了这个特征,我们可以自己动手轻松的实现单例类,关于如何实现线程安全的单例类,请查看c++:自己动手实现线程安全的c+ ...

随机推荐

  1. catkin 工作空间 - Package 组成

    package 是 ROS 软件的基本组织形式,ROS 就是由一个个的 package 组成的 package 是 catkin 的编译基本单元 一个 package 可以包含多个可执行文件(节点) ...

  2. STM32与PC机串口通讯

    有时要将板子的信息输出到电脑上来调试之类的,或者把传感器收集到的数据显示到电脑. 当然了,这只是最基本的串口通信,简单的说,是有一根USB线连着的. mbed上并没有能显示printf的功能.需要自己 ...

  3. HDFS的介绍

    设计思想 分而治之:将大文件.大批量文件,分布式存放在大量服务器上,以便于采取分而治之的方式对海量数据进行运算分析: 在大数据系统中作用:为各类分布式运算框架(如:mapreduce,spark,te ...

  4. nat123安装启动教程帮助

    转自:http://www.nat123.com/Pages_17_291.jsp 本文就nat123安装启动可能遇到的问题及与安全狗影响处理. 下载安装nat123客户端安装包.第一次安装使用,可选 ...

  5. 一些c++

    1.static 静态局部对象: 一旦被创建,在程序结束前都不会被撤销.当定义静态局部对象的函数结束时,静态局部对象不会撤销. 2.内联函数: 避免函数调用的开销. 在函数返回类型前加上关键字 inl ...

  6. 实例解说Linux命令行uniq

    Linux命令uniq的作用是过滤重复部分显示文件内容,这个命令读取输入文件,并比较相邻的行.在正常情况下,第二个及以后更多个重复行将被删去,行比较是根据所用字符集的排序序列进行的.该命令加工后的结果 ...

  7. Money Systems 货币系统(母函数)

    Description 母牛们不但创建了他们自己的政府而且选择了建立了自己的货币系统. [In their own rebellious way],,他们对货币的数值感到好奇. 传统地,一个货币系统是 ...

  8. MySql 里的IFNULL、NULLIF、ISNULL和IF用法

    isnull(expr) 的用法: 如expr 为null,那么isnull() 的返回值为 1,否则返回值为 0. 实例: select ISNULL(NULL) 输出结果: ) 输出结果: IFN ...

  9. less使用变量实现Url的前缀

    @url-prefix: "../../../../../Skin/Template/Default"; .test { background: url("@{url-p ...

  10. lazyload is not a function解决方式

    使用jQuery图片延迟加载插件时,可能会报出$("img").lazyload is not a function的错误.(关于如何使用lazyload插件,请看另外一篇文章:j ...