C Programming: Low-Level Mastery
C Fundamentals

Constants & Literals

Master constants, literals, and the const qualifier in C. Learn to create immutable values, use symbolic constants, and write safer, more maintainable code with proper constant usage.

Understanding Constants

Constants are values that cannot be changed during program execution. They're essential for writing maintainable code - using named constants instead of "magic numbers" makes your code self-documenting and easier to modify. If you need to change a value used throughout your program, updating one constant definition is much easier than finding and changing dozens of hardcoded numbers.

C provides two main ways to create constants: using the #define preprocessor directive to create symbolic constants, and using the const keyword to create read-only variables. Each approach has different characteristics and appropriate use cases. Understanding both helps you write clearer, safer code.

C
/* Using #define for constants */
#define PI 3.14159
#define MAX_SIZE 100
#define GREETING "Hello, World!"

int main(void) {
    double radius = 5.0;
    double area = PI * radius * radius;  // PI replaced at compile time
    printf("Area: %f\n", area);
    
    // PI = 3.14;  // ERROR: Can't assign to a macro
    return 0;
}

/* Using const keyword */
const int MAX_USERS = 50;
const float TAX_RATE = 0.08f;
const char NEWLINE = '\n';

// MAX_USERS = 100;  // ERROR: Can't modify const variable

Literal Values

Literals are fixed values written directly in your code. They represent themselves - the number 42 is an integer literal with value 42, the string "Hello" is a string literal. C recognizes several types of literals, each with specific syntax and rules. Understanding literal types helps you write correct expressions and avoid subtle bugs.

Integer Literals

Integer literals can be written in decimal (base 10), octal (base 8), hexadecimal (base 16), or binary (base 2, in C23). Suffixes indicate the type: L for long, LL for long long, U for unsigned. These suffixes help prevent overflow and ensure correct type matching.

C
/* Decimal literals */
int decimal = 100;
int negative = -42;

/* Octal literals (start with 0) */
int octal = 0144;        // 100 in decimal (1*64 + 4*8 + 4)

/* Hexadecimal literals (start with 0x or 0X) */
int hex = 0x64;          // 100 in decimal
int hex2 = 0xFF;         // 255 in decimal

/* Binary literals (C23, start with 0b or 0B) */
int binary = 0b1100100;  // 100 in decimal

/* Suffixes for type specification */
long l = 100L;           // Long literal
long long ll = 100LL;    // Long long literal
unsigned u = 100U;       // Unsigned literal
unsigned long ul = 100UL; // Unsigned long

/* Large numbers with digit separators (C23) */
long population = 8'000'000'000L;  // More readable
int hex_color = 0xFF'00'FF;        // RGB: 255, 0, 255

Floating-Point Literals

Floating-point literals contain decimal points or exponents. By default, they're double precision. Use the f or F suffix for float, and l or L for long double. Scientific notation (e or E) represents very large or small numbers compactly.

C
/* Floating-point literals */
double d1 = 3.14;        // Default: double
double d2 = 3.14159265;
float f = 3.14f;         // float suffix
float f2 = 3.14F;        // Capital F also works
long double ld = 3.14L;  // long double suffix

/* Scientific notation */
double avogadro = 6.022e23;      // 6.022 × 10^23
double electron = 9.109e-31;     // 9.109 × 10^-31
float speed_of_light = 2.998e8f; // 2.998 × 10^8

/* Different forms of same value */
double a = 100.0;
double b = 1e2;          // Same as 100.0
double c = 0.01;
double d = 1e-2;         // Same as 0.01

/* Must have f suffix for float */
float x = 3.14;   // Warning: double converted to float
float y = 3.14f;  // Correct: float literal

Character Literals

Character literals are enclosed in single quotes and represent a single character. They're actually integer values (ASCII or Unicode codes). Escape sequences represent special characters that can't be typed directly. Wide character literals (L prefix) support Unicode.

C
/* Character literals */
char letter = 'A';       // Single character
char digit = '9';        // Character '9', not number 9
char space = ' ';

/* Escape sequences */
char newline = '\n';     // Newline
char tab = '\t';         // Tab
char backslash = '\\';  // Backslash itself
char quote = '\'';       // Single quote
char dquote = '\"';      // Double quote
char null_char = '\0';   // Null character (0)

/* Numeric escape sequences */
char hex_char = '\x41';  // Hexadecimal (A)
char octal_char = '\101'; // Octal (A)

/* All these represent 'A' */
char a1 = 'A';           // Direct
char a2 = 65;            // ASCII value
char a3 = '\x41';        // Hex escape
char a4 = '\101';        // Octal escape

/* Character arithmetic */
char lower = 'a';
char upper = lower - 32; // Convert to 'A'
char next = 'B' + 1;     // Results in 'C'

String Literals

String literals are sequences of characters enclosed in double quotes. They're stored as arrays of characters with a null terminator (\\0) automatically added at the end. String literals are immutable - modifying them is undefined behavior.

C
/* String literals */
char *str = "Hello, World!";
printf("%s\n", str);

/* String with escape sequences */
char *message = "Line 1\nLine 2\tTabbed";

/* Adjacent strings concatenate automatically */
char *long_str = "This is a very long string "
                 "split across multiple lines "
                 "for readability.";

/* String literals are const */
char *s = "Hello";
// s[0] = 'h';  // UNDEFINED BEHAVIOR: modifying string literal

/* Use array for modifiable strings */
char modifiable[] = "Hello";
modifiable[0] = 'h';  // OK: array is writable
printf("%s\n", modifiable);  // Prints: hello

/* Special string literals */
char *empty = "";           // Empty string (just \0)
char *quote = "He said \"Hi\""; // Embedded quotes
char *path = "C:\\Users\\file";  // Backslashes

Using #define for Constants

The #define directive creates symbolic constants through text substitution. The preprocessor replaces every occurrence of the defined name with its value before compilation. This approach is simple and widely used, but has limitations - macros have no type safety and can cause subtle bugs if not used carefully.

C
#include <stdio.h>

/* Define constants at top of file or in header */
#define MAX_BUFFER 1024
#define PI 3.14159265359
#define TAX_RATE 0.08
#define COMPANY_NAME "Tech Corp"
#define DEBUG 1

/* Macro constants with expressions */
#define CIRCLE_AREA(r) (PI * (r) * (r))
#define MAX(a, b) ((a) &gt; (b) ? (a) : (b))

int main(void) {
    char buffer[MAX_BUFFER];
    double radius = 5.0;
    double area = CIRCLE_AREA(radius);
    
    printf("Area: %f\n", area);
    
    #ifdef DEBUG
        printf("Debug: MAX_BUFFER = %d\n", MAX_BUFFER);
    #endif
    
    return 0;
}

/* Advantages of #define */
// - Simple syntax
// - Works in any context (array sizes, case labels)
// - Conditional compilation (#ifdef)

/* Disadvantages */
// - No type checking
// - No scope (global throughout file)
// - Debugging harder (no symbol in debugger)
// - Potential side effects with macros

Using const Keyword

The const keyword creates read-only variables that have type safety and scope like regular variables. Unlike #define macros, const variables exist at runtime and appear in the debugger. They're generally preferred in modern C for their type safety, though they can't be used everywhere macros can.

C
#include <stdio.h>

int main(void) {
    /* const variables */
    const int MAX_SIZE = 100;
    const float PI = 3.14159f;
    const char GRADE = 'A';
    
    /* Type checking works */
    // const int x = "Hello";  // ERROR: type mismatch
    
    /* Cannot modify const */
    // MAX_SIZE = 200;  // ERROR: assignment to const
    
    /* const in function parameters */
    void print_value(const int value) {
        printf("%d\n", value);
        // value = 10;  // ERROR: can't modify
    }
    
    /* const with pointers (covered later) */
    const int *ptr1;       // Pointer to const int
    int *const ptr2 = NULL; // Const pointer to int
    const int *const ptr3 = NULL; // Const pointer to const int
    
    return 0;
}

/* Global const (file scope) */
const double GRAVITY = 9.81;
const char *APP_VERSION = "1.0.0";

/* Advantages of const */
// - Type safety
// - Scope control
// - Debugging support
// - Self-documenting code

/* Limitations */
// - Can't use for array sizes in C89
// - Can't use in case labels
// - Slightly more verbose

When to Use Each Approach

Both #define and const have their place in C programming. Understanding when to use each makes your code clearer and more maintainable. Modern C style generally prefers const for type safety, but #define remains necessary for certain situations.

C
/* Use #define for: */

// 1. Array sizes (C89)
#define ARRAY_SIZE 100
int array[ARRAY_SIZE];  // const won't work in C89

// 2. Conditional compilation
#define DEBUG 1
#ifdef DEBUG
    printf("Debug mode\n");
#endif

// 3. Include guards
#ifndef MYHEADER_H
#define MYHEADER_H
// Header contents
#endif

// 4. Complex macros
#define SQUARE(x) ((x) * (x))
#define MIN(a,b) ((a) < (b) ? (a) : (b))

/* Use const for: */

// 1. Type-safe constants
const int MAX_USERS = 50;  // Has type
const float TAX_RATE = 0.08f;

// 2. Function parameters (prevent modification)
void process(const char *input) {
    // input can't be modified
}

// 3. Pointers to read-only data
const char *ERROR_MSG = "Error occurred";

// 4. Local constants with scope
void function(void) {
    const int LOCAL_MAX = 10;  // Scope limited to function
}

/* Best practice: Use const by default, #define when necessary */
const double PI = 3.14159;        // Preferred
const int BUFFER_SIZE = 1024;    // Preferred

#define ARRAY_SIZE 100            // When needed for array size
#define DEBUG                     // When needed for conditionals

Enumeration Constants

Enumerations (enum) create sets of named integer constants. They're perfect for representing related options or states. The compiler assigns sequential integer values starting from 0, though you can specify custom values. Enums improve code readability and type safety.

C
#include <stdio.h>

/* Enumeration definition */
enum day {
    MONDAY,      // 0
    TUESDAY,     // 1
    WEDNESDAY,   // 2
    THURSDAY,    // 3
    FRIDAY,      // 4
    SATURDAY,    // 5
    SUNDAY       // 6
};

/* Enum with custom values */
enum status {
    SUCCESS = 0,
    ERROR = -1,
    PENDING = 100,
    TIMEOUT = 101
};

/* Enum with explicit values */
enum permissions {
    READ = 1,      // Binary: 001
    WRITE = 2,     // Binary: 010
    EXECUTE = 4    // Binary: 100
};

int main(void) {
    enum day today = FRIDAY;
    
    if (today == FRIDAY) {
        printf("It's Friday!\n");
    }
    
    /* Enum values are just integers */
    printf("MONDAY = %d\n", MONDAY);    // 0
    printf("FRIDAY = %d\n", FRIDAY);    // 4
    
    /* Can use in switch */
    switch (today) {
        case MONDAY:
            printf("Start of week\n");
            break;
        case FRIDAY:
            printf("Almost weekend!\n");
            break;
        default:
            printf("Regular day\n");
    }
    
    return 0;
}

Summary & What's Next

Key Takeaways:

  • ✅ Constants are immutable values that can't change
  • ✅ Literals: integer, floating-point, character, string
  • ✅ Use suffixes (L, U, f) to specify literal types
  • ✅ #define creates symbolic constants via text substitution
  • ✅ const keyword creates type-safe read-only variables
  • ✅ Prefer const for type safety in modern C
  • ✅ Use #define for conditional compilation and array sizes
  • ✅ Enumerations create sets of named integer constants

What's Next?

Now let's learn about input and output functions!