C++ 面向对象,内存管理(未完。。。)
对象内存模型
类型转换 dynamic_cast type_info
多态
- 虚函数 override final
- 虚表结构 运行时类型信息(RTII)
- 基类析构函数必须为虚函数,否则会有内存泄漏的危险
- 继承有两层含义
- 实现继承:复用父类定义的数据成员和函数成员
- 接口继承:通过父类定义虚函数来约定函数的行为
- 虚继承时,内存中会多一个指针,指向虚继承的基类,可避免菱形继承中重复继承的情况
Pimp设计习语
- Pointer to Implementation,将类数据成员声明为指针,指向某具体实现类或抽象类(前置声明)
- 编译依赖:消除头文件对实现类的以依赖(只需要声明,无需定义)
- 延迟分发:如果使用指针指向抽象类,由于虚函数的多态,将函数的调用延迟到运行时,将具有更好的弹性
RAII(资源获取即初始化)
- RAII(Resource Acquisition Is Initialization)是c++内存和资源管理最重要的管理机制之一,其主要思想是i将资源的生命周期与对象的生命周期进行绑定在一起,确保资源的获取和释放总是成对出现,并且完全受控于对象的构造和析构过程;
- **工作原理:**在RAII机制下,当一个对象被创建(初始化)时,它会立即获取并管理一项资源(例如内存、文件句柄、互斥锁等),一旦对象超出其作用域,编译器会自动调用对象的析构函数,析构函数负责释放先前获取的资源。只要对象被正确创建,资源最终必然会正确释放
- RAII通过三个环节来保证内存或资源得到确定行的释放
- 构造器中获取内存或资源
- 析构器中释放内存或资源
- 栈对象在作用域结束即确定行调用析构器回收内存(编译器自动管理)
- 异常免疫,出现异常也能够确定析构
- 可以同时管理内存对象与非内存对象(文件句柄,锁,网络IO…)
- 不仅管理栈对象、同时管理堆对象
- RAII与移动语义、智能指针结合,可达成资源管理正确性和性能的双重保证
优点
- 异常安全:由于资源是在对象构造时获取,析构时释放,即使在构造后、析构前抛出异常,析构函数依然会被调用,确保资源得正确的释放,避免了因异常导致的资源泄露,增加代码的健壮性;
- 简化代码:RAII模式下、资源概念里的任务提交给了对象自身,开发者无需在代码中过多的关乎手动获取和释放资源这个繁琐复杂的过程,增加了代码的简洁性和可读性;
- 内存泄露的防护:在内存管理方面,RAII通常使用只能智能指针等来实现内存的自动管理,极大减少了内存泄露的风险和可能性;
缺点
- 依赖与构造函数和析构函数:如果对象构造函数和析构函数的过程出现问题,没有将该部分正确的处理,可能会导致资源无法正确获取或释放。如析构函数未正确实现,或者构造函数中获取资源失败却未妥善处理;
- 有时过度封装可能会导致代码过于抽象而难以理解,增加维护成本;
主要使用场景
- 内存管理:通过使用智能指针实现内存的自动管理,如std::uniquea_ptr和std::shared_ptr
- 文件操作:std::fstream或在自定义文件类的构造时打开文件,在析构时关闭文件
- 锁机制:std::lock_guard或std::unique_lock在构造函数时获取互斥锁,在析构时释放锁,确保临界区戴拿的原子性和资源的争取释放
- 其他资源:如网络连接、数据库连接、图形资源等都可以通过RAII机制实现资源的自动管理
析构函数出现崩溃
- 双重释放:其他地方手动释放过,在析构函数中二次释放会发生崩溃
- 悬浮指针:析构函数访问已经释放的成员变量或者析构韩式中调用了已经释放的对象指针,产生悬浮指针,会导致崩溃;
- 析构函数内部出现爆抛出异常:
- 依赖资源释放顺序错误:对象之间存在依赖时,如果析构函数按照错误的顺序释放资源,可能会导致依赖的对象提前被销毁,后面对它引用或者访问导致崩溃;
- 线程同步问题:在多线程环境下,析构函数与线程操作同时进行,可能导致竞争条件,如析构过程中其他线程还在访问或修改改对象,导致崩溃;
- 内存越界:析构函数释放内存时,访问数组越界,会导致崩溃;
- 未初始化的成员变量:析构函数中依赖于未初始化的成员变量进行清理操作,可能会导致未定义的行为,导致崩溃;
Move(移动语义)
- 移动发挥最大价值的场景
- 对象内部有分离内存(通常是指指针指向的堆内存)
- 对象拷贝为深拷贝
- 移动仅仅复制对象内本身的数据,不复制分离内存;拷贝即赋值对象本身数据,也复制分离内存
- 移动永远不比拷贝慢,通常更快,移动是拷贝的优化
- 移动构造函数/移动赋值操作符
- 对象移动之后不再使用(即右值)
字符串字面量(const字符数组)是左值 “qwert”; 字符串字面量对象是右值 (“qwer”s)
- void func(vector&& v)
- 类型为右值引用的函数形参,其实是个左值
- 对于函数调用者来说,v是一个右值引用参数
- 对于函数内不来说,v是一个左值
- 移动一个左值是不安全的
有移动构造,编译器会调用移动构造,没有移动构造函数,调用普通构造函数、
返回值优化 > 移动 > 拷贝(返回值优化必须是对象初始化时才会发生,函数参数不会做返回值优化)
构造函数可以抛异常,析构函数不可以抛异常
含有智能指针的类对象,编译器自动生成的拷贝构造函数和赋值操作符是不正确的,要自己实现
sdynamic_cast<sub*>(b2.release())
智能指针最佳使用场景
- 智能指针仅用于内存管理,不要用于管理非内存资源,非内存资源使用RAII类封装
- 优先使用unique_ptr而不是shared_ptr,除非需要共享使用权
- 使用make_unique()创建unique_ptr
- 使用make_shared()创建shared_ptr
- 使用weak_ptr 防止shared_ptr 的循环引用
模板编程
-
c++模板是一种编译时机制,在编译时生成具体代码,使用实参将模板定义时类化为具体的类型或参数;c++支持两种模板参数:类模板、函数模板
-
模板实例化时,编译器会对实参类型进行检查,确保实参类型符合模板参数的操作要求;C++模板参数支持两种:
- 类型参数,可隐式约束、也可以显示约束
- 值参数、编译时常量、或constexpr函数,不同值参数是不同类型
-
数据成员:只要类型被使用,编译器胡根据其数成员、生成对应类型结构
-
函数成员–选择性实例化
- 非虚函数,如果实际调用到,则会生成代码;如果没有调用到,则不生成
- 虚函数,无论是否调用,总会生成代码,因为在运行是,有可能调用到;
-
隐藏编译错误:如果某些模板方法没有被调用,即使含又编译错误,也会被忽略
-
强制实例化模板
- 使用template class name; 来强制要求编译所有模板类函数成员,可以排除所有编译错误
-
模板类型推导
- 引用性被忽略:引用类型的实参被当作非引用类型使用
- 转发引用:左值参数按左值,右值参数按右值
- 安置传递:实参中的const/volatile修饰会被去掉,都为拷贝
类的萃取(Trait)
- 通过附加属性,基于类型特征来提供正交设计的灵活性
- 聚合了各种相关的类型和变量,一般不包含成员函数
- 可以是固定traits(不使用模板参数化),也可以是模板,可以构成Traits Template
- Trait参数通常依赖其他模板的参数
- 作为模板参数,通常有默认值
- traits与policy的区别:traits基于类型特征,policy基于行为特征
c++20概念
template<typename T>
concept IsPointer = std::is_pointer_v<T>;
template<typename T>
concept CanCompute=requires(T x, T y) {
x + y; // 支持+
x - y; // 支持 -
x * y; //支持 *
x / y;
};
template<CanCompute T>
T compute(T a,T b)
{
return (a+b)*(a-b);
}
int main()
{
int x=200, y=100;
cout << compute(x, y) << endl;
string s1="hello ", s2="cpp";
cout << compute(s1, s2) << endl;//错误的调用,不支持CanCompute概念的约束
}
- c++概念是一种显示的类型接口,再编译时执行类型约束检查,是泛型编程的灵魂
- 概念可以帮助根号的理解泛型组件之间的合约
- 有概念约束的版本,可以van与重载百辨析,相当于通用版本的一个特化
- 概念比traits、编译时表达式都更强大的抽象
- 概念是一种约束,不是代码,没有类型、存储,生命周期以及地址等
- 概念本质是一组对类型参数T的编译时表达式求值 true或false
requires表达式约束
- requires表达式可以指定多个类型约束,多个约束是没有顺序关系,可以是一下组合
- 编译时bool值表达式:类型判断式,编译时变量或函数
- requires表达式:类型定义,有效表达式,表达式残生的类型需求
- concept
- requires表达式执行是编译时检查,对运行时代码没有任何影响、没有性损失
- 多个约束之间何以使用 && 或 || 实现逻辑组合
模板元编程
-
使用模板在编译时操作类型和函数的编程,成为模板元编程
- 提高性能:将部分计算冲运行时一道编译时
- 类型安全:计算一个数据结构或者算法的准确类型
-
泛型编程和模板元编程
- 泛型编程是通过玉树模板参数的定义,进行通用的类型、算法设计;更关注接口合同(concept)
- 模板元编程时通过编译时选择、或者某中形式的迭代,将类型当作值,进行编译时计算
-
constexpr 具有常量属性,同时具有编译时确定性;const 仅仅具有常量属性,但是并不确保编译期确定性
-
constexpr可以应用于一切休要编译时常量的地方(数组大小,模板值参数,枚举数值)
-
constexpr也可以用于函数和成员函数
int a = 100;
const int a1 = 100; //编译时常量
int array1[a1]; //正确
const int a2 = a; //运行时常量
int array2[a2]; //c++中错误(行为未定义),c中正确(栈变长数组)
constexpr int a3 = 100;//编译时常量
constexper函数
- 支持全局函数,类成员函数,模板函数,只要满足编译时常量的计算要求
- 如果结果要求未编译时常量:
- 传参为编译时常量,函数可以正常得到编译时常量值
- 传参为运行时变量,函数编译报错
- 如果结果要求为运行时变量:
- 参数传值可以时运行时变量,也可以为编译时值
//可变参模板
template<typename T, typename... Types>
void print (T const& firstArg, Types const&... args)
{
cout << firstArg << endl;
if constexpr(sizeof...(args) > 0) {
print(args...);
}
}
//模板元编程案例一
template<int n> constexpr int fib()
{
return fib<n-1>() + fib<n-2>();
}
template<> constexpr int fib<1>()
{
return 1;
}
template<> constexpr int fib<2>()
{
return 1;
}
//模板元编程案例二
template<int n> struct Fib
{
static constexpr int value=Fib<n-1>::value+ Fib<n-2>::value;
};
template<> struct Fib<1>
{
static constexpr int value=1;
};
template<> struct Fib<2>
{
static constexpr int value=1;
};
int main()
{
//1,1,2,3,5,8,13,21,34,55
constexpr int f6= Fib<6>::value;//编译时常量
constexpr int f10= Fib<10>::value;//编译时常量
cout<<f6<<endl;
cout<<f10<<endl;
}
static constexpr int value=1;
};
int main()
{
//1,1,2,3,5,8,13,21,34,55
constexpr int f6= Fib<6>::value;//编译时常量
constexpr int f10= Fib<10>::value;//编译时常量
cout<<f6<<endl;
cout<<f10<<endl;
}