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
#pragma once
class 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_H
class 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" {
#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
实现接口:
// 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_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;
}
};
上面编译异常就是在这里报错的:
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.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_delete
和 checked_array_delete
, 可以解决这个删除不完整类型的问题。它没有运行期的额外开销,只是直接调用 delete
, 因此说 checked_delete
带来的安全性实际上是免费的。如果你需要在调用delete
时确保类型是完整的,就使用 checked_delete
5.总结
不完整类型(Incomplete Type)是 C/C++ 中一种非常有用的技术,能够帮助开发者实现封装性、减少代码耦合和依赖。通过前置声明,我们可以隐藏类型的实现细节,使得接口更为简洁、类型安全,尤其是在跨语言或模块化设计中,不完整类型发挥了重要的作用。
本文还介绍了两种检测不完整类型的检测器:default_delete 和 checked_delete,它们能在静态编译时监测出delete是否有问题,也可以安全的删除,不用再担心内存泄漏了。