Operators in C
Following up my notes on Data Types and Variables in C here are notes on operators in C.
An operator is a symbol that represents a mathematical or logical operation. An operator effects operands.
C provides a number of operators.
Some arithmetic operators include,
+

*
/
%
%
is the most exciting of the list, it is called modulo and it returns the remainder after division. Of note, modulo can only be used on integers
while the others can be used on any number.
This group of arithmetic operators are called “binary arithmetic operators.”
There are also unary operators, or operators that work on just 1 operand.
++

These increment or decrement the value of the operand by 1. These work on both integers and floating point numbers.
Their behavior changes based on their position relative to the operand. E.g.
b = a++
Postincrement A by 1.
b = ++a
Here, preincrement.
int a, b;
a = 0;
b = a++;
// => a is 1
// => b is 0
a = 0;
b = ++a;
// => a is 1
// => b is 1
Preincrement is preformed before assignment to a new variable, while postincrement is preformed after assignment to a new variable.
Next up, assignment operators, or, relational operators.
These are operators that check for a relationship between two operands and return either 1 (true), or 0 (false). They always return int
s, but can compare numbers or characters.
These operators include,
==
!=
>
<
>=
<=
Logical operators come from boolean algebra.
&&

!
&&
is an operation of conjunction — intersection.

is an operation of union.
!
is an operation of exclusion.
In boolean algebra variables can only be assigned either true or false values. In C, 1 or 0.
Truth table!
x  y  x && y  x  y  !x 

0  0  0  0  1 
1  0  0  1  0 
0  1  0  1  1 
1  1  1  1  0 
Each row shows a possible combination of values.
!
is unique among the the logical operators because it is actually a unary operator, taking only 1 operand.
The result of a boolean operator is always an int
, either 0 or 1. 0 is false. 1 (or any non0 number) is true.
boolean operators pair nicely with the bool
data type if you are using C99 or newer.
Bitwise operators allow for the direct manipulation of bits. This is useful when working with memory addresses. They’re a wee bit complicated, but allow for extremely efficient operations.
The operators include,
&

^
~
<<
>>
These match the logical operators, which preform boolean operation on an entire number. Bitwise operators also preform boolean operation, but rather than doing so on a single number, they do so on every single bit of the operands…bit by bit.
This means that if you had 2 operands, A
and B
like so,
a = 10101010
b = 00001111
Where c = a & b
The bitwise & would check against each bit so that c = 000010101
. This is, at first blush, admittedly a little baffling. To make matters a wee bit more confusing, shifting focus to >>
and <<
, the right and left shift operators.
b = a >> n
This shifts the bits in a
to the “right” by n
steps, so b = a >> 3
would shift the bits of a
by 3 steps.
If a
started as 11001100
it would finish as 00011001
, with 0’s being introduced to the left as the bits are shifted over. Another way of thinking about this is that b
is a
, but missing the 3 least significant bits.
1 << 0 = 1
1 << 1 = 2
1 << 2 = 4
1 << 3 = 8
The result is the same as multiplying the leading operand (here, 1) by 2 for each shifted bit, e.g. 1 << 1 = 2
can be thought of as 1 * 2
, and 1 << 2 = 4
thought of as 1 * 2 * 2
, a*2^n
.
On the other hand, shifting right is dividing by 2 for each shifted bit, a/2^n
.
#include <stdio.h>
#include <stdint.h>
int main (void)
{
uint8_t a = 12; // 0000 1100
uint8_t b = 5; // 0000 0101
// A & B > 0000 0100 = 4
// A  B > 0000 1101 = 13
// A ^ B > 0000 1001 = 9
// A << 1 > 0001 1000 = 24
// A >> 1 > 0000 0110 = 6
printf("A = %d\n", a);
printf("B = %d\n", b);
printf("A & B = %d\n", a & b);
printf("A  B = %d\n", a  b);
printf("A ^ B = %d\n", a ^ b);
printf("A << 1 = %d\n", a << 1);
printf("A >> 1 = %d\n", a >> 1);
return 0;
}
Bitwise operators are frequently used with bitmasks.
Using the bitwise &
operator, two different bitmasks can be defined, one for bit clearing (if the bitmask is 0) and one for bit testing (if the bitmask is 1).
The bitwise 
operator allows for a bitmask useful for bit setting, where, if a bitmask is 1, the result is 1.
Finally, the bitwise ^
operator allows for a bitmask that works as a toggle, switching the value of a bit from 1 to 0 or 0 to 1.
But how does this actually work? How can one actually preform bit manipulation? What if you’d like to set the Nth bit — set the bit in the 6th position to 1, for instance.
To do this use the bitwise 
with a bitmask set to 1 in the 6th position of the bit.
result = date  0b01000000; // the mask is a binary literal
// where the bit in the 6th position is set to 1
Of course…this is grossly impractical and a pain in the butt to read.
Instead, create a bitmask using the <<
operator!
result = data  (1 << 6);
This’ll set the bit in position 6 to 1, and all others to 0.
A similar process works for clearing the Nth bit. To set the bit at the 5th position to 0 use the bitwise &
with a mask set to 0 for the bit we’d like to reset.
result = data & ~(1 << 5);
Here, making use of the <<
operator again, creating a bitmask set to 1 everywhere but the 5th bit.
NOTE! Since ~
is outside of the parentheses inside of which the left shift is calculated the compliment operation happens after the bits are shifted. Shift followed by compliment.
Next, how to select a subset of bits, e.g. select the bits from position 3  5.
To start, shift the bits from position 3 to position 0. In other words, shift right by 3 bits.
Now to select the bits in positions 0, 1, and 2. Bitwise &
with a bitmask where those same positions are all set to 1 will allow for this.
Put together, this looks like this,
result = (data >> 3) & 0b111;
To be totally honest I find a lot of this bitlevel stuff baffling. In the programming stuff I do on the day to day I’ve never had to reach for these tools — this may be because they aren’t needed for what I do, or because I don’t understand them enough to even realize when I should be reaching for them 🤷♂️
Assignment operators are used to assign a value to a variable. The simplest is =
. There are other assignment operators, though, like the compound assignment operators,
+=
=
*=
/=
%=
a += b
is the same as a = a + b
, and a *= b
is the same as a = a * b
and so on.
There are also compound forms of the bitwise operators,
&=
=
^=
>>=
<<=
These work the same as the previous compound operators.
An entirely different beast when it comes to operators is the sizeof
operator. The sizeof
operator returns the number of bytes an operand takes up in memory. The size is determined by the operand’s type, and is known at compiletime, not runtime. The result will be an integer constant, and the operand can be a variable, a basic or a derived datatype, or even an expression.
The type of the returned data is of type size_t
. The number of bytes available to size_t
varies from compiler to compiler. The sizeof
operator is useful because it allows one to avoid the hardcoding of certain fixed values into a program, instead, they can be determined from the data itself. This leads to more portable code.
No conversation about operators would be complete without discussing type conversion. C is strongly typed, but that doesn’t mean data is stuck forever and always as a specific type after initial declaration. The cast operator allows for the conversion of one data type to another. Be warned, sometimes casting from one type to another can result in a loss of some information because not all data types have the same size in memory, e.g. a char
is teeny tiny, while a long long
is pretty big.
Sometimes type conversion happens implicitly, the compiler takes the wheel! I think the most common scenario for this is integer promotion, where a char
, for instance, is “promoted” to an integer during certain mathematical operations. Similarly, when assigning a short int
to a long int
there is an implicit conversion, making the data type “wider.”
There is a hierarchy to data conversion,
int // smallest
unsigned int
long int
unsigned long int
long long int
unsigned long long int
float
double
long double // largest
Implicit conversions can only apply “upwards,” e.g. a data type can only be cast to a “larger” type. To convert “downwards” an explicit conversion must be used.
A totally fabricated example of implicit conversion:
#include <stdio.h>
int main()
{
int a = 1;
long int b = 2;
double c = 3.3;
b = b + a; // implicit conversion: a is promoted to a long int
c = c * b; // implicit conversion: b is promoted to a double
return 0;
}
But wait! There are more operators!?
The ternary operator, the reference operator, the dereference operator, the array reference operator, the member selection operator, and the member selection operator for pointers!
☠️☠️☠️☠️☠️
Operators have precedence rules, too. They are strict, and never changing. The means that an expression is always evaluated in the same way, no matter what.
Parenthesis can be added to help make precedence a bit more obvious and to control precedence, too, because items in parenthesis get evaluated first. So, one can change evaluation order by adding parenthesis.
Here is a link to a chart that describes operator precedence in C.