《C++ Primer》读书笔记之第15章:面向对象编程
一、面向对象概述
1. 面向对象的三个基本特性
封装、继承和多态。
2. 封装
指把隐藏对象的实现细节,仅对外提供接口,从而达到接口与实现分离的效果。封装的好处:一是提高数据的安全性,用户只能使用对象提供的接口,而不能随意修改对象的数据。试想如果用户能够获取权限访问对象的所有实现细节并进行修改,那对象的安全性将无法保证。这和用外壳把电路板封装起来,以免用户随便拆卸电子器件的道理是类似的。二是方便使用。用户只需知道接口而不需了解内部细节便可使用。
3. 继承
指新对象可以直接使用现有对象的功能,并且可以在此基础上进行拓展,从而避免“重复造轮子”。
4. 多态
指某个接口的使用者可能是原有对象也可能是新对象,这个要在实际运行时才能确定,而在编译时无法确定。比如:定义一个函数void func(const Base&),其中Base是基类对象,假设类Derived继承类Base,则函数func的实际输入参数可能是Base对象也可能是Derived对象,视实际运行情况而定。
5. 代码重用和接口重用
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是——代码重用。而多态则是为了实现另一个目的——接口重用!
二、基类与派生类
1. 派生类的声明与定义
(1) 派生类的声明与普通类的声明一样,“class” + 类名即可。注意不要在声明派生类时列出其继承的基类,因为声明的目的是让编译器知道某个名字存在以及它代表何种数据类型(类、函数或变量等)即可,不需要知道细节。
(2) 在声明类时可以在类名后面加上“final”,表明此类不可被继承。比如:class NoDerived final : Base {/* */};
(3) 定义派生类时,应该在紧跟类名的“:”后面、“{”前面写出继承列表,声明所继承的每一个基类的名字以及继承方式(public, protected或private)。比如:
class Derived : public Base1, private Base2 {
/* */
};
2. 派生类构造函数
(1) 每个类负责自己定义的成员的初始化,因此派生类应该使用基类构造函数来初始化派生类中的基类成员。虽然派生类构造函数可以对public或protected的基类成员直接赋值,但不应该这样做,我们应该通过类提供的接口来访问类对象。
(2) 派生类构造函数初始化数据成员的顺序:首先初始化基类成员,然后初始化派生类自身定义的成员。另外,在初始化一个类的各成员时,按照各成员在该类中被声明的顺序进行初始化。
3. 派生类对基类的访问权限
(1) 派生类继承基类的方式(public, protected或者private)不影响派生类对基类的访问权限,派生类的成员或友元函数总是可以访问基类的public和protected成员,而不能访问基类的private成员。派生类继承基类的方式影响的是继承类的用户对基类的访问权限,其影响方式可总结为3条公式:
public * T = T; // T may be public, private or protected.
private * T = private;
protected * protected = protected.
(2) 派生类的成员或友元函数只能通过派生类对象访问基类的protected成员,而不能通过基类对象访问基类的protected成员。这样可以防止派生类或友元函数绕过保护机制去修改基类的protected成员。比如:
class Base {
protected:
int prot_mem;
}; class Derived {
friend void func(Derived&); // can access Derived::prot_mem
friend void func(Base&); // can't access Base::prot_mem
};
(3) “派-基”转换的使用权限:
只有当派生类以public方式继承基类时,派生类的用户才能使用“派-基”转换。
派生类的成员或友元函数总是可以使用“派-基”转换。
只有当派生类以public或protected方式继承基类时,派生类的派生类的成员或友元函数才能使用“派-基”转换。
(4) 友元函数的“友谊”不可传递,亦不可继承。
(5) 可以在派生类中使用using语句改变其基类成员的访问权限。比如:
class Base {
public:
std::size_t size() const { return n;}
protected:
std::size_t n;
}; class Derived: private Base { // private inheritance.
public:
using Base::size; // now users of Derived can use size().
protected:
using Base::n; // now class derived from Derived can use n.
};
(6) 派生类对基类的继承方式默认为private,这和类的成员的访问权限默认为private类似。但即使派生类要以private方式继承基类,也应该加上private关键字,而非依赖缺省值。这样可以清晰表明自己的意图。
(7) struct和class的区别只是两者的成员的默认访问方式不同,除此之外没其他区别。
4. "派-基"转换(派生类到基类的转换)
(1) 派生类对象由两部分组成:一部分是派生类自身定义的成员,另一部分是派生类所继承的各个基类的成员。
(2) 一个变量或表达式的类型可分为静态类型和动态类型两种。静态类型是变量声明的类型或表达式计算结果所代表的类型,在编译时便可确定;动态类型是变量或表达式实际使用时的类型,在运行时才能确定。对于非指针且非引用的变量或表达式,其静态类型与动态类型相同;对于基类指针或基类引用,其动态类型与静态类型可能不同。
(3) 由于派生类包含基类成员,我们将一个派生类对象看作是基类对象来使用,特别是,可以把基类指针或基类引用绑定到派生类对象上,这叫做“派-基”转换(derived-to-base conversion)。
(4) 当我们直接使用派生类对象来初始化或赋值给基类对象时,只有派生类的基类成员被复制或赋值,派生类的自身成员会被忽略。
(5) 基类对象不能自动转换成派生类成员,因为编译器无法确定这样的转换是否安全。即使一个基类指针或基类引用已经与一个派生类对象绑定,仍然不能将其用于“基-派转换”:
Derived derived;
Base *basePtr = &derived; // ok: dynamic type is Derived
Derived *derivedPtr = basePtr; // error: can't convert base to derived
(6) 如果我们明确某个”基-派“转换是安全的,可以使用static_cast来让编译器允许这样的转换。
三、虚函数
1. 虚函数的定义
(1) 除构造函数外的所有非satic函数均可定义为虚函数。
(2) 如果基类定义了一个static成员,则无论有多少个继承此基类的派生类,都只存在于一个这样的static成员。
(3) 基类必须定义一个虚析构函数,即使它不会被使用。
(4) 所有的虚函数都必须被定义,即使它不会被使用。因为编译器无法知道某个虚函数是否被用到,为确保安全,要求定义所有的虚函数。
2. 虚函数的覆盖
(1) 派生类可以只对基类的部分而非全部虚函数进行覆盖(override)。
(2) 派生类可以使用“override”显式覆盖基类的虚函数,“override”应写在虚函数的参数列表后面。如果虚函数是const或reference函数,则把override写在const或&的后面。显式覆盖的一个好处是可以避免写错虚函数的形参,因为编译器一旦发现派生类中显式覆盖的虚函数的形参与基类的虚函数不一致时会报错。
(3) 继承类中要进行override的虚函数的形参必须与基类的同名虚函数一致,若不一致则不会覆盖,而会重载。除了一个例外,继承类中要进行override的虚函数的返回类型也必须与基类的一致。这个例外是如果待覆盖的虚函数的返回类型是基类对象的指针或引用时,继承类的虚函数可以返回继承类对象的指针或引用。
(4) 当继承类要覆盖基类的虚函数时,可以但不必在函数名前面重复virtual关键字。一旦某函数被声明为virtual,则在所有的继承类中它仍然为虚函数。(一日为虚,终身为虚。)
(5) 如果虚函数含有缺省参数,则其缺省值永远是基类中该虚函数定义的缺省值。因此,如果派生类中的虚函数含有缺省参数,则其缺省值应该和基类的该虚函数的缺省值相同。
3. 动态绑定
(1) 当使用基类指针或基类引用来调用某个虚函数时,会导致动态绑定;当使用基类对象来调用某个虚函数时,不会发生动态绑定。比如:
base = derived; // copies the Base part of derived to base
base.virtual_func(); // calls Base::virtual_func()
(2) 有时我们需要禁止动态绑定,最常见的一个例子是派生类的虚函数希望调用基类的同名虚函数。这时,可以使用::操作符禁止动态绑定。比如:
int data = baseP->Base::func(); // calls the version from the base class regardless of the dynamic type of baseP
四、抽象类与纯虚函数
1. 纯虚函数的定义
在某些场合,基类本身生成对象是不合情理的。例如动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。这时便引入纯虚函数的概念。当在基类中无法给出某个虚函数的具体实现时,我们可以将其声明为纯虚函数,而不进行定义,其定义留给派生类来实现。
2. 纯虚函数的声明
声明纯虚函数的方法是在函数名后面写上“= 0”,比如:virtual int pure_virtual_func() = 0;
3. 抽象类的定义
含有纯虚函数的类叫抽象类。因此,如果抽象类的派生类仍然含有纯虚函数,则此派生类也是抽象类。抽象类不能被实例化。
五、继承关系下的类作用域
1. 作用域的嵌套
派生类的作用域嵌套在基类的作用域里面。这意味着编译器在查找派生类中出现的所有名字时,总是先在派生类里查找,找不到的话再到基类中找。
2. 名字查找发生在编译时刻
因此,一个对象的成员能否被编译器“看见”(查找),取决于这个对象的静态类型(亦即此对象被声明时的类型),而非动态类型。因此,尽管基类指针或基类引用可以绑定到派生类对象,它们还是不能访问只在派生类定义的函数。比如:
class Base {
public:
virtual void func();
// other members
}; class Derived : public Base {
public:
9 void Derived_func();
// other members
}; Derived derived;
Base *pBase = &derived; // static and dynamic types differ
pBase->Derived_func(); // error: pBase has type Base*
3. 编译器查找名字的步骤
假如给定函数调用语句p->mem()或obj.mem(),则编译器查找mem的步骤如下:
(1) 确定p或obj的静态类型。
(2) 在其静态类型对应的类中查找mem,如果找不到,再到其直接基类中继续找,如此沿着继承链一直找,直到找到为止。
(3) 找到mem便进行类型检查,确定此调用是否合法。
(4) 如果此调用合法,编译器将生成代码。
4. 派生类对基类的覆盖
(1) 派生类的作用域嵌套在基类的作用域里面。内部作用域会覆盖外部作用域的同名对象。这是因为,编译器在查找名字时,是按照“由内到外”的顺序找的,即先在内层作用域里找,只有内层作用域找不到该名字时才到外层作用域找。
(2) 可以通过作用域操作符(::)来使用被隐藏的基类成员。
class Base{
public:
Base() : data() {};
int func();
int data;
}; class Derived{
public:
Derived(int i) : data(i) {};
int GetData() { return data};
int func(int); // hide func() in the Base
int data; // hide data in the Base
}; Derived d();
d.func(); // calls Derived::func
d.func(); // error: func with no arguments is hidden
d.Base::func(); // ok: calls Base::func()
cout << d.GetData() << endl; // print 100
(3) 派生类继承基类的虚函数时要保持函数参数一致,否则与派生类绑定的基类指针或引用无法调用派生类版本的该函数。
class Base {
public:
virtual int fcn();
}; class Derived {
public:
int fcn(int); // hides fcn in the Base; this fcn is not virtual.
}; Base bobj;
Derived dobj;
Base *pBase1 = &bobj;
Base *pBase2 = &dobj;
pBase1->fcn(); // calls Base::fcn at run time
pBase2->fcn(); // calls Base::fcn at run time
pBase2->fcn(); // error: Base has no version of fcn that takes an int.
(4) 若派生类希望能够使用其继承自基类的所有重载虚函数,则要么全部覆盖,要么全部不覆盖。如果部分覆盖,则基类中未被覆盖的函数会被隐藏。为简便起见,派生类可以使用using语句来表明使用基类的重载虚函数。使用using时,只需写函数名字而不必写函数参数列表。
六、构造函数与复制控制
FAQ:
1. 动态绑定是如何实现的?
《C++ Primer》读书笔记之第15章:面向对象编程的更多相关文章
- 【c++ Prime 学习笔记】第15章 面向对象程序设计
15.1 OOP:概述 面向对象程序设计(object-oriented programming)的核心思想是:数据抽象.继承.动态绑定 使用数据抽象,可将类的接口与实现分离 使用继承,可定义相似的类 ...
- C++ Primer 读书笔记:第11章 泛型算法
第11章 泛型算法 1.概述 泛型算法依赖于迭代器,而不是依赖容器,需要指定作用的区间,即[开始,结束),表示的区间,如上所示 此外还需要元素是可比的,如果元素本身是不可比的,那么可以自己定义比较函数 ...
- C++ Primer 读书笔记:第10章 关联容器
第10章 关联容器 引: map set multimap multiset 1.pair类型 pair<string, int> anon anon.first, anon.second ...
- C++ Primer 读书笔记: 第9章 顺序容器
第9章 顺序容器 引: 顺序容器: vector 支持快速随机访问 list 支持快速插入/删除 deque 双端队列 顺序容器适配器: stack 后进先出栈 queue 先进先出队列 priori ...
- C++ Primer 读书笔记: 第8章 标准IO库
第8章 标准IO库 8.1 面向对象的标准库 1. IO类型在三个独立的头文件中定义:iostream定义读写控制窗口的类型,fstream定义读写已命名文件的类型,而sstream所定义的类型则用于 ...
- C#图解教程读书笔记(第15章 委托)
委托是C#的一个很重要的知识点. 1.什么是委托 委托在我认为,就是一系列格式相同的方法列表,可能就是定义的名称不一致,参数.返回值等都是一样的. 2.如何声明委托 delegate void MyF ...
- C#图解教程读书笔记(第2章 C#编程概述)
这章主要是一个对于C#程序的概括解释 和C/C++不同,不是用include声明引用的头文件,而是通过using的方式,声明引用的命名空间. 命名和C/C++类似,并且也是区分大小写的,这件事情在VB ...
- C++ Primer(第4版)-学习笔记-第4部分:面向对象编程与泛型编程
第15章 面向对象编程OOP(Object-oriented programming) 面向对象编程基于三个基本概念:数据抽象.继承和动态绑定. 在 C++ 中,用类进行 ...
- 《Python 3.5从零开始学》笔记-第8章 面向对象编程
前几章包括开启python之旅.列表和元组.字符串.字典.条件和循环等语句.函数等基本操作.主要对后面几章比较深入的内容记录笔记. 第8章 面向对象编程 8.3深入类 #!/usr/local/bin ...
随机推荐
- 「ARC103D」Robot Arms「构造」
题意 给定\(n\)个点,你需要找到一个合适的\(m\)和\(d_1,d_2,...,d_m\),使得从原点出发每次向四个方向的某一个走\(d_i\)个单位,最终到达\((x_t, y_t)\).输出 ...
- thinkphp5 大量数据批量插入数据库的解决办法
对于数据量很小,怎么玩都是可以的. but!!! 如果有几十万或者百万级别的数据,该怎么处理,请往下面看
- spring-boot 定时任务需要注意的地方
spring-boot 跑定时任务非常容易 启动类上添加两个注解基本OK @EnableScheduling @EnableAsync 当然要记录的肯定不是这里的问题了 首先, fixedDelayf ...
- Apache Web服务器 安装步骤 和遇到的坑
Apache Web服务器是开发放源码的网页服务器,我们看到的网页都是上传到服务器然后呈现给用户的. 在开发中,在自己的电脑上安装Apache Web服务器,你的电脑也会成为服务器,配置文件,访问你的 ...
- 20175313 张黎仙《Java程序设计》第十周学习总结
目录 一.教材学习内容总结 二.教材学习中的问题和解决过程 三.代码调试中的问题和解决过程 四.代码托管 五.心得体会 六.学习进度条 七.参考资料 一.教材学习内容总结 第十二章内容 主要内容 杂项 ...
- K-Means算法及代码实现
1.K-Means算法 K-Means算法,也被称为K-平均或K-均值算法,是一种广泛使用的聚类算法.K-Means算法是聚焦于相似的无监督的算法,以距离作为数据对象间相似性度量的标准,即数据对象间的 ...
- fastjson在将Map<Integer, String>转换成JSON字符串时,出现中文乱码问题
fastjson在将Map<Integer, String>转换成JSON字符串时,出现中文乱码问题. 先记下这个坑,改天在看看是怎么导致的,暂时通过避免使用Integer作为键(使用St ...
- MyEclipse的Git配置
1.下载:git的插件egit 并解压 插件 下载地址:http://www.eclipse.org/egit/download/ 所有版本:http://wiki.eclipse.org/EGit ...
- MySQL truncate()函数的使用说明
1.TRUNCATE()函数介绍 TRUNCATE(X,D) 是MySQL自带的一个系统函数. 其中,X是数值,D是保留小数的位数. 其作用就是按照小数位数,进行数值截取(此处的截取是按保留位数直接进 ...
- Spark中的CombineKey()详解
CombineKey()是最常用的基于键进行聚合的函数,大多数基于键聚合的函数都是用它实现的.和aggregate()一样,CombineKey()可以让用户返回与输入数据的类型不同的返回值.要理解C ...