Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Comprehensive Guide to C Pointers: Fundamentals

Notes May 8 5

Memory Architecture and Addressing

Consider a large warehouse divided into storage units. Without a numbering system, locating a specific package would be incredibly inefficient. By assigning a unique number to each unit, retrieval becomes instantaneous. Computer memory operates on a similar principle.

The CPU processes data fetched from memory and writes the results back. Memory is divided into manageable units, each typically one byte in size. Every byte is assigned a unique identifier known as its address.

Hardware Addressing

Addresses are not stored in a list; rather, they are implemented through hardware design. The CPU communicates with memory via a set of wires called the address bus. On a 32-bit system, 32 address lines exist. Each line can transmit a high or low signal (1 or 0). The combination of these 32 binary digits forms an address, allowing for 2^32 unique memory locations. When an address is sent to memory, the corresponding byte is located, and data is transferred over the data bus.

Pointers and Address Operators

Extracting Addresses with &

Declaring a variable allocates memory space. For instance, creating an integer requests 4 bytes.

#include <stdio.h>

int main() {
    int count = 25;
    printf("Address of count: %p\n", &count);
    return 0;
}

The & operator retrieves the memory address of a variable. Since an integer occupies 4 bytes, the address returned is the lowest byte among the four.

Storing Addresses in Pointer Variables

A pointer variable is designed specifically to hold memory addresses.

#include <stdio.h>

int main() {
    int count = 25;
    int* ptr = &count; // ptr stores the address of count
    return 0;
}

The type int* signifies a pointer to an integer.

Dereferencing with *

To access or modify the value at the stored address, the dereference operator * is used.

#include <stdio.h>

int main() {
    int count = 25;
    int* ptr = &count;
    *ptr = 50; // Modifies the value of count
    printf("New count: %d\n", count);
    return 0;
}

Pointer Size

The size of a pointer depends on the system architecture, not the data type it points to. On a 32-bit system, addresses are 32 bits (4 bytes), while 64-bit systems use 64-bit addresses (8 bytes).

#include <stdio.h>

int main() {
    printf("Size of char*: %zu\n", sizeof(char*));
    printf("Size of int*: %zu\n", sizeof(int*));
    printf("Size of double*: %zu\n", sizeof(double*));
    return 0;
}

Purpose of Pointer Types

If all pointers share the same size, why do we need different types? The type determines how the pointer interprets memory during dereferencing and arithmetic.

Dereferencing Permissions

#include <stdio.h>

int main() {
    int data = 0x11223344;
    int* int_ptr = &data;
    *int_ptr = 0; // Clears all 4 bytes

    int data2 = 0x11223344;
    char* char_ptr = (char*)&data2;
    *char_ptr = 0; // Clears only the first byte
    return 0;
}

An int* accesses 4 bytes upon dereference, whereas a char* accesses only 1 byte.

Pointer Arithmetic

#include <stdio.h>

int main() {
    int val = 10;
    int* ip = &val;
    char* cp = (char*)&val;

    printf("ip: %p, ip+1: %p\n", ip, ip + 1);
    printf("cp: %p, cp+1: %p\n", cp, cp + 1);
    return 0;
}

Adding 1 to an int* increments the address by 4 bytes (size of an int). Adding 1 to a char* increments it by 1 byte.

Generic Pointers (void*)

A void* pointer can hold the address of any data type. However, it cannot be dereferenced directly or used in pointer arithmetic because its target size is unknown.

#include <stdio.h>

int main() {
    int num = 5;
    void* generic_ptr = &num;
    // *generic_ptr = 10; // Error: cannot dereference void*
    return 0;
}

Applying const to Pointers

The const keyword can enforce read-only behavior, but its position changes the restriction.

Pointer to Constant Data

When const precedes the type, the data cannot be modified through the pointer, but the pointer itself can point to a different address.

int x = 10, y = 20;
const int* ptr = &x;
// *ptr = 15; // Error
ptr = &y;    // Allowed

Constant Pointer

When const follows the asterisk, the pointer must always point to the same address, but the data at that address can be changed.

int x = 10, y = 20;
int* const ptr = &x;
*ptr = 15;    // Allowed
// ptr = &y; // Error

Pointer Operations

Pointer Addition and Subtraction

Since arrays are stored contiguously, pointer arithmetic is ideal for traversal.

#include <stdio.h>

int main() {
    int records[5] = {10, 20, 30, 40, 50};
    for (int i = 0; i < 5; i++) {
        printf("%d ", *(records + i));
    }
    return 0;
}

Pointer Difference

Subtracting two pointers yields the number of elements between them. Adding pointers is undefined.

#include <stdio.h>

int str_len(char* str) {
    char* start = str;
    while (*str != '\0') {
        str++;
    }
    return str - start;
}

int main() {
    printf("Length: %d\n", str_len("hello"));
    return 0;
}

Relational Operations

Pointers can be compared using relational operators, which is useful for iteration limits.

#include <stdio.h>

int main() {
    int records[5] = {1, 2, 3, 4, 5};
    int* cursor = records;
    int* end = records + 5;

    while (cursor < end) {
        printf("%d ", *cursor);
        cursor++;
    }
    return 0;
}

Wild Pointers

Wild pointers reference unpredictable or invalid memory locations. They often lead to crashes or data corruption.

Common Causes

Uninitialized Pointers: Local pointers default to garbage values.

int* ptr;
printf("%d", *ptr); // Undefined behavior

Out-of-Bounds Access: Accessing memory beyond an allocated array.

int arr[3] = {0};
for (int i = 0; i <= 3; i++) {
    arr[i] = i; // Out of bounds when i == 3
}

Dangling Pointers: Pointing to memory that has been deallocated or destroyed.

int* create_value() {
    int local = 42;
    return &local; // Warning: address of local variable returned
}

int main() {
    int* ptr = create_value();
    printf("%d", *ptr); // Undefined behavior
    return 0;
}

Preventing Wild Pointers

Initialization: Assign a valid address or NULL upon declaration. NULL represents a non-functional address (0).

int val = 10;
int* p1 = &val;
int* p2 = NULL;

Bounds Checking: Ensure pointer arithmetic remains within allocated boundaries.

Nullify Unused Pointers: Set pointers to NULL when their target memory is no longer needed. Always validate pointers before dereferencing.

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    int* ptr = arr;

    for (int i = 0; i < 3; i++) {
        *(ptr + i) = 0;
    }

    ptr = NULL; // Safe state

    // Later usage
    ptr = arr;
    if (ptr != NULL) {
        *ptr = 10; // Safe
    }
    return 0;
}

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.