Computer Fundamentals with C and Unix
(Memory allocation)
Lecture 13: Jul. 20, 2023 Prof. K.R. Chowdhary : Professor of CS

Disclaimer: These notes have not been subjected to the usual scrutiny reserved for formal publications. They may be distributed outside this class only with the permission of the Instructor.

13.1 How you statically allocate memory in C?

In C, you can statically allocate memory by declaring variables with fixed sizes at compile-time. Static memory allocation means that the memory for these variables is reserved during the program’s compilation and remains fixed throughout the program’s execution. Here are some ways to statically allocate memory in C:

Statically Allocated Arrays:

 #include <stdio.h>
int main() {
// Statically allocate an integer array of size 5
  int staticArray[5] = {10, 20, 30, 40, 50};

 // Statically allocate a character array (string)
  char message[] = "Hello, World!";

 // Access and modify the elements of the arrays
  printf("staticArray[2]: %d\n", staticArray[2]);
  printf("message: %s\n", message);

  return 0;
}

Statically Allocated Variables:

#include <stdio.h>

int main() {
    // Statically allocate integer variables
    int age = 25;
    int weight = 75;

    // Statically allocate character variable
    char grade = ’A’;

    // Access and modify the variables
    printf("age: %d\n", age);
    printf("weight: %d\n", weight);
    printf("grade: %c\n", grade);

    return 0;
}

Statically Allocated Structs:

#include <stdio.h>

struct Person {
    char name[30];
    int age;
};

int main() {
    // Statically allocate a struct variable
    struct Person person1 = {"John", 30};

    // Access and modify the struct members
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);

    return 0;
}

In all the above examples, the memory for the variables (arrays, variables, and structs) is allocated during the compilation process. The size of the arrays and structs must be known at compile-time. Static allocation is suitable when you know the size of the data beforehand or when you need the variables to have a fixed memory location throughout the program’s execution.

13.2 What is advantages of static allocation of memory

Static allocation of memory refers to reserving memory for variables during the compile-time or at a fixed memory location before the program starts executing. This is in contrast to dynamic allocation, where memory is allocated during the runtime using functions like ‘malloc()’ or ‘new’ in C and C++.

There are several applications of static allocation of memory:

1. Global Variables: Variables declared outside of any function are typically allocated statically. Global variables are accessible throughout the entire program, and their memory is reserved for the entire program’s lifetime.

2. Constants: Constants declared using ‘const‘ keyword are often allocated statically. These constants hold fixed values that remain unchanged throughout the program’s execution.

3. Configuration Settings: In some cases, configuration settings or constants used in a program are known at compile-time. Allocating memory for these settings statically can be more efficient and simpler than using dynamic memory allocation.

4. Small Local Arrays: In certain situations, when the size of an array is known and relatively small, it can be declared as a static array. This avoids the overhead of dynamic memory allocation and deallocation.

5. Lookup Tables: Lookup tables, which are arrays of precomputed values, can be allocated statically when their values are known at compile-time.

6. Data Structures with Fixed Sizes: If the size of a data structure is known and constant, it can be allocated statically. For example, simple data structures like a fixed-size queue or stack can be implemented using statically allocated arrays.

Advantages of Static Allocation:

- Simplicity: Static allocation is straightforward and requires no explicit memory management during runtime.

- Performance: Static allocation can be faster than dynamic allocation since there’s no overhead of memory allocation and deallocation.

- Predictability: Memory locations of statically allocated variables are known at compile-time, making memory access predictable.

However, static allocation has limitations:

- Fixed Sizes: The size of statically allocated arrays and data structures must be known at compile-time, which may not be suitable for all scenarios.

- Memory Wastage: Static allocation may reserve memory even when it’s not in use, potentially wasting memory if the variable’s lifetime is limited.

In summary, static allocation is useful when the size and lifetime of variables are known at compile-time and when memory efficiency and performance are important factors. For more flexible memory management or when dealing with dynamically sized data structures, dynamic memory allocation is more appropriate.

13.3 How you dynamically allocate memory in C?

In C, you can dynamically allocate memory at runtime using functions like malloc(), calloc(), and realloc(). These functions allow you to request memory from the operating system during the execution of the program. Dynamically allocated memory remains in use until explicitly freed with the free() function. Here’s how you can dynamically allocate memory in C:

Using malloc():

#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 5;

    // Dynamically allocate an array of integers
    int *dynamicArray = (int*)malloc(size * sizeof(int));

    if (dynamicArray == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    // Access and modify the elements of the dynamically allocated array
    for (int i = 0; i < size; ++i) {
        dynamicArray[i] = i * 10;
        printf("dynamicArray[%d]: %d\n", i, dynamicArray[i]);
    }

    // Don’t forget to free the dynamically allocated memory
    free(dynamicArray);

    return 0;
}

Using calloc():

#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 5;

    // Dynamically allocate an array of integers and initialize to zero
    int *dynamicArray = (int*)calloc(size, sizeof(int));

    if (dynamicArray == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    // Access and modify the elements of the dynamically allocated array
    for (int i = 0; i < size; ++i) {
        dynamicArray[i] = i * 10;
        printf("dynamicArray[%d]: %d\n", i, dynamicArray[i]);
    }

    // Don’t forget to free the dynamically allocated memory
    free(dynamicArray);

    return 0;
}

Using realloc() for resizing:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 5;

    // Dynamically allocate an array of integers
    int *dynamicArray = (int*)malloc(size * sizeof(int));

    if (dynamicArray == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    // Access and modify the elements of the dynamically allocated array
    for (int i = 0; i < size; ++i) {
        dynamicArray[i] = i * 10;
        printf("dynamicArray[%d]: %d\n", i, dynamicArray[i]);
    }

    // Resize the dynamically allocated array to a larger size
    size = 10;
    int *resizedArray = (int*)realloc(dynamicArray, size * sizeof(int));

    if (resizedArray == NULL) {
        printf("Memory reallocation failed.\n");
        free(dynamicArray); // Free original memory before exiting
        return 1;
    }

    dynamicArray = resizedArray;

    // Continue using the resized array with more elements
    for (int i = 5; i < size; ++i) {
        dynamicArray[i] = i * 10;
        printf("dynamicArray[%d]: %d\n", i, dynamicArray[i]);
    }

    // Don’t forget to free the dynamically allocated memory
    free(dynamicArray);

    return 0;
}

When dynamically allocating memory, it’s important to check if the memory allocation was successful (i.e., the returned pointer is not NULL). Also, remember to free the dynamically allocated memory using free() to avoid memory leaks. Dynamically allocated memory has the advantage of being flexible and can be useful when the size of data structures is unknown at compile-time or when memory requirements change during program execution.

13.4 Advantages and disadvantages of dynamic allocation of memory

Dynamic allocation of memory refers to reserving memory for variables during the program’s runtime using functions like ‘malloc()‘ in C or ‘new‘ in C++. Here are some advantages of dynamic allocation of memory:

1. Flexible Memory Management: Dynamic allocation allows you to allocate memory based on runtime requirements. You can determine the size of memory needed during program execution, making it suitable for situations where the size is not known at compile-time or may vary throughout the program’s execution.

2. Efficient Memory Usage: Dynamic allocation helps in efficient memory utilization. Memory is allocated only when needed, and it can be released when no longer required. This prevents wastage of memory by allocating only what is necessary, improving overall memory efficiency.

3. Data Structures with Variable Size: Dynamic allocation is well-suited for implementing data structures with variable sizes, such as linked lists, trees, and resizable arrays (vectors). These data structures can grow or shrink based on actual data requirements.

4. Dynamic Arrays: Dynamic memory allocation enables the creation of dynamic arrays, where the size of the array is determined at runtime. This allows you to create arrays of any size as needed, avoiding fixed-size limitations.

5. Resource Sharing: Dynamic allocation enables multiple parts of a program to share data by allocating memory in one part and passing its address to other parts of the program. This facilitates data sharing between different modules or functions.

6. Memory Reuse: Dynamic allocation allows you to reuse memory for different purposes, leading to better memory management. When a dynamic object is no longer needed, its memory can be released and used for other objects.

7. Less Memory Footprint: In contrast to static allocation, dynamic allocation can reduce the overall memory footprint of a program, especially when dealing with large or varying amounts of data.

However, it’s important to consider some potential drawbacks:

1. Slower Allocation and Deallocation: Dynamic allocation can be slower than static allocation due to the overhead of requesting memory from the operating system and freeing it when no longer needed. This overhead can be significant for frequent memory allocations and deallocations.

2. Memory Leaks: If memory is allocated dynamically but not properly deallocated, it can lead to memory leaks, where memory is not released even after it is no longer in use.

3. Fragmentation: Frequent allocation and deallocation of memory can lead to memory fragmentation, which may reduce the efficiency of memory usage over time.

In summary, dynamic allocation of memory provides flexibility, efficient memory usage, and the ability to manage data structures with varying sizes. However, it requires careful management to avoid memory leaks and excessive fragmentation. When used judiciously, dynamic allocation can be a powerful tool for creating flexible and efficient programs.

13.5 What is heap allocation of memory in C?

Heap allocation of memory in C refers to the process of dynamically allocating memory at runtime from the heap memory segment. Unlike the stack, which is used for automatic memory allocation and deallocation for local variables, the heap provides a region of memory that can be used for more flexible memory management, particularly when dealing with data structures of variable or unknown size.

Heap allocation is typically done using functions like malloc(), calloc(), and realloc(). These functions allow you to request memory from the heap during program execution and return a pointer to the allocated memory. The allocated memory remains available until explicitly freed using the free() function.

Here’s a brief explanation of the heap allocation functions:

malloc(): This function stands for ”memory allocation.” It allocates a specified number of bytes from the heap and returns a pointer to the first byte of the allocated memory. The memory is not initialized, and its initial content is undetermined.

calloc(): This function stands for ”contiguous allocation.” It allocates memory for an array of elements, each with a specified size, and initializes all bytes to zero. The total size of the allocated memory is num_elements * element_size.

realloc(): This function stands for ”reallocate.” It allows you to resize previously allocated memory. If the new size is larger than the original, the new memory is uninitialized. If the new size is smaller, the excess memory is deallocated.

Here’s an example demonstrating heap allocation in C:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 5;

    // Dynamically allocate an array of integers on the heap
    int* dynamicArray = (int*)malloc(size * sizeof(int));

    if (dynamicArray == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    // Access and modify the elements of the dynamically allocated array
    for (int i = 0; i < size; ++i) {
        dynamicArray[i] = i * 10;
        printf("dynamicArray[%d]: %d\n", i, dynamicArray[i]);
    }

    // Don’t forget to free the dynamically allocated memory
    free(dynamicArray);

    return 0;
}

Heap allocation is useful when you need to allocate memory for data structures whose sizes are not known at compile-time or when you need to manage memory efficiently during program execution. However, it’s essential to handle heap memory carefully to avoid memory leaks and undefined behavior due to accessing freed memory. Always remember to free dynamically allocated memory using free() when it is no longer needed.

13.6 What are the stack and register allocation in C?

In C, register and stack allocation are two different methods used for allocating and managing memory for variables:

Register Allocation: The register keyword is used as a hint to the compiler, suggesting that a particular variable should be stored in a CPU register for faster access. CPU registers are small, fast, and directly accessible by the processor, making register variables more efficient than variables stored in memory. However, it’s important to note that the register keyword is just a hint, and the compiler may choose to ignore it.

Example of register variable:

#include <stdio.h>

int main() {
    register int x = 10; // Suggests the compiler to use a register for ’x’

    // Accessing the register variable
    printf("Value of x: %d\n", x);

    return 0;
}

Register allocation is mainly useful for frequently accessed variables with critical performance requirements. However, the use of the register keyword is often unnecessary, as modern compilers are capable of optimizing variable storage automatically.

Stack Allocation:

The stack is a region of memory used for automatic memory allocation and deallocation of local variables within functions. When a function is called, its local variables are allocated on the stack, and when the function exits, the memory used by those variables is automatically deallocated. This process allows for efficient and automatic management of local variables’ lifetimes.

Example of stack allocation:

#include <stdio.h>

void foo() {
    int x = 5; // Local variable ’x’ is allocated on the stack
    printf("Value of x inside foo: %d\n", x);
} // ’x’ is automatically deallocated when the function exits

int main() {
    foo(); // Function call

    // ’x’ is not accessible here since it was local to the ’foo’ function

    return 0;
}

Stack allocation is suitable for managing local variables with well-defined and limited lifetimes. The stack memory is organized in a last-in-first-out (LIFO) manner, meaning that the most recently allocated local variable is the first one to be deallocated when the function exits.

In summary, register allocation suggests that a variable should be stored in a CPU register for faster access, while stack allocation is used for automatic memory management of local variables within functions. The register keyword is rarely used in modern C programming, as compilers are proficient in optimizing variable storage without explicit hints. Stack allocation, on the other hand, is a fundamental and essential aspect of C programming for managing local variables.

13.7 What is static Checking of C program

Static checking of a C program refers to the process of analyzing the source code without actually running the program. It involves using specialized tools or techniques to detect potential errors, bugs, or code quality issues in the codebase before the program is executed. This form of analysis helps identify various types of programming mistakes and improves the overall code quality by identifying potential problems early in the development process.

Some common aspects that static checking can help identify include:

1. Syntax Errors: Static analysis tools can detect and report syntax errors in the code, such as missing semicolons, mismatched parentheses, or incorrect variable declarations.

2. Type Errors: Static type checking can help catch type-related errors, such as using a variable of the wrong type or performing invalid operations on data types.

3. Null Pointer Dereferences: Tools can identify potential instances where a pointer might be dereferenced without first being checked for null (i.e., null pointer dereference).

4. Memory Leaks: Static analysis can help identify potential memory leaks, where dynamically allocated memory is not freed, leading to wastage of memory resources.

5. Code Style and Best Practices: Static checking can enforce coding guidelines and best practices, ensuring consistent coding style and improving code maintainability.

6. Uninitialized Variables: Analysis tools can identify cases where variables might be used before being initialized, which can lead to unpredictable behavior.

7. Unreachable Code: Static analysis can detect code segments that are logically impossible to execute, helping to identify dead code.

8. Potential Security Vulnerabilities: Some static analysis tools can detect common programming errors that may lead to security vulnerabilities, such as buffer overflows or injection attacks.

Static checking is typically performed using specialized tools known as static analyzers or linters. These tools examine the code without executing it, allowing developers to identify and fix potential issues early in the development process. Static checking complements other forms of testing (such as dynamic testing or runtime debugging) and contributes to building robust and reliable software.

Popular static analysis tools for C programs include Clang Static Analyzer, GCC’s ‘-Wall‘ and ‘-Wextra‘ flags, and other third-party tools like Coverity and PVS-Studio. Using static checking tools as part of the development process can help reduce the number of bugs and improve overall code quality.

13.8 What is dynamic checking of C Program?

Dynamic checking of a C program refers to the process of analyzing the behavior of the program while it is running or executing. Unlike static checking, which examines the source code without executing it, dynamic checking involves observing the program’s behavior at runtime to detect errors, bugs, and other runtime issues.

Dynamic checking involves various techniques and tools that monitor the program’s execution, collect data, and analyze its behavior. Some common types of dynamic checking in C programs include:

Runtime Assertions: Placing assertions (using assert() macro) at specific points in the code to check conditions that should be true during program execution. If an assertion fails (i.e., the condition is false), the program terminates with an error message, helping to catch unexpected situations.

#include <assert.h>
#include <stdio.h>

int divide(int dividend, int divisor) {
    assert(divisor != 0); // Ensure divisor is not zero
    return dividend / divisor;
}

int main() {
    int result = divide(10, 0); // This will trigger an assertion failure
    printf("Result: %d\n", result);
    return 0;
}

Memory Debugging: Tools like Valgrind can be used to detect memory-related errors such as memory leaks, invalid memory access, and uninitialized memory reads.

Dynamic Analysis Tools: These tools monitor the program’s execution and collect runtime data to analyze the program’s behavior, performance, and memory usage. Examples include tools like GDB (GNU Debugger) for debugging and profiling tools like gprof.

Dynamic checking is useful for identifying issues that only occur during program execution, such as invalid memory access, division by zero, or unexpected behavior in edge cases. However, it incurs a slight performance overhead due to the monitoring and data collection during runtime.

It’s essential to use dynamic checking in conjunction with other testing methodologies, such as static checking, unit testing, and integration testing, to achieve comprehensive testing and ensure the reliability and correctness of a C program. By combining static and dynamic checking approaches, developers can create more robust software and catch a wider range of bugs and errors.

13.9 GNU Debugger tools

I believe you meant GDB (GNU Debugger), which is a powerful and popular tool for debugging C (and other programming language) programs. GDB allows developers to inspect and manipulate a running program, set breakpoints, examine variables, and step through the code to find and fix bugs and errors.

Here are some key features and functionalities provided by GDB:

Setting Breakpoints: You can set breakpoints at specific lines or functions in your C code, and GDB will pause the program’s execution when it reaches those points, allowing you to inspect the program’s state at that moment.

Examining Variables: GDB allows you to examine the values of variables and data structures at any point during program execution. You can print the values of variables, arrays, and structures to understand their current states.

Stepping through Code: You can use GDB to step through your C code one line at a time, allowing you to understand the program’s flow and identify any issues.

Backtrace: GDB can provide a backtrace of the function calls leading up to the current point in the program’s execution, helping you understand the call stack.

Memory Inspection: GDB allows you to inspect the memory of your program, which is particularly useful for identifying issues related to pointers and dynamic memory allocation.

Conditional Breakpoints: You can set breakpoints that only trigger when a certain condition is met, allowing you to focus on specific scenarios during debugging.

Watchpoints: GDB can set watchpoints on variables, which will pause the program when the value of the variable changes, helping you identify when specific variables are modified.

Using GDB effectively requires some familiarity with command-line debugging, as GDB is primarily a command-line tool. However, it provides a rich set of commands and features for analyzing and debugging C programs. Many integrated development environments (IDEs) and code editors also provide integration with GDB, making it easier to use GDB alongside your C development environment.

To use GDB to debug a C program, you need to compile your C code with debugging information enabled. This is typically achieved by adding the -g flag to the compiler command:

gcc -g -o my_program my_program.c

Once your program is compiled with debugging information, you can run GDB by executing:

gdb my_program

GDB will start, and you can use its commands to interactively debug your C program.

13.10 What are the tools for writing good quality C program?

Writing good quality C programs involves a combination of best practices, coding standards, and tools to assist in various aspects of software development. Here are some essential tools that can help you write high-quality C programs:

1. **IDEs (Integrated Development Environments)**: - IDEs like Visual Studio Code, Code::Blocks, or Eclipse CDT provide a friendly development environment with code editing, syntax highlighting, debugging, and version control integration.

2. **Static Code Analyzers**: - Tools like Clang Static Analyzer, cppcheck, and Coverity can help identify potential bugs, code quality issues, and security vulnerabilities in your C code without actually executing it.

3. **Dynamic Analysis Tools**: - Valgrind is a widely used dynamic analysis tool that helps detect memory leaks, memory errors, and performance issues during program execution.

4. **Debuggers**: - GDB (GNU Debugger) is a powerful debugger for C programs that allows you to inspect and manipulate the program’s execution at runtime, making it easier to identify and fix bugs.

5. **Code Formatters**: - Tools like clang-format and Uncrustify can automatically format your C code according to a specified coding style, ensuring consistent and readable code.

6. **Version Control Systems**: - Git, Mercurial, or Subversion are version control systems that help you manage changes to your codebase, collaborate with others, and maintain a history of your code revisions.

7. **Unit Testing Frameworks**: - Unit testing frameworks like CUnit, Unity, or Check enable you to write and run tests for individual units of your code to ensure proper functionality and catch regressions.

8. **Code Review Tools**: - Tools like Phabricator or Gerrit facilitate code reviews, allowing team members to review and provide feedback on code changes, leading to higher code quality and knowledge sharing.

9. **Memory Management Tools**: - For C programs, memory management is crucial. Tools like Electric Fence and Dmalloc can help detect memory-related issues during program execution.

10. **Profiling Tools**: - Profilers like gprof, Valgrind’s Callgrind, or perf can help you identify performance bottlenecks in your code, allowing you to optimize critical sections.

11. **Automated Build Systems**: - Tools like Make or CMake automate the build process, making it easier to compile, link, and build your C project with all its dependencies.

Using these tools, adhering to coding standards, and following best practices can significantly improve the quality of your C programs, reduce bugs, enhance maintainability, and make the development process more efficient.