一、什么是空类
空类的定义
📌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 函数的第一个参数,表示这个参数没有任何实际作用,只是用来标记这个函数的用途。
七、总结
空类是指没有任何成员的类,虽然它们看起来似乎没有什么用处,但实际上它们可以在一些情况下发挥重要作用。
首先,空类可以被用于定义类型别名,这种情况下,空类没有任何成员,只是用来标识某个类型。空类还可以被用作占位符类型,表示某个类型是未知的或者并不重要的。这种情况下,空类可以被用在泛型编程中,为模板提供默认类型。
最后,空类还可以被用作标记类型,表示某个函数或者类只是用来做某个特定的操作,而不是存储任何数据。这种情况下,空类被用来作为函数或者类的参数或者模板参数,表示这个参数没有任何实际作用,只是用来标记这个函数或者类的用途。
总之,空类虽然看起来很简单,但是它们可以在一些情况下发挥重要作用,特别是在泛型编程和标记类型的场景下。因此,我们应该了解空类的用途,并在需要的时候灵活运用。