Declaring variables

A variable in C is a named piece of memory of a given type. You declare it with the type first, and you should give it a value at the same time:

#include <stdio.h>

int main(void)
{
    int meters = 400000;              /* declaration with initialization */
    double tariff = 0.27;
    const int hours_per_year = 8760;  /* const: may not be changed */
    int readings_today;               /* declared, no value yet */

    readings_today = 96;              /* assignment */
    printf("%d meters, %d readings each today\n", meters, readings_today);
    printf("tariff %.2f, %d hours per year\n", tariff, hours_per_year);
    return 0;
}
400000 meters, 96 readings each today
tariff 0.27, 8760 hours per year

Names follow the same rules as in most languages: letters, digits and underscores, not starting with a digit, case-sensitive. A const variable cannot be assigned to after its initialization; the compiler refuses it (see constants in C).

A local variable without an initializer does not start at zero. It holds whatever bytes were in that memory before: a garbage value that can differ per run. Using it is a bug that -Wall often catches:

#include <stdio.h>

int main(void)
{
    int total;
    printf("Total: %d kWh\n", total);
    return 0;
}
example.c:6:5: error: ‘total’ is used uninitialized [-Werror=uninitialized]

"Often", not "always": the same mistake inside a loop (int total; followed by total += 100; in a for loop) compiled without any warning on the test machine and printed a different large number on each run. Initialize every variable when you declare it, and int total = 0; is never wrong.

The basic types and their sizes

C has a handful of built-in arithmetic types. Their sizes are not fixed by the standard: it only sets minimum sizes (an int must have at least 16 bits, a long at least 32) and leaves the rest to the platform. The sizeof operator tells you the size of a type or a variable in bytes, as a value of type size_t; it is worked out by the compiler, not at run time (except for variable-length arrays). This is what a 64-bit Linux machine (ARM64) reports:

#include <stdio.h>

int main(void)
{
    printf("char        %zu\n", sizeof(char));
    printf("short       %zu\n", sizeof(short));
    printf("int         %zu\n", sizeof(int));
    printf("long        %zu\n", sizeof(long));
    printf("long long   %zu\n", sizeof(long long));
    printf("float       %zu\n", sizeof(float));
    printf("double      %zu\n", sizeof(double));
    printf("long double %zu\n", sizeof(long double));
    printf("void *      %zu\n", sizeof(void *));
    printf("'A'         %zu\n", sizeof('A'));
    return 0;
}
char        1
short       2
int         4
long        8
long long   8
float       4
double      8
long double 16
void *      8
'A'         4

sizeof needs parentheses around a type name (sizeof(int)) but not around a variable (sizeof readings); %zu is the printf format for size_t. A char is always exactly 1 byte, by definition. The last line surprises many people: a character constant such as 'A' has type int in C, not char.

Data models: LP64 and others

The combination of sizes is called a data model. Linux, macOS and the BSDs on 64-bit processors use LP64: long and pointers are 64 bits, int stays 32 bits. 64-bit Windows uses a different model, LLP64, in which long is still 32 bits and only long long and pointers are 64. Old 32-bit systems and many microcontrollers use yet other sizes; on some small 8- and 16-bit microcontrollers an int is only 16 bits.

Type Size (LP64) Range on this machine Typical use
char 1 byte 0 to 255 here (see below) Characters of text, raw bytes
short 2 bytes −32,768 to 32,767 Rarely used, except in file formats
int 4 bytes −2,147,483,648 to 2,147,483,647 The default for whole numbers and counters
long 8 bytes about ±9.2 × 1018 Large values, but 4 bytes on Windows
long long 8 bytes about ±9.2 × 1018 At least 64 bits everywhere
float 4 bytes about 6–7 significant digits Sensors on small chips; prefer double
double 8 bytes about 15–16 significant digits Measurements, calculations
_Bool / bool 1 byte 0 or 1 Flags; write bool after #include <stdbool.h> (a keyword in C23)

The exact limits are available as macros in <limits.h>, so a program never has to guess:

#include <limits.h>
#include <stdio.h>

int main(void)
{
    printf("int:       %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned:  0 to %u\n", UINT_MAX);
    printf("long:      %ld to %ld\n", LONG_MIN, LONG_MAX);
    printf("char:      %d to %d\n", CHAR_MIN, CHAR_MAX);
    printf("CHAR_BIT:  %d\n", CHAR_BIT);
    return 0;
}
int:       -2147483648 to 2147483647
unsigned:  0 to 4294967295
long:      -9223372036854775808 to 9223372036854775807
char:      0 to 255
CHAR_BIT:  8

CHAR_BIT is the number of bits in a byte; 8 on every machine you are likely to meet. Negative numbers are stored in two's complement, which is why the negative limit is one larger than the positive one.

Fixed-width integers and size_t

When the size of a number matters, for a file format, a network packet, a hardware register or a value that must not overflow, the vague sizes of int and long are a problem. C99 added the header <stdint.h> with a fixed-width integer type for each common size: int8_t, int16_t, int32_t and int64_t are signed and exactly 8, 16, 32 and 64 bits; uint8_t to uint64_t are their unsigned versions. Firmware uses them everywhere: the CRC check of a P1 telegram on Smart Meters and the P1 Port works on uint8_t bytes and a uint16_t result.

The second important type is size_t: an unsigned integer type big enough to hold the size of any object in memory. It is what sizeof and strlen return and what malloc takes, so it is the natural type for sizes, lengths and array indexes. On LP64 it is 8 bytes.

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    int32_t meters = 400000;
    int32_t hours_per_year = 8760;
    int64_t meter_hours = (int64_t)meters * hours_per_year;
    uint8_t level = 255;
    size_t name_length = sizeof "Kestrel" - 1;

    printf("meter-hours per year: %" PRId64 "\n", meter_hours);
    printf("uint8_t maximum:      %" PRIu8 "\n", level);
    printf("name length:          %zu\n", name_length);
    printf("int64_t is %zu bytes, size_t is %zu bytes\n",
           sizeof(int64_t), sizeof(size_t));
    return 0;
}
meter-hours per year: 3504000000
uint8_t maximum:      255
name length:          7
int64_t is 8 bytes, size_t is 8 bytes

Printing these types needs care, because printf must be told the exact type. <inttypes.h> provides macros such as PRId64 ("print int64_t as decimal") that expand to the right letters for the platform; they are placed outside the quotes and glued to the format string. For size_t there is the length modifier z: %zu. The cast (int64_t)meters matters too; see signed overflow below.

Signed and unsigned integers

Every integer type comes in two flavors. A signed type holds negative and positive values; an unsigned integer type holds only zero and positive values, and uses the bit that would hold the sign for a doubled positive range: unsigned int goes from 0 to 4,294,967,295. Plain int, short and long are signed; add unsigned to get the other kind.

Unsigned wraparound

Unsigned arithmetic never overflows in the dangerous sense: the standard defines unsigned wraparound, so a result that does not fit is reduced modulo 2n, where n is the number of bits. Below zero you come out at the maximum, above the maximum you come out at zero, like a car's odometer:

#include <limits.h>
#include <stdio.h>

int main(void)
{
    unsigned int stock = 0;
    stock--;                          /* 0 - 1 wraps around */
    printf("0 - 1  = %u\n", stock);

    unsigned int max = UINT_MAX;
    max++;                            /* UINT_MAX + 1 wraps to 0 */
    printf("max + 1 = %u\n", max);

    unsigned char counter = 250;
    for (int i = 0; i < 10; i++) {
        counter++;
    }
    printf("250 + 10 in an unsigned char = %d\n", counter);
    return 0;
}
0 - 1  = 4294967295
max + 1 = 0
250 + 10 in an unsigned char = 4

Wraparound is useful for counters and checksums (the math is on Binary Arithmetic and Bitwise Operations), and a trap everywhere else: a stock count of "4294967295" after subtracting one too many is a real bug. Because a size_t is unsigned, length - 1 on an empty string gives a huge number instead of −1.

Signed overflow

For signed integers, overflow is undefined behavior: the standard says nothing about what happens. In practice the result often wraps to a negative number, but the compiler is allowed to assume overflow never happens and optimize on that assumption, so you cannot rely on anything. C does not check for it at run time.

Kestrel Energy has about 400,000 smart meters. Over a year of 8,760 hours, that is 3,504,000,000 meter-hours, more than the 2,147,483,647 an int can hold. This example is compiled with UndefinedBehaviorSanitizer, a run-time checker in GCC and Clang, which reports the overflow at the moment it happens (by default the program then carries on, but nothing computed from the overflowed value can be trusted):

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    int meters = 400000;
    int hours = 8760;

    int64_t right = (int64_t)meters * hours;
    printf("as int64_t: %" PRId64 "\n", right);

    printf("as int:\n");
    int wrong = meters * hours;       /* overflows: undefined behavior */
    (void)wrong;
    return 0;
}
as int64_t: 3504000000
as int:
example.c:14:9: runtime error: signed integer overflow: 400000 * 8760 cannot be represented in type 'int'

The fix is in the first calculation: cast one operand to int64_t before multiplying, so the multiplication itself happens in 64 bits. (int64_t)(meters * hours) would be too late: the overflow has already happened inside the parentheses. Without the sanitizer the program would quietly continue with a wrong, negative number.

Converting a value that does not fit into a signed type is a different case: not undefined, but implementation-defined. GCC documents that it keeps the low bits, so this compiles under -Wall -Wextra without a word and prints a negative number:

#include <stdio.h>

int main(void)
{
    int big = 3000000000;
    printf("%d\n", big);
    return 0;
}
-1294967296

Only -Wconversion warns about this one. For small types and constants the compiler is stricter: uint8_t level = 300; gets a warning even without any options (and is refused under -Werror): "unsigned conversion from ‘int’ to ‘uint8_t’ … changes value from ‘300’ to ‘44’".

The signedness of char

There are three distinct character types: char, signed char and unsigned char. Plain char, written without signed or unsigned, is meant for text, and the standard lets each platform decide whether it is signed. On a 64-bit Linux machine (ARM64) it is unsigned; on most x86-64 systems it is signed. The same byte value then reads differently:

#include <stdio.h>

int main(void)
{
    char c = (char)200;
    signed char s = (signed char)200;
    unsigned char u = 200;

    printf("char:          %d\n", c);
    printf("signed char:   %d\n", s);
    printf("unsigned char: %d\n", u);
    return 0;
}
char:          200
signed char:   -56
unsigned char: 200

On an x86-64 PC the first line would print -56. This matters whenever bytes above 127 appear, such as UTF-8 text or binary data, and it is behind one of the most famous C bugs:

char c;                               /* should be int */
while ((c = getchar()) != EOF) {
    putchar(c);
}

getchar returns an int: a byte value from 0 to 255, or the special value EOF (−1) at the end of the input. Squeezed into an unsigned char, −1 becomes 255 and is never equal to EOF, so on ARM64 the loop never ends. With a signed char, on x86-64, the loop seems to work but stops early at the first byte 0xFF in the data. The fix is int c;. GCC catches the ARM64 case with -Wextra:

eof.c:5:28: error: comparison is always true due to limited range of data type [-Werror=type-limits]
    5 |     while ((c = getchar()) != EOF) {
      |                            ^~

Floating point and money

float and double are IEEE 754 binary floating-point numbers, exactly as in Python, Java and JavaScript, with the same rounding effects: most decimal fractions, such as 0.1 or 0.29, cannot be stored exactly. A float has only 24 bits of precision, so it cannot even count every whole number above 16,777,216:

#include <stdio.h>

int main(void)
{
    float f = 16777217;
    double d = 16777217;
    printf("%.1f %.1f\n", f, d);
    return 0;
}
16777216.0 16777217.0

C adds one danger of its own: converting a floating-point value to an integer type simply cuts off the fraction, and the compiler does it without a word unless you ask for -Wconversion:

#include <math.h>
#include <stdio.h>

int main(void)
{
    double price = 0.29;
    int truncated = price * 100;            /* 28.999999... becomes 28 */
    long rounded = lround(price * 100);

    printf("0.1 + 0.2 = %.17f\n", 0.1 + 0.2);
    printf("equal to 0.3? %d\n", 0.1 + 0.2 == 0.3);
    printf("0.29 * 100 as int: %d\n", truncated);
    printf("rounded:           %ld\n", rounded);
    printf("2.675 with %%.2f:   %.2f\n", 2.675);
    return 0;
}
0.1 + 0.2 = 0.30000000000000004
equal to 0.3? 0
0.29 * 100 as int: 28
rounded:           29
2.675 with %.2f:   2.67

0.29 * 100 is 28.999999999999996 in binary, so the conversion gives 28 cents. lround from <math.h> rounds to the nearest whole number (link with -lm). And printf("%.2f") rounds the exact binary value, which for 2.675 is just below 2.675, so it prints 2.67. These small errors become real money on invoices.

The solution is the one used on Floating-Point Numbers: C has no decimal type, so store amounts as whole numbers of the smallest unit you need. Kestrel's fixed tariff of €0.2700 per kWh has four decimals, so it fits exactly as 2700 ten-thousandths of a euro:

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    const int64_t price_per_kwh = 2700;      /* €0.2700 in ten-thousandths */
    const int64_t kwh = 350;

    int64_t cost = price_per_kwh * kwh;      /* 945000 ten-thousandths */
    int64_t cents = (cost + 50) / 100;       /* round to whole cents */

    printf("%" PRId64 " kWh at €0.2700 = €%" PRId64 ".%02" PRId64 "\n",
           kwh, cents / 100, cents % 100);
    return 0;
}
350 kWh at €0.2700 = €94.50

Every step is exact integer arithmetic; the only rounding is the one you wrote yourself, to whole cents. Adding 50 before dividing by 100 rounds halves up for positive amounts; refunds (negative amounts) need their own rule, because integer division truncates toward zero (see Operators, Expressions and Conversions).

Literals and constants

A literal also has a type. A whole number is an int if it fits, otherwise the next larger signed type; suffixes ask for a type directly: u for unsigned, L for long, LL for long long. A number with a decimal point or exponent is a double; with the suffix f it is a float.

#include <stdio.h>

int main(void)
{
    printf("%d %d %d\n", 017, 0x1F, 17);
    printf("%u %ld %lld\n", 4000000000u, 3000000000L, 400000LL * 8760 * 1000);
    printf("%.2f %.2e\n", 1.25e3, 0.000123);
    return 0;
}
15 31 17
4000000000 3000000000 3504000000000
1250.00 1.23e-04

A leading 0 makes a number octal: 017 is 15, not 17. That is a classic trap with postal codes, ticket numbers and time values such as 08 (which is not even valid octal). 0x starts a hexadecimal number. Binary literals such as 0b1010 are standard only since C23, though GCC and Clang have long accepted them as an extension. In 400000LL * 8760 * 1000 the LL makes the whole calculation happen in long long; without it, the first multiplication would overflow an int; GCC warns about this even without options (an error under this site's -Werror).

For named constants C offers three tools, each with its own place:

  • const int hours_per_year = 8760;: a read-only variable with a type, visible in the debugger.
  • #define HOURS_PER_YEAR 8760: a preprocessor macro, replaced by its text before compilation; common for array sizes and in older code (see The Preprocessor and Build Tools).
  • enum { MAX_READINGS = 96 };: an integer constant the compiler knows, usable for array sizes (see Structs, Unions and Enums). C23 adds constexpr for the same purpose.