C++不完整类型(Incomplete Type)的检测与避免

embedded/2024/11/22 11:53:02/

目录

1.引言

2.为什么使用不完整类型?

3.C++默认删除器default_delete

4.boost库中checked_delete

5.总结


1.引言

        在C++中,类型有Complete type和Incomplete type之分,对于Complete type, 它的大小在编译时是可以确定的,而对于Incomplete type, 它的大小在编译时是不能确定的。

       用delete删除一个只有声明但无定义的类型的指针(即不完整类型),是危险的。这通常导致无法调用析构函数(包括对象本身的析构函数、成员/基类的析构函数),从而泄露资源。

      不完整类类型Imcomplete class type:只见声明不见定义的类、结构体或是联合体;相对应的就是complete type,就是编译器可以确定的类型。

        示例:有CA和CB两个类

        A.h

#pragma onceclass CA
{
public:CA();~CA();public:void test();
};

        A.cpp

#include "A.h"
#include <iostream>CA::CA()
{std::cout << "CA()" << std::endl;
}
CA::~CA()
{std::cout << "~CA()" << std::endl;
}void CA::test()
{}

    B.h

#pragma once
#include <memory>class CA;class CB
{
public:CB();~CB();private:std::unique_ptr<CA> m_pA;
};

        B.cpp

#include "B.h"
//#include "A.h"
#include <iostream>CB::CB()
{std::cout << "CB()" << std::endl;
}CB::~CB()
{std::cout << "~CB()" << std::endl;
}

编译的时候会出现如下报错:

这里的m_pA对象在delete的时候就是不确定对象,编译器不知道它的类型,无法调用析构函数,最终导致内存泄漏。解决的最简单的方法,就是在B.cpp文件中增加#include “A.h”语句即可。

2.为什么使用不完整类型?

        1) 封装性
        不完整类型允许实现细节隐藏。我们可以在头文件中仅声明类型的存在,具体的实现则放在源文件中,从而防止用户代码直接访问类的成员。这种设计提高了封装性,避免了用户代码依赖于类的实现细节。

        2) 减少头文件的依赖
        通过前置声明可以减少头文件之间的相互依赖。如果我们只需要声明一个指针类型,而不需要完整的类型定义,前置声明就可以避免包括额外的头文件。这有助于减少编译时间和代码耦合。

        3) 类型安全
        前置声明结合指针可以创建不透明类型(Opaque Type),从而实现类型安全。例如,如果不同类型使用相似的接口,编译器会捕获到类型不匹配的错误,这样可以避免因错误地互换类型而导致的问题。

        不完整类型的实际应用

        不完整类型在 C 和 C++ 结合使用时非常有用。我们可以在 C++ 中实现类,并通过 C 兼容的接口暴露给 C 代码,利用前置声明和不透明指针来隐藏实现细节。

        以下是一个实际的示例,展示如何使用不完整类型实现 C++ 类的封装性。

        C++ 代码:实际类实现:

// Person.h
#ifndef PERSON_H
#define PERSON_Hclass Person {
public:Person(int age);int getAge() const;void setAge(int age);private:int m_age;
};#endif

        C 代码:不完整类型与接口:

// PersonWrapper.h
#ifndef PERSONWRAPPER_H
#define PERSONWRAPPER_H#ifdef __cplusplus
extern "C" {
#endifstruct Person_t;  // 前置声明,不完整类型typedef struct Person_t* PersonHandle;PersonHandle Person_create(int age);
void Person_destroy(PersonHandle handle);
int Person_getAge(PersonHandle handle);
void Person_setAge(PersonHandle handle, int age);#ifdef __cplusplus
}
#endif#endif

           实现接口:

// PersonWrapper.cpp
#include "PersonWrapper.h"
#include "Person.h"extern "C" {PersonHandle Person_create(int age) {return reinterpret_cast<PersonHandle>(new Person(age));}void Person_destroy(PersonHandle handle) {delete reinterpret_cast<Person*>(handle);}int Person_getAge(PersonHandle handle) {return reinterpret_cast<Person*>(handle)->getAge();}void Person_setAge(PersonHandle handle, int age) {reinterpret_cast<Person*>(handle)->setAge(age);}
}

在这个例子中:

  • Person_t 是一个前置声明,C 代码无法知道它的内部细节。
  • 在接口函数中使用 PersonHandle,它是一个指向 Person_t 的指针,这样可以实现封装性。
  • 编译器在处理前置声明时,只记录类型信息,不会进行内存分配,直到类的具体实现出现为止。

3.C++默认删除器default_delete

        default_delete 是 C++ 标准库中的一个模板类,它定义在头文件 <memory> 中。这个类模板用于提供默认的删除操作,主要用于智能指针(如 std::unique_ptr 和 std::shared_ptr)中,以指定如何删除其所管理的对象。
        default_delete 的主要作用是提供一个简单的删除函数对象,它调用 delete 操作符来销毁给定指针指向的对象。默认情况下,std::unique_ptr 和 std::shared_ptr 使用 default_delete 作为它们的删除器。

template <class _Ty>
struct default_delete;template <class _Ty, class _Dx = default_delete<_Ty>>
class unique_ptr;

default_delete的实现代码如下:

// STRUCT TEMPLATE default_delete
//指针版本
template <class _Ty>
struct default_delete { // default deleter for unique_ptrconstexpr default_delete() noexcept = default;template <class _Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0>default_delete(const default_delete<_Ty2>&) noexcept {}void operator()(_Ty* _Ptr) const noexcept /* strengthened */ { // delete a pointerstatic_assert(0 < sizeof(_Ty), "can't delete an incomplete type");delete _Ptr;}
};//数组版本
template <class _Ty>
struct default_delete<_Ty[]> { // default deleter for unique_ptr to array of unknown sizeconstexpr default_delete() noexcept = default;template <class _Uty, enable_if_t<is_convertible_v<_Uty (*)[], _Ty (*)[]>, int> = 0>default_delete(const default_delete<_Uty[]>&) noexcept {}template <class _Uty, enable_if_t<is_convertible_v<_Uty (*)[], _Ty (*)[]>, int> = 0>void operator()(_Uty* _Ptr) const noexcept /* strengthened */ { // delete a pointerstatic_assert(0 < sizeof(_Uty), "can't delete an incomplete type");delete[] _Ptr;}
};

        上面编译异常就是在这里报错的:

static_assert(0 < sizeof(_Uty), "can't delete an incomplete type");

        系统找不到_Uty的定义,sizeof(_Uty)返回0,引发static_assert断言异常。

        从上面的代码可以看到,检测的原理是针对不完整类型,在不同编译器下sizeof会报错或者返回0,返回0时会引发编译时断言失败,这也是不允许的,所以如果T为不完整类型,编译时会报错,方便检查代码。

        虽然 default_delete 提供了一种默认的方式来删除对象,但你也可以为智能指针提供自定义的删除器。自定义删除器可以是任何可以像函数那样被调用的对象,它接受一个指针作为参数,并负责销毁该指针指向的对象。

C++智能指针的自定义销毁器(销毁策略)_c++指针销毁-CSDN博客

4.boost库中checked_delete

        boost库中的checked_delete也是用来检测不完整类型的,它的实现方法如下:

    //utiles.htemplate<typename T> inline void checked_delete(T * x)  {  // intentionally complex - simplification causes regressions   typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];  (void) sizeof(type_must_be_complete);  delete x;  } template<typename T> inline void checked_array_delete(T * x)  {  // intentionally complex - simplification causes regressions   typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];  (void) sizeof(type_must_be_complete);  delete[] x;  } template<typename T>struct checked_deleter{typedef void result_type;typedef T * argument_type;void operator()(T * p) const{ checked_delete(p);}};template<typename T>struct checked_array_deleter{typedef void result_type;typedef T * argument_type;void operator()(T * p) const{ checked_array_delete(p);}};

        这里的思路和第3章节的思路差不多,原理是创建一个char的数组,数组的元素数量为T的大小。如果 checked_delete 被一个不完整的类型 T 所实例化,编译将会失败,因为 sizeof(T) 会返回 0, 而创建一个0个元素的(自动)数组是非法的,进而引发编译错误,从而达到检测不完整类型的目的。

         删除一个动态分配的对象时,必须调用它的析构函数。如果这个类型是不完整的,即只有声明没有定义,那么析构函数可能会没被调用。这是一种潜在的危险状态,所以应该避免它。对于类模板及函数模板,风险会更大,因为无法预先知道会使用什么类型。用 checked_delete 和 checked_array_delete, 可以解决这个删除不完整类型的问题。它没有运行期的额外开销,只是直接调用 delete, 因此说 checked_delete 带来的安全性实际上是免费的。如果你需要在调用delete时确保类型是完整的,就使用 checked_delete 

5.总结

        不完整类型(Incomplete Type)是 C/C++ 中一种非常有用的技术,能够帮助开发者实现封装性、减少代码耦合和依赖。通过前置声明,我们可以隐藏类型的实现细节,使得接口更为简洁、类型安全,尤其是在跨语言或模块化设计中,不完整类型发挥了重要的作用。

        本文还介绍了两种检测不完整类型的检测器:default_delete 和 checked_delete,它们能在静态编译时监测出delete是否有问题,也可以安全的删除,不用再担心内存泄漏了。


http://www.ppmy.cn/embedded/139619.html

相关文章

【Patroni官方文档】复制模式

Patroni 使用 PostgreSQL 的流复制。有关流复制的更多信息,请参阅 Postgres 文档。Patroni 默认将 PostgreSQL 配置为异步复制。选择哪种复制模式取决于业务需求。请研究异步和同步复制以及其他高可用性(HA)解决方案,以确定哪种解决方案最适合您。 异步模式的持久性 在异…

5、AI测试辅助-生成测试用例思维导图

AI测试辅助-生成测试用例思维导图 创建测试用例两种方式1、Plantuml思维导图版本 (不推荐&#xff09;2、Markdown思维导图版本&#xff08;推荐&#xff09; 创建测试用例两种方式 完整的测试用例通常需要包含以下的元素&#xff1a; 1、测试模块 2、测试标题 3、前置条件 4、…

初试无监督学习 - K均值聚类算法

文章目录 1. K均值聚类算法概述2. k均值聚类算法演示2.1 准备工作2.2 生成聚类用的样本数据集2.3 初始化KMeans模型对象&#xff0c;并指定类别数量2.4 用样本数据训练模型2.5 用训练好的模型生成预测结果2.6 输出预测结果2.7 可视化预测结果 3. 实战小结 1. K均值聚类算法概述…

javaScript交互案例2

1、京东侧边导航条 需求&#xff1a; 原先侧边栏是绝对定位当页面滚动到一定位置&#xff0c;侧边栏改为固定定位页面继续滚动&#xff0c;会让返回顶部显示出来 思路&#xff1a; 需要用到页面滚动事件scroll&#xff0c;因为是页面滚动&#xff0c;所以事件源是document滚动…

兼顾高性能与低成本,浅析 Apache Doris 异步物化视图原理及典型场景

在现代化的数据分析场景中&#xff0c;数据量以指数级速度快速膨胀&#xff0c;分析维度在不断扩展&#xff0c;查询逻辑的复杂度也在日益增加。从性能角度考虑&#xff0c;在承担高并发查询的压力下&#xff0c;秒级别甚至更快的响应速度已成为基本需求。同时&#xff0c;面对…

C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)

最近在做一个与激光器用串口进行通讯的程序文档中要求将频率参数以3个字节的方式进行发送。这里记录一下过程。以便以后再有类似问题时可以快速解决。 /// <summary>/// 设置频率/// </summary>/// <param name"sender"></param>/// <par…

hhdb数据库介绍(9-24)

计算节点参数说明 failoverAutoresetslave 参数说明&#xff1a; PropertyValue参数值failoverAutoresetslave是否可见是参数说明故障切换时&#xff0c;是否自动重置主从复制关系默认值falseReload是否生效否 参数设置&#xff1a; <property name"failoverAutor…

论文学习——基于协同进化和多样性增强的动态约束多目标优化算法

论文题目&#xff1a;Dynamic constrained multi-objective optimization algorithm based on co-evolution and diversity enhancement 基于协同进化和多样性增强的动态约束多目标优化算法&#xff08;Wang Che a,b, Jinhua Zheng a,b,∗, Yaru Hu a,b, Juan Zou a,b, Shengx…