Deep C Dives: Bits!
Written by Mike James   
Wednesday, 09 April 2025
Article Index
Deep C Dives: Bits!
The Bitwise Operators
A Single Function To Write Bits

The Bitwise Operators

C has a number of operators designed to allow you to perform bit manipulation. There are four bitwise operators:

AND

&

OR

|

XOR (exclusive or)

^

NOT

~

 

As you would expect, the NOT operator has the highest priority.

Notice that there are also corresponding Boolean operators, &&, || and ! which only work with Boolean values – with zero as false and anything non-zero as true - and not with bit patterns.

The bitwise operators work with integer types. For example:

int a = 0xF0;
int b = 0xFF;
int c = ~a & b;
printf("%X\n",c)

This first works out the bitwise NOT of a, i.e. 0F. This is then bitwise ANDed with b, i.e. 0F & FF which is F. The %X format specifier prints the value in hex. You can use %x for lower case and %d for decimal.

Signed v Unsigned

Now we come to a subtle and troublesome point. Bitwise operators are only uniquely defined for unsigned values. The reason is that unsigned values have an unambiguous representation in binary and hence the operations are well-defined in terms of the values the bit patterns represent.

That is:

unsigned int value=5;

is always 0101 and hence:

value | 0x2

is not only always 0111, but also always represents +7.

The same is not true for signed values simply because the way in which negative numbers are represented isn’t fixed. The most common representation is two’s complement, but this is not part of the C language standard, before C23, and so logical operations on unsigned numbers are implementation-dependent. The C23 standard changes this and makes two's-complement mandatory for negative values, but it will be some time before this comes widely into effect. Notice that this does not mean logical operations on signed values are undefined behavior – if this was the case most C programs would stop working properly. In addition “implementation-dependent” has one fairly consistent meaning:

take the bit pattern that represents the value and perform the specified logical operation as if everything involved was an unsigned value.

This is the only sane way to deal with the problem and it is exactly what you would expect.

For example:

signed int value = -5;

if the machine uses two's-complement representation then the bit pattern is:

11111111111111111111111111111011‬

and:

value | 0x4

changes the third bit to 1, i.e. -1 in two’s-complement.

As long as you assume a specific representation for the signed value then the logical operation is usually well defined as the bitwise application of the operator on the bit pattern. It is the representation that is implementation-dependent and not the operation.

 

Masks

Bitwise operations are the key to accessing individual bits. We usually think of this as setting or unsetting a particular bit or testing its state. You can set and unset bits using another value, usually called a mask, that defines the bits to be changed. In this sense the bits in the mask act as the addresses of the bits that you want to work with. For example, if you only want to change the first (least significant) bit then the mask would be 0x01. If you wanted to change the first and second bits the mask would be 0x03 and so on. That is, the mask has the bits you want to access set to one with all other bits zero.

The mask gives the address of the bits you want to work with and what you do to those bits depends on the operation you use to combine the mask with the value. For example:

value | mask;

returns a bit pattern with the same bits set to one as in the mask. Notice that the bits that the mask doesn't specify, i.e. are zero in the mask, are left at their original values.

If you OR the mask you set the addressed bits to one and leave all of the others unchanged. For example:

int mask = 0x03; 
int value = 0xFFF0;
int result = value | mask;
printf("%X\n",result);

sets result to 0xFFF3, i.e. it sets the first (least significant) two bits.

Similarly if you use:

value & ~mask;

then the bits specified in the mask are set to zero, or are “unset” if you prefer this jargon. Notice that you have to apply a NOT operator to the mask. For example:

int mask = 0x03;
int value = 0xFFFF;
int result = value & ~mask;
printf("%X\n",result);

sets result to 0xFFFC, i.e. it unsets the first two bits.

As well as setting and unsetting particular bits, you might also want to "flip" the specified bits, i.e. negate them so that if the bit was a one it is changed to a zero and vice versa. You can do this using the XOR, exclusive or, operator:

value ^ mask

which flips the bits specified by the mask. For example:

int mask = 0x03;
int value = 0xFFFF;
int result = value ^ mask;
printf("%X\n",result);

sets result to 0xFFFC because it changes the lower two bits from ones to zeros. Again, bits not specified by mask are unaffected.

If you want to update the flag rather than derive a new result, you can use:

&=
|= 

and:

^= 

to perform the update directly. For example:

int value = 0xFFFF;
int value ^= 0x03 ;
printf("%X\n",value);

To summarize:

  • Create a mask that has 1 at the position of each bit you want to change.

  • To set the designated bits OR the mask with the value.

  • To unset the designated bits AND the NOT of the mask with the value.

  • To flip the designated bits XOR the mask with the value.

Bits in the mask that are 0 are unaffected by any of the operations.

Cdive180



Last Updated ( Wednesday, 09 April 2025 )