Variables, Types and Integers
C's basic types and their sizes on a 64-bit machine, fixed-width integers from stdint.h, signed versus unsigned, overflow and wraparound, the signedness of char, and floating-point traps with money.
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 addsconstexprfor the same purpose.