Lecture File
slides/06_mutation.pdf
Data and Memory
Returning extra values through pointer parameters, output parameters, and using double pointers to mutate caller-owned pointers.
C passes arguments by value. That means every function parameter is a local copy. If a function needs to change the caller's data, the caller must pass an address, and the function must write through that pointer.
This lecture uses that rule in two stages:
The central question is always:
Which variable in the caller must be different after the function returns?
If the caller's x must change, pass &x. If the caller's arr pointer must point somewhere new, pass &arr, which has type int ** when arr has type int *.
C functions return one value directly. To produce additional results, write into caller-provided memory.
The lecture's maxInfo example computes both:
From lecture_code/cpl/mutation/maxinfo.c:
float maxInfo(float *arr, int len, int *indLoc) {
assert(len > 0);
int maxInd = 0;
float maxVal = arr[0];
for (int i = 1; i < len; ++i) {
if (arr[i] > arr[maxInd]) {
maxInd = i;
maxVal = arr[i];
}
}
*indLoc = maxInd;
return maxVal;
}
The caller:
int maxInd = 0;
float max = maxInfo(arr, len, &maxInd);
Reasoning:
maxInfo returns the maximum value normally.&maxInd gives the function the address of the caller's variable.*indLoc = maxInd; writes into that caller variable.max and maxInd.return (a, b) Does Not WorkThis statement is legal C:
return (maxSoFar, maxInd);
But it does not return two values. It uses the comma operator:
maxSoFar.maxInd.maxInd.This is often an exam trap. The code compiles, but it does not mean what a beginner might hope.
A function can also return void and write all results through pointers:
void maxInfo(float *arr, size_t len, float *max, size_t *ind) {
assert(len > 0);
float maxSoFar = arr[0];
size_t maxInd = 0;
for (size_t i = 1; i < len; ++i) {
if (arr[i] > maxSoFar) {
maxSoFar = arr[i];
maxInd = i;
}
}
*max = maxSoFar;
*ind = maxInd;
}
Caller:
float max;
size_t ind;
maxInfo(arr, len, &max, &ind);
This style is useful when there is no single obvious primary result, or when a function's direct return value is reserved for success/failure.
This function cannot update the caller's length:
void bad_inc(size_t len) {
++len;
}
len is a local copy. The caller's variable is unchanged.
This function can update the caller's length:
void good_inc(size_t *len) {
++(*len);
}
Caller:
size_t len = 0;
good_inc(&len);
Memory model:
&len is the address of the caller's variable.len is a pointer storing that address.*len names the caller's actual variable.append: Why Each Attempt FailsThe lecture builds toward this final interface:
void append(int **arr, int *len, int *cap, int v);
Each pointer level corresponds to caller state that may need to change.
void push(int *arr, int val, size_t len, size_t cap) {
if (len == cap) {
int *newArr = malloc(sizeof(int) * cap * 2);
for (size_t i = 0; i < len; ++i) {
newArr[i] = arr[i];
}
free(arr);
cap = cap * 2;
arr = newArr;
}
arr[len] = val;
++len;
}
Problems:
len is a copy, so the caller's length does not change.cap is a copy, so the caller's capacity does not change.arr is a copy of the pointer, so reassigning it does not reassign the caller's pointer.If no resize happens, the value may be written into the array, but the caller still thinks len is unchanged. Printing len elements prints nothing if len remains 0.
len Onlyvoid push(int *arr, int val, size_t *len, size_t cap) {
...
arr[*len] = val;
++*len;
}
This lets the function update the caller's length, but cap and arr are still local copies. Once resizing happens, the caller may keep stale state.
len and capvoid push(int *arr, int val, size_t *len, size_t *cap) {
if (*len == *cap) {
int *newArr = malloc(*cap * sizeof(int) * 2);
for (size_t i = 0; i < *len; ++i) {
newArr[i] = arr[i];
}
free(arr);
*cap = *cap * 2;
arr = newArr;
}
arr[*len] = val;
++*len;
}
This still fails when resizing happens:
arr in main points to heap block A.push receives a local copy of that address.push allocates heap block B.push copies A into B.push frees A.push sets its local arr copy to B.push returns.arr still points to A, which is now freed.This explains both the dangling pointer and the double-free crash from the slides.
append with a Double PointerFrom lecture_code/cpl/mutation/doublingArray.c:
void append(int **arr, int *len, int *cap, int v) {
if (*cap == *len) {
int *newArr = malloc(sizeof(int) * (*cap * 2));
for (int i = 0; i < *len; ++i) {
newArr[i] = (*arr)[i];
}
free(*arr);
*arr = newArr;
*cap = *cap * 2;
}
(*arr)[(*len)++] = v;
}
Caller:
int cap = 4;
int *arr = malloc(sizeof(int) * cap);
int len = 0;
append(&arr, &len, &cap, 10);
Why each argument is an address:
&arr lets append change the caller's pointer variable.&len lets append change the caller's length.&cap lets append change the caller's capacity.Inside append:
arr has type int **.*arr is the caller's int * variable.(*arr)[i] indexes the caller's active heap array.*arr = newArr changes the caller's pointer.Parentheses matter. (*arr)[i] means index the integer array pointed to by the caller's pointer. *arr[i] would index arr first and then dereference, which is not this data shape.
appendInitial state:
main:
arr -> A: [4, 8, 15, 16]
len = 4
cap = 4
Call:
append(&arr, &len, &cap, 23);
Inside append:
*len == *cap, so the array is full.B[0] = A[0] = 4
B[1] = A[1] = 8
B[2] = A[2] = 15
B[3] = A[3] = 16
free(*arr) frees A.*arr = newArr changes main's arr to point at B.*cap = *cap * 2 changes main's cap to 8.(*arr)[(*len)++] = 23 writes B[4] and then changes main's len to 5.Final state:
main:
arr -> B: [4, 8, 15, 16, 23, _, _, _]
len = 5
cap = 8
pop and What It Really MutatesThe lecture code has:
void pop(int **arr, int *len, int *cap) {
--*len;
}
This only removes the last item logically. It does not erase the old value from memory and does not shrink the capacity.
For an array:
arr = [3, 5, 9, 12, _, _, _, _]
len = 4
cap = 8
After pop, len = 3. The old 12 may still sit in arr[3], but it is outside the meaningful range. The valid elements are only indices 0 through len - 1.
A robust pop should check that len > 0 before decrementing. Otherwise --*len can underflow when len is an unsigned type or become negative when signed.
Use the "caller variable" test:
int *arr may be enough.int **arr.size_t *len.Draw two boxes:
main's arr variable: stores address A
function's arr parameter: stores address of main's arr variable
Then decode:
arr is the address of the caller's pointer variable.*arr is the caller's pointer variable.**arr is the first integer in the caller's array.(*arr)[i] is the ith integer in the caller's array.& at the call site for an output parameter.ptr when you meant *ptr.malloc, leaking the new allocation.(*arr)[i].pop on an empty array.len, cap, and arr are separate pieces of state that must stay consistent.int and size_t inconsistently in comparisons and loops.Output parameters make a function more powerful but also easier to misuse. Good C interfaces should document:
For example:
int append(int **arr, size_t *len, size_t *cap, int v);
Returning int could report success or failure:
1 for success;0 for allocation failure.That is often stronger than a void function because callers can handle malloc failure.
When asked whether a function needs a pointer:
int, pass int *.int *, pass int **.struct List *, replacing it would require struct List **.When tracing a bad resize:
free(A), mark every pointer to A as dangling.T *out lets a function write a T into caller-owned storage.T **out lets a function replace a caller-owned T * pointer.&x when the function should mutate x.&arr when the function may make arr point to a new allocation.(*arr)[i] is the correct form when arr is an int ** pointing to the caller's array pointer.maxInfo use an output parameter for the index?return (maxSoFar, maxInd); not a valid multi-value return?push need int **arr instead of int *arr?size_t *len and size_t len as a function parameter?append(&arr, &len, &cap, x) work while append(arr, len, cap, x) does not?append, explain the difference between arr, *arr, and (*arr)[0].pop check before decrementing len?Built from summaries/06_mutation.md and reviewed against slides/06_mutation.pdf plus matching files in lecture_code/.