C Advanced Concepts - Memory, Operators, and Special Topics

This page covers intricate topics in C, including dynamic memory allocation, a deeper dive into memory representation (endianness, alignment), advanced operators, and the nuances of integer overflow/underflow and floating-point precision.

1. Dynamic Memory Allocation

Allocating memory during program execution (runtime) using functions like malloc(), calloc(), realloc(), and free(). This is crucial for managing data structures whose size isn't known at compile time.

Code Example (Dynamic Memory Allocation)

Copied!
#include <stdio.h>
#include <stdlib.h> // For malloc, calloc, realloc, free

int main() {
    int *arr;
    int n;

    printf("Enter the number of elements: ");
    scanf("%d", &n);

    // Allocate memory for n integers using malloc
    arr = (int *) malloc(n * sizeof(int));

    // Check if malloc was successful
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1; // Indicate error
    }

    printf("Enter %d integers:\n", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }

    printf("The entered elements are: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // Reallocate memory for more elements
    int new_n = n + 2;
    arr = (int *) realloc(arr, new_n * sizeof(int));
    if (arr == NULL) {
        printf("Memory reallocation failed!\n");
        return 1;
    }

    printf("Enter %d more integers for new_n:\n", 2);
    for (int i = n; i < new_n; i++) {
        scanf("%d", &arr[i]);
    }

    printf("Elements after realloc: ");
    for (int i = 0; i < new_n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");


    // Free the allocated memory
    free(arr);
    arr = NULL; // Good practice to set freed pointer to NULL

    return 0;
}
            

Expected Output

Enter the number of elements: 3
Enter 3 integers:
1 2 3
The entered elements are: 1 2 3
Enter 2 more integers for new_n:
4 5
Elements after realloc: 1 2 3 4 5
            

(User input is shown on the same line as the prompt in this example.)

2. Memory Representation and Endianness

How data is physically stored in memory.

Code Example (Endianness)

Copied!
#include <stdio.h>

int main() {
    unsigned int i = 1;
    char *c = (char*)&i; // Point to the first byte of 'i'

    if (*c) {
        printf("System is Little-Endian\n");
    } else {
        printf("System is Big-Endian\n");
    }

    printf("Value of i: %u\n", i);
    printf("First byte: %d\n", *c); // Will be 1 for little-endian, 0 for big-endian

    return 0;
}
            

Expected Output (on a typical x86 system)

System is Little-Endian
Value of i: 1
First byte: 1
            

Code Example (Struct Padding - Conceptual)

Copied!
#include <stdio.h>
#include <stddef.h> // For offsetof

// Without explicit packing
struct Example1 {
    char c1;    // 1 byte
    int i;      // 4 bytes
    char c2;    // 1 byte
};

// With members ordered by size (often reduces padding)
struct Example2 {
    int i;      // 4 bytes
    char c1;    // 1 byte
    char c2;    // 1 byte
};

// Using pragma pack (compiler-specific, might not be portable)
// #pragma pack(push, 1) // Set packing to 1 byte
// struct Example3 {
//     char c1;
//     int i;
//     char c2;
// };
// #pragma pack(pop) // Restore default packing

int main() {
    printf("Size of Example1: %zu bytes\n", sizeof(struct Example1));
    printf("Size of Example2: %zu bytes\n", sizeof(struct Example2));
    // printf("Size of Example3 (packed): %zu bytes\n", sizeof(struct Example3)); // Uncomment if using pragma pack

    // Output of Example1 will likely be 12 (1 + 3 (padding) + 4 + 1 + 3 (padding))
    // Output of Example2 will likely be 8 (4 + 1 + 1 + 2 (padding))
    // The actual sizes depend on compiler, architecture, and default alignment.

    return 0;
}
            

Expected Output (actual values may vary, but demonstrate padding)

Size of Example1: 12 bytes
Size of Example2: 8 bytes
            

3. Operators (Beyond Arithmetic/Relational/Logical - Bitwise, sizeof, Type Casting)

Code Example (Bitwise, sizeof, Type Casting)

Copied!
#include <stdio.h>

int main() {
    // Bitwise operators
    int a = 5;  // Binary: 0101
    int b = 3;  // Binary: 0011

    printf("a & b (AND): %d\n", a & b);   // 0001 (1)
    printf("a | b (OR): %d\n", a | b);    // 0111 (7)
    printf("a ^ b (XOR): %d\n", a ^ b);   // 0110 (6)
    printf("~a (NOT) (for 8-bit, 2's complement): %d\n", ~a); // Depends on int size, usually -6 for 32-bit int
    printf("a << 1 (Left Shift): %d\n", a << 1); // 1010 (10)
    printf("b >> 1 (Right Shift): %d\n", b >> 1); // 0001 (1)

    // sizeof operator
    printf("Size of int: %zu bytes\n", sizeof(int));
    printf("Size of double: %zu bytes\n", sizeof(double));
    printf("Size of a: %zu bytes\n", sizeof(a));

    // Type Casting
    float f_val = 10.75;
    int i_val = (int)f_val; // Cast float to int (truncates decimal part)
    printf("Float value: %.2f, Integer value after cast: %d\n", f_val, i_val);

    int int_sum = 5;
    int int_count = 2;
    double average = (double)int_sum / int_count; // Cast one operand to double for float division
    printf("Average: %.2f\n", average);

    return 0;
}
            

Expected Output

a & b (AND): 1
a | b (OR): 7
a ^ b (XOR): 6
~a (NOT) (for 8-bit, 2's complement): -6
a << 1 (Left Shift): 10
b >> 1 (Right Shift): 1
Size of int: 4 bytes
Size of double: 8 bytes
Size of a: 4 bytes
Float value: 10.75, Integer value after cast: 10
Average: 2.50
            

4. Integer Overflow/Underflow and Floating-Point Precision

Code Example (Overflow/Underflow/Precision)

Copied!
#include <stdio.h>
#include <limits.h> // For INT_MAX, INT_MIN
#include <float.h>  // For DBL_EPSILON
#include <math.h>   // For fabs

int main() {
    // Integer Overflow
    int max_int = INT_MAX;
    printf("Max int: %d\n", max_int);
    printf("Max int + 1: %d\n", max_int + 1); // Undefined behavior for signed int

    unsigned int max_unsigned_int = UINT_MAX;
    printf("Max unsigned int: %u\n", max_unsigned_int);
    printf("Max unsigned int + 1: %u\n", max_unsigned_int + 1); // Wraps around to 0

    // Integer Underflow
    int min_int = INT_MIN;
    printf("Min int: %d\n", min_int);
    printf("Min int - 1: %d\n", min_int - 1); // Undefined behavior for signed int

    // Floating-point precision
    float sum_float = 0.0f;
    for (int i = 0; i < 1000000; i++) {
        sum_float += 0.000001f; // Adding a small number many times
    }
    printf("Sum of 1,000,000 * 0.000001 (float): %.10f\n", sum_float); // Might not be exactly 1.0

    double sum_double = 0.0;
    for (int i = 0; i < 1000000; i++) {
        sum_double += 0.000001; // Adding a small number many times
    }
    printf("Sum of 1,000,000 * 0.000001 (double): %.10lf\n", sum_double); // More accurate, closer to 1.0

    // Comparing floating-point numbers (use an epsilon)
    double a = 0.1 + 0.2;
    double b = 0.3;
    if (a == b) { // This comparison might be false due to precision issues
        printf("0.1 + 0.2 == 0.3 (direct comparison)\n");
    } else {
        printf("0.1 + 0.2 != 0.3 (direct comparison)\n");
    }

    // Correct way to compare floats
    if (fabs(a - b) < DBL_EPSILON) {
        printf("0.1 + 0.2 == 0.3 (with epsilon comparison)\n");
    } else {
        printf("0.1 + 0.2 != 0.3 (with epsilon comparison)\n");
    }

    return 0;
}
            

Expected Output (actual results for max_int + 1 and min_int - 1 are undefined, but often show wraparound)

Max int: 2147483647
Max int + 1: -2147483648
Max unsigned int: 4294967295
Max unsigned int + 1: 0
Min int: -2147483648
Min int - 1: 2147483647
Sum of 1,000,000 * 0.000001 (float): 0.9999999404
Sum of 1,000,000 * 0.000001 (double): 1.0000000000
0.1 + 0.2 != 0.3 (direct comparison)
0.1 + 0.2 == 0.3 (with epsilon comparison)