《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 ...
随机推荐
- 【算法题目】Leetcode算法题思路:两数相加
在LeetCode上刷了一题比较基础的算法题,一开始也能解出来,不过在解题过程中用了比较多的if判断,看起来代码比较差,经过思考和改进把原来的算法优化了. 题目: 给出两个 非空 的链表用来表示两个非 ...
- mac brew 使用教程
brew services list #查看系统通过 brew 安装的服务 brew services cleanup #清除已卸载无用的 ...
- Python WEB框架之Flask
前言: Django:1个重武器,包含了web开发中常用的功能.组件的框架:(ORM.Session.Form.Admin.分页.中间件.信号.缓存.ContenType....): Tornado: ...
- UOJ #164 [清华集训2015]V (线段树)
题目链接 http://uoj.ac/problem/164 题解 神仙线段树题. 首先赋值操作可以等价于减掉正无穷再加上\(x\). 假设某个位置从前到后的操作序列是: \(x_1,x_2,..., ...
- Python学习日记(六)——内置函数和文件操作(lambda)
lambda表达式 学习条件运算时,对于简单的 if else 语句,可以使用三元运算来表示,即: # 普通条件语句 if 1 == 1: name = 'prime' else: name = 'c ...
- python3安装web.py
今天准备测试代理池IPProxyPool获取到ip的质量,在安装web.py的时候遇到了些问题,在此记录一下. 1.安装资料 web.py官网:http://webpy.org/ web.py的git ...
- P5149 会议座位
P5149 会议座位 题意: 其实还是求逆序对数. 解法: 用离散化统计每个数,再用树状数组求逆序对. CODE: #include<iostream> #include<cstdi ...
- Hadoop环境搭建|第二篇:hadoop环境搭建
硬件配置:1台NameNode节点.2台DataNode节点 一.Linux环境配置 这里我只配置NameNode节点,DataNode节点的操作相同. 1.1.修改主机名 命令:vi /etc/sy ...
- ORM 数据库使用
使用 Flask-SQLAlchemy 来操作数据库 1 配置 本文使用sqlite来作为例子演示,在config.py里面更新下数据库的配置 import os basedir = os.path ...
- 入门display:inline-block运用
这是我第一篇博客,是我新的开始,我要用博客记录我的学习之旅,在这里我要感谢我的哥哥,他带我开阔了眼界,纠正了我的格局,给我带来了正能量.我是一个小白,学习的路还很长很长,学习了10天HTML与css, ...