Back to main

Operators in Detail

All of the C language operators are covered, including many example uses. As well as a full description of their functionality, some style issues are introduced.

A summary of all of the operators available to C was introduced in the section on language constructs, but they now deserve a fuller exposition.

Operator Precedence and Associativity

If more than one operator appears in an expression, the order of evalulation is determined by the operators' level of precedence. For example, 5+3*2 is evaluated as 5 + (3*2) because the precedence of the multiplication operator, 3, causes it to be evaluted before the addition operator (precedence 4).

(Parentheses) have amongst the highest precedence, so to change the order of evaluation given above, one can simply write (5+3) * 2 as expected.

Where operators of the same precedence occur in an expression, the order of evaluation is determined by rules of associativity. Most operators associate from left to right, so a + b - c means (a + b) - c because + and - have the same precedence, and are left-to-right associative. Direction of association is important. If the assignment operator associated left-to-right, it wouldn't be possible to write x = y = 2 because that would be interpreted as (x=y) = 5 which is a syntax error (because x=y is not an lvalue which is to say one cannot assign to it). Fortunately, the right-to-left association of = makes sure the result is x = (y=5), which is fine.

Tuplicity of Operators

Most operators in C are binary, infix operators. This means that they take exactly two arguments, and the operator goes inbetween them. We write 2 + 3, not + 2 3 (because + is not a prefix operator) nor 2 3 + (because + is not a postfix operator).

Unary -, unary +, bitwise and logical "not" (~, !), reference and dereference (&, *), increment and decrement (++, --), sizeof, and typecast operators are all unary, which is to say they take exactly one argument.

C has only one ternary operator: the conditional operator. It is sometimes referred to, rather imprecisely, as "the ternary operator".

Arithmetic Operators

Apart from the four obvious arithmetic operators *, /, + and -, there is a remainder operator %. All of these can be appiled to integers and floating-point values: the normal type-promotion rules apply.

Example Uses of Arithmetic Operators

9 / 5;         /* Result is 1 because both args are integer */
(double)9 / 5; /* Result is 1.8... */
9 / 5.0        /* ...because at least one arg is floating point */
9 % 5          /* Result is 4 */

Increment and Decrement

It is often required to increment or decrement a numerical value by 1. Because it becomes very tedious to write expressions like a = a + 1, the increment and decrement operators are made available. This permits the above to be abbreviated to ++a or a++. Similarly, to reduce the value of a variable, one can write --a or a--.

Because the increment and decrement operators really mean "increase to the next greatest value" or "reduce to the next lower value", they cannot be applied to floating point values. Instead, one can write x -= 1.0 to reduce the value held in a floating point variable by 1.0 (see Assignment below).

The increment and decrement operators can be used before the variable or after it (i.e. as prefix or postfix operators). There is a subtle difference which becomes very important when they are used as part as a larger expression.

In the postfix form, the value of the variable is used and then its value is changed. In the prefix form, the value of the variable is altered before it is used in evaluating the rest of the expression. The following example might help.

Assignment Operators

Many computer languages have only one assignment operator, usually = (like C) or :=. As well as simple assignment, C offers a shorthand for all of the binary operators when they are used directly with an assignment statement. The result are the operators +=, -=, *=, /=, %=, >>=, <<=, &=, ^= and |=. For example, one can write x /= 3 rather than the longhand x = x / 3.

The benefit becomes more obvious when the assignment target is a bit more of a mouthful than just "x"; this example from the Linux kernel:

      hostdata->time_write[cmd->target] += (jiffies - hostdata->timebase);
    
This might be bad enough, but is much better than the equivalent
      hostdata->time_write[cmd->target] =
        hostdata->time_write[cmd->target] + (jiffies - hostdata->timebase);
    

Logical Operators

Logical operators are used only with int or char operands. They mostly used in conditional statements (see Program Control) where it might be desired to execute a particular statement only if condition 1 AND condition 2 both hold.

When C evaluates a boolean expression using logical operators, any non-zero value is considered true and zero is considered false.

The operators are: ! (Logical NOT), && (Logical AND), || (Logical OR).

The following table shows shows a few examples of how logical operators can be combined to produce boolean values.

int x = 0;
!x;
TRUE
!0 TRUE
!2 FALSE
1 && 2 TRUE
0 && 2 FALSE
0 || 2 TRUE
(9 > 4) && !(5 > 8) TRUE


In evaluating logical expressions, the C standard says that evaluation proceeds until the answer is known. So in evaluating a string of logical expressions separated by || the evaluation ceases when a term is TRUE; in evaluating a string of logical expressions separated by && the evaluation ceases when a term is FALSE. Evaluating any further cannot change the result.

This sounds innocent enough, but consider the expression x > 0 && y++. If the initial value of y is 10, what is the value of y after the expression has been evaluated?

The answer depends on the value of x. If it is negative, the first term will be false, so the second does not need to be evaluated, so y remains unchanged. If x is non-negative, the result of the whole expression depends on y, which is incremented after its value is read. So the answer is "10 or 11".

This is a very bad way of incrementing y depending upon the value of x. So bad, in fact, that it is almost certainly not what the programmer intended and is a potential bug, unless an expectation of this bizare behaviour is expressed in comments. Remember: clever programming is bad. Avoid operators with side-effects in boolean expressions.


Relational Operators

Relational operators compare the values of two expressions and return 1 if the relation holds and zero if it does not. The values returned may therefore be freely manipulated using the logical operators described above.

The following relational operators are available:

==Equal to
!=Not equal to
>Greater than
>=Greater than or equal
<Less than
<=Less than or equal

Common Programming Errors with Relational Operators

Expecting Exact Results from Floating Point Operations

There is an intrinsic danger in carrying out floating point tests for equality. It is quite legal to write x == 10.0 which would be expected to evaluate to TRUE only if x is exactly 10.0. However, rounding errors could cause an inequality when not expected. This is particularly problematic since some numbers which are very easily expressed in decimal, such as 0.2, turn out to be recurring when expressed in binary.

If possible, therefore, avoid tests for equality between floating point numbers. If you have to do them, round the number to an appropriate resolution before performing the test (in the above example, one might test fabs(x-10.0) < 0.001. Alternatively, if the test is a termination condition for an iterative algorithm, try to recast the program so that a much less dangerous test like x <= 10.0 can be used instead. Best of all, use integer arithmetic.

Confusing the Assignment and Equality Operators

It is quite easy to make the error of typing = where == was intended.

Introducing Bugs Quickly and Easily

if (x==y) {
    /* This code executes if x equals y */
    ...

if (x=y) {
    /* This code exectues if y is non-zero */
    ...
Recall that the assignment operator returns a value equal to the value of the assignment, so whereas the intention is that of the first part of the example, that code should be executed only when x and y are numerically equal, in fact x is set to the value of y and then the code is executed if y was non-zero.

Bitwise Operators

Bitwise operations allow the testing, setting, clearing or shifting of bits within int or char operands.

~Bitwise one's complement NOT
<<Bitwise left shift
>>Bitwise right shift
&Bitwise AND
^Bitwise eXclusive OR
|Bitwise inclusive OR

The table below shows example expressions involving bitwise operators between two unsigned character variables.

ExpressionBinaryDecimalComment
A0101010185
B0001110129
A&B0001010121Bitwise AND
A|B0101110193Bitwise OR
A^B0100100072Bitwise XOR
~A10101010170Bitwise NOT
B<<201110100116Left Shift
B>>20000011114Right Shift

The left-shift operator causes the bit pattern in the first operand to be shifted left the number of bits specified by the second operand. Bits vacated by the shift operation are zero-filled.

The right-shift operator causes the bit pattern in the first operand to be shifted right the number of bits specified by the second operand. Bits vacated by the shift operation are zero-filled for unsigned quantities. For signed quantities, the sign bit is propagated into the vacated bit positions.

Bitwise operators are frequently used to test, set or reset individual bits. This can be useful to store arbitrary flags in a C program, or to perform hardware-oriented operations in an on-chip register of a peripheral IC of an embedded system. For example, a word processor might need to store whether a chacter is bold, italic, neither or both. The programmer might use the following code:

#define BOLD 1
#define ITALIC 2
int chrFlags;
...
/* Set BOLD; don't change ITALIC */
chrFlags |= BOLD;
...
/* Reset ITALIC, don't change BOLD */
chrFlags &= (~ITALIC)
...
/* Test to see whether the charcter is either italic or bold or both.
   Notice the combination of logical and bitwise operators */
if (chrFlags & ITALIC || chrFlags & BOLD) {
    ...


There is a very useful but little known feature of C which permits the sort of "bit-twiddling" descibed above without recourse to bitwise operators. It is possible to specify integer of character variables of reduced, arbitrary length by specifying a bit-field size. If the integers are parts of a struct, the bitfields are packed into the same machine word or byte, which can come in very handy for manipulating individual bits in an I/O register, for example.

Unfortunately, in spite of the fact that it is cheap easy to port compilers like gcc to even the simplest of processors, companies which build microcontroller development hardware frequently ship home-grown, half-baked compilers which don't support lesser-used C features, or support them badly. However, if a proper compiler is available, the programmer has the choice of implementing the above code as shown in the following session:

$ cat bitfield.c
struct {
  unsigned int bold:1;
  unsigned int italic:1;
} chrFlg;

main()
{
  chrFlg.bold = 1;
  printf("%d\n", sizeof(chrFlg)/sizeof(int));
}
$ gcc bitfield.c
$ ./a.out
1
Note that the integers are declared unsigned, because a 1-bit signed integer represents only the numbers -1 and 0. Note that the size of the resulting structure is the same as the size of an integer, so may such bitfields (up to 32-bits' worth on a modern machine) are packed into the memory occupied by a single integer.

You can't use this as a trick to make very long integers by saying int x:128 for example. gcc just says "warning: width of `italic' exceeds its type", and your code probably won't work. Most compilers refuse to split bit fields over word boudaries, so declaring three such integers of 20 bits each will probably end up using 3 32-bit words of storage (on a 32-bit machine) rather than two, because otherwise the second 20-bit entitiy would overlap a word boundary. In this case, padding is inserted by the compiler automatically.

Of course, this doesn't come for free: the machine has to perform many extra operations to isolate the bitfields in a single byte or word. This technique should therefore be saved for special occasions when it is really needed. Operating on the bits in I/O registers is perhaps such a situation.


The Conditional Operator

The ternary operator ? can replace the if...else form for certain statements. It takes the general form: expression1 ? expression2 : expression3. expression1 is evaluated. If it is TRUE, then expression2 is evaluated and its value becomes the value of the whole expression. If it is FALSE expression3 is evaluated insteasd, and its value becomes the value of the expression.

The Conditional (Ternary) Operator

/* Instead of writing... */
if (b > 10)
    x = 15;
else
  x = 5;

/* we could write */
x = b>10 ? 15 : 5;
The usefulness of this operator is really a matter of personal taste. For some simple cases, it is fine, but if it isn't clear then using the extended if..else form is better. Consider the utility of the following conditional:

Nice Ternery Operator

puts(a >= 0 ? "Result is non-negative" : "Result is negative");
If a is greater than or equal to 0 then the string "Result is non-negative" is passed as an argument to puts() and consequently printed. If not, the second string is printed. This is a clear and good use of the conditional operator. The following counter-example is an unclear and bad one.

Nasty Ternery Operator

puts(a>=0?a==0?"zero":"positive":"negative");
Work out what it does, and how it does it. How long did that take? Too long. It is easy to write bad code given any powerful language, but that is not an excuse for doing so. Remember: clever programming is bad.

The sizeof Operator

The sizeof operator is a unary compile-time operator that returns the length (in bytes) of the following identifier (or type-cast expression if enclosed by parenthesis). The result is evaluated when the program is compiled, because the size of a data type is fixed at this time and can not change. The resulting constant is used substituted for the sizeof term.

Summary:

This section has covered the different operators available in the C language in depth. Some of the stylistic points are rather advanced, and rely on concepts not introduced; nevertheless, it would be out of place to locate them elsewhere. As the concepts are fundamental, it would be beneficial to revist this section towards the end of the course to ensure the material is thoroughly understood.

System Requirements

To view this web resource, you will need:

Copyright and Acknowledgements

The tools upon which this course relies are Copyright the Free Software Foundataion where they are made available under the GPL (GNU Public Licence).

The content of this course was derived from that generated by many ex-colleagues at the University of Leeds, Department of Electronics and Electrical Engineering. Much of the content has been reworked, and substantially augmented, but Dr N J Bailey, Centre for Music Technology, The University of Glagsow. This manifestation is Copyright N J Bailey; some of the content is Copyright The University of Leeds.

Diagrams on this resource are drawn in XFig and are rendered by the browser using The University of Hamburg's Simple FIG viewer applet which is Copyright (C) 1996-2002 F.N.Hendrich, hendrich@informatik.uni-hamburg.de.

The source code, programming examples and exercises are all specific to this course, and are Copyright, Dr N J Bailey.

The applet for viewing and demonstrating C programs is Copyright Dr N J Bailey, and is to be found documented and with its source code on the Centre for Music Technology website under Software