C++ 空类的艺术:理解空类的用法与实现

简介: C++ 空类的艺术:理解空类的用法与实现

一、什么是空类

空类的定义

📌C++的空类是指这个类不带任何数据,即类中没有非静态 (non-static)数据成员变量,没有虚函数 (virtual function),也没有虚基类 (virtual base class)。

class EmptyClass {
};

为什么需要空类

空类的存在是为了满足特定的编程需求。一些编程场景中,需要定义一个类来作为其他类的基类,但是这个基类并不需要包含任何成员变量或成员函数,只需要作为一个标识符存在即可。例如,C++标准库中的std::empty就是一个空类,用来表示一个容器是否为空。

空类还可以用来占位,等待后续扩展。例如,某个项目中需要定义一些类,但是这些类的具体实现和继承关系还没有确定,可以先定义为空类,等到后续确定后再进行扩展。

总之,空类的存在并不是为了提供实际的功能,而是为了满足特定的编程需求。

二、空类的特点

空类是指没有任何成员变量和成员函数的类,也称为空类或纯粹的虚类。

空类的大小

在C++中空类会占一个字节,这是因为C++编译器规定,每个对象在内存中至少占用1字节的空间,让对象的实例能够相互区别。

具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。

如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址。 注意:当该空类作为基类时,该类的大小就优化为0了,这就是所谓的空白基类最优化。
注意:空白基类最优化无法被施加于多重继承上只适合单一继承。

空类的默认构造函数

空类的默认构造函数会被编译器自动合成,它不需要任何参数,也不需要执行任何操作。可以通过以下代码验证:

class EmptyClass {};
int main() {
    EmptyClass obj;
    return 0;
  } 

上述代码中,创建了一个空类的对象obj,我们没有定义任何构造函数,但是程序可以正常运行,说明编译器已经自动合成了默认构造函数。


空类的析构函数

空类的析构函数也会被编译器自动合成,它同样不需要任何参数,也不需要执行任何操作。可以通过以下代码验证:

class EmptyClass {};
int main() {
    EmptyClass obj;
    return 0;
}

上述代码中,创建了一个空类的对象obj,当程序结束时,obj会被自动销毁,此时编译器会自动调用析构函数。由于空类没有任何成员变量和成员函数,因此析构函数不需要执行任何操作。


三、空类的组成

空类中的成员函数

空类,声明时编译器不会生成任何成员函数,只会生成1个字节的占位符.

空类在定义时会生成6个成员函数.


空类中默认的成员函数

class Empty 
{   public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本) 
    };

空类的成员函数调用

Empty *e = new Empty();    //缺省构造函数 
delete e;                 //析构函数 
Empty e1;                  //缺省构造函数                            
Empty e2(e1);              //拷贝构造函数 
e2 = e1;                   //赋值运算符
Empty *pe1 = &e1;          //取址运算符(非const) 
const Empty *pe2 = &e2;    //取址运算符(const)

C++编译器对这些函数的实现

inline Empty::Empty()                          //缺省构造函数
{
}
inline Empty::~Empty()                         //析构函数
{
}
inline Empty *Empty::operator&()               //取址运算符(非const)
{
 return this; 
}           
inline const Empty *Empty::operator&() const    //取址运算符(const)
{
 return this;
}
inline Empty::Empty(const Empty &rhs)           //拷贝构造函数
{
 //对类的非静态数据成员进行以"成员为单位"逐一拷贝构造
 //固定类型的对象拷贝构造是从源对象到目标对象的"逐位"拷贝
}
inline Empty& Empty::operator=(const Empty &rhs) //赋值运算符
{
 //对类的非静态数据成员进行以"成员为单位"逐一赋值
 //固定类型的对象赋值是从源对象到目标对象的"逐位"赋值。
}

四、空类的应用

空类是指没有任何成员的类,它的大小为1字节。虽然空类看起来毫无用处,但在模板编程中却有很多应用。下面我们来看一些例子。

占位符类型

在模板编程中,我们经常需要定义一个类型,但这个类型并不重要,只是为了占位。这时候就可以使用空类作为占位符类型。例如,下面的代码定义了一个函数模板,它接受两个参数,第一个参数是占位符类型,第二个参数是一个整数,返回值是第二个参数的平方。

template <typename T>
int square(T, int x)
{
    return x * x;
}

我们可以使用任何类型作为第一个参数,因为它并不会被使用。


定义类型别名

空类也可以用来定义类型别名。例如,下面的代码定义了一个类型别名void_t,它代表一个空类型。

class void_t {};
template <typename T, typename = void_t>
struct has_member : std::false_type {};
template <typename T>
struct has_member<T, void_t<decltype(T::member)>> : std::true_type {};

这段代码定义了一个模板类has_member,它用于判断一个类型是否有名为member的成员变量。如果类型T有这个成员变量,那么has_member::value为true,否则为false。这里使用了一个void_t类型作为占位符,如果T没有member成员变量,那么void_t<decltype(T::member)>就是一个空类型,否则就是decltype(T::member)类型。


具有特定含义的标记类型

空类还可以用来定义具有特定含义的标记类型。例如,下面的代码定义了一个标记类型trivially_copyable_tag,它表示一个类型是否是trivially copyable的。

class trivially_copyable_tag {};
template <typename T>
void foo(T x, trivially_copyable_tag)
{
   // 如果T是trivially copyable的,就可以使用memcpy等函数
   // 来进行内存拷贝,否则就需要使用其他方法
}

这里的trivially_copyable_tag类型并不会被使用,它只是用来表示一个含义,方便我们在代码中进行区分。在调用foo函数时,我们可以传递一个trivially_copyable_tag类型的参数,来告诉函数是否可以使用memcpy等函数。如果T是trivially copyable的,就传递一个空的trivially_copyable_tag对象,否则就传递一个不同的标记对象。


五、空类的注意事项

空类是指没有任何成员变量和成员函数的类。在实际开发中,经常需要定义空类来作为其他类的基类或占位符。但是,空类也存在一些注意事项,下面将分别介绍空类的命名规范、大小问题和继承问题。

空类的命名规范

空类的命名应该具有一定的描述性,能够反映出该类的用途或作用。空类的命名应该遵循驼峰命名法,并且应该以一个大写字母开头。

例如,一个空类用于占位符的命名可以是:

class Placeholder;

空类的大小问题

空类在内存中占用的空间大小为1字节。这是因为在C++中,每个对象都必须占用至少1字节的存储空间。因此,空类的大小至少为1字节。


空类的继承问题

当一个类继承自一个空类时,编译器会自动为子类分配1字节的存储空间,用来保证其对象的唯一性。因此,如果一个子类继承自多个空类,那么它的大小也会至少为1字节。

例如,一个子类同时继承自两个空类的情况下,其大小为2字节:

class Empty1 {};
class Empty2 {};
class Child : public Empty1, public Empty2 {};
sizeof(Child); // 输出2

六、空类的示例

使用空类定义类型别名

空类可以被用于定义类型别名,这种情况下,空类没有任何成员,只是用来标识某个类型。

class EmptyClass {};
using MyType = EmptyClass;

这里,MyType 被定义为 EmptyClass 类型的别名。虽然 EmptyClass 是空类,但是它可以在其他地方被用作类型,例如:

void foo(MyType arg) {
   // ...
}

使用空类作为占位符类型

空类也可以被用作占位符类型,表示某个类型是未知的或者并不重要的。这种情况下,空类可以被用在泛型编程中,例如:

template <typename T, typename U = EmptyClass>
 class MyClass {
    // ...
};

使用空类作为标记类型

空类还可以被用作标记类型,表示某个函数或者类只是用来做某个特定的操作,而不是存储任何数据。例如:

class MyLogger {
public:
   static void log(const EmptyClass& /* unused */, const std::string& message) {
       // ...
   }
};
MyLogger::log(EmptyClass{}, "Hello, world!");

这里,EmptyClass 被用来作为 MyLogger::log 函数的第一个参数,表示这个参数没有任何实际作用,只是用来标记这个函数的用途。


七、总结

空类是指没有任何成员的类,虽然它们看起来似乎没有什么用处,但实际上它们可以在一些情况下发挥重要作用。

首先,空类可以被用于定义类型别名,这种情况下,空类没有任何成员,只是用来标识某个类型。空类还可以被用作占位符类型,表示某个类型是未知的或者并不重要的。这种情况下,空类可以被用在泛型编程中,为模板提供默认类型。

最后,空类还可以被用作标记类型,表示某个函数或者类只是用来做某个特定的操作,而不是存储任何数据。这种情况下,空类被用来作为函数或者类的参数或者模板参数,表示这个参数没有任何实际作用,只是用来标记这个函数或者类的用途。

总之,空类虽然看起来很简单,但是它们可以在一些情况下发挥重要作用,特别是在泛型编程和标记类型的场景下。因此,我们应该了解空类的用途,并在需要的时候灵活运用。

目录
相关文章
|
23天前
|
存储 C++ 容器
【C++】map、set基本用法
本文介绍了C++ STL中的`map`和`set`两种关联容器。`map`用于存储键值对,每个键唯一;而`set`存储唯一元素,不包含值。两者均基于红黑树实现,支持高效的查找、插入和删除操作。文中详细列举了它们的构造方法、迭代器、容量检查、元素修改等常用接口,并简要对比了`map`与`set`的主要差异。此外,还介绍了允许重复元素的`multiset`和`multimap`。
29 3
【C++】map、set基本用法
|
21天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
1天前
|
C++
第十三问:C++中静态变量的用法有哪些?
本文介绍了 C++ 中静态变量和函数的用法及原理。静态变量包括函数内的静态局部变量和类中的静态成员变量,前者在函数调用间保持值,后者属于类而非对象。静态函数不能访问非静态成员,但可以通过类名直接调用。静态链接使变量或函数仅在定义文件内可见,避免命名冲突。
9 0
|
1天前
|
存储 安全 编译器
第二问:C++中const用法详解
`const` 是 C++ 中用于定义常量的关键字,主要作用是防止值被修改。它可以修饰变量、指针、函数参数、返回值、类成员等,确保数据的不可变性。`const` 的常见用法包括:
16 0
|
27天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
70 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
72 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
82 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
26 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1