C — Variables & Data Types
C is a statically typed language — every variable must have a declared type known at compile time. Unlike dynamically typed languages, you cannot change a variable's type after declaration. This rigidity gives C its speed and low-level control, but demands careful attention to type selection, overflow behavior, and format specifier matching.
Understanding C's data types is foundational. Each type has a specific size in bytes, a defined range of values, and particular rules about how it interacts with arithmetic operations, format specifiers, and memory layout. Choosing the wrong type can lead to overflow, truncation, undefined behavior, or subtle portability bugs across platforms.
This page covers every primitive type in C, their unsigned variants, the sizeof operator, type limits from limits.h, constants, type qualifiers, implicit conversions, format specifiers, storage classes, and initializer rules.
| 1 | #include <stdio.h> |
| 2 | #include <limits.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | int age = 28; |
| 6 | double salary = 75000.50; |
| 7 | char grade = 'A'; |
| 8 | |
| 9 | printf("Age: %d\n", age); |
| 10 | printf("Salary: %.2f\n", salary); |
| 11 | printf("Grade: %c\n", grade); |
| 12 | |
| 13 | return 0; |
| 14 | } |
C provides five fundamental signed integer types, three floating-point types, and the char type. The C standard guarantees minimum ranges for each type, but actual sizes depend on the platform. On most modern 64-bit systems, int is 4 bytes and long is 8 bytes on Unix-like systems but 4 bytes on Windows (LLP64 model).
| Type | Typical Size | Min Value | Max Value | Format Specifier |
|---|---|---|---|---|
| char | 1 byte | -128 | 127 | %c / %hhd |
| short | 2 bytes | -32,768 | 32,767 | %hd |
| int | 4 bytes | -2,147,483,648 | 2,147,483,647 | %d |
| long | 4 or 8 bytes | -2,147,483,648 | 2,147,483,647+ | %ld |
| long long | 8 bytes | -9.2 × 10¹⁸ | 9.2 × 10¹⁸ | %lld |
| float | 4 bytes | ±1.2 × 10⁻³⁸ | ±3.4 × 10³⁸ | %f |
| double | 8 bytes | ±2.2 × 10⁻³⁰⁸ | ±1.8 × 10³⁰⁸ | %lf / %f |
| long double | 8–16 bytes | platform-dependent | platform-dependent | %Lf |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | char c = 'A'; // 1 byte, stores ASCII value 65 |
| 5 | short s = 32000; // 2 bytes |
| 6 | int i = 2000000000; // 4 bytes |
| 7 | long l = 2000000000L; // 4 or 8 bytes depending on platform |
| 8 | long long ll = 9000000000000000000LL; // 8 bytes |
| 9 | |
| 10 | float f = 3.14f; // 4 bytes, single precision |
| 11 | double d = 3.141592653589793; // 8 bytes, double precision |
| 12 | long double ld = 3.141592653589793238L; // 8-16 bytes |
| 13 | |
| 14 | printf("char: %c (%%c)\n", c); |
| 15 | printf("short: %d (%%hd)\n", s); |
| 16 | printf("int: %d (%%d)\n", i); |
| 17 | printf("long: %ld (%%ld)\n", l); |
| 18 | printf("long long: %lld (%%lld)\n", ll); |
| 19 | printf("float: %f (%%f)\n", f); |
| 20 | printf("double: %f (%%f)\n", d); |
| 21 | printf("long double: %Lf (%%Lf)\n", ld); |
| 22 | |
| 23 | return 0; |
| 24 | } |
Every signed integer type has an unsigned counterpart. Unsigned types cannot represent negative values, but their positive range is roughly doubled. Use unsigned when you know the value will never be negative — bitmasks, array indices, sizes, and counters.
| Signed Type | Unsigned Type | Unsigned Max | Format Specifier |
|---|---|---|---|
| char | unsigned char | 255 | %hhu |
| short | unsigned short | 65,535 | %hu |
| int | unsigned int | 4,294,967,295 | %u |
| long | unsigned long | platform-dependent | %lu |
| long long | unsigned long long | 1.8 × 10¹⁹ | %llu |
warning
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | unsigned int count = 4000000000u; |
| 5 | printf("Unsigned value: %u\n", count); |
| 6 | |
| 7 | // Dangerous: signed/unsigned comparison |
| 8 | int signed_val = -1; |
| 9 | unsigned int unsigned_val = 1; |
| 10 | |
| 11 | if (signed_val > unsigned_val) { |
| 12 | // This branch executes! -1 converts to UINT_MAX |
| 13 | printf("-1 is greater than 1 (unsigned conversion!)\n"); |
| 14 | } |
| 15 | |
| 16 | // Use size_t for sizes and indices |
| 17 | size_t len = 42; |
| 18 | printf("Size: %zu\n", len); |
| 19 | |
| 20 | return 0; |
| 21 | } |
The sizeof operator returns the size in bytes of a type or variable. It is evaluated at compile time (for types) or by the compiler for variables. It returns a value of type size_t, an unsigned integer type defined in stddef.h.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | // sizeof with types — parentheses required |
| 5 | printf("char: %zu byte\n", sizeof(char)); |
| 6 | printf("short: %zu bytes\n", sizeof(short)); |
| 7 | printf("int: %zu bytes\n", sizeof(int)); |
| 8 | printf("long: %zu bytes\n", sizeof(long)); |
| 9 | printf("long long: %zu bytes\n", sizeof(long long)); |
| 10 | printf("float: %zu bytes\n", sizeof(float)); |
| 11 | printf("double: %zu bytes\n", sizeof(double)); |
| 12 | |
| 13 | // sizeof with variables — parentheses optional |
| 14 | int x = 42; |
| 15 | printf("x: %zu bytes\n", sizeof x); |
| 16 | |
| 17 | // sizeof with expressions — parentheses required |
| 18 | printf("x + 1: %zu bytes\n", sizeof(x + 1)); |
| 19 | |
| 20 | // Array size trick |
| 21 | int arr[] = {10, 20, 30, 40, 50}; |
| 22 | int len = sizeof(arr) / sizeof(arr[0]); |
| 23 | printf("Array length: %d\n", len); // 5 |
| 24 | |
| 25 | return 0; |
| 26 | } |
info
The limits.h header defines macros for the minimum and maximum values of every integer type. The float.h header provides limits for floating-point types. Always use these macros instead of hardcoding values — type sizes vary across platforms.
| 1 | #include <stdio.h> |
| 2 | #include <limits.h> |
| 3 | #include <float.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | printf("--- Integer Limits (limits.h) ---\n"); |
| 7 | printf("CHAR_BIT: %d\n", CHAR_BIT); |
| 8 | printf("CHAR_MIN: %d\n", CHAR_MIN); |
| 9 | printf("CHAR_MAX: %d\n", CHAR_MAX); |
| 10 | printf("SHRT_MIN: %d\n", SHRT_MIN); |
| 11 | printf("SHRT_MAX: %d\n", SHRT_MAX); |
| 12 | printf("INT_MIN: %d\n", INT_MIN); |
| 13 | printf("INT_MAX: %d\n", INT_MAX); |
| 14 | printf("UINT_MAX: %u\n", UINT_MAX); |
| 15 | printf("LONG_MIN: %ld\n", LONG_MIN); |
| 16 | printf("LONG_MAX: %ld\n", LONG_MAX); |
| 17 | printf("LLONG_MIN: %lld\n", LLONG_MIN); |
| 18 | printf("LLONG_MAX: %lld\n", LLONG_MAX); |
| 19 | printf("ULLONG_MAX: %llu\n", ULLONG_MAX); |
| 20 | |
| 21 | printf("\n--- Float Limits (float.h) ---\n"); |
| 22 | printf("FLT_DIG: %d\n", FLT_DIG); |
| 23 | printf("FLT_MAX: %e\n", FLT_MAX); |
| 24 | printf("FLT_MIN: %e\n", FLT_MIN); |
| 25 | printf("DBL_DIG: %d\n", DBL_DIG); |
| 26 | printf("DBL_MAX: %e\n", DBL_MAX); |
| 27 | printf("DBL_MIN: %e\n", DBL_MIN); |
| 28 | |
| 29 | return 0; |
| 30 | } |
C provides three mechanisms for defining constants: preprocessor macros with #define, the const qualifier, and enum values. Each has distinct tradeoffs.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // #define — preprocessor macro, simple text replacement |
| 4 | #define PI 3.14159265358979 |
| 5 | #define MAX_SIZE 100 |
| 6 | #define GREETING "Hello, World!" |
| 7 | |
| 8 | // const — typed constant, subject to scope rules |
| 9 | const int buffer_size = 4096; |
| 10 | const double GRAVITY = 9.81; |
| 11 | |
| 12 | // enum — integer constants, great for related groups |
| 13 | enum Color { RED, GREEN, BLUE }; |
| 14 | enum Boolean { FALSE = 0, TRUE = 1 }; |
| 15 | |
| 16 | int main(void) { |
| 17 | // #define is replaced before compilation |
| 18 | double area = PI * 5.0 * 5.0; |
| 19 | printf("Area: %.2f\n", area); |
| 20 | |
| 21 | // const creates a named, typed constant |
| 22 | printf("Buffer: %d bytes\n", buffer_size); |
| 23 | |
| 24 | // enum values are integers |
| 25 | enum Color bg = GREEN; |
| 26 | printf("Background color: %d\n", bg); |
| 27 | |
| 28 | // const is NOT a true constant in C (unlike C++) |
| 29 | int arr[buffer_size]; // OK in C99 (VLA), not portable |
| 30 | |
| 31 | return 0; |
| 32 | } |
note
Type qualifiers modify the behavior of a type without changing its fundamental nature. C has three qualifiers: const, volatile, and restrict (C99).
| Qualifier | Purpose | Example |
|---|---|---|
| const | Value cannot be modified after initialization | const int x = 10; |
| volatile | Tells compiler the value may change externally (hardware, signal handler, another thread) | volatile int flag; |
| restrict | Pointer is the sole alias to its object — enables optimization | void copy(int *restrict dst, const int *restrict src) |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // volatile — essential for memory-mapped I/O and signal handlers |
| 4 | volatile int heartbeat_counter = 0; |
| 5 | |
| 6 | void signal_handler(void) { |
| 7 | heartbeat_counter++; // compiler won't optimize this away |
| 8 | } |
| 9 | |
| 10 | // restrict — promises the compiler no aliasing |
| 11 | void add_arrays(int *restrict dest, const int *restrict a, |
| 12 | const int *restrict b, int n) { |
| 13 | for (int i = 0; i < n; i++) { |
| 14 | dest[i] = a[i] + b[i]; // compiler can vectorize this |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | int main(void) { |
| 19 | const int MAX = 100; // cannot modify MAX |
| 20 | // MAX = 200; // compilation error |
| 21 | |
| 22 | int data[5] = {1, 2, 3, 4, 5}; |
| 23 | int result[5]; |
| 24 | add_arrays(result, data, data, 5); |
| 25 | |
| 26 | return 0; |
| 27 | } |
C automatically converts between types in certain contexts. Understanding these rules is critical to avoid bugs. The two key concepts are integer promotion and usual arithmetic conversions.
Integer promotion: Types narrower than int (i.e., char, short, unsigned char, etc.) are automatically promoted to int when used in expressions. This is why char c = 65; printf("%c", c); works — the char is promoted to int before being passed to printf.
Usual arithmetic conversions: When two different types appear in a binary operation, the "smaller" type is converted to the "larger" type. The hierarchy is: int -> unsigned int -> long -> unsigned long -> long long -> unsigned long long -> float -> double -> long double.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | // Integer promotion: char and short promoted to int |
| 5 | char a = 100; |
| 6 | char b = 50; |
| 7 | int sum = a + b; // a and b promoted to int before addition |
| 8 | printf("Sum: %d\n", sum); |
| 9 | |
| 10 | // Unsigned conversion trap |
| 11 | unsigned int u = 5; |
| 12 | int s = -10; |
| 13 | if (u > s) { |
| 14 | // This executes! s converts to unsigned (4294967286) |
| 15 | printf("u > s is true due to unsigned conversion\n"); |
| 16 | } |
| 17 | |
| 18 | // Float promotion: int promoted to float in mixed expressions |
| 19 | int x = 5; |
| 20 | double y = 2.5; |
| 21 | double result = x + y; // x promoted to double |
| 22 | printf("Result: %.1f\n", result); |
| 23 | |
| 24 | // Narrowing conversion — potential data loss |
| 25 | int big = 300; |
| 26 | char small = big; // 300 truncated to char (undefined behavior if outside range) |
| 27 | printf("Small: %d\n", small); |
| 28 | |
| 29 | return 0; |
| 30 | } |
warning
Format specifiers control how printf and scanf interpret values. Using the wrong specifier causes undefined behavior — a leading cause of C bugs and security vulnerabilities.
| Specifier | Type | Description |
|---|---|---|
| %d | int | Signed decimal integer |
| %u | unsigned int | Unsigned decimal integer |
| %ld | long | Signed long decimal |
| %lu | unsigned long | Unsigned long decimal |
| %lld | long long | Signed long long decimal |
| %llu | unsigned long long | Unsigned long long decimal |
| %f | double (promoted from float) | Decimal floating point |
| %e | double | Scientific notation |
| %lf | double | Double (for scanf only) |
| %Lf | long double | Long double floating point |
| %c | char (int promoted) | Single character |
| %s | char * | Null-terminated string |
| %p | void * | Pointer address |
| %x | unsigned int | Hexadecimal (lowercase) |
| %o | unsigned int | Octal |
| %zu | size_t | Size type (for sizeof result) |
| %% | (none) | Literal percent sign |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int n = 42; |
| 5 | unsigned int u = 4294967295u; |
| 6 | long long ll = 9000000000000000000LL; |
| 7 | double pi = 3.141592653589793; |
| 8 | char c = 'Z'; |
| 9 | const char *str = "Hello, C!"; |
| 10 | int *ptr = &n; |
| 11 | |
| 12 | // Integer specifiers |
| 13 | printf("int: %d\n", n); |
| 14 | printf("unsigned: %u\n", u); |
| 15 | printf("long long: %lld\n", ll); |
| 16 | |
| 17 | // Float specifiers |
| 18 | printf("float: %f\n", pi); |
| 19 | printf("scientific: %e\n", pi); |
| 20 | printf("compact: %g\n", pi); |
| 21 | |
| 22 | // Character and string |
| 23 | printf("char: %c\n", c); |
| 24 | printf("string: %s\n", str); |
| 25 | |
| 26 | // Pointer and special |
| 27 | printf("pointer: %p\n", (void *)ptr); |
| 28 | printf("hex: %x\n", 255); |
| 29 | printf("octal: %o\n", 255); |
| 30 | printf("percent: %%\n"); |
| 31 | |
| 32 | // Width and precision |
| 33 | printf("padded: [%10d]\n", n); |
| 34 | printf("left: [%-10d]\n", n); |
| 35 | printf("zero-pad: [%010d]\n", n); |
| 36 | printf("precision: [%.3f]\n", pi); |
| 37 | |
| 38 | return 0; |
| 39 | } |
Reading input with scanf requires matching format specifiers to the correct pointer types. Forgetting the & address-of operator is a common and dangerous mistake.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int i; |
| 5 | unsigned int u; |
| 6 | long l; |
| 7 | long long ll; |
| 8 | float f; |
| 9 | double d; |
| 10 | char c; |
| 11 | char str[64]; |
| 12 | unsigned int hex_val; |
| 13 | |
| 14 | printf("Enter an int: "); |
| 15 | scanf("%d", &i); |
| 16 | |
| 17 | printf("Enter an unsigned: "); |
| 18 | scanf("%u", &u); |
| 19 | |
| 20 | printf("Enter a long: "); |
| 21 | scanf("%ld", &l); |
| 22 | |
| 23 | printf("Enter a long long: "); |
| 24 | scanf("%lld", &ll); |
| 25 | |
| 26 | printf("Enter a float: "); |
| 27 | scanf("%f", &f); |
| 28 | |
| 29 | printf("Enter a double: "); |
| 30 | scanf("%lf", &d); |
| 31 | |
| 32 | printf("Enter a char: "); |
| 33 | scanf(" %c", &c); // space before %c skips whitespace |
| 34 | |
| 35 | printf("Enter a word: "); |
| 36 | scanf("%63s", str); // str is already a pointer, no & needed |
| 37 | |
| 38 | printf("Enter hex: "); |
| 39 | scanf("%x", &hex_val); |
| 40 | |
| 41 | printf("\nYou entered:\n"); |
| 42 | printf("int: %d, unsigned: %u\n", i, u); |
| 43 | printf("long: %ld, long long: %lld\n", l, ll); |
| 44 | printf("float: %f, double: %f\n", f, d); |
| 45 | printf("char: %c, string: %s\n", c, str); |
| 46 | printf("hex: 0x%x\n", hex_val); |
| 47 | |
| 48 | return 0; |
| 49 | } |
info
In C, uninitialized variables contain indeterminate values — reading them is undefined behavior. Always initialize variables explicitly. The C standard specifies different default initialization rules for different storage durations.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // Global and static variables are zero-initialized |
| 4 | int global_var; // guaranteed to be 0 |
| 5 | static int static_var; // guaranteed to be 0 |
| 6 | |
| 7 | int main(void) { |
| 8 | // Local (auto) variables — UNINITIALIZED, indeterminate value |
| 9 | int a; // DON'T read this — undefined behavior |
| 10 | int b = 0; // explicit initialization |
| 11 | int c = {42}; // brace initialization (valid in C) |
| 12 | int d = (int){0}; // compound literal, C99 |
| 13 | |
| 14 | // Partial initialization — remaining elements are zero |
| 15 | int arr[5] = {1, 2}; // {1, 2, 0, 0, 0} |
| 16 | |
| 17 | // Fully initialized with zeros |
| 18 | int zeros[100] = {0}; // all elements are 0 |
| 19 | |
| 20 | // Array initialization — size inferred |
| 21 | int auto_arr[] = {10, 20, 30}; // size is 3 |
| 22 | |
| 23 | // Character arrays |
| 24 | char greeting[] = "Hello"; // {'H','e','l','l','o','\0'} — 6 bytes |
| 25 | char exact[5] = "Hi"; // {'H','i','\0','\0','\0'} — 5 bytes |
| 26 | |
| 27 | // Initializer list overwriting |
| 28 | int mixed[4] = {[0] = 1, [3] = 9}; // {1, 0, 0, 9} — C99 |
| 29 | |
| 30 | printf("global: %d, static: %d\n", global_var, static_var); |
| 31 | printf("b: %d, c: %d, d: %d\n", b, c, d); |
| 32 | printf("arr: %d, %d, %d\n", arr[0], arr[1], arr[2]); |
| 33 | printf("greeting: %s\n", greeting); |
| 34 | |
| 35 | return 0; |
| 36 | } |
Storage classes determine a variable's lifetime (how long it exists), scope (where it can be accessed), and linkage (whether it's visible across translation units). C has four storage class specifiers: auto, static, extern, and register.
| Class | Lifetime | Scope | Linkage |
|---|---|---|---|
| auto | Block (stack) | Block | None |
| static (local) | Program duration | Block | None |
| static (file) | Program duration | File | Internal |
| extern | Program duration | File | External |
| register | Block (stack) | Block | None |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // extern — declares a variable defined in another file |
| 4 | extern int shared_count; |
| 5 | |
| 6 | // static at file scope — internal linkage, only visible in this file |
| 7 | static int internal_counter = 0; |
| 8 | |
| 9 | void increment(void) { |
| 10 | // auto — default storage class, automatic storage duration |
| 11 | auto int local = 0; // same as: int local = 0; |
| 12 | |
| 13 | // static local — persists across function calls |
| 14 | static int call_count = 0; |
| 15 | call_count++; |
| 16 | |
| 17 | internal_counter++; |
| 18 | printf("Call %d, internal: %d\n", call_count, internal_counter); |
| 19 | } |
| 20 | |
| 21 | int main(void) { |
| 22 | // register — hint to store in CPU register (cannot take address) |
| 23 | register int fast_var = 42; |
| 24 | // &fast_var would be an error |
| 25 | |
| 26 | for (int i = 0; i < 5; i++) { |
| 27 | increment(); |
| 28 | } |
| 29 | |
| 30 | printf("Final internal_counter: %d\n", internal_counter); |
| 31 | return 0; |
| 32 | } |
The following table summarizes all C primitive types, their typical sizes on 64-bit systems, ranges, format specifiers, and recommended use cases.
| Type | Bytes | Range | Printf | Use Case |
|---|---|---|---|---|
| char | 1 | -128 to 127 | %c | ASCII characters, small integers |
| unsigned char | 1 | 0 to 255 | %hhu | Raw bytes, pixel data, network |
| short | 2 | -32,768 to 32,767 | %hd | Small integers, audio samples |
| unsigned short | 2 | 0 to 65,535 | %hu | Port numbers, small counters |
| int | 4 | ±2.1 billion | %d | General-purpose integers |
| unsigned int | 4 | 0 to 4.29 billion | %u | Bitmasks, sizes, flags |
| long | 4/8 | platform-dependent | %ld | Large numbers, time_t |
| long long | 8 | ±9.2 × 10¹⁸ | %lld | 64-bit counters, timestamps |
| float | 4 | ±3.4 × 10³⁸ | %f | Graphics, games, memory-constrained |
| double | 8 | ±1.8 × 10³⁰⁸ | %f | Scientific computing (default float type) |
| long double | 8–16 | platform-dependent | %Lf | Precision-critical calculations |
| _Bool | 1 | 0 or 1 | %d | Boolean (C99 <stdbool.h>) |
| 1 | #include <stdio.h> |
| 2 | #include <limits.h> |
| 3 | #include <float.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | // Practical: use the right type for the job |
| 7 | unsigned char pixel = 255; // byte |
| 8 | short temperature = -40; // room sensor |
| 9 | unsigned int population = 7900000; // city population |
| 10 | long long bytes_on_disk = 5000000000LL; // 5GB file |
| 11 | double pi = 3.141592653589793; // math |
| 12 | _Bool flag = 1; // boolean (C99) |
| 13 | |
| 14 | printf("Pixel: %u (size: %zu)\n", pixel, sizeof(pixel)); |
| 15 | printf("Temp: %hd (size: %zu)\n", temperature, sizeof(temperature)); |
| 16 | printf("Pop: %u (size: %zu)\n", population, sizeof(population)); |
| 17 | printf("Bytes: %lld (size: %zu)\n", bytes_on_disk, sizeof(bytes_on_disk)); |
| 18 | printf("Pi: %f (size: %zu)\n", pi, sizeof(pi)); |
| 19 | printf("Flag: %d (size: %zu)\n", flag, sizeof(flag)); |
| 20 | |
| 21 | return 0; |
| 22 | } |