C Programming: Low-Level Mastery
HomeInsightsCoursesC ProgrammingVariables & Data Types
C Fundamentals

Variables & Data Types

Master C's type system with integers, floating-point numbers, and characters. Learn variable declaration, initialization, type sizes, and how data is stored in memory.

What is a Variable?

A variable is a named location in memory that stores a value. Think of it as a labeled box where you can put data, retrieve it later, and change it as needed. In C, every variable must have a specific data type that determines what kind of value it can hold, how much memory it occupies, and what operations you can perform on it.

Unlike high-level languages like Python where variables can hold any type, C requires you to declare each variable's type before using it. This static typing allows the compiler to catch type errors early and generate efficient machine code because it knows exactly how much memory each variable needs and how to perform operations on it.

C
/* Variable declaration and initialization */
int age;                  // Declaration: reserve memory for int
age = 25;                 // Assignment: store value 25

int count = 0;            // Declaration + initialization combined

/* Multiple variables of same type */
int x, y, z;              // Declare three integers
int a = 1, b = 2, c = 3;  // Declare and initialize

/* Variables must be declared before use */
// printf("%d\n", value);  // ERROR: 'value' not declared
int value = 10;
printf("%d\n", value);     // OK: value is declared

Basic Data Types

C provides several fundamental data types for storing different kinds of information. These primitive types are built into the language and map directly to hardware representations, giving C its efficiency. Understanding these types and their characteristics is essential for writing correct C programs.

Integer Types

Integers store whole numbers without fractional parts. C offers multiple integer types with different sizes and ranges. The int type is most common and typically 32 bits (4 bytes) on modern systems, capable of storing values from about -2 billion to +2 billion.

C
/* Integer types */
char c = 'A';             // Typically 1 byte (-128 to 127)
short s = 1000;           // Typically 2 bytes (-32,768 to 32,767)
int i = 100000;           // Typically 4 bytes (-2^31 to 2^31-1)
long l = 1000000000L;     // Typically 4 or 8 bytes
long long ll = 9223372036854775807LL;  // At least 8 bytes (C99)

/* Unsigned integers (no negative values) */
unsigned char uc = 255;   // 0 to 255
unsigned short us = 65535; // 0 to 65,535
unsigned int ui = 4000000000U;  // 0 to 2^32-1
unsigned long ul = 4294967295UL;

/* Check actual sizes on your system */
printf("char: %zu bytes\n", sizeof(char));       // 1 byte
printf("short: %zu bytes\n", sizeof(short));     // Usually 2
printf("int: %zu bytes\n", sizeof(int));         // Usually 4
printf("long: %zu bytes\n", sizeof(long));       // 4 or 8
printf("long long: %zu bytes\n", sizeof(long long)); // At least 8

Floating-Point Types

Floating-point types store numbers with fractional parts (decimals). They use IEEE 754 floating-point representation, which trades precision for range - they can represent huge or tiny numbers but with limited precision. Never use floats for exact decimal arithmetic (like money) due to rounding errors.

C
/* Floating-point types */
float f = 3.14f;          // Single precision (4 bytes, ~7 digits)
double d = 3.14159265359; // Double precision (8 bytes, ~15 digits)
long double ld = 3.14159265358979323846L; // Extended (10-16 bytes)

/* Scientific notation */
double big = 1.5e10;      // 1.5 × 10^10 = 15,000,000,000
double small = 3.2e-5;    // 3.2 × 10^-5 = 0.000032

/* Floating-point precision issues */
float x = 0.1f + 0.2f;
if (x == 0.3f) {  // Might be false due to rounding!
    printf("Equal\n");
}

/* Check sizes */
printf("float: %zu bytes\n", sizeof(float));          // 4
printf("double: %zu bytes\n", sizeof(double));        // 8  
printf("long double: %zu bytes\n", sizeof(long double)); // 10-16

Character Type

The char type stores single characters using ASCII or Unicode encoding. Despite being called a character type, char is actually a small integer (typically 1 byte). This dual nature means you can treat characters as small numbers and perform arithmetic on them.

C
/* Character type */
char letter = 'A';        // Single quotes for characters
char digit = '5';         // Character '5', not number 5
char newline = '\n';      // Escape sequences
char tab = '\t';

/* Characters are small integers */
char c = 'A';
printf("%c\n", c);        // Prints: A
printf("%d\n", c);        // Prints: 65 (ASCII value)

/* Character arithmetic */
char lower = 'a';
char upper = lower - 32;  // Convert to uppercase
printf("%c\n", upper);   // Prints: A

/* Escape sequences */
'\n'  // Newline
'\t'  // Tab
'\\'  // Backslash
'\''  // Single quote
'\"'  // Double quote
'\0'  // Null character (string terminator)

Type Modifiers

Type modifiers change the properties of basic types. The signed and unsigned modifiers control whether a type can hold negative values. The short and long modifiers affect the size and range. These modifiers give you fine control over memory usage and value ranges.

C
/* Signed vs Unsigned */
signed int si = -100;     // Can be negative (default for int)
unsigned int ui = 100;    // Only non-negative values

/* short and long */
short int si = 1000;      // Smaller range, less memory
long int li = 1000000L;   // Larger range, more memory
long long int lli = 9223372036854775807LL; // C99: even larger

/* Abbreviations (int is optional) */
short s = 1000;           // short int
long l = 1000000L;        // long int  
unsigned u = 100U;        // unsigned int

/* Range examples */
unsigned char: 0 to 255
signed char: -128 to 127
unsigned short: 0 to 65,535
short: -32,768 to 32,767
unsigned int: 0 to 4,294,967,295 (on 32-bit systems)
int: -2,147,483,648 to 2,147,483,647 (on 32-bit systems)

The sizeof Operator

The sizeof operator returns the size of a type or variable in bytes. It's evaluated at compile time, not runtime. Use sizeof to write portable code that adapts to different systems where int might be 2, 4, or 8 bytes. Always use sizeof rather than hardcoding sizes.

C
#include <stdio.h>

int main(void) {
    /* sizeof with types */
    printf("char: %zu bytes\n", sizeof(char));
    printf("int: %zu bytes\n", sizeof(int));
    printf("float: %zu bytes\n", sizeof(float));
    printf("double: %zu bytes\n", sizeof(double));
    printf("pointer: %zu bytes\n", sizeof(void*));
    
    /* sizeof with variables */
    int x = 42;
    double d = 3.14;
    printf("x: %zu bytes\n", sizeof(x));
    printf("d: %zu bytes\n", sizeof(d));
    
    /* sizeof with expressions (not evaluated!) */
    int y = 10;
    printf("%zu\n", sizeof(y++));  // Prints size, doesn't increment y
    printf("%d\n", y);              // Still 10, not 11!
    
    /* Arrays */
    int arr[10];
    printf("Array: %zu bytes\n", sizeof(arr));  // 10 * sizeof(int)
    printf("Elements: %zu\n", sizeof(arr) / sizeof(arr[0]));
    
    return 0;
}

Variable Initialization

Uninitialized variables in C contain garbage values - whatever random data was in that memory location. Reading uninitialized variables is undefined behavior and a common source of bugs. Always initialize variables before using them, even if you plan to assign them later.

C
/* Good: Initialize on declaration */
int count = 0;
float price = 19.99f;
char grade = 'A';

/* Bad: Uninitialized variables */
int x;
printf("%d\n", x);  // UNDEFINED BEHAVIOR: x contains garbage

/* Initialize to zero */
int a = 0;
float b = 0.0f;
char c = '\0';

/* Multiple initializations */
int x = 0, y = 0, z = 0;

/* Initialization vs Assignment */
int value = 10;      // Initialization (declaration + assignment)
value = 20;          // Assignment (changes existing variable)

/* Can't declare same variable twice in same scope */
int num = 5;
// int num = 10;     // ERROR: redeclaration
num = 10;            // OK: assignment

Type Ranges and Limits

Each type has minimum and maximum values it can represent. Exceeding these limits causes overflow (wrapping around) for unsigned types or undefined behavior for signed types. Use limits.h to check actual ranges on your system.

C
#include <stdio.h>
#include <limits.h>
#include <float.h>

int main(void) {
    /* Integer limits */
    printf("char: %d to %d\n", CHAR_MIN, CHAR_MAX);
    printf("short: %d to %d\n", SHRT_MIN, SHRT_MAX);
    printf("int: %d to %d\n", INT_MIN, INT_MAX);
    printf("long: %ld to %ld\n", LONG_MIN, LONG_MAX);
    
    printf("unsigned char: 0 to %u\n", UCHAR_MAX);
    printf("unsigned short: 0 to %u\n", USHRT_MAX);
    printf("unsigned int: 0 to %u\n", UINT_MAX);
    
    /* Float limits */
    printf("float min: %e\n", FLT_MIN);
    printf("float max: %e\n", FLT_MAX);
    printf("double min: %e\n", DBL_MIN);
    printf("double max: %e\n", DBL_MAX);
    
    /* Overflow example */
    unsigned char uc = 255;
    uc = uc + 1;  // Wraps to 0
    printf("%d\n", uc);  // Prints 0
    
    return 0;
}

Summary & What's Next

Key Takeaways:

  • ✅ Variables must be declared with a type before use
  • ✅ Integer types: char, short, int, long, long long
  • ✅ Floating-point types: float, double, long double
  • ✅ Use signed for negative values, unsigned for 0 and positive only
  • ✅ sizeof returns size in bytes
  • ✅ Always initialize variables before using them
  • ✅ Each type has size and range limits
  • ✅ Include limits.h and float.h for type ranges

What's Next?

Let's learn about constants and literals!