Date | Notes | Env |
---|---|---|
2019-08-17 | 首次提交 | clang++ 、macOS 10.14.4 |
Preface
C/C++ 中有许多「奇技淫巧」来让开发者能够以效率更高的方式使用内存,这也是一些高级语言着力去避讳的一点,但这种能够直面内存细节的「踏实感」让我个人觉得非常安心。本文将简单涉及 C/C++ 中的位域(Bit Field)与共用体(Union)的概念。
位域
布尔类型
对于布尔(Boolean)类型,我们都知道其只有两种值:逻辑真或逻辑假。在 C 语言中,其实并没有严格意义上的布尔类型;而在 C++ 中,则诞生了专用的 bool
类型:
#include <iostream>
using namespace std;
int main()
{
bool foo = true;
bool bar = 0;
cout << foo << endl; // 1
cout << bar << endl; // 0
cout << sizeof(foo) << endl; // 1
cout << sizeof(bar) << endl; // 1
return 0;
}
如上,在 C++ 中,true
和非 0
代表布尔类型中的逻辑真,false
和 0
代表逻辑假。bool
类型的值通常在 C++ 中占用一个字节长度(「通常」代表对于不同的编译器结果可能并非完全一致)。
Why
熟悉计算机存储单位的同学都应该知道,bit
正是存储 0
或 1
的单位,那么为什么 bool
要占用一字节而不是一位的内存空间呢?
Because the CPU can’t address anything smaller than a byte.
– Paul Tomblin, StackOverflow
原因是:字节是 CPU 可处理的最小单位。而一个字节为 8 位,当我们使用到多个相关的 bool
类型时,有没有方法可以优化内存的占用呢?
What
位域(也称位段),是一种数据结构,即可以将数据以位为单位存储,节省内存空间。
我们尝试定义一个 FooBarBaz
位域结构,其中 foo
& bar
各占用 1 位,baz
占用 2 位。由于位数之和并没有占满 8 位,整个结构的大小使用一个字节就足够了:
struct
{
char foo : 1;
char bar : 1;
char baz : 2;
} FooBarBaz;
int main()
{
cout << sizeof(FooBarBaz) << endl; // 1
return 0;
}
需要注意的是,在上述的位域结构里,这里我们使用了 char
类型。在 C99 标准中,位域被解释为特定位数的有符号或无符号整型,或者一些其它由实现定义的类型。因此其实这里的类型多是用作描述整个位域结构的大小,而非定义位中存储的数据类型。
所以在小端模式的机器上,char
类型的一个字节中,假设为 0b 0000 0110
,foo
为 0
,bar
为 1
,baz
为 01
(从右往左)。
How
那么到底如何使用位域来存储多个 bool
值呢?
class Demo
{
struct
{
char foo : 1;
char bar : 1;
char baz : 1;
} fooBarBaz;
public:
void setFoo(bool foo)
{
fooBarBaz.foo = foo;
}
bool getFoo()
{
return fooBarBaz.foo;
}
void setBar(bool bar)
{
fooBarBaz.bar = bar;
}
bool getBar()
{
return fooBarBaz.bar;
}
void setBaz(bool baz)
{
fooBarBaz.baz = baz;
}
bool getBaz()
{
return fooBarBaz.baz;
}
};
int main()
{
Demo d = Demo();
d.setFoo(true);
d.setBar(false);
d.setBaz(true);
cout << d.getFoo() << endl; // 1
cout << d.getBar() << endl; // 0
cout << d.getBaz() << endl; // 1
return 0;
}
// LLDB:
// (lldb) p/t true
// (bool) $0 = 0b00000001
// (lldb) p/t false
// (bool) $1 = 0b00000000
// (lldb) p/t d.fooBarBaz
// (Demo::(anonymous struct)) $2 = (foo = 0b0, bar = 0b0, baz = 0b0)
// (lldb) p/t d.fooBarBaz
// (Demo::(anonymous struct)) $3 = (foo = 0b1, bar = 0b0, baz = 0b1)
// (lldb) p 0b00000001 == 0b1
// (bool) $4 = true
// (lldb) p 0b00000000 == 0b0
// (bool) $5 = true
在 C++ 中 true
和 false
是整型十进制中的 1
和 0
,整型在内存中占用一个字节,即 8 位,换算为二进制为 0b 0000 0001
(即 0b1
)和 0b 0000 0000
(即 0b0
),而 foo
、bar
、baz
各占一位,因此当赋值 true
和 false
时,将其各自的位置为 1
或 0
,也就达到了使用一位来存储布尔类型的目的。
共用体
在 C/C++ 等其它语言中都有结构体这种数据结构,我们经常会定义结构体类型来存储多个相关变量,在内存中变量依次占据连续的内存空间。而 C/C++ 中的共用体中也可以存储多个相关变量,不同的是变量在内存中占用同一起点的内存空间。
我们定义一个 FooBar
共同体,其中有两个类型不同的变量,int
类型的 foo
和 char
类型的 bar
。整个共用体结构的大小为其中最大元素的大小,即 int
的 4 个字节。由于共用用一块内存空间,当我们为 foo
赋值 65
时,bar
所占用的一个字节也被赋值了,因此最终输出了 A
:
union FooBar
{
int foo;
char bar;
};
int main()
{
cout << sizeof(FooBar) << endl; // 4
FooBar fbb;
fbb.foo = 65;
cout << fbb.foo << endl; // 65
cout << fbb.bar << endl; // A
return 0;
}
位域与共用体
上面我们简单认识了位域与共用体各自的作用,但位域只能按位赋值,共用体中各自变量内存重叠用处也似乎有局限。那为何不将多个位共用一个变量大小的内存空间呢?
union FooBar
{
char bits;
struct
{
char foo : 1;
char bar : 1;
char baz : 1;
} fooBarBaz;
};
这里我们定义了一个 FooBar
共用体,虽然其中有两个变量,但根据共用体的定义,bits
将和位域 fooBarBaz
共享同一个字节的内存空间;而根据位域的描述,这一个字节的 8 位中,foo
、bar
、baz
各自占用一位。这样的好处是,比单一使用共用体更加直观,外界操作时却只需要访问 bits
即可。
那么如何只访问 bits
就可以操作三个不同的变量呢?我们要首先了解一下 &
和 |
即按位与、按位或运算符。&
按位与,即按位计算,只有同时为 1
时该位结果才为 1
;|
按位或,即按位计算,只有同时为 0
时该位结果才为 0
;另外还要知道左移 <<
、右移 >>
、取反码 ~
运算符,左移即将二进制位整体向左移动,右移反之,取反码则根据 0
和 1
对立互取即可。
通过位域与共用体,以及上述运算符我们就可以实现了:
#define FooMask (1 << 0) // 0b 0000 0001
#define BarMask (1 << 1) // 0b 0000 0010
#define BazMask (1 << 2) // 0b 0000 0100
class Demo
{
FooBar fooBar;
public:
void setFoo(bool foo)
{
if (foo)
{
fooBar.bits |= FooMask;
}
else
{
fooBar.bits &= ~FooMask;
}
}
bool getFoo()
{
return fooBar.bits & FooMask;
}
void setBar(bool bar)
{
if (bar)
{
fooBar.bits |= BarMask;
}
else
{
fooBar.bits &= ~BarMask;
}
}
bool getBar()
{
return fooBar.bits & BarMask;
}
void setBaz(bool baz)
{
if (baz)
{
fooBar.bits |= BazMask;
}
else
{
fooBar.bits &= ~BazMask;
}
}
bool getBaz()
{
return fooBar.bits & BazMask;
}
};
int main(int argc, const char * argv[]) {
cout << sizeof(FooBar) << endl; // 1
Demo d = Demo();
d.setFoo(true);
d.setBar(false);
d.setBaz(true);
cout << d.getFoo() << endl; // 1
cout << d.getBar() << endl; // 0
cout << d.getBaz() << endl; // 1
return 0;
}
当然,第一次使用可能比较抽象,我们以一对 getter & setter 为例说明下:
void setFoo(bool foo)
{
if (foo) // 当 foo 为真时,我们需要将 foo 的二进制位置为 1
{
// FooMask 是 foo 的掩码,即 1 << 0,0b 0000 0001
// 此时我们需要仅将 bits 的最后一位置为 1,而不能更改其它位
// 因此这里选择需要与掩码逻辑或,保证其它位和 0 运算都能得到其原先值并将 foo 置为 1
fooBar.bits |= FooMask;
}
else // 当 foo 为假时,我们需要将 foo 的二进制位置为 0
{
// 此时我们需要仅将 bits 的最后一位置为 0,而不能更改其它位
// 因此首先需要取掩码的反码,并进行逻辑与,保证其它位不变并将 foo 置为 0
fooBar.bits &= ~FooMask;
}
}
bool getFoo()
{
// 获取值时,我们仅需要特定位即可,因此与掩码逻辑与可将其它位置为 0,并将需要的位取出
return fooBar.bits & FooMask;
}