Case Study 2: From Pascal Pointers to C Pointers
Overview
One of the core themes of this textbook is that discipline learned in Pascal transfers directly to other languages. Nowhere is this more evident than with pointers. This case study compares Pascal's pointer model — type-safe, no arithmetic, explicit New/Dispose — with C's more permissive pointer model — malloc/free, void*, full pointer arithmetic. We will see that every concept from Chapter 14 has a direct C counterpart, and that the safety habits you develop in Pascal become essential survival skills in C.
The Same Concepts, Different Syntax
Let us place Pascal and C side by side for every pointer operation we learned in this chapter.
Declaring Pointer Types
Pascal:
type
PInteger = ^Integer;
var
p: PInteger;
C:
int *p;
In Pascal, we typically create a named pointer type (PInteger) and then declare variables of that type. In C, the pointer-ness is part of the variable declaration itself: the * before p means "p is a pointer to int." C does not require (but allows) separate typedef declarations for pointer types.
The discipline: In C, complex declarations can become hard to read (int **pp is a pointer to a pointer to an int). Pascal's named types (PInteger, PPInteger) make complex pointer types more readable. When you move to C, consider using typedef to name your pointer types — a habit directly inherited from Pascal's type block.
Allocation and Deallocation
Pascal:
New(p); { Allocates sizeof(Integer) bytes }
p^ := 42;
Dispose(p); { Frees the memory }
p := nil;
C:
p = malloc(sizeof(int)); /* Allocates sizeof(int) bytes */
*p = 42;
free(p); /* Frees the memory */
p = NULL;
Key differences:
-
Newknows the type;mallocdoes not. Pascal'sNew(p)automatically allocates the right amount of memory because the compiler knowspis aPInteger. C'smalloctakes an explicit byte count — you must calculatesizeof(int)yourself. Getting the size wrong is a bug. -
mallocreturnsvoid*. In C,mallocreturns an untyped pointer (void *) that can be assigned to any pointer type without a cast (in C, though C++ requires a cast). This means you can accidentally assign the result to the wrong pointer type with no compiler warning. -
C has no type safety on
free. You can pass any pointer tofree, regardless of how it was allocated. Pascal'sDisposeat least requires a typed pointer.
The discipline: The habit of writing Dispose(p); p := nil; in Pascal becomes free(p); p = NULL; in C. It is exactly the same pattern. The habit of matching every New with a Dispose becomes matching every malloc with a free. These are not Pascal-specific practices — they are universal memory management discipline.
Dereferencing
Pascal:
p^ := 42; { Write through pointer }
x := p^; { Read through pointer }
student^.Name := 'Alice'; { Dereference + field access }
C:
*p = 42; /* Write through pointer */
x = *p; /* Read through pointer */
student->name = "Alice"; /* Dereference + field access (-> operator) */
Pascal uses ^ postfix; C uses * prefix. Pascal uses ^. for record fields; C uses the -> operator (which combines dereference and member access). The semantics are identical — follow the address, access the data.
Address-Of
Pascal:
p := @x; { p gets the address of x }
C:
p = &x; /* p gets the address of x */
Same concept, different symbol. The warnings are the same in both languages: never return the address of a local variable from a function.
Nil/Null Checks
Pascal:
if p <> nil then
WriteLn(p^);
C:
if (p != NULL)
printf("%d\n", *p);
Identical pattern. Same reason. Same bugs if omitted.
What C Adds: The Dangerous Features
Pointer Arithmetic
This is the biggest difference. In C, you can add an integer to a pointer:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; /* p points to arr[0] */
p = p + 2; /* p now points to arr[2] */
printf("%d\n", *p); /* Prints 30 */
When you add n to a pointer of type int *, the pointer advances by n * sizeof(int) bytes. This is how C arrays work under the hood: arr[i] is syntactic sugar for *(arr + i).
Pascal deliberately forbids this. Why? Because pointer arithmetic is the root cause of buffer overflow vulnerabilities — one of the most exploited bug categories in the history of computing. If a C programmer increments a pointer past the end of an array, the pointer lands on whatever happens to be adjacent in memory. Reading that memory leaks data; writing to it corrupts data or executes malicious code.
The discipline: Because you learned pointers in Pascal — where pointer arithmetic does not exist — you have built your mental model around pointer-to-node structures (linked lists, trees) rather than pointer-to-element arithmetic. When you encounter C, you will be less likely to rely on raw pointer arithmetic and more likely to use array indexing (which is bounds-checked by many tools) or data structure abstractions.
Void Pointers
C's void * is a pointer that can point to anything:
void *generic = malloc(100); /* 100 bytes, no type */
int *ip = (int *)generic; /* Cast to int pointer */
char *cp = (char *)generic; /* Or cast to char pointer */
Pascal's Pointer type is similar but more restricted — you cannot dereference it without casting. The danger of void * in C is that you can cast it to the wrong type, and the compiler will not object:
double *dp = (double *)generic; /* Wrong cast — but compiles fine */
*dp = 3.14; /* Writes 8 bytes into a 4-byte int space */
The discipline: In Pascal, typed pointers prevent this class of bug entirely. You develop the habit of thinking about pointer types carefully. Carry this habit into C: minimize void * usage, and when you must use it, document the intended type clearly.
Manual Size Calculations
In C, you must manually specify the size for allocation:
/* Correct */
TStudent *s = malloc(sizeof(TStudent));
/* Bug: allocated the size of the pointer, not the struct */
TStudent *s = malloc(sizeof(s)); /* sizeof(s) is 4 or 8 bytes! */
This bug — allocating sizeof(pointer) instead of sizeof(pointed-to-type) — is extremely common in C and causes subtle memory corruption. Pascal's New never has this problem because it always allocates based on the pointer's declared type.
The discipline: In C, always write malloc(sizeof(*p)) rather than malloc(sizeof(SomeType)). This way, if the type of p changes, the allocation automatically stays correct. This defensive habit mirrors Pascal's automatic size calculation.
A Side-by-Side Example: Linked List
Let us implement the simple linked chain from Section 14.9 in both Pascal and C to see the direct correspondence.
Pascal Version
program LinkedChainPascal;
type
PNode = ^TNode;
TNode = record
Value: Integer;
Next: PNode;
end;
var
Head, Temp: PNode;
procedure AddNode(var Head: PNode; Val: Integer);
var
NewNode: PNode;
begin
New(NewNode);
NewNode^.Value := Val;
NewNode^.Next := Head;
Head := NewNode;
end;
procedure PrintList(Head: PNode);
var
Current: PNode;
begin
Current := Head;
while Current <> nil do
begin
Write(Current^.Value, ' ');
Current := Current^.Next;
end;
WriteLn;
end;
procedure FreeList(var Head: PNode);
begin
while Head <> nil do
begin
Temp := Head;
Head := Head^.Next;
Dispose(Temp);
end;
end;
begin
Head := nil;
AddNode(Head, 30);
AddNode(Head, 20);
AddNode(Head, 10);
PrintList(Head); { Output: 10 20 30 }
FreeList(Head);
end.
C Version
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
void addNode(Node **head, int val) {
Node *newNode = malloc(sizeof(Node));
newNode->value = val;
newNode->next = *head;
*head = newNode;
}
void printList(Node *head) {
Node *current = head;
while (current != NULL) {
printf("%d ", current->value);
current = current->next;
}
printf("\n");
}
void freeList(Node **head) {
Node *temp;
while (*head != NULL) {
temp = *head;
*head = (*head)->next;
free(temp);
}
}
int main() {
Node *head = NULL;
addNode(&head, 30);
addNode(&head, 20);
addNode(&head, 10);
printList(head); /* Output: 10 20 30 */
freeList(&head);
return 0;
}
Comparison Notes
| Aspect | Pascal | C |
|---|---|---|
| Forward reference | PNode = ^TNode before TNode |
struct Node *next inside struct Node |
| Pass-by-reference for Head | var Head: PNode |
Node **head (pointer to pointer) |
| Allocation | New(NewNode) |
malloc(sizeof(Node)) |
| Field access via pointer | NewNode^.Value |
newNode->value |
| Null sentinel | nil |
NULL |
| Deallocation | Dispose(Temp) |
free(temp) |
| Traversal pattern | Identical | Identical |
| Deallocation pattern | Identical (save-advance-free) | Identical |
The structure, the logic, and the patterns are the same. If you can write a linked list in Pascal, you can write one in C — the translation is nearly mechanical. The difference is that C will not stop you from making type errors, arithmetic errors, or size errors that Pascal prevents.
The C++ Evolution: Smart Pointers
C++ recognized that manual malloc/free is error-prone and introduced smart pointers that automate deallocation:
#include <memory>
// unique_ptr: exactly one owner, auto-freed when owner goes out of scope
std::unique_ptr<int> p = std::make_unique<int>(42);
// No need to call delete — freed automatically
// shared_ptr: reference-counted, freed when last reference is destroyed
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1; // Two owners now
// Freed when both p1 and p2 go out of scope
The unique_ptr is the compiler-enforced version of the ownership discipline from Section 14.8. The shared_ptr automates the case where multiple pointers legitimately need to share ownership. These are the same concepts you learned in Pascal — automated by the C++ standard library.
The Rust Endgame: Compiler-Enforced Safety
Rust takes the ownership principle and makes it a compile-time requirement:
fn main() {
let p = Box::new(42); // Allocate on heap, p owns it
let q = p; // Ownership MOVES to q; p is now invalid
// println!("{}", p); // COMPILE ERROR: p was moved
println!("{}", q); // OK: q owns the data
} // q goes out of scope — memory freed automatically
In Rust, the "pointer copied but original still used" bug from Section 14.5 is a compile-time error. The "dangling pointer after free" bug cannot occur because Rust automatically frees memory when the owner goes out of scope, and it disallows use-after-free at compile time.
Every concept in Rust's ownership system maps directly to what you practiced in this chapter:
- Ownership = one pointer responsible for Dispose
- Move semantics = ownership transfer (set old pointer to nil)
- Borrowing = temporary alias that must not free
- Lifetime = ensuring references do not outlive the data they point to
Key Takeaways
-
Pascal and C pointers are the same concept with different syntax and safety levels.
New=malloc,Dispose=free,^=*,@=&,nil=NULL. -
C adds dangerous capabilities — pointer arithmetic, void pointers, manual size calculation — that Pascal deliberately omits. These additions enable buffer overflows, type confusion, and size mismatch bugs.
-
The defensive habits you build in Pascal transfer directly to C: nil-checking before dereference, setting pointers to nil after free, matching every allocation with a deallocation, using the ownership principle.
-
The evolution from Pascal to C to C++ to Rust is a progression of enforcing the same discipline at increasingly higher levels: manual convention (Pascal) -> manual discipline in a dangerous environment (C) -> library automation (C++ smart pointers) -> compiler enforcement (Rust).
-
Learning pointers in Pascal first is the ideal preparation. You internalize the concepts in a safe environment where the compiler prevents the worst mistakes. When you move to C, you carry the discipline with you. When you reach Rust, the ownership model feels natural because you have been practicing it all along.