Dr. Roger Ianjamasimanana

Arithmetic operations and operators in C

By Dr. Roger Ianjamasimanana

C has many arithmetic operators which can be unary (acting on a single operand) or binary (acting on two operands). In this article, we will look at various arithmetic operations in C, operator precedence, overflow, and special unary operations like increment and decrement.


1. Binary arithmetic operators

C provides the following binary arithmetic operators:

  • Plus (+): adds two numbers (either integer or float).
  • Minus (-): subtracts the second operand from the first.
  • Multiply (*): multiplies two operands.
  • Divide (/): divides the first operand by the second.
  • Modulo (%): returns the remainder when dividing the first operand by the second. Only valid for integral types (e.g., int, char).

While +, -, *, and / can be used on integers or floats, integer division truncates any fractional part. For example, 17 / 5 will yield 3, not 3.4. The % operator only applies to integer types (including char, short, etc.).

Note about negatives and truncation: If negative integers appear, the way truncation or sign usage with / and % can vary between different C implementations. A portable workaround is to ensure you manage negative operands carefully, or otherwise rely on well-defined behaviors specific to your target platform.


2. Unary operators: +, -, ++, and --

Unary plus (+) and unary minus (-) can operate on integers or floats. Generally, unary plus is redundant (numbers are already positive by default), while unary minus inverts the sign:

// Example of unary usage
int pos = +42;    // Redundant: 'pos' is 42
double neg = -56.5; // 'neg' is -56.5

Difference between x++ and ++x:

The increment (++) and decrement (--) operators add and subtract 1 from a variable, respectively. They come in two forms, prefix and postfix. When used as a prefix (++x), the variable is updated first, then the expression uses the new value. For postfix (x++), the expression uses the old value, then the variable is updated afterward.

double x = 3.2;
double y = ++x; // x becomes 4.2, then y = 4.2
double z = x++; // z gets old x (4.2), then x becomes 5.2 

The expression x++ is equivalent to x = x + 1; let's illustrate it with the code below.

#include <stdio.h>
  int main(void) {
      // Example 1: Prefix
      int x = 5;
      int y = ++x;  
      // Steps:
      //  1) x becomes 6
      //  2) y = the updated x (6)
      // So x=6, y=6
  
      printf("After prefix increment (from 5):\n");
      printf("  x=%d, y=%d\n\n", x, y);
      
      // Example 2: Postfix
      int m = 7;
      int n = m++;
      // Steps:
      //  1) n = old m (7)
      //  2) m becomes 8
      // So m=8, n=7
  
      printf("After postfix increment (from 7):\n");
      printf("  m=%d, n=%d\n\n", m, n);
  
      // Example 3: Demonstrate usage in one expression
      // (Potentially confusing scenario)
      int a = 10;
      int b = ++a + a++; 
      //   In detail:
      //   - ++a => a=11, expression sees 11
      //   - a++ => expression sees 11 (the old value), then a=12 afterward
      //   - so b = 11 + 11 => 22, then a=12 finally.
      //   If we did something else, the outcome might differ if we reversed the increments
  
      printf("Combined increments:\n");
      printf("  a=%d, b=%d\n", a, b);
      
      return 0;
  }

3. Operator precedence and associativity

C evaluates operators in a specific order when they appear in an expression. Some operators have higher “precedence” than others. If two operators share the same precedence level, associativity (usually left to right) decides their grouping.

Category Operator precedence Associativity
Postfix () [] -> . ++ -- Left to right
Unary + - ! ~ ++ -- (type) * & sizeof Right to left
Multiplicative * / % Left to right
Additive + - Left to right
Shift << >> Left to right
Relational < <= > >= Left to right
Equality == != Left to right
Bitwise AND & Left to right
Bitwise XOR ^ Left to right
Bitwise OR | Left to right
Logical AND && Left to right
Logical OR || Left to right
Conditional ?: Right to left
Assignment =   +=   −=   *=   /=   %=   >>=   <<=   &=   ^=   |= Right to left
Comma , Left to right

For example, in 2 + 3 * 4, multiplication happens before addition, resulting in 14. If you want (2 + 3) * 4 = 20, add parentheses to override the default precedence.


4. Type conversions and casts in C

In C, when an operator sees operands of different types, it applies a sequence of type conversions so that both operands match. This is typically referred to as type promotion or the usual arithmetic conversions.

4.1. Promotion rules for binary expressions

Consider a binary expression like a * b. If neither operand is unsigned, these rules usually apply:

  • If either operand is long double: convert the other operand to long double.

    Example 1:

    long double ld_val = 2.0L;
      int i_val = 3;
      
      /* Expression */
      long double result = ld_val * i_val;
      
      /* Explanation:
          - ld_val is a long double, so i_val (which is an int) is 
            converted to long double before multiplication.
          - The result is of type long double.
      */

    Example 2:

    long double ld_val = 2.0L;
    double d_val       = 3.0;
    
    long double result = ld_val * d_val;
    
    /* Explanation:
       - One operand is long double (ld_val).
       - The double (d_val) is converted to long double.
       - The result type is long double.
    */

    Example 4

    long double ld_val = 3.0L;
    float f_val        = 2.0f;
    
    long double result = ld_val * f_val;
    /*
       - f_val (float) is converted to long double.
       - The multiplication is then performed as (long double * long double).
       - The final result is long double.
    */
  • Else if either operand is double: convert the other operand to double.
    double d_val = 2.0;
    int i_val = 3;
    
    /* Expression */
    double result = d_val * i_val;
    
    /* Explanation:
        - d_val is a double, so i_val (an int) is 
          converted to double before multiplication.
        - The result is of type double.
    */
  • Else if either operand is float: convert the other operand to float.
    float f_val = 2.0f;
    short s_val = 3;
    
    float result = f_val * s_val;
    // s_val is promoted to int, then converted to float; result is float
  • Otherwise: smaller integer types like char and short become int. If one operand is long, convert the other to long.

If one operand is an unsigned type and the other is a signed type of the same size, the signed operand is converted to unsigned. This can produce unexpected results if the signed value was negative.

Let's summarize everything in a table.

In the table below, the row indicates the type of a, the column represents the type of b, and the cell shows the result type of a * b.

a \ b short unsigned short int unsigned int float double long double
short int int int unsigned int float double long double
unsigned short int int int unsigned int float double long double
int int int int unsigned int float double long double
unsigned int unsigned int unsigned int unsigned int unsigned int float double long double
float float float float float float double long double
double double double double double double double long double
long double long double long double long double long double long double long double long double

How to read this table:

  1. Example 1: short * short
    • Both short are promoted to int, so the operation is int * int; the result is int.
  2. Example 2: short * unsigned int
    • short is promoted to int.
    • Now we have int * unsigned int.
    • Since int cannot represent all possible values of unsigned int (on a 32-bit system), the int is converted to unsigned int.
    • Result type: unsigned int.
  3. Example 3: unsigned short * float
    • unsigned short is promoted to int, then converted to float because the other operand is float.
    • Final type: float.
  4. Example 4: double * long double
    • double is converted to long double.
    • Result type: long double.
  5. Example 5: unsigned int * double
    • unsigned int is converted to double.
    • Result type: double.

4.2. Example of type promotion


short a = 3;
int b = 15;
float c = 24.1f;
double d = c + a * b;

In this scenario, the multiplication (a * b) happens first. Because a (a short) is promoted to int, the product is an int. Next, that int is promoted to float to match c, and the addition result is then promoted to double for assignment to d.

Note on char promotion: Some implementations treat a plain char as signed, others as unsigned. This influences whether the compiler sign-extends or zero-extends the char value.

4.3. Narrowing conversions

Sometimes assignments push a value into a smaller (narrower) data type. This can result in the loss of information. For instance, converting a double to an int drops any fractional component, while converting a bigger integer to a smaller one truncates higher-order bits. Many compilers warn when a narrowing conversion occurs.

int iresult = 0.5 + 3 / 5.0;

In this expression:

  • 3 / 5.0 is a floating-point operation, yielding 0.6 (or close to that).
  • 0.5 + 0.6 is 1.1.
  • Assigned to iresult (an int), the result is truncated to 1.

Similarly, moving from double to float can lead to rounding or truncation, depending on the target platform and how it handles the conversion.

4.4. Using casts in C

It’s best to explicitly cast values when a narrowing conversion is intentional or you want to override the normal promotion rules. Below are examples of each.

// Example: Making a narrowing conversion explicit
int iresult = (int)(0.5 + 3 / 5.0);

// Example: Forcing float instead of double
float result = (float)5.0 + 3.0f;

By casting, you make it clear you intend the potential loss of precision or to circumvent automatic promotions. This clarity helps with code maintainability and correctness checks.

4.5. Example programs

Let's illustrate the type conversions and casts in C in the code below.

/*
 * Demonstration of type conversions and casts in C
 *
 * This program illustrates:
 * 1) Automatic type promotion (short -> int, int -> float, etc.)
 * 2) Mixed-type expressions leading to different final types.
 * 3) Explicit casts for narrowing or overriding promotion rules.
 */

#include <stdio.h>

int main(void)
{
    /* 1) Basic integer promotion */
    short a = 5;
    int b = 10;
    float c = 23.1f;

    /* Here, a * b -> (short -> int) * int = int
       Then (int -> float) to add with c,
       Finally (float -> double) for assignment. */
    double d = c + a * b;
    /* Expected logic: a is promoted to int, multiply with b=10 => 50
       50 as int is promoted to float => 50.0f
       23.1f + 50.0f => 73.1f, then assigned to d as double => 73.1 */
    printf("Result of c + a*b (short->int->float->double): %.2f\n", d);

    /* 2) Mixed types in an expression */
    int x = 3;
    float y = 5.0f;
    /* x is int, y is float, so the expression uses float arithmetic */
    float mixed_result = x * y / 2; 
    /* x*y => int * float => float, then /2 => float as well */
    printf("Mixed-type expression x*y/2 => %.2f\n", mixed_result);

    /* 3) Assignment with narrowing conversions */
    /* The fraction part will be lost when assigned to an int */
    float fraction = 10.75f;
    int narrow = fraction;  /* Implicit truncation */
    printf("Assigning float(10.75) to int => %d\n", narrow);

    /* 4) Explicit cast example: forcing truncation on purpose */
    double pi_approx = 3.14159;
    int truncated_pi = (int) pi_approx;  /* integer is 3 */
    printf("Casting double(3.14159) to int => %d\n", truncated_pi);

    /* 5) Another cast to override normal promotion rules 
       e.g., forcing sum as float instead of double */
    double five_double = 5.0;
    float sum_forced_float = (float) five_double + 3.0f;
    /* five_double is double, but we cast it to float => no double-based arithmetic,
       so sum is done as float, though small difference is subtle. */
    printf("Casting 5.0 double to float, then add 3.0f => %.2f\n", sum_forced_float);

    /* 6) Negative operand with unsigned operand example 
       (for demonstration purpose only, it might produce unexpected results) */
    int negative_val = -2;
    unsigned int positive_val = 10;
    /* The negative_val is converted to unsigned, which can lead
       to a large number if negative_val was truly negative. */
    unsigned int strange = negative_val + positive_val;
    printf("Mixing signed(-2) and unsigned(10) => %u (unpredictable if negative too large)\n",
           strange);

    return 0;
}

5. Errors: divide-by-zero and overflow

  • Divide-by-zero: if you do x / y or x % y where y is 0, the result is undefined. This can cause the program to crash or produce erratic results.
  • Overflow: occurs when the result of an operation exceeds what the variable type can represent. For instance, adding 1 to the largest int can overflow, leading to unpredictable outcomes or wrap-around.

6. Examples of arithmetic operations in C

The following example shows how these operators behave. We’ll experiment with integer division, floating-point division, and observe how we might handle zero divisors.

#include <stdio.h>
#include <limits.h> // For INT_MAX

int main(void) {
    int a = 9, b = 4;
    float c = 9.0f, d = 4.0f;

    // 1) Integer arithmetic
    printf("Integer arithmetic:\n");
    printf("  a + b = %d\n", a + b);        // 9 + 4 = 13
    printf("  a - b = %d\n", a - b);        // 9 - 4 = 5
    printf("  a * b = %d\n", a * b);        // 9 * 4 = 36
    printf("  a / b = %d (integer division)\n", a / b); // 9 / 4 = 2
    printf("  a %% b = %d (modulus)\n", a % b);        // 9 % 4 = 1

    // 2) Floating-point arithmetic
    printf("\nFloating-point arithmetic:\n");
    printf("  c + d = %.2f\n", c + d);   // 9.0 + 4.0 = 13.00
    printf("  c - d = %.2f\n", c - d);   // 9.0 - 4.0 = 5.00
    printf("  c * d = %.2f\n", c * d);   // 9.0 * 4.0 = 36.00
    printf("  c / d = %.2f\n", c / d);   // 9.0 / 4.0 = 2.25

    // 3) Operator precedence example
    int precedence_result = a + b * 2;  // b*2 -> 4*2=8, then a+8 -> 17
    printf("\nOperator precedence:\n");
    printf("  a + b * 2 = %d\n", precedence_result);
    int forced = (a + b) * 2; // (9+4)*2=13*2=26
    printf("  (a + b) * 2 = %d\n", forced);

    // 4) Checking for zero divisor
    printf("\nDivide-by-zero check:\n");
    int zero = 0;
    if (zero == 0) {
        printf("  Can't do a / zero or a %% zero; skipping\n");
    }

    // 5) Overflow scenario
    int big = INT_MAX;
    printf("\nOverflow check:\n");
    printf("  INT_MAX = %d\n", big);
    printf("  INT_MAX + 1 => overflow leads to undefined result\n");
    // In practice, this might wrap around or cause other errors
    // but we demonstrate that it's not safe or predictable:
    int overflow_result = big + 1;
    printf("  overflow_result is %d (not reliable)\n", overflow_result);

    return 0;
}

What we learned from the code:

  • Integer division drops decimals (2 in place of 2.25).
  • Parentheses override default operator precedence.
  • Zero divisors are invalid; we avoid them by checking if (zero == 0) first.
  • Overflow can produce undefined results, so use caution with operations near type limits.

7. Conclusion and reference

When dealing with arithmetic operations in C, we need to be careful about integer vs. floating-point division, operator precedence, unary vs. binary operators, and edge cases like divide-by-zero or overflow. We can avoid misleading or unexpected result by using parentheses to clarify expressions, carefully handling negative operands, and avoiding zero divisors.

Reference: B.W. Kernighan and D.M. Ritchie. The C Programming Language. Prentice-Hall, 2nd edition, 1988

feature-top
Readers’ comment
feature-top
Log in to add a comment
🔐 Access