前言
该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。
本文大致整理了第十五章的知识点,涉及到C++关于类的多态和继承,重要!
链接目录
- 第二章:变量与基本类型
- 第三章:字符串、向量和数组
- 第四章:表达式
- 第五章:语句
- 第六章:函数
- 第七章:类
- 第八章:IO库
- 第九章:顺序容器
- 第十章:泛型算法
- 第十一章:关联容器
- 第十二章:动态内存
- 第十三章:拷贝控制
- 第十四章:重载运算与类型转换
- 第十五章:面向对象程序设计
- 第十六章:模板与泛型编程
- 第十七章:标准库特殊设施
- 第十八章:用于大型程序的工具
- 第十九章:特殊工具与技术
面向对象概述
面向对象核心思想是数据抽象、继承和动态绑定。
- 数据抽象:将接口和实现分离
- 继承:定义相似的类型,对相似关系建模
- 动态绑定:忽略相似类型的区别,按统一的方式使用
定义基类
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;
存在继承关系的类型之间的转换规则:
- 从派生类向基类的类型转换只对指针或引用类型有效
- 基类向派生类不存在隐式类型转换
- 派生类向基类的类型转换可能会由于访问受限而不可行
虚函数
每一个虚函数都要提供定义,不管它是否被用到
final
和override
说明符:如果定义虚函数时弄错了参数列表,可能导致无法覆盖基类的虚函数(虽然同名,但被当作两个独立的函数),在调用时产生预期外的结果,可以使用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
出现在哪,他的访问级别和基类中的构造函数相同。
如果构造函数是explicit
或constexpr
的,会被继承且无法修改。
当一个基类构造函数有默认实参,则实参不会被继承。
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的指针