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


七、总结

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

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

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

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

目录
相关文章
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
140 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
222 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
273 12
|
8月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
160 16
|
8月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
8月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
8月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
442 6
|
8月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
9月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。