C++Primer笔记-面向对象程序设计

前言

该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。

本文大致整理了第十五章的知识点,涉及到C++关于类的多态和继承,重要!

链接目录

面向对象概述

面向对象核心思想是数据抽象、继承和动态绑定。

  • 数据抽象:将接口和实现分离
  • 继承:定义相似的类型,对相似关系建模
  • 动态绑定:忽略相似类型的区别,按统一的方式使用

定义基类

class Quote{
public:
	Quote() = default;
	Quote(const string &book, double Sales_price)
		:bookNo(book), price(sales_price){}
	string isbn() const {
		return bookNo;
	}

	virtual double net_price(std::size_t n) const {
		return n*price;
	}
	virtual ~Quote() = default;
private:
	string bookNo;
protected:
	double price = 0.0;
};

定义派生类

class Bulk_quote : public Quote{
public:
	Bulk_quote() = default;
	Bulk_quote(const string& book, double p, std::size_t qty, double disc)
	:Quote(book, p), min_qty(qty), discount(disc){}
	double net_price(std::size_t) const override;
private:
	std::size_t min_qty = 0;//折扣最低购买数量
	double discount = 0.0;//折扣额
};

派生类应该遵循基类的接口:即使派生类构造函数可以直接对基类公有成员赋值,但最好不要这么做,应该遵循基类的接口,调用基类额构造函数来初始化。

继承和静态成员:

class Base{
public:
	static void statmem();
};
class Deried : public Base{
	void f(const Derived&);
};

void Derived::f(const Derived &derived_obj){
	Base::statmem();//正确
	Derived:statmem();//正确,Derived继承了statmem
	derived_obj.statmem();//通过Derived访问
	statmem();//通过this访问
}

防止继承的发生:通过final关键字

class NoDerived final{...};//NoDerived不能作为基类

类型转换与继承

可以将基类的指针或引用绑定到派生类上(智能指针也支持)

静态类型与动态类型:

  • 静态类型在编译时是已知的
  • 动态类型直到运行时才是已知的,如基类指针所指向的派生类对象

不存在从基类向派生类的隐式类型转换:但如果已知转换是安全的,可以使用static_cast强制转换

Bulk_quote bulk;
Quote *itemP = &bulk;
//即使基类指针绑定派生类,也无法执行从基类到派生类的转换
Bulk_quote *bulkP = itemP;

存在继承关系的类型之间的转换规则:

  • 从派生类向基类的类型转换只对指针或引用类型有效
  • 基类向派生类不存在隐式类型转换
  • 派生类向基类的类型转换可能会由于访问受限而不可行

虚函数

每一个虚函数都要提供定义,不管它是否被用到

finaloverride说明符:如果定义虚函数时弄错了参数列表,可能导致无法覆盖基类的虚函数(虽然同名,但被当作两个独立的函数),在调用时产生预期外的结果,可以使用override避免这种情况

struct B{
	virtual void f1(int) const final;
	virtual void f2();
	void f3();
};
struct D1 : B{
	void f1(int) const override;//错误,不能覆盖final
	void f2(int) override;//错误,参数不匹配
	void f3() override;//错误,f3不是虚函数
};

虚函数和默认实参:通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数,所以最好将派生类和基类的默认实参设为一致。

回避虚函数的机制:强制执行某个版本的虚函数,通常只有成员函数或友元中的代码才需要使用。

//强制调用Quote版本的net_price函数,而不管baseP指向什么类型
double undiscounted = baseP->Quote::net_price(42);

抽象基类

纯虚函数:基类无定义,且不能创建存在纯虚函数的对象,只能创建覆盖了纯虚函数(定义)的派生类对象。
声明纯虚函数但未定义的类是抽象基类,只提供接口,无法实例化。

class Disc_quote : public Quote{
public:
	Disc_quote() = default
	Disc_quote(...):...{}
	double net_price(std::size_t) const = 0;//纯虚函数
protected:
	size_t quantity = 0;
	double discount = 0.0;
}

派生类构造函数只初始化它的直接基类:Bulk_quote有直接基类Disc_quote和间接基类Quote,则每个类控制各自的初始化过程。

class Bulk_quote : public Disc_quote{
public:
	Bulk_quote() = default;
	Bulk_quote(const string& book, double price, std::size_t qyt, double disc)
	:Disc_quote(book, price, pty, disc){}
	...
};

访问控制与继承

受保护的成员:protected

  • 和私有成员类似,对于类的用户是不可访问的
  • 和公有成员类似,对于类的成员和友元来说是可访问的
  • 基类中的受保护成员对于派生类的友元是不可访问的
class Base{
protected:
	int i;
};
class Sneaky : public Base{
	friend void f(Sneaky&);
	friend void f(Base&);
	int j;
};

//正确,访问的是Sneaky::i和Sneaky::j
void f(Sneaky &s){
	s.j = s.i = 0;
}
//错误,访问的是Base::i
void f(Base &b){
	b.i = 0;
}

其实就是派生类的成员和友元可以访问基类中的受保护成员,要通过派生类这一媒介,而不能跳过媒介,直接访问普通的基类对象中的受保护成员,即使该函数是派生类的友元或成员函数。

公有、私有和受保护继承:派生访问说明符目的是控制派生类用户的访问权限

  • public继承:继承自Base的成员是public的,遵循原有的访问说明符
  • private继承:继承自Base的成员是private

派生类向基类转换的可访问性:假定D继承自B

  • 只有当D公有地继承B时,才可以完成从派生类向基类地转换
  • 不论D以什么方式继承,D的成员函数和友元都能使用派生类向基类的转换

友元与继承:友元关系不能传递,友元关系也不能继承

改变个别成员的可访问性:通过using改变个别成员的可访问性

class Base{
public:
	std::size_t size() const {return n;}
protected:
	std::size_t n;
};
class Derived : private Base{//Base的数据成员本该是private的
public:
	using Base::size;
protected:
	using Base::n;
}

继承中的类作用域

派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域内无法解析,会继续在外层的基类中寻找定义。
例如:

  • 首先在Bulk_quote中查找,没找到isbn成员
  • 继续在基类Disc_quote中查找,仍然找不到
  • 在直接基类的Disc_quote的基类Quote中查找,最终解析为Quote中的isbn
Bulk_quote bulk;
cout << bulk.isbn();

虽然动态类型和静态类型可能不一致,但能使用哪些成员仍然是由静态类型决定的,即使此时动态类型可能支持额外的操作。

名字冲突与继承:和内层作用域中的名字会屏蔽掉外层作用域中的名字一样,在派生类中的成员会隐藏在基类中的同名成员。

通过作用域运算符来使用隐藏的成员:

class Base;
struct Derived : Base{
	int i;//Base中也有名为i的成员
	void f(){
		cout << i << endl;//输出本类中的成员
		cout << Base::i << endl;//输出基类的成员
	}
};

除了覆盖继承来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

函数调用的解析过程:假定p->mem()

  • 首先确定p的静态类型
  • p静态类型对应的类中查找mem,如果找不到则在基类中查找,递归直到达到继承链的顶端或找到
  • 找到了mem之后进行常规的类型检查,以检测本次调用是否合法
  • 假设合法,编译器会根据是否是虚函数来产生不同的代码:
    • 如果mem是虚函数,则根据对象的动态类型来决定调用哪个版本
    • 如果不是虚函数,则产生一个常规函数调用

名字查找先于类型检查:声明在内层作用域的函数并不会重载外层作用域中的函数,所以派生类中的函数也不会重载基类中的成员。所以如果派生类和基类的某个成员函数同名,则派生类会在作用域内隐藏基类的成员函数。
简而言之,编译器查找到对应的名字,就不会再往外层作用域查找

虚函数与作用域:由上,当派生类声明一个和虚函数同名但参数不同的函数,此时屏蔽了外层作用域(基类)中的函数,是一个独立的函数。

通过基类调用隐藏的虚函数:

class Base{
public:
	virtual int fcn();
};
class D1 : public Base{
public:
	int fcn(int);//不覆盖虚函数
	virtual void f2();
};
class D2 : public D1{
public:
	int fcn(int);//非虚函数,隐藏了D1中的fcn
	int fcn();//虚函数,覆盖Base中的fcn
	void f2();//虚函数,覆盖D1中的f2
};

Base bobj;
D1 d1obj;
D2 d2obj;

Base *bp1 = &bobj, *bp2 = &d1obj, *bo3 = &d2obj;
bp1->fcn();//虚调用,调用Base::fcn
bp2->fcn();//虚调用,调用Base::fcn
bp3->fcn();//虚调用,调用D2::fcn

D1 *d1p = &d1obj;
D2 *d2p = &d2obj;
bp2->f2();//错误,Base没有f2成员,本质是无法完成基类向派生类的转换
d1p->f2();//虚调用,D1::f2()
d2p->f2();//虚调用,D2::f2()

虚析构函数

可能出现指针的静态类型与被删除对象的动态类型不匹配的情况,所以此时要通过虚析构函数正确删除对象。
只有虚函数才会进行动态类型的匹配,否则都只会执行静态类型的版本。

虚析构函数会阻止合成移动操作。

合成拷贝控制与继承

关于拷贝控制的详细内容
除了对类本身的成员进行初始化、赋值或销毁,还要使用直接基类对应的操作对基类部分进行初始化、赋值或销毁。

派生类中删除的拷贝控制与基类的关系:
导致派生类拷贝控制成员成为被删除的函数:基类的默认构造函数、拷贝构造函数、拷贝赋值或析构是被删除/不可访问的。

移动操作与继承:基类缺少移动操作会阻止派生类拥有合成的移动操作,所以我们需要移动操作时,应该先在基类中定义。
显式定义移动操作:

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的成员初始值*/
	}
	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不会改变该构造函数的访问级别,不管using出现在哪,他的访问级别和基类中的构造函数相同。
如果构造函数是explicitconstexpr的,会被继承且无法修改。
当一个基类构造函数有默认实参,则实参不会被继承。

class Bulk_quote : public Disc_quote{
public:
	using Disc_quote::Disc_quote;//继承Disc_quote的构造函数
	//等价于Bulk_quote(...):Disc_quote(...){}
	double net_price(std::size_t) const;
}

继承存在两个例外:构造函数不会被继承

  • 派生类定义的构造函数与基类构造函数有相同的参数列表
  • 默认、拷贝和移动构造函数不会被继承

容器与继承

在容器中放置指针而非对象,否则会失去动态性

vector<shared_ptr<Quote>> vec;//所有元素存的都是Quote的指针
vec.push_back(make_shared<Quote>(...));
vec.push_back(make_shared<Bulk_quote>(...));//会隐式地转换成Quote的指针
上一篇 下一篇

评论 | 0条评论