Primitive Types
C has a small set of built-in types. Every other data structure you will ever use in C — structs, arrays, linked lists — is built from these primitives. Understanding their sizes, behaviors, and quirks is essential because C exposes the machine-level representation in ways that higher-level languages hide.
Integer Types
The Basic Integer Types
char c = 'A'; // at least 8 bits
short s = 32000; // at least 16 bits
int i = 2000000; // at least 16 bits (usually 32)
long l = 2000000000L; // at least 32 bits
long long ll = 9000000000000000000LL; // at least 64 bits
The keyword here is "at least." The C standard does not specify exact sizes — it specifies minimums. On a 64-bit Linux system, int is 32 bits and long is 64 bits. On 64-bit Windows, int is 32 bits but long is also 32 bits. This is why portable code avoids relying on the size of int or long.
Fixed-Width Integers: stdint.h
When you need a specific size, use <stdint.h>:
#include <stdint.h>
#include <stdio.h>
int main(void) {
int8_t a = 127; // exactly 8 bits, signed
uint8_t b = 255; // exactly 8 bits, unsigned
int16_t c = 32767; // exactly 16 bits, signed
uint16_t d = 65535; // exactly 16 bits, unsigned
int32_t e = 2147483647; // exactly 32 bits, signed
uint32_t f = 4294967295U; // exactly 32 bits, unsigned
int64_t g = 9223372036854775807LL; // exactly 64 bits, signed
uint64_t h = 18446744073709551615ULL; // exactly 64 bits, unsigned
printf("int32_t: %d\n", e);
printf("uint64_t: %lu\n", (unsigned long)h);
return 0;
}
Use int for loop counters and local arithmetic. Use stdint.h types when the size matters — network protocols, file formats, hardware registers, cross-platform data structures.
The sizeof Operator
sizeof tells you the size of a type or variable in bytes. It is evaluated at compile time, not runtime.
#include <stdio.h>
#include <stdint.h>
int main(void) {
printf("char: %zu bytes\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("void*: %zu bytes\n", sizeof(void *));
return 0;
}
char: 1 bytes
short: 2 bytes
int: 4 bytes
long: 8 bytes
long long: 8 bytes
float: 4 bytes
double: 8 bytes
void*: 8 bytes
The only guarantee: sizeof(char) is always 1. Everything else depends on the platform. Always use sizeof instead of hardcoding sizes.
Floating-Point Types
float f = 3.14f; // 32 bits, ~7 decimal digits of precision
double d = 3.14159265; // 64 bits, ~15 decimal digits of precision
long double ld = 3.14159265358979323846L; // at least 64 bits
Use double by default. float has limited precision and is only worth using when memory is tight (large arrays, GPU programming). Floating-point arithmetic is not exact:
#include <stdio.h>
int main(void) {
double a = 0.1 + 0.2;
printf("0.1 + 0.2 = %.20f\n", a);
printf("Equal to 0.3? %d\n", a == 0.3);
return 0;
}
0.1 + 0.2 = 0.30000000000000004441
Equal to 0.3? 0
Never compare floats with ==. Use a tolerance:
#include <math.h>
int doubles_equal(double a, double b, double epsilon) {
return fabs(a - b) < epsilon;
}
Signed vs Unsigned
Every integer type comes in signed and unsigned variants. Signed integers can be negative. Unsigned integers are always non-negative but can represent larger positive values.
#include <stdio.h>
#include <limits.h>
int main(void) {
printf("int range: %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int range: 0 to %u\n", UINT_MAX);
return 0;
}
int range: -2147483648 to 2147483647
unsigned int range: 0 to 4294967295
The Unsigned Underflow Bug
Unsigned integers cannot be negative. Subtracting past zero wraps around to a large positive value:
#include <stdio.h>
int main(void) {
unsigned int x = 0;
x = x - 1;
printf("0 - 1 as unsigned: %u\n", x);
// This loop never terminates
unsigned int len = 5;
for (unsigned int i = len - 1; i >= 0; i--) {
printf("%u ", i);
// when i is 0, i-- wraps to 4294967295, which is >= 0
// this is an infinite loop
if (i == 0) break; // must explicitly break
}
printf("\n");
return 0;
}
0 - 1 as unsigned: 4294967295
4 3 2 1 0
Use size_t (an unsigned type) for sizes and indices. But be careful when decrementing — the pattern for (size_t i = n - 1; i >= 0; i--) is an infinite loop because i can never be less than 0.
Type Promotion Rules
When you mix types in an expression, C implicitly converts (promotes) narrower types to wider types:
#include <stdio.h>
int main(void) {
char c = 10;
int i = 20;
double d = 3.14;
// char is promoted to int
int result1 = c + i; // 30
// int is promoted to double
double result2 = i + d; // 23.14
// integer division vs floating-point division
int a = 7, b = 2;
printf("7 / 2 = %d\n", a / b); // 3 (integer division)
printf("7 / 2.0 = %f\n", a / 2.0); // 3.500000
printf("result1: %d\n", result1);
printf("result2: %f\n", result2);
return 0;
}
7 / 2 = 3
7 / 2.0 = 3.500000
result1: 30
result2: 23.140000
The promotion rules: char/short promote to int. If one operand is double, the other promotes to double. If one is unsigned and the other signed of the same rank, the signed value converts to unsigned — which is where bugs hide.
Boolean: stdbool.h
C did not have a boolean type until C99. Before that, programmers used int with 0 for false and nonzero for true.
#include <stdio.h>
#include <stdbool.h>
int main(void) {
bool is_valid = true;
bool is_empty = false;
if (is_valid) {
printf("valid\n");
}
// Any nonzero value is truthy in C
int x = 42;
if (x) {
printf("42 is truthy\n");
}
// Zero is falsy
int y = 0;
if (!y) {
printf("0 is falsy\n");
}
return 0;
}
valid
42 is truthy
0 is falsy
Use stdbool.h for readability. Understand that underneath, bool is still an integer — true is 1 and false is 0.
char Is Just a Small Integer
char is an integer type that happens to hold character values. ASCII maps characters to numbers: 'A' is 65, '0' is 48, '\n' is 10.
#include <stdio.h>
int main(void) {
char c = 'A';
printf("character: %c\n", c);
printf("integer value: %d\n", c);
printf("next character: %c\n", c + 1);
// Character arithmetic
char digit = '7';
int value = digit - '0'; // converts char '7' to int 7
printf("digit '%c' has value %d\n", digit, value);
return 0;
}
character: A
integer value: 65
next character: B
digit '7' has value 7
Whether char is signed or unsigned is implementation-defined. Use signed char or unsigned char when the signedness matters. Use uint8_t for raw byte data.
printf Format Specifiers
Every type needs the correct format specifier in printf. Using the wrong one is undefined behavior.
| Specifier | Type | Example |
|---|---|---|
%d |
int |
printf("%d", 42) |
%u |
unsigned int |
printf("%u", 42U) |
%ld |
long |
printf("%ld", 42L) |
%f |
double |
printf("%f", 3.14) |
%e |
double (scientific) |
printf("%e", 3.14) |
%c |
char |
printf("%c", 'A') |
%s |
char * (string) |
printf("%s", "hello") |
%p |
void * (pointer) |
printf("%p", (void *)ptr) |
%zu |
size_t |
printf("%zu", sizeof(int)) |
%x |
unsigned int (hex) |
printf("0x%x", 255) |
For <stdint.h> types, use the macros from <inttypes.h>: printf("%" PRId64 "\n", my_int64). These expand to the correct format string for the platform.
Common Pitfalls
- Assuming
intis 32 bits — it is on most modern systems, but the standard only guarantees 16 bits minimum. Useint32_twhen the size matters. - Comparing signed and unsigned integers — when a negative signed value is compared to an unsigned value, the signed value is converted to unsigned, producing a huge positive number.
-1 < 1Uevaluates to false because-1becomes4294967295U. - Using
floatwhen you needdouble—floathas only ~7 digits of precision. Accumulated rounding errors infloatarithmetic cause real bugs in real programs. - Forgetting that integer division truncates —
7 / 2is3, not3.5. Cast one operand todoubleif you need the fractional part. - Using
%dforsize_t—size_tis unsigned and may be 64 bits. Use%zu.
Key Takeaways
- C has a small set of primitive types:
char,short,int,long,long long,float,double. Their sizes are platform-dependent. - Use
<stdint.h>types (int32_t,uint64_t) when you need exact sizes. Usesizeofinstead of hardcoding byte counts. - Unsigned underflow (subtracting past zero) wraps to a large positive value. This is the source of many C bugs.
charis an integer type. Boolean values are integers. C's type system is thinner than it looks.- Always use the correct
printfformat specifier. Mismatches are undefined behavior. - Type promotion rules govern how C converts between types in expressions. Signed/unsigned mixing is especially dangerous.