C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
54: Dynamic Allocation with malloc and calloc
Up until now, you've likely been declaring your arrays with a fixed size, like int scores[100];. In a controlled classroom environment, that works fine. But in the real world, you rarely know exactly how much data you're dealing with before the program actually starts running. I've seen countless junior devs try to solve this by just picking a "magic number"—declaring an array of 10,000 elements "just in case."
The Danger of the Magic Number
If you declare a massive static array, you're gambling. If only ten users sign up for your service, you've wasted a huge chunk of memory for no reason. Worse, if 10,001 users sign up, your program will either crash or, even more dangerously, start writing data into memory it doesn't own, leading to those nightmare-inducing segmentation faults. Static allocation happens on the stack, and the stack is relatively small. If you try to allocate a massive array there, you'll blow the stack before your main function even gets a chance to run.
// The naive way: guessing the size
int scores[1000]; // What if we have 1001 players? What if we have 2?
Asking for Exactly What You Need
This is where we move to the heap. Instead of telling the compiler "I need space for 1000 ints," we tell the operating system at runtime, "I need exactly this much space." We do this with malloc (memory allocation). It takes a single argument: the total number of bytes you want. Since different systems have different sizes for integers, we always multiply the number of elements by sizeof(type).
The catch is that malloc returns a void*—a generic pointer. You'll see some people cast this pointer to the desired type, but in C, that's generally unnecessary and can actually hide bugs. I prefer to just assign it directly to a typed pointer.
int num_players;
printf("How many players? ");
scanf("%d", &num_players);
// Request exactly enough space for the players provided
int *scores = malloc(num_players * sizeof(int));
if (scores == NULL) {
// Always check for NULL. If the OS is out of memory, malloc fails.
fprintf(stderr, "Memory allocation failed!\n");
return 1;
}
Clean Slates vs. Raw Speed
Now, here is a nuance that often trips people up: malloc doesn't clear the memory it gives you. It gives you a raw block of RAM that might still contain "garbage" data from whatever program used that memory last. If you try to read from a malloc'd array before writing to it, you'll get random, unpredictable numbers.
If you need your memory to be zeroed out from the start, you can use calloc (contiguous allocation). It takes two arguments: the number of elements and the size of each element. I usually reach for calloc when I'm creating something like a frequency counter or a lookup table where starting at zero is a requirement. The trade-off is a tiny bit of performance; zeroing out memory takes time, though in 99% of applications, you'll never notice the difference.
// malloc: Fast, but contains garbage
int *raw_data = malloc(10 * sizeof(int));
// calloc: Slightly slower, but everything is initialized to 0
int *clean_data = calloc(10, sizeof(int));
The Debt You Always Have to Pay
The biggest difference between stack memory and heap memory is that the heap is not self-cleaning. When a function ends, its local variables vanish. But memory allocated with malloc or calloc stays there until you explicitly tell the OS you're done with it. If you lose the pointer to that memory without freeing it, you've created a memory leak. Do this inside a loop in a long-running server, and you'll eventually eat all the available RAM on the machine.
I make it a habit to write the free() call the very moment I write the malloc() call. It keeps the "debt" visible so you don't forget to pay it back.
free(scores);
scores = NULL; // Good practice to prevent "dangling pointers"
📋 Practical Task
Exercise: Dynamic Student Grade Tracker
Write a program that manages student grades using dynamic memory allocation. Your program should follow these requirements:
- Prompt the user to enter the number of students in a class.
- Use
mallocto allocate an array offloatvalues based on that number. - Use a loop to allow the user to input the grade for each student.
- Calculate and print the class average.
- Properly
freethe allocated memory before the program exits. - Include a check to ensure
mallocdid not returnNULL.
There are no comments for now.