Understand C syntax, types, control flow, functions, arrays, strings, pointers, dynamic memory, structs, file I/O, and safe memory practices with complete programs.
What Is C?
C is a compiled, general-purpose language widely used in operating systems, embedded software, databases, and performance-sensitive libraries. It gives programmers direct control over memory, so correct pointer and buffer handling is essential.
Why learn C
- It teaches how programs interact with memory and the machine.
- Many other languages (C++, Java, C#, JavaScript) borrow its syntax.
- It is the foundation of operating systems and embedded devices.
How C programs are built
- Preprocess:
#includeand#definelines are expanded. - Compile: source code becomes object code.
- Link: object files and libraries are combined into an executable.
- Run: the operating system loads and executes the program.
Hello, world and how to compile it
#include <stdio.h>
int main(void) {
printf("Hello, C!\n");
return 0;
}Compiling with warnings enabled
cc -Wall -Wextra -Wpedantic -std=c11 main.c -o main
./mainTypes, Variables, and Operators
- Integer types:
char,short,int,long,long long, withunsignedvariants. - Floating-point types:
floatanddouble. - Use
size_tfor sizes and array indexes, and fixed-width types such asint32_tfrom<stdint.h>when exact size matters. - Always initialize variables; reading an uninitialized variable is undefined behavior.
- Integer division truncates:
7 / 2is3. Cast todoublefor fractional results. - Signed integer overflow is undefined behavior; check ranges before arithmetic.
Types, sizes, and casting
#include <stdio.h>
#include <stdint.h>
int main(void) {
int apples = 7;
int people = 2;
double share = (double)apples / people; // 3.5, not 3
uint8_t small = 255;
int32_t exact = -42;
printf("share = %.2f\n", share);
printf("sizeof(int) = %zu bytes\n", sizeof(int));
printf("small = %u, exact = %d\n", (unsigned)small, exact);
return 0;
}Control Flow and Input
if/else,switch,for,while, anddo...whilecontrol execution.switchcases needbreakto avoid falling through to the next case.- Prefer
fgetsovergets(removed) or unboundedscanf("%s")for reading text. - Always check return values of input functions.
Reading a number safely and branching
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char line[64];
printf("Enter your score: ");
if (fgets(line, sizeof line, stdin) == NULL) {
fprintf(stderr, "No input\n");
return 1;
}
int score = (int)strtol(line, NULL, 10);
if (score >= 90) puts("Grade A");
else if (score >= 75) puts("Grade B");
else puts("Keep practicing");
for (int i = 1; i <= 3; ++i) {
printf("Round %d\n", i);
}
return 0;
}Functions and Header Files
- Declare a function's prototype before use so the compiler can check calls.
- C passes arguments by value; pass a pointer to let a function change the caller's variable.
- Use
conston pointer parameters that the function only reads. - Put prototypes in
.hfiles with include guards and definitions in.cfiles. - Mark file-private helpers
static.
Prototype, definition, and a header file
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int clamp(int value, int low, int high);
#endif
// math_utils.c
#include "math_utils.h"
int clamp(int value, int low, int high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
// main.c
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("%d\n", clamp(150, 0, 100)); // 100
return 0;
}Arrays and Strings
- Arrays store elements of one type in contiguous memory.
- C does not check array bounds; the programmer must keep indexes valid.
- An array passed to a function decays to a pointer, so also pass its length.
- A C string is a
chararray ending with a null terminator'\0'. - Use bounded functions such as
snprintfandstrncpycarefully, and always leave room for the terminator.
Arrays and string basics
#include <stdio.h>
#include <string.h>
double average(const int values[], size_t count) {
if (count == 0) return 0.0;
long long sum = 0;
for (size_t i = 0; i < count; ++i) sum += values[i];
return (double)sum / (double)count;
}
int main(void) {
int scores[] = {82, 91, 76, 88};
size_t count = sizeof scores / sizeof scores[0];
printf("Average: %.2f\n", average(scores, count));
char name[16];
snprintf(name, sizeof name, "%s %d", "User", 42);
printf("%s (length %zu)\n", name, strlen(name));
return 0;
}Pointers
A pointer stores an address. Pointers let functions modify caller data, work with arrays, and build dynamic data structures.
&xgives the address ofx;*pdereferences pointerp.- A
NULLpointer points to nothing; never dereference it. - Pointer arithmetic moves by whole elements:
p + 1advances bysizeof *pbytes. - Only dereference pointers that refer to valid, live objects.
- Do not return the address of a local variable; it is destroyed when the function ends.
Pointers, swap, and pointer arithmetic
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int x = 3, y = 9;
swap(&x, &y);
printf("x=%d y=%d\n", x, y); // x=9 y=3
int nums[] = {10, 20, 30};
int *p = nums; // points to nums[0]
printf("%d %d\n", *p, *(p + 2)); // 10 30
int *none = NULL;
if (none == NULL) puts("pointer is NULL");
return 0;
}Dynamic Memory: malloc and free
mallocallocates memory on the heap and returnsNULLon failure; always check it.callocallocates and zeroes memory;reallocresizes a block.- Every successful allocation must be released with exactly one
free. - Set pointers to
NULLafter freeing to avoid accidental reuse. - Common bugs: memory leaks, double free, use after free, and buffer overflows.
Allocating and freeing a dynamic array
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *data = malloc(n * sizeof *data);
if (data == NULL) {
fprintf(stderr, "Out of memory\n");
return 1;
}
for (size_t i = 0; i < n; ++i) data[i] = (int)(i * i);
int *bigger = realloc(data, 10 * sizeof *data);
if (bigger == NULL) { // original block still valid
free(data);
return 1;
}
data = bigger;
printf("data[4] = %d\n", data[4]);
free(data);
data = NULL;
return 0;
}Structs and Custom Types
structgroups related values under one name.- Use
typedefto avoid repeating thestructkeyword. - Pass large structs by pointer, using
constwhen they are read-only. - Access members with
.on values and->on pointers.
A struct with functions and heap allocation
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
int age;
double gpa;
} Student;
void print_student(const Student *s) {
printf("%s (%d) GPA %.2f\n", s->name, s->age, s->gpa);
}
int main(void) {
Student a = {"Asha", 20, 3.8};
print_student(&a);
Student *b = malloc(sizeof *b);
if (!b) return 1;
snprintf(b->name, sizeof b->name, "%s", "Ravi");
b->age = 21;
b->gpa = 3.4;
print_student(b);
free(b);
return 0;
}File I/O
fopenreturnsNULLon failure; always check it.- Use
fgets,fprintf,fread, andfwritefor reading and writing. - Close every file with
fclose. - Use
perrorto print a helpful system error message.
Writing and reading a text file
#include <stdio.h>
int main(void) {
FILE *out = fopen("notes.txt", "w");
if (!out) { perror("fopen"); return 1; }
fprintf(out, "line one\nline two\n");
fclose(out);
FILE *in = fopen("notes.txt", "r");
if (!in) { perror("fopen"); return 1; }
char line[128];
while (fgets(line, sizeof line, in)) {
printf("read: %s", line);
}
fclose(in);
return 0;
}Common Bugs and How to Catch Them
- Buffer overflow: writing past the end of an array. Use bounded functions and check lengths.
- Use after free: reading memory after
free. Set pointers toNULLafter freeing. - Memory leak: forgetting to free. Pair each allocation with a free path.
- Uninitialized variables: always initialize before reading.
- Off-by-one errors: remember arrays are zero-indexed.
- Format string mismatches: match
printfspecifiers to argument types.
Using sanitizers and warnings to find bugs
cc -g -Wall -Wextra -Wpedantic -std=c11 -fsanitize=address,undefined main.c -o main
./main
# Memory checking on Linux:
valgrind --leak-check=full ./mainBest Practices
- Compile with
-Wall -Wextra -Wpedanticand fix every warning. - Check the result of
malloc,fopen, and input functions. - Prefer
size_tfor sizes and indexes; pass array lengths explicitly. - Use
constwherever data is not modified. - Keep functions short and give them clear ownership rules for memory.
- Never use unsafe functions such as
gets; avoid unboundedstrcpyandsprintf. - Test with sanitizers and a memory checker regularly.
Complete Example: Calculating an Average Safely
Compile with a C compiler such as GCC or Clang. For example: cc -Wall -Wextra -Wpedantic -std=c11 main.c -o main.
Calculate an average safely
#include <stdio.h>
#include <stddef.h>
double average(const int values[], size_t count) {
if (count == 0) {
return 0.0;
}
long long sum = 0;
for (size_t i = 0; i < count; ++i) {
sum += values[i];
}
return (double)sum / (double)count;
}
int main(void) {
const int scores[] = {82, 91, 76, 88};
const size_t count = sizeof scores / sizeof scores[0];
printf("Average: %.2f\n", average(scores, count));
return 0;
}Frequently Asked Questions
Is C difficult to learn?
C requires careful attention to types, pointers, and memory, but learning it builds a strong understanding of how programs interact with memory.
What is a pointer?
A pointer is an object that stores the address of another object or function, subject to C's pointer rules.
Why compile with warnings?
Compiler warnings can catch suspicious conversions, missing declarations, and other mistakes before the program runs.
What is the difference between malloc and calloc?
malloc allocates uninitialized memory, while calloc allocates memory for an array and sets all bytes to zero.
What is undefined behavior?
It means the C standard places no requirements on what happens, such as reading out of bounds or overflowing a signed integer. The program might crash, appear to work, or behave unpredictably.
What is the difference between C and C++?
C++ started from C but adds classes, templates, a larger standard library, and many other features. Idiomatic C and idiomatic C++ look quite different.