Data Types in C
Explore C's built-in data types including integers, floating-point numbers, characters, and how to use sizeof and type limits.
Integer Types
C provides several integer types of varying sizes. Choosing the right one matters: too small and you overflow, too large and you waste memory. The <limits.h> header defines the exact range of each type on the current platform.
#include <stdio.h>
#include <limits.h> /* INT_MAX, CHAR_MIN, etc. */
int main(void) {
char c = 127; /* typically 1 byte, -128 to 127 */
short s = 32767; /* at least 2 bytes */
int i = 2147483647; /* at least 2 bytes, typically 4 */
long l = 2147483647L; /* at least 4 bytes */
long long ll = 9223372036854775807LL; /* at least 8 bytes */
printf("char: %d bytes, max = %d\n", (int)sizeof(char), CHAR_MAX);
printf("short: %d bytes, max = %d\n", (int)sizeof(short), SHRT_MAX);
printf("int: %d bytes, max = %d\n", (int)sizeof(int), INT_MAX);
printf("long: %d bytes, max = %ld\n", (int)sizeof(long), LONG_MAX);
printf("long long: %d bytes, max = %lld\n",(int)sizeof(long long), LLONG_MAX);
return 0;
}
Unsigned Variants
Adding unsigned to any integer type removes negative values and doubles the positive range. Use unsigned types when a value is inherently non-negative (counts, sizes, bit masks).
unsigned char uc = 255; /* 0 to 255 */
unsigned short us = 65535; /* 0 to 65535 */
unsigned int ui = 4294967295U; /* 0 to 4,294,967,295 */
unsigned long ul = 4294967295UL;
Beware of unsigned arithmetic. Subtracting from zero wraps around rather than going negative:
unsigned int x = 0;
x--; /* x is now 4294967295, not -1 — wraps around */
Fixed-Width Integer Types (stdint.h)
The built-in types like int have platform-dependent sizes. When the exact size matters — network protocols, file formats, hardware registers — use the fixed-width types from <stdint.h>. These guarantee the exact number of bits on every platform.
#include <stdint.h>
#include <inttypes.h> /* PRId32 etc. for printf */
int8_t a = -128;
uint8_t b = 255;
int16_t c = -32768;
uint16_t d = 65535;
int32_t e = -2147483648;
uint32_t f = 4294967295U;
int64_t g = -9223372036854775807LL - 1;
uint64_t h = 18446744073709551615ULL;
/* Use the PRI* macros to print fixed-width types portably */
printf("32-bit int: %" PRId32 "\n", e);
printf("64-bit uint: %" PRIu64 "\n", h);
Also useful:
intptr_t/uintptr_t— wide enough to hold a pointersize_t— returned bysizeof, used for array indices and memory sizesptrdiff_t— result of subtracting two pointers
Floating-Point Types
Floating-point types represent real numbers with fractional parts. They trade off range and precision — double is the default choice for most numeric work because it offers roughly 15-17 significant decimal digits of precision, which is sufficient for the vast majority of applications.
#include <stdio.h>
#include <float.h> /* FLT_MAX, DBL_MAX, etc. */
int main(void) {
float f = 3.14f; /* ~7 significant decimal digits */
double d = 3.141592653589793; /* ~15-17 significant decimal digits */
long double ld = 3.141592653589793238L; /* 18-21 digits (platform dependent) */
printf("float: %f (size: %d bytes)\n", f, (int)sizeof(float));
printf("double: %.15f (size: %d bytes)\n", d, (int)sizeof(double));
printf("long double: %.18Lf (size: %d bytes)\n", ld, (int)sizeof(long double));
printf("float max: %e\n", FLT_MAX);
printf("double max: %e\n", DBL_MAX);
return 0;
}
Floating-point is not exact. Numbers like 0.1 cannot be represented exactly in binary. Never compare floats with == — always compare within a small tolerance:
double a = 0.1 + 0.2;
double b = 0.3;
/* WRONG — may not print "equal" even though the values look the same */
if (a == b) printf("equal\n");
/* CORRECT — compare within a small epsilon */
#include <math.h>
if (fabs(a - b) < 1e-9) printf("equal\n");
The char Type
char holds a single character and is also an integer type — you can do arithmetic on it. Characters are stored as their ASCII (or Unicode) integer values, so 'A' is just the number 65.
#include <stdio.h>
int main(void) {
char letter = 'A';
printf("%c = %d\n", letter, letter); /* A = 65 */
/* Characters are just small integers — loop through the alphabet */
for (char c = 'a'; c <= 'z'; c++) {
printf("%c", c);
}
printf("\n"); /* prints: abcdefghijklmnopqrstuvwxyz */
/* Arithmetic on chars works naturally */
char upper = 'a' - 32; /* 'A' — uppercase is 32 less than lowercase in ASCII */
char digit = '7' - '0'; /* 7 — subtract '0' to get the integer value of a digit */
printf("upper=%c, digit=%d\n", upper, digit);
return 0;
}
Whether plain char is signed or unsigned is implementation-defined. Use signed char or unsigned char explicitly when it matters.
The sizeof Operator
sizeof returns the size in bytes of a type or expression at compile time. It never evaluates the expression — only its type matters. This is essential for writing portable code that doesn’t assume a particular type size.
#include <stdio.h>
int main(void) {
printf("sizeof(char) = %zu\n", sizeof(char)); /* always 1 */
printf("sizeof(short) = %zu\n", sizeof(short)); /* at least 2 */
printf("sizeof(int) = %zu\n", sizeof(int)); /* typically 4 */
printf("sizeof(long) = %zu\n", sizeof(long)); /* 4 or 8 */
printf("sizeof(long long) = %zu\n", sizeof(long long)); /* at least 8 */
printf("sizeof(float) = %zu\n", sizeof(float)); /* typically 4 */
printf("sizeof(double) = %zu\n", sizeof(double)); /* typically 8 */
printf("sizeof(pointer) = %zu\n", sizeof(void *)); /* 4 or 8 */
/* sizeof on an array gives the total byte size — not the pointer size */
int arr[10];
printf("array size = %zu bytes, elements = %zu\n",
sizeof(arr), sizeof(arr) / sizeof(arr[0]));
return 0;
}
Type Conversion
C performs implicit conversions in arithmetic expressions, always promoting to the “wider” type. Understanding this prevents subtle bugs where you expect floating-point division but get integer truncation.
#include <stdio.h>
int main(void) {
int i = 7;
float f = 2.5f;
/* int promotes to float when combined with float — result is float */
float result = i + f; /* 9.5 */
printf("%.1f\n", result);
/* Integer division — both operands are int, result truncates toward zero */
int quotient = 7 / 2; /* 3, not 3.5 */
printf("%d\n", quotient);
/* Force floating-point division by casting one operand */
double exact = (double)7 / 2; /* 3.5 */
printf("%.1f\n", exact);
/* Narrowing conversion — truncates, does not round */
double big = 3.99;
int truncated = (int)big; /* 3, not 4 — truncates toward zero */
printf("%d\n", truncated);
return 0;
}
Explicit casts signal to the compiler (and the reader) that you know a conversion is happening. Always cast explicitly when narrowing to document your intent.
Type Qualifiers
Type qualifiers modify how a variable can be accessed. They communicate intent to both the compiler and future readers, enabling optimizations and catching mistakes.
const
const marks a variable as read-only after initialization. The compiler will reject any attempt to modify it, which prevents accidental changes and allows the compiler to optimize.
const double PI = 3.14159265358979;
/* PI = 3.0; */ /* compile error — cannot assign to const */
const int DAYS_IN_WEEK = 7;
volatile
volatile tells the compiler not to cache or optimize reads/writes to this variable, because it may change outside the program’s control — for example, a hardware register, a signal handler, or shared memory.
volatile int hardware_flag = 0; /* re-read from hardware on every access */
while (!hardware_flag) {
/* spin-wait — compiler must re-read hardware_flag each iteration */
/* without volatile, the compiler might cache the value and loop forever */
}
restrict (C99)
restrict tells the compiler that a pointer is the only way to access the memory it points to. This promise lets the compiler generate better-optimized code, particularly for loops.
void add_arrays(int *restrict dst,
const int *restrict src,
int n) {
for (int i = 0; i < n; i++) {
dst[i] += src[i]; /* compiler can safely vectorize this */
}
}