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 函数的第一个参数,表示这个参数没有任何实际作用,只是用来标记这个函数的用途。


七、总结

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

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

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

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

目录
相关文章
|
5天前
|
存储 编译器 C语言
c++的学习之路:5、类和对象(1)
c++的学习之路:5、类和对象(1)
19 0
|
5天前
|
C++
c++的学习之路:7、类和对象(3)
c++的学习之路:7、类和对象(3)
19 0
|
3天前
|
设计模式 Java C++
【C++高阶(八)】单例模式&特殊类的设计
【C++高阶(八)】单例模式&特殊类的设计
|
4天前
|
编译器 C++
【C++基础(八)】类和对象(下)--初始化列表,友元,匿名对象
【C++基础(八)】类和对象(下)--初始化列表,友元,匿名对象
|
8天前
|
存储 安全 C语言
【C++】string类
【C++】string类
|
存储 编译器 Linux
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
|
9天前
|
编译器 C++
标准库中的string类(上)——“C++”
标准库中的string类(上)——“C++”
|
10天前
|
编译器 C++
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(中)——“C++”
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(中)——“C++”
|
10天前
|
存储 编译器 C++
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(上)——“C++”
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(上)——“C++”
|
11天前
|
C++
【C++成长记】C++入门 | 类和对象(下) |Static成员、 友元
【C++成长记】C++入门 | 类和对象(下) |Static成员、 友元