非受限联合体

发布时间 2023-12-11 18:34:24作者: Beasts777

文章参考:

爱编程的大丙 (subingwen.cn)

1. 联合体

联合体又名共用体,使用方式和struct相似,其特点在于:

  • 联合体中所有的成员变量,引用的都是内存中的相同位置。
  • 如果联合的不同成员有不同的长度,取最长的那个变量作为联合的长度。
  • 如果将结构体作为联合的成员变量,那么联合的大小取决于变量中最大的结构体的大小。

EG:

struct VARIABLE_UNION{
    enum { INT, FLOAT, STRING } type;
    union {
        int int_value;
        float float_value;
        char *string_value;
    } value;
};

局限性:

  • 不允许联合体拥有非POD类型的成员。
  • 不允许联合体拥有静态函数。
  • 不允许联合体拥有引用类型的成员。

非受限联合体:

在C++11中,取消了上述关于联合体的局限性,重新规定任何非引用类型都可以成为联合体的数据成员,这就是非受限联合体。

2. 非受限联合体

2.1 静态类型的成员

非受限联合体中,静态成员有两种:

  • 静态成员变量。
  • 静态成员函数。

EG:

  • 代码:

    #include <iostream>
    using namespace std;
    
    union Test {
        int age;
        long id;
        // int& temp = age;     // error
        // static char c = 'a';     // error
        static char c;
        static int print() {
            // cout << "age = " << age << endl;     // error
            cout << "c=" << c << endl;
            return 1;
        };
    };
    char Test::c = 'a';
    
    int main(void){
        Test t;
        t.age = 1;
        t.id = 2l;
        t.c = 'x';
        cout << "age=" << t.age << endl;
        cout << "id=" << t.id << endl;
        cout << "c=" << t.c << endl;
        cout << "sizeof(union)=" << sizeof(Test) << endl;
        return 0;
    }
    
  • 输出:

    age=2
    id=2
    c=x
    sizeof(union)=8
    
  • 分析:

    • 第7行:错误。即使是非受限联合体,依旧禁止使用用引用作为成员。
    • 第8行:错误。non-const static data numbet must be initilized out of line(非常量静态数据成员的初始化必须在行外),因此在16行进行了初始化。
    • 第11行:错误。静态成员函数只能访问静态成员。
    • 第23行:因为ageid共用一块内存,而id的赋值在age之后,因此age原本的值被id的值覆盖了。
    • 第24行:id的值覆盖了age的值。但id的值并没有被c覆盖,这说明静态成员变量和非静态成员变量使用的不是一块内存。
    • 第28行:说明联合体的大小依旧由非静态成员决定。

2.2 非POD类型成员

在C++11中,如果某受限联合体有一个非POD的成员,而且该成员拥有非平凡的构造函数/拷贝构造哈桑农户/拷贝复制操作符/移动构造函数/移动赋值操作符/析构函数,那么该受限联合体的默认析构函数将会被删除

#include <iostream>
using namespace std;

class Base{
public:
    Base();			// 非平凡的构造函数,因此Base是非POD类型
};

union Test {
	int a;
    Base b;
}

int main(void){    
    // Test t;		// error
    return 0;
}

如上所示,由于联合体Test的默认构造函数已经被删除,因此第15行报错。在这种情况下,我们要为非受限联合体定义构造函数,这时我们需要用到定位放置new操作。

2.2.1 placement new

一般情况下,我们使用new申请空间,这时会从系统的堆(heap)中分配空间,申请所得的空间的位置是根据当时的内存实际使用决定的。但在有些时候,我们要在已经分配的特定内存创建对象,这种操作就是placement new,也就是定位放置 new

语法:

ClassName* ptr = new (address of memory)ClassName(parameter list);

Eg:

  • 代码:

    #include <iostream>
    using namespace std;
    
    class Base{
    public:
        int num;
        Base();			// 非平凡的构造函数,因此Base是非POD类型
    };
    
    int main(void){    
        int n = 100;
        Base* b = new (&n)Base();
        cout << b->num << endl;
        char c = 'a';
        // Base* b1 = new (&c)Base;		// error
        return 0;
    }
    
  • 输出:

    100
    
  • 分析:

    • 第12行:将变量n的地址分配给指针b,此时指针b指向的内存和变量n对应的内存是同一块栈内存。
    • 第13行:输出100是因为b指向的地址就是n的地址,而Base类的成员变量number的歧视地址和Base对象的起始地址是相同的,所以打印出的number的值和n的值相同。
    • 第15行:错误。这是因为Base需要的内存为4个字节,而char的内存只有1个字节,内存大小不够,无法初始化。

特点:

  • 放置new操作,可以在栈/堆上生成对象。
  • 放置new操作并不是新申请了一份空间,而是利用已经申请好的空间,因此该空间大小必须>=类所需空间大小
  • 使用放置new操作创建对象会自动调用对应的构造函数,但是由于对象的空间不会自动释放,因此如果需要释放堆内存则必须显式调用类的析构函数
  • 使用放置new操作,我们可以反复利用同一块堆内存,而不用多次创建销毁堆内存,这样可以提高程序的执行效率。(例如网络通信中数据的接收和发送)。

2.2.2 自定义非受限联合体构造函数

通过使用placement new操作,我们可以自定义非受限联合体构造函数。

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Base {
    public:
        string msg; 
        Base(){
            cout << "non-parameter constructor" << endl;
        }        
        Base(string a): msg(a){
            cout << "parameterized constructor " << endl;
        }
        void print(){
            cout << msg << endl; 
        }
    };
    
    union Test {
        string msg;
        int a;
        Base b;
        Test(){ new (&msg)string; }
        ~Test(){}
    };
    
    int main(void){
        Test t;
        t.msg = "aaa";
        t.b.msg = "bbb";
        cout << t.msg << endl;
        cout << t.b.msg << endl;
        t.b.print();
        return 0;
    }
    
  • 输出:

    bbb
    bbb
    bbb
    
  • 分析:

    • 第23行:为非受限联合体指定构造函数,通过placement new的方式将构造出的对象地址指定到了联合体成员msg上,这样联合体内内部其余非静态成员也可以访问这一块内存了。
    • 第30行:此时联合体成员变量b的位置和msg的位置一致,而变量b是Base类型,Base的成员变量msg的首地址和Base的首地址一致,总结下来就就是&t.b.msg==&t.msg,因此对t.b.msg的修改会覆盖第29行的操作。

2.2.3 匿名的非受限联合体

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

EG:

  • 场景:现在对某个村子进行人口普查,人口登记方式如下:

    • 学生只登记所在学校的编号。
    • 本村除了学生以外的人,登记身份证号。
    • 外来人员,登记:户口所在地、联系方式。
  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    // 外来人口信息
    struct Foreigner
    {
        Foreigner(string s, string ph) : addr(s), phone(ph) {}
        string addr;
        string phone;
    };
    // 人口类型
    enum class Category : char 
    {
        Student, 
        Local, 
        Foreign
    };
    
    // 登记人口信息
    class Person
    {
    private:
        Category type;
        union
        {
            int number;
            string idNum;
            Foreigner foreign;
        };
    public:
        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;
            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;
            }
        }
    };
    
    int main()
    {
        cout << sizeof(string) << endl;
        cout << sizeof(Category) << endl;
        cout << sizeof(Foreigner) << endl;
        cout << sizeof(Person) << endl;
        return 0;
    }
    
  • 输出:

    32
    1
    64
    72
    
  • 分析:

    • 第25行:定义匿名非受限联合体,Person类可以直接访问非受限联合体内部的数据成员
    • 非受限联合体中,numberidNumforeign三者共用一块内存。从而节省了空间。
    • 为什么Person的大小为72,而不是64+1=65?这涉及到C/C++中的字节对齐问题,此处不展开讲述。