当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义
在编译时进行名字查找:
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型与动态类型不一致:
#include<bits/stdc++.h>
using namespace std;
class A{
protected:
int x=0;
public:
ostream& print(ostream&os){
os<<x<<endl;
}
};
class B:public A{
public :
void f(ostream&os){
os<<x<<endl;
}
void ff(ostream&os){
os<<y<<endl;
}
private:
int y;
};
int main(){
A *a=new B;
a->print(cout);//正确,a的静态类型决定print是可见的
//a->f(cout);//报错,a的静态类型决定f不可见
B *b=new B;
b->f(cout);//正确,B的静态类型决定f可见,在编译过程中,A的作用域包含B的作用域,所以x也是可见的
return 0;
}
名字冲突与继承:
派生类的成员将隐藏同名的基类成员:
#include<bits/stdc++.h>
using namespace std;
class A{
protected:
int x=5;
};
class B:public A{
public :
void f(ostream&os){
os<<x<<endl;
}
private:
int x=0;
};
int main(){
B b;
b.f(cout);//因为B中有一个x,隐藏了父类的名字
return 0;
}
名字冲突与继承:
派生类会隐藏父类的名字
可以通过域运算符来使用隐藏的成员
#include<bits/stdc++.h>
using namespace std;
class A{
protected:
int x=5;
};
class B:public A{
public :
void f(ostream&os){
os<<x<<endl;
}
void f1(ostream&os){
os<<A::x<<endl;
}
private:
int x=0;
};
int main(){
B b;
b.f(cout);//因为B中有一个x,隐藏了父类的名字
b.f1(cout);//可以通过域运算符来使用隐藏的成员
return 0;
}
名字查找先于类型检查:
#include<bits/stdc++.h>
using namespace std;
class A{
public :
int f(){}
};
class B:public A{
public:
int f(int a){}
};
int main(){
B b;
b.f(10);
b.f();//错误:名字查找优于类型检查,所以首先名字查找到B::F,然后进行类型检查,然后报错,,
return 0;
}
注意:如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员仍然会被隐藏
虚函数与作用域:
由上面这段话我们可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了。假如基类与派生类的虚函数形参列表不同,则基类的同名函数会在派生类中被隐藏,我们也就无法通过基类的引用或指针调用派生类的虚函数了:
虚函数的形参列表必须相同是因为:如果不一样,就会出现小作用域隐藏大作用域名字的现象。
#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(42);//静态类型D1中的fcn(int)是一个非虚函数,执行静态绑定,调用D1::fcn(int)
p3->fcn(42);//静态类型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;
}
** 覆盖重载的函数:**
成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的 0 个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有版本,或者一个也不覆盖。
我们可以为重载的成员提供一条 using 声明语句,这样我们就无需覆盖基类中的每一个版本。using 声明指定一个名字而不指定形参列表,所以一条基类成员函数的 suing 声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就可以了,而无需为继承而来的其它函数重新定义。
class Base
{
public:
void func() { printf("Base func()\n"); };
void func(int a) { printf("Base func(int a)\n");}
};
class D1 : public Base
{
public:
void func(string& str) { printf("D1 func(string& str)\n"); }
};
int main(void)
{
D1 d1;
d1.func(); //报错,基类的func()函数已被隐藏
d1.func(2); //报错,基类的func(int)函数已被隐藏
return 0;
}
显然,派生类D1的func(string& str)函数把基类Base中所有名为”func”的函数都隐藏了,要想通过D1调用func()和func(int)函数,要么在D1类中重新定义函数,要么使用using关键字将Base类中所有”func”成员包含到派生类中:
class D1 : public Base
{
public:
using Base::func; //将基类的func所有重载实例到包含到D1中
void func(string& str) { printf("D1 func(string& str)\n"); }
};
int main(void)
{
D1 d1;
d1.func();
d1.func(2);
return 0;
}
using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以将函数的所有重载实例添加到派生类作用域中。此时,派生类只需要定义自身特有的函数,不需要为继承而来的其他函数重新定义。这样,外界对派生类没有重新定义的重载函数的访问实际上是对using声明点的访问。
构造函数与拷贝控制
继承关系对基类拷贝控制的最直接的影响是基类应该定义一个虚析构函数:
如果基类指针指向派生类。那么基类指针就必须清楚他应该执行派生类的析构函数
只要基类的析构函数是虚函数,那么就能确认所有的派生类都是虚函数:
A *it=new A;
delete it;
it=new B;
delete it;//如果A 有虚析构函数,那么就能正确删除B,否则会发生未定义的行为
虚析构函数阻止合成移动操作:
继承关系对基类拷贝控制的另外的影响是基类定义一个虚析构函数后,编译器不会为其合成移动操作,即便=default之后也不会!!!
合成拷贝控制和继承:
基类或派生类的合成拷贝控制成员的行为与其它合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员一次进行初始化、赋值或销毁操作。此外,这些合成的成员还负责适用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作
就比如:B继承A,C继承B。C的构造函数会指向B的,B的会执行A的。析构,拷贝操作也是如此
派生类中删除的拷贝控制与基类的关系:
基类或者派生类都可能会将合成默认构造函数或者拷贝控制成员定义成被删除的函数。某些定义基类的方式可能导致有的派生类成员成为删除的函数:
1、如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或不可访问的,则派生类中对应的成员将是被删除的,原因是编译器不能适用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作:
#include<bits/stdc++.h>
using namespace std;
class A{
public :
int x;
A(int a):x(a){};
};
class B:public A{};
int main(){
B b;//基类默认构造函数是删除的,所以这里无法自动合成默认构造函数
return 0;
}
如果在基类中有一个不可访问或删除的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类的基类部分
#include<bits/stdc++.h>
using namespace std;
class A{
private :
~A()=delete;
};
class B:public A{};
int main(){
B *b=new B;//析构函数是删除的
delete b;
}
编译器不会合成一个删除掉的移动操作。当我们使用 =default 请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的:
#include<bits/stdc++.h>
using namespace std;
class B{
public :
virtual B(){}
B(const B&)=delete;
};
class D:public B{
public :
};
int main(){
D d;//D有合成的默认构造函数
D d1(d);//错误,拷贝构造函数删除
D d2(std::move(d));//错误:有了虚构造函数,就没有了移动构造函数,就转而使用拷贝构造函数,但是拷贝构造函数是被删除掉的
}
移动操作与继承:
大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作(派生类的合成移动构造函数会调用基类的移动构造函数来完成继承自基类的数据成员的移动操作),所以当我们确实需要执行移动操作时应该首先在基类中定义:
#include<bits/stdc++.h>
using namespace std;
class A{
public :
A()=default;
A(const A&)=default;
A(const A&&)=default;
A& operator=(const A&)=default;
A& operator=(const A&&)=default;
virtual ~A()=default;
};
int main(){}
//注意:一旦基类定义了自己的移动操作,那么它必须同时显式地定义拷贝操作,否则拷贝操作成员将被默认合成为删除函数
派生类的拷贝控制成员:
移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。对象的成员是被隐式销毁的,类似的,派生类对象的基类部分也是自动销毁的:
class D : public Base{
public:
//Base::~Base被自动调用
~D(){}// 该处由用户定义释放派生类资源的操作
};
对象销毁的顺序与创建的顺序相反
注意:在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(赋值或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(赋值或移动)构造函数
不要在构造函数和析构函数中调用虚函数:
C++prime556**:当我们创建一个对象的时候,需要把对象的类和构造函数的类看做同一个。**
如果构造函数或析构函数调用了某个虚函数,则执行与构造函数或析构函数所属类型相对应的虚函数版本(这可能不是我们所期望的)
继承的构造函数:
类不能继承默认、拷贝、和移动构造函数,如果派生类没有直接定义这些函数,编译器会为派生类合成。
派生类继承构造函数的方式是提供using声明:
#include<bits/stdc++.h>
using namespace std;
class A{};
class B{
public :
using A::A;//继承A的构造函数
};
通常using只是让某个名字在当前作用域可见,但是当作用域构造函数的时候,using会产生代码:
B():A(){}
继承的构造函数的特点:
1、构造函数的using不会改变构造函数的访问级别。基类的私有构造函数在派生类中还是私有,protected和public类似。
2、using 不能指定explicit和constexpr。继承的构造函数有和基类相同的属性。
3、当基类构造函数有默认实参的时候,派生类会获得多个继承的构造函数。每个构造函数会省略掉一个含有默认实参的形参。
#include<bits/stdc++.h>
using namespace std;
class A{
public :
int x,y;
A(int a,int b=0):x(a),y(b){}
};
class B:public A{
public :
//会产生两个构造函数 (int,int)和(int b=0)
};
int main(){}
4、默认、拷贝、移动构造函数不会被继承
容器和继承
当我们希望在容器中存储具有继承关系的对象时,在容器中存放基类(智能)指针而非对象 ,因为其动态类型既可以是基类类型,也可以是派生类类型。
否则会发生切掉~