C Programming Tutorial
C is a general-purpose, compiled programming language that gives you direct control over memory and hardware while still being portable across almost every kind of computer. More than fifty years after its creation, it remains the language of operating system kernels, embedded devices, databases, language runtimes and countless libraries that other languages build on.
This tutorial teaches C from the ground up: how a C program is built, variables and types, operators, control flow, functions, arrays and strings, pointers, dynamic memory, structures, file handling and multi-file programs. It also explains undefined behaviour, the tools that help you write safe C, and how C relates to Java. Every section includes small programs you can compile and run.
1. What Is the C Language?
C is a procedural language: a program is a set of functions that run step by step and work on data. It is compiled ahead of time into native machine code, so there is no virtual machine or interpreter between your program and the processor, and no garbage collector managing memory for you.
C is often described as a "high-level assembly language". It is high-level enough to write readable, portable code with variables, loops and functions, yet low-level enough that you can work with memory addresses, individual bits and hardware registers. That balance explains why it is still used wherever performance, small size or hardware access matters:
- Operating systems: the Linux kernel and large parts of other operating systems are written in C.
- Embedded systems: microcontrollers in cars, appliances, medical devices and IoT sensors.
- Databases and servers: SQLite, PostgreSQL, Redis and the nginx web server.
- Language runtimes: the reference Python interpreter (CPython) and many other interpreters.
- Libraries: compression, cryptography, image and audio libraries used from almost every other language.
Learning C also teaches you how computers really work: how memory is laid out, what a pointer is and what happens when a function is called. That knowledge makes you a better programmer in Java, Python or any other language.
2. A Short History of C
C was created by Dennis Ritchie at Bell Labs in the early 1970s. It grew out of an earlier language called B, written by Ken Thompson, which was itself based on BCPL. In 1973 the Unix operating system was largely rewritten in C, which proved that an operating system could be written in a portable high-level language.
| Year | Milestone |
|---|---|
| 1972 | C developed at Bell Labs by Dennis Ritchie. |
| 1973 | The Unix kernel rewritten in C. |
| 1978 | Kernighan and Ritchie published The C Programming Language, the informal standard known as "K&R C". |
| 1989 / 1990 | The first official standard: ANSI C (C89), adopted by ISO as C90. |
| 1999 | C99 added // comments, long long, stdint.h, bool through stdbool.h, variable-length arrays and declarations anywhere in a block. |
| 2011 | C11 added a memory model with threads and atomics, _Generic, _Static_assert and anonymous structures. |
| 2018 | C17 fixed defects in C11 without adding new features. |
| 2024 | C23 (ISO/IEC 9899:2024) made bool, true and false keywords and added nullptr, typeof, constexpr objects, binary literals, digit separators and #embed. |
C's syntax strongly influenced many later languages, including C++, Java, C#, JavaScript, Go and PHP. If you know Java, much of C's syntax for expressions, loops and conditions will look familiar.
3. Key Features of C
| Feature | What it means for you |
|---|---|
| Small and simple | The core language has only a few dozen keywords, so you can learn all of it. |
| Fast | Compiled to native code with no runtime overhead, so programs start instantly and run close to hardware speed. |
| Portable | Standard C code compiles on everything from 8-bit microcontrollers to supercomputers. |
| Pointers | Direct access to memory addresses for efficient data structures and hardware access. |
| Manual memory management | You decide exactly when memory is allocated and freed, with no garbage-collection pauses. |
| Rich standard library | Functions for input/output, strings, maths, memory and time in headers such as stdio.h and string.h. |
| Interoperability | Almost every language, including Java, Python and Rust, can call C functions, making C the common language of libraries. |
4. Setting Up a C Compiler
To write C you need a text editor and a compiler. The two most popular free compilers are GCC and Clang.
- Linux: install GCC with your package manager, for example
sudo apt install build-essentialon Debian or Ubuntu. - macOS: run
xcode-select --installto get Clang (thegcccommand on macOS also runs Clang). - Windows: install MSYS2 and its MinGW-w64 GCC package, use Visual Studio with the MSVC compiler, or use GCC inside WSL (Windows Subsystem for Linux).
Check the installation with gcc --version or clang --version. Any editor works; Visual Studio Code with the C/C++ extension, CLion and Vim are common choices.
5. Your First C Program
#include <stdio.h> /* gives us printf */
int main(void)
{
printf("Hello, C!\n");
return 0; /* 0 tells the operating system the program succeeded */
}
# Compile with warnings enabled, then run
gcc -std=c17 -Wall -Wextra -o hello hello.c
./hello
Line by line:
#include <stdio.h>is a preprocessor directive. It pastes in the declarations from the standard input/output header so the compiler knows whatprintfis.int main(void)defines the main function, where every C program starts. It returns anintstatus code and, here, takes no arguments.printfprints formatted text.\nis the newline character.return 0;endsmainand reports success.
The -Wall -Wextra flags turn on many useful warnings. Treat warnings as errors to fix, not noise: in C they often point to real bugs.
6. How a C Program Is Built
Typing gcc hello.c actually runs four separate stages:
- Preprocessing (
gcc -E): handles lines starting with#. It inserts header files, expands macros and removes code excluded by#ifconditions. - Compilation (
gcc -S): checks the code for errors and translates it into assembly language for the target processor. - Assembly (
gcc -c): turns the assembly into an object file of machine code. - Linking: combines object files with the libraries they use (such as the C standard library) into a final executable, resolving calls like
printfto real addresses.
Understanding these stages makes error messages clearer: "undefined reference to ..." is a linker error (a function was declared but its code was not found), while "expected ';'" is a compiler error.
7. Variables, Data Types and Constants
Every variable in C has a fixed type, chosen when it is declared. The basic types are:
| Type | Typical size on 64-bit systems | Holds |
|---|---|---|
char | 1 byte | A single character or a small integer. |
short | 2 bytes | Small integers. |
int | 4 bytes | The "natural" integer for the machine. |
long | 8 bytes on Linux and macOS, 4 bytes on Windows | Larger integers. |
long long | 8 bytes | At least 64-bit integers. |
float / double | 4 / 8 bytes | Floating-point numbers; prefer double. |
bool | 1 byte | true or false (a keyword in C23; include stdbool.h in older standards). |
The C standard only guarantees minimum sizes; for example, an int must be at least 16 bits. When you need an exact size, such as for file formats or network protocols, use the fixed-width types from <stdint.h>: int8_t, uint16_t, int32_t, uint64_t and so on. Integer types can also be unsigned, which doubles the positive range but cannot hold negative values.
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#define MAX_USERS 100 /* preprocessor constant: plain text substitution */
int main(void)
{
int age = 29;
double price = 249.50;
char grade = 'A';
bool active = true;
const double TAX_RATE = 0.18; /* typed constant: cannot be changed */
uint32_t packet_id = 4000000000u;
printf("age=%d price=%.2f grade=%c active=%d\n", age, price, grade, active);
printf("price with tax = %.2f\n", price * (1 + TAX_RATE));
printf("packet_id=%u, int is %zu bytes, max users=%d\n",
(unsigned)packet_id, sizeof(int), MAX_USERS);
return 0;
}
8. Operators and Input/Output
C's operators will feel familiar if you know Java:
- Arithmetic:
+ - * / %. Dividing two integers discards the fraction:7 / 2is3, while7 / 2.0is3.5. - Comparison:
== != < > <= >=, which give1for true and0for false. - Logical:
&& || !. In C, any non-zero value counts as true. - Bitwise:
& | ^ ~ << >>, used heavily for flags and hardware registers. - Assignment and increment:
= += -= *= /= ++ --. - Others: the conditional operator
a ? b : c,sizeof, and the pointer operators&and*.
The standard I/O functions printf and scanf use format specifiers: %d for int, %ld for long, %u for unsigned, %f for double in printf, %c for characters, %s for strings, %zu for size_t and %p for pointers.
#include <stdio.h>
int main(void)
{
int a, b;
printf("Enter two whole numbers: ");
if (scanf("%d %d", &a, &b) != 2) { /* scanf returns how many values it read */
fprintf(stderr, "Invalid input\n");
return 1;
}
printf("sum=%d difference=%d product=%d\n", a + b, a - b, a * b);
if (b != 0) {
printf("quotient=%d remainder=%d exact=%.3f\n", a / b, a % b, (double)a / b);
}
return 0;
}
Note the & before a and b in scanf: it passes the address of each variable so that scanf can store values in them. You will see why in the section on pointers.
9. Control Flow
C provides the familiar decision and loop statements: if/else, switch, while, do/while and for, along with break and continue.
#include <stdio.h>
int main(void)
{
/* for loop: print prime numbers below 30 */
for (int n = 2; n < 30; n++) {
int is_prime = 1;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) { is_prime = 0; break; }
}
if (is_prime) printf("%d ", n);
}
printf("\n");
/* switch: choose a branch by an integer value */
int day = 6;
switch (day) {
case 6:
case 7: printf("Weekend\n"); break;
case 1: printf("Start of the week\n"); break;
default: printf("Weekday\n"); break;
}
/* do-while: the body always runs at least once */
int countdown = 3;
do {
printf("%d... ", countdown);
} while (--countdown > 0);
printf("lift-off!\n");
return 0;
}
In a switch, execution "falls through" to the next case unless you write break. That is useful for grouping cases, as with 6 and 7 above, but forgetting a break is a common bug.
10. Functions
Functions break a program into named, reusable pieces. A function must be declared before it is called, either by defining it earlier in the file or by writing a prototype.
#include <stdio.h>
/* Prototypes: tell the compiler the function signatures */
double average(const int values[], int count);
unsigned long long factorial(unsigned n);
int main(void)
{
int marks[] = {72, 88, 95, 61, 79};
int count = sizeof marks / sizeof marks[0];
printf("average = %.2f\n", average(marks, count));
printf("10! = %llu\n", factorial(10));
return 0;
}
double average(const int values[], int count)
{
long sum = 0;
for (int i = 0; i < count; i++) {
sum += values[i];
}
return count > 0 ? (double)sum / count : 0.0;
}
/* Recursive function: calls itself with a smaller problem */
unsigned long long factorial(unsigned n)
{
return n <= 1 ? 1 : n * factorial(n - 1);
}
C passes arguments by value: the function receives a copy, so changing a parameter does not affect the caller's variable. To let a function change the caller's data, you pass a pointer to it. Arrays are the special case: an array argument is passed as a pointer to its first element, so the function can change the original array, which is why average marks its parameter const to promise it will not.
A static local variable keeps its value between calls, and a static function is visible only inside its own source file, which is C's way of making functions private.
11. Arrays and Strings
An array is a fixed-size sequence of elements of the same type stored next to each other in memory. Indexes start at 0, and C does not check whether an index is within bounds.
A string in C is simply an array of char ending with the null character '\0'. The string "cat" therefore needs 4 bytes. Functions in <string.h> work with these null-terminated strings.
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main(void)
{
int scores[5] = {10, 20, 30}; /* remaining elements become 0 */
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("scores[4]=%d grid[1][2]=%d\n", scores[4], grid[1][2]);
char city[32] = "Pune";
printf("'%s' has %zu characters\n", city, strlen(city));
/* snprintf never writes past the buffer size and always adds '\0' */
char greeting[64];
snprintf(greeting, sizeof greeting, "Welcome to %s!", city);
puts(greeting);
/* Read a whole line safely; fgets keeps the newline, so remove it */
char name[40];
printf("Your name: ");
if (fgets(name, sizeof name, stdin) != NULL) {
name[strcspn(name, "\n")] = '\0';
for (size_t i = 0; name[i] != '\0'; i++) {
name[i] = (char)toupper((unsigned char)name[i]);
}
printf("Hello, %s\n", name);
}
if (strcmp(city, "Pune") == 0) { /* never compare strings with == */
printf("Same city\n");
}
return 0;
}
gets (removed from the language in C11), strcpy and sprintf will happily write beyond the end of an array if the input is too long, corrupting memory and creating security holes. Prefer fgets, snprintf and functions that take the buffer size.12. Pointers
A pointer is a variable that stores a memory address. Pointers are what make C powerful, and also what make it demanding. Two operators are central:
&xgives the address ofx.*pdereferencesp: it means "the value at the address stored inp".
#include <stdio.h>
/* Swap works because it receives the ADDRESSES of the caller's variables */
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void)
{
int x = 5;
int *p = &x; /* p holds the address of x */
printf("x=%d, address=%p, via pointer=%d\n", x, (void *)p, *p);
*p = 42; /* changes x through the pointer */
printf("x is now %d\n", x);
int m = 1, n = 2;
swap(&m, &n);
printf("m=%d n=%d\n", m, n);
/* Arrays and pointers: arr decays to a pointer to arr[0] */
int arr[] = {3, 6, 9, 12};
int *q = arr;
for (int i = 0; i < 4; i++) {
printf("%d ", *(q + i)); /* pointer arithmetic: same as arr[i] */
}
printf("\n");
int *nothing = NULL; /* NULL means "points to nothing" */
if (nothing == NULL) {
printf("Always check a pointer before dereferencing it\n");
}
return 0;
}
Pointer arithmetic is scaled by the size of the type: if q points to an int, then q + 1 points to the next int, 4 bytes further on, not the next byte. This is exactly how array indexing works: arr[i] is defined as *(arr + i).
Pointers are also used for pointers to pointers (int **), for passing large structures efficiently, for building linked data structures and for function pointers, which let you pass behaviour as an argument, as the standard qsort function does with its comparison function.
13. Memory Layout and Dynamic Allocation
A running C program's memory is divided into regions:
When the size of data is known only at run time, allocate it on the heap with functions from <stdlib.h>:
malloc(size)allocatessizebytes of uninitialised memory.calloc(count, size)allocates memory forcountelements and sets it to zero.realloc(ptr, new_size)resizes an earlier allocation, possibly moving it.free(ptr)releases memory so it can be reused.
#include <stdio.h>
#include <stdlib.h>
/* A growable array of ints, similar in spirit to Java's ArrayList */
typedef struct {
int *data;
size_t size;
size_t capacity;
} IntList;
int list_push(IntList *list, int value)
{
if (list->size == list->capacity) {
size_t new_cap = list->capacity ? list->capacity * 2 : 4;
int *bigger = realloc(list->data, new_cap * sizeof *bigger);
if (bigger == NULL) {
return -1; /* out of memory: old data is still valid */
}
list->data = bigger;
list->capacity = new_cap;
}
list->data[list->size++] = value;
return 0;
}
int main(void)
{
IntList list = {NULL, 0, 0};
for (int i = 1; i <= 10; i++) {
if (list_push(&list, i * i) != 0) {
fprintf(stderr, "Out of memory\n");
free(list.data);
return 1;
}
}
for (size_t i = 0; i < list.size; i++) {
printf("%d ", list.data[i]);
}
printf("\n(size %zu, capacity %zu)\n", list.size, list.capacity);
free(list.data); /* every allocation needs exactly one free */
list.data = NULL; /* avoid a dangling pointer */
return 0;
}
Manual memory management brings three classic bugs: memory leaks (forgetting to free), use-after-free (using memory after freeing it) and double free (freeing the same memory twice). A simple rule helps: for every allocation, decide which part of the code owns it and is responsible for freeing it.
14. Structures, Unions and Enums
A structure groups related values of different types into one type, much like a class with only fields. An enum gives names to a set of integer constants, and a union lets several members share the same memory, so only one is valid at a time.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum { DRAFT, PUBLISHED, ARCHIVED } Status;
typedef struct {
int id;
char title[64];
double rating;
Status status;
} Article;
static const char *status_name(Status s)
{
switch (s) {
case DRAFT: return "draft";
case PUBLISHED: return "published";
case ARCHIVED: return "archived";
}
return "unknown";
}
static int by_rating_desc(const void *a, const void *b)
{
const Article *x = a, *y = b;
return (y->rating > x->rating) - (y->rating < x->rating);
}
int main(void)
{
Article items[] = {
{1, "Pointers Explained", 4.6, PUBLISHED},
{2, "Memory Layout", 4.9, PUBLISHED},
{3, "Bit Manipulation", 4.2, DRAFT},
};
size_t n = sizeof items / sizeof items[0];
/* qsort from the standard library takes a function pointer for comparison */
qsort(items, n, sizeof items[0], by_rating_desc);
for (size_t i = 0; i < n; i++) {
const Article *a = &items[i]; /* use -> to reach members through a pointer */
printf("#%d %-20s %.1f (%s)\n", a->id, a->title, a->rating, status_name(a->status));
}
Article copy = items[0]; /* structs are copied by value */
strcpy(copy.title, "Changed copy");
printf("original title still: %s\n", items[0].title);
return 0;
}
Use . to reach a member of a structure variable and -> to reach it through a pointer (a->id is shorthand for (*a).id). Pass large structures to functions by pointer, using const when the function should not change them, to avoid copying them.
15. File Handling
Files are accessed through a FILE * handle returned by fopen. Always check that opening succeeded, and close the file with fclose.
#include <stdio.h>
int main(void)
{
/* Write a small CSV file: "w" creates or truncates, "a" appends */
FILE *out = fopen("expenses.csv", "w");
if (out == NULL) {
perror("fopen"); /* prints the reason, such as a permission error */
return 1;
}
fprintf(out, "item,amount\n");
fprintf(out, "books,450.00\n");
fprintf(out, "lunch,180.50\n");
fprintf(out, "metro,60.00\n");
fclose(out);
/* Read it back line by line and total the amounts */
FILE *in = fopen("expenses.csv", "r");
if (in == NULL) {
perror("fopen");
return 1;
}
char line[128];
char item[64];
double amount, total = 0;
fgets(line, sizeof line, in); /* skip the header row */
while (fgets(line, sizeof line, in) != NULL) {
if (sscanf(line, "%63[^,],%lf", item, &amount) == 2) {
printf("%-8s %8.2f\n", item, amount);
total += amount;
}
}
fclose(in);
printf("total %8.2f\n", total);
return 0;
}
Open files in binary mode ("rb", "wb") when working with non-text data, and use fread and fwrite to transfer blocks of bytes.
16. Header Files and Multi-File Programs
Real programs are split across several files. The convention is:
- A header file (
.h) contains declarations: function prototypes, type definitions and constants that other files need. - A source file (
.c) contains the definitions: the actual function bodies.
#ifndef MATHUTIL_H /* include guard: prevents double inclusion */
#define MATHUTIL_H
int gcd(int a, int b);
int lcm(int a, int b);
#endif
#include "mathutil.h"
int gcd(int a, int b)
{
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a < 0 ? -a : a;
}
int lcm(int a, int b)
{
return (a == 0 || b == 0) ? 0 : (a / gcd(a, b)) * b;
}
#include <stdio.h>
#include "mathutil.h" /* quotes: search the project directory first */
int main(void)
{
printf("gcd(84, 36) = %d\n", gcd(84, 36));
printf("lcm(4, 6) = %d\n", lcm(4, 6));
return 0;
}
# Compile each file to an object file, then link them together
gcc -Wall -Wextra -c mathutil.c
gcc -Wall -Wextra -c main.c
gcc -o calc main.o mathutil.o
./calc
Compiling files separately means only changed files need to be recompiled. Build tools automate this: Make uses a Makefile of rules and dependencies, and CMake generates build files for many platforms and IDEs.
17. Undefined Behaviour and Safe C
The C standard leaves some operations undefined. If a program does one of them, the standard places no requirements on what happens: it might work, crash, give wrong results, or behave differently after a compiler upgrade. Optimising compilers assume undefined behaviour never happens, so its effects can be surprising. Common examples:
- Reading or writing outside the bounds of an array.
- Dereferencing a
NULL, freed or uninitialised pointer. - Overflowing a signed integer (unsigned integers wrap around safely).
- Reading an uninitialised local variable.
- Modifying a variable twice without a sequence point, as in
i = i++ + 1;. - Modifying a string literal, such as
char *s = "hi"; s[0] = 'H';.
Modern tools catch most of these problems during development:
# Strict warnings, debug symbols and runtime checks for memory and undefined behaviour
gcc -std=c17 -Wall -Wextra -Wpedantic -g -fsanitize=address,undefined -o app app.c
./app
# Step through the program in a debugger
gdb ./app
# On Linux, Valgrind finds leaks and invalid memory use without recompiling
valgrind --leak-check=full ./app
18. C vs. C++ vs. Java
| Aspect | C | C++ | Java |
|---|---|---|---|
| Paradigm | Procedural | Multi-paradigm: procedural, object-oriented, generic | Object-oriented with functional features |
| Execution | Compiled to native code | Compiled to native code | Compiled to bytecode, run by the JVM with JIT compilation |
| Memory management | Manual (malloc/free) | Manual, plus RAII and smart pointers | Automatic garbage collection |
| Pointers | Full pointer arithmetic | Pointers and references | References only; no pointer arithmetic |
| Safety checks | Few; bounds are not checked | Few by default; safer library types available | Array bounds and null checks at run time |
| Standard library | Small | Large (containers, algorithms, strings) | Very large |
| Typical uses | Kernels, embedded, system libraries | Games, browsers, high-performance applications | Enterprise back ends, Android, big data |
Calling C from Java: Java can use native C libraries through the classic Java Native Interface (JNI) or, more simply, the Foreign Function and Memory API (java.lang.foreign), which became a standard feature in Java 22. It lets Java code call C functions and work with native memory without writing any C glue code. Start with the Java tutorial if you want to compare the two languages side by side.
19. Best Practices and Common Mistakes
Best practices
- Compile with warnings (
-Wall -Wextra) and fix every one. - Initialise every variable when you declare it.
- Check every return value that can signal failure:
malloc,fopen,scanf,fgets. - Always pass buffer sizes and use bounded functions such as
snprintfandfgets. - Use
constfor data a function should not change, andsize_tfor sizes and indexes. - Keep ownership clear: document which function frees each allocation, and set pointers to
NULLafter freeing. - Keep functions small and hide internal helpers with
static. - Test with sanitizers and Valgrind before release.
Common mistakes to avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
Writing if (x = 5) | Assigns 5 and is always true. | Use ==; compiler warnings catch this. |
Off-by-one loops such as i <= n on an array of n | Writes one element past the end. | Loop with i < n. |
Forgetting room for '\0' | Strings run into neighbouring memory. | Allocate strlen(s) + 1 bytes. |
Comparing strings with == | Compares addresses, not contents. | Use strcmp. |
| Returning a pointer to a local variable | The memory is gone when the function returns. | Return by value, use a caller-supplied buffer, or allocate on the heap. |
Using sizeof on an array parameter | Gives the size of a pointer, not the array. | Pass the length as a separate parameter. |
Ignoring malloc returning NULL | Crash or memory corruption when memory runs out. | Check the result and handle the failure. |
20. Summary
- C is a small, fast, portable, compiled language that gives direct control over memory and hardware.
- A C program is built in four stages: preprocessing, compilation, assembly and linking.
- Types have minimum sizes; use
<stdint.h>when you need exact widths. - Arrays are fixed-size and unchecked; strings are
chararrays ending in'\0'. - Pointers store addresses; they enable pass-by-reference, dynamic data structures and function pointers.
- Heap memory comes from
malloc,callocandreallocand must be released withfree. - Structures, enums and unions model data; header files and separate compilation organise larger programs.
- Undefined behaviour is C's biggest risk; warnings, sanitizers, debuggers and Valgrind are your safety net.
A great next step is to write a small project that uses everything here, such as a command-line contact book that stores records in a file, grows a dynamic array and sorts entries with qsort. Then open the FAQ tab for answers to the questions learners most often ask about C.
Frequently Asked Questions (FAQ)
Q1. Is C still worth learning today?
Yes. C still runs operating system kernels, embedded devices, databases, network equipment and the runtimes of other languages, so C skills remain in demand in systems, embedded and performance-critical work.
Even if you mainly use Java or Python, learning C teaches you how memory, pointers and the call stack really work, which helps you understand performance and debug problems in any language.
Q2. Is C a good first programming language?
C can be an excellent first language if you want to understand how computers work, because it hides very little. It is also less forgiving than languages such as Python or Java: mistakes with memory or strings can crash a program or cause strange behaviour rather than producing a clear error message.
If you start with C, compile with warnings and sanitizers enabled from the beginning so the tools help you find mistakes early.
Q3. What is the difference between C and C++?
C++ began as an extension of C and can compile most C code, but today they are separate languages with separate standards. C++ adds classes, templates, exceptions, namespaces, references, RAII and a large standard library of containers and algorithms. C stays deliberately small and simple.
Some valid C is not valid C++, for example assigning the result of malloc to a typed pointer without a cast. C is often chosen for kernels, embedded firmware and libraries that must be callable from many languages.
Q4. Which C standard should I use?
For new code, C17 is the safe, widely supported choice, and C23 is the newest standard if your compiler supports it well (recent GCC and Clang versions do). Choose the standard explicitly with a compiler flag such as -std=c17 or -std=c23 so your code behaves the same on every machine.
Embedded toolchains sometimes support only C99 or C11, so check your target platform first.
Q5. What does #include actually do?
#include is handled by the preprocessor before compilation. It copies the entire contents of the named file into your source file at that point. Headers mainly contain declarations, so including <stdio.h> tells the compiler what printf looks like; the actual code for printf is added later by the linker from the C library.
Angle brackets search the system include directories, while double quotes search the current project directory first.
Q6. Why does main return an int?
The value returned from main becomes the program's exit status, which the operating system passes to whoever started the program, such as a shell script or build tool. By convention, 0 (or EXIT_SUCCESS) means success and any other value signals an error.
Since C99, reaching the end of main without a return statement behaves as if it returned 0, but writing it explicitly is clearer.
Q7. What is a pointer, in simple terms?
A pointer is a variable that holds the address of another value in memory, like a note that says where something is stored rather than the thing itself. Using &x gives the address of x, and *p follows the pointer p to the value it points to.
Pointers let functions change the caller's variables, let you work with dynamically allocated memory and arrays efficiently, and are the building block of linked lists, trees and other data structures.
Q8. What is the difference between an array and a pointer?
An array is a block of elements with a fixed size; a pointer is a single variable holding an address. They are closely related because, in most expressions, an array name automatically converts ("decays") into a pointer to its first element, and a[i] is the same as *(a + i).
They are not the same, though: sizeof on an array gives the size of the whole array, while on a pointer it gives the size of the pointer, and an array name cannot be reassigned to point elsewhere.
Q9. What is a segmentation fault?
A segmentation fault ("segfault") happens when a program tries to access memory it is not allowed to, and the operating system stops it. Common causes are dereferencing a NULL or uninitialised pointer, using memory after it has been freed, accessing far outside an array and infinite recursion that overflows the stack.
Compile with -g -fsanitize=address or run the program in gdb to see exactly which line caused it.
Q10. What is the difference between malloc and calloc?
malloc(size) allocates the requested number of bytes and leaves their contents uninitialised. calloc(count, size) allocates space for count elements of size bytes each and sets every byte to zero; it also checks that count * size does not overflow.
Use calloc when you need zeroed memory and malloc when you will immediately fill the memory yourself. Both return NULL on failure and both must be released with free.
Q11. What is a memory leak, and how do I find one?
A memory leak happens when a program allocates heap memory and loses every pointer to it without calling free. The memory cannot be reused, so a long-running program gradually consumes more and more.
Tools such as Valgrind (valgrind --leak-check=full ./app) and LeakSanitizer, included with -fsanitize=address on many platforms, report each leaked block and the line where it was allocated.
Q12. What is undefined behaviour?
Undefined behaviour is any operation for which the C standard sets no rules, such as reading past the end of an array, overflowing a signed integer or using a freed pointer. The program might appear to work, crash, or produce wrong results, and the effect can change with compiler versions or optimisation levels.
Because compilers optimise on the assumption that it never happens, the safest approach is to avoid it entirely and use sanitizers to detect it during testing.
Q13. Why is gets() dangerous?
gets reads a line into a buffer but has no way to know how big the buffer is, so a long input overwrites neighbouring memory. This made it a famous source of security vulnerabilities, and it was removed from the C language in C11.
Use fgets(buffer, sizeof buffer, stdin) instead, which never writes more than the buffer can hold.
Q14. What is the difference between a struct and a union?
In a struct, each member has its own storage, so all members can hold values at the same time and the struct's size is at least the sum of its members. In a union, all members share the same storage, so only one member is meaningful at a time and the union's size is that of its largest member.
Unions are used to save memory or to represent a value that can be one of several types, usually together with a field that records which member is currently in use.
Q15. What does the static keyword mean in C?
static has two main meanings depending on where it is used:
- On a local variable, it keeps the variable alive for the whole program, so it remembers its value between function calls.
- On a global variable or function, it limits visibility to the current source file, which is C's way of making something private.
Q16. What is the difference between a macro and a function?
A macro, defined with #define, is replaced by text before compilation, so there is no function call and no type checking. A function is real compiled code with typed parameters that are evaluated exactly once.
Macros can cause surprises: #define SQUARE(x) x * x turns SQUARE(a + 1) into a + 1 * a + 1. Prefer static inline functions and const or enum constants; use macros mainly for include guards and conditional compilation.
Q17. Does C have strings or booleans?
C has no built-in string type. A string is an array of char ending with the null character '\0', and <string.h> provides functions to work with such arrays.
Booleans were added in C99 as _Bool, with the friendlier names bool, true and false from <stdbool.h>. In C23 these became proper keywords, so the header is no longer needed.
Q18. Can C be used for object-oriented programming?
C has no classes, but you can apply object-oriented ideas. Structures hold the data, functions that take a pointer to the structure act as methods, opaque types declared in headers (typedef struct Stack Stack;) hide implementation details, and structures of function pointers provide a form of polymorphism.
Large C projects, including the Linux kernel and GTK, use these techniques extensively.
Q19. How can Java code call a C library?
There are two main options. The traditional Java Native Interface (JNI) requires writing C glue code with special function names and compiling it into a shared library. The modern Foreign Function and Memory API in java.lang.foreign, standard since Java 22, lets Java look up and call C functions directly and manage native memory safely, with no C glue code.
The jextract tool can generate Java bindings automatically from a C header file.
Q20. What are good projects to practise C?
Projects that combine files, strings, structures and dynamic memory are ideal:
- A command-line to-do list or contact book that saves to a file.
- Your own dynamic array, linked list and hash table, tested with Valgrind.
- A text tool such as a word counter or a simple
grep. - A calculator that parses expressions.
- A small interpreter, an image converter for a simple format, or programs for a microcontroller board such as an Arduino or Raspberry Pi Pico.