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.
Most systems require data to be aligned on boundaries equal to their size:
// 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
// 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));
C provides both high-level (stdio.h) and low-level file operations. Understanding both gives you full control over file handling.
#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;
}
#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;
}
// 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);
}
#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 *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);
Proper buffer management prevents overflow attacks, memory corruption, and ensures efficient I/O operations.
#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;
}
// 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);
// 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);
C uses pass-by-value for all parameters, but you can simulate other passing methods using pointers and arrays.
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;
}
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;
}
// 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;
}
// 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
}
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 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.
// 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;
};
#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.