Every variable lives somewhere in memory, and understanding where it lives and whether a function receives the original or a copy will influence what your program can and cannot do. In this lab you will trace the lifecycle of variables in C++.
§1 Pass-by-Reference vs Pass-by-Value in Functions #
In C++, you can pass arguments to functions in two main ways:
- Pass-by-Value
- Pass-by-Reference
§1.1 Pass-by-Value #
When passing by value, a copy of the variable is made and passed to the function. Any changes made to the parameter inside the function do not affect the original argument. This method is safe but can be inefficient for large data types because of the overhead of copying.
Example:
#include <iostream>
void modifyValue(int num) {
num = 20; // This variable is scoped only to this function
}
int main() {
int myVar = 10;
modifyValue(myVar); // Pass-by-value, a copy is made
std::cout << "Value of myVar: " << myVar << std::endl; // Output: 10
}
In the above example, myVar remains 10 after the function call because only a copy of myVar was passed to the function.
§1.2 Pass-by-Reference #
When passing by reference, the function receives a reference to the original variable, meaning any modifications to the parameter affect the original variable. This method is more efficient for large data types as it avoids copying the data.
In C++, passing by reference is performed using the ampersand (&) symbol.
Example:
#include <iostream>
void modifyValue(int &num) {
num = 20;
}
int main() {
int myVar = 10;
modifyValue(myVar); // Pass-by-reference
std::cout << "Value of myVar: " << myVar << std::endl; // Output: 20
}
In this case, myVar is modified to 20 because the function uses a reference to the variable, therefore the num = 20; operation in the function affects the original variable outside of the function.
Both “int& val” and “int &val” are syntactically identical and refer to the same concept. The position of the & does not affect the meaning, and is purely a style choice.
§2 Array Memory Layout #
Arrays in C++ are contiguous blocks of memory. When you declare an array, the elements are laid out sequentially.
Example:
int arr[5] = {10, 20, 30, 40, 50};
This creates an array of integers, and its memory layout looks like this:
| Memory Address | Value |
|---|---|
| 0x1000 | 10 |
| 0x1004 | 20 |
| 0x1008 | 30 |
| 0x100C | 40 |
| 0x1010 | 50 |
Memory Diagram:
┌───┬──┬──┬──┬──┬──┬───┐
│...│10│20│30│40│50│...│
└───┴──┴──┴──┴──┴──┴───┘
If arr is located at memory address 0x1000, then arr[1] is stored at 0x1004 (assuming each int occupies 4 bytes).
Arrays are stored in contiguous memory locations and are allocated at the time of creation. The size of each element determines the offset of the next element. Arrays in C++ do not store their size, so you must manage it manually or use modern containers like std::vector.
§2.1 Arrays and Pass-by-Value #
An array variable (e.g., arr) is always a pointer to the first element. Therefore, arr and &arr[0] both return the same value, a memory address.
std::cout << arr << " " << &arr[0] << std::endl; // Both print the same address
// 0x16b003330 0x16b003330
In C++, you cannot pass an array by value directly. When you pass an array to a function, it decays into a pointer to its first element. Only a pointer is passed, not a copy of the entire array. This will allow the function to access and modify the original array.
Example:
#include <iostream>
void modifyArray(int arr[], int size) {
arr[0] = 888; // Modifies the first element in the the original array
}
int main() {
int myArray[3] = {1, 2, 3};
modifyArray(myArray, 3);
std::cout << "First element: " << myArray[0] << std::endl; // Output: 888
}
In this case, the array variable myArray is passed to modifyArray(), but only a pointer to the first element of the array is actually passed, not a copy of the entire array. Changes to arr[0] in the function affect the original myArray.
To pass a full copy of the array, you must use an alternative such as std::array or std::vector, or manually copy the array.
§3 The Stack #
The stack is a region of memory used for managing function calls, local variables, and control flow.
When a function is called, a new “stack frame” is pushed onto the stack. When the function returns, the frame is popped off and the memory is freed. The stack is finite in size but can often be configured.
§3.1 Stack Overflow #
When you call a recursive function, each recursive call pushes a new stack frame onto the stack. If there are too many recursive calls, you can exhaust the stack, leading to a stack overflow error. Most commonly seen as a: segmentation fault.
A stack overflow occurs when the stack exceeds its allocated size. This can happen with deeply recursive functions or in programs that allocate large or too many local variables.
Example:
#include <iostream>
void recursiveFunction() {
recursiveFunction(); // Infinite recursion
}
int main() {
recursiveFunction();
return 0;
}
$ g++ -Wall -o test_program main.cpp # this compiles fine
# but -Wall will provide a warning...
main.cpp:3:20: warning: all paths through this function will call itself [-Winfinite-recursion]
3 | void recursiveFunction() {
| ^
1 warning generated.
$ ./test_program
Segmentation fault
In this example, the function recursiveFunction() calls itself indefinitely. Each call creates a new stack frame until the stack is full, resulting in a stack overflow.
When a stack overflow occurs, the operating system typically terminates the program because it cannot allocate more memory for the stack.
§4 Hexadecimal Format and Memory Representation #
In C++, memory addresses and data values are often represented in hexadecimal (hex) format. This format is crucial for low-level programming, debugging, and understanding how data is stored in memory.
Memory addresses in C++ are commonly represented as hex values. When you use a pointer or reference to access a variable’s memory address, the address is typically printed in hexadecimal format.
§4.1 What is Hexadecimal? #
Hexadecimal (or hex) is a base-16 number system that uses 16 symbols to represent values:
- The digits
0-9represent values0-9. - The letters
A-F(or lowercasea-f) represent values10-15.
Since 16 is a power of 2 (i.e., 2^4 = 16), hex is a convenient way to represent binary data (used in computers) in a more compact form. Every hex digit represents four binary bits.
Example:
- Binary:
1010 1101 0010 1111 - Hex:
AD2FwhereA = 10,D = 13,2 = 2, andF = 15
§5 Questions #
- In the following array, what is the memory address of
arr[3]ifarrstarts at memory address0x7ffdf000, and each integer occupies 4 bytes?int arr[4] = {100, 200, 300, 400};
- Creating deeply (or infinitely) recursive functions and declaring too many variables can both lead to a stack overflow. What is another way you can cause a stack overflow?
- The following simple program causes a
segmentation faulterror when you try to run it. Explain why.int main() { double a[999999999]; }
- If an
inttakes up 4 bytes of memory, how much memory is the following array going to allocate? (Convert your answer to kilobytes.)int a[500];
- Consider the following program:What will the output be? Explain why each variable holds its respective value.
#include <iostream> int main() { int x = 5; int &ref1 = x; int &ref2 = ref1; int val1 = ref2; ref1 = 10; std::cout << "x: " << x << ", ref1: " << ref1 << ", ref2: " << ref2 << ", val1: " << val1; }
- This happens when you run the following program repeatedly. Explain why the addresses are different each time.
#include <iostream> int main() { int myNum = 5; std::cout << &myNum << std::endl; }$ ./test_program && ./test_program && ./test_program 0x16b86332c 0x16eef732c 0x16ae0f32c