CSE 240 Module 4

Advanced C Programming: Memory, Files, and Data Structures

Structure Padding & Memory Layout

The compiler adds padding bytes between structure members to ensure proper memory alignment for efficient access. Understanding this is crucial for memory optimization and avoiding bugs.

Memory Alignment Rules

Most systems require data to be aligned on boundaries equal to their size:

Example: Poor vs Good Structure Design

// Poor design - lots of padding struct bad_example { char a; // 1 byte // 3 bytes padding int b; // 4 bytes char c; // 1 byte // 7 bytes padding double d; // 8 bytes }; // Total: 24 bytes // Better design - minimized padding struct good_example { double d; // 8 bytes int b; // 4 bytes char a; // 1 byte char c; // 1 byte // 2 bytes padding }; // Total: 16 bytes
Memory Layout Visualization:

Bad Structure (24 bytes): a___bbbbc_______dddddddd

Good Structure (16 bytes): ddddddddbbbbac__
Tip: Use `sizeof()` to check actual structure sizes and `offsetof()` from stddef.h to see member positions.

Packed Structures

// Force no padding (compiler-specific) #pragma pack(1) struct packed_struct { char a; int b; char c; }; // Exactly 6 bytes, but slower access #pragma pack() // Or use __attribute__((packed)) with GCC struct packed_struct2 { char a; int b; char c; } __attribute__((packed));

File I/O Operations

C provides both high-level (stdio.h) and low-level file operations. Understanding both gives you full control over file handling.

Standard File Operations

#include <stdio.h> #include <stdlib.h> int main() { FILE *file; char buffer[256]; // Opening files file = fopen("data.txt", "r"); // read if (file == NULL) { perror("Error opening file"); return 1; } // Reading operations fgets(buffer, sizeof(buffer), file); // read line rewind(file); // go back to start char ch; while ((ch = fgetc(file)) != EOF) { // read char by char putchar(ch); } fclose(file); // Writing operations file = fopen("output.txt", "w"); fprintf(file, "Number: %d\n", 42); fputs("Hello world\n", file); fputc('!', file); fclose(file); return 0; }

File Modes

File Opening Modes:
"r" - Read only (file must exist) "w" - Write only (truncates or creates) "a" - Append (writes at end) "r+" - Read/write (file must exist) "w+" - Read/write (truncates or creates) "a+" - Read/append "rb" - Binary read "wb" - Binary write

Working with Struct Arrays and Files

#include <stdio.h> #include <stdlib.h> typedef struct { int id; char name[50]; double gpa; } Student; int write_students(const char *filename, Student students[], int count) { FILE *file = fopen(filename, "wb"); if (!file) { perror("Cannot open file for writing"); return 0; } size_t written = fwrite(students, sizeof(Student), count, file); fclose(file); return written == count; } int read_students(const char *filename, Student **students, int *count) { FILE *file = fopen(filename, "rb"); if (!file) return 0; // Get file size to determine number of students fseek(file, 0, SEEK_END); long file_size = ftell(file); rewind(file); *count = file_size / sizeof(Student); *students = malloc(*count * sizeof(Student)); if (!*students) { fclose(file); return 0; } size_t read = fread(*students, sizeof(Student), *count, file); fclose(file); return read == *count; }

Advanced Position-Based Operations

// Random access to records in a file void update_student_gpa(const char *filename, int student_id, double new_gpa) { FILE *file = fopen(filename, "r+b"); // read/write binary if (!file) return; Student temp; long position = 0; // Search for student by ID while (fread(&temp, sizeof(Student), 1, file) == 1) { if (temp.id == student_id) { // Found student, update GPA temp.gpa = new_gpa; // Seek back to this record's position fseek(file, position, SEEK_SET); fwrite(&temp, sizeof(Student), 1, file); break; } position = ftell(file); // save position for next iteration } fclose(file); } // Get file statistics void analyze_student_file(const char *filename) { FILE *file = fopen(filename, "rb"); if (!file) return; fseek(file, 0, SEEK_END); long file_size = ftell(file); int num_students = file_size / sizeof(Student); printf("File: %s\n", filename); printf("Size: %ld bytes\n", file_size); printf("Students: %d\n", num_students); printf("Record size: %zu bytes\n", sizeof(Student)); fclose(file); }

Buffer Interaction: Formatted vs Unformatted

#include <stdio.h> void demonstrate_buffer_mixing() { FILE *file = fopen("mixed_io.txt", "w+"); // Write formatted data fprintf(file, "Line 1: %d\n", 100); fprintf(file, "Line 2: %d\n", 200); // Write unformatted data char raw_data[] = "RAW DATA"; fwrite(raw_data, 1, sizeof(raw_data), file); // IMPORTANT: Flush before switching read/write modes fflush(file); rewind(file); // Read back with mixed methods char line_buffer[100]; fgets(line_buffer, sizeof(line_buffer), file); printf("Formatted read: %s", line_buffer); char raw_buffer[20]; fread(raw_buffer, 1, 10, file); printf("Unformatted read: %.10s\n", raw_buffer); fclose(file); } // Buffer management functions void buffer_control_demo() { FILE *file = fopen("test.txt", "w"); // Different buffering modes char *buffer = malloc(8192); setvbuf(file, buffer, _IOFBF, 8192); // full buffering // Write lots of data - stays in buffer for (int i = 0; i < 1000; i++) { fprintf(file, "Line %d\n", i); } printf("Data written to buffer, not disk yet\n"); // Force flush to disk fflush(file); printf("Now data is on disk\n"); fclose(file); free(buffer); }
typedef struct { int id; char name[50]; double salary; } Employee; int write_employee_binary(const char *filename, Employee emp) { FILE *file = fopen(filename, "wb"); if (!file) return 0; size_t written = fwrite(&emp, sizeof(Employee), 1, file); fclose(file); return written == 1; } int read_employee_binary(const char *filename, Employee *emp) { FILE *file = fopen(filename, "rb"); if (!file) return 0; size_t read = fread(emp, sizeof(Employee), 1, file); fclose(file); return read == 1; }

File Positioning

FILE *file = fopen("data.txt", "r+"); // Get current position long pos = ftell(file); // Move to specific position fseek(file, 0, SEEK_SET); // beginning fseek(file, 0, SEEK_END); // end fseek(file, -10, SEEK_CUR); // 10 bytes back from current // Alternative positioning rewind(file); // same as fseek(file, 0, SEEK_SET) fclose(file);

Buffer Management

Proper buffer management prevents overflow attacks, memory corruption, and ensures efficient I/O operations.

Buffer Types and Control

#include <stdio.h> int main() { FILE *file = fopen("output.txt", "w"); // Buffer control setvbuf(file, NULL, _IONBF, 0); // no buffering setvbuf(file, NULL, _IOLBF, 0); // line buffering setvbuf(file, NULL, _IOFBF, 8192); // full buffering // Custom buffer char custom_buffer[4096]; setvbuf(file, custom_buffer, _IOFBF, sizeof(custom_buffer)); // Force flush fprintf(file, "This gets flushed immediately\n"); fflush(file); // force write to disk fclose(file); return 0; }

Safe String Handling

// Unsafe - buffer overflow risk char buffer[50]; gets(buffer); // NEVER USE THIS! scanf("%s", buffer); // Also dangerous // Safe alternatives char safe_buffer[50]; fgets(safe_buffer, sizeof(safe_buffer), stdin); // Remove newline from fgets safe_buffer[strcspn(safe_buffer, "\n")] = '\0'; // Safe formatted input char name[50]; scanf("%49s", name); // limit to buffer size - 1 // Even safer with snprintf char output[100]; snprintf(output, sizeof(output), "Hello %s", name);

Buffer Overflow Prevention

Always validate input sizes! Buffer overflows are a major security vulnerability. Use functions like fgets(), snprintf(), and strncpy() instead of their unsafe counterparts.
// Safe string copying and concatenation char dest[100]; char src[] = "Hello World"; // Safe copy strncpy(dest, src, sizeof(dest) - 1); dest[sizeof(dest) - 1] = '\0'; // ensure null termination // Safe concatenation size_t remaining = sizeof(dest) - strlen(dest) - 1; strncat(dest, " - Added text", remaining);

Parameter Passing Mechanisms

C uses pass-by-value for all parameters, but you can simulate other passing methods using pointers and arrays.

Pass by Value

void modify_value(int x) { x = 100; // only changes local copy printf("Inside function: %d\n", x); } int main() { int num = 5; modify_value(num); printf("After function: %d\n", num); // still 5 return 0; }

Pass by Reference (using pointers)

void modify_reference(int *x) { *x = 100; // changes original value printf("Inside function: %d\n", *x); } void swap_values(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main() { int num = 5; modify_reference(&num); printf("After function: %d\n", num); // now 100 int x = 10, y = 20; swap_values(&x, &y); printf("x=%d, y=%d\n", x, y); // x=20, y=10 return 0; }

Array Parameters

// Arrays are always passed by reference (pointer to first element) void print_array(int arr[], int size) { // arr is actually int *arr for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); } void modify_array(int arr[], int size) { for (int i = 0; i < size; i++) { arr[i] *= 2; // modifies original array } } int main() { int numbers[] = {1, 2, 3, 4, 5}; int size = sizeof(numbers) / sizeof(numbers[0]); print_array(numbers, size); modify_array(numbers, size); print_array(numbers, size); // values doubled return 0; }

Pass by Alias (References in C++)

// C++ reference parameters (pass-by-alias) #ifdef __cplusplus void swap_by_alias(int &a, int &b) { int temp = a; a = b; b = temp; } void modify_by_alias(int &x) { x = 999; // directly modifies original variable } // References must be initialized and cannot be reassigned void reference_demo() { int original = 42; int &ref = original; // ref is an alias for original ref = 100; // changes original to 100 printf("original = %d\n", original); // prints 100 } #endif // In C, simulate pass-by-alias with pointers void c_style_alias(int *const x) { *x = 999; // const pointer simulates reference behavior }

Complex Type Parameter Passing

typedef struct { int id; char name[50]; double scores[5]; } Student; // Pass struct by value (expensive - full copy) void print_student_copy(Student s) { printf("ID: %d, Name: %s\n", s.id, s.name); // Changes to s don't affect original } // Pass struct by address (efficient - pointer) void print_student_ref(const Student *s) { printf("ID: %d, Name: %s\n", s->id, s->name); // Use const to prevent accidental modification } // Modify struct through pointer void update_student(Student *s, double new_score) { s->scores[0] = new_score; // modifies original } // Return struct by value Student create_student(int id, const char *name) { Student s; s.id = id; strncpy(s.name, name, sizeof(s.name) - 1); s.name[sizeof(s.name) - 1] = '\0'; // Initialize scores for (int i = 0; i < 5; i++) { s.scores[i] = 0.0; } return s; // returns copy of struct }
// Function pointer declarations int add(int a, int b) { return a + b; } int multiply(int a, int b) { return a * b; } // Function pointer variable int (*operation)(int, int); int main() { operation = add; printf("5 + 3 = %d\n", operation(5, 3)); operation = multiply; printf("5 * 3 = %d\n", operation(5, 3)); return 0; }

Linked Lists

Linked lists are dynamic data structures where elements (nodes) contain data and a pointer to the next element. They allow efficient insertion and deletion but require sequential access.

Basic Node Structure

// Simple linked list node typedef struct Node { int data; struct Node *next; } Node; // Alternative with typedef for cleaner syntax typedef struct ListNode ListNode; struct ListNode { int data; ListNode *next; };

Linked List Visualization

10 20 30 NULL

Complete Linked List Implementation

#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; // Create new node Node* create_node(int data) { Node *new_node = malloc(sizeof(Node)); if (new_node == NULL) { printf("Memory allocation failed\n"); exit(1); } new_node->data = data; new_node->next = NULL; return new_node; } // Insert at beginning Node* insert_front(Node *head, int data) { Node *new_node = create_node(data); new_node->next = head; return new_node; // new head } // Insert at end Node* insert_end(Node *head, int data) { Node *new_node = create_node(data); if (head == NULL) { return new_node; } Node *current = head; while (current->next != NULL) { current = current->next; } current->next = new_node; return head; } // Delete by value Node* delete_node(Node *head, int data) { if (head == NULL) return NULL; // Delete head node if (head->data == data) { Node *temp = head; head = head->next; free(temp); return head; } // Find and delete other nodes Node *current = head; while (current->next != NULL && current->next->data != data) { current = current->next; } if (current->next != NULL) { Node *temp = current->next; current->next = current->next->next; free(temp); } return head; } // Print list void print_list(Node *head) { Node *current = head; while (current != NULL) { printf("%d", current->data); if (current->next != NULL) printf(" -> "); current = current->next; } printf(" -> NULL\n"); } // Free entire list void free_list(Node *head) { Node *current = head; while (current != NULL) { Node *temp = current; current = current->next; free(temp); } } // Search for value Node* search(Node *head, int data) { Node *current = head; while (current != NULL) { if (current->data == data) { return current; } current = current->next; } return NULL; }

Practical Application: Student Database System

Let's combine all module 4 concepts into a complete program that manages student records using files, proper parameter passing, and linked lists.

#include <stdio.h> #include <stdlib.h> #include <string.h> // Well-designed struct with minimal padding typedef struct { double gpa; // 8 bytes (largest first) int id; // 4 bytes char name[32]; // 32 bytes char grade; // 1 byte // 3 bytes padding = 48 bytes total } StudentRecord; // Linked list node for in-memory operations typedef struct StudentNode { StudentRecord data; struct StudentNode *next; } StudentNode; // Function prototypes demonstrating different parameter passing void print_record_copy(StudentRecord student); // pass-by-value void print_record_ref(const StudentRecord *student); // pass-by-address void update_gpa(StudentRecord *student, double new_gpa); // pass-by-address StudentRecord create_student(int id, const char *name, double gpa, char grade); // Linked list operations StudentNode* add_student_front(StudentNode *head, StudentRecord student); StudentNode* remove_student_front(StudentNode *head); void traverse_students(StudentNode *head); StudentNode* find_student_by_id(StudentNode *head, int id); void free_student_list(StudentNode *head); // File operations with proper buffering int save_students_to_file(const char *filename, StudentNode *head); StudentNode* load_students_from_file(const char *filename); int main() { StudentNode *student_list = NULL; // Create some students StudentRecord s1 = create_student(1001, "Alice Johnson", 3.8, 'A'); StudentRecord s2 = create_student(1002, "Bob Smith", 3.2, 'B'); StudentRecord s3 = create_student(1003, "Carol Davis", 3.9, 'A'); // Add to linked list student_list = add_student_front(student_list, s1); student_list = add_student_front(student_list, s2); student_list = add_student_front(student_list, s3); printf("Student List:\n"); traverse_students(student_list); // Demonstrate parameter passing printf("\nDemonstrating parameter passing:\n"); print_record_copy(s1); // pass-by-value (copy) print_record_ref(&s1); // pass-by-address (pointer) // Modify through pointer update_gpa(&s1, 4.0); printf("After GPA update:\n"); print_record_ref(&s1); // Save to file if (save_students_to_file("students.dat", student_list)) { printf("\nStudents saved to file successfully\n"); } // Clean up free_student_list(student_list); // Load back from file StudentNode *loaded_list = load_students_from_file("students.dat"); if (loaded_list) { printf("\nLoaded students from file:\n"); traverse_students(loaded_list); free_student_list(loaded_list); } return 0; } // Pass-by-value: receives copy of entire struct void print_record_copy(StudentRecord student) { printf("COPY - ID: %d, Name: %s, GPA: %.2f\n", student.id, student.name, student.gpa); // Any changes here don't affect original } // Pass-by-address: receives pointer to struct void print_record_ref(const StudentRecord *student) { printf("REF - ID: %d, Name: %s, GPA: %.2f\n", student->id, student->name, student->gpa); // const prevents accidental modification } void update_gpa(StudentRecord *student, double new_gpa) { student->gpa = new_gpa; // modifies original struct } StudentRecord create_student(int id, const char *name, double gpa, char grade) { StudentRecord student; student.id = id; student.gpa = gpa; student.grade = grade; // Safe string copy strncpy(student.name, name, sizeof(student.name) - 1); student.name[sizeof(student.name) - 1] = '\0'; return student; // return by value } // Linked list implementations StudentNode* add_student_front(StudentNode *head, StudentRecord student) { StudentNode *new_node = malloc(sizeof(StudentNode)); if (!new_node) { fprintf(stderr, "Memory allocation failed\n"); exit(EXIT_FAILURE); } new_node->data = student; new_node->next = head; return new_node; } StudentNode* remove_student_front(StudentNode *head) { if (head == NULL) { printf("Cannot remove from empty list\n"); return NULL; } StudentNode *temp = head; head = head->next; printf("Removing student: %s (ID: %d)\n", temp->data.name, temp->data.id); free(temp); return head; } void traverse_students(StudentNode *head) { StudentNode *current = head; int position = 0; while (current != NULL) { printf("[%d] ID: %d, Name: %-15s, GPA: %.2f, Grade: %c\n", position, current->data.id, current->data.name, current->data.gpa, current->data.grade); current = current->next; position++; } } StudentNode* find_student_by_id(StudentNode *head, int id) { StudentNode *current = head; while (current != NULL) { if (current->data.id == id) { return current; } current = current->next; } return NULL; } void free_student_list(StudentNode *head) { StudentNode *current = head; while (current != NULL) { StudentNode *temp = current; current = current->next; free(temp); } } // File operations with proper buffering int save_students_to_file(const char *filename, StudentNode *head) { FILE *file = fopen(filename, "wb"); if (!file) { perror("Cannot open file for writing"); return 0; } // Set up efficient buffering for batch writes char *buffer = malloc(8192); if (buffer) { setvbuf(file, buffer, _IOFBF, 8192); } // Write each student record StudentNode *current = head; int count = 0; while (current != NULL) { size_t written = fwrite(¤t->data, sizeof(StudentRecord), 1, file); if (written != 1) { fprintf(stderr, "Error writing student %d\n", current->data.id); fclose(file); free(buffer); return 0; } current = current->next; count++; } fflush(file); // ensure all data is written fclose(file); free(buffer); printf("Saved %d student records\n", count); return 1; } StudentNode* load_students_from_file(const char *filename) { FILE *file = fopen(filename, "rb"); if (!file) { perror("Cannot open file for reading"); return NULL; } StudentNode *head = NULL; StudentRecord temp_student; // Read records and build linked list while (fread(&temp_student, sizeof(StudentRecord), 1, file) == 1) { head = add_student_front(head, temp_student); } fclose(file); return head; }
This complete example demonstrates: structure padding optimization, file I/O with binary data, proper buffering, all parameter passing methods, and full linked list operations including the specific functions required by the learning objectives.

Advanced: Doubly Linked Lists

typedef struct DNode { int data; struct DNode *next; struct DNode *prev; } DNode; DNode* insert_sorted(DNode *head, int data) { DNode *new_node = malloc(sizeof(DNode)); new_node->data = data; new_node->next = NULL; new_node->prev = NULL; // Empty list if (head == NULL) { return new_node; } // Insert at beginning if (data < head->data) { new_node->next = head; head->prev = new_node; return new_node; } // Find insertion point DNode *current = head; while (current->next != NULL && current->next->data < data) { current = current->next; } // Insert after current new_node->next = current->next; new_node->prev = current; if (current->next != NULL) { current->next->prev = new_node; } current->next = new_node; return head; }

Doubly Linked List

NULL 10 20 30 NULL

Advanced Topics & Common Patterns

Memory Management Best Practices

// Always check malloc return values void* safe_malloc(size_t size) { void *ptr = malloc(size); if (ptr == NULL) { fprintf(stderr, "Memory allocation failed\n"); exit(EXIT_FAILURE); } return ptr; } // RAII-style cleanup with function pointers typedef struct { void *data; void (*cleanup)(void*); } Resource; Resource* create_resource(size_t size, void (*cleanup_func)(void*)) { Resource *res = safe_malloc(sizeof(Resource)); res->data = safe_malloc(size); res->cleanup = cleanup_func; return res; } void destroy_resource(Resource *res) { if (res && res->cleanup) { res->cleanup(res->data); } free(res); }

Error Handling Patterns

typedef enum { SUCCESS = 0, ERROR_NULL_POINTER, ERROR_MEMORY_ALLOCATION, ERROR_FILE_NOT_FOUND, ERROR_INVALID_INPUT } ErrorCode; ErrorCode process_file(const char *filename, int **result, size_t *count) { if (!filename || !result || !count) { return ERROR_NULL_POINTER; } FILE *file = fopen(filename, "r"); if (!file) { return ERROR_FILE_NOT_FOUND; } // Process file... *result = malloc(100 * sizeof(int)); if (!*result) { fclose(file); return ERROR_MEMORY_ALLOCATION; } // Success *count = 100; fclose(file); return SUCCESS; }

Practice Exercise

Click to reveal a coding challenge:

Glossary

Alignment
Memory addresses that are multiples of the data type size for efficient processor access.
Buffer
Temporary storage area in memory used to hold data during I/O operations.
EOF
End-of-File constant (-1) returned when no more data can be read from a file.
FILE*
Pointer to a FILE structure that represents an open file stream.
Padding
Extra bytes added by the compiler between structure members to maintain alignment.
Pass-by-value
Parameter passing where a copy of the argument is made; original value unchanged.
Pass-by-reference
Simulated in C using pointers to pass the address of a variable.
Node
Basic unit of a linked list containing data and pointer(s) to other nodes.
Traversal
Process of visiting each node in a linked list sequentially.
Memory Leak
Allocated memory that is not freed, causing gradual memory consumption.
Dangling Pointer
Pointer that references memory that has been deallocated or is invalid.
Buffer Overflow
Writing data beyond the allocated buffer size, potentially overwriting adjacent memory.

Key Takeaways

Memory Management: Always pair malloc() with free(). Check for NULL returns. Use tools like valgrind to detect leaks.
Structure Design: Order members by size (largest first) to minimize padding and memory usage.
File Operations: Always check if file operations succeed. Close files when done. Use appropriate buffering for your use case.
Safe Programming: Use bounds-checking functions. Validate all inputs. Initialize pointers to NULL.
Common Mistakes to Avoid: Using gets(), forgetting to free memory, not checking malloc returns, buffer overflows, accessing freed memory, infinite loops in linked lists.