目录
C提供按位逻辑运算符和移位运算符。
1、按位逻辑运算符
4个按位逻辑运算符都用于整形数据,包括char。之所以叫作按位运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。
1.1 二进制反码或按位取反:~
一元运算符~把1变为0,把0变为1。
~(10011010) //表达式
(01100101) //结果值
在二进制中,00000010表示2,那么取反的值为11111101,即253,该运算符不会改变变量的值。
如果要把val的值改为~val,可以用下面的语句:
val = ~val;
1.2 按位与:&
二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1(从真/假方面看,只有当两个位都为真时,结果才为真)。
(10010011) & (00111101) //表达式
(00010001) //结果值
C有一个按位与和赋值结合的运算符: &=。下面两条语句产生的最终结果相同”
val &= 0337;
val = val & 0337;
1.3 按位或:|
二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1(从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真)。
(10010011) | (00111101) //表达式
(10111111) //结果值
//效果相同
val |= 0377;
val = val | 0377;
1.4 按位异或:^
二元运算符^逐位比较两个运算对象。如果两个运算对象中相应的位一个为1(只有一个1),结果为1(从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个同为1,那么结果为真)。
(10010011) ^(00111101) //表达式
(10101110) //结果值
//效果相同
val ^= 0377;
val = val ^ 0377;
2、用法
2.1 掩码(&)
按位与运算符常用于掩码/所谓掩码指的是一些设置位开(1)或关(0)的位组合。例如,假设定义符号常量MASK为2(即,二进制形式为00000010),只有1号位为1,其他位都是0。下面的语句:
flags = flags & MASK;
把flags中除1号位以外的所有位都设置位0,因为使用按位与运算符(&)任何位与0组合都得0。1号位的值不变(如果1号位是1,那么1&1得1;如果1号位是0,那么0&1也得0)。这个过程叫作“使用掩码”,因为掩码中的0隐藏了flags中相应的位。
2.2 打开位(设置位)(|)
以上一节的flags和MASK为例。下面的语句:
flags = flags | MASK;
把flags的1号位设置为1,且其他位不变。因为使用 | 运算符,任何位与0组合,结果都为本身;任何位与1组合,结果都为1。
2.3 关闭位(清空位)(& ~)
和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位。
flags = flags & ~MASK;
由于MASK除1号位为1以外,其他位全为0,所以~MASK除1号位为0以外,其他位全为1。使用&,任何位与1组合都得本身,所以这条语句保持除1号位以外的其他各位不变。
例如,假设flags是00001111,MASK是10110110.下面的表达式:
flags & ~MASK
//即是:
(00001111) & ~(10110110) //表达式
//其结果为:
(00001001) //结果值
MASK中为1的位在结果中都被设置(清空)为0.flags中与MASK为0 的位相应的位在结果中都未改变。
2.4 切换位(^)
切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或运算符(^)切换位。
假设b是一个位(1或0),如果b为1,则1^b为0;如果b为0,则1^b为1。另外,无论b为1还是0,0^b均为b。 因此,如果使用 ^ 组合一个值和一个掩码,将切换该值与MASK为1的位相对应的位,该值与MASK为0的位相对应的位不变。
flags = flags ^ MASK;
例如,假设flags是00001111,MASK是10110110。
flags ^ MASK
//即是:
(00001111) ^ (10110110) //表达式
//其结果为:
(10111001) //结果值
2.5 检查位的值
例如,检查flags中1号位是否被设置为1?可以这样比较:
if ((flags & MASK) == MASK)
puts("wow!");
必须先覆盖flags中的其他位,只用1号位和MASK比较。
为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同。
3、移位运算符
3.1 左移:<<
左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。
(10001010) << 2 //表达式
(00101000) //结果值
该操作产生了一个新的位值,但是不改变其运算对象。例如,假设stonk为1,那么stonk<<2为4,但是stonk本身不变,仍为1.可以使用左移赋值运算符(<<=)来更改变量的值。
3.2 右移:>>
右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢失。对于无符号了类型,用0填充空出的位置;对于有符号类型,其结果取决于机器。
对于无符号的值:
(10001010) >> 2
(00100010)
同左移相同,也可以用(>>=)将其左侧的变量向右移动指定数量的位数。
移位运算符针对2的幂提供快速有效的乘法和除法。
number << n //number乘以2的n次幂
number >> n //如果number为非负,则用number除以2的n次幂
4、位字段
操控位的第2种方法是位字段。位字段是一个signed int 或unsigned int 类型变量中的一组相邻的位。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。
struct
{
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
}prnt;
根据该声明,prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:
prnt.itals = 0;
prnt undln = 1;
由于每个字段恰好为1位,所以只能为其赋值1或0。但是,要确保所赋的值不超过字段可容纳的范围。
如果声明的总位数超过了一个unsigned int 类型的大小会怎么样?会用到下一个unsigned int 类型的存储位置。一个字段不允许跨越两个unsigned int 之间的边界。编译器会自动移动跨界的字段,保持unsigned int 的边界对齐。一旦发生这样的情况,第1个unsigned int 中会留下一个未命名的“洞”。
可以用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐:
struct
{
unsigned int field1 : 1;
unsigned int : 2;
unsigned int field2 : 1;
unsigned int : 0;
unsigned int field3 : 1;
}stuff;
这里,在stuff.field1和stuff.field2之间,有一个2位的空隙;stuff.field3将存储在下一个unsigned int中。
5、位字段和按位运算符
在同类型的编程中问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。可以通过一个联合吧结构方法和位方法放在一起。
假定声明了 struct box_props 类型,然后这样声明联合:
union Views //把数据看作结构或unsigned int类型的变量
{
struct box_props st_view;
unsigned shord us_view;
};
在某些系统中,unsigned int 和box_props 类型的结构都占用16位内存。但是,在其他系统中是32位。无论哪种情况,通过联合,都可以使用 st_view 成员吧一块内存看作是一个结构,或者使用 us_view 成员吧相同的内存块看作是一个unsigned short。联合只允许初始化第1个成员,所以初始化值必须与结构相匹配。
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
//位字段符号常量
//边框线样式
#define SOLID 0
#define DOTTED 1
#define DASHED 2
//三原色
#define BLUE 4
#define GREEN 2
#define RED 1
//混合颜色
#define BLACK 0
#define YELLOW (RED | GREEN)
#define MAGENTA (RED | BLUE)
#define CYAN (GREEN | BLUE)
#define WHITE (RED | GREEN | BLUE)
//按位方法中用到的符号常量
#define OPAQUE 0x1
#define FILL_BLUE 0x8
#define FILL_GREEN 0x4
#define FILL_RED 0x2
#define FILL_MASK 0xE
#define BORDER 0x100
#define BORDER_BLUE 0x800
#define BORDER_GREEN 0x400
#define BORDER_RED 0x200
#define BORDER_MASK 0xE00
#define B_SOLID 0
#define B_DOTTED 0x1000
#define B_DASHED 0x2000
#define STYLE_MASK 0x3000
const char* colors[8] = { "black","red","green","yellow","blue","magenta","cyan","white" };
struct box_props
{
bool opaque : 1;
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1;
unsigned int border_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
};
union Views//把数据看作结构或unsigned short 类型的变量
{
struct box_props st_view;
unsigned short us_view;
};
void show_settings(const struct box_props* pb);
void show_settings1(unsigned short);
char* itobs(int n, char* ps);
int main(void)
{
//创建Views联合,并初始化initialize struct box view
union Views box = { {true,YELLOW,true,GREEN,DASHED} };
char bin_str[8 * sizeof(unsigned int) + 1];
printf("Original box settings:\n");
show_settings(&box.st_view);
printf("\nBox settings using unsigned int view:\n");
show_settings1(box.us_view);
printf("bits are %s\n", itobs(box.us_view, bin_str));
box.us_view &= ~FILL_MASK; //把表示填充色的位清0
box.us_view |= (FILL_BLUE | FILL_GREEN);//重置填充色
box.us_view ^= OPAQUE; //切换是否透明的位
box.us_view |= BORDER_RED; //错误的方法
box.us_view &= ~STYLE_MASK; //把样式的位清0
box.us_view |= B_DOTTED; //把样式设置为点
printf("\nModified box settings:\n");
show_settings(&box.st_view);
printf("\nBox settings using unsigned int view:\n");
show_settings1(box.us_view);
printf("bits are %s\n", itobs(box.us_view, bin_str));
return 0;
}
void show_settings(const struct box_props* pb)
{
printf("Box is %s.\n", pb->opaque == true ? "opaque" : "transparent");
printf("The fill color is %s.\n", colors[pb->fill_color]);
printf("Border %s.\n", pb->show_border == true ? "showm" : "not shown");
printf("The border color is %s.\n", colors[pb->border_color]);
printf("The border style is ");
switch (pb->border_style)
{
case SOLID:
printf("solid.\n");
break;
case DOTTED:
printf("dotted.\n");
break;
case DASHED:
printf("dashed.\n");
break;
default:
printf("unknown type.\n");
}
}
void show_settings1(unsigned short us)
{
printf("box is %s.\n", (us & OPAQUE) == OPAQUE ? "opaque" : "transparent");
printf("The fill color is %s.\n", colors[(us >> 1) & 07]);
printf("Border %s.\n", (us & BORDER) == BORDER ? "showm" : "not shown");
printf("The border style is ");
switch (us & STYLE_MASK)
{
case B_SOLID:
printf("solid.\n");
break;
case B_DOTTED:
printf("dotted.\n");
break;
case B_DASHED:
printf("dashed.\n");
break;
default:
printf("unknown type.\n");
}
printf("The border color is %s.\n", colors[(us >> 9) & 07]);
}
char* itobs(int n, char* ps)
{
int i;
const static int size = CHAR_BIT * sizeof(int);
for (i = size - 1; i >= 0; i--, n >>= 1)
{
ps[i] = (01 & n) + '0';
ps[size] = '\0';
return ps;
}
}
6、对齐特性(C11)
_Alignof运算符给出一个类型的对齐要求,在关键字_Alignof后面的圆括号中写上类型名即可:
size_t d_align = _Alignof(float);
假设d_align 的值是4,意思是 float 类型对象的对齐要求是4.也就是说,4是存储该类型值相邻地址的字节数。一般而言,对齐值都应该是2的非负整数次幂。较大的对齐值被称为stricter或stronger 较小的对齐值被称为weaker。
可以使用 _Alignas 说明符指定一个变量或类型的对齐值。但是,不应该要求该值小于基本对齐值。例如,如果 float 类型的对齐要求是4,不要请求其对齐值是1或2.搞说明符用作声明的一部分,说明符后面的圆括号内包含对齐值或类型:
_Alignas(double) char c1;
_Alignas(8) char c2;
C11在 stdlib.h 库还添加了一个新的内存分配函数,用于对齐动态分配的内存。该函数的原型如下:
void *aligned_alloc(size_t alignment, size_t size);
第一个参数代表指定的对齐,第2个参数是所需的字节数,其值应是第1个参数的倍数。与其他内存分配函数一样,要使用 free() 函数释放之前分配的内存。