联合体union

非受限联合体

联合体

在C++中,union 是一种特殊的数据结构,允许在同一内存位置存储不同的数据类型。union 的每个成员都从同一内存位置开始,这就意味着 union 中的所有成员共享同一块内存。

union 的语法如下:

cpp 复制代码
union MyUnion {
    int intValue;
    double doubleValue;
    char charValue;
};

在这个例子中,MyUnion 是一个包含三个成员的 union,分别是 intValue(整数)、doubleValue(双精度浮点数)和 charValue(字符)。union 中的每个成员都从同一内存位置开始,其大小等于最大成员的大小。

代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

union MyUnion {
    int intValue;
    double doubleValue;
    char charValue;
};

int main() {
    MyUnion myUnion;

    myUnion.intValue = 42;
    cout << "intValue: " << myUnion.intValue << endl;
    cout << "doubleValue: " << myUnion.doubleValue << endl; // -9.25596e+61
    cout << "intValue: " << myUnion.intValue << endl;
    cout << "charValue: " << myUnion.charValue << endl; // *

    myUnion.doubleValue = 3.14;
    cout << "doubleValue: " << myUnion.doubleValue << endl;
    cout << "intValue: " << myUnion.intValue << endl; // 1374389535

    myUnion.charValue = 'A';
    cout << "charValue: " << myUnion.charValue << endl;
    cout << "intValue: " << myUnion.intValue << endl; // 1374389569

    return 0;
}

程序输出结果为:

cpp 复制代码
intValue: 42
doubleValue: -9.25596e+61
intValue: 42
charValue: *
doubleValue: 3.14
intValue: 1374389535
charValue: A
intValue: 1374389569

在这个例子中,myUnion 的三个成员共享同一块内存。通过修改一个成员的值,其他成员的值也会受到影响。这是因为它们共享相同的内存空间

注意事项:

  • 使用 union 需要小心,因为在不同的时刻只能使用 union 中的一个成员,对于其他成员的值可能会不可预测。
  • 当你需要在不同的数据类型之间共享内存空间,或者在节省内存时需要将不同类型的数据共用一个存储位置时,union 可能是一种有用的工具。
  • 要确保使用 union 时的操作是安全的,尽量避免引发未定义行为
非受限联合体(c++11之后的union)

在C++11之前我们使用的联合体是有局限性的,主要有以下三点:

  1. 不允许联合体拥有非POD类型的成员

  2. 不允许联合体拥有静态成员

  3. 不允许联合体拥有引用类型的成员

在新的C++11标准中,取消了关于联合体对于数据成员类型的限定,规定任何非引用类型都可以成为联合体的数据成员,这样的联合体称之为非受限联合体(Unrestricted Union)

非受限联合体的使用
静态类型的成员

对于非受限联合体来说,静态成员有两种分别是静态成员变量和静态成员函数,我们来看一下下面的代码:

java 复制代码
union Test
{
    int age;
    long id;
    // int& tmp = age; // error
    static char c;
    static int print()
    {
        cout << "c value: " << c << endl;
        return 0;
    }
};
char Test::c;
// char Test::c = 'a';

int main()
{
    Test t;
    Test t1;
    t.c = 'b';
    t1.c = 'c';
    t1.age = 666;
    cout << "t.c: " << t.c << endl;
    cout << "t1.c: " << t1.c << endl;
    cout << "t1.age: " << t1.age << endl;
    cout << "t1.id: " << t1.id << endl;
    t.print();
    Test::print();
    return 0;
}

执行程序输出的结果如下:

cpp 复制代码
t.c: c
t1.c: c
t1.age: 666
t1.id: 666
c value: c
c value: c

接下来我们逐一分析一下上面的代码:

  • 第5行:语法错误,非受限联合体中不允许出现引用类型

  • 第6行:非受限联合体中的静态成员变量

  • ​ 需要在非受限联合体外部声明(第13行)或者初始化(第14行)之后才能使用

  • ​ 通过打印的结果可以发现18、19行的t和t1对象共享这个静态成员变量(和类 class/struct 中的静态成员变量的使用是一样的)。

  • 第7行:非受限联合体中的静态成员函数

  • ​ 在静态函数print()只能访问非受限联合体Test中的静态变量,对于非静态成员变量(age、id)是无法访问的。

  • ​ 调用这个静态方法可以通过对象(第27行)也可以通过类名(第28行)实现。

  • 第24、25、26行:通过打印的结果可以得出结论在非受限联合体中静态成员变量和非静态成员变量使用的不是同一块内存。

非POD类型成员

在 C++11标准中会默认删除一些非受限联合体的默认函数。比如,非受限联合体有一个非 POD 的成员,而该非 POD成员类型拥有 非平凡的构造函数,那么**非受限联合体的默认构造函数将被编译器删除。其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将遵从此规则。**下面来举例说明:

cpp 复制代码
union Student
{
    int id;
    string name;
};

int main()
{
    Student s;
    return 0;
}

编译程序会看到如下的错误提示:

cpp 复制代码
warning C4624: "Student": 已将析构函数隐式定义为"已删除"
error C2280: "Student::Student(void)": 尝试引用已删除的函数

上面代码中的非受限联合体Student中拥有一个非POD类型的成员string namestring 类中有非平凡构造函数,因此Student的构造函数被删除 (通过警告信息可以得知它的析构函数也被删除了)导致对象无法被成功创建出来。解决这个问题的办法就是由程序猿自己为非受限联合体定义构造函数,在定义构造函数的时候我们需要用到定位放置 new操作。

placement new

一般情况下,使用new申请空间时,是从系统的堆(heap)中分配空间 ,申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这种操作就叫做placement new即定位放置 new。

定位放置new操作的语法形式不同于普通的new操作:

使用new申请内存空间:Base* ptr = new Base;

使用定位放置new申请内存空间:

cpp 复制代码
ClassName* ptr = new (定位的内存地址)ClassName;

我们来看下面的示例程序:

cpp 复制代码
#include <iostream>
using namespace std;

class Base
{
public:
    Base() {}
    ~Base() {}
    void print()
    {
        cout << "number value: " << number << endl;
    }
private:
    int number;
};

int main()
{
    int n = 100;
    Base* b = new (&n)Base;
    b->print();
    return 0;
}

程序运行输出的结果为:

cpp 复制代码
number value: 100

在程序的第20行,使用定位放置的方式为指针b申请了一块内存,也就是说此时指针 b指向的内存地址和变量 n对应的内存地址是同一块(栈内存),而在Base类中成员变量 number的起始地址和Base对象的起始地址是相同的,所以打印出 number 的值为100也就是整形变量 n 的值。

最后,给大家总结一下关于placement new的一些细节:

  1. 使用定位放置new操作,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象,这取决于定位时指定的内存地址是在堆还是在栈上。
  2. 从表面上看,定位放置new操作是申请空间,其本质是利用已经申请好的空间,真正的申请空间的工作是在此之前完成的。
  3. 使用定位放置new 创建对象时会自动调用对应类的构造函数,但是由于对象的空间不会自动释放,如果需要释放堆内存必须显示调用类的析构函数。
  4. 使用定位放置new操作,我们可以反复动态申请到同一块堆内存,这样可以避免内存的重复创建销毁,从而提高程序的执行效率(比如网络通信中数据的接收和发送)。
自定义非受限联合体构造函数

掌握了**placement new的使用,我们通过一段程序来演示一下如何在非受限联合体中自定义构造函数**:

cpp 复制代码
#include <iostream>
using namespace std;

class Base
{
public:
    void setText(string str)
    {
        notes = str;
    }
    void print()
    {
        cout << "Base notes: " << notes << endl;
    }
private:
    string notes;
};

union Student
{
    Student()
    {
        new (&name)string;
    }
    ~Student() {}

    int id;
    Base tmp;
    string name;
};

int main()
{
    Student s;
    s.name = "蒙奇·D·路飞";
    s.tmp.setText("我是要成为海贼王的男人!");
    s.tmp.print();
    cout << "Student name: " << s.name << endl;
    return 0;
}

程序打印的结果如下:

cpp 复制代码
Base notes: 我是要成为海贼王的男人!
Student name: 我是要成为海贼王的男人!

我们在上面的程序里边给非受限制联合体显示的指定了构造函数和析构函数,在程序的**第31行需要创建一个非受限联合体对象,这时便调用了联合体内部的构造函数,在构造函数的第20行通过定位放置 new的方式将构造出的对象地址定位到了联合体的成员string name的地址上了,这样联合体内部其他非静态成员也就可以访问这块地址了(通过输出的结果可以看到对联合体内的tmp对象赋值,会覆盖name对象中的数据)**。

匿名的非受限联合体

一般情况下我们使用的非受限联合体都是具名的(有名字),但是我们也可以定义匿名的非受限联合体,一个比较实用的场景就是配合着类的定义使用。我们来设定一个场景:

cpp 复制代码
木叶村要进行第99次人口普查,人员的登记方式如下:
    - 学生只需要登记所在学校的编号
    - 本村学生以外的人员需要登记其身份证号码
    - 本村外来人员需要登记户口所在地+联系方式
cpp 复制代码
#include <iostream>
using namespace std;

// 外来人口信息
struct Foreigner
{
    Foreigner(string s, string ph) : addr(s), phone(ph) {}
    string addr;
    string phone;
};

// 登记人口信息
class Person
{
public:
    enum class Category : char { Student, Local, Foreign };
    Person(int num) : number(num), type(Category::Student) {}
    Person(string id) : idNum(id), type(Category::Local) {}
    Person(string addr, string phone) : foreign(addr, phone), type(Category::Foreign) {}
    ~Person() {}

    void print()
    {
        cout << "Person category: " << (int)type << endl; // 打印0代表学生, 1代表Local...
        switch (type)
        {
        case Category::Student:
            cout << "Student school number: " << number << endl;
            break;
        case Category::Local:
            cout << "Local people ID number: " << idNum << endl;
            break;
        case Category::Foreign:
            cout << "Foreigner address: " << foreign.addr
                << ", phone: " << foreign.phone << endl;
            break;
        default:
            break;
        }
    }

private:
    Category type;
    union
    {
        int number;
        string idNum;
        Foreigner foreign;
    };
};

int main()
{
    Person p1(9527);
    Person p2("1101122022X");
    Person p3("砂隐村村北", "1301810001");
    p1.print();
    p2.print();
    p3.print();
    return 0;
}

程序输出的结果:

cpp 复制代码
Person category: 0
Student school number: 9527
Person category: 1
Local people ID number: 1101122022X
Person category: 2
Foreigner address: 砂隐村村北, phone: 1301810001

根据需求我们将木叶村的人口分为了三类并通过枚举记录了下来,在Person类中添加了一个匿名的非受限联合体用来存储人口信息,仔细分析之后就会发现这种处理方式的优势非常明显:尽可能地节省了内存空间。

  • Person类可以直接访问匿名非受限联合体内部的数据成员。
  • 不使用匿名非受限联合体申请的内存空间等于 number、 idNum 、 foreign 三者内存之和。
  • 使用匿名非受限联合体之后number、 idNum 、 foreign 三者共用同一块内存。

因为不同类型的Person对象用到不同的数据, 用到两部分数据的Foreigner类型, 我们把这种类型的两种信息放到了一个结构体中, 把这个结构体类型放到union中, 需要的时候访问.

相关推荐
我们的五年6 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
kitesxian8 分钟前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
做人不要太理性32 分钟前
【C++】深入哈希表核心:从改造到封装,解锁 unordered_set 与 unordered_map 的终极奥义!
c++·哈希算法·散列表·unordered_map·unordered_set
程序员-King.41 分钟前
2、桥接模式
c++·桥接模式
chnming19871 小时前
STL关联式容器之map
开发语言·c++
VertexGeek1 小时前
Rust学习(八):异常处理和宏编程:
学习·算法·rust
石小石Orz1 小时前
Three.js + AI:AI 算法生成 3D 萤火虫飞舞效果~
javascript·人工智能·算法
程序伍六七1 小时前
day16
开发语言·c++
小陈phd1 小时前
Vscode LinuxC++环境配置
linux·c++·vscode
火山口车神丶2 小时前
某车企ASW面试笔试题
c++·matlab