Expressions and Associativity
When an expression can be interpreted in more than one way, there are rules that govern how the expression gets interpreted by the compiler. Such expressions follow C’s precedence and associativity rules. The precedence of operators determine a rank for the operators. The higher an operator’s precedence, the higher “binding” it has on the operands.
For example, the expression a * b + c can be interpreted as (a * b) + c or a * (b + c), but the first interpretation is the one that is used because the multiplication operator has higher precedence than addition.
Associativity determines the grouping of operations among operators of the same precedence. In the expression a * b / c, since multiplication and division have the same precedence we must use the associativity to determine the grouping. These operators are left associative which means they are grouped left to right as if the expression was (a * b) / c.
The operators’ order of precedence from highest to lowest and their associativity is shown in this table:
Identifier, constant or string literal, parenthesized expression
The ++ and – on the second row of the table are the postfix increment and decrement operators. The func(arglist) on the second row is a function call. Functions will be covered in detail in another tutorial.
The ++ and – on the third row are the prefix increment and decrement operators. The single + and – on the third row are unary operators used to indicate a positive value or negate a value such as +3 or -a. The & on the third row is the address-of operator; the & on the 10th row is the bitwise AND operator. The (type-name) on the fourth row is an explicit cast which will be covered later in this tutorial.
Example
int a = 9;
int b = 4;
int c = 6;
printf( “%dn”, a + b * c );
The output will be 33 because the b * c multiplication has higher precedence.
The next example demonstrates the usage assignment and relational operators. Say func() is a function that returns some value. You want to assign the returned value to a variable and perform some action if the value is 3:
int a;
if ( a = func() == 3 )
{
/* do something */
}
/* do something with a */
However this will not work because the == has higher precedence than =, so the expression in the if statement is parsed as if (a = (func() == 3)). Therefore a is not assigned to the return value of func(), it will be set to 1 if func() returns 3, or 0 if func() does not return 3. The correct way is to use a parenthesized expression:if (( a = func()) == 3)
This example shows associativity:
int a = 3 + 4 – 2 + 7;
This works as you would expect. The expression is parsed as ((3 + 4) – 2) + 7 and 12 is assigned to a. Here is another dealing with associativity:
This program outputs:
a = 1, b = 0
All the operators on line 6 ( logical not, prefix increment, dereference and address-of) all have the same precedence but they are right associative. So the right operand of the = on line 6 can be written like this: ! ( ++ ( * (&a))).
This takes the address of a, then de-references it to get a again, increments it which stores 1 in a and returns 1, then takes the logical not of 1 which is 0 and assigns that to b.
These examples show that you must pay attention to operator precedence and associativity when writing expressions. Most expressions will work as expected but there are few that can surprise you.
Pay attention to the relational operators which have higher precedence than bitwise, logical and assignment operators. If you are not sure about how an expression will be interpreted then either breaks up the expression into several expressions over multiple statements or fully parenthesize the expression.
Here is a simple example of how to break up or parenthesize an expression. Suppose the original code is like this:
int a;
int c;
/* calculate a and c */
if ( a & 0x01 == c >> 2 )
{
/* do something */
}
This will be interpreted as if (a & (0x01 == (c >> 2))) which is probably not what you wanted. You could use temporary variables like this:
int expr1 = a & 0x01;
int expr2 = c >> 2;
if ( expr1 == expr2 )
{
/* do something */
}
The other way is to add parentheses:
if ((a & 0x01) == (c >> 2))
{
/* do something */
}
C Expressions – Evaluation
It is important to note that the above section on operator precedence and associativity does not define the order that the operands are evaluated; it only defines how an expression is interpreted. The operands to most operators may be evaluated in any order.
For example the simple expression a * b + c is interpreted as (a * b) + c according to the precedence rules. But that does not mean a * b is evaluated first. The c variable could be evaluated first, then a * b, which in turn could evaluate either a or b.
When dealing with simple variables this is not a problem. However consider what happens if the operands to an operator have side effects:
In this example both functions func1() and func2() modify the variable a and return its value. The output of the program depends on which order these functions are called on line 19. If func1() is called first it will assign 0 to a and return 0, then when func2() is called it will assign 3 to a and return 3. This leaves a and b both set to 3.
If func2() is called first, it will set a to 3 and return 3, then when func1() is called it will set a to 9 and return 9. This leaves a set to 9 and b set to 12.
Both outputs would be valid since C does not specify which operand should be evaluated first. C also does not specify in which order arguments to functions are evaluated. If in the previous example we had a function like this:
int func3( int arg1, int arg2 )
{
return arg1 + arg2;
}
and we changed line 19 to:
int b = func3( func1(), func2() );
We still have the same problem. Either func1() or func2() could be called first which will change the arguments passed to func3().
These examples have emphasized the fact that the order of evaluation of operands may not be what you expect. You must be especially careful with expressions that cause side effects. They could cause situations where the program works in one environment but fails in another.
{mospagebreak title=C Expressions – Type Conversions}
C Expressions – Type Conversions
Sometimes when expressions are evaluated the type of an operand is converted. These conversions may happen implicitly or explicitly. Implicit conversion is done automatically. For example when the operands to some operators have different types the smaller operand is converted to the larger operand’s type. You have already seen several examples of implicit type conversion in the tutorial on operators.
One thing to note here is that operators that take integers usually perform what is called “integer promotion” – converting smaller integral types such as char and short into int before carrying out the operation. Let us look at an example of integer promotion:
c = 128, d = -128
The addition operator performs implicit integer promotion. On this environment a char is a signed 8 bit value, which has a range of -128 to 127. On line 10, though both a and b are chars they are converted into int before the addition is done, then the result is assigned to c. On line 11 the same thing happens, but the int result of 128 is converted back into a char, which causes the value to “wrap around” to -128 since a positive 128 is not within the range of a char.
C Expressions – Explicit Type Conversion
With explicit type conversion you can tell the compiler to treat a value as a certain type. The way to do explicit type conversion is with the “casting” operator.
The syntax for explicit type conversion is :
(type-name) expression
For example the expression (char *) ptr + 1 will force the compiler to treat ptr as a char pointer (“cast ptr as a char pointer”) and then add one to it. Looking at the operator precedence table above, you can see that the casting operator (on row 4) has higher precedence than the addition operator, so the cast is done first, then the addition is done.
Casting is done when the normal type conversions will not give you the result you want. This example shows the difference:
The output is:d = 0.000000 d = 0.833333
On line 6, since i and the constant 6 are both integers an integer division is done which gives 0. On line 10 i is cast to a double, which forces the division to be done using double precision floating point values. This gives the result 0.83.
Another way to get the right result on line 10 would have been to not use the cast and instead change the constant 6 to 6.0:
10 d = i / 6.0;
Explicit type conversion is also used to change the compiler’s idea of what type a pointer is pointing to. In this next example we use void * which is basically a pointer to any type.
#include <stdio.h>
typedef enum { INT_ARG, CHAR_ARG, DBL_ARG } ArgType;
void printit( ArgType type, void * data )
{
switch( type )
{
case INT_ARG:
printf("data is %dn", *(int *)data );
break;
case CHAR_ARG:
printf("data is %cn", *(char *)data );
break;
case DBL_ARG:
printf("data is %fn", *(double *)data );
break;
}
}
void main()
{
int i = 20;
double d = 78.9571;
char c = 'A';
printit( CHAR_ARG, &c );
printit( INT_ARG, &i );
printit( DBL_ARG, &d );
}
The output follows:data is A data is 20 data is 78.957100
This example uses some features of C you probably have not seen before. On line 3 we define a new type called ArgType that can have one of three values: INT_ARG, CHAR_ARG or DBL_ARG. The switch statement on line 7 works basically like multiple if statements, comparing type to each case value and executing the statements after the case if it is a match.
The important part for this tutorial is the code in lines 10,13 and 16. The printit() function takes two arguments. The first argument (type) tells what kind of variable the second argument (data) is pointing to. When the printit() function is called data is a pointer to void, which is basically type-less, so it may point to any different type.
However we cannot dereference a void pointer because the compiler does not know what type it is pointing to. On line 10, if type is an INT_ARG data is cast into a pointer to an int which is then de-referenced to get the integer value. On line 13 if type is a CHAR_ARG data is cast into a pointer to char which is de-referenced to get the char value. Line 16 is similar except data is cast into a pointer to a double.