文章目录
0. 命名空间
- c标准库可以认为是在匿名空间中
- 匿名的命名空间中的实体不能跨模块使用
- 匿名命名空间中的实体和全局变量同名时,会出现两种情况:
- 使用
::entryName
, 只能访问到全局变量。 - 使用
entryName
, 产生二义性。 - 有名命名空间可以嵌套使用、可以进行追加、可以跨模块使用
1. const限定符
1.1 宏定义和const的区别
宏定义:
- 发生在 预处理阶段(发生时机)
- 宏定义常量是 简单的文本替换,存储在代码段
const
- 在 编译阶段 由编译器 进行类型检查
- const常量需要进行内存分配
1.2 常量指针 v.s. 指针常量
- 常量指针 即 指向常量的指针;(不能通过该指针间接修改其所指向对象的值)
- 指针常量 是指 指针变量本身是一个常量。(指针的指向是固定的)
1.3 数组指针 v.s. 指针数组
- 数组指针 即 指向数组的指针;(该指针的偏移是以该数组类型的长度大小为单位的)
- 指针数组 即 元素类型为指针的数组;
1.4 函数指针 v.s. 指针函数
-
函数指针 即 指向函数的指针; (函数指针可以看作是一个函数对象)
普通函数的函数名
funcName
即 函数的入口地址,通过funcName
和&funcName
都可以进行调用;静态成员函数可以通过
MyClass::funcName
或&MyClass::funcName
进行调用;成员函数的函数类型为
ReturnType MyClass::funcName (...);
, 定义对应的成员函数指针类型为ReturnType (MyClass::*FuncPtr) (...);
。 -
指针函数 即 返回值类型是指针的函数;(返回值不能是一个局部变量的地址)
2. new & delete
2.1 形态
new… delete…
- int *p = new int;
- 没有进行初始化
- int *p = new int();
- 默认初始化
- 此处默认初始化为0
new…[] delete[]…
2.2 new & malloc区别
- new 是c++的表达式或操作符;malloc是c的库函数(stdlib.h)
- new 返回是一个对应申请类型的指针;malloc返回的是
void*
指针 - new 在申请时只需指定要申请的类型或者申请元素的个数;malloc需要指定分配内存空间大小的字节数
- new 在申请空间时可以进行初始化;malloc不能在申请时进行初始化
2.3 new的底层实现
- 通过
operator new
函数申请对应大小的未初始化的内存空间 - 调用对应的构造函数进行初始化(如果有的话)
- 返回一个对应申请类型的指针
2.4 delete的底层实现
- 调用对应类型的析构函数(如果有的话)
- 调用
operator delete
函数释放所申请的对应大小的内存空间
2.5 operator new 的底层实现
- malloc
- malloc失败则抛出崩溃异常
2.6 operator delete 的底层实现
- 检查机制
- free
3. 引用
- 引用底层实现还是一个指针常量,只不过编译器限制获取其地址。因此,引用占据的空间大小仍然是一个指针大小。
3.1 用途
-
引用作为函数参数
可以实现传入传出
-
引用作为函数的返回值
- 不能返回局部变量的引用
- 不能轻易返回一个堆空间变量的引用,除非有了相应的内存回收机制
3.2 引用 v.s. 指针
-
同
- 引用和指针占据内存空间都是4或8字节
- 指针常量和引用都必须初始化
-
异
- 指针允许寻址,引用不允许寻址
- 指针常量和引用都必须初始化,但指针常量可以为空,引用不可以
- 数组元素的类型可以是指针,但不能是引用
sizeof
运算符 对于引用是获得其所绑定对象的大小
4. 类型转换
4.1 static_cast
一般用于
void*
和其他类型指针之间的转换
4.2 dynamic_cast
运行时类型转换,通常用于基类和派生类之间的 向下转型
-
若 基类指针指向派生类对象,将该基类指针强制转换为派生类指针的操作是安全的
-
若 基类指针指向基类对象,那么将该基类指针转换为派生类指针是不安全的
-
编译器默认将基类指针转换为派生类指针的操作是不允许的,那么要实现上面安全的操作,就需要用到
dynamic_cast
,并且基类必须要有虚函数。- 若是基类指针指向派生类对象,则
dynamic_cast
就会使得基类指针转换为派生类指针 - 若是基类指针指向基类对象,则
dynamic_cast
就会返回一个nullptr
- 若是基类指针指向派生类对象,则
4.3 const_cast
去除(底层)const属性,原来的const常量不会发生改变
- 实现底层修改的接口(在非const接口中调用const版本是安全的)
class Test {
public:
Test() {
for(int i = 0; i< 5; ++i) {
_arr[i] = i;
}
}
const int * test() const {
return _arr;
}
int * test() {
return const_cast<int*>(const_cast<const Test *>(this)->test());
}
private:
int _arr[5];
};
- 可以使用 模板、自动推导 替代
class Test {
private:
template<class T>
static auto fun_impl(T * t) {
return t->_arr;
}
public:
Test() {
for(int i = 0; i< 5; ++i) {
_arr[i] = i;
}
}
int * test() {
return fun_impl(this);
}
const int * test() const {
return fun_impl(this);
}
private:
int _arr[5];
};
4.4 reintepret_cast
对于二进制类型重新解释
5. 函数重载
- c不支持函数重载,c++支持,利用的原理是 名字改编
name mangling
- 函数重载是指 当函数名相同时,根据 函数参数类型、参数个数、参数顺序进行改编的;并且函数返回值类型不作为函数重载标志。
- c和c++函数调用方式不同,要是在c++中使用c的函数调用方式,则要使用
extern "C" {...}
6. 默认实参
- 声明时指定,从左到右
- 实现时不能重复出现
- 默认实参的确定是在编译时,但是默认实参的值的确定分情况而定,若默认实参是字面值,则在编译时就以确定其值;若默认实参是函数调用或全局变量,则其值是在运行时确定。
因此对于虚函数中默认参数而言,其默认实参类型的确定是在编译时,因此是静态联编。
7. inline函数
-
是在编译时进行替换,并且会进行参数类型检查,没有函数调用的开销
-
宏函数只是简单的文本替换,发生时机是在预处理
-
不适于使用Inline函数场景
- 当函数体很长时
- 当函数体中含有循环时
-
由于inline函数的发生时机是在编译时,因此不能将其的声明和实现分开放置于不同文件中;通常inline函数位于头文件中
8. bool类型
- c++中的
bool
类型只占1个字节 - c99中的
_Bool
类型就是int
类型,占4个字节
9. try…catch…
- google_cpp编程规范不推荐使用,破坏了代码结构
10. 类 (base)
10.1 类类型大小
- 类类型的大小只与非静态数据成员有关,与静态数据成员、成员函数无关
- 空类大小为1个字节
10.2 数据成员初始化顺序
- 数据成员的初始化顺序只与其在类中声明顺序有关,与其在初始化列表中的顺序无关
10.3 构造函数
-
默认构造函数
- 若定义了有参默认构造函数,则缺省构造函数不会自动合成
-
拷贝构造函数
ClassName(const ClassName &);
- “&” 不能去掉, 否则在进行调用拷贝构造函数时,会递归调用,从而造成栈溢出
- “const” 不能去掉, 否则对于参数是右值的类对象时,非const引用不能进行绑定
- 调用时机:
- 当用一个已存在的类对象初始化一个新构造的类对象时。(值初始化)
- 当函数的参数类型是一个类类型时,将实参传递给形参时。
- 当函数的返回值是一个类类型时,执行return语句时。(若返回的是一个临时类对象,若该类有移动构造函数的话,则会调用移动构造函数)
-
移动构造函数
ClassName(ClassName &&);
10.4 析构函数
- 每个类只能有一个析构函数
- 调用时机: 当类对象被销毁时,会自动调用析构函数
- 栈对象被销毁时(作用域结束时)
- 堆对象被delete时
- 全局类对象销毁时(程序退出时)
- 静态类对象销毁时(程序退出时)
- 析构函数也可以显式调用,但不会使得所对应类对象销毁。即析构函数的调用是类对象销毁的必要条件。
- 由于每个类都有唯一的析构函数,因此编译器在编译时会将其名字改变为
Destructor
。因此对于虚析构函数而言,只有基类将析构函数声明为虚函数,则其派生类的析构函数会自动成为虚函数
10.5 this指针
- 成员函数中的第一个参数是隐含的this指针
- 类的this指针类似于一个指针常量,绑定一个类对象
10.6 深拷贝 、 浅拷贝
- 是针对于 堆空间变量的复制而言的
10.7 特殊的数据成员
- const数据成员
在初始化列表中进行初始化
- 引用数据成员
在初始化列表中进行初始化
- 类数据成员
- 需要在列表初始化中显示初始化,否则默认调用缺省默认构造函数
- 静态数据成员
- 需要在类外进行初始化
- 存储于全局静态区,初始化位于data段,未初始化位于bss段,不占据类对象的存储空间,被所有类对象共享
10.8 特殊的成员函数
- 常成员函数
对
this
指针进行const
修饰 - 静态成员函数
没有
this
指针,因此可以直接访问静态数据成员
10.9 栈、堆 类对象
-
栈类对象
要创建一个栈类对象,则类的构造函数和析构函数必须具有public访问权限
-
堆类对象
new 、 delete
-
只能创建栈对象,不能创建堆对象
将
operator new
、operator delete
函数 私有化 或者 删除 -
只能创建堆对象,不能创建栈对象
将类的 析构函数 私有化
并且定义一个具有public
权限的函数去调用delete
来销毁堆对象
10.10 友元
- 从类的外部访问类的私有成员
- (友元函数)普通函数、成员函数、(友元类)类
- 不能被继承、不具有传递性、具有单向性
10.11 运算符的重载
c++将 运算符 进行 泛化成函数
-
不可重载的运算符
.
.*
::
?::
sizeof
-
必须以成员函数形式重载的运算符
=
[]
()
->
->*
-
规定
- 不能对标准类型进行运算符重载,c++规定重载运算符的操作数必须要有一个自定义类型或枚举类型
- 重载运算符之后,运算符的结合性和优先级不发生改变
- 重载运算符之后,运算符的操作数的个数和顺序不发生改变
- 重载运算符函数不能具有默认实参
- 重载运算符 && || 之后,不再具有 短路求值的特性
- 不能臆造运算符
-
赋值运算符
自复制、回收左操作数空间、深拷贝、
return *this
-
自增(减)运算符
为了区别前置自增和后置自增运算符,c++规定 在后置自增运算符重载函数的形参中增加一个int类型
-
函数调用运算符
重载了函数调用运算符的类对象成为 函数对象
-
函数对象 v.s. 普通函数
相对于普通函数,函数对象携带了状态;不同函数对象具有不同状态
普通函数也可以携带状态,如static局部变量,只不过普通函数携带的状态是全局的
-
函数对象
普通函数、函数指针、成员函数、成员函数指针都可以看成是 广义上的函数对象
-
std::function
- 用来存储函数对象的容器
- 函数对象的调用中的参数的个数是由
std::function
的类模板参数决定的
-
std::bind
- 改变函数调用的形态
std::bind
的函数参数是进行的值传递,为了进行引用传递,需要使用std::ref
引用包装器std::bind
的函数参数个数是由被绑定的函数的参数个数相关的std::bind
的参数中的占位符代表的是实参的位置
-
-
-
下标运算符
key --> value
-
解引用、箭头运算符
智能指针的雏形
class Data { public: int getData() { return 10; } }; class SecondLayer { public: SecondLayer(Data * pData) : _pData(pData) { cout << "SecondLayer::SecondLayer()" << endl; } ~SecondLayer() { cout << "SecondLayer::~SecondLayer()" << endl; if(_pData) { delete _pData; _pData = nullptr; } } Data * operator->() { return _pData; } Data & operator*() { return *_pData; } private: Data *_pData; }; class ThirdLayer { public: ThirdLayer (SecondLayer *pSecLayer) : _pSecLayer(pSecLayer) { } ~ThirdLayer() { if(_pSecLayer) { delete _pSecLayer; _pSecLayer = nullptr; } } SecondLayer & operator->() { return *_pSecLayer; } private: SecondLayer * _pSecLayer; };
- 编译器将
secLayer->->getData()
简化为secLayer->getData()
- 由于编译器的简化操作,
SecondLayer
类似智能指针,因此在ThirdLayer
中重载->
运算符时返回的是SecondLayer
的引用
- 编译器将
-
类类型转换
-
其他类型 => 类类型
通过有参构造函数
TestClass t1 = 1;
先调用有参构造函数创建一个临时对象
调用拷贝构造函数进行复制
若存在移动构造函数,则优先调用移动构造函数TestClass t2(1);
直接调用有参构造函数t3 = 1;
先调用有参构造函数创建一个临时对象
调用赋值运算符函数进行复制
-
类类型 => 其他类型
类型转换成员函数
- 没有返回值、参数
- 函数名为
operator OtherType
- 函数体中return 一个
OtherType
类型
-
10.12 类域
类的作用域 和能否从类外部访问无关
-
全局作用域
-
类作用域
嵌套类
- 类A中定义了一个类B
- 若B的访问权限是private,则只能在A类的内部进行创建类B对象
- 要想在嵌套类中使用外部类的非静态数据成员/成员函数,则需要传递外部类对象的地址给嵌套类
-
块作用域
-
类名作用域
名字(数据成员、成员函数)的隐藏
-
对象作用域
11. 继承
11.1 构造函数
在创建派生类对象时,只会调用派生类的构造函数
- 完成派生类对象所占用空间的开辟
- 对基类部分进行初始化
显式初始化时使用
BaseName(...)
- 对初始化列表中的其他部分进行初始化
- 执行派生类构造函数的函数体
由于需要初始化派生类的基类部分,因此才需要调用基类的构造函数
11.2 析构函数
在调用派生类析构函数时,会自动调用基类的析构函数
- 调用派生类析构函数
- 调用派生类中成员对象的析构函数
- 调用基类的析构函数
11.3 派生类适应于基类
-
向上转换
- 派生类指针 => 基类指针
- 基类引用 绑定派生类对象
- 用派生类对象初始化基类对象
-
向下转换
- 基类指针 => 派生类指针
编译器禁止
- 基类指针指向基类对象(不安全)
- 基类指针指向派生类对象(安全)
- 基类指针 => 派生类指针
11.4 多重继承
- 基类的初始化顺序和继承时声明的顺序一致
- 默认继承的权限是
private
- 会造成二义性
- 函数成员同名的二义性
- 菱形继承中存储二义性
解决方法:虚拟继承
11.5 虚拟继承
-
虚基指针
vbptr
-
虚基表
vbtable
- 第一个表项
虚基指针与其所对应子对象的首地址偏移
- 第二个表项
虚基指针与其所对应的虚基类对象的首地址偏移
- 第一个表项
-
在虚拟继承的存储布局中,虚基类位于最末尾
-
特征:
-
存在
虚基指针、虚基表确实存在
-
间接
通过虚基指针访问虚基表,从而访问其对应的虚基类
-
共享
对于菱形继承而言,只存在一份虚基类的存储空间
-
-
虚拟继承中的构造函数和析构函数
- 若在继承链上存在虚基类,则其初始化是由最底层的子类显示指定完成的;若不显示指定,则编译器会自动调用虚基类的缺省构造函数,若不存在则报错
- 并且不管初始化顺序如何,虚基类的初始化优先于其他普通基类,并且其虚基类的构造函数只会调用一次
11.6 复制控制(基类、派生类间的复制)
拷贝构造函数、赋值运算符函数
- 若派生类中没有复制控制函数,则执行派生类对象的复制时会自动调用基类的复制控制函数
- 若派生类中有复制控制函数,则执行派生类对象的复制时不会自动调用基类的复制控制函数
12. 多态
12.1 静态多态v.s. 动态多态
静态联编
- 发生时机:编译时
- 函数重载、模板、运算符重载
动态联编
- 发生时机:运行时
- 基类指针(基类引用) + 虚函数
12.2 动态多态被激活的条件
- 基类定义一个虚函数
- 派生类中覆盖该虚函数
- 创建一个派生类对象
- 用一个基类指针指向该派生类对象 或 用一个基类引用绑定该派生类对象
- 通过该基类指针/引用 访问 虚函数
12.3 虚函数
-
虚函数的访问权限是由其静态类型决定的
因为访问权限只是用于编译器进行检查,发生在编译时期
-
虚函数可以有默认实参,但若某次函数调用了默认实参,则该默认实参的值是由本次调用的静态类型决定的
因此,若虚函数有默认实参,则基类、派生类中的默认实参最好一致
-
实现原理:
-
虚函数指针
vfptr
-
虚函数表(虚表)
vftable
- 存放虚函数的入口地址以及一些跳转指令
- 存放于只读数据段.rodata
不能修改
-
当一个类中含有虚函数,则创建该类的对象时,其对象的存储空间布局的起始位置会多出一个(只有一个)虚函数指针
vfptr
,指向其所对应的虚表 -
回避虚函数机制
使用 作用域限定符
通常,只在成员函数 或 友元中使用 -
在构造函数中使用虚函数,采用的是静态联编
-
虚析构函数
由于编译器对析构函数的底层函数名进行统一化
Destructor
,因此将基类的析构函数虚化后,派生类的析构函数自动成为虚函数 -
不能设置为虚函数的函数
- 构造函数
还没有创建完一个对象
- 普通函数
- 静态成员函数
- inline函数
当
inline
和virtual
关键字同时出现时,inline
将失效 - 友元函数
- 构造函数
-
特征
-
存在
虚函数指针、虚表是存在的
-
间接
对虚函数的访问是需要通过动态机制激活的
-
共享
基类共享派生类重定义后的虚函数
-
-
12.4 函数重载 v.s. 函数隐藏 v.s. 函数覆盖
-
overload
同一类中,函数名相同,不同参数类型、参数个数、参数顺序
-
overseek
基类和派生类中,相同函数名
-
override
基类和派生类中,虚函数,相同函数原型、相同函数名
- 发生时机:编译时
12.5 纯虚函数
virtual ReturnType FuncName (...) = 0;
- 定义了一个与派生类中一致的接口
12.6 抽象类
不能创建对象
-
含有纯虚函数的类
只有当派生类实现所有的纯虚函数时,该派生类才不是一个抽象类
-
构造函数的访问权限是
protected
的类