现代C++新特性 非受限联合类型

16 篇文章 0 订阅
7 篇文章 0 订阅

   文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载 

1.联合类型在C++中的局限

在编程的问题中,用尽量少的内存做尽可能多的事情一直都是一个重要的课题。C++中的联合类型(union)可以说是节约内存的一个典型代表。因为在联合类型中多个对象可以共享一片内存,相应的这片内存也只能由一个对象使用,例如:

#include <iostream> 

union U {
    int x1;  
    float x2;
};

int main(int argc, char** argv)
{
    U u;
    u.x1 = 5;
    cout << u.x1 << endl;  
    cout << u.x2 << endl;

    u.x2 = 5.0;
    cout << u.x1 << endl; 
    cout << u.x2 << endl;
    return 0;
}

在上面的代码中联合类型U里的成员变量x1和x2共享同一片内存,所以修改x1的值,x2的值也会发生相应的变化,反之亦然。不过需要注意的是,虽然x1和x2共享同一片内存,但是由于CPU对不同类型内存的理解存在区别,因此即使内存相同也不能随意使用联合类型的成员变量,而是应该使用之前初始化过的变量。像这样多个对象共用一片内存的情况在内存紧缺时是非常实用的。不过令人遗憾的是,过去的联合类型在C++中的使用并不广泛,因为C++中的大多数对象不能成为联合类型的成员。过去的C++标准规定,联合类型的成员变量的类型不能是一个非平凡类型,也就是说它的成员类型不能有自定义构造函数,比如:

union U {
    int x1;   
    float x2;  
    string x3;
};

上面的代码是无法通过编译的,因为x3存在自定义的构造函数,所以它是一个非平凡类型。但事实上,面向对象的编程中一个好的类应该隐藏内部的细节,这就要求构造函数足够强大并正确地初始化对象的内部数据结构,而编译器提供的构造函数往往不具备这样的能力,于是大多数情况下,我们会为自己的类添加一个好用的构造函数,但是这种良好的设计却造成了这个类型无法在联合类型中使用。基于这些问题,C++委员会在新的提案当中多次强调“我们没有任何理由限制联合类型使用的类型”。在这份提案中有一段话非常好地阐述了C++的设计理念,同时也批判了联合类型的限制对这种理念的背叛,这段话是这样说的:当面对一个可能被滥用的功能时,语言的设计者往往有两条路可走,一是为了语言的安全性禁止此功能,另外则是为了语言的能力和灵活性允许这个功能,C++的设计者一般会采用后者。但是联合类型的设计却与这一理念背道而驰。这种限制完全没有必要,去除它可以让联合类型更加实用。

回味这段话,C++的设计确实一直遵从这样的理念,我们熟悉的指针就是一个典型的代表!

2. 使用非受限联合类

为了让联合类型更加实用,在C++11标准中解除了大部分限制,联合类型的成员可以是除了引用类型外的所有类型。不过这样的修改引入了另外一个问题,如何精确初始化联合类型成员对象。这一点在过去的联合类型中不是一个问题,因为对于平凡类型,编译器只需要对成员对象都执行编译器提供的默认构造即可,虽然从同一内存多次初始化的角度来说这是不正确的,但是从结果上看没有任何问题。现在情况发生了变化,由于允许非平凡类型的存在,对所有成员一一进行默认构造明显是不可取的,因此我们需要有选择地初始化成员对象。实际上,让编译器去选择初始化本身也是不合适的,这个事情应该交给程序员来做。基于这些考虑,在C++11中如果有联合类型中存在非平凡类型,那么这个联合类型的特殊成员函数将被隐式删除,也就是说我们必须自己至少提供联合类型的构造和析构函数,比如:

#include <iostream> 
#include <string> 
#include <vector> 

union U {
    U() {}        // 存在非平凡类型成员,必须提供构造函数 
    ~U() {}       // 存在非平凡类型成员,必须提供析构函数   
    int x1;
    float x2;
    string x3;
    vector<int> x4;
};

int main(int argc, char** argv)
{
    U u;
    u.x3 = "hello world";
    cout << u.x3 << endl;
    return 0;
}

在上面的代码中,由于x3和x4的类型string和vector是非平凡类型,因此U必须提供构造和析构函数。虽然这里提供的构造和析构函数什么也没有做,但是代码依然可以成功编译。不过请注意,能够编译通过并不代表没有问题,实际上这段代码会运行出错,因为非平凡类型x3并没有被构造,所以在赋值操作的时候必然会出错。现在修改一下代码:

#include <iostream> 
#include <string> 
#include <vector> 

union U {
    U() : x3() {}
    ~U()
    {
        x3.~basic_string();
    }
    int x1;
    float x2;
    string x3;
    vector<int> x4;
};

int main(int argc, char** argv)
{
    U u;
    u.x3 = "hello world";
    cout << u.x3 << endl;
    return 0;
}

在上面的代码中,我们对联合类型U的构造和析构函数进行了修改。其中在构造函数中添加了初始化列表来构造x3,在析构函数中手动调用了x3的析构函数。前者很容易理解,而后者需要注意,联合类型在析构的时候编译器并不知道当前激活的是哪个成员,所以无法自动调用成员的析构函数,必须由程序员编写代码完成这部分工作。现在联合类型U的成员对象x3可以正常工作了,但是这种解决方案依然存在问题,因为在编写联合类型构造函数的时候无法确保哪个成员真正被使用。具体来说,如果在main函数内使用U的成员x4,由于x4并没有经过初始化,因此会导致程序出错:

#include <iostream> 
#include <string> 
#include <vector> 

union U {
    U() : x3() {}
    ~U()
    {
        x3.~basic_string();
    }

    int x1;
    float x2;
    string x3;
    vector<int> x4;
};

int main(int argc, char** argv)
{
    U u;
    u.x4.push_back(58);
    return 0;
}

基于这些考虑,我还是比较推荐让联合类型的构造和析构函数为空,也就是什么也不做,并且将其成员的构造和析构函数放在需要使用联合类型的地方。让我们继续修改上面的代码:

#include <iostream> 
#include <string> 
#include <vector> 

union U {
    U() {}
    ~U() {}
    int x1;
    float x2;
    string x3;
    vector<int> x4;
};
int main(int argc, char** argv)
{
    U u;
    new(&u.x3) string("hello world");
    cout << u.x3 << endl;

    u.x3.~basic_string();
    new(&u.x4) vector<int>;
    u.x4.push_back(58);
    cout << u.x4[0] << endl;
    u.x4.~vector();
    return 0;
}

请注意,上面的代码用了placement new的技巧来初始化构造x3和x4对象,在使用完对象后手动调用对象的析构函数。通过这样的方法保证了联合类型使用的灵活性和正确性。后简单介绍一下非受限联合类型对静态成员变量的支持。联合类型的静态成员不属于联合类型的任何对象,所以并不是对象构造时被定义的,不能在联合类型内部初始化。实际上这一点和类的静态成员变量是一样的,当然了,它的初始化方法也和类的静态成员变量相同:

#include <iostream> 

union U {
    static int x1;
};

int U::x1 = 42;

int main(int argc, char** argv)
{
    cout << U::x1 << endl;
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
相比于 C++98 带来的面向对象的革命性,C++11 带来的 XIII  却并“翻天覆地”式的改变。很多时候,程序员保持着“C++98 式”的观点来看待 C++11 代码也同样是合理的。因为在编程思想上,C++11 依然遵从了一贯的面向对象的思想,并深 入加强了泛型编程的支持。从我们的观察来看,C++11 更多的是对步入“成熟稳重”的中年 时期的 C++ 的一种改造。比如,像 auto 类型推导这样的特性,展现出的是语言的亲和力 ; 而右值引用、移动语义的特性,则着重于改变一些使用 C++ 程序库时容易发生的性能不佳的 状况。当然,C++11 中也有局部的创,比如 lambda 函数的引入,以及原子类型的设计等, 都体现了语言与时俱进的活力。语言的诸多方面都在 C++11 中再次被锤炼,从而变得更加合 理、更加条理清晰、更加易用。C++11 对 C++ 语言改进的每一点,都呈现出了经过长时间技 术沉淀的编程语言的特色与风采。所以从这个角度上看,学习 C++11 与 C++98 在思想上是 一脉相承的,程序员可以用较小的代价对 C++ 的知识进行更换代。而在现实中,只要修改 少量已有代码(甚至不修改) ,就可以使用 C++11 编译器对旧有代码进行升级编译而获得 标准带来的好处,这也常具有实用性。因此,从很多方面来看,C++ 程序员都应该乐于升 级换代已有的知识,而学习及使用 C++11 也正是大势所趋。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神奇的小强

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值