目录
1. 类与对象
3 c++ 的编译以及运行
3.1 以 Linux 下的编译过程为例
3.2 Windows 下编译运行
3.3 总结
5 c++ 头文件与名词空间
6 常见的存储类型以及字节数
- define SHRT_MIN??? (-32768)??????? // minimum (signed) short value
- define SHRT_MAX????? 32767??
7 c++ 动态的分配内存
8 引用
8.1 参数传递的本质
9 继承与派生
10 构造函数与析构函数
11. 多继承
12 多态
- overloading"and"overriding"?
- overloading
- overriding
13 纯虚函数与抽象类
14 C++ typeid 运算符: 获取类型信息
15 C++ RTTI 机制精讲(C++ 运行时类型识别机制)
16 C++ 静态绑定和动态绑定, 彻底理解多态~~
17 C++ RTTI 机制下的对象内存模型(透彻)
18 dynamic_cast
20C++ 模板和泛型程序设计
21 异常 catch
22 浅层拷贝与深拷贝
智能指针
23 STL 标准模板库
1. 类与对象
类与对象的区别是什么?
答:
类是一个抽象的概念, 它不存在于现实中的时间 / 空间里, 类只是为所有的对象定义了抽象的属
性与行为.
? 对象是一个动态的概念. 每一个对象都存在着有别于其它对象的属于自己的独特的属性和行为.
3.. 类与对象的关系就如模具和铸件的关系, 类的实例化结果就是对象, 而对一类对象的抽象就是类. 类描述了一组有相同特性 (属性) 和相同行为 (方法) 的对象
2 类与对象区别
相同之处:
结构体中也可以包含函数; 也可以定义 public,private,protected 数据成员; 定义了结构体之后, 可以用结构体名来创建对象.
也就是说在 C++ 当中, 结构体中可以有成员变量, 可以有成员函数, 可以从别的类继承, 也可以被别的类继承, 可以有虚函数.
区别
对于成员访问权限以及继承方式, class 中默认的是 private, 而 struct 中则是 public.class 还可以用于表示模板类型, struct 则不行.
不能使用 abstract 和 protected 等修饰符, 不能初始化实例字段.
总结
概念: class 和 struct 的语法基本相同, 从声明到使用, 都很相似, 但是 struct 的约束要比 class 多, 理论上, struct 能做到的 class 都能做到, 但 class 能做到的 stuct 却不一定做的到.
类型: struct 是值类型, class 是引用类型, 因此它们具有所有值类型和引用类型之间的差异.
效率: 由于堆栈的执行效率要比堆的执行效率高, 但是堆栈资源却很有限, 不适合处理逻辑复杂的大对象, 因此 struct 常用来处理作为基类型对待的小对象, 而 class 来处理某个商业逻辑.
关系: struct 不仅能继承也能被继承 , 而且可以实现接口, 不过 Class 可以完全扩展. 内部结构有区别, struct 只能添加带参的构造函数, 不能使用 abstract 和 protected 等修饰符, 不能初始化实例字段.
使用场景
(1) 在表示诸如点, 矩形等主要用来存储数据的轻量级对象时, 首选 struct.
(2) 在表示数据量大, 逻辑复杂的大对象时, 首选 class.
(3) 在表现抽象和多级别的对象层次时, class 是最佳选择?
3 c++ 的编译以及运行
3.1 以 Linux 下的编译过程为例
(1)预处理器, 编译器, 汇编器和链接器的工作是什么? hello 程序的生命周期是从一个源程序 (hello.c)(称为高级 c 语言) 开始, 被其它程序转化为一系列的低级机器语言指令, 这些指令按照一种称为可执行目标程序的格式打包好, 以二进制磁盘文件的形式保存. 例: unix>?gcc -o hello hello.c 可以实现源文件向目标文件的转化, 该过程由编译程序完成. hello.c ?---->hello.i ?---->hello.s ?---->hello.o ?-->hello
拿一个简单的例子, 例子叫做 Base.c, 内容如下:#include <stdio.h>/ 这是一条注释 / int main(){printf("Hello world\n");return 0;}(1). 预处理(cpp): 预处理器不止一种, 而 C/C++ 的预处理器就是其中最低端的一种 -- 词法预处理器, 主要是进行文本替换, 宏展开, 删除注释这类简单工作. gcc -E 选项可以得到预处理后的结果, 扩展名为. i;C/C++ 预处理不做任何语法检查, 不仅是因为它不具备语法检查功能, 也因为预处理命令不属于 C/C++ 语句(这也是定义宏时不要加分号的原因), 语法检查是编译器要做的事情; 预处理之后, 得到的仅仅是真正的源代码; GCC 确实很强大, 如果是用 VC 这种 IDE, 恐怕就不能看到预处理后的结果.?
e.g. 所谓预处理, 就是把程序中的宏展开, 把头文件的内容展开包含进来, 预处理不会生成文件, 所以需要重定向
(2). 编译器(ccl): 将文本文件. i 翻译成文本文件. s, 得到汇编语言程序(把高级语言翻译为机器语言), 该种语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令. gcc -S 选项可以得到编译后的汇编代码, 扩展名为. s; 汇编语言为不同高级语言的不同编译器提供了通用的输出语言, 比如, C 编译器和 Fortran 编译器产生的输出文件用的都是一样的汇编语言. e.g.
(3). 汇编(as): 将. s 翻译成机器语言指令, 把这些指令打包成一种叫做可重定位目标程序的格式, 并将结果保存在目标文件. o 中(把汇编语言翻译成机器语言的过程).gcc -c? 选项可以得到汇编后的结果, 扩展名为. o;.o 是一个二进制文件, 它的字节编码是机器语言指令而不是字符. 如果在文本编辑器中打开. o 文件, 看到的将是一堆乱码. 把一个源程序翻译成目标程序的工作过程分为五个阶段: 词法分析; 语法分析; 语义检查和中间代码生成; 代码优化; 目标代码生成. 主要是进行词法分析和语法分析, 又称为源程序分析, 分析过程中发现有语法错误, 给出提示信息. e.g.
(4). 链接(ld):gcc 会到系统默认的搜索路径 "/usr/lib" 下进行查找, 也就是链接到 libc.so.6 库函数中去.? 函数库一般分为静态库和动态库两种. 静态库是指编译链接时, 把库文件的代码全部加入到可执行文件中, 因此生成的文件比较大, 但在运行时也就不再需要库文件了. 其后缀名一般为 ".a". 动态库与之相反, 在编译链接时并没有把库文件的代码加入到可执行文件中, 而是在程序执行时由运行时链接文件加载库, 这样可以节省系统的开销. 动态库一般后缀名为 ".so", 如前面所述的 libc.so.6 就是动态库. gcc 在编译时默认使用动态库.
3.2 Windows 下编译运行
编译器先对工程中三个源文件 main.cpp,animal.cpp,human.cpp 进行单独编译 (Compiling...)
在编译时, 由预处理器对预处理指令 (#include,#define...) 进行处理, 在内存中输出翻译单元(就是将 include 等在源文件上替换了以后产生的临时文件).
编译器接受临时文件, 将其翻译成包含机器语言指令的三个目标文件(main.obj,animal.obj,human.obj)
接下去就是链接过程(Linking...), 连接器将目标文件和你用到的相关库文件一起链接形成 main.exe.
到此, 编译也就结束了.
注意: 在编译过程中头文件不参与编译, 预编译时进行各种替换以后, 头文件就完成了其光荣使命, 不再具有任何作用
最后以一张图来结束本次内容:
3.3 总结
参考 https://www.douban.com/group/topic/126236162/
总的来说, 程序的运行包括四个过程: 预处理, 编译, 汇编和链接.
预编译阶段
. 将所有的 #define 删除,
并且展开所有的宏定义. 处理所有的条件预编译指令, 比如 #if #ifdef #elif #else #endif 等. 处理 #include 预编译指令,
将被包含的文件插入到该预编译指令的位置.
. 删除所有注释 "//" 和 "/* */"
添加行号和文件标识, 以便编译时产生调试用的行号及编译错误警告行号.
. 保留所有的 #pragma 编译器指令, 因为编译器需要使用它们
经过预编译后的. i 文件不含任何宏定义, 所有的宏已经被展开并且插入到. i 文件中.
编译阶段
在此过程经过一系列的词法分析, 语法分析, 语义分析及优化最终得到一个汇编代码文件.
注:
词法分析: 源代码进入扫描器, 进而扫描器将代码的字符分割成一系列的记号(关键字, 标识符, 字面量, 特殊 符号如加号减号), 同 时, 标识符放符号表, 数字, 字符串放文字表.
语法分析: 对扫描器产生的记号进行语法分析, 产生语法数, 生成以表达式为节点的树,
同时, 很多运算符号的 优先级和含义也被确定, 以及区分多重含义符号, 若出现表达式不合法(括号不匹配, 缺少操作符等),
编译器将报告语法分析阶段的错误.
语义分析: 此时一般对声明和类型的匹配, 类型之间的转换进行分析, 类型不匹配, 就会报错; 以及分析与语义 相关的问题, 比如: 0 作为除数等, 错误进行报错.
汇编阶段:
此阶段相对编译器比较简单, 它没有复杂的语法, 语义以及优化, 只是根据汇编指令和机器指令的对照表一一翻译即可.
4. 链接阶段: 链接器 (linker) 将一个个的目标文件 (或许还会有若干程序库) 链接在一起生成一个完整的可执行文件
本质上来说库是一种可执行代码的二进制形式, 可以被操作系统载入内存执行. 库有两种: 静态库 (.a,.lib) 和动态库(.so,.dll).
所谓静态, 动态是指链接.
# 4 命令空间
namespace 是 C++ 中的关键字, 用来定义一个命名空间, 语法格式为: namespace name{
??? //variables, functions, classes
}name 是命名空间的名字, 它里面可以包含变量, 函数, 类, typedef,#define 等, 最后由 { } 包围
5 c++ 头文件与名词空间
iostream.h: 用于控制台输入输出头文件.
fstream.h: 用于文件操作的头文件.
complex.h: 用于复数计算的头文件.
和 C 语言一样, C++ 头文件仍然以. h 为后缀, 它们所包含的类, 函数, 宏等都是全局范围的.
下面是我总结的 C++ 头文件的现状:
1) 旧的 C++ 头文件, 如 iostream.h,fstream.h 等将会继续被支持, 尽管它们不在官方标准中. 这些头文件的内容不在命名空间 std 中.
2) 新的 C++ 头文件, 如 iostream,fstream 等包含的基本功能和对应的旧版头文件相似, 但头文件的内容在命名空间 std 中. 注意: 在标准化的过程中, 库中有些部分的细节被修改了, 所以旧的头文件和新的头文件不一定完全对应.
3) 标准 C 头文件如 stdio.h,stdlib.h 等继续被支持. 头文件的内容不在 std 中.
4) 具有 C 库功能的新 C++ 头文件具有如 cstdio,cstdlib 这样的名字. 它们提供的内容和相应的旧的 C 头文件相同, 只是内容在 std 中.
6 常见的存储类型以及字节数
位: 我们常说的 bit, 位就是传说中提到的计算机中的最小数据单位: 说白了就是 0 或者 1; 计算机内存中的存储都是 01 这两个东西. 字节: 英文单词:(byte),byte 是存储空间的基本计量单位. 1byte? 存 1 个英文字母, 2 个 byte 存一个汉字. 规定上是 1 个字节等于 8 个比特(1Byte?= 8bit).
- 1024Byte(字节)=1KB
- 1024KB=1MB
- 1024MB=1GB
- 1024GB=1TB
二, 不同编译器下的数据类型长度
16 位编译器:(Turbo C/Turbo C++)
- sizeof(char) = 1
- sizeof(short) = 2
- sizeof(float) = 4
- sizeof(double) = 8
- sizeof(int) = 2
- sizeof(long) = 4
- sizeof(long double) = 16
- sizeof(void *) = 4
32 位编译器:(Visual Studio C++)
- sizeof(char) = 1
- sizeof(short) = 2
- sizeof(float) = 4
- sizeof(double) = 8
- sizeof(int) = 4
- sizeof(long) = 4
- sizeof(long long) = 8
- sizeof(void *) = 4 (表示指针类型数据长度)
64 位编译器:
- sizeof(char) = 1
- sizeof(short) = 2
- sizeof(float) = 4
- sizeof(double) = 8
- sizeof(int) = 4
- sizeof(long) = 8
- sizeof(long long) = 8
- sizeof(void *) = 8
64 位编译器:
| 数据类型 | 字节数(byte) | 位数(bit) |
| --- | --- | --- |
| char | 1 个字节 | 8 位 |
| short | 2 个字节 | 16 位 |
|int |4 个字节 | 32 位 |
|float |4 个字节 | 32 位 |
|double |8 个字节 | 64 位 |
|long |8 个字节 | 64 位 |
|long long |8 个字节 | 64 位 |
|void * |8 个字节 | 64 位 |
- define SHRT_MIN??? (-32768)??????? // minimum (signed) short value
- define SHRT_MAX????? 32767??
7 c++ 动态的分配内存
在 C 语言中, 动态分配内存用 malloc() 函数, 释放内存用 free() 函数. 如下所示: int p = (int) malloc( sizeof(int) * 10 ); // 分配 10 个 int 型的内存空间 free(p); // 释放内存
在 C++ 中, 这两个函数仍然可以使用, 但是 C++ 又新增了两个关键字, new 和 delete:new 用来动态分配内存, delete 用来释放内存. 用 new 和 delete 分配内存更加简单:
- int *p = new int; // 分配 1 个 int 型的内存空间
- delete p; // 释放内存
new 操作符会根据后面的数据类型来推断所需空间的大小. 如果希望分配一组连续的数据, 可以使用 new[]:
- `int *p = new int[10]; // 分配 10 个 int 型的内存空间
- delete[] p;`
用 new[] 分配的内存需要用 delete[] 释放, 它们是一一对应的. 和 malloc() 一样, new 也是在堆区分配内存, 必须手动释放, 否则只能等到程序运行结束由操作系统回收. 为了避免内存泄露, 通常 new 和 delete,new[] 和 delete[] 操作符应该成对出现, 并且不要和 C 语言中 malloc(),free() 一起混用.
8 引用
8.1 参数传递的本质
1. 参数的传递本质上是一次赋值的过程, 赋值就是对内存进行拷贝. 所谓内存拷贝, 是指将一块内存上的数据复制到另一块内存上. 对于像
char,bool,int,float 等基本类型的数据, 它们占用的内存往往只有几个字节, 对它们进行内存拷贝非常快速. 而数组, 结构体, 对象是
一系列数据的集合, 数据的数量没有限制, 可能很少, 也可能成千上万, 对它们进行频繁的内存拷贝可能会消耗很多时间, 拖慢程序的执行
效率
在 C/C++ 中, 我们将 char,int,float 等由语言本身支持的类型称为基本类型, 将数组, 结构体, 类 (对象) 等由基本类型组合而成
的类型称为聚合类型(在讲解结构体时也曾使用复杂类型, 构造类型这两种说法).
但是在 C++ 中, 我们有了一种比指针更加便捷的传递聚合类型数据的方式, 那就是引用(Reference).
引用 (Reference) 是 C++ 相对于 C 语言的又一个扩充. 引用可以看做是数据的一个别名, 通过这个别名和原来的名字都能够找到
这份数据. 引用类似于 Windows 中的快捷方式, 一个可执行程序可以有多个快捷方式, 通过这些快捷方式和可执行程序本身都能够运行
程序; 引用还类似于人的绰号 (笔名), 使用绰号(笔名) 和本名都能表示一个人. 引用的定义方式类似于指针, 只是用 & 取代了 *,
语法格式为:
type &name = data;
type 是被引用的数据的类型, name 是引用的名称, data 是被引用的数据. 引用必须在定义的同时初始化, 并且以后也要从一而终, 不能再引用其它数据, 这有点类似于常量(const 变量).
引用的规则:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化).
(2)不能有 NULL 引用, 引用必须与合法的存储单元关联(指针则可以是 NULL).
(3)一旦引用被初始化, 就不能改变引用的关系(指针则可以随时改变所指的对象).
引用的主要功能是传递函数的参数和返回值.
C++ 语言中, 函数的参数和返回值的传递方式有三种: 值传递, 指针传递和引用传递.
以下是 "值传递" 的示例程序.
由于 Func1 函数体内的 x 是外部变量 n 的一份拷贝, 改变 x 的值不会影响 n, 所以 n 的值仍然是 0.
- void Func1(int x)
- {
- x = x + 10;
- }
- ...
- int n = 0;
- Func1(n);
- cout << "n =" << n << endl; // n = 0 以下是 "指针传递" 的示例程序.
由于 Func2 函数体内的 x 是指向外部变量 n 的指针, 改变该指针的内容将导致 n 的值改变, 所以 n 的值成为 10.
- void Func2(int *x)
- {
- (* x) = (* x) + 10;
- }
- ...
- int n = 0;
- Func2(&n);
- cout << "n =" << n << endl; // n = 10 以下是 "引用传递" 的示例程序. 由于 Func3 函数体内的 x 是外部变量 n 的引用, x 和 n 是同
一个东西, 改变 x 等于改变 n, 所以 n 的值成为 10.
- void Func3(int &x)
- {
- x = x + 10;
- }
- ...
- int n = 0;
- Func3(n);
- cout << "n =" << n << endl; // n = 10
9 继承与派生
继承 (Inheritance) 可以理解为一个类从另一个类获取成员变量和成员函数的过程. 例如类 B 继承于类 A, 那么 B 就拥有 A 的成员变量和成员函数. 在 C++ 中, 派生 (Derive) 和继承是一个概念, 只是站的角度不同. 继承是儿子接收父亲的产业, 派生是父亲把产业传承给儿子. 被继承的类称为父类或基类, 继承的类称为子类或派生类."子类" 和 "父类" 通常放在一起称呼,"基类" 和 "派生类" 通常放在一起称呼.
public protected private 在基类和派生类中的对应关系
1)? 基类成员在派生类中的访问权限不得高于继承方式中指定的权限. 例如, 当继承方式为 protected 时, 那么基类成员在派生类中的访问权限最高也为 protected, 高于 protected 的会降级为 protected, 但低于 protected 不会升级. 再如, 当继承方式为 public 时, 那么基类成员在派生类中的访问权限将保持不变. 也就是说, 继承方式中的 public,protected,private 是用来指明基类成员在派生类中的最高访问权限的.
2)? 不管继承方式如何, 基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用).
3)? 如果希望基类的成员能够被派生类继承并且毫无障碍地使用, 那么这些成员只能声明为 public 或 protected; 只有那些不希望在派生类中使用的成员才声明为 private.
4)?+ 如果希望基类的成员既不向外暴露 (不能通过对象访问), 还能在派生类中使用, 那么只能声明为 protected. 注意, 我们这里说的是基类的 private 成员不能在派生类中使用, 并没有说基类的 private 成员不能被继承. 实际上, 基类的 private 成员是能够被继承的, 并且(成员变量) 会占用派生类对象的内存, 它只是在派生类中不可见, 导致无法使用罢了. private 成员的这种特性, 能够很好的对派生类隐藏基类的实现, 以体现面向对象的封装性
5)?. 下表汇总了不同继承方式对不同属性的成员的影响结果
| 继承方式 / 基类成员 | public 成员 | protected 成员 | private 成员 |
|--|--|--|--|
|public 继承 | public|protected | 不可见 |
|protected 继承 | protected|protected | 不可见 |
|private 继承 | private|private | 不可见 |
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限, 导致继承关系复杂, 所以实际开发中我们一般使用 public.
10 构造函数与析构函数
构造函数的使用情况
类 oop 从用户的角度出发是是描述对象所需数据以及描述用户与数据交互所需操作的 c++ 工具
类的设计, 首先需要对于对象的特征进行本质抽象, 并且根据抽象的特征设计解决方案.
类的基本类型的指定
决定数据对象所需的内存数量
决定如何解释内存中的位.
决定可以使用数据对象执行的操作以及方法.
类是一种将抽象转化为用户定义类型的 c++ 工具, 它将数据表示与操作数据的方法组合为一个简介的包.
类的规范分为两部分
1. 类的声明: 以数据成员的方式描述数据部分, 以成员函数 (方法) 的方式描述公有接口
2. 类方法的定义: 描述实现类的成员函数
类设计的要点
类的声明部分
类设计尽可能的将公有接口和实现细节分开.
公有的接口表示设计的抽象组件. 将实现细节放在一起, 并将它们与抽象分开, 称为封装
数据隐藏 (将数据放在类的私有部分中) 也是一种封装,**** 将实现细节放在私有部分中 **.*
将类函数定义和类的声明放在不同的文件当中也是一种封装.
** 数据隐藏不仅可以防止直接访问数据, 还可以让类的用户无需了解数据是如何表示的.
原则上将是实现细节与接口设计分开, 这样解耦彻底, 方便后续修改
数据隐藏是 oop 的主要目标之一, 因此数据一般放在私有部分, 组装类接口的成员函数一般放在公有部分
8
类的实现部分
1.** 定义成员函数的时候, 使用作用域标识符 (::) 来标识函数所属的类.
2. 类中的函数可以访问其私有组件.
C++ 基类和派生类的析构函数
创建派生类对象时, 构造函数的执行顺序和继承顺序相同, 即先执行基类构造函数, 再执行派生类构造函数.
而销毁派生类对象时, 析构函数的执行顺序和继承顺序相反, 即先执行派生类析构函数, 再执行基类析构函数.
11. 多继承
我不提倡在程序中使用多继承, 只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承, 能用单一继承解决的问题就不要使用多继承. 也正是由于这个原因, C++ 之后的很多面向对象的编程语言, 例如? Java,C#,PHP 等, 都不支持多继承.
虚继承的目的是让某个类做出声明, 承诺愿意共享它的基类. 其中, 这个被共享的基类就称为虚基类(Virtual Base Class), 本例中的 A 就是一个虚基类. 在这种机制下, 不论虚基类在继承体系中出现了多少次, 在派生类中都只包含一份虚基类的成员. 现在让我们重新梳理一下本例的继承关系, 如下图所示:
图 2: 使用虚继承解决菱形继承中的命名冲突问题观察这个新的继承体系, 我们会发现虚继承的一个不太直观的特征: 必须在虚派生的真实需求出现前就已经完成虚派生的操作. 在上图中, 当定义 D 类时才出现了对虚派生的需求, 但是如果 B 类和 C 类不是从 A 类虚派生得到的, 那么 D 类还是会保留 A 类的两份成员. 换个角度讲, 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类, 它不会影响派生类本身. 在实际开发中, 位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题. 通常情况下, 使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的. 对于一个独立开发的类来说, 很少需要基类中的某一个类是虚基类, 况且新类的开发者也无法改变已经存在的类体系.
C++ 标准库中的 iostream 类就是一个虚继承的实际应用案例. iostream 从 istream 和 ostream 直接继承而来, 而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类, 是典型的菱形继承. 此时 istream 和 ostream 必须采用虚继承, 否则将导致 iostream 类中保留两份 base_ios 类的成员.
图 3: 虚继承在 C++ 标准库中的实际应用虚基类成员的可见性因为在虚继承的最终派生类中只保留了一份虚基类的成员, 所以该成员可以被直接访问, 不会产生二义性. 此外, 如果虚基类的成员只被一条派生路径覆盖, 那么仍然可以直接访问这个被覆盖的成员. 但是如果该成员被两条或多条路径覆盖了, 那就不能直接访问了, 此时必须指明该成员属于哪个类. 以图 2 中的菱形继承为例, 假设 A 定义了一个名为 x 的成员变量, 当我们在 D 中直接访问 x 时, 会有三种可能性:
如果 B 和 C 中都没有 x 的定义, 那么 x 将被解析为 A 的成员, 此时不存在二义性.
如果 B 或 C 其中的一个类定义了 x, 也不会有二义性, 派生类的 x 比虚基类的 x 优先级更高.
如果 B 和 C 中都定义了 x, 那么直接访问 x 将产生二义性问题. 可以看到, 使用多继承经常会出现二义性问题, 必须十分小心. 上面的例子是简单的, 如果继承的层次再多一些, 关系更复杂一些, 程序员就很容易陷人迷魂阵, 程序的编写, 调试和维护工作都会变得更加困难, 因此我不提倡在程序中使用多继承, 只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承, 能用单一继承解决的问题就不要使用多继承.
在虚继承中, 虚基类是由最终的派生类初始化的, 换句话说, 最终派生类的构造函数必须要调用虚基类的构造函数. 对最终的派生类来说, 虚基类是间接基类, 而不是直接基类. 这跟普通继承不同, 在普通继承中, 派生类构造函数中只能调用直接基类的构造函数, 不能调用间接基类的.
12 多态
多态(polymorphism)" 指的是同一名字的事物可以完成不同的功能.
多态可以分为编译时的多态和运行时的多态.
前者主要是指函数的重载(包括运算符的重载), 对重载函数的调用, 在编译时就能根据实参确定应该调用哪个函数, 因此叫编译时的多态; 重载
在基类的函数前加上 virtual 关键字, 在派生类中重写该函数, 运行时将会根据对象的实际类型来调用相应的函数. 如果对象类型是派生类, 就调用派生类的函数; 如果对象类型是基类, 就调用基类的函数. overriding 重写
实现原理:
每一个类都有虚表
虚表可以继承, 如果子类没有重写虚函数, 那么子类虚表中仍然会有该函数的地址, 只不过这个地址指向的是基类的虚函数实现, 如果基类有 3 个虚函数, 那么基类的虚表中就有三项(虚函数地址), 派生类也会虚表, 至少有三项, 如果重写了相应的虚函数, 那么虚表中的地址就会改变, 指向自身的虚函数实现, 如果派生类有自己的虚函数, 那么虚表中就会添加该项.
派生类的虚表中虚地址的排列顺序和基类的虚表中虚函数地址排列顺序相同. 这就是 c++ 中的多态性, 当 c++ 编译器在编译的时候, 发现 Father 类的 Say()函数是虚函数, 这个时候 c++ 就会采用晚绑定技术, 也就是编译时并不确定具体调用的函数, 而是在运行时, 依据对象的类型来确认调用的是哪一个函数, 这种能力就叫做 c++ 的多态性, 我们没有在 Say()函数前加 virtual 关键字时, c++ 编译器就确定了哪个函数被调用, 这叫做早期绑定.
overloading" and"overriding"?
Overload: 顾名思义, 就是 Over(重新)--load(加载), 所以中文名称是重载. 它可以表现类的多态性, 可以是函数里面可以有相同的函数名但是参数名, 返回值, 类型不能相同; 或者说可以改变参数, 类型, 返回值但是函数名字依然不变.
Override: 就是 ride(重写)的意思, 在子类继承父类的时候子类中可以定义某方法与其父类有相同的名称和参数, 当子类在调用这一函数时自动调用子类的方法, 而父类相当于被覆盖 (重写) 了.
相当佩服第一个把这两个词翻译过来的人, 相当贴切! 方法的重写 Overriding 和重载 Overloading 是 Java 多态性的不同表现. 重写 Overriding 是父类与子类之间多态性的一种表现, 重载 Overloading 是一个类中多态性的一种表现. 如果在子类中定义某方法与其父类有相同的名称和参数, 我们说该方法被重写 (Overriding). 子类的对象使用这个方法时, 将调用子类中的定义, 对它而言, 父类中的定义如同被 "屏蔽" 了, 而且如果子类的方法名和参数类型和个数都和父类相同, 那么子类的返回值类型必须和父类的相同; 如果在一个类中定义了多个同名的方法, 它们或有不同的参数个数或有不同的参数类型, 则称为方法的重载(Overloading).Overloaded 的方法是可以改变返回值的类型. 也就是说, 重载的返回值类型可以相同也可以不同.
overloading
1, 重载
重载就是简单的复用一个已经存在的名字, 来操作不用的类型. 这个名字可以是一个函数名, 也可以是一个操作符. 由于主要是针对函数的重载, 所以对于操作符的重载在后续进行解释.
虽然可以通过默认参数的方式可以使用不同数据的参数可以调用同一个函数, 但是对于不同参数类型的操作, 可就是爱莫能助了. 为了实现同一个函数来实现不同类型的操作, 这就需要 C++ 中的一个重要的特性: 重载.
实现函数重载的主要条件是:
1) 首先发生重载的函数需要在相同的作用域中;
2)函数名称需要相同;
3)函数的参数类型不相同;
4)与 virtual 关键字无关;
下面的示例中, Base 类中的? getIndex(int x)和 getIndex(float x)为相互重载, 与 virtual 无关. 当调用 getIndex 函数的时候根据传入的参数选择不同的函数进行执行.
overriding
2, 重写
有时候希望同一个方法在基类和派生类中表现不同的行为. 也就是说通过不同的对象调用, 来实现不同的功能. 这就是面向对象中的多态, 同一个方法在不同的上下文中表现出多种形态. 重写的时候就引入了 virtual, 将需要在派生类中重写的函数在基类中声明为 virtual 类型的.
? ? ? ? 实现重写的特性
? ? ? 1)在基类中将需要重写的函数声明为 virtual;
? ? ? 2)派生类类和基类中的函数的名称相同;
? ? ? 3)函数的参数类型相同;
? ? ?4)在不同的作用范围中;(基类和派生类中)
13 纯虚函数与抽象类
在 C++ 中, 可以将虚函数声明为纯虚函数, 语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
包含纯虚函数的类称为抽象类(Abstract Class).
是因为它无法实例化, 也就是无法创建对象. 原因很明显, 纯虚函数没有函数体, 不是完整的函数, 无法调用, 也无法为其分配内存空间. 抽象类通常是作为基类, 让派生类去实现纯虚函数. 派生类必须实现纯虚函数才能被实例化.
抽象类只能作为派生类的基类, 不能定义对象, 但可以定义指针. 在派生类实现该纯虚函数后, 定义抽象类对象的指针, 并指向或引用子类对象.
1)在定义纯虚函数时, 不能定义虚函数的实现部分;
2)在没有重新定义这种纯虚函数之前, 是不能调用这种函数的.
抽象类的唯一用途是为派生类提供基类, 纯虚函数的作用是作为派生类中的成员函数的基础, 并实现动态多态性. 继承于抽象类的派生类如果不能实现基类中所有的纯虚函数, 那么这个派生类也就成了抽象类. 因为它继承了基类的抽象函数, 只要含有纯虚函数的类就是抽象类. 纯虚函数已经在抽象类中定义了这个方法的声明, 其它类中只能按照这个接口去实现.
14 C++ typeid 运算符: 获取类型信息
typeid 的操作对象既可以是表达式, 也可以是数据类型, 下面是它的两种使用方法: typeid( dataType )
typeid( expression )
15 C++ RTTI 机制精讲(C++ 运行时类型识别机制)
16 C++ 静态绑定和动态绑定, 彻底理解多态~~
17 C++ RTTI 机制下的对象内存模型(透彻)
- 18 dynamic_cast
- # 19 运算符号重载
运算符重载的格式为:
返回值类型 operator 运算符名称 (形参表列){
??? //TODO:
}
operator 是关键字, 专门用于定义重载运算符的函数. 我们可以将 operator 运算符名称这一部分看做函数名, 对于上面的代码, 函数名就是 operator+. 运算符重载函数除了函数名有特定的格式, 其它地方和普通函数并没有区别.
const 放在函数后
样的函数叫常成员函数. 常成员函数可以理解为是一个 "只读" 函数, 它既不能更改数据成员的值, 也不能调用那些能引起数据成员值变化的成员函数, 只能调用 const 成员函数.
20C++ 模板和泛型程序设计
型程序设计 (generic programming) 是一种算法在实现时不指定具体要操作的数据的类型的程序设计方法. 所谓 "泛型", 指的是算法只要实现一遍, 就能适用于多种数据类型. 泛型程序设计方法的优势在于能够减少重复代码的编写.
所谓函数模板, 实际上是建立一个通用函数, 它所用到的数据的类型 (包括返回值类型, 形参类型, 局部变量类型) 可以不具体指定, 而是用一个虚拟的类型来代替(实际上是用一个标识符来占位), 等发生函数调用时再根据传入的实参来逆推出真正的类型. 这个通用函数就称为函数模板(Function Template).
21 异常 catch
多级异常 catch
前面的例子中, 一个 try 对应一个 catch, 这只是最简单的形式. 其实, 一个 try 后面可以跟多个 catch:try{// 可能抛出异常的语句}catch (exception_type_1 e){// 处理异常的语句}catch (exception_type_2 e){// 处理异常的语句}// 其他的 catchcatch (exception_type_n e){// 处理异常的语句}
当异常发生时, 程序会按照从上到下的顺序, 将异常类型和 catch 所能接收的类型逐个匹配. 一旦找到类型匹配的 catch 就停止检索, 并将异常交给当前的 catch 处理(其他的 catch 不会被执行). 如果最终也没有找到匹配的 catch, 就只能交给系统处理, 终止程序的运行.
22 浅层拷贝与深拷贝
对于基本类型的数据以及简单的对象, 它们之间的拷贝非常简单, 就是按位复制内存. 这种默认的拷贝行为就是浅拷贝, 这和调用 memcpy() 函数的效果非常类似.
这种将对象所持有的其它资源一并拷贝的行为叫做深拷贝, 我们必须显式地定义拷贝构造函数才能达到深拷贝的目的.
如果一个类拥有指针类型的成员变量, 那么绝大部分情况下就需要深拷贝, 因为只有这样, 才能将指针指向的内容再复制出一份来, 让原有对象和新生对象相互独立, 彼此之间不受影响. 如果类的成员变量没有指针, 一般浅拷贝足以.
智能指针
头文件:#include C++ 98std::auto_ptrps (new std::string(str));
- C++ 11
- shared_ptr
- unique_ptr
- weak_ptr
- auto_ptr(被 C++11 弃用)
- shared_ptr
多个智能指针可以共享同一个对象, 对象的最末一个拥有着有责任销毁对象, 并清理与该对象相关的所有资源. 支持定制型删除器 (custom deleter), 可防范 Cross-DLL 问题(对象在动态链接库(DLL) 中被 new 创建, 却在另一个 DLL 内被 delete 销毁), 自动解除互斥锁
weak_ptr
weak_ptr 允许你共享但不拥有某对象, 一旦最末一个拥有该对象的智能指针失去了所有权, 任何 weak_ptr 都会自动成空 (empty). 因此, 在 default 和 copy 构造函数之外, weak_ptr 只提供 "接受一个 shared_ptr" 的构造函数. 可打破环状引用(cycles of references, 两个其实已经没有被使用的对象彼此互指, 使之看似还在 "被使用" 的状态) 的问题
unique_ptr
unique_ptr 是 C++11** 才开始提供的类型, 是一种在异常时可以帮助避免资源泄漏的智能指针. 采用独占式拥有, 意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有. 一旦拥有着被销毁或编程 empty, 或开始拥有另一个对象, 先前拥有的那个对象就会被销毁, 其任何相应资源亦会被释放. unique_ptr 用于取代 auto_ptrauto_ptr 被 c++11 弃用, 原因是缺乏语言特性如 "针对构造和赋值" 的? std::move? 语义, 以及其他瑕疵 **.
auto_ptr 与 unique_ptr 比较 auto_ptr 可以赋值拷贝, 复制拷贝后所有权转移; unqiue_ptr 无拷贝赋值语义, 但实现了 move? 语义; auto_ptr 对象不能管理数组(析构调用? delete),unique_ptr 可以管理数组(析构调用? delete[]?);
23 STL 标准模板库
来源: http://www.bubuko.com/infodetail-3280088.html