|$ curl https://forge-ai.dev/api/markdown?path=docs/c/variables
$cat docs/c-—-variables-&-data-types.md
updated Recently·40 min read·published

C — Variables & Data Types

CVariablesData TypesBeginnerBeginner🎯Free Tools
Introduction

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.

intro.c
C
1#include <stdio.h>
2#include <limits.h>
3
4int 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}
Primitive Types

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).

TypeTypical SizeMin ValueMax ValueFormat Specifier
char1 byte-128127%c / %hhd
short2 bytes-32,76832,767%hd
int4 bytes-2,147,483,6482,147,483,647%d
long4 or 8 bytes-2,147,483,6482,147,483,647+%ld
long long8 bytes-9.2 × 10¹⁸9.2 × 10¹⁸%lld
float4 bytes±1.2 × 10⁻³⁸±3.4 × 10³⁸%f
double8 bytes±2.2 × 10⁻³⁰⁸±1.8 × 10³⁰⁸%lf / %f
long double8–16 bytesplatform-dependentplatform-dependent%Lf
types.c
C
1#include <stdio.h>
2
3int 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}
Unsigned Types

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 TypeUnsigned TypeUnsigned MaxFormat Specifier
charunsigned char255%hhu
shortunsigned short65,535%hu
intunsigned int4,294,967,295%u
longunsigned longplatform-dependent%lu
long longunsigned long long1.8 × 10¹⁹%llu

warning

Mixing signed and unsigned types in expressions can produce unexpected results due to implicit conversion rules. When a signed and unsigned value are compared, the signed value is converted to unsigned — which can make negative numbers wrap around to very large positive values.
unsigned.c
C
1#include <stdio.h>
2
3int 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

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.

sizeof.c
C
1#include <stdio.h>
2
3int 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

Use sizeof to calculate array lengths at compile time. The expression sizeof(arr) / sizeof(arr[0]) gives the number of elements. However, this only works for arrays, not pointers — arrays decay to pointers when passed to functions.
Type Limits (limits.h and float.h)

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.

limits.c
C
1#include <stdio.h>
2#include <limits.h>
3#include <float.h>
4
5int 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}
Constants

C provides three mechanisms for defining constants: preprocessor macros with #define, the const qualifier, and enum values. Each has distinct tradeoffs.

constants.c
C
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
9const int buffer_size = 4096;
10const double GRAVITY = 9.81;
11
12// enum — integer constants, great for related groups
13enum Color { RED, GREEN, BLUE };
14enum Boolean { FALSE = 0, TRUE = 1 };
15
16int 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

Unlike C++, the const keyword in C does not make a value a true compile-time constant. You cannot use const in array sizes, case labels, or bit-field widths in standard C. Use #define or enum for those purposes. C99 introduced variable-length arrays (VLAs) that accept runtime values, but they are optional in C11+.
Type Qualifiers

Type qualifiers modify the behavior of a type without changing its fundamental nature. C has three qualifiers: const, volatile, and restrict (C99).

QualifierPurposeExample
constValue cannot be modified after initializationconst int x = 10;
volatileTells compiler the value may change externally (hardware, signal handler, another thread)volatile int flag;
restrictPointer is the sole alias to its object — enables optimizationvoid copy(int *restrict dst, const int *restrict src)
qualifiers.c
C
1#include <stdio.h>
2
3// volatile — essential for memory-mapped I/O and signal handlers
4volatile int heartbeat_counter = 0;
5
6void signal_handler(void) {
7 heartbeat_counter++; // compiler won't optimize this away
8}
9
10// restrict — promises the compiler no aliasing
11void 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
18int 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}
Implicit Type Conversions

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.

conversions.c
C
1#include <stdio.h>
2
3int 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

Assigning a value to a type that cannot represent it causes implementation-defined behavior for signed types. For unsigned types, the value wraps around modulo 2ⁿ. Always ensure values fit their target types.
Format Specifiers

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.

SpecifierTypeDescription
%dintSigned decimal integer
%uunsigned intUnsigned decimal integer
%ldlongSigned long decimal
%luunsigned longUnsigned long decimal
%lldlong longSigned long long decimal
%lluunsigned long longUnsigned long long decimal
%fdouble (promoted from float)Decimal floating point
%edoubleScientific notation
%lfdoubleDouble (for scanf only)
%Lflong doubleLong double floating point
%cchar (int promoted)Single character
%schar *Null-terminated string
%pvoid *Pointer address
%xunsigned intHexadecimal (lowercase)
%ounsigned intOctal
%zusize_tSize type (for sizeof result)
%%(none)Literal percent sign
specifiers.c
C
1#include <stdio.h>
2
3int 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}
scanf for Each Type

Reading input with scanf requires matching format specifiers to the correct pointer types. Forgetting the & address-of operator is a common and dangerous mistake.

scanf.c
C
1#include <stdio.h>
2
3int 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

Arrays decay to pointers when passed to functions, so scanf("%s", str) does not need &. But for all scalar variables (int, double, etc.), you must pass their address with &.
Initializer Rules & Default Values

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.

initializers.c
C
1#include <stdio.h>
2
3// Global and static variables are zero-initialized
4int global_var; // guaranteed to be 0
5static int static_var; // guaranteed to be 0
6
7int 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

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.

ClassLifetimeScopeLinkage
autoBlock (stack)BlockNone
static (local)Program durationBlockNone
static (file)Program durationFileInternal
externProgram durationFileExternal
registerBlock (stack)BlockNone
storage.c
C
1#include <stdio.h>
2
3// extern — declares a variable defined in another file
4extern int shared_count;
5
6// static at file scope — internal linkage, only visible in this file
7static int internal_counter = 0;
8
9void 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
21int 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}
Complete Type Reference

The following table summarizes all C primitive types, their typical sizes on 64-bit systems, ranges, format specifiers, and recommended use cases.

TypeBytesRangePrintfUse Case
char1-128 to 127%cASCII characters, small integers
unsigned char10 to 255%hhuRaw bytes, pixel data, network
short2-32,768 to 32,767%hdSmall integers, audio samples
unsigned short20 to 65,535%huPort numbers, small counters
int4±2.1 billion%dGeneral-purpose integers
unsigned int40 to 4.29 billion%uBitmasks, sizes, flags
long4/8platform-dependent%ldLarge numbers, time_t
long long8±9.2 × 10¹⁸%lld64-bit counters, timestamps
float4±3.4 × 10³⁸%fGraphics, games, memory-constrained
double8±1.8 × 10³⁰⁸%fScientific computing (default float type)
long double8–16platform-dependent%LfPrecision-critical calculations
_Bool10 or 1%dBoolean (C99 <stdbool.h>)
type-reference.c
C
1#include <stdio.h>
2#include <limits.h>
3#include <float.h>
4
5int 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}
$Blueprint — Engineering Documentation·Section ID: C-VARIABLES·Revision: 1.0