第十五章-面向对象(二)

继承中的类作用域设计

  • 在继承关系中,派生类的作用域嵌套在基类的作用域中。如果一个名字在派生类中不能正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义(正是这种作用域嵌套关系,所以派生类才能像使用自己的成员一样使用基类的成员)
Bulk_quote bulk;
cout << bulk.isbn();
//第一步,因为是Bulk_quote调用的isbn,所以先在Bulk_quote中找isbn的名字,没有找到
//第二步,Bulk_quote是Disc_quote的派生类,所以在Disc_quote中找,还是没找到
//第三步,Disc_quote又是Quote的派生类,所以在Quote中找,找到了,最终解析为Quote的isbn

编译时进行名字查找

  • 对象、引用或指针类型决定了这个对象哪些成员是可见的。即使静态类型和动态类型不一致(引用、指针导致),但使用哪些成员还是由静态类型决定
class Disc_quote : public Quote
{
public:
	pair<size_t, double> discount_policy() const
		{ return {quantity, discount}; }
};
//只能通过Disc_quote及其派生类的对象、引用或指针来使用discount_policy
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk;
Quote *itemP = &bulk; //itemP静态类型是Quote
bulkP->discount_policy(); //正确!bulkP的类型是Bulk_quote*
itemP->diccount_policy(); //错误!
//itemP是Quote的指针,所以对discount_policy的搜索从Quote开始
//而Quote没有discount_policy

名字冲突和继承

  • 派生类能重用定义在其直接基类或间接基类中的名字,此时定义在派生类的名字会隐藏定义在基类的名字
struct Base
{
	Base() : mem(0) {}
protected:
	int mem;
};
struct Derived : Base
{
	Derived(int i) : mem(i) {}
	int get_mem() { return mem; } //返回自己的mem,而不是基类的
	//int get_mem() { return Base::mem; }
	//使用作用域运算符来使用隐藏的成员
protected:
	int mem; //隐藏基类的mem
};
  • 但是除了覆盖继承的虚函数之外,派生类最好不要重用其他定义在基类中的名字

理解调用的解析过程,假设p->mem()
1.首先确定p的静态类型,因为是调用一个成员,所以该类型必然是类类型
2.在p的静态类型对应的类中查找mem。找不到就依次从直接基类中找,直到继承链的顶端还没找到则报错
3.如果调用合法,编译器还要根据调用是否是虚函数而产生不同代码

  • 如果mem是虚函数且我们是通过引用或指针来调用的,则依据动态类型决定运行哪个版本
  • 如果不是虚函数,或者我们是hi通过对象调用的,则编译器产生常规函数调用
  • 函数那章讲过,如果内层作用域的成员和外层作用域的成员同名,则内层的将在其作用域内隐藏外层的成员。在类中也一样
struct Base
{
	int memfcn();
};
struct Derived : Base
{
	int memfcn(int); //隐藏基类的memfcn
};
Derived d;
d.memfcn(); //错误
//编译器在Dervied中找memfcn,也找到了
//编译器是一旦找到名字,就不再继续查找
//但是Derived的memfcn需要int参数,这里没有提供,所以是错的

虚函数和作用域

  • 我们之所以让基类和派生类的虚函数有相同形参列表,是因为如果基类和派生类的虚函数接受的实参不同,那我们就无法通过基类的引用或指针调用派生类的虚函数
class Base
{
public:
	virtual int fcn();
};
class D1 : public Base
{
public:
	int fcn(int); //自己的fcn,隐藏了基类的fcn
	//注意这里D1实际有两个fcn,其中一个被隐藏
	virtual void f2();
};
class D2 : public D1
{
public:
	int fcn(int); //隐藏了D1::fcn(int)
	int fcn(); //覆盖了Base的fcn
	void f2(); //覆盖了D1的f2
};
Base b; D1 d1; D2 d2;
Base *bp = &b, *bp1 = &d1, *bp2 = &d2;
bp->fcn(); //虚调用,调用Base::fcn
bp1->fcn(); //虚调用,调用Base::fcn
//bp1绑定的是D1,所以在D1寻找不接受实参的fcn
//但是D1没有覆盖这个fcn,所以会解析为Base的版本
bp2->fcn(); //虚调用,调用D2::fcn

D1 *d1p = &d1; D2 *d2p = &d2;
bp2->f2(); //错误!bp2是Base指针,而Base中没有f2
d1p->f2(); //虚调用,调用D1::f2()
d2p->f2(); //虚调用,调用D2::f2()

Base *p1 = &d2; D1 *p2 = &d2; D2 *p3 = &d2;
p1->fcn(42); //错误!Base没有接受int的fcn
p2->fcn(42); //静态绑定,调用D1::fcn(int)
p3->fcn(42); //静态绑定,调用D2::fcn(int)
//这里的fcn是非虚函数,所以没有动态绑定
//所以调用函数的版本由指针静态类型决定

构造函数与拷贝控制

  • 继承的类也需要定义拷贝控制操作,如果没定义,编译器会合成一个版本

虚析构函数

  • 如果指针指向继承体系中的某个类型,则可能出现指针的静态类型和被删除对象的动态类型不符的情况。比如delete一个Quote*指针,这个指针可能指向Quote也可能是Bulk_quote,那该执行谁的析构函数呢?我们通过再基类中将析构函数定义成虚函数来保证执行正确的版本
class Quote
{
public:
	//如果删除的是一个指向派生类对象的指针,则需要虚析构函数
	virtual ~Quote() = default;
};
  • 析构函数的虚属性会被继承。所以无论派生类使用合成的还是自定义的,都将是虚析构,delete指针时都会执行正确的析构函数版本
Quote *itemP = new Quote;
delete itemP; //执行Quote的析构函数
itemP = new Bulk_Quote;
delete itemP; //执行Bulk_Quote的析构函数
  • 如果基类的析构函数不是虚函数,那delete一个指向派生类对象的基类指针会产生未定义行为
  • 之前的三五准则(如果一个类需要析构函数,那它一定需要拷贝和赋值操作)对基类的析构函数不适用:一个基类总是需要一个析构函数,而且设定为虚,内容为空,就无法推断还需不需要赋值和拷贝构造

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

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的或不可访问的,则派生类中对应的成员也是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作
class B
{
public:
	B();
	B(const B&) = delete;
};
class D : public B
{
	//没有声明任何构造函数
};
D d; //正确,使用合成的默认构造函数
D d2(d); //错误!D的合成拷贝构造函数被删除
D d3(std::move(d)); //错误,这里隐式的使用了D被删除的拷贝构造函数
//D如果想让自己的对象能拷贝,就必须定义自己的构造函数
//但是这时还得考虑如何拷贝基类部分的成员
//实际如果基类中没有默认、拷贝或移动构造函数
//那派生类也不会定义相应的操作

移动操作和继承

  • 大多数基类会定义虚析构,因此基类不含合成的移动操作(虚析构函数将阻止合成移动操作)。如果我们确实需要移动操作时应该首先在基类中定义,哪怕是合成的也要显式定义。一旦定义了自己的移动操作,必须同时显式定义拷贝操作
class Quote
{
public:
	Quote() = default;
	QUote(const QUote&) = default;
	Quote(Quote&&) = default;
	Quote& operator=(const Quote&) = default;
	Quote& operator=(Quote&&) = default;
	virtual ~Quote() = default;
};

派生类的拷贝控制成员

  • 派生类的拷贝和移动构造函数在拷贝移动自己的成员的同时,也要拷贝和移动基类部分的成员。赋值运算符也要为其基类部分的成员赋值。但是析构函数只销毁派生类自己分配的资源,然后自动调用基类的析构函数

定义派生类的拷贝或移动构造函数

  • 默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(移动)构造函数
class Base {...}
class D : public Base
{
public:
	D(const D& d) : Base(d) //d绑定到Base构造函数的Base&形参上,将d的基类部分拷贝到要创建的对象
	 {/* D自己成员的初始值 */}
	D(D&& d) : Base(std::move(d)) //移动基类成员
	 {/* D自己成员的移动 */}
};

派生类赋值运算符

  • 派生类的赋值运算符也必须显式的为基类部分赋值
//Base::operator=(const Base&)
D &D::operator=(const D &rhs)
{
	//显式调用基类赋值运算符
	Base::operator=(rhs);
	/*
	派生类自己成员赋值
	注意处理自赋值、重复释放等情况
	*/
	return *this;
}

派生类析构函数

  • 和构造函数及赋值运算符不同,派生类析构函数只负责销毁由派生类自己分配的资源
  • 销毁顺序和创建顺序相反,派生类析构函数先执行,然后是基类析构函数
class D : public Base
{
public:
	~D() { /*清除派生类自己的成员*/}
	//Base::~Base自动调用执行

继承的构造函数

  • C++11中派生类能够继承直接基类的构造函数,不能继承默认、拷贝和移动构造函数
class Bulk_quote : public Disc_quote
{
public:
	//使用using来继承
	using Disc_quote::Disc_quote; //继承Disc_quote的构造函数
	double net_price(size_t) const;
};
//等价于下面这样
Bulk_quote(string& book, double price, size_t qty, double disc) :
	Disc_quote(book, price, qty, disc) { }
  • 对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数
  • 构造函数的using声明不会改变该构造函数的访问级别;using声明语句不能执行explicit或constexpr
  • 当一个基类构造函数含有默认实参时,这些实参并不会被继承。派生类会获得多个继承的构造函数,其中每个构造函数分别省略一个含有默认实参的形参(比如构造函数两个参数,第二个形参有默认实参,则派生类会有两个构造函数:一个是两个形参都没有默认实参的版本;还有一个是只含有左边没有默认实参的形参的版本)
  • 有两个例外派生类会没继承基类的构造函数
    1. 派生类定义了基类中相同参数列表的自己版本的构造函数,这些构造函数会替换继承来的构造函数
    2. 默认、拷贝和移动构造函数不会被继承
  • 继承的构造函数不会被用作用户定义的构造函数来用,所以一个类如果只有继承的构造函数,则它也会有一个合成的默认构造函数
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值