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

目录

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

cpp 复制代码
#pragma once

class CA
{
public:
	CA();
	~CA();

public:
	void test();
};

A.cpp

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

cpp 复制代码
#pragma once
#include <memory>

class CA;

class CB
{
public:
	CB();
	~CB();

private:
	std::unique_ptr<CA> m_pA;
};

B.cpp

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. 封装性

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

  1. 减少头文件的依赖

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

  1. 类型安全

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

不完整类型的实际应用

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

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

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

cpp 复制代码
// Person.h
#ifndef PERSON_H
#define PERSON_H

class Person {
public:
    Person(int age);
    int getAge() const;
    void setAge(int age);

private:
    int m_age;
};

#endif

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

cpp 复制代码
// PersonWrapper.h
#ifndef PERSONWRAPPER_H
#define PERSONWRAPPER_H

#ifdef __cplusplus
extern "C" {
#endif

struct 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

实现接口:

cpp 复制代码
// 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 作为它们的删除器。

cpp 复制代码
template <class _Ty>
struct default_delete;

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

default_delete的实现代码如下:

cpp 复制代码
// STRUCT TEMPLATE default_delete
//指针版本
template <class _Ty>
struct default_delete { // default deleter for unique_ptr
    constexpr 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 pointer
        static_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 size
    constexpr 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 pointer
        static_assert(0 < sizeof(_Uty), "can't delete an incomplete type");
        delete[] _Ptr;
    }
};

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

cpp 复制代码
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也是用来检测不完整类型的,它的实现方法如下:

cpp 复制代码
    //utiles.h
    template<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_deletechecked_array_delete, 可以解决这个删除不完整类型的问题。它没有运行期的额外开销,只是直接调用 delete, 因此说 checked_delete 带来的安全性实际上是免费的。如果你需要在调用delete时确保类型是完整的,就使用 checked_delete

5.总结

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

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

相关推荐
老赵的博客3 分钟前
QT 自定义界面布局要诀
开发语言·qt
gma9998 分钟前
brpc 与 Etcd 二次封装
数据库·c++·rpc·etcd
ö Constancy12 分钟前
设计LRU缓存
c++·算法·缓存
香菜大丸14 分钟前
leetcode 面试150之 156.LUR 缓存
算法
小白不太白95018 分钟前
设计模式之建造者模式
java·设计模式·建造者模式
p-knowledge19 分钟前
建造者模式(Builder Pattern)
java·开发语言·建造者模式
网络安全(king)25 分钟前
【Python】【持续项目】Python-安全项目搜集
开发语言·python·安全
工业甲酰苯胺26 分钟前
Python脚本消费多个Kafka topic
开发语言·python·kafka
infiniteWei31 分钟前
【Lucene】搜索引擎和文档相关性评分 BM25 算法的工作原理
算法·搜索引擎·lucene
麻花201336 分钟前
C#之WPF的C1FlexGrid空间的行加载事件和列事件变更处理动态加载的枚举值
开发语言·c#·wpf