写在前面
记录一下C++的要点,参考的书籍如下:
- 《C++ Primer Plus》是偏向教学的工具书,可以视为偏基础;
- 《C++ Primer》 是偏向工程实践的工具书,可以视为偏进阶;
- 《深度探索C++对象模型》则针对C++对象模型进行剖析,比较进阶;
一、C++对象模型
- 主要来自《深度探索C++对象模型》;
1. C++对象模型概述
- C++对象模型说明了一个C++类对象所需要的存储空间(包括成员变量和成员函数),以及如何使用这些空间实现C++类对象的全部功能(成员函数);
- 主要来自:
- 《第一章 Object Lessons》;
- 《第三章 Data语意》;
1.1 未继承的类对象模型
- 一个未继承其他类的C++对象模型如下:
-
每个类对象有自己的数据,由
this
指针指向,包括:- 非静态数据成员,含位对齐所需的额外空间;
- 按照声明的顺序从低地址到高地址排序;
- 由访问控制修饰符划分的变量区块不影响总数据的大小;
- 指向虚函数表的指针(CPU字长,64位8字节,32位4字节);
- 放在最高地址上;
- 注意:只有本类定义了新的虚函数才会有虚函数表,也才会有指向虚函数表的指针;
- 非静态数据成员,含位对齐所需的额外空间;
-
虚函数表包括:
- 指向本类类型信息结构的指针(CPU字长,64位8字节,32位4字节);
- 指向本类新定义的虚成员函数的指针;
-
余下的在其他地方存储的成员有:
- 静态数据成员:在读写数据段的
.data
或者.bss
中; - 虚成员函数、静态成员函数、非静态成员函数:在只读数据段的
.text
中;
- 静态数据成员:在读写数据段的
1.2 单一继承无多态的类对象模型
- 基类未实现虚函数,派生类的继承方式也不是虚继承;
- 则派生类的C++对象模型如下:
-
每个派生类对象有自己的数据,由
this
指针指向,包括:- 基类:
- 非静态成员变量,含位对齐所需的额外空间;
- 无指向虚函数表的指针,因为基类未定义虚函数;
- 派生类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针;
- 注意:只有派生类定义了新的虚函数才会有虚函数表,也才会有指向虚函数表的指针;
- 基类:
-
虚函数表包括:
- 指向派生类类型信息结构的指针;
- 指向派生类新定义的虚成员函数的指针;
1.3 单一继承有多态的类对象模型
- 基类实现了虚函数,但派生类的继承方式不是虚继承;
- 则派生类的C++对象模型如下:
-
每个派生类对象有自己的数据,由
this
指针指向,包括:- 基类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针;
- 派生类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针(等同于基类指向虚函数表的指针);
- 注意:
- 无论派生类是否定义了新的虚函数,都会有虚函数表,也会有指向虚函数表的指针,因为起码要指向基类的虚函数表从而使用基类的虚函数以实现多态;
- 单一继承的派生类中仅有一个虚函数表指针,和基类共用一个指针;
- 注意:
- 基类:
-
虚函数表包括:
- 指向派生类类型信息结构的指针;
- 指向基类和派生类重写的虚成员函数的指针;
-
一些说明:
- 用基类指针指向派生类的时候,实际上只能访问派生类的基类部分数据;
1.4 多重继承的类对象模型
- 派生类继承了多个基类,基类可以实现了虚函数也可以没有实现虚函数,但派生类的继承方式不是虚继承;
- 则派生类的C++对象模型如下:
-
每个派生类对象有自己的数据,由
this
指针指向,包括:- 基类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针;
- 注意:只有基类定义了新的虚函数才会有虚函数表,也才会有指向虚函数表的指针;
- 派生类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针;
- 注意:
- 如果某个基类有虚函数表,则无论派生类是否定义了新的虚函数,都会有虚函数表,也会有指向虚函数表的指针,因为起码要指向父类的虚函数表从而使用父类的虚函数以实现多态;
- 多重继承的派生类的虚函数表指针和主基类共用一个,非主基类如果有虚函数表,则会有额外的虚函数表指针(占对象模型空间)作虚函数表地址偏移;
- 注意:
- 基类:
-
虚函数表包括:
- 主基类,和派生类共用一个:
- 指向派生类类型信息结构的指针;
- 指向主基类和派生类重写的虚成员函数的指针;
- 非主基类:
- 指向派生类类型信息结构的指针;
- 指向非主基类的虚成员函数的指针;
- 主基类,和派生类共用一个:
1.5 虚继承的类对象模型
- 派生类继承了多个基类,这些基类使用了虚继承,但派生类的继承方式不是虚继承;
- 则派生类的C++对象模型如下:
-
每个派生类对象有自己的数据,由
this
指针指向,包括:- 基类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向被虚继承基类的指针;
- 被虚拟继承基类放在也在派生类对象自己的数据中,但在最下面(也就是最高地址);
- 注意:只有基类是虚继承其他基类,才会有指向被虚继承基类的指针;
- 指向虚函数表的指针;
- 注意:
- 如果基类定义了新的虚函数,会有虚函数表,也会有指向虚函数表的指针;
- 如果基类虚继承其他基类,则也会有虚函数表,因为需要存放指向虚基类的指针偏移,不然找不到虚基类的入口;
- 注意:
- 派生类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针;
- 注意:
- 如果派生类定义了新的虚函数,会有虚函数表,也会有指向虚函数表的指针;
- 注意:
- 被虚继承基类:
- 非静态成员变量,含位对齐所需的额外空间;
- 指向虚函数表的指针;
- 注意:只有基类定义了新的虚函数才会有虚函数表,也才会有指向虚函数表的指针;
- 基类:
-
虚函数表包括:
- 基类:
- 指向类类型信息结构的指针;
- 指向其他虚成员函数的指针;
- 派生类:
- 指向类类型信息结构的指针;
- 指向其他虚成员函数的指针;
- 基类:
-
一些说明:
- 使用虚继承虽然能够节省空间,但会牺牲运行的效率;
- 因为对被虚继承的基类的访问是间接访问,无法直接通过偏移地址来访问;
- 所以访问的效率会比访问普通基类或者派生类本身的数据要慢得多;
- 因此,被虚继承的基类中最好不要有任何的成员变量,也不要实现任何成员函数,仅包含纯虚函数(抽象类),只作接口用;
- 使用虚继承虽然能够节省空间,但会牺牲运行的效率;
2. 三种程序设计范式
- 主要来自《第一章 Object Lessons》;
2.1 过程范式(Procedural)
- 类C风格;
- 将变量和变量相关的函数分离开;
2.2 抽象数据类型范式(Abstract Data Type,ADT)
- 也称Object-Based,OB;
- 类C++风格;
- 将变量和变量相关的函数封装在一起;
- 由public函数向外提供接口;
- 未实现多态的类(基类虚函数+派生类重写虚函数)就是抽象数据类型模型;
2.3 面向对象范式(Object-Oriented,OO)
- 类C++风格;
- 先通过一个抽象类作为接口,统一定义向外提供的服务;
- 然后在派生类中实现该接口;
- 用继承实现了多态的类就是面向对象模型;
3. 构造函数和析构函数
- 主要来自《第二章 Constructors语意》;
3.1 构造函数的初始化顺序
-
初始化顺序如下:
- (1) 构造虚基类,按照被继承的顺序构造;
- (2) 构造非虚基类,按照被继承的顺序构造;
- (3) 构造对象成员变量,按照在类中被声明的顺序构造;
- 如果在声明普通成员变量时赋初值,也相当于构造对象成员变量,也是在这个时候进行赋值的;
- (4) 调用执行类构造函数;
- 先调用初始化参数列表进行构造或者赋值(如果有);
- 再调用类构造函数;
-
参考:
- C++ 构造函数初始化顺序;
3.2 析构函数的析构顺序
-
和构造函数的次序相反:
-
析构顺序如下:
- (1) 调用执行类析构函数;
- (2) 析构对象成员变量,按照在类中被声明的逆序析构;
- (3) 析构非虚基类,按照被继承的逆序析构;
- (4) 析构虚基类,按照被继承的逆序析构;
-
参考:
- C++ 构造函数初始化顺序;
3.3 默认构造函数
- 默认构造函数是由编译器自动生成的无参构造函数;
- 默认构造函数仅在被编译器需要时才会被编译器生成(nontrivial);
- 如果编译器没有用到某个类的默认构造函数,则该函数仅在概念上存在,但实际不会生成(trivial);
- 编译器需要生成默认构造函数的情况有
4
种:- (1) 当前类含一个带无参构造函数(可以显式定义,也可由编译器默认生成)的对象成员变量时,编译器为当前类生成默认构造函数;
- (2) 当前类继承了一个带无参构造函数(可以显式定义,也可由编译器默认生成)的基类时,编译器为当前类生成默认构造函数;
- (3) 当前类声明或继承了一个虚函数时,编译器为当前类生成默认构造函数;
- (4) 当前类继承了一个虚基类或者含虚基类的基类时,编译器为当前类生成默认构造函数;
- 如果当前类带虚函数和继承虚基类,则编译器需要为当前类的对象放入指向虚函数表的指针,因此需要用构造函数来进行;
3.4 拷贝构造函数
-
如果当前类满足按位拷贝语义,除非必要,否则不要显式定义拷贝构造函数;
- 因为默认的拷贝构造函数是按位拷贝的,虽然是浅拷贝,但是效率很高;
- 一个例子如下:
-
当前类不满足按位拷贝语义的情况有
4
种,类似于默认构造函数:- (1) 当前类含一个带非按位拷贝的拷贝构造函数(可以显式定义,也可由编译器默认生成)的对象成员变量时,编译器为当前类生成默认非按位拷贝的拷贝构造函数;
- (2) 当前类继承了一个带非按位拷贝的拷贝构造函数(可以显式定义,也可由编译器默认生成)的基类时,编译器为当前类生成默认非按位拷贝的拷贝构造函数;
- (3) 当前类声明或继承了一个虚函数时,编译器为当前类生成默认非按位拷贝的拷贝构造函数;
- (4) 当前类继承了一个虚基类或者含虚基类的基类时,编译器为当前类生成默认非按位拷贝的拷贝构造函数;
- 如果当前类带虚函数和继承虚基类,则编译器需要为复制的类对象也复制一个指向正确虚函数表的指针;
- 将派生类复制给派生类,则按位拷贝是可行的,因为它们的虚函数表是同一个;
- 将派生类复制给基类,则按位拷贝是不可行的,因为它们的虚函数表不是同一个;
- 因此不能统一用按位拷贝;
- 如果当前类带虚函数和继承虚基类,则编译器需要为复制的类对象也复制一个指向正确虚函数表的指针;
3.5 初始化参数列表
- 必须使用初始化参数列表进行初始化的情况有
4
种:- (1) 初始化一个引用成员变量;
- (2) 初始化一个
const
成员变量; - (3) 调用基类的有参构造函数,因为默认调用无参构造函数;
- (4) 调用对象成员变量的有参构造函数,因为默认调用无参构造函数;
- 即使不是上述4种情况,也推荐使用初始化参数列表进行成员变量初始化;
- 因为在构造函数里面显式赋值,用的是成员变量的赋值运算符重载函数;
- 如果复制构造函数的参数并非是同类型对象,则还要先调用构造函数构造一个对象;
- 而直接用初始化参数列表是直接调用成员变量的构造函数的;
- 一个例子如下:
- 注意:
- 初始化参数列表的赋值顺序是按照成员变量在类中的声明顺序;
- 并不是按照初始化参数列表的先后顺序;
- 所以赋值有依赖关系时,要小心初始化的先后逻辑的问题;
- 建议是用别的类成员变量赋初始化值的初始化不要放在初始化列表中,而是放在构造函数中;
4. 成员函数的调用方式
- 主要来自《第四章 Function语意》;
4.1 非静态成员函数的调用方式
-
设计原则:
- 非静态成员函数的调用效率必须至少和非类成员函数的调用效率相同;
-
也就是说,非静态成员函数在调用时应该是一个非类的普通函数:
- 非静态成员函数在设计上只是将原本放在外面的零散函数集成起来;
- 而且对象模型中实际上是不存储非静态成员函数的指针的,仅保存和多态实现相关虚函数指针,换言之,在对象模型中是找不到非静态成员函数的调用入口,这和非类的函普通函数是一样的;
-
非静态成员函数转换成非类普通函数的过程分成
3
步:- (1) 将
this
指针常量加到函数的形式参数中,如下:
- (2) 为每个非静态成员变量添加
this
指针指向,如下:
- (3) 将该非静态成员函数改写成非类普通函数,并作重命名,如下:
- (1) 将
-
此时,调用该非静态成员函数本质上就和调用非类普通函数一样了,如下:
-
另外,函数重命名的规则虽然不同编译器都不相同,但给出一个gcc规则的例子如下:
- 参考:探查C++ name mangling;
- 可以看到,函数返回类型是不会在重命名的函数名中出现的,所以编译器会判断两个仅返回类型不同的函数是相同的函数而不是重载的函数;
// 原函数
返回类型 类名::函数名(type1 param1, type2, param2) {};
// 重命名为
返回类型 __Z[类名长度][类名][函数名长度][函数名][type1简写][type2简写] {};
4.2 静态成员函数的调用方式
- 静态成员函数本质上就更像是一个非类成员函数了;
- 因为它并不需要在函数内部使用
this
指针; - 因此也无法使用非静态成员变量,和对象模型就更没有关系了;
- 因为它并不需要在函数内部使用
- 静态成员函数转换成非类普通函数的过程仅需
1
步:- (1) 将该非静态成员函数改写成非类普通函数,并作重命名;
- 此时,调用该静态成员函数本质上就和调用非类普通函数一样了;
5. 虚函数表
-
主要来自《第四章 Function语意》;
-
需要特别注意的是,下面增加了gcc环境下的虚函数表实现例子,其中:
- (1) 虚函数表只有一个,相当于是把所有的虚函数表都整合到一个表中,然后通过地址偏移区分不同类的虚函数表;
- 这和《深度探索C++对象模型》中的示意图描述可能会有不同,也就是不会存储多个虚函数表指针;
- (2) 对象模型中先放指向虚函数表的指针,再放非静态成员变量;
- 这和《深度探索C++对象模型》中的示意图描述可能会有不同,也就是不是把指向虚函数表的指针放在非静态成员变量之后;
- (3) 继承多个基类时,在最后一个基类中作了内存紧凑,也就是可以利用最后一个基类的内存对齐空闲空间追加派生类的非静态成员变量;
- 这和《深度探索C++对象模型》中的示意图描述可能会有不同,也就是不是完整拷贝每个基类的实际占用空间(包括内存对齐产生的额外空间)到派生类的对象模型中;
- 这些是设计原理(书)和具体实现(gcc编译器)上不同的体现,但如果可能的话会在下面的介绍中尽量让这两者趋同描述的;
- 参考:
- C++ GCC 对象模型 从汇编代码分析虚函数、动态绑定原理;
- 深入探索C++对象模型之C++对象(vs,gcc,clang测试);
- 怎么理解C++虚函数?fat pointer in GO/Rust vs thin pointer in C++;
- c/c++: c++继承 内存分布 虚表 虚指针 (转);
- C++:虚函数内存布局解析(以 clang 编译器为例);
- Vtable内存布局分析;
- (1) 虚函数表只有一个,相当于是把所有的虚函数表都整合到一个表中,然后通过地址偏移区分不同类的虚函数表;
-
如果是单一继承,则派生类只能有一个虚函数表指针;
-
如果是多重继承,则派生类的虚函数表指针和主基类共用一个,非主基类如果有虚函数表,则会有额外的虚函数表指针(占对象模型空间)作虚函数表地址偏移;
5.0 虚函数表结构
- 这里的虚函数表是指仅单个基类或者仅派生类本身的虚函数表部分,而不是gcc意义上的一整个虚函数表(包含了所有基类和派生类的虚函数表);
- 虚函数表结构描述了一个一般虚函数表所具备的成员,但这些成员不是必须出现的,包括:
virtual call offsets
,仅虚继承含有;virtual base offsets
,仅虚继承含有;offset to top
,均含有;typeinfo pointer
,均含有;virtual function pointers
,均含有;
- 参考虚函数表的官方设计文档:
- https://refspecs.linuxbase.org/cxxabi-1.83.html#vtable;
5.1 单一继承无多态的虚函数表
-
基类未实现虚函数,派生类的继承方式也不是虚继承;
-
基类并没有虚函数表;
-
派生类只有定义了新的虚函数才会有虚函数表,也才会有指向虚函数表的指针;
-
gcc编译的一个派生类无定义虚函数的例子如下:
- 派生类和基类均无虚函数表;
- 关于
BaseA
的大小:base size
:原始数据大小,含a(4) = 4;size
:内存对齐后实际对象模型的总大小,含a(4) = 4;
- 关于
DeriveB
的大小:base size
:原始数据大小,含BaseA(4) + b(4) = 8;size
:内存对齐后实际对象模型的总大小,含BaseA(4) + b(4) = 8;
- gcc编译的一个派生类有定义虚函数的例子如下:
- 基类无虚函数表;
- 派生类有虚函数表,含新定义的虚函数
fun1()
; - 关于
BaseA
的大小:base size
:原始数据大小,含a(4) = 4;size
:内存对齐后实际对象模型的总大小,含a(4) = 4;
- 关于
DeriveB
的大小:base size
:原始数据大小,含vptr(8) + BaseA(4) + b(4) = 16;size
:内存对齐后实际对象模型的总大小,含vptr(8) + BaseA(4) + b(4) = 16;
5.1 单一继承有多态的虚函数表
- 基类实现了虚函数,但派生类的继承方式不是虚继承;
- 则派生类的虚函数表如下:
-
派生类中继承基类的虚函数表规则:
- 如果派生类中有虚函数的重写,则将虚函数表中的指向改为派生类重写的虚函数;
- 如果派生类中没有虚函数的重写,则直接拷贝基类虚函数的指向到虚函数表中;
-
派生类中自己的虚函数表规则:
- 直接将自己新定义的虚函数的指向添加到虚函数表中;
- 注意:
- 无论派生类是否定义了新的虚函数,都会有虚函数表,也会有指向虚函数表的指针,因为起码要指向基类的虚函数表从而使用基类的虚函数以实现多态;
- 单一继承的派生类中仅有一个虚函数表指针,和基类共用一个指针;
-
gcc编译的一个例子如下:
- 关于
BaseA
的大小:base size
:原始数据大小,含vptr(8) + a(4)= 12;size
:内存对齐后实际对象模型的总大小,含vptr(8) + a(8, aligned) = 16;
- 关于
DeriveB
的大小:base size
:原始数据大小,含BaseA(12) + b(4) = 16;size
:内存对齐后实际对象模型的总大小,含BaseA(12) + b(4) = 16;
- 关于
BaseA
的虚函数表:- 包含:
- 偏移值;
BaseA
的类型信息;- 新定义的
fun1()
; - 新定义的
fun2()
;
- 包含:
- 关于
DeriveB
的虚函数表:- 包含:
- 偏移值;
DeriveB
的类型信息;- 派生类重写的
fun1()
; - 基类的
fun2()
; - 派生类新加的
fun3()
;
- 基类的虚函数表指针和子类的虚函数表指针是同一个;
- 如果派生类有新定义的虚函数,则在基类的虚函数后追加即可;
- 包含:
- 关于
5.2 多重继承的虚函数表
- 派生类继承了多个基类,基类可以实现了虚函数也可以没有实现虚函数,但派生类的继承方式不是虚继承;
- 则派生类的虚函数表如下:
-
派生类中继承基类的虚函数表规则:
- 如果派生类中有虚函数的重写,则将虚函数表中的指向改为派生类重写的虚函数;
- 如果派生类中没有虚函数的重写,则直接拷贝基类虚函数的指向到虚函数表中;
-
派生类中自己的虚函数表规则:
- 直接将自己新定义的虚函数的指向添加到主基类的虚函数表中;
- 注意:
- 如果某个基类有虚函数表,则无论派生类是否定义了新的虚函数,都会有虚函数表,也会有指向虚函数表的指针,因为起码要指向基类的虚函数表从而使用基类的虚函数以实现多态;
- 多重继承的派生类的虚函数表指针和主基类共用一个,非主基类如果有虚函数表,则会有额外的虚函数表指针(占对象模型空间)作虚函数表地址偏移;
-
gcc编译的一个例子如下:
- 关于
DeriveC
的大小:base size
:原始数据大小,含BaseA(16) + BaseB(12) + c(4) = 32;size
:内存对齐后实际对象模型的总大小,含BaseA(16) + BaseB(12) + c(4) = 32;- 也就是说基类和基类之间需要用size完整拷贝(包括对齐的额外空间),但在最后一个基类后如果还有size是空闲的话可以继续追加派生类的非静态成员变量,不用浪费这些空间,这也是为了多态的使用作准备的,因为基类指针需要裁切出纯粹的基类对象数据,不能和别的数据混在一起;
- 关于
DeriveC
的虚函数表:- 包含:
- 主基类和派生类自己的虚函数表:
- 距离派生类自己的对象模型起始地址的偏移值;
DeriveC
的类型信息;- 派生类重写的
fun1()
; - 派生类重写的
fun3()
; - 派生类新加的
fun4()
;
- 非主基类的虚函数表:
- 距离派生类自己的对象模型起始地址的偏移值;
DeriveC
的类型信息;- 非主基类的
fun2()
; - 派生类重写的
fun3()
;
- 主基类和派生类自己的虚函数表:
- 包含:
- 关于
5.3 虚继承的虚函数表
- 派生类单一虚继承了基类;
- 一个例子如下:
- 则派生类的虚函数表如下(图中有些错误):
Virtual Table Point3d
中,- 应该没有是指向未重写的
Point2d::mumble()
虚函数的虚函数指针的;
- 应该没有是指向未重写的
Virtual Table Point2d subobject for Point3d
中,- 应该是指向无重写的
Point2d::mumble()
虚函数;
- 应该是指向无重写的
-
派生类中自己的虚函数表规则:
- 直接将自己新定义的虚函数的指向添加到虚函数表中;
- 注意:如果派生类没有新定义虚函数,则还是会有自己的虚函数表;
- 因为这里的虚函数表需要存放指向虚基类的指针偏移,不然找不到虚基类的入口;
-
派生类中继承基类的虚函数表规则:
- 如果派生类中有虚函数的重写,则将虚函数表中的指向改为派生类重写的虚函数;
- 如果派生类中没有虚函数的重写,则直接拷贝基类虚函数的指向到虚函数表中;
-
gcc编译的一个例子如下:
- 关于
DeriveB
的大小:base size
:仅派生类原始数据大小,含vptr(8) + b(4) + c(4) = 16;size
:内存对齐后实际对象模型的总大小,含vptr(8) + b(4) + c(4) + BaseA(16, aligned) = 32;- 也就是说虚基类的完整对象模型是最后才拷贝的,而且统计base size的时候不算上;
- 关于
DeriveB
的虚函数表:- 包含:
- 派生类自己的虚函数表:
- 在对象模型中,距离虚基类的起始地址(也就是高地址)的偏移值,即
virtual_base_offsets
; - 在对象模型中,距离派生类自己的对象模型的起始地址(也就是0)的偏移值,即
offset_to_top
; DeriveB
的类型信息;- 派生类重写的
fun1()
; - 派生类新加的
fun3()
;
- 在对象模型中,距离虚基类的起始地址(也就是高地址)的偏移值,即
- 虚基类的虚函数表:
- 在对象模型中,距离虚基类的起始地址(也就是高地址)的偏移值,即
virtual_base_offsets
; - 在虚函数表中,虚基类被派生类重写的函数的地址偏移量,即
virtual_call_offsets
; - 在对象模型中,距离派生类自己的对象模型的起始地址(也就是0)的偏移值,即
offset_to_top
; DeriveB
的类型信息;- 派生类重写的
fun1()
; - 虚基类的
fun2()
;
- 在对象模型中,距离虚基类的起始地址(也就是高地址)的偏移值,即
- 派生类自己的虚函数表:
- 包含:
- 关于
6. 两种机制
6.1 RAII机制
-
资源获取即初始化(Resource Acquisition Is Initialization)机制;
-
作用:
- 利用C++的构造函数和析构函数,来管理资源和避免资源泄露;
- 在对象构造时获取对应资源;
- 在对象生命周期内控制对资源的访问;
- 在对象析构时释放构造时获取的资源;
- 利用C++的构造函数和析构函数,来管理资源和避免资源泄露;
-
使用:
- (1) 封装一个类;
- 将类涉及到的资源封装在类中;
- 在构造函数中获取资源;
- 在生命周期内,通过类成员函数操作资源;
- 在析构函数中释放资源;
- (2) 用这个类定义一个局部对象(注意不是指针),以控制资源;
- 一旦超过了局部对象的作用域,局部对象会自动调用析构函数,释放资源;
- 资源的获取和释放均不需要手动管理;
- (1) 封装一个类;
-
参考:
- C++中的RAII机制;
6.2 RTTI机制
- 运行时类型信息(Runtime Type Information)机制;
- 作用:
- 提供在运行时确定对象类型的方法;
- 如果类不含虚函数,则在编译期即需要确定对象的类型信息;
- 如果类含虚函数,则对象的类型可以在运行期才确定;
- 因此RTTI机制必须是带虚函数的类型才能实现,否则均在编译期就需要确定类型;
- 提供在运行时确定对象类型的方法;
6.2.1 typeid函数
- 使用:
- 使用
typeid()
可以获取对象的类型信息; - 使用如下:
- 使用
#include <typeinfo>
// 获取对象类型信息
// 返回类型信息字符数组
inline const char* typeid(对象).name() const;
// 判断两个对象类型是否相等
assert(typeid(对象1) == typeid(对象2));
6.2.2 dynamic_cast函数
-
使用:
- 使用
dynamic_cast()
可以将指向派生类的基类指针转换为派生类指针; dynamic_cast()
利用的是RTTI机制,即仅当基类指针确实是指向一个派生类对象时,才提供安全的转换,否则不予转换而返回空指针NULL;- 由于RTTI机制只有含指向虚函数表指针的对象才有,所以
dynamic_cast()
转换的指针指向的对象也必须是含指向虚函数表指针的对象,也就是实现了多态的派生类;
- 使用
-
参考:
- C++中的RTTI机制;
二、C++语言特性
1. 访问控制
- 参考:
- C++ 类访问控制;
- 【C++深度解析】31、三种继承方式(public,protected,private);
1.1 三种类成员访问控制权限
- 如下:
private
:仅类内和友元函数可以访问;protected
:增加派生类可以访问;public
:增加实例化对象可以访问;
1.2 三种访问控制权限的继承
- 如下:
public
继承:维持父类设定的访问权限;protected
继承:最低权限都是protected
;private
继承:最低权限都是private
;
2. 重载、重写和重定义
- 参考:
- Tanky Woo的前博客;
- c++中的函数重载、函数重写、函数重定义 ;
3.1 重载overload
-
作用:
- 重载多个函数供不同的参数调用;
- 静态绑定(编译期绑定);
-
使用条件:
- 在同一个作用域中(相同类中,或者均是全局函数);
- 函数的名字相同;
- 函数的参数不同;
- 函数的返回值可以相同也可以不同;
- 因为C++忽略了函数返回值作为函数标识;
- virtual关键字可有可无;
3.2 重写override
-
作用:
- 派生类重新定义基类的虚函数,以实现多态;
- 动态绑定(运行期绑定);
- 基类指针可以调用派生类重定义的函数;
-
使用条件:
- 不在同一个作用域中(分别在基类和派生类);
- 函数的名字相同;
- 函数的参数相同;
- 函数的返回值相同;
- 基类虚函数不能有static;
- 因为虚函数依赖于对象的虚函数表实现,一定要通过对象来调用;
- 但静态函数不能通过对象来调用;
- 访问修饰符可以相同也可以不同;
3.3 重定义
-
作用:
- 派生类隐藏基类定义的函数,仅保留自己的同名函数;
- 静态绑定(编译期绑定);
- 派生类中的基类同名函数,不满足重写的条件就一定是重定义;
- 基类同名函数仍然存在于派生类空间中,但是被隐藏,需要用
基类::
来访问; - 基类指针不能调用派生类重定义的函数;
-
使用条件:
- 不在同一个作用域中(分别在基类和派生类);
- 函数的名字相同;
- 函数的参数:
- 可以相同,且基类不是虚函数;
- 可以不同,无论基类是不是虚函数;
- 返回值可以相同也可以不同;
3. 继承方式
3.1 单一继承
- 派生类仅继承一个基类,拥有基类的全部非
private
成员变量和函数; - 使用:
class B: public A {
// 构造函数
B(形参列表): A(实参列表) {
//其他操作
}
}
3.2 多重继承
- 派生类同时继承多个基类;
- 使用:
- 直接在继承的位置声明继承多个类即可;
- 构造函数的初始化参数列表可以直接调用基类的构造函数;
- 但如果继承的多个基类中含相同的基类,在当前类中就会出现两次该相同基类的变量和函数,极大浪费空间,需要用虚继承解决;
class D: public A, private B, protected C{
// 构造函数
D(形参列表): A(实参列表), B(实参列表), C(实参列表){
//其他操作
}
}
- 参考:http://c.biancheng.net/view/2277.html;
3.3 虚继承
- 作用:
- 解决多重继承中关于继承的多个基类又继承了某个相同的基类而导致的空间浪费问题;
- 一个例子如下,
ios
类被iostream
类继承的两个基类同时包含,iostream
类中就有会多个ios
类实体:
- 使用:
- 在包含相同基类的基类中声明虚拟继承该基类,则往后的派生类进行多重继承时就不会包含多个该相同基类;
- 被虚继承的基类中最好不要有任何的成员变量,也不要实现任何成员函数,仅包含纯虚函数(抽象类),只作接口用;
class ios;
class ostream:public virtual ios;
class istream:public virtual ios;
class iostream:public ostream,public istream;
- 参考:关于C++中的虚拟继承的一些总结;
4. C++的显式强制类型转换
- 当然可以直接用
(转换后类型)(转换前变量)
的方式做强制类型转换,例如int()
,double()
,(unsigned int)()
等; - 但这种方式有三个不足:
- 在调试的时候没有办法快速帮助定位这些强制类型转换,毕竟它的写法和变量的类型定义一模一样,很难通过查找代码的方式快速找到哪些地方用了强制类型转换,之所以需要查找是因为有不少的Bug会出现在强制类型转换的数据丢失上;
- 在形式上很难区分当前的类型转换的意图;
- 不能检查强制类型转换的安全性;
- 因此,C++提供了另外的四种强制类型转换符;
4.1 static_cast函数
-
使用:
static_cast<转换后类型>(转换前变量)
;
-
作用:
- 用于基本数据类型之间的转换;
- 可以用于:
- 用于调用类重载的强制类型转换运算符;
- 可以用于
void*
转其他类型指针,这个转换是安全的,但不能进行其他类型的指针转换(用reinterpret_cast); - 可以用于左值引用和右值引用的转换,这个转换是安全的;
- 不能用于:
- 不能进行指针类型和普通类型之间的转换(用reinterpret_cast);
- 不能去除常量属性(用const_cast);
- 不能进行基类和派生类指针之间的转换(用dynamic_cast);
- 总的来说,它负责一些安全性高的转换,使用的优先级最高,但它并不提供检查来确保转换的安全性;
4.2 dynamic_cast函数
-
使用:
dynamic_cast<转换后类型>(转换前变量)
;
-
作用:
- 主要用于多态基类指针或者引用转换为派生类指针或者引用(下行转换),且提供安全性检查:如果基类指针确实是指向了要转换的派生类对象,则转换成功,否则,返回空指针;
- 也可以用于其他类型指针转
void*
指针; - 在派生类指针或者引用转换成基类指针或者引用(上行转换),dynamic_cast的效果和static_cast一致,因为转换的安全性比较高;
- 也就是说,允许基类指针指向派生类对象,因为所有功能派生类都有,但不允许派生类指针指向基类对象,因为某些功能基类并没有;
4.3 const_cast函数
-
使用:
const_cast<转换后类型>(转换前变量)
;
-
作用:
- 仅用于带常量属性的指针或者引用向非常量属性指针或者引用的转换,常量属性包括
const
、volatile
和__unaligned
; - 尽量不要使用,因为会破环
const
;
- 仅用于带常量属性的指针或者引用向非常量属性指针或者引用的转换,常量属性包括
4.4 reinterpret_cast函数
- 使用:
reinterpret_cast<转换后类型>(转换前变量)
;
- 作用:
- 可以用于不同类型指针之间的转换;
- 可以用于不同类型引用之间的转换;
- 主要用于指针和能容纳指针的整数类型之间的转换;
- 底层执行的是逐比特的复制操作;
- 拥有最强的转换灵活性,其他类型不能转的它都能转,但是不提供转换的安全性,使用的优先级最低;
5. 模板、特化模板和普通类型
- 参考:
- C++普通函数与模板函数以及特化函数重载的优先级问题;
- (P68)模板一:模板,函数模板,函数模板特化,重载函数模板,非模板函数重载;
- Template的基本语法;
- 深入理解特化与偏特化;
5.1 函数模板
5.1.1 函数模板定义
template <typename 模板形参1, typename 模板形参2, ...>
返回类型 函数名(函数形参1, 函数形参2, ...)
{
// 函数体
}
- 匹配函数模板时:
- (1) 可以通过函数实参列表来自动匹配;
- (2) 也可以通过指定模板实参来匹配;
- 一个例子如下:
template <typename T>
T add(T a, T b)
{
return a + b;
}
int a = 3, b = 5;
int result1 = add(a, b); // 通过自动推导匹配函数模板
int result2 = add<int>(a, b); // 通过指定模板实参匹配函数模板
5.1.2 函数模板重载
- 函数模板的重载本质上还是函数的重载:
- 每个重载的函数均具有独立且平等的地位;
- 函数模板重载有两种方式:
- (1) 普通函数重载;
- 不使用
template<>
的同名函数;
- 不使用
- (2) 函数模板重载;
- 使用
template<>
的同名函数;
- 使用
- (1) 普通函数重载;
// 函数模板重载
template <typename T, typename U>
void add(T a, U b) {}
// 函数模板重载
template <typename T, typename U, typename V>
void add(T a, U b, V c) {}
// 函数模板重载
template <typename T>
void add(T a, int b) {};
// 函数模板重载
template <>
void add(int a, int b) {};
// 普通函数重载,和上面的函数模板可以共存
void add(int a, int b) {};
// 普通函数重载
void add(int a, int b, int c) {};
- 函数模板重载的匹配规则:
- (1) 如果能够完全匹配,则优先考虑普通函数重载的版本;
- a) 完全匹配:可以通过无关紧要的转换匹配上的普通函数重载,如:
- 引用和对象名之间的转换;
- 数组名和数组指针之间的转换;
- 函数名和函数指针之间的转换;
const
和非const
之间的转换;
- a) 完全匹配:可以通过无关紧要的转换匹配上的普通函数重载,如:
- 以下补充另外两种匹配转换,如果没有合适的函数模板重载,则会尝试用下面的方法进行普通函数重载版本的类型转换匹配;
- b) 提升匹配:可以通过提升类型的转换匹配上的普通函数重载,如:
- 短整型转换为长整型;
- c) 标准匹配:需要通过标准转换函数来匹配上的普通函数重载,如:
int
转char
;long
转double
;
- b) 提升匹配:可以通过提升类型的转换匹配上的普通函数重载,如:
- (2) 如果使用函数模板重载的版本可以产生一个更好的匹配,则使用该版本;
- 但如果有两个及以上的函数模板重载版本都能匹配上,且无法比较哪个匹配更好,则会编译出错;
- (3) 如果调用时使用了空模板实参
<>
,则只会匹配函数模板重载版本;
- (1) 如果能够完全匹配,则优先考虑普通函数重载的版本;
5.2 类模板
5.2.1 类模板定义
template <typename 模板形参1, typename 模板形参2, ...>
class 类名
{
// 类内成员变量
// 类内成员函数
}
- 匹配类模板时:
- (1) 必须通过指定模板实参来匹配;
5.2.2 类模板偏特化
-
类模板偏特化是对类模板原型的特殊化:
- 首先需要符合类模板原型的限制,而后再在它的基础上做进一步的限制;
-
类模板偏特化仅有一种方式:
- (1) 在类名后增加
<>
声明在类模板原型中偏特化的形参;<>
中的形参个数取决于类模板原型中的形参个数,必须相同;
- (1) 在类名后增加
// 类模板原型,类名后无需增加<>
template <typename T, typename U>
class DoWork {};
// 类模板偏特化,T类型偏特化为int类型
template <typename U>
class DoWork<int, U> {};
// 类模板偏特化,T类型偏特化为T*类型(指针类型)
template <typename T, typename U>
class DoWork<T*, U> {};
// 类模板偏特化,T类型偏特化为int类型,U类型偏特化为char类型
template <>
class DoWork<int, char> {};
- 类模板偏特化的匹配规则:
- 根据模板实参的类型进行匹配;
- (1) 如果有偏特化的版本可以匹配,则使用偏特化的版本;
- 如果有多个匹配的偏特化版本,则编译报错;
- (2) 如果没有偏特化的版本可以匹配,则使用类模板原型;
5.3 模板形参
5.3.1 类型形参
- 就是普通的模板形参:
- 由
typename
或者class
关键字声明;
- 由
- 实参是一个类型;
- 使用:
- 函数模板和类模板都可以使用;
- 相当于是传入了一个类型;
- 一个例子如下:
// 参数T就是类型形参
template<typename T>
5.3.2 非类型形参
-
不由
typename
、class
或者template
定义的模板形参,只能是:- (1) 整型类型,如int,char,short,long;
- (2) 指针或者左值引用;
-
实参必须是编译时常量表达式,包括:
- (1) 字面量(不包括字符串);
- (2)
const
修饰的变量; - (3)
sizeof()
的返回值;
-
使用:
- 必须显式用
<>
指定非类型形参的实参,且实参必须是编译时常量; - 函数模板和类模板都可以使用;
- 但函数模板不能用自动匹配的方式调用,必须显式指定模板实参;
- 相当于是传入了一个常量;
- 必须显式用
-
一个例子如下:
// 参数N、DR和CCP就是非类型形参
template<int N>
template<double &DR>
template<const char* CCP>
5.3.3 模板形参
-
由
template<>
类型定义的形参; -
实参是一个类模板;
-
使用:
- 函数模板和类模板都可以使用;
- 相当于是传入了一个模板类型;
-
一个例子如下:
template<typename T, int N>
class A {}
// 参数V就是模板形参
template<template<typename, int> class V>
class B {
// 实例化模板形参
V<int, 5> ...
}
// 用模板类A作为传入的模板实参
B<A> ...
5.4 模板实例化
- 实例化是指根据函数模板或者类模板,实例出可调用的函数或者类类型;
- 和函数模板重载以及类模板特化的区别:
- (1) 函数模板重载和类模板特化仍然是类型上的定义;
- 函数体或者类定义仍需要程序员手动实现;
- 可以针对不同的类型定制实现不同的函数体或者类定义;
- (2) 实例化是编译器根据已有的函数模板和类模板进行生成和调用;
- 函数体和类定义均由编译器自动推导生成;
- 不能再增加新的函数体或者类定义,只能匹配已有的函数模板或者类模板;
- (1) 函数模板重载和类模板特化仍然是类型上的定义;
6. 变量作用域
- 参考:
- https://www.runoob.com/cplusplus/cpp-variable-scope.html;
6.1 全局作用域
- 在函数和代码块之外声明的变量具有全局作用域;
- 可以被程序中的任何函数访问;
- 在进程开始时创建,在进程结束时销毁;
6.2 局部作用域
- 在函数内部声明的变量具有局部作用域;
- 只能在函数内部访问;
- 在函数调用时创建,在函数执行完后销毁;
6.3 块作用域
- 在代码块内部声明的变量具有块作用域;
- 比局部作用域的范围(也就是函数之内)更小;
- 只能在代码块内部访问;
- 在代码块执行时创建,在代码块执行完后销毁;
6.4 类作用域
- 在类内部声明的非静态成员变量具有类作用域;
- 可以被类的所有成员函数访问;
- 还可以通过降低访问控制方式,如设置为
protected
和public
,然后被类外的函数访问;
- 还可以通过降低访问控制方式,如设置为
- 在类对象创建时创建,在类对象销毁时销毁;
- 另外,类的静态成员变量是在进程开始时创建,在进程结束时销毁;
6.5 与变量的对应关系
- 局部变量:对应局部作用域或者块作用域;
- 全局变量:对应全局作用域;
- 类成员变量:
- 静态成员变量:对应全局作用域;
- 非静态成员变量:对应类作用域;
7. 外部变量、全局变量和静态变量
- 这三种变量的共同点是:
- 生命周期都贯穿当前程序文件的运行周期;
- 都放在虚拟地址空间中的程序代码和数据空间;
- 都会自动初始化为类型的默认初始值,无需手动初始化;
- 都放在可重定位目标文件的 .data或者.bss 节中;
7.1 外部变量
- 外部变量是指在函数外部定义的变量,在其作用域范围中,任意函数可以使用该外部变量;
- 外部变量的默认作用域为:外部变量定义处到当前程序文件结束处;
- 可以通过增加外部变量的显式声明来拓展外部变量的作用域:
- 显式声明的形式是:
extern 变量类型 外部变量名;
; - 显式声明并不会为外部变量分配内存空间;
- 经过显式声明后,外部变量的作用域拓宽至:显式声明处到本程序文件结束处;
- 有两种情况需要拓展作用域:
- a)在当前函数中使用在当前函数之后定义的外部变量,需要在当前函数之前增加外部变量的显式说明;
- b) 在当前程序文件中使用在别的程序文件中定义的外部变量,需要在当前程序文件中增加外部变量的显式说明;
- 显式声明的形式是:
7.2 全局变量
- 全局变量实际上就是指外部变量;
- 它这里的【全局】是很具有迷惑性的,因为实际上,它的全局仅指当前定义处到本程序文档结束处,而非整个程序文档(本地全局)或者整个项目(跨文档全局);
- 更具有迷惑性的是,通过增加
extern
声明,确实是可以将全局变量的作用域拓展到整个程序文档甚至是整个项目; - 事实上,未经过作用域拓展的全局变量仅是一个勉勉强强的本地全局变量而非跨文档全局变量;
7.3 静态变量
- 静态变量有两种:静态全局变量和静态局部变量;
- 静态全局变量:
- 实际上是加了
static
的全局变量; - 它的作用域仍然是全局变量的默认作用域,也就是当前定义处到本程序文档结束处;
- 但它禁止了
extern
显式声明的使用,也就是说全局静态变量的作用域无法拓展了,无论是在本程序文件中拓展还是跨文件拓展都不行;
- 实际上是加了
- 静态局部变量:
- 实际上是加了
static
的局部变量; - 它的作用域仍然是局部变量的作用域,也就是当前定义处到本函数结束处;
- 由于本来就是局部变量,所以它的作用域是无法拓展的;
- 但它的生命周期贯穿整个程序的运行周期,不会随函数的执行完成而消亡,这点和全局变量相同,虽然只能在函数中使用,但每次调用该函数时,都共用同一个局部静态变量;
- 实际上是加了
7.4 变量初始化时机
- (1) 非静态变量:
- 非静态全局变量:在main函数执行前初始化,放在
.bss
段;- 如果无需调用函数(包括构造函数)进行初始化,也不是初始化为0值,则在程序加载时即初始化,放在
.data
段;
- 如果无需调用函数(包括构造函数)进行初始化,也不是初始化为0值,则在程序加载时即初始化,放在
- 非静态全局变量:在main函数执行前初始化,放在
- (2) 静态变量:
- 静态全局变量:在main函数执行前初始化,放在
.bss
段;- 如果无需调用函数(包括构造函数)进行初始化,也不是初始化为0值,则在程序加载时即初始化,放在
.data
段;
- 如果无需调用函数(包括构造函数)进行初始化,也不是初始化为0值,则在程序加载时即初始化,放在
- 静态成员变量:在main函数执行前初始化,放在
.bss
段;- 如果无需调用函数(包括构造函数)进行初始化,也不是初始化为0值,则在程序加载时即初始化,放在
.data
段;
- 如果无需调用函数(包括构造函数)进行初始化,也不是初始化为0值,则在程序加载时即初始化,放在
- 静态局部变量:在第一次调用时初始化,放在
.bss
段;
- 静态全局变量:在main函数执行前初始化,放在
8. C++标准输入输出流
- 大部分内容参考ChatGPT-3.5;
- 标准输入输出流的继承关系如下:
8.1 std::cout对象
std::cout
是C++标准输出流std::ostream
的全局对象;- 生命周期伴随整个进程的生命周期;
- 通过重载
operator<<
运算符来输出变量;- 重载了不同类型的参数;
- 返回类型是
ostream&
,可以作为左值继续往下调用<<
,以实现链式编程风格的流操作;
namespace std
{
extern ostream cout; // 声明全局变量 cout
class ostream : virtual public ios
{
public:
// 拷贝构造函数和赋值操作符省略
ostream& operator<<(bool);
ostream& operator<<(short);
ostream& operator<<(unsigned short);
ostream& operator<<(int);
ostream& operator<<(unsigned int);
ostream& operator<<(long);
ostream& operator<<(unsigned long);
ostream& operator<<(float);
ostream& operator<<(double);
ostream& operator<<(long double);
ostream& operator<<(void*);
ostream& operator<<(streambuf*);
// 调用std::endl和std::flush等实际上是调用下面的重载函数
ostream& operator<<(ostream& (*pf)(ostream&)) {
return (*pf)(*this); // 调用指定的流操作函数并返回流对象的引用
}
ostream& put(char);
ostream& write(const char *, int);
ostream& flush();
// ...
};
static ios_base::Init __ioinit; // 静态初始化器
ostream cout(0); // 初始化全局变量 cout
}
8.2 std::cin对象
std::cin
是C++标准输入流std::istream
的全局对象;- 生命周期伴随整个进程的生命周期;
- 通过重载
operator>>
运算符来输出变量;- 重载了不同类型的参数;
- 参数类型是引用,因为输入的数据要写入到参数中;
- 返回类型是
istream&
,可以作为左值继续往下调用>>
,以实现链式编程风格的流操作;
- 重载了不同类型的参数;
namespace std
{
extern istream cin;
class istream : virtual public basic_istream<char>
{
public:
// 构造函数和析构函数
explicit istream(streambuf* sb);
virtual ~istream();
// 流提取器函数,主要包括以下几种
istream& operator>>(short& val);
istream& operator>>(unsigned short& val);
istream& operator>>(int& val);
istream& operator>>(unsigned int& val);
istream& operator>>(long& val);
istream& operator>>(unsigned long& val);
istream& operator>>(long long& val);
istream& operator>>(unsigned long long& val);
istream& operator>>(float& val);
istream& operator>>(double& val);
istream& operator>>(long double& val);
istream& operator>>(void*& val);
istream& operator>>(std::streambuf* sb);
istream& operator>>(std::string& str);
istream& operator>>(char* str);
istream& operator>>(char& ch);
// get() 函数
int get();
// getline() 函数
istream& getline(char* str, std::streamsize count);
istream& getline(char* str, std::streamsize count, char delim);
// ignore() 函数和 peek() 函数
istream& ignore(std::streamsize count = 1, int delim = EOF);
int peek();
// unget() 函数
istream& unget();
// ...
}
}
8.3 std::endl函数
std::endl
本质上是一个函数名或者函数指针;- 作用:
- 输出换行符并刷新输出缓冲区;
- 如果是输出到文件的话,则会强制写入到磁盘;
- 频繁调用会忽略输出缓冲区的缓冲作用,大幅降低写性能;
- 但在输出末尾应当使用,以清空缓冲区;
std::cout << std::endl;
// 等价于
std::cout << '\n' << std::flush;
9. const和static关键字的使用
9.1 const关键字
-
(1) 修饰变量:
- 声明该变量为常量,值不可被修改;
-
(2) 修饰指针:
- 指针常量:指针本身是常量,指向不可修改;
- 常量指针:指向常量的指针,指向的空间不可修改;
-
(3) 在普通函数中使用:
- 修饰形参:参数变量在函数中不可修改;
- 修饰返回值:返回值需要用const类型变量来接,接完后不可修改;
-
(4) 在类中使用:
- 修饰成员变量:
- 只能在声明时或者构造函数中赋值;
- 一旦赋值则不可修改;
- 修饰成员函数:
- 当前函数不能修改对象的成员变量;
- 修饰成员变量:
// 修饰变量
const int a;
// 指针常量
int * const ptr;
// 常量指针
const int *ptr;
int const *ptr;
// 修饰形参
void print(const int a)
// 修饰返回值
const int get()
class Obj {
private:
// 修饰成员变量
const int a = 5;
public:
// 修饰成员函数
int get() const {
return a;
}
};
9.2 static关键字
-
(1) 修饰变量:
- 修饰全局变量:
- 该全局变量不可用
extern
扩展作用域,仅本文件可见;
- 该全局变量不可用
- 修饰局部变量:
- 将局部变量放到.bss段或者.data段,延长生命周期直到进程结束;
- 修饰全局变量:
-
(2) 修饰普通函数:
- 该普通函数不可用
extern
扩展作用域,仅本文件可见;
- 该普通函数不可用
-
(3) 在类中使用:
- 修饰成员变量:
- 可以不通过对象调用,所有对象共享该变量;
- 该变量放到.bss段或者.data段,延长生命周期直到进程结束;
- 需要额外在类外初始化以分配空间;
- 修饰成员函数:
- 可以不通过对象调用;
- 只能访问静态成员变量,不能访问非静态成员变量;
- 修饰成员变量:
// 修饰全局变量
static int a;
// 修饰函数
static void get() { return 3; }
class Obj {
private:
// 修饰成员变量
static int a;
public:
// 修饰成员函数
static int get() {
return a;
}
};
int Obj::a = 5; // 类外初始化
10. 指针偏移量
10.1 数组名
- 假设有
int a[5]
,则:*(a + 1)
:偏移一个int
值,即a[1];*(&a[0] + 1)
:偏移一个int
值,即a[1];*(&a + 1)
:偏移一整个数组大小;
a
部分等价于int * const
,但包含数组大小信息;&a[0]
完全等价于int * const
;&a
等价于int (*)[5]
;
10.2 一级指针
- 假设有
int *a
,则:*(a + 1)
:偏移一个int
值;*(&a + 1)
:偏移一个int*
值,即指针本身大小;
10.3 二级指针
- 假设有
int **a
,则:*(a + 1)
:偏移一个int*
值,即一级指针本身大小;
10.4 小结
- (1) 数组名本质上可以看作是一个带了数组信息的指针常量;
- 它可以被赋值给别的指针,但不能被修改指向;
- (2) 但数组名不完全等价于指针常量,差别在于:
- 取地址的偏移量是一整个数组的大小,而不是指针本身大小;
- sizeof的值是一整个数组的大小,而不是指针本身的大小;
- (3) 指针偏移量和大小:
- 指针的大小就是它本身的大小,取决于CPU字长;
- 指针的偏移量不是本身的大小,而是它指向空间的大小;
- 取指针地址的偏移量是它本身的大小,取决于CPU字长;