Data types II: record, union, pointer and type equivalence
Records and tuples, free and discriminated unions, pointers/references, dangling pointers and memory leaks, heap management, type checking, strong typing and name/structure type equivalence.
The previous page was about types that hold single values. This page covers types that bring values together (record, union), types that point to other values (pointer, reference), and the compatibility of types with each other (type checking, type equivalence). The most asked on exams: the difference between a dangling pointer and a memory leak, free and discriminated unions, name and structure equivalence.
Golden rules:
- Dangling pointer: a pointer that points to a deleted cell. Leak: a cell that nobody points to.
- A free union is not checked, a discriminated union carries a tag.
- Name equivalence = same name, structure equivalence = same structure.
- Reference counting cannot collect cycles.
Record
A record is a collection of heterogeneous elements (fields) accessed by their names. Unlike an array, the elements can be of different types; access is by name, not index: emp.name. COBOL and C struct, classes and record in Java.
record P(int x, int y) {}
P p = new P(1, 2), q = new P(1, 2);
System.out.println(p.equals(q) + " " + (p == q));
System.out.println(p);
true false
P[x=1, y=2]
Field access is translated at compile time into a fixed offset, which is why it is fast. A tuple is similar to a record but the fields have no names (Python (3, "a"), ML); access is by position.
Union
A union is a type in which a variable can hold values of different types at different times during execution. All fields share the same memory.
- Free union (C, C++): nothing keeps track of which field is valid. No type checking.
- Discriminated union (Ada, ML, Haskell): a tag (discriminant) field says which type is valid; reading the wrong field is an error.
union { int i; float f; } u;
u.i = 1;
printf("%g", u.f); /* the bits of the int are read as a float */
The float interpretation of the same bits can be shown with Java:
System.out.println(Float.intBitsToFloat(1));
System.out.println(Float.intBitsToFloat(1065353216));
System.out.println(Float.floatToIntBits(1.0f));
1.4E-45
1.0
1065353216
The integer 1 as a float is 1.4E-45; the bits of 1.0f as an int are 1065353216. A free union makes this confusion silently. Java and C# did not take unions at all; for reliability, inheritance is used.
Pointer and reference
A pointer holds a memory address or the special value nil (null). It has two uses: indirect addressing and dynamic memory management on the heap.
| Operation | C/C++ | Meaning |
|---|---|---|
| Assignment | p = &x; | p holds the address of x |
| Dereference | *p | the value in the cell p points to |
| Arithmetic | p + 1 | The next element; the address increases by sizeof(type) |
| Field access | p->x | (*p).x |
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* p = &a[0] */
p += 2; /* p = &a[2] */
/* *p + 1 = 31, *(p + 1) = 40, p - a = 2 */
A reference type (C++ int&, Java and C# class variables) points to an object but arithmetic cannot be done. In Java, references never point to freed memory, because freeing is the job of the garbage collector. That is why there are no dangling references in Java.
Pointer problems
Dangling pointer
A pointer that still holds the address of a freed (deallocated) heap cell.
int *p = new int(5);
int *q = p;
delete p; // p and q now point to the freed cell
You can set p to nullptr, but you forget q. If that cell is later given to another variable, writing through q overwrites other data.
Lost heap-dynamic variable (memory leak)
An allocated heap cell that no pointer can reach anymore. These are called garbage.
int *p = new int; // cell 1
p = new int; // cell 2; cell 1 is LOST
int *q = p; // q points to cell 2
p = new int; // cell 3; cell 2 was saved thanks to q
The number of lost cells is 1. On an exam, number every new, and at the end write which pointer points to each cell. If nobody points to it, it is a leak; if something points to it but the cell is deleted, it is dangling.
Dangling pointer solutions
- Tombstone: an extra cell for each heap object; pointers point to this cell. When the object is deleted, the tombstone becomes nil. Cost: time and memory.
- Locks-and-keys: a pointer is a (key, address) pair; the heap cell carries a lock. On access key == lock is checked; the lock of a deleted cell is changed.
- Best: not giving the programmer the power to delete (Java, C#, LISP: garbage collection).
Heap management: garbage collection
| Method | How | Pro | Con |
|---|---|---|---|
| Reference counting (eager) | Each cell counts how many pointers point to it, freed at 0 | Collection is immediate, no pauses | Memory and time for counters; cannot collect cycles |
| Mark-sweep (lazy) | When memory runs out: first consider everything garbage, mark what is reachable from the root, collect the unmarked | Collects cycles | The program stops during collection |
A.next = B; B.next = A; root = A; root = null;
reference count: A = 1 (from B), B = 1 (from A) → neither is 0, not collected
mark-sweep: no path from the root to A → both unmarked → collected
Type checking and strong typing
Type checking is checking that the operands of an operator are of compatible types. A compatible type is either directly valid or can be converted automatically by the language’s rules (coercion). Using an operand of an inappropriate type is a type error.
If type bindings are static, checking can be done almost entirely at compile time; with dynamic type binding (JavaScript, Python) checking has to be done at run time. The earlier you catch an error, the cheaper it is to fix.
A strongly typed language: type errors are always caught. ML and F# are strongly typed. Java and C# nearly (an explicit cast can produce an error but it is caught at run time). C and C++ are not: free unions and pointer conversions are outside checking. Too much coercion reduces the benefit of strong typing.
Type equivalence
When are two types considered “the same”?
| Name type equivalence | Structure type equivalence | |
|---|---|---|
| Rule | If declared with the same type name | If the structure of the types is the same |
| Implementation | Easy | Hard (nested structures, ordering, array bounds) |
| Flexibility | Restrictive | Flexible |
| Safety | Differently named types don’t get mixed | Types that should be kept apart can get mixed |
| Used by | Ada, Java classes | C (except struct and union) |
type Celsius = float;
type Fahrenheit = float;
c: Celsius; f: Fahrenheit;
f := c; name equivalence: ERROR structure equivalence: valid
PITFALL · Ada anonymous types. In the line
A, B : array (1..10) of Integer;, A and B are not equivalent: an anonymous type expression produces a separate type for each variable. For them to be equivalent, the type must be given a name first.
Common mistakes
| Wrong | Correct |
|---|---|
| Dangling = a cell nobody points to | That is a leak; dangling points to a deleted cell |
After delete p only p is dangerous | Every pointer holding the same address, like q, is dangling |
p + 1 increases the address by 1 byte | It increases by sizeof(type) |
| A C union is checked | Free union, no checking |
| Java has unions | It does not |
| Reference counting collects everything | It cannot collect cycles |
| Celsius = Fahrenheit always | Not under name equivalence |
| Java is not strongly typed because there are casts | Cast errors are caught at run time; nearly strongly typed |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is printed in Java?
class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; } }
Point a = new Point(1, 2);
Point b = a;
b.x = 5;
System.out.println(a.x);Show answer
In Java, variables of class type are references. b = a does not copy the object; it creates a second name (alias) for the same object. A change made through b is visible from a.
What is printed in Java (16+)?
record P(int x, int y) {}
P p = new P(1, 2), q = new P(1, 2);
System.out.println(p.equals(q) + " " + (p == q));Show answer
A record is a record type whose fields are immutable; the compiler generates equals so that it compares field by field. == compares the addresses of two separate objects: false.
Consider the C++ code. After the last line, which pointers are dangling?
int *p = new int(5);
int *q = p;
delete p;Show answer
delete p frees the cell, but the address inside p and q does not change. Both point to a freed cell: dangling pointers. Setting p to nullptr does not save q; the programmer may have forgotten that q exists. The danger: if that cell is given to another variable, its data gets corrupted through q.
How many heap cells are lost (memory leak) after this C++ code?
int *p = new int;
p = new int;
int *q = p;
p = new int;Show answer
Cell 1: the one p first pointed to; when p points elsewhere with the second new, no pointer points to it → lost. Cell 2: q still points to it. Cell 3: p points to it. Total 1 lost heap-dynamic variable (garbage).
In C, int a[5] = {10, 20, 30, 40, 50}; int *p = a; p += 2; What are *p + 1 and *(p + 1)?
Show answer
p += 2 moves the pointer forward by two elements (the address increases by 2 · sizeof(int) = 8 bytes), and p now points to a[2]. *p + 1: first dereference (30), then +1 = 31. *(p + 1): the next element, a[3] = 40.
A pointer p to int holds address 1000 and int is 4 bytes. What address does p + 3 hold? What if p were a pointer to double (8 bytes)?
Show answer
Pointer arithmetic is scaled by the size of the pointed-to type: p + k = address + k · sizeof(type). int: 1000 + 12. double: 1000 + 24.
In C, union { int i; float f; } u; u.i = 1; and then u.f is read. What kind of union is this and what is the problem?
Show answer
C and C++ unions are free unions: there is no tag that records which field was last written, and the compiler does not check. When the bits of 1 are read as a float, a very small number comes out (in Java Float.intBitsToFloat(1) = 1.4E-45). A discriminated union in Ada and ML carries a tag and catches reading the wrong field. Java and C# have no unions.
Two types are declared as below. Under name type equivalence and under structure type equivalence, may a variable of type Celsius be assigned to a variable of type Fahrenheit?
type Celsius = float;
type Fahrenheit = float;Show answer
Name type equivalence: types are equivalent if their names are the same; Celsius ≠ Fahrenheit. Safe (temperatures cannot be mixed up) but restrictive. Structure type equivalence: types are equivalent if their structure is the same; both are float. Flexible, but it cannot separate types the programmer wants to keep apart, and it is hard to implement.
Which heap management technique fails to reclaim the two objects that point to each other below, after root is set to null?
A.next = B; B.next = A;
root = A;
root = null;Show answer
Reference counting keeps in each cell how many pointers point to it, and frees it when the counter reaches 0. When root is gone, A's counter stays 1 (from B), and B's counter stays 1 (from A): neither reaches zero, but neither can be reached from the program. Mark-sweep marks everything reachable from the root and collects the unmarked; it collects the cycle.
Which dangling-pointer solution adds an extra heap cell between every pointer and its target, and sets it to nil when the target is deallocated?
Show answer
In the tombstone method, pointers point not to the object directly but to a tombstone cell. When the object is deleted, the tombstone becomes nil, and nobody can access the deleted memory. The cost: extra memory and an extra indirection on every access. Locks-and-keys: a pointer is a (key, address) pair and the cell carries a lock; on access the key is compared with the lock.
Which of these languages are strongly typed or nearly so: C, C++, Java, ML? Give one reason why C is not.
Show answer
Strongly typed: type errors are always caught (at compile time or run time). ML and F# are strongly typed. Java and C# nearly (explicit casts can produce errors but they are caught at run time). C and C++ are not: free unions and unchecked pointer conversions miss type errors. Coercion rules also weaken strong typing.
In Ada, A, B : array (1..10) of Integer; declares two variables with an anonymous array type. Are A and B type equivalent?
Show answer
In Ada an anonymous type expression creates a separate, unique type for each variable; so A and B are not considered equivalent (A := B does not compile). For them to be equivalent, a named type is defined first: type List is array (1..10) of Integer; A, B : List;. Ada takes name equivalence seriously.
Verification: The Java outputs were run with the JDK (java verify/SEN2001/W05b.java); the bit reinterpretation of a C union was shown on the same bits with Java's Float.intBitsToFloat function. C pointer arithmetic, the lost and dangling counts after new/delete, and the reference counting cycle were computed in node with a small memory simulation (verify/SEN2001/05b.mjs). The C code was not run directly because no C compiler is installed.