Upon completion of this short, you will:
In C, memory allocation is a critical aspect of programming, and it can be done in several ways. Memory in C is typically allocated in several areas: the stack, the heap, the text segment, and the data segment. The text and data segment are often collectively referred to as the text segment or the static segment (as it cannot be changed at run-time).
When compiling a C/C++ program, the C compiler (often gcc or clang) translates the C code into machine language instructions. Next, the linker combines the compiled binary from the compiler with pre-compiled system libraries (such as “stdio”) and forms a binary executable file: the program.
When we execute the program, the operating system creates a new process (through fork()
followed by an exec()
) and loads the executable’s code into that new process’ memory. The binary file of the executable is loaded into RAM in a particular manner.
After being loaded into the memory (RAM), the memory layout of a C Program has six components: the text segment containing the program’s instructions, the initialized data segment, the uninitialized data segment, the command-line arguments, the stack, and the heap. Each of these six different segments has its own read and write permissions. If a program tries to access memory in any segment differently than it is supposed to, it results in a segmentation fault error (segfault or core dump 1).
The Edit-Compile-Link-Run (ECLR) cycle is a fundamental process in software development, particularly in languages like C and C++ where code must be transformed from human-readable source code into an executable program. This cycle outlines the steps a developer typically follows when creating and running a software application. Here’s an explanation of each step:
Edit: This is the first step where the developer writes or modifies the source code of the software using a text editor or integrated development environment (IDE). During this phase, the developer can add new features, fix bugs, or make any necessary changes to the code.
Compile: After writing or editing the source code, the next step is compilation. Compilation is the process of translating the human-readable source code into machine-readable instructions (binary code) that the computer’s CPU can execute. A program responsible for this translation is called a compiler. The compiler checks the code for syntax errors and, if none are found, generates an object file or a set of object files.
Link: In many software projects, especially larger ones, the code is divided into multiple source files and libraries. The linking phase combines these separate object files and libraries into a single executable program. The linker resolves references between different parts of the code, ensuring that functions and variables are correctly connected. If there are unresolved references, it will result in linker errors.
Run: Once the source code is compiled and linked successfully, the final step is to run the program. This involves executing the compiled binary file or executable. The program will start and perform its intended tasks, which can be anything from performing calculations to interacting with users through a graphical user interface.
Here’s a simplified diagram of the Edit-Compile-Link-Run cycle:
This cycle is iterative and continuous throughout the software development process. Developers frequently make changes to the code, recompile, relink, and rerun the program to test their modifications. It allows them to incrementally build, test, and refine their software until it meets the desired functionality and quality standards. Debugging and troubleshooting are also essential parts of this cycle, as developers often need to identify and fix errors or unexpected behavior during the development process.
The memory layout for a C/C++ program is comprised of six distinct areas: heap, stack, code segment, command-line arguments, uninitialized and initialized data segments. Each of these segments has separate read and write permissions and writing into a area for which the program does not have permission results in a segmentation fault and the program “crashes” – a process often referred to as “dumping core”. The figure below illustrates the general memory layout.
In C, “scope” refers to the region or context in which a variable or identifier is valid and can be used. It determines where in your code a particular variable or identifier is visible and accessible. “Extent” refers to the lifetime of a variable’s memory and the time from which memory is allocated to the time when the memory is deallocated.
Understanding the scope and extent of variables and identifiers is crucial for writing correct and maintainable C programs. Scope and extent are not always the same. A variable can be allocated (it has “extent”) but not be currently accessible. For example, a local variable declared as static within a function has an extent that is the same as the program, i.e., the memory is allocated upon loading of the program and remains allocated until the program exits. However, the variable can only be accessed within the function. Of course, it is possible to get a “pointer” to the variable’s memory and manipulate the variable’s contents outside the function, but the fact remains that the variable’s scope is local to the function, even though the memory remains allocated whether the function is active or not.
There are several levels of scope in C:
Block Scope:
{}
, have block scope.Function Scope:
File Scope (Global Scope):
static
to limit their visibility to the current file only (internal linkage), or they can be declared as extern
to allow them to be accessed from other files (external linkage).Function Prototype Scope:
It’s important to understand the scope of variables and identifiers in C to avoid naming conflicts, manage the lifetime or extent of variables, and control access to data within your program. Properly scoping variables helps improve code readability, maintainability, and prevents unintended side effects.
Access to a variable is always its most recently declared scope. In the fragment below, within the function myFunc()
, both the global and the function scope declarations of i are hidden by the more recently declared locally scoped i. The i in main()
and the i in myFunc()
are in separate scopes and thus do not conflict in any way.
static int i = 100;
int myFunc (int i)
{
int i = 500;
// i is 500
}
void otherFunc()
{
// which _i_ and what is its value here?
}
void main (void)
{
int i = 99;
myFunc (i);
// i is 99
}
So, then which i is referred to in otherFunc()
?2
The stack is a region of memory that is used for function call management and local variables. Memory allocated on the stack is automatically managed by the compiler and is typically small in size. Variables allocated on the stack have a limited scope, typically within the function in which they are declared. When a function is called, space is allocated on the stack for its local variables and function call information. When the function exits, this space is automatically deallocated.
Example of stack allocation:
In the above code fragment, the memory for both x and c is allocated automatically on the stack when the function is called. The scope of the variables is local to the function. The memory is deallocated as soon as the function exits, wich is either when its last statement is executed or via an explicit return.
The heap is a larger region of memory that is used for dynamic memory allocation. It is typically used for objects whose size or lifetime cannot be determined at compile time. Memory on the heap is managed explicitly by the programmer. You allocate memory when needed and release it when no longer required.
Heap memory must be explicitly allocated using functions like malloc()
, calloc()
, or realloc()
, and it should be explicitly deallocated using free()
when it is no longer needed to avoid memory leaks.
Example of heap allocation:
int* dynamicArray = (int*)malloc(sizeof(int) * 10); // memory on the heap
// ...
free(dynamicArray); // deallocate memory when done
A call to malloc()
returns the address of the first byte of allocated memory. The caller is responsible for knowing how much memory was allocated and to track the address so it can be accessed and freed later when no longer needed.
The regions of memory used for the stack and the heap are not pre-allocated. They grow (and shrink) as needed. The program’s process requests additional memory from the operating as needed by calling a system function. Memory requests are honored until an out of memory situation is reached. The allocated memory is most commonly virtual memory rather than physical RAM.
The dynamic memory from the heap can be accessed as long as there is a pointer to it, i.e., there is a variable that holds the address of the allocated memory.
malloc()
In C, you can request memory from the operating system using the malloc()
function, which stands for “memory allocation.” The malloc()
function is part of the C Standard Library (stdlib.h) and is commonly used to dynamically allocate memory at runtime.
Here’s how you can use the malloc()
function to request memory:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr; // Declare a pointer to int
// Request memory for an integer (4 bytes)
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL) {
fprintf(stderr, "Memory allocation failed.\n");
return 1; // Return an error code
}
// Now, you can use 'ptr' to store and access an integer
*ptr = 42;
// Don't forget to free the allocated memory when you're done with it
free(ptr);
return 0;
}
A few important points to note when using malloc()
:
It returns a pointer to the first byte of the allocated memory block, or it returns NULL
if memory allocation fails (e.g., due to insufficient memory).
You should cast the result of malloc()
to the appropriate pointer type. In the example above, we cast it to (int *)
because we are allocating memory for an integer.
Always check if the returned pointer is NULL
after calling malloc()
. If it’s NULL
, it means that memory allocation failed, and you should handle the error gracefully.
After you’re done using the allocated memory, it’s crucial to free it using the free()
function to avoid memory leaks.
The size to be passed to malloc()
should be the size (in bytes) of the data type you want to store in the allocated memory. In the example, sizeof(int)
is used because we’re allocating memory for an integer.
malloc()
allocates memory on the heap, which means the memory persists until you explicitly free it using free()
or until the program exits.
Variables declared with the static
keyword are allocated in a special section of memory separate from the stack and the heap. They retain their values between function calls.
Global variables are also allocated in a similar manner, and they can be accessed from anywhere in the program.
Example of static allocation:
In the above example, the memory remains allocated for the duration of the program’s execution and the scope of the variable is file scope.
Also sometimes called the text segment, this segment contains the binary machine code of the compiled program. Most commonly, the text segment is shared among all processes executing the same program, so that it only needs to be in memory once. For example, if multiple users were to start ‘vi’ or run ‘gcc’, then only one copy is loaded and the memory pages are shared among the processes. This sharing is managed by the operating system’s virtual memory subsystem and is of no concern to programmers.
The code segment is generally a read-only segment, which means that a program cannot modify its code. Doing so (often by accident) results in a segmentation fault.
This area of memory holds the command line arguments that are passed to main()
. Often those are simply pushed onto the stack when the program is loaded rather than being a completely separate area.
There is always at least one command line argument, namely the name of the running program.
The initialized data segment stores all global, static, constant, and external variables (those declared with extern keyword ) that are initialized at time of declaration. This memory segment is read-write as the values can be modified at run-time, although some variables can be protected from modification by the compiler (but nothing is stopping someone from getting the address of a const variable and modifying it that way, so the memory is not actually protected).
#include <stdio.h>
char c[] = "read-write"; // global variable stored in the initialized
// data segment and is read-write
const char s[] = "const-var"; // global variable stored in the initialized
// data segment and treated as read-only
int glob_var; // global variables stored in the uninitialized
// data segment
int main(int argc, char* argv[])
{
static int x = 99; // static local variable stored in the
// initialized data segment*/
return 0;
}
Note that one should assume that all variables are uninitialized unless explicitly initialized.
If you prefer a narrated version with code examples, then watch the narrated lesson by Martin Schedlbauer below:
The stack is not only used to store local variables, but is an integral part of the calling mechanism for functions. In C, the stack plays a critical role in managing program execution and function calls. It is a region of memory used for organizing and tracking function call information, including local variables, function parameters, return addresses, and control flow.
Below is an explanation of how the stack is used in C programs and to enable and support function calls:
Here’s a simplified visual representation of the stack during function calls:
Stack grows ↓
+----------------------+
| Stack Frame 1 | <- Function 1
| Local Vars |
| Parameters |
| Return Address | <- Return address for Function 1
+----------------------+
| Stack Frame 2 | <- Function 2 (nested within Function 1)
| Local Vars |
| Parameters |
| Return Address | <- Return address for Function 2
+----------------------+
| ... | <- More stack frames for nested functions
+----------------------+
| Stack Frame N | <- Most recent function call
| Local Vars |
| Parameters |
| Return Address | <- Return address for Function N
+----------------------+
Stack shrinks ↑
In summary, the stack is a fundamental data structure used in C programs to manage function calls and their associated data. It ensures proper scoping, local variable allocation, and control flow within the program. Understanding how the stack works is essential for writing efficient and well-structured C code.
As an aside, while this lesson deals with memory management in C, all of this also applies to C++ and to many other languages, such as Java.
In summary, C programs allocate memory on the stack for local variables with a fixed lifetime and size, while memory on the heap is used for dynamic data that can vary in size and lifetime. It’s crucial to manage heap memory properly to avoid memory leaks and to release allocated memory using free()
when it is no longer needed. Failure to deallocate memory can lead to memory leaks, where your program consumes more and more memory over time, potentially causing it to run out of available memory.
TBD
The term “core dump” has its origins in early computing and the use of magnetic core memory, which was a type of primary storage used in early computers. Today, the term “core dump” is still used to describe the practice of saving a program’s memory state when it encounters a critical error, even though the underlying technology has changed significantly. Instead of magnetic core memory, modern systems typically save this snapshot to a file named “core” or with an extension like “.core.” This file can then be used for debugging and analysis, as described in the previous response.↩︎
it would refer to the file scope i with value 100↩︎