C++ 学习笔记
C++学习笔记
version : v1.0 「2022.8.7」 最后补充
author: Y.Z.T.
摘要: 自己在C++学习时做的笔记,随便记录一下
简介: 其实就是学习《C++ Primer Plus》的读书笔记,因为目前开发还是主要用C语言,部分感觉不常用的特性,仅简单记录或不记录
1. 基础部分
1.1 std::
作用域解析符(::);
一般在调用c++标准库的时候,要写上std
1.2 cout打印变量
1 | cout << carrots; |
cout能智能根据其后的数据类型相应调整其行为,是一个运算符重载的例子。
1.2.1 cout与printf()
1.3 cin输入语句
cin是与cout对应的用于输入的对象
1.4 类简介
1.5 位与字节
1.6 数据类型长度
short、int和long类型都表示整型值,存储空间的大小不同。一般,short类型为半个机器字长(word)长,int类型为一个机器字长,而long类型为一个或两个机器字长(在32位机器中int类型和long类型通常字长是相同的)。
类型 | 16位系统/字节 | 32位系统/字节 | 64位系统/字节 |
---|---|---|---|
char | 1 | 1 | 1 |
char* | 2 | 4 | 8 |
short | 2 | 2 | 2 |
int | 2 | 4 | 4 |
long | 4 | 4 | 8 |
long long | 8 | 8 | 8 |
数据类型 | 说明 | 32位字节数 | 64位字节数 | 取值范围 |
---|---|---|---|---|
bool | 布尔型 | 1 | 1 | true,false |
char | 字符型 | 1 | 1 | -128~127 |
unsigned char | 无符号字符型 | 1 | 1 | 0~255 |
short | 短整型 | 2 | 2 | -32768~32767 |
unsigned short | 无符号短整型 | 2 | 2 | 0~65535 |
int | 整型 | 4 | 4 | -2147483648~2147483647 |
unsigned int | 无符号整型 | 4 | 4 | 0~4294967295 |
long | 长整型 | 4 | 8 | – |
unsigned long | 无符号长整型 | 4 | 8 | – |
long long | 长整型 | 8 | 8 | -264 ~ 264 -1 |
float | 单精度浮点数 | 4 | 4 | 范围-2128~2128 精度为6~7位有效数字 |
double | 双精度浮点数 | 8 | 8 | 范围-21024~21024 精度为15~16位 |
long double | 扩展精度浮点数 | 8 | 8 | 范围-21024~21024 精度为15~16位 |
* | 地址 | 4 | 8 | – |
2的:
8次方: 256
16次方: 65536
32次方: 4 294 967 296
1.7 浮点数的E表示法
用于表达很大或很小的数
1.7.1 浮点数在内存中的储存
1️⃣
现在绝大多数计算机使用的浮点数,一般采用的是 IEEE 制定的国际标准,这种标准形式如下图:
这三个重要部分的意义如下:
- 符号位: 表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
- 指数位: 指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大;
- 尾数位: 小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2-2,尾数部分就是
0011
,而且 尾数的长度决定了这个数的精度 ,因此如果要表示精度更高的小数,则就要提高尾数位的长度;
2️⃣
用 32
位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float
变量,而用 64
位来表示的浮点数,称为双精度浮点数,也就是 double
变量,它们的结构如下:
可以看到:
double
的尾数部分是52
位,float 的尾数部分是23
位,由于同时都带有一个固定隐含位(这个后面会说),所以 double 有 53 个二进制有效位,float 有 24 个二进制有效位,所以所以它们的精度在十进制中分别是log10(2^53)
约等于15.95
和log10(2^24)
约等于7.22
位,因此 double 的有效数字是15~16
位,float 的有效数字是7~8
位,这些是有效位是包含整数部分和小数部分;- double 的指数部分是 11 位,而 float 的指数位是 8 位,意味着 double 相比 float 能表示更大的数值范围;
3️⃣
例: 我们就以 10.625
作为例子,看看这个数字在 float 里是如何存储的
- 首先,我们计算出 10.625 的二进制小数为 1010.101;
- 然后把小数点,移动到第一个有效数字后面,即将 1010.101 右移
3
位成1.010101
, - 右移 3 位就代表 +3,左移 3 位就是 -3,float 中的「指数位」就跟这里移动的位数有关系,把移动的位数再加上「偏移量」,float 的话偏移量是 127,相加后就是指数位的值了,即指数位这 8 位存的是
10000010
(十进制 130),因此你可以认为「指数位」相当于指明了小数点在数据中的位置。 1.010101
这小数点右侧的数字就是 float 里的「尾数位」,由于尾数位是 23 位,则后面要补充 0,所以最终尾数位存储的数字是01010100000000000000000
。
4️⃣
在算指数的时候,为什么要加上偏移量呢?
前面也提到,指数可能是正数,也可能是负数,即指数是有符号的整数,而有符号整数的计算是比无符号整数麻烦的,所以为了减少不必要的麻烦,在实际存储指数的时候,需要把指数转换成无符号整数,float 的指数部分是 8 位,IEEE 标准规定单精度浮点的指数取值范围是 -127 ~ +128
,于是为了把指数转换成无符号整数,就要加个偏移量,比如 float 的指数偏移量是 127
,这样指数就不会出现负数了。
比如,指数如果是 8,则实际存储的指数是 8 + 127 = 135,即把 135 转换为二进制之后再存储,而当我们需要计算实际的十进制数的时候,再把指数减去偏移量即可。
细心的朋友肯定发现,移动后的小数点左侧的有效位(即 1)消失了,它并没有存储到 float 里,这是因为 IEEE 标准规定,二进制浮点数的小数点左侧只能有 1 位,并且还只能是 1,既然这一位永远都是 1,那就可以不用存起来了,于是就让 23 位尾数只存储小数部分,电路在计算时会自动把这个 1 加上,这样就可以节约 1 位的空间,尾数就能多存一位小数,相应的精度就更高了一点。
那么,对于我们在从 float 的二进制浮点数转换成十进制时,要考虑到这个隐含的 1,转换公式如下:
举个例子,我们把下图这个 float 的数据转换成十进制,过程如下:
类型 | 符号位 | 指数 | 尾数 |
---|---|---|---|
Float | 1位(第31位) | 8位(第23~30位) | 23位(第0~22位) |
Double | 1位(第63位) | 11位(第52~62位) | 52位(第0~51位) |
1.8 string类简介
sring类能让我们像处理普通变量一样处理字符串。
1.9 左值/右值
凡是能对其进行取地址的都是左值*[即能开辟内存空间储存数据数值的]*;不能进行取地址的是右值[如:a+b、10等]。
1.10 类vector简介
相当于动态数组
1.11 类array简介
静态数组,用栈分配内存
2. 函数指针
2.1 声明函数指针
如函数原型为:
1 | double pam(int); |
则可声明一个函数指针:
1 | double (*pf)(int); |
[其中double是返回值, int 是特征标; 特征标 和返回值必须与原型相同]
这与函数pam()的声明相似, 只是将pam 替换成了 *(pf)
[注意: (*pf)也是函数 , 而pf则是函数指针]。
正确声明pf后 , 可将对应的函数地址赋给它:
1 | pf = pam; |
2.2 调用函数
将地址赋给函数指针后 , 直接将(*pf)看作函数名就行:
1 | double x = (*pf)(5); |
3. 函数指针数组
如果想定义一个包含三个函数指针的数组,可定义如下:
1 | double * f1(const double ar[],int n); |
其中 (pa)[i] 表示的是数组中的元素 , 即函数指针*。
4. 内联函数与宏
4.1 宏与带参宏
宏定义#define
本质是替换,从宏变成最终替换文本的过程称为宏展开。
4.2 带参宏
带参宏的用法与函数调用不完全相同,可能存在部分陷阱。
1 |
|
[注意 :上面两个运行结果分别是 17 ;42]
4.2.1 原因如下:
SQUARE(x+2);
被替换成(5+2 * 5 +2 = 17)
SQUARE(++x);
被替换成(++x * ++x = 6*7 =42)
4.2.2 总结:
- 宏起到的作用只是替换,而不提供计算;
- 宏是在代码处不加任何验证的简单替代
- 宏不可以在运行时调试
4.3 内联函数
4.3.1 定义:
内联函数可以定义在头文件中,因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题
如:
1 | inline double square(double x){return x*x;} |
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说,inline可以带来一定的效率提升,而且和C时代的宏函数相比,inline 更安全可靠。可是这个是以增加空间消耗为代价的。因为内联函数相当于代码复制,在运行时,将内联的代码复制上去。
内联函数一般使用情况:
- 一个函数不断被重复调用。
- 函数只有简单的几行,且函数不包含for、while、switch语句。
[一般来说,我们写小程序没有必要定义成inline,但是如果要完成一个工程项目,当一个简单函数被调用多次时,则应该考虑用inline。]
4.3.2 总结:
- 宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
- 宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。
- inline函数是函数,但在编译中不单独产生代码,而是将有关代码嵌入到调用处。
- 编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定 义则不会;
5. c++引用
用作函数参数,会使得函数中的变量名成为调用程序中的变量的别名。称为引用传递
引用时,两个变量本质上是同样的,修改其中一个,另一个也会改变。
5.1 左值引用:
只能是引用左值:
1 | int &c = a; |
5.2 常引用
能引用右值,但只能用来读取数据而不能用来修改数据:
1 | const int &c = (a + b); |
5.3 右值引用:
用于支持移动语义
1 | int &&x = 10; |
5.4 结构体引用
引用主要就是用于在结构和类中应用的:
- 可以将指向该结构的引用作为参数:
1 | void set_pc(free_throws & ft); |
2.如果加入const,这只是使用这些数据 而不进行修改
1 | void set_pc(const free_throws & ft); |
3.返回引用
1 | free_throws & accumulate (free_throws & target , const free_throws & source); |
将accumulate()返回值作为参数传递给了display(),意味着将team传递给了display(),意味着display()中的ft指向team
等效于:
1 | accumulate(team,one); |
[注意: 返回引用的函数实际上是被引用的变量的别名]
5.5 类引用
基类引用可以指向派生类,而无需强制类型转换 ;(可以定义一个接收基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数)
5.5 何时使用引用参数
6. c++默认参数
指在当函数调用中省略了实参时自动使用的一个值;
[ 在设计类时,通过使用默认参数,可以减少要定义的构析函数、方法、及方法重载的数量];
定义默认值:
7. 函数重载
[注意: 编译器会把类型引用和类型本身视为同一个特征标]
如:
1 | double cube(double x); |
这两个函数会被视为同样的特征标。
7.1 何时使用函数重载
8. 函数模版
需要多个对不同类型使用同一种算法的函数时,可使用模版。
8.1 定义
1 | template <typename AnyType> |
类型名:AnyType 可任意命名
[注意:原型声明时,记得将template <typename AnyType>
带上]
8.2 注意事项:
编写一个函数,在编译调用后仍会生成多个不同的函数
9. 实例化和具体化
10. const 限定符
const
定义的变量,在初始化之后不可被改变
10.1 const
和 指针
1 | const int x = 0; |
10.2 顶层 const
如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此, 指针本身是不是常量 以及 指针所指的是不是一个常量 就是两个相互独立的问题。
- 用名词顶层const(top-level const)表示指针本身是个常量
- 而用名词底层const (low-level const)表示指针所指的对象是一个常量。
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显
1 | int i = 0; |
10. 3constexpr
变量
常量表达式(const expression
) ,声明为此类型表示此变量一定为常量,而且必须用常量初始化
11. 作用域
11.1 头文件的内容
- 函数原型
- 使用
#define
和const
定义的符号常量 - 结构声明
- 类声明
- 模版声明
- 内联函数
[注意: 不要将函数定义和变量声明放在头文件中,容易导致重定义。 除非该函数是内联函数]
11.2 #inlcude
中“ ” 和 < >的区别
11.3 内存在程序中保留的时间
- 自动储存:局部变量、函数参数 在程序开始执行其所属的函数和代码块时被创建, 在执行完函数和代码块时,内存被释放
- 静态储存:全局变量、
staic
关键字定义的局部变量和全局变量 。在整个程序运行过程中都存在。 - 动态存储: 用
new
分配的内存会一直存在,直到使用delete
关键字将其释放。 也称为堆
11.4 作用域和链接
- 作用域: 值在上面范围内能看到这个( 函数/变量 ),描述了名称在文件的多大范围内可见。
- 链接性: 描述了名称在不同单元键的共享
局部变量作用域: 作用域只在定义它的代码块中
全局变量作用域: 作用域为定义位置到文件结尾
自动变量作用域: 作用域为局部
函数体作用域: 整个类或整个名称空间,但不能是局部的
[例如: 局部变量和函数形参的 储存持续性为自动,作用域为局部; 没有链接性。 当程序开始执行时,为该变量分配内存,当函数结束时,这些变量会消失。]
特别注意:
11.5 静态储存的链接性
11.5.1 静态变量的链接性:
- 外部链接性: 可在其他文件中访问
- 内部链接性: 只能在当前文件中访问
- 无链接性: 只能在当前函数或代码块中访问
静态变量的数量在程序运行期间是不变的, 编译器会分配固定的内存块来储存所有的静态变量,这些变量在程序
执行期间一直存在。
1 | int a = 0; // 静态持续变量,连接性为外部 |
特别注意: 变量c是储存在静态数据区的,会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
即使funct
函数没有被执行,c变量也是留在内存中的,而变量d则会消失。
11.5.2 静态变量的初始化特性
所有静态持续变量在初始化时都会被初始化为0 ,称为零初始化 。
在静态数据区,内存中所有的字节默认值都是 0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 \0
太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 \0
。
11.6 变量储存方式总结
11.7 volatile
限定符
11.8 C++在哪里查找函数
12. C++名称空间
12.1 声明区域与作用域
-
声明区域: 声明区域是可以在其中进行声明的区域(如:全局变量 -> 声明区域在所在文件; 局部变量 -> 声
明区域在其所在的代码块)
-
作用域: 变量对程序而言可见的范围称为作用域(变量的作用域从声明点开始,到其声明区域的结尾,比声明区域小。)
特别注意: 变量并非在作用域的任何位置都是可见的。(例如:在函数中定义的局部变量[ 该变量的声明区域
为整个函数],将隐藏同一文件下的同名全局变量)
12.2 namespace
关键字
通过namespace
关键字 来定义一个新的名称空间;
一个名称空间中的名称不会与另一个名称空间中的名称发生冲突
1 | namespace Jack |
特别注意: 名称空间可以是全局的,也可以在另一个名称空间中; 但不能在代码块中
除了自定义的名称空间外,还存在一个全局名称空间,全局变量都位于全局名称空间。
12.3 访问名称空间中的名称
12.3.1 通过作用域解析运算符 ::
1 | Jack :: pail = 12; // 使用变量 |
- 其中 包含名称空间的名称(如Jack :: pail)称为限定的名称
- 未被修饰的名称 (如 pail) 称为未限定的名称
12.3.2 using
声明和using
编译指令
-
using
声明 : using声明由限定的名称和前面的关键字using组成1
using Jack :: pail; // using声明
-
using 编译指令: 由名称空间名和前面关键字 using namespace 组成
1 | using namespace Jack; // 让名称空间内的所有名称可用 |
12.3.2.1 using
声明
using 声明 会将特定的名称添加到其所属的声明区域中
1 | void main() |
- main函数中的 using 声明将 Jack :: pail 中的pail添加到main()定义的声明区域中。
- 完成声明后,可用
pail
代替Jack :: pail
使用。 - 如果在函数外面使用using声明 会将名称添加到全局名称空间中
12.3.2.2 using
编译指令
using
编译指令会使得所有名称可用,而不需要使用作用域解析运算符 。- 在全局声明区域使用
using
编译指令,会使得该名称空间中的名称全局可用。 - 对应的在函数中 使用
using
编译指令, 会使得该名称空间中的名称在该函数中可用。
特别注意: 使用 using
编译指令 和 using
声明 可能会增加名称冲突的可能
例如:
- 使用作用域解析运算符:
1 | jack :: pail = 3; |
[ 变量jack :: pail
和 jill :: pail
是不同的标识符,表示不同的内存单元 。]
- 使用
using声明
:
1 | using jack :: pail ; |
[ 这样会导致名称冲突,导致编译器报错 ]
12.3.3 using
声明和using
编译指令的区别
- 使用
using声明
就像声明了相应的名称一样。 using
编译指令 就像是大量使用作用域解析符。
12.4 名称空间嵌套
1 | namespace ele |
这里flame
指的是 ele :: fire :: flame
,也可以使用using编译指令是内部空间可用:
1 | using namespace ele :: fire ; |
12.5 在名称空间内使用using
声明和using
编译指令
1 | namespace ma |
因为jill :: pail
既位于名称空间 ma
中也位于 名称空间 jill
中, 所以
jill :: pail
等价于 ma :: pail
12.6 未命名名称空间
1 | namespace |
12.7 名称空间使用原则
13. 面向对象
13.1 OPP的特性
- 抽象
- 封装和数据隐藏
- 多态
- 继承
- 代码的可重用性
13.2 类的组成部分
-
类声明: 以数据成员的方式描述了数据部分,以成员函数(即方法)的方式描述了接口。
-
类方法: 描述如何实现类成员的函数。
[ 类声明提供了类的蓝图 ,方法定义则提供细节 ]
通常: 会将类定义 放在头文件中,将类方法的代码放在源文件中。
例如:
1 | chass Stock // 类声明 |
13.3 访问控制
- 类一般会有两个部分:
private
(私有的,数据隐藏);public
(公共接口) private
部分,凡是在private
部分出现的标识(如函数,变量等),只能 通过public
的接口访问。- 不必在类声明中显式的使用
private
关键字,因为这是类对象默认的访问控制
13.3.1 类和结构体
13.3.2 类成员函数定义
类成员函数的定义与正常函数类似, 不同点主要是;
-
定义成员函数时,使用作用域解析符(::)来标识所属的类。
-
可以将另一个类成员函数命名为相同的名字(比如可以将另一个类成员函数也命名为update())
1
2
3
4
5
6
7
8
9void Stock::update(double price)
{
...
}
void abc::update(double price)
{
....
} -
类方法可以访问类的
private
部分 -
同一个类(如
Stock
类)的其他成员,可以不加解析符(::),直接使用update()
方法,因为同属一个类,因此update()
是可见的 -
定义位于类声明中的函数会自动成为内联函数 , 通常将短小的成员函数作为内联函数。
13.4 类对象的内存
-
每个创建的类对象都有自己的储存空间,用于储存器内部的变量和成员。
-
所有类对象共享一组类方法,即所有类对象执行统一代码块的类方法。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/****************** 头文件 *****************/
chass Stock // 类声明
{
private:
std :: string company;
long shares;
double share_val;
double total_val;
void set_tol() { total_val = shares * share_val; }
public:
void acquire (const std :: string & co, long n , double pr);
void buy(long num, double price);
void sell(long num , double price);
void update(double price);
void show();
} ;
/*********************** mian.cpp ************************/
Stock a; // 声明a对象
Stock b; // 声明b对象
void main(void)
{
a.shares = 1; // 变量占有独立的内存块
b.shares = 1;
a.show(); // 类方法都调用的是 Stock::show() 代码块中的内容,只是用于不同数据
b.show();
}
13.5 数据封装
所有的 C++ 程序都有以下两个基本要素:
- 程序语句(代码): 这是程序中执行动作的部分,它们被称为函数。
- 程序数据: 数据是程序的信息,会受到程序函数的影响。
- 封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
- 数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。
例如:
1 |
|
14. 构造函数 和 析构函数
因为类的数据成员大部分情况下是被隐藏的,无法直接访问来进行初始化。
1 | struct thing |
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
14.1 构造函数定义
构造函数原型在类声明的共有部分 ,构造函数定义与正常函数定义类似(注意没有返回值)
1 | /******************* 头文件 ********************/ |
特别注意: 构造函数的参数表示的是不是类成员,而是赋给类成员的值, 因此参数名不能与类成员相同 ,
所以通常可以在数据成员前面使用 m_前缀。
1 | chass Stock // 类声明 |
14.2 使用构造函数
-
可以显式的调用构造函数
1
Stock a = Stock("wor",1 ,1.5);
-
也可以用更常见的方式来调用
1
Stock b("wor",1 ,1.5); // 等价于 Stock b = Stock("wor",1 ,1.5);
特别注意: 无法使用对象来调用构造函数 , 因为在构造函数构造出对象之前 ,对象是不存在的 ,因此构造
函数用来创建对象,而不能通过对象来调用
14.3 析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
1 | /********************* 头文件 ********************/ |
运行结果:
1 | Object is being created |
总结:
- 就像对象被创建时调用构造函数一样 , 当对象被删除时,程序将调用析构函数 ;
- 每个类只能有一个析构函数
- 析构函数没有返回值,也没有参数。
15. this指针
15.1 简介
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成
员函数内部,它可以用来指向调用对象,只有成员函数才有 this 指针。
15.2 为什么要引入this指针
当我们调用成员函数时,实际上是替某个对象调用它。
成员函数通过一个名为 this 的额外隐式参数来访问调用它的那个对象
用c语言的方式表达this指针的作用:
1 | struct Car |
16. 对象数组初始化
16.1 使用默认构造函数初始化
1 | Stock buff[4]; // 创建一个包含4个成员的Stock对象数组 |
注意: 要求没有显示定义任何构造函数的时候,才能这样使用声明对象数组
16.2 使用构造函数初始化
1 | Stock buff[4] = { |
注意: 这样必须为每个元素调用构造函数 , 除非定义了多个构造函数
17. 类作用域
17.1 简单特性
- c++相比C语言,在全局作用域和局部作用域外,引入了新的类作用域。
- 类中定义的名称(如数据成员名和类成员函数名等) 作用域是整个类. 只有在该类中是已知的,在类外都是不可知
- 可以在不同类中使用相同的成员名
- 不能直接从外部访问类的成员,公有函数也不可以, 要调用公有函数成员,必须通过对象
- 在定义成员函数时,必须使用类作用域解析符
18. C++中的运算符重载
可以重定义或重载大部分 C++ 内置的运算符, 之后就能使用自定义类型的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator
和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
1 | Box operator+(const Box&); |
可以通过重载 +
运算符 用于把两个 Box 对象相加,返回最终的 Box 对象。大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。
例:
1 |
|
19. 嵌套结构和类
- 在类中声明中声明的结构、类、枚举被称为是被嵌套在类中, 其作用域为整个类 ;
- 这个被嵌套的类的作用域就只在它的上一级类中。
- 这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。
- 如果是在类的私有部分 进行声明的, 则只能在这个类中使用被声明的类型,在上一级类中可见,而对于外部是不可见的。
- 如果是在类的公共部分 进行声明的,则可以在类的外部使用作用域解析符(::) 使用被声明的类型。
例:
1 |
|
运行结果:
1 | 1 |
20. 类继承
20.1 基本概念
- 面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
- 当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为 基类 ,新建的类称为 派生类 。
- 继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。
1 | // 基类 |
20.2 派生类
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生
列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
1 | class derived-class: access-specifier base-class |
- 其中
derived-class
是派生类的名 - 修饰符
access-specifier
是 public、protected 或 private 其中的一个。如果未使用访问修饰符access-specifier
,则默认为 private。- 使用公用派生,基类的公有成员将成为派生类的公有成员
- 而基类的私有成员也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
base-class
是之前定义过的某个类的名称(即基类)
举例如下:
1 |
|
一般需要在继承特性中添加:
- 派生类需要自己的构造函数
- 派生可以根据需要添加额外的成员数据和成员函数
20.3 继承与访问控制
20.3.1 派生类对象初始化:
派生类不能直接访问基类的私有成员而必须通过基类方法访问,所以派生类构造函数必须使用基类构造函数。
1 | Rectangle::Rectangle(int b, int wid, int hig):Shape(wid,hig) |
如上所示:
Shape(wid,hig)
调用基类的Shape()
构造函数Rectangle
构造函数把实参“2”,“3”
赋予形参“wid”,“hig”
,然后这些参数作为实参传递给Shape()
构造函数- 后者将创建一个嵌套的
Shape
对象 ,并将数据“2”,“3”
储存在该对象中。 - 然后,程序进入
Rectangle
构造函数体 ,完成对Rectangle
对象的创建。
注意:
- 创建派生类对象时,程序先调用基类构造函数,再调用派生类构造函数。
- 基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新的数据成员。
- 派生类构造函数总是调用一个基类构造函数。
- 可以使用初始化列表指定所使用的基类构造函数,否则将使用默认的基类构造函数。
20.3.2 访问权限
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private
。
C++ 中 class
的访问权限分的很细,有 public
、protected
及 private
三种,派生子类时又有三种派生类型,搭配起来就有 9 中不同的组合了。下面用一张表总结了这些搭配的不同。
其中,括号中的是在派生类中这些成员变量(函数)的角色。
成员变量修饰符 | 类外的普通函数 | public 派生类 | private 派生类 | protected 派生类 |
---|---|---|---|---|
public | 可以访问 | 可以访问(public) | 可以访问(private) | 可以访问(protected) |
protected | 无法访问 | 可以访问(protected) | 可以访问(private) | 可以访问(protected) |
private | 无法访问 | 无法访问(private) | 无法访问(private) | 无法访问(private) |
如上所示: [派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员]
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
20.3.3 继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过前面讲解的访问修饰符 access-specifier
来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。 (即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行任何操作。)
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
20.3.4 多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++ 类可以从多个类继承成员,语法如下:
1 | class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… |
其中,访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔
如下面例子所示:
1 |
|
21. 多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。方法的行为应取决于调用
该方法的对象
构成多态的条件:
- 必须存在继承关系;
- 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
- 存在基类的指针,通过该指针调用虚函数。
有两种重要机制可用于实现多态公有继承:
- 在派生类中重新定义基类的方法
- 使用虚函数
21.1 虚函数
21.1.1 静态联编译和动态联编
21.1.2 虚函数简介
C++多态是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖,或者称为重写。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,动态绑定。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为 “虚”函数 。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。
例:
1 |
|
注意: 虚函数只能借助于指针或者引用来达到多态的效果。
21.1.3 虚函数定义
- 通常可以在基类中将派生类会重新定义的函数声明为虚函数
- 函数在基类中被声明为虚函数后,它在派生类中将自动成为虚函数
- 如果在派生类中使用关键字
virtual
来指出哪些函数是虚函数也可以。 - 如果要在派生类中重新定义基类的方法,通常应将基类函数声明为虚的,这样程序将根据对象类型而不是引用或指针的类型来选择函数版本
- 将基类声明一个虚析构函数也是惯例。
注意: 关键字virtual
只用于类声明的方法原型中,而不能用在定义中
21.1.4 虚函数的注意事项
-
使用虚函数时,在内存和执行速度方面有一定的成本,包括:
-
每个对象都将增大,增大量为存储地址的空间;
-
对于每个类,编译器都创建一个虚函数地址表(数组);
-
对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
-
-
在基类函数的声明中使用关键字
virtual
可使该方法在基类以及 所有的派生类 (包括从派生类派生出来的类)中是虚的。 -
如果使用指向对象的引用或指针来调用虚方法,程序将使用为 对象类型定义的方法, 而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。
-
如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
-
构造函数不能是虚函数。
-
如果派生类没有重新定义函数,将使用该函数的基类版本。
21.2 抽象基类和纯虚函数
21.2.1 纯虚函数介绍
如果想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
例如:
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
所以引入纯虚函数的概念:
- 将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)
- 则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
- 声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
21.2.2 纯虚函数定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:
1 | virtual void funtion1()=0 |
21.2.3 抽象类
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义: 称带有纯虚函数的类为抽象类。
(2)抽象类的作用: 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意:
- 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。