访问控制与继承:
一个类使用protected关键字来声明那些希望与派生类分享但是不想被其他公共访问的成员。其性质如下:
(1)和私有成员类似,受保护的成员对类的用户是不可访问的
(2)和公有成员类似,受保护的成员对于派生类的成员和友元来时是可访问的。
(3)派生类的成员或友元只能通过派生类对象来访问 基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
某个类对其继承而来的成员的访问权限受两个因素的影响:
(1)基类中该成员的访问说明符
(2)派生类的派生列表中的访问说明符
对基类成员的访问权限只与基类中的访问说明符有关。派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
派生访问说明符还可以继承自派生类的新类的访问权限。
派生类向基类转换的可访问性:
对于代码中的某个给定节点来说,如果基类的共有成员是可访问的,则派生类向基类的类型转换也是可访问的。反之,则不行。
//用户代码可以使用派生类向基类转换如果派生类继承基类的方式是公有的,其他方式则不行
//考虑类base和它的派生类已知
Base *p=&d1;//d1的类型是Pub_Derv,正确
p=&d2;//d2的类型是Priv_Derv,错误
p=&d3;//d3的类型是Prot_Derv,错误
p=&dd1;//dd1的类型是Derived_from_public,正确
p=&dd2;//dd2的类型是Derived_from_private,错误
p=&dd3;//dd3的类型是Derived_from_protected,错误
友元关系不具有继承性,每个类负责控制各自成员的访问权限。
当我们需要改变派生类继承的某个名字的访问级别,可以通过使用using声明。
class Base
{
public:
std::size_t size() const{return n;}
protected:
std::size_t n;
};
class Derived:private Base //Base的成员在Derived是私有的。
{
public:
using Base::size;
protected:
using Base::n;//改变访问权限
};
默认的继承保护级别:
继承中的类作用域:
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域寻找该名字的定义。
派生类的成员将隐藏同名的基类成员。
我们可以通过作用域运算符来使用隐藏的成员。除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类的名字。
如果派生类(内层作用域)的成员与基类(外层作用域)的某个成员同名,则派生类将在其作用域隐藏该基类成员,即使派生类成员和基类成员的形参列表不一致。
通过基类调用隐藏的虚函数,调用的时候主要是看是什么类的指针,是否有虚函数覆盖。
#include <iostream>
class Base
{
public:
Base(int v):v(v){}
virtual int fcn()
{
return v;
}
int v=0;
};
class D1:public Base
{
public:
D1(int d1):Base(d1){}
int fcn(int d1)
{
return d1+1;
}
virtual void f2(){std::cout<<"f2() in D1"<<std::endl;}
};
class D2:public D1
{
public:
D2(int dd):D1(dd){}
int fcn(int d2)
{
return d2;
}
int fcn()
{
return v+100;
}
void f2(){std::cout<<"f2() in D2"<<std::endl;}
};
int main()
{
Base b(10);
D1 d1(10);
D2 d2(10);
Base *bp1=&b,*bp2=&d1,*bp3=&d2;
std::cout<<bp1->fcn()<<std::endl;//10,虚调用,Base::fcn()
std::cout<<bp2->fcn()<<std::endl;//10,虚调用,Base::fcn()
std::cout<<bp3->fcn()<<std::endl;//110,调用D2::fcn()
D1 *d1p=&d1;
D2 *d2p=&d2;
// bp2->f2();//错误,base没有f2的成员
d1p->f2();//D1::f2()
d2p->f2();//D2::f2()
Base *p1=&d2;
D1 *p2=&d2;
D2 *p3=&d2;
//p1->fcn(42);//错误,Base没有fcn(int)
std::cout<< p2->fcn(42)<<std::endl;//43,D1::fcn(42)
std::cout<< p3->fcn(42)<<std::endl;//42,D2::fcn(42)
return 0;
}
虚折构函数:
继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚折构函数,这样我们就能动态分配继承体系中的对象了。如果基类的折构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
Quote *itemp=new Quote;
delete itemp;//调用Quote的折构函数
itemp=new Bulk_quote;
delete itemp;//调用Bulk_quote的折构函数
基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或折构函数类似,即它们对类本身的成员依次进行初始化,赋值或销毁操作。此外,这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类进行初始化、赋值或销毁操作(从上到下)。
对于派生类的折构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类,该直接基类又销毁它自己的直接基类,以此类推直至继承链的顶端。(自下而上)。
某些定义基类的方式可能导致有的派生类成员成为被删除的函数:
class B
{
public:
B();
B(const B&)=delete;
//其他成员,不含移动构造函数
};
class D:public B
{
//没有声明任何构造函数
};
D d;//正确。D的合成默认构造函数使用B的默认构造函数
D d2(d);//错误,D的合成拷贝构造函数是删除的。
D d3(std::move(d));//错误,隐式使用D的被删除的拷贝构造函数
//因为Quote定义了折构函数而不能拥有合成的移动操作,因此当我们移动Quote对象时实际使用的是合成的拷贝操作。
因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们却是需要执行移动操作时应该首先在基类进行定义。但必须显式的定义这些成员。
当派生类定义拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝或移动构造函数。
class B {/*......*/};
class D:public Base
{
public:
//Base(d)会匹配Base的拷贝构造函数。D类型的对象d被绑定到该构造函数的Base&形参上。Base的拷贝构造函数负责将d的基类部分拷贝给要创建的对象。
D (const D& d):Base(d)//拷贝基类成员
/* D的成员的初始值 */{......}
D (D&& d):Base(std::move(d))//移动基类成员
/* D的成员的初始值 */{......}
};
派生类的赋值运算符也必须显式地为其基类部分赋值。
D D&::operator=(const D &rhs)
{
Base::operator=(rhs);//为基类部分赋值
.....
return *this
}
派生类折构函数只负责销毁由派生类自己分配的资源。
class D:public Base
{
public:
//Base::~Base自动执行
~D(){//用户定义};
};
我们可以使用基类名的using声明语句来说明派生类继承基类构造函数。但是一个构造函数的using声明不会改变该构造函数的访问级别。而且,一个using声明的语句不能指定explicit或constexpr.
class B:public A
{
public:
using A::A;//继承基类A的构造函数
};
当一个基类构造函数含有默认实参,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。