C Variables and types
By Flavio Copes
Learn how C variables and fundamental types work, how to inspect their limits and sizes, and why signed and unsigned overflow differ.
C is a statically typed language.
This means that any variable has an associated type, and this type is known at compilation time.
This is different from how you work with variables in dynamically typed languages such as Python and JavaScript.
When you create a variable in C, you have to specify the type of a variable at the declaration.
In this example we declare a variable named age with type int:
int age;
A variable name can contain any uppercase or lowercase letter, can contain digits and the underscore character, but it can’t start with a digit. AGE and Age10 are valid variable names, 1age is not.
You can also initialize a variable at declaration, specifying the initial value:
int age = 37;
Once you declare a variable, you can assign another value that can be converted to its type:
age = 100;
Conversions can lose information. In this example, 37.2 is converted to 37 before it is stored:
#include <stdio.h>
int main(void) {
int age = 37.2;
printf("%d\n", age);
return 0;
}
A compiler can warn about this conversion when suitable warning options are enabled, but you must not rely on every compiler diagnosing every lossy conversion.
The C fundamental arithmetic types include integer types such as char, short, int, long, and long long, plus the floating-point types float, double, and long double.
Integer numbers
C provides us the following types to define integer values:
signed charandunsigned charshortandunsigned shortintandunsigned intlongandunsigned longlong longandunsigned long long
Most of the time, you’ll use int for ordinary integer values.
The plain char type is used to store characters and small integer values. It is always exactly 1 byte by definition, but a C byte contains at least 8 bits and can contain more. Whether plain char behaves like signed char or unsigned char is implementation-defined.
The standard guarantees minimum ranges and this ordering:
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
Two adjacent types can have the same size. Their exact widths and ranges depend on the implementation.
Use the macros from <limits.h> instead of guessing:
#include <limits.h>
#include <stdio.h>
int main(void) {
printf("int range: %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int maximum: %u\n", UINT_MAX);
printf("bits in one byte: %d\n", CHAR_BIT);
return 0;
}
This matters on embedded devices. For example, an int can be 16 bits on one microcontroller and 32 bits on another.
When you need an exact-width type, <stdint.h> defines names such as int32_t and uint32_t on implementations that provide those widths. Use the matching format macros from <inttypes.h> when printing them.
Unsigned integers
Unsigned integer types start at 0. The standard guarantees at least these ranges:
unsigned charwill range from0to at least255unsigned intwill range from0to at least65,535unsigned shortwill range from0to at least65,535unsigned longwill range from0to at least4,294,967,295unsigned long longwill range from0to at least18,446,744,073,709,551,615
What happens on overflow?
Unsigned arithmetic is performed modulo one more than the type’s maximum value. If UCHAR_MAX is 255, this conversion produces 9:
#include <limits.h>
#include <stdio.h>
int main(void) {
unsigned char j = UCHAR_MAX;
j = j + 10;
printf("%u\n", (unsigned int)j);
return 0;
}
The addition happens after integer promotion, then the result is converted back to unsigned char. The conversion applies the unsigned modulo rule.
Signed integer overflow is different. It causes undefined behavior:
#include <limits.h>
int main(void) {
int value = INT_MAX;
value = value + 1; /* undefined behavior */
return 0;
}
It is not guaranteed to wrap. A compiler can optimize code on the assumption that signed overflow never occurs. Check a value before performing an operation that could exceed its range.
Converting an out-of-range value to a signed integer type is not the same operation as signed arithmetic overflow. The result is implementation-defined, or the implementation can raise an implementation-defined signal. Avoid depending on it.
Compile with useful diagnostics, for example -Wall -Wextra -Wpedantic -Wconversion with GCC or Clang, and treat the warnings as problems to investigate.
Floating point numbers
Floating-point types can represent fractions and values over a very large range, but only with limited precision.
You can write floating-point constants using decimal exponent notation:
double small = 1.29e-3;
double large = -2.3e+5;
The following types:
floatdoublelong double
are used to represent numbers with decimal points (floating point types). All can represent both positive and negative numbers.
Most modern systems use binary IEEE 754 formats, so many decimal fractions cannot be represented exactly. The exact formats and precision are implementation-defined. The macros in <float.h>, including FLT_DIG, DBL_DIG, and LDBL_DIG, describe the implementation.
double provides at least as much precision and range as float, and long double provides at least as much as double. They can still have the same representation on a particular implementation.
Inspecting type sizes
You can use sizeof to inspect how much storage a type occupies:
#include <stdio.h>
int main(void) {
printf("char: %zu byte\n", sizeof(char));
printf("short: %zu bytes\n", sizeof(short));
printf("int: %zu bytes\n", sizeof(int));
printf("long: %zu bytes\n", sizeof(long));
printf("long long: %zu bytes\n", sizeof(long long));
printf("float: %zu bytes\n", sizeof(float));
printf("double: %zu bytes\n", sizeof(double));
printf("long double: %zu bytes\n", sizeof(long double));
return 0;
}
The result of sizeof has type size_t, and %zu is the matching printf() conversion specifier.
Remember that sizeof reports C bytes, not necessarily 8-bit octets. Check CHAR_BIT when the number of bits matters.
Related posts about clang: