Sep 27, 2026

C Programming from Zero to Advanced: A Complete Guide for Absolute Beginners

A complete C guide for absolute beginners: from installing a compiler through pointers, memory management, linked lists, files, and multi-file programs, ending with a student grade manager you build yourself.

Start by installing a compiler and finish by building a working "student grade manager" on your own. The guide has 20 chapters (0–19) and 6 appendices; Chapters 1–17 end with exercises, and all the answers are collected in Appendix A.

How to Use This Guide

Who this guide is for

  • People who have never written code and want C to be their first programming language;
  • People who have learned some C but feel shaky about pointers, memory, or files;
  • Anyone about to study data structures, operating systems, or embedded development, or preparing for exams and technical interviews, who needs a solid foundation in C.

How to get the most out of it

  1. Read in order. The chapters build on one another, and later material depends on earlier material. Sections marked "(Optional)" can be skipped on a first read.
  2. Type every example yourself. Don't copy and paste: code that passes through your fingers stays in your head. Once an example runs, change something and watch how the result changes.
  3. Do the exercises before reading the answers. Turn to Appendix A only after you've been stuck for half an hour or more. Once you understand the answer, close it and write the solution again from scratch.
  4. When pointers and memory come up, draw pictures. Sketching variables, addresses, and arrows on paper beats staring at the code ten times.

Suggested pace (1.5–2 hours a day)

PartChaptersCore topicsSuggested time
Part I: The Basics0–6Tools, data types, input and output, operators, branches, loopsAbout 2 weeks
Part II: The Core7–10Functions, arrays, strings, pointersAbout 2–3 weeks
Part III: Going Further11–17Structures, dynamic memory, linked lists, files, multi-file programs, bitwise operations, debuggingAbout 2–3 weeks
Part IV: Putting It All Together18–19Capstone project, where to go nextAbout 1 week

Conventions

  • The code follows the C11 standard and also works under C99, C17, and C23. Every complete example program has been compiled and run with both GCC and Clang.
  • Lines starting with $ are commands typed in a terminal; don't type the $ itself.
  • In the "Output" blocks, what you type on the keyboard appears together with the program's output, just as it does on your screen.
  • The call-outs mean: Tip—a practical technique or extra detail; Caution—an easy place to go wrong; Common mistake—a pitfall beginners fall into again and again.

Part I: The Basics

Goal of this part: set up a development environment and master C's most basic "syntax parts"—data types, variables, input and output, operators—plus the branches and loops that control a program's flow. By the end, you'll be able to write small programs like a calculator or a number-guessing game.

Chapter 0: Getting to Know C

0.1 Computers only understand 0s and 1s

A computer's "brain," the CPU (central processing unit), can only execute machine instructions made of 0s and 1s. On a common x86 processor, for example, the instruction "put the number 5 into register eax" looks like this:

text
10111000 00000101 00000000 00000000 00000000

The earliest programmers wrote programs this way, one bit at a time, which was both painful and error-prone. So assembly language was invented, replacing the 0s and 1s with short English abbreviations:

text
mov eax, 5

Assembly is much easier to read, but it's tied to a specific CPU: switch to a different kind of CPU and the program has to be rewritten. Later came high-level languages, which read much more like the way people think and don't depend on any particular hardware:

c
int a = 5;

The CPU still understands only machine instructions, though, so something has to translate high-level code into them. There are two ways to translate:

  • Compiled: like translating an entire book before publishing it, so readers read the finished translation. You translate (compile) once and can then run the result as many times as you like, quickly. C, C++, Go, and Rust work this way.
  • Interpreted: like a simultaneous interpreter who translates while the speaker talks. It's flexible, but the "interpreter" has to be present every time the program runs, so it's relatively slow. Python and JavaScript are classic examples.

C is a classic compiled language. The program that does the translating is called a compiler.

0.2 Where C came from

Starting in 1969, Ken Thompson and Dennis Ritchie at Bell Labs in the United States began developing the Unix operating system. Thompson designed a language called B; Ritchie added data types and other features to B, and by around 1972 the result had become C. In 1973 the Unix kernel was rewritten in C, which made Unix one of the first operating systems written mainly in a high-level language. From then on, Unix could be ported to different computers relatively easily, and C spread around the world along with it.

In 1978, Brian Kernighan and Dennis Ritchie published The C Programming Language, affectionately known as K&R, which served as C's de facto standard for many years. C has since been standardized several times:

StandardPublishedMain changes
C89 / C901989 / 1990The first official standard (often called ANSI C); ironed out the differences between compilers
C991999// single-line comments, long long, a Boolean type, variable-length arrays, declarations in the middle of code, for (int i = 0; ...)
C112011Multithreading support, _Generic, _Static_assert, anonymous structures; removed the dangerous gets function
C172018Defect fixes only, no new features (sometimes called C18)
C232024bool/true/false become keywords; adds nullptr, constexpr, typeof, binary literals such as 0b1010, digit separators such as 1'000'000, and more

The code in this guide targets C11, and it compiles unchanged under newer standards. Starting with version 15, GCC uses C23 by default, so you'll have no trouble running this guide's code on a recent compiler.

0.3 Why learn C today?

C is more than fifty years old. Why is it still worth learning?

  1. It's everywhere. The kernels of Linux, Windows, and macOS are written mostly in C; embedded devices such as appliances, car electronics, and microcontrollers run huge amounts of C code; the SQLite, Redis, and PostgreSQL databases, the Nginx web server, the Git version-control tool, and Python's official interpreter (CPython) are all written in C.
  2. It shows you how computers really work. Memory, addresses, pointers, compiling and linking—concepts that Python and Java hide from you—are all out in the open in C. Once you know C well, every other language comes faster.
  3. It's the ancestor of many languages. C++, Java, C#, JavaScript, and Go all borrow heavily from C's syntax: if, for, while, curly braces... After C, these languages will look very familiar.
  4. School and career. C is the first programming course in many university science and engineering programs; data structures courses and coding interviews commonly use C or C++; and jobs in embedded systems, device drivers, operating systems, and high-performance computing all require it.

C's strengths and weaknesses can be summed up like this:

StrengthsWeaknesses
Fast, and uses few resourcesYou manage memory by hand
Close to the hardware; you can work with memory directlyAlmost no safety checks: out-of-bounds array accesses, dangling pointers, and similar errors aren't caught automatically
Portable: nearly every platform has a C compilerThe standard library is small, so you implement many things yourself
Small, simple syntax with few keywordsSome syntax (such as complicated pointer declarations) is hard to read

In one sentence: C gives you enormous freedom, and expects you to take matching responsibility.

0.4 How to learn C well

  1. Practice first. Programming is a craft; reading without practicing teaches you nothing. Type in and run every example in this guide yourself.
  2. Learn to read error messages. Compiler errors are your best teacher. They look cryptic at first, but the common ones number only a few dozen (Appendix F lists the most frequent), and you'll soon recognize them.
  3. Understand rather than memorize. Don't memorize syntax rules; understand why things are designed the way they are. Once you understand the memory model, pointers stop being scary.
  4. Draw pictures. With arrays, pointers, and linked lists, sketching memory cells and arrows on paper often solves the problem by itself.
  5. Take small steps. Compile and run after every few lines. Don't write hundreds of lines before compiling for the first time: facing dozens of errors at once, you won't know where to start.
  6. Learn to use the documentation. To find out how a function works, look it up on cppreference (en.cppreference.com, which has a C section).
  7. Use AI tools wisely. Asking an AI to explain an error message, walk you through a concept, or review code you've written is a great way to learn. But letting it write all your exercises is like lending your gym card to someone else so they can run for you: the card got used, but you didn't get any stronger.

Chapter 1: Setting Up and Writing Your First Program

1.1 What tools do you need?

To write C programs you need two things:

  • An editor for writing source code. In principle Notepad would do, but a real code editor gives you syntax highlighting, autocompletion, and error hints that make you far more productive.
  • A compiler to translate C source code into a program you can run. The three major C compilers are GCC (the GNU Compiler Collection, standard on Linux), Clang (the default compiler on macOS), and MSVC (Microsoft's compiler, installed with Visual Studio).

Software that bundles an editor, a compiler, and a debugger is called an IDE (integrated development environment). With an IDE installed, you can write, compile, run, and debug all in one place.

The whole process looks like this:

text
     write                    compile                           run
hello.c (text file) ──────────────▶ hello.exe (executable) ──────────────▶ output on screen

1.2 Installing a development environment

Pick the option that matches your operating system.

Windows option 1: Visual Studio Community (the easiest)

  1. Download Visual Studio Community (free for individual learners) from Microsoft's website.
  2. During installation, select the "Desktop development with C++" workload (it includes the C compiler as well).
  3. Create a new project from the "Empty Project" template, then right-click "Source Files" → Add → New Item, and name the file main.c. The extension must be `.c`: with .cpp, your code will be compiled as C++.
  4. When your code is ready, press Ctrl + F5 (Start Without Debugging) to run it.
Caution
Visual Studio reports error C4996 when you use scanf; see Section 3.2 for the fix.

Windows option 2: VS Code + MinGW-w64 (lightweight and flexible)

  1. Download and install MSYS2 from msys2.org. The default install location is C:\msys64.
  2. When installation finishes, an "MSYS2 UCRT64" terminal window opens. Install the GCC toolchain by running the command below. When asked which packages to install, press Enter to accept the default (all of them), then type Y to confirm:
bash
   pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain
  1. Add C:\msys64\ucrt64\bin to the Path environment variable (Settings → System → About → Advanced system settings → Environment Variables).
  2. Open a new Command Prompt window and type gcc --version. If you see version information, the installation worked.
  3. Install VS Code, and from the Extensions view, install Microsoft's official C/C++ extension.

Windows option 3: Code::Blocks or Dev-C++ (ready to go)

These lightweight IDEs come with a compiler, so you can start writing code right after installing them. For Code::Blocks, choose the installer whose name includes mingw (such as codeblocks-25.03mingw-setup.exe). They're widely used in classrooms; their features are fairly basic, which makes them a good fit for beginners.

macOS

Open Terminal and run:

bash
xcode-select --install

Click Install in the dialog that appears to get Apple's Command Line Tools, which include the Clang compiler. Afterward, run clang --version to check. On macOS, typing gcc actually runs Clang, and it's used in exactly the same way. For an editor, VS Code or CLion are good choices.

Linux

On most distributions, one command does the job:

bash
# Ubuntu / Debian
sudo apt update
sudo apt install build-essential gdb

# Fedora
sudo dnf install gcc gdb

# Arch Linux
sudo pacman -S base-devel gdb

Afterward, check the installation with gcc --version.

Other options

  • CLion: JetBrains' professional C/C++ IDE, free for non-commercial use (including personal learning) since 2025.
  • Online compilers such as Compiler Explorer (godbolt.org) and OnlineGDB (onlinegdb.com) let you write and run code in a browser, which is handy for quick experiments. For long-term learning, though, set up an environment on your own computer.
Tip
Don't spend long agonizing over tools. For a beginner, the differences between these compilers barely matter. Pick one that works and start learning.

1.3 Your first program: Hello, World!

By programming tradition (made popular by K&R's The C Programming Language), your first program says hello to the world. Create a file named hello.c and enter the following code:

c
#include <stdio.h>

int main(void)
{
    printf("Hello, World!\n");
    return 0;
}

Output:

text
Hello, World!

These seven short lines contain the basic skeleton of every C program. Let's go through them one at a time:

CodeMeaning
#include <stdio.h>Includes the standard input/output header (standard input/output). The printf function is declared in this file; without it, the compiler doesn't know what printf is
int main(void)Defines the main function. main is the program's entry point: execution always starts here, and every program has exactly one main. int means it returns an integer to the operating system when it finishes; void means it takes no parameters
{ and }The part enclosed in braces is the function body, the statements the function executes
printf("Hello, World!\n");Calls printf to print text on the screen. The text in double quotes is a string; \n means "new line"; the semicolon ; at the end marks the end of a statement
return 0;Ends main and returns 0 to the operating system, meaning "the program finished normally"

A few rules you must remember:

  • C is case-sensitive. printf can't be written Printf, and main can't be written Main.
  • Declarations and ordinary statements end with a semicolon. Statements such as printf(...); and return 0;, and declarations such as the variable definitions you'll meet in Chapter 2 (int a = 5;), all need one at the end; the semicolon is to C what the period is to an English sentence. Preprocessing directives that start with #, the function header int main(void), and the braces { and } that enclose a block of code don't take one. (Braces that are part of a declaration are different: with an array's initial values in Chapter 8, as in int a[5] = {1, 2, 3, 4, 5};, or a structure definition in Chapter 11, the declaration still ends with a ; after the }.)
  • Use plain ASCII punctuation in code. Word processors and some web pages turn straight quotes " into curly quotes “ ”, and code containing curly quotes won't compile. (Curly quotes and other non-ASCII characters are fine inside a string, between the straight quotes.)
Caution
Some older textbooks write the main function as void main(). That's not standard C, and some compilers reject it or warn about it. Always use int main(void), or the version with parameters introduced in Chapter 10, int main(int argc, char *argv[]).

1.4 Compiling and running

If you use an IDE, just click Run. But it's important to understand the command line too, because those IDE buttons do exactly this behind the scenes. Open a terminal, go to the folder containing hello.c, and type:

bash
$ gcc hello.c -o hello
$ ./hello
Hello, World!
  • The first command runs GCC to compile hello.c; -o hello names the resulting program hello. Without -o, the default name is a.out on Linux and macOS and a.exe on Windows.
  • The second command runs the program. In the Windows Command Prompt, type hello (or hello.exe); in PowerShell, type .\hello.exe.

For everyday learning, use this "enhanced" compile command instead:

bash
$ gcc -Wall -Wextra -std=c11 -g hello.c -o hello
OptionEffect
-Wall -WextraTurn on the common warnings and extra warnings. Many hidden mistakes show up early as warnings
-std=c11Compile according to the C11 standard
-gInclude debugging information so you can investigate problems with a debugger (Chapter 17)
-o helloName the output program
Tip
Treat compiler warnings as if they were errors, and aim for programs with zero warnings. This habit will save you a great deal of wasted time.

1.5 What happens during compilation

The single command gcc hello.c -o hello actually performs four steps in a row:

text
hello.c ──preprocess──▶ hello.i ──compile──▶ hello.s ──assemble──▶ hello.o ──link──▶ hello
 source                expanded source      assembly code        object file         executable
  1. Preprocessing: handles the directives that start with #. For example, #include <stdio.h> is replaced by the entire contents of stdio.h, so the preprocessed file often runs to hundreds or thousands of lines.
  2. Compilation: translates the C code into assembly code. Syntax errors are caught in this step.
  3. Assembly: translates the assembly code into machine instructions, producing an object file (a .o file).
  4. Linking: the machine code for printf isn't in your hello.o; it lives in the C standard library. The linker joins your object file with the standard library to produce the final executable.

You can watch each stage separately with these commands:

bash
$ gcc -E hello.c -o hello.i    # preprocess only
$ gcc -S hello.i -o hello.s    # compile to assembly
$ gcc -c hello.s -o hello.o    # assemble into an object file
$ gcc hello.o -o hello         # link into an executable

Open hello.i and hello.s and take a look (hello.o is a binary file and looks like gibberish in a text editor). It's fine if you can't understand them yet; what matters is remembering that source code becomes a program in four steps: preprocessing, compiling, assembling, and linking. Later, when you hit an error, you'll be able to tell which step it came from.

1.6 Comments

Comments are notes for human readers; the compiler ignores them completely. C has two kinds:

c
// Single-line comment: from // to the end of the line

/* Multi-line comment: from slash-star
   to star-slash,
   and it can span several lines */

Multi-line comments can't be nested: in /* outer /* inner */ outer */, the first */ ends the comment, and the leftover outer */ causes a compile error.

Good comments explain why something is done instead of repeating what the code does:

c
i = i + 1;   // add 1 to i                <- useless: the code already says this
i = i + 1;   // skip the header line      <- useful: explains the intent
Tip
While debugging, you can use comments to temporarily "switch off" code. In most editors, select the lines and press Ctrl + / (Cmd + / on macOS) to comment or uncomment them.

1.7 The most common compile errors for beginners

Making mistakes is normal; what matters is being able to read the error messages. GCC's error format is:

text
file:line:column: error: description

Here are the errors beginners hit most often (shown as GCC 13 prints them; the wording varies slightly between versions):

What went wrongCompiler messageCause and fix
Missing semicolon after printf("Hello\n")error: expected ';' before 'return'A missing semicolon. The compiler only notices that the statement didn't end when it reaches return on the next line; GCC marks the position at the end of the line that's missing the semicolon. In some other cases (such as a missing semicolon after a variable definition), the reported line is the next one
Curly quotes “Hello”, often from copying code out of a document or web pageerror: stray '\342' in programNon-ASCII characters in the code. Retype them as straight quotes "
print("Hello\n");implicit declaration of function 'print'; did you mean 'printf'? and undefined reference to 'print'A misspelled function name: the compiler can't find a function called print
int mian(void)undefined reference to 'main'main is misspelled, so the linker can't find the program's entry point
Forgot #include <stdio.h>implicit declaration of function 'printf' and include '<stdio.h>' or provide a declaration of 'printf'Using a function that hasn't been declared. Newer compilers (such as GCC 14 and later) treat this as an error outright
printf("Hello\n);error: missing terminating " characterThe quotes around a string don't match
Missing } at the enderror: expected declaration or statement at end of inputThe braces don't match. Get into the habit of typing } right after {
printf("%d\n", cout); (meant count)error: 'cout' undeclared (first use in this function); did you mean 'count'?A misspelled or undefined variable

Two principles for handling errors:

  1. Fix the first error first. One mistake often triggers a cascade of follow-on errors; fix the first, and the rest may disappear.
  2. Look at the reported line, and at the one above it. With mistakes like a missing semicolon or unmatched parentheses, the real problem is sometimes before the reported position.

1.8 Frequently asked questions

Problem 1: Non-English characters in the output come out garbled.

This mostly happens on Windows when a program prints characters outside plain ASCII, such as accented letters like é, symbols like °, or text in other writing systems. Editors such as VS Code save source files as UTF-8 by default, while the Windows console traditionally displays text using an older "code page," so the same bytes are interpreted differently. Either of these fixes works:

  • Before running the program, run chcp 65001 in the console window to switch it to UTF-8;
  • Or, in Windows' region settings, enable "Beta: Use Unicode UTF-8 for worldwide language support" (this affects the whole system, and some older programs may then display garbled text, so choose carefully).

All the example programs in this guide print plain English text, so you won't run into this unless you add such characters yourself.

Problem 2: The program window flashes and disappears.

When you double-click an .exe file to run it, the window closes as soon as the program finishes. To fix this, run the program from a Command Prompt window; in Visual Studio, run it with Ctrl + F5 (the window stays open after the program ends); or temporarily add getchar(); before return 0; so that the program waits for you to press Enter.

Problem 3: "'gcc' is not recognized as an internal or external command."

This means Windows can't find GCC. Check that the compiler's folder (such as C:\msys64\ucrt64\bin) is in your Path environment variable, and reopen the Command Prompt window (windows that were already open don't pick up new environment variables).

Exercises

  1. Business card. Write a program that prints your name, your major (or job), and a favorite motto on separate lines, framed by a line of ==================== above and below.
  2. Make mistakes on purpose. Introduce each of these errors into the Hello World program: delete the semicolon, change printf to Printf, delete the closing brace }, and replace the straight double quotes with curly quotes “ ”. Make one change at a time, compile, and write down the error message, so that you learn to connect each mistake with its message.
  3. Star triangle. Use printf to print the pattern below (use only printf, no loops):
text
      *
     ***
    *****
   *******

Chapter 2: Data Types, Variables, and Constants

Programs are all about processing data: grades, prices, names, coordinates... This chapter shows how C represents and stores different kinds of data.

2.1 How computers store data

Inside a computer there are only 0s and 1s. A single 0 or 1 is called a bit, the smallest unit of storage; 8 bits make a byte, the unit you'll use most often. In computing, 1 KB traditionally means 1024 bytes, 1 MB means 1024 KB, and so on. (Strictly speaking, the international standard names for these binary units are KiB and MiB, while in the metric system 1 kB is 1000 bytes—the decimal definition drive makers use when they label capacity.)

Picture memory as a long row of lockers. Each locker holds 1 byte and has a unique number called its address:

text
address   0x1000   0x1001   0x1002   0x1003   0x1004  ...
        +--------+--------+--------+--------+--------+
content |01001000|01101001|00000000|11111111|00101010| ...
        +--------+--------+--------+--------+--------+

Addresses are usually written in hexadecimal (starting with 0x). You'll need to be comfortable with three number systems:

  • Decimal: what we use every day; base 10, with the digits 0–9.
  • Binary: what computers use; base 2, with only 0 and 1.
  • Hexadecimal: a shorthand for binary; base 16, with the digits 0–9 and A–F (standing for 10–15). One hexadecimal digit corresponds to exactly 4 binary digits, which makes hex very convenient for writing out memory contents.
DecimalBinaryHexDecimalBinaryHex
000000810008
100011910019
200102101010A
300113111011B
401004121100C
501015131101D
601106141110E
701117151111F

Decimal to binary: divide by 2 repeatedly and read the remainders in reverse order. For 13: 13 ÷ 2 = 6 remainder 1, 6 ÷ 2 = 3 remainder 0, 3 ÷ 2 = 1 remainder 1, 1 ÷ 2 = 0 remainder 1. Reading the remainders from last to first gives 1101.

Binary to decimal: multiply each digit by its place value and add: 1101 = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8 + 4 + 0 + 1 = 13.

Binary to hexadecimal: split the bits into groups of 4 starting from the right, and look each group up in the table: 1010 1111 → AF, that is, 0xAF.

2.2 Variables: named lockers

A variable is a named piece of memory that holds data that can change. Before you use a variable, you must define it, telling the compiler its type and its name:

c
#include <stdio.h>

int main(void)
{
    int age = 18;            // define an integer variable age and initialize it to 18
    double height = 1.75;    // a variable for a number with a fractional part
    char grade = 'A';        // a character variable

    printf("Age: %d\n", age);
    printf("Height: %.2f m\n", height);
    printf("Grade: %c\n", grade);

    age = 19;                // assign the variable a new value
    printf("Age next year: %d\n", age);
    return 0;
}

Output:

text
Age: 18
Height: 1.75 m
Grade: A
Age next year: 19

The %d, %f, and %c inside printf are format specifiers, meaning "put an integer / a floating-point number / a character here." Chapter 3 covers them in detail.

A few key points about variables:

1. Initialization vs. assignment. Giving a variable a value when you define it is initialization (int age = 18;); changing its value afterward is assignment (age = 19;). You can define several variables of the same type at once: int a = 1, b = 2, c;.

2. Always initialize. A variable defined inside a function without an initializer holds an indeterminate value: the locker still contains whatever its previous user left behind, often called a "garbage value." Using an uninitialized variable is a common and hard-to-spot mistake:

c
int sum;          // not initialized: the value of sum is indeterminate
sum = sum + 10;   // the result is unpredictable!

3. Naming rules. Variable names (along with function names and the other names you'll meet later, collectively called identifiers) must follow these rules:

  • They may contain only letters, digits, and underscores _;
  • They can't start with a digit;
  • They're case-sensitive: score and Score are two different variables;
  • They can't be C keywords, such as int, return, or if (Appendix E has the complete list).
NameValid?Notes
scoreYes
total_scoreYesUnderscores separate the words; recommended
score2YesDigits are fine anywhere except at the start
2scoreNoStarts with a digit
total-scoreNoContains a minus sign
my scoreNoContains a space
returnNoIt's a keyword

4. A good name beats a comment. Names should say what they mean: student_count is much clearer than sc or a1. The C community traditionally uses lowercase words separated by underscores (such as max_value), and so does this guide.

Tip
In C89, variables had to be defined at the beginning of a block; since C99, you can define them wherever you need them. Define each variable where you first use it, and initialize it right away.

2.3 Basic data types

Different kinds of data need "lockers" of different sizes. Here are C's basic data types ("typical size" means on today's mainstream 64-bit computers):

TypeTypical sizeTypical rangeUsed forprintf format
char1 byte-128 to 127Characters, very small integers%c / %d
short2 bytes-32768 to 32767Smaller integers%hd
int4 bytesAbout ±2.1 billionThe most common integer type%d
long4 or 8 bytesDepends on the platformLarger integers%ld
long long8 bytesAbout ±9.2×10¹⁸Very large integers%lld
float4 bytesAbout 6–7 significant digitsSingle-precision floating point%f
double8 bytesAbout 15–16 significant digitsDouble-precision floating point; the usual choice%f
long double8–16 bytesDepends on the platformEven more precise floating point%Lf

The C standard doesn't fix the exact size of each type; it only specifies minimum ranges and an ordering (short ≤ int ≤ long ≤ long long). For example, long is 4 bytes on 64-bit Windows but 8 bytes on 64-bit Linux and macOS. The sizeof operator tells you the actual size of a type, in bytes, on your computer:

c
#include <stdio.h>

int main(void)
{
    printf("sizeof(char)        = %zu\n", sizeof(char));
    printf("sizeof(short)       = %zu\n", sizeof(short));
    printf("sizeof(int)         = %zu\n", sizeof(int));
    printf("sizeof(long)        = %zu\n", sizeof(long));
    printf("sizeof(long long)   = %zu\n", sizeof(long long));
    printf("sizeof(float)       = %zu\n", sizeof(float));
    printf("sizeof(double)      = %zu\n", sizeof(double));
    printf("sizeof(long double) = %zu\n", sizeof(long double));
    return 0;
}

Output on 64-bit Linux:

text
sizeof(char)        = 1
sizeof(short)       = 2
sizeof(int)         = 4
sizeof(long)        = 8
sizeof(long long)   = 8
sizeof(float)       = 4
sizeof(double)      = 8
sizeof(long double) = 16

The result of sizeof has type size_t (an unsigned integer type), and its format specifier is %zu.

Signed and unsigned. Integer types are signed by default, which means they can hold negative numbers. Put unsigned in front of a type and it becomes unsigned: it can hold only non-negative values, but its positive range doubles. For example, unsigned int ranges from 0 to 4294967295, and its format specifier is %u.

Caution
Whether plain char is signed or unsigned is up to the compiler and platform (it's usually signed on x86 and unsigned on some ARM platforms). If you use char to store small integers, write signed char or unsigned char explicitly.

The headers <limits.h> and <float.h> define the ranges of the various types:

c
#include <stdio.h>
#include <limits.h>
#include <float.h>

int main(void)
{
    printf("int range: %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned int max: %u\n", UINT_MAX);
    printf("long long range: %lld to %lld\n", LLONG_MIN, LLONG_MAX);
    printf("float significant digits: %d\n", FLT_DIG);
    printf("double significant digits: %d\n", DBL_DIG);
    return 0;
}

Output:

text
int range: -2147483648 to 2147483647
unsigned int max: 4294967295
long long range: -9223372036854775808 to 9223372036854775807
float significant digits: 6
double significant digits: 15

Overflow. When a result falls outside the range a type can represent, it overflows. Unsigned integers "wrap around" when they overflow: UINT_MAX + 1 is 0, just like a car's odometer rolling over from 999999 to 000000. But signed integer overflow is undefined behavior: the C standard doesn't say what happens. Usually you get a strange negative number, but even stranger things can occur. So when choosing a type, estimate the range of your data: 20 factorial (about 2.4×10¹⁸), for example, doesn't fit in an int; you need long long.

Tip
Undefined behavior (UB) is a crucial idea in C: it means the C standard places no requirements at all on the result of an operation. The program might crash, might print nonsense, or might "seem to work perfectly"—and that last case is the most dangerous. We'll run into various kinds of undefined behavior as we go, and Chapter 17 gathers them in one place.

2.4 How integers look in memory: two's complement (Optional)

How does a computer represent negative numbers? The most obvious idea is to use the highest bit as a sign (0 for positive, 1 for negative); this is called sign-magnitude. With 8 bits:

text
+5 in sign-magnitude: 0000 0101
-5 in sign-magnitude: 1000 0101

But sign-magnitude has two problems: zero has two representations (0000 0000 and 1000 0000), and the circuits for addition and subtraction get complicated. So modern computers almost universally use two's complement:

  • A positive number is represented as itself;
  • A negative number is represented by taking the bits of the corresponding positive number, inverting every bit, and adding 1.
text
+5:            0000 0101
invert:        1111 1010
add 1:         1111 1011    <- this is -5 in two's complement

Let's check: 0000 0101 (5) + 1111 1011 (-5) = 1 0000 0000. The carry out of the top bit doesn't fit in 8 bits and is discarded, leaving exactly 0000 0000. Two's complement lets subtraction be done with the same circuit as addition, and zero has only one representation.

A fun consequence: -1 in two's complement is all ones (1111 1111). Read those same 8 bits as an unsigned number and you get 255. That's why assigning -1 to an unsigned int variable gives 4294967295 (32 ones).

Tip
The C23 standard requires signed integers to use two's complement. In practice, virtually every computer has done so for decades.

2.5 Floating-point numbers

Numbers with a decimal point are called floating-point numbers; C's two common floating-point types are float and double. There are two ways to write them:

c
double a = 3.14;       // ordinary notation
double b = 1.5e3;      // scientific notation: 1.5 * 10^3 = 1500.0
double c = 2.5E-3;     // 2.5 * 10^-3 = 0.0025
float  d = 3.14f;      // the f suffix makes it a float

A decimal number written directly in the code (such as 3.14) has type double by default.

Floating-point numbers are approximations. Just as decimal notation can't represent 1/3 exactly (0.3333...), binary can't represent numbers like 0.1 exactly; it can only store a very close approximation. Look at this:

c
#include <stdio.h>
#include <math.h>

int main(void)
{
    double a = 0.1 + 0.2;
    printf("0.1 + 0.2 = %.17f\n", a);
    printf("a == 0.3 gives %d\n", a == 0.3);
    printf("close enough? %d\n", fabs(a - 0.3) < 1e-9);

    float f = 1.0f / 3;
    double d = 1.0 / 3;
    printf("float  1/3: %.20f\n", f);
    printf("double 1/3: %.20f\n", d);
    return 0;
}

Output:

text
0.1 + 0.2 = 0.30000000000000004
a == 0.3 gives 0
close enough? 1
float  1/3: 0.33333334326744079590
double 1/3: 0.33333333333333331483

== tests whether two values are equal, giving 1 if they are and 0 if they aren't (Chapter 4 covers it in detail). fabs is the math function for absolute value, declared in <math.h>. The results teach three lessons:

  1. Don't compare two floating-point numbers directly with `==`. Instead, check whether the absolute value of their difference is smaller than some tiny number (such as 1e-9).
  2. Prefer `double`. float has only 6–7 significant digits, so its errors pile up quickly; double is much more precise and, on modern computers, just about as fast.
  3. Don't use floating point for money. Store amounts as integers, in cents.
Tip
On Linux, when you compile with GCC a program that uses functions from <math.h>, you may need to add the -lm option at the end of the command to link the math library, as in gcc demo.c -o demo -lm.

2.6 Characters and ASCII

The char type stores a single character, written in single quotes: 'A', '7', '#'.

Computers can store only numbers, so how are characters stored? The answer is to give each character a number. The most basic numbering scheme is ASCII, which assigns each of 128 common characters (English letters, digits, punctuation, and control characters) a number from 0 to 127. What a `char` variable actually stores is that number, so char is really just a very small integer (Appendix B lists the common ASCII codes).

A few codes worth memorizing: '0' is 48, 'A' is 65, and 'a' is 97. Digits, uppercase letters, and lowercase letters are each numbered consecutively.

c
#include <stdio.h>

int main(void)
{
    char c = 'A';
    printf("The ASCII code of %c is %d\n", c, c);
    printf("ASCII code 97 is the character %c\n", 97);
    printf("'A' + 1 gives %c\n", c + 1);
    printf("Lowercase: %c\n", c + 32);      // 'a' - 'A' is 32

    char digit = '7';
    printf("The numeric value of '7' is %d\n", digit - '0');
    return 0;
}

Output:

text
The ASCII code of A is 65
ASCII code 97 is the character a
'A' + 1 gives B
Lowercase: a
The numeric value of '7' is 7

The same value prints as a character with %c and as a number with %d. digit - '0' is a very common trick: the codes of the digit characters '0' to '9' are consecutive, so subtracting '0' gives the corresponding numeric value.

Common mistake
'A' and "A" are completely different. Single quotes make a character, which takes 1 byte when stored in a char variable; double quotes make a string, which also hides an end marker at its end, so it takes 2 bytes (Chapter 9 explains why). (A fine point: in C, a character constant such as 'A' actually has type int, so sizeof('A') equals sizeof(int), which is 4 on common platforms, not 1. This is one of the differences between C and C++.) Also, characters outside ASCII, such as é or 你, take more than one byte in UTF-8, so they don't fit in a single char variable.

2.7 The Boolean type

A Boolean type has just two values: true and false. Early C had no Boolean type; instead, the convention was that 0 means false and any nonzero value means true. That convention still holds today.

C99 introduced the <stdbool.h> header, which gives you bool, true, and false:

c
#include <stdio.h>
#include <stdbool.h>

int main(void)
{
    bool is_passed = true;
    bool is_excellent = false;
    printf("%d %d\n", is_passed, is_excellent);   // prints: 1 0
    return 0;
}

In C23, bool, true, and false became keywords, so they work even without the header. Including it does no harm, so for compatibility this guide always includes <stdbool.h>.

2.8 Constants

A value that doesn't change while the program runs is a constant.

1. Literal constants: values written directly in the code.

Written asMeaning
42A decimal integer
052An octal integer (it starts with 0), equal to decimal 42
0x2AA hexadecimal integer, equal to decimal 42
0b101010A binary integer (standard in C23; GCC and Clang have supported it for a long time)
42u, 42L, 42LLOf type unsigned int, long, and long long, respectively
3.14, 3.14f, 1.5e3Floating-point numbers (double by default, float with the f suffix)
'A'A character
"Hello"A string
Common mistake
Never pad an integer with leading zeros to "line it up"! In int x = 010;, x is 8, not 10, because an integer that starts with 0 is octal.

2. `const` constants: put const in front of a variable definition, and its value can no longer be changed.

3. Macro constants: defined with #define. During preprocessing, every occurrence of the macro name in the code is replaced by its value. Note the syntax: there's no equals sign and no semicolon at the end.

c
#include <stdio.h>

#define PI 3.14159265358979     // a macro constant

int main(void)
{
    const int DAYS_PER_WEEK = 7;   // a const constant
    double r = 2.0;

    printf("Area of a circle with radius %.1f: %.2f\n", r, PI * r * r);
    printf("A year has about %d weeks\n", 365 / DAYS_PER_WEEK);
    // DAYS_PER_WEEK = 8;          // compile error: a const variable can't be modified
    return 0;
}

Output:

text
Area of a circle with radius 2.0: 12.57
A year has about 52 weeks

Using named constants instead of bare numbers has two benefits: readability, since DAYS_PER_WEEK means far more than 7, and easy maintenance, since a change only has to be made in one place. A number that appears out of nowhere with no explanation is jokingly called a magic number; avoid them. By convention, constant names are written in all capitals.

2.9 Type conversion

When data of different types is mixed in an operation or an assignment, type conversion takes place.

1. Conversion on assignment: the value is converted to the type of the variable on the left. When you assign a floating-point value to an integer variable, the fractional part is simply discarded (not rounded).

2. Automatic conversion in expressions:

  • Types smaller than int, such as char and short, are converted to int before they take part in arithmetic (this is called integer promotion; strictly speaking, if int can't represent every value of the original type, the value is promoted to unsigned int instead, but on common platforms they all become int);
  • When the two operands of an operation have different types, the "smaller" type is converted to the "larger" one. For example, in an operation between an int and a double, the int is converted to double first.

3. Explicit conversion (a cast): write (type) to convert a value by hand, as in (double)sum.

c
#include <stdio.h>

int main(void)
{
    int n = 3.99;                  // the fractional part is discarded, so n is 3
    double avg1 = 7 / 2;           // 7 / 2 is integer division giving 3, which becomes 3.0
    double avg2 = 7.0 / 2;         // 7.0 is a double, so 2 becomes 2.0; the result is 3.5
    double avg3 = (double)7 / 2;   // an explicit cast; also 3.5

    printf("n = %d\n", n);
    printf("avg1 = %.1f, avg2 = %.1f, avg3 = %.1f\n", avg1, avg2, avg3);
    return 0;
}

Output:

text
n = 3
avg1 = 3.0, avg2 = 3.5, avg3 = 3.5

Some compilers (Clang, for example) warn about the first line, reminding you that the fractional part will be lost. avg1, meanwhile, is a classic trap: dividing one integer by another gives an integer, so the fractional part is already gone by the time the division finishes, and converting the result to double afterward can't bring it back. When computing an average, make sure at least one operand is a floating-point number.

The trap of mixing signed and unsigned:

c
#include <stdio.h>

int main(void)
{
    int a = -1;
    unsigned int b = 1;
    printf("a < b gives %d\n", a < b);   // prints 0, which means "false"!
    return 0;
}

-1 is less than 1, yet the result is "false"? The reason is that when an int is compared with an unsigned int, a is converted to unsigned. -1 in two's complement is 32 ones, which as an unsigned number is 4294967295—certainly not less than 1. With -Wextra, GCC warns about this: comparison of integer expressions of different signedness. Avoid mixing signed and unsigned numbers in the same expression; this is one more reason to take compiler warnings seriously.

Exercises

  1. Use sizeof to print the sizes of char, short, int, long, long long, float, double, and long double on your computer, and compare them with the table in this chapter.
  2. Decide whether each of these identifiers is valid, and explain why the invalid ones are invalid: _count, 2nd_place, my-name, int, Int, total_sum, a b, score3.
  3. Given char c = 'm';, and without looking at an ASCII table, write expressions for: the uppercase version of c; the position of c in the alphabet (with a as 1); and the letter 3 places after c. Print them with printf to check.
  4. Without running it, write down what the following code prints, then run it to check:
c
   int a = 7 / 2;
   double b = 7 / 2;
   double c = 7.0 / 2;
   int d = (int)3.99;
   unsigned int e = -1;
   printf("%d %.1f %.1f %d %u\n", a, b, c, d, e);

Chapter 3: Input and Output

Programs need to communicate with their users: they read data from the keyboard (input) and show results on the screen (output). C's most commonly used input and output functions are scanf and printf, both declared in <stdio.h>.

3.1 printf: formatted output

The basic form of printf is:

c
printf("format string", arg1, arg2, ...);

Ordinary text in the format string is printed as is, while each format specifier, which starts with %, is replaced in turn by one of the arguments that follow.

Common format specifiers:

SpecifierMeaningExampleOutput
%dDecimal integer (int)printf("%d", 42)42
%uUnsigned decimal integerprintf("%u", 42u)42
%ld / %lldlong / long long integerprintf("%lld", 10LL)10
%fDecimal notation, 6 digits after the point by defaultprintf("%f", 3.14)3.140000
%eScientific notationprintf("%e", 1500.0)1.500000e+03
%g6 significant digits by default; uses %f or %e style depending on the magnitude of the number, and drops trailing zerosprintf("%g", 3.14)3.14
%cA single characterprintf("%c", 'A')A
%sA stringprintf("%s", "Hi")Hi
%x / %XHexadecimal (lowercase / uppercase letters)printf("%x", 255)ff
%oOctalprintf("%o", 8)10
%pA pointer (an address)See Chapter 10
%zusize_t (such as the result of sizeof)printf("%zu", sizeof(int))4
%%A literal percent signprintf("100%%")100%

Controlling width, precision, and alignment. You can add modifiers between the % and the letter:

c
#include <stdio.h>

int main(void)
{
    int n = 42;
    double pi = 3.14159265;

    printf("[%d]\n", n);            // as is
    printf("[%5d]\n", n);           // width 5, right-aligned by default
    printf("[%-5d]\n", n);          // width 5, left-aligned
    printf("[%05d]\n", n);          // width 5, padded with zeros on the left
    printf("[%+d]\n", n);           // show the plus sign
    printf("[%f]\n", pi);           // 6 decimal places by default
    printf("[%.2f]\n", pi);         // 2 decimal places
    printf("[%8.3f]\n", pi);        // total width 8, 3 decimal places
    printf("[%e]\n", 123456.789);   // scientific notation
    printf("[%x] [%X] [%o]\n", 255, 255, 8);
    printf("[%.3s]\n", "Hello");    // print at most 3 characters of the string
    printf("[100%%]\n");            // print % itself
    return 0;
}

Output:

text
[42]
[   42]
[42   ]
[00042]
[+42]
[3.141593]
[3.14]
[   3.142]
[1.234568e+05]
[ff] [FF] [10]
[Hel]
[100%]

With widths you can print neat tables:

c
#include <stdio.h>

int main(void)
{
    printf("%-10s%6s%10s\n", "Item", "Qty", "Price");
    printf("%-10s%6d%10.2f\n", "Apple", 3, 4.5);
    printf("%-10s%6d%10.2f\n", "Banana", 12, 0.75);
    printf("%-10s%6d%10.2f\n", "Cherry", 100, 25.8);
    return 0;
}

Output:

text
Item         Qty     Price
Apple          3      4.50
Banana        12      0.75
Cherry       100     25.80
Caution
Widths are counted in bytes, not in the columns a character occupies on screen. In UTF-8, a letter like é takes 2 bytes and a Chinese character takes 3, yet each occupies only one or two columns, so text containing non-ASCII characters won't line up when you rely on widths.
Tip
%.2f rounds the value, but because floating-point numbers are approximations, the result occasionally doesn't "look" rounded. For example, 2.675 is actually stored as 2.67499999..., so printf("%.2f", 2.675) prints 2.67. Also, for a value exactly halfway between, the C library used by GCC and others rounds to the even digit ("round half to even"): printf("%.2f", 0.125) prints 0.12.

Escape sequences. Some characters can't be typed directly into a string (a newline, for instance), so they're written as escape sequences that start with a backslash \:

Escape sequenceMeaning
\nNewline
\tTab; handy for simple alignment
\\A backslash \
\"A double quote
\'A single quote
\0The null character (ASCII code 0), which marks the end of a string
\rCarriage return (moves the cursor back to the start of the line)
\aAlert (some terminals beep)

For example, to print He said: "C is fun!", write printf("He said: \"C is fun!\"\n");.

Common mistake
Format specifiers must match the types of their arguments. printf("%d", 3.14) prints a double with %d and gives a bizarre number (this is undefined behavior); printf("%f", 10) is just as wrong. With -Wall, the compiler warns about such mismatches.

3.2 scanf: formatted input

scanf reads data from the keyboard, and it looks a lot like printf:

c
#include <stdio.h>

int main(void)
{
    int a, b;
    printf("Enter two integers: ");
    scanf("%d %d", &a, &b);
    printf("%d + %d = %d\n", a, b, a + b);
    return 0;
}

Output:

text
Enter two integers: 3 5
3 + 5 = 8

Why the `&`? & is the address-of operator: &a means "the address of variable a." scanf needs to know which "locker" to put the data it reads into, so you give it the locker's number—its address. Chapter 10 explains the principle behind this when we cover pointers; for now, just remember: when you read into an ordinary variable with `scanf`, put `&` in front of the variable name. Forgetting the & is one of the most common beginner mistakes, and it usually makes the program crash.

`scanf` format specifiers are mostly the same as printf's, with one crucial difference: reading a double requires %lf:

Variable typescanf formatprintf format
int%d%d
long long%lld%lld
float%f%f
double`%lf`%f (%lf also works)
char%c%c
String (a character array)%s (no &; Chapter 9 explains why)%s

Separating the values. When reading numbers with %d, %lf, and so on, scanf automatically skips any spaces, tabs, and newlines in front of them, so typing 3 5, or 3, Enter, 5, has the same effect.

Ordinary characters in the format string must be typed exactly. If you write scanf("%d,%d", &a, &b);, the user must type 3,5 (with a comma in between); typing 3 5 makes reading b fail. So put only format specifiers in a `scanf` format string, with no extra text—and especially no \n: scanf("%d\n", &a) makes the program keep "hanging" even after you press Enter.

The return value of `scanf`. scanf returns the number of items it successfully read and stored. If the input doesn't match the format, it returns a smaller number (possibly 0); if the input ends (or a read error occurs) before a single item has been read, it returns EOF (a negative constant, -1 in common implementations; Section 3.4 shows how to signal the end of input from the keyboard). Note that if some items have already been read when the input ends, scanf returns the number read so far, not EOF: if scanf("%d %d", &a, &b) reads 42 and then the input ends, it returns 1.

c
#include <stdio.h>

int main(void)
{
    int n = 0;
    printf("Enter an integer: ");
    int count = scanf("%d", &n);
    printf("scanf read %d item(s), n = %d\n", count, n);
    return 0;
}

Output for the inputs 25 and abc:

text
Enter an integer: 25
scanf read 1 item(s), n = 25
text
Enter an integer: abc
scanf read 0 item(s), n = 0

Once we've learned the if statement in Chapter 5, we'll use this return value to check whether the user's input is valid.

Caution (Visual Studio users, please read)
In Visual Studio, scanf triggers compile error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. Microsoft recommends its own scanf_s function, but not every compiler supports scanf_s. The fix: put #define _CRT_SECURE_NO_WARNINGS on the first line of the source file (before all the #includes), or set "SDL checks" to "No" under Project Properties → C/C++ → General. The code in the rest of this guide doesn't repeat this line; Visual Studio users should add it themselves.

3.3 The input buffer: understanding scanf's quirks

The characters you type aren't handed straight to the program. They're first stored in an input buffer, and the program starts reading from it only when you press Enter. The Enter key itself also goes into the buffer, as a newline character `'\n'`.

This leads to a classic problem:

c
#include <stdio.h>

int main(void)
{
    int age;
    char grade;

    printf("Enter your age: ");
    scanf("%d", &age);
    printf("Enter your grade: ");
    scanf("%c", &grade);
    printf("age = %d, grade = [%c]\n", age, grade);
    return 0;
}

Output:

text
Enter your age: 18
Enter your grade: age = 18, grade = [
]

The program didn't wait for us to enter a grade at all! Here's what happened: after you type 18 and press Enter, the buffer contains 18\n. The first scanf("%d") takes the 18 and leaves the \n in the buffer; the second scanf("%c") sees a character waiting in the buffer and reads the \n immediately.

There are two ways to fix this:

Fix 1: put a space in front of %c, as in scanf(" %c", &grade);. A space in a format string means "skip all whitespace (spaces, tabs, newlines)."

Fix 2: after reading, clear whatever is left in the buffer:

c
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
    // empty loop body: just read and discard the rest of the line, one character at a time
}

This uses a while loop, which you'll learn in Chapter 6; for now, treat it as a ready-made "tool."

Common mistake
Some online resources use fflush(stdin) to clear the input buffer. The C standard defines what fflush does only for output streams; using it on an input stream is undefined behavior that just "happens to work" with some compilers. Don't use it.

3.4 Character I/O: getchar and putchar

getchar() reads one character from the input buffer and returns it, and putchar(c) prints one character. They're simpler and more efficient than scanf and printf:

c
#include <stdio.h>

int main(void)
{
    printf("Enter a lowercase letter: ");
    int ch = getchar();
    printf("Its ASCII code is %d, and its uppercase form is ", ch);
    putchar(ch - 32);
    putchar('\n');
    return 0;
}

Output:

text
Enter a lowercase letter: b
Its ASCII code is 98, and its uppercase form is B

Notice that getchar() returns an int, not a char. Besides ordinary characters, it can also return EOF (a negative value, usually -1) to mean "end of input," so it needs a type that can hold every character plus EOF. You can type the end-of-input signal on the keyboard: on Windows, press Ctrl + Z and then Enter; on Linux and macOS, press Ctrl + D.

3.5 Putting it together: a BMI calculator

BMI (body mass index) = weight (kg) ÷ height (m) squared.

c
#include <stdio.h>

int main(void)
{
    double weight, height;

    printf("Enter your weight (kg): ");
    scanf("%lf", &weight);
    printf("Enter your height (m): ");
    scanf("%lf", &height);

    double bmi = weight / (height * height);
    printf("Your BMI is %.1f\n", bmi);
    return 0;
}

Output:

text
Enter your weight (kg): 65
Enter your height (m): 1.75
Your BMI is 21.2

After Chapter 5, we'll be able to take this further and have the program classify the BMI as "underweight," "normal," "overweight," and so on.

Exercises

  1. Circumference and area. Read a circle's radius and print its circumference and area, rounded to 2 decimal places.
  2. Sum and average. Read three integers and print their sum, plus their average rounded to 2 decimal places. (Hint: watch out for integer division.)
  3. Uppercase to lowercase. Read an uppercase letter and print the corresponding lowercase letter and its ASCII code.
  4. Bases and formats. Read an integer and print it in decimal, octal, and hexadecimal. Then print it three more times with a width of 10: right-aligned, left-aligned, and padded with zeros on the left. To make the width visible, wrap each one in square brackets, as in [ 255].

Chapter 4: Operators and Expressions

Once you have data, the next step is to compute with it. C provides a rich set of operators, and this chapter introduces them one by one.

4.1 Expressions and statements

A formula built from operators and operands is an expression, such as 3 + 5, a * b, or x = 10. Expressions usually have a value: 3 + 5 has the value 8, and the assignment expression x = 10 has the value 10. (The main exception is a call to a function whose return type is void, covered in Chapter 7: such an expression has no value.)

Put a semicolon after an expression and you get a statement: x = 10;.

4.2 Arithmetic operators

OperatorMeaningExampleResult
+Addition7 + 29
-Subtraction7 - 25
*Multiplication7 * 214
/Division7 / 23 (integer division)
%Remainder (modulo)7 % 21

Division and remainder need special care:

c
#include <stdio.h>

int main(void)
{
    printf("7 / 2 = %d\n", 7 / 2);
    printf("-7 / 2 = %d\n", -7 / 2);
    printf("7 %% 3 = %d\n", 7 % 3);
    printf("-7 %% 3 = %d\n", -7 % 3);
    printf("7 %% -3 = %d\n", 7 % -3);
    printf("7.0 / 2 = %.1f\n", 7.0 / 2);
    return 0;
}

Output:

text
7 / 2 = 3
-7 / 2 = -3
7 % 3 = 1
-7 % 3 = -1
7 % -3 = 1
7.0 / 2 = 3.5
  1. Integer division throws away the fractional part (it truncates toward zero), so 7 / 2 is 3 and -7 / 2 is -3. If at least one operand is a floating-point number, the division is done in floating point.
  2. `%` works only on integers, and the result has the same sign as the dividend (the left operand).
  3. Integer division by 0 is undefined behavior, and the program usually just crashes. Make sure the divisor isn't 0 before you divide.

Used together, / and % can do a lot of useful work:

ExpressionPurposeWith n = 1234
n % 10Get the last digit4
n / 10Remove the last digit123
n % 2 == 0Test whether n is even1 (true)
n % 7 == 0Test whether n is divisible by 70 (false)

4.3 Assignment operators

= is the assignment operator: it stores the value on its right into the variable on its left. It has nothing to do with "equals" in mathematics. x = x + 1 is nonsense in algebra but perfectly normal in C: "take the value of x, add 1, and store the result back in x."

The left side of an assignment must be a "location" that can hold data (called an lvalue), such as a variable. 5 = x; and a + b = 10; are both errors.

Assignment groups from right to left, so you can chain assignments: a = b = c = 0; is equivalent to a = (b = (c = 0));.

Compound assignment operators are shorthands:

Written asEquivalent to
x += 5x = x + 5
x -= 5x = x - 5
x *= 5x = x * 5
x /= 5x = x / 5
x %= 5x = x % 5

Let's use these operators in a "making change" program that pays an amount with as few bills as possible:

c
#include <stdio.h>

int main(void)
{
    int money;
    printf("Enter an amount in dollars: ");
    scanf("%d", &money);

    int n100 = money / 100;
    money %= 100;              // what's left after the $100 bills
    int n50 = money / 50;
    money %= 50;
    int n20 = money / 20;
    money %= 20;
    int n10 = money / 10;
    money %= 10;
    int n5 = money / 5;
    money %= 5;
    int n1 = money;

    printf("$100 x %d, $50 x %d, $20 x %d, $10 x %d, $5 x %d, $1 x %d\n",
           n100, n50, n20, n10, n5, n1);
    return 0;
}

Output:

text
Enter an amount in dollars: 388
$100 x 3, $50 x 1, $20 x 1, $10 x 1, $5 x 1, $1 x 3

4.4 Increment and decrement

++ adds 1 to a variable, and -- subtracts 1. Each has two forms, which differ in the value of the expression:

  • Prefix ++i: adds 1 to i first, and the expression's value is i after the increment;
  • Postfix i++: the expression's value is i before the increment, and then i is incremented.
c
#include <stdio.h>

int main(void)
{
    int i = 5;
    int a = i++;    // a gets 5, then i becomes 6
    printf("a = %d, i = %d\n", a, i);

    int b = ++i;    // i becomes 7 first, then b gets 7
    printf("b = %d, i = %d\n", b, i);

    int c = i--;    // c gets 7, then i becomes 6
    int d = --i;    // i becomes 5 first, then d gets 5
    printf("c = %d, d = %d, i = %d\n", c, d, i);
    return 0;
}

Output:

text
a = 5, i = 6
b = 7, i = 7
c = 7, d = 5, i = 5

When i++; or ++i; stands alone as a statement, the two do exactly the same thing: both add 1 to i.

Common mistake
Don't modify a variable and then modify or read it again elsewhere in the same expression, as in i = i++;, a = i++ + i++;, or printf("%d %d", i, i++);. There's no defined order among these reads and writes, and the C standard explicitly makes this undefined behavior, so different compilers give different results. Puzzles like "what does i++ + ++i equal?" that circulate online have no correct answer, so don't waste time on them.

4.5 Relational operators

Relational operators compare two values and give 1 (true) or 0 (false):

OperatorMeaningExample (a = 5, b = 3)Result
>Greater thana > b1
<Less thana < b0
>=Greater than or equal toa >= 51
<=Less than or equal tob <= 20
==Equal toa == 51
!=Not equal toa != b1
Common mistake
Test for equality with two equals signs, ==. if (x = 5) doesn't check whether x equals 5; it assigns 5 to x, and the whole expression has the value 5 (nonzero, which means "true"), so the condition is always true. With -Wall, the compiler warns: suggest parentheses around assignment used as truth value.
Common mistake
You can't copy the math notation 1 < x < 10 straight into C. It's read as (1 < x) < 10: 1 < x is computed first, giving 0 or 1, and both 0 and 1 are less than 10, so the whole expression is always true. The correct form is x > 1 && x < 10.

4.6 Logical operators

Logical operators combine conditions:

OperatorMeaningRule
&&Logical AND ("and")True only if both sides are true
||Logical OR ("or")True if at least one side is true
!Logical NOTTrue becomes false, and false becomes true

Truth table (1 means true, 0 means false):

aba && ba || b!a
00001
01011
10010
11110

Remember that in C, 0 is false and any nonzero value is true. So !5 is 0, and !0 is 1.

Short-circuit evaluation. && and || both evaluate from left to right, and they stop as soon as the result is known:

  • For A && B, if A is false, the result must be false, so B isn't evaluated;
  • For A || B, if A is true, the result must be true, so B isn't evaluated.
c
#include <stdio.h>

int main(void)
{
    int a = 0, b = 5;
    int r = (a != 0) && (b / a > 1);   // a != 0 is false, so the right side never runs: no division by 0
    printf("r = %d\n", r);

    int x = 1;
    int y = (x > 0) || (++x > 0);      // the left side is already true, so ++x never runs
    printf("y = %d, x = %d\n", y, x);
    return 0;
}

Output:

text
r = 0
y = 1, x = 1

Short-circuiting is very useful: put a "guard" condition on the left (such as "the divisor isn't 0"), and the operation on the right is guaranteed to be safe.

Example: leap years. The rule for leap years is "divisible by 4 but not by 100, or divisible by 400," and it translates directly into logical operators:

c
#include <stdio.h>

int main(void)
{
    int year;
    printf("Enter a year: ");
    scanf("%d", &year);

    int is_leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
    printf("Is %d a leap year? %d\n", year, is_leap);
    return 0;
}

Output:

text
Enter a year: 2024
Is 2024 a leap year? 1

Try 1900 (not a leap year) and 2000 (a leap year) too.

4.7 The conditional operator

The conditional operator ? : is the only C operator that takes three operands:

c
condition ? expression1 : expression2

If the condition is true, the value of the whole expression is expression 1; otherwise it's expression 2. It's good for simple either-or choices:

c
int max = (a > b) ? a : b;                        // the larger of a and b
printf("%d is %s\n", n, (n % 2 == 0) ? "even" : "odd");

Conditional operators can be nested, but nested ones soon become hard to read; at that point, switch to the if statement from Chapter 5.

4.8 The comma operator

The comma operator joins several expressions together. They're evaluated from left to right, and the value of the whole expression is the value of the last one:

c
int a, b;
a = (b = 3, b + 2);   // first b = 3 runs, then b + 2 is computed; a is 5

Its most common use is controlling two variables at once in a for loop (Chapter 6). Note that the comma in a definition (int a, b;) and the commas between function arguments are just separators, not comma operators.

4.9 Bitwise operators (a first look)

Bitwise operators work directly on the binary digits of integers. Here's a quick introduction; Chapter 16 explains what they're used for in depth. Taking a = 12 (binary 1100) and b = 10 (binary 1010):

OperatorNameRuleExampleResult
&Bitwise ANDA bit is 1 only if both bits are 1a & b1000, i.e., 8
|Bitwise ORA bit is 1 if either bit is 1a | b1110, i.e., 14
^Bitwise XORA bit is 1 if the two bits differ, 0 if they're the samea ^ b0110, i.e., 6
~Bitwise NOTEvery 0 becomes 1 and every 1 becomes 0~a-13 (for an int)
<<Left shiftShifts all bits left, filling with 0s on the righta << 2110000, i.e., 48
>>Right shiftShifts all bits righta >> 211, i.e., 3

Don't confuse & and | with the logical operators && and ||: 12 && 10 is 1 (both sides are "true"), while 12 & 10 is 8.

4.10 The sizeof operator

sizeof gives the number of bytes a type or variable occupies. It looks like a function, but it's actually an operator:

c
int n = 10;
printf("%zu %zu %zu\n", sizeof(int), sizeof(n), sizeof n);   // prints: 4 4 4

When you apply sizeof to a variable, the parentheses are optional; with a type, they're required. The result has type size_t. For ordinary types and variables it's determined at compile time; the exception is the variable-length array introduced in Section 8.10, whose size isn't known until run time, so sizeof applied to it is computed at run time too.

4.11 Precedence and associativity

When an expression contains several operators, precedence and associativity decide how the expression is grouped—that is, which operands each operator applies to. An operator with higher precedence binds more tightly to its operands: in a + b * c, * has higher precedence than +, so the expression means a + (b * c). When the precedence is the same, associativity decides the direction of grouping: in a - b - c, the two - operators have the same precedence and group from left to right, so it means (a - b) - c.

Here are the common operators from highest to lowest precedence (Appendix C has the complete table):

LevelOperatorsAssociativity
1 (highest)() [] -> . postfix ++ --Left to right
2! ~ prefix ++ -- unary + - * (dereference) & (address-of) sizeof castsRight to left
3* / %Left to right
4+ -Left to right
5<< >>Left to right
6< <= > >=Left to right
7== !=Left to right
8&Left to right
9^Left to right
10|Left to right
11&&Left to right
12||Left to right
13?:Right to left
14= += -= and the other assignment operatorsRight to left
15 (lowest),Left to right

The general order to remember is: unary > arithmetic > shifts > relational > bitwise > logical > conditional > assignment > comma.

Two classic traps:

  • x & 1 == 0 is meant to "test whether the lowest bit of x is 0," but == has higher precedence than &, so it's actually read as x & (1 == 0), that is, x & 0, which is always 0. Write (x & 1) == 0 instead.
  • a << 1 + 1 means a << 2, because + has higher precedence than <<.

Be careful here: precedence and associativity only decide the grouping; they don't decide the order in which the operands are evaluated. In f() + g() * h() (where f, g, and h are three functions), the result is always combined as f() + (g() * h()), but the C standard doesn't say which of the three functions is called first; f() may well be called last. Only &&, ||, ?:, and the comma operator guarantee that their left (first) operand is evaluated first. So don't write code that depends on the order of evaluation; this is also part of why code like a = i++ + i++ from Section 4.4 goes wrong.

Tip
Nobody remembers the precedence of every operator, professional programmers included. When in doubt, add parentheses. They make the expression group exactly the way you intend and make the code easier to read.

Exercises

  1. Split a three-digit number. Read a three-digit integer and print its hundreds, tens, and ones digits. For the input 365, print hundreds 3, tens 6, ones 5.
  2. Converting seconds. Read a number of seconds and print it in the form hours:minutes:seconds, with each part two digits wide and padded with a leading 0 if needed. For the input 3725, print 01:02:05.
  3. Predict the output. Without running it, write down what the following code prints, then run it to check and explain the results:
c
   int a = 5, b = 3;
   printf("%d %d %d %d\n", a / b, a % b, -a / b, -a % b);
   int x = 0, y = 0;
   int r = (x++ > 0) && (y++ > 0);
   printf("%d %d %d\n", r, x, y);
  1. Largest of three. Read three integers and find the largest one using only the conditional operator ?: (no if).

Chapter 5: Branching: Teaching Your Program to Choose

So far, our programs have run from top to bottom, one statement after another. But real problems often call for "it depends": different comments for passing and failing grades, different calculations for different operators. This chapter shows how to make a program choose between different paths of execution based on conditions.

5.1 The three basic structures

Computer scientists have proved that any algorithm can be built from just three basic structures:

  • Sequence: statements run one after another, in order;
  • Selection (branching): a condition decides which statements run—the subject of this chapter;
  • Repetition (looping): statements run repeatedly while a condition holds—the subject of Chapter 6.

5.2 The if statement

The form of the if statement is:

c
if (condition) {
    statements    // run when the condition is true
}

Here's the flow:

text
         Is the condition true?
            /             \
          yes              no
           |                |
    run the statements      |
            \              /
      continue with what comes next

For example, here's how to find the absolute value of an integer:

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter an integer: ");
    scanf("%d", &n);

    if (n < 0) {
        n = -n;
    }
    printf("Its absolute value is %d\n", n);
    return 0;
}

Output:

text
Enter an integer: -7
Its absolute value is 7

The condition can be any expression: a value of 0 means false, and anything nonzero means true. So if (n != 0) can be shortened to if (n), and if (n == 0) can be written if (!n).

About braces. If the block contains only one statement, the braces are optional as far as the syntax goes, but leaving them out is the source of many bugs:

c
if (score >= 60)
    printf("You passed.\n");
    printf("Congratulations!\n");    // this line is NOT part of the if: it runs whether or not you passed!

Indentation is only for human readers; the compiler doesn't care about it. Without braces, the if controls only the one statement that immediately follows it. With -Wall, GCC warns about this kind of "misleading indentation."

Tip
Always use braces, however many statements the block contains. Many companies' coding standards make this a hard rule.
Common mistake
Don't put a semicolon after if (...). In if (x > 0);, the semicolon is itself an "empty statement": the if controls that empty statement, and the code in the braces below runs unconditionally.

5.3 The if-else statement

if-else chooses between two alternatives:

c
if (condition) {
    statements1    // run when the condition is true
} else {
    statements2    // run when the condition is false
}

For example, to test whether a number is odd or even:

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter an integer: ");
    scanf("%d", &n);

    if (n % 2 == 0) {
        printf("%d is even\n", n);
    } else {
        printf("%d is odd\n", n);
    }
    return 0;
}

Output:

text
Enter an integer: 17
17 is odd

5.4 else if: choosing among many

When there are several possibilities, you can chain if-else statements together:

c
if (condition1) {
    statements1
} else if (condition2) {
    statements2
} else if (condition3) {
    statements3
} else {
    statements4    // run when none of the conditions above are true
}

The program tests the conditions from top to bottom, runs the first branch whose condition is true, and then skips all the remaining branches.

Example: converting a percentage score into a letter grade (90 and above is A, 80–89 is B, 70–79 is C, 60–69 is D, and below 60 is F). Along the way, we'll use the technique mentioned in Chapter 3—checking scanf's return value—to reject invalid input:

c
#include <stdio.h>

int main(void)
{
    int score;
    printf("Enter a score (0-100): ");
    if (scanf("%d", &score) != 1 || score < 0 || score > 100) {
        printf("Invalid input!\n");
        return 1;
    }

    char grade;
    if (score >= 90) {
        grade = 'A';
    } else if (score >= 80) {
        grade = 'B';
    } else if (score >= 70) {
        grade = 'C';
    } else if (score >= 60) {
        grade = 'D';
    } else {
        grade = 'F';
    }
    printf("Grade: %c\n", grade);
    return 0;
}

Output:

text
Enter a score (0-100): 85
Grade: B

Two things to notice:

  1. The order of the conditions matters. The second branch says only score >= 80, not score < 90, because reaching that branch means the first condition was false, so score must be below 90. But if score >= 60 came first, a 95 would be graded D.
  2. The input check relies on short-circuiting in ||: if scanf fails, the comparisons after it are never made. Returning a nonzero value from main signals that the program ended "abnormally."

5.5 Nested ifs and the "dangling else"

An if statement can contain another if statement; this is called nesting. When nesting, be careful about which if an else belongs to. Look at this:

c
if (a > 0)
    if (b > 0)
        printf("a and b are both positive\n");
else
    printf("a is not positive\n");

The indentation suggests that the else belongs to if (a > 0), but C's rule is that an `else` always pairs with the nearest preceding `if` that doesn't already have an `else`. So this else actually belongs to if (b > 0), and when a > 0 but b <= 0, the program wrongly prints "a is not positive."

The cure, once again, is to always use braces:

c
if (a > 0) {
    if (b > 0) {
        printf("a and b are both positive\n");
    }
} else {
    printf("a is not positive\n");
}

5.6 The switch statement

When you need to do different things depending on the value of an integer, switch is clearer than a long chain of else ifs:

c
switch (expression) {
case constant1:
    statements1
    break;
case constant2:
    statements2
    break;
default:
    statements3
    break;
}

The program evaluates the expression, jumps to the case whose value matches, and starts running from there; when it reaches a break, it leaves the switch. If no case matches, the default part runs.

Example: a simple calculator.

c
#include <stdio.h>

int main(void)
{
    double a, b;
    char op;
    printf("Enter an expression (e.g. 3 + 4): ");
    scanf("%lf %c %lf", &a, &op, &b);

    switch (op) {
    case '+':
        printf("%g %c %g = %g\n", a, op, b, a + b);
        break;
    case '-':
        printf("%g %c %g = %g\n", a, op, b, a - b);
        break;
    case '*':
        printf("%g %c %g = %g\n", a, op, b, a * b);
        break;
    case '/':
        if (b == 0) {
            printf("Error: division by zero\n");
        } else {
            printf("%g %c %g = %g\n", a, op, b, a / b);
        }
        break;
    default:
        printf("Unsupported operator: %c\n", op);
        break;
    }
    return 0;
}

Output:

text
Enter an expression (e.g. 3 + 4): 10 / 4
10 / 4 = 2.5

Rules for using switch:

  • The expression in parentheses after switch must have an integer type (char counts as an integer); it can't be a floating-point number or a string;
  • Each case must be followed by a constant: not a variable, and not a range (such as "90 to 100");
  • No two cases can have the same value;
  • default is optional, but it's a good idea to always include it to handle unexpected values.

Don't forget `break`. If a case doesn't end with break, execution carries on into the statements of the next case. This is called fall-through:

c
#include <stdio.h>

int main(void)
{
    int level = 2;
    switch (level) {
    case 1:
        printf("Level 1\n");
    case 2:
        printf("Level 2\n");
    case 3:
        printf("Level 3\n");
    default:
        printf("Other\n");
    }
    return 0;
}

Output:

text
Level 2
Level 3
Other

After entering at case 2, the program runs all the way to the bottom because there's no break. With -Wextra, GCC warns: this statement may fall through.

Fall-through can occasionally be put to clever use, letting several cases share the same code:

c
switch (month) {
case 3: case 4: case 5:
    printf("Spring\n");
    break;
case 6: case 7: case 8:
    printf("Summer\n");
    break;
case 9: case 10: case 11:
    printf("Fall\n");
    break;
case 12: case 1: case 2:
    printf("Winter\n");
    break;
default:
    printf("Invalid month\n");
    break;
}

`switch` or `if`? switch can only test "is it equal to this constant?", while if can handle any condition (ranges, combinations of several variables, and so on). Sometimes a little math lets switch handle ranges. For letter grades, for instance, a score / 10 of 10 or 9 means A, 8 means B, and so on:

c
switch (score / 10) {
case 10:
case 9:
    grade = 'A';
    break;
case 8:
    grade = 'B';
    break;
case 7:
    grade = 'C';
    break;
case 6:
    grade = 'D';
    break;
default:
    grade = 'F';
    break;
}
Tip
If you need to define a variable inside a case, wrap that case's statements in braces, as in case 1: { int t = 0; ... break; }. For one thing, standards before C23 don't allow a variable definition immediately after a case label (some compilers report an error); for another, without braces the variable's scope extends into the cases that follow, and when execution jumps there from another case, the variable hasn't been initialized.

5.7 Putting it together: solving a quadratic equation

Let's solve ax² + bx + c = 0. Depending on the sign of the discriminant Δ = b² − 4ac, the equation has two distinct real roots, one repeated real root, or two complex conjugate roots.

c
#include <stdio.h>
#include <math.h>

int main(void)
{
    double a, b, c;
    printf("Enter the coefficients a b c of ax^2 + bx + c = 0: ");
    if (scanf("%lf %lf %lf", &a, &b, &c) != 3) {
        printf("Invalid input!\n");
        return 1;
    }

    if (fabs(a) < 1e-12) {
        printf("a must not be 0, or this isn't a quadratic equation.\n");
        return 1;
    }

    double delta = b * b - 4 * a * c;
    if (delta > 1e-12) {
        double x1 = (-b + sqrt(delta)) / (2 * a);
        double x2 = (-b - sqrt(delta)) / (2 * a);
        printf("Two distinct real roots: x1 = %.2f, x2 = %.2f\n", x1, x2);
    } else if (delta >= -1e-12) {
        printf("One repeated real root: x1 = x2 = %.2f\n", -b / (2 * a));
    } else {
        double real = -b / (2 * a);
        double imag = fabs(sqrt(-delta) / (2 * a));
        printf("Two complex conjugate roots: x1 = %.2f + %.2fi, x2 = %.2f - %.2fi\n",
               real, imag, real, imag);
    }
    return 0;
}

Output from three runs:

text
Enter the coefficients a b c of ax^2 + bx + c = 0: 1 -3 2
Two distinct real roots: x1 = 2.00, x2 = 1.00
text
Enter the coefficients a b c of ax^2 + bx + c = 0: 1 2 1
One repeated real root: x1 = x2 = -1.00
text
Enter the coefficients a b c of ax^2 + bx + c = 0: 1 2 5
Two complex conjugate roots: x1 = -1.00 + 2.00i, x2 = -1.00 - 2.00i

This program brings together input checking, floating-point comparison (using a tiny number, 1e-12, instead of == 0), multi-way branching, and the math function sqrt (square root). When compiling with GCC on Linux, remember to add -lm.

Exercises

  1. Sort three numbers. Read three integers and print them from smallest to largest. (Hint: a few rounds of "compare, and swap if necessary" will do it. Swapping two variables takes a third one: t = a; a = b; b = t;)
  2. Days in a month. Read a year and a month, and print how many days that month has. Use switch, and handle February in leap years.
  3. Tiered electricity pricing. A utility charges 0.50 per kWh for the first 200 kWh used in a month, 0.55 per kWh for usage above 200 kWh up to 400 kWh, and 0.80 per kWh for everything above 400 kWh. Read a month's usage in kWh and print the bill with two decimal places. For example, 450 kWh costs 250.00.
  4. Character classes. Read a character and determine whether it's an uppercase letter, a lowercase letter, a digit, or some other character.

Chapter 6: Loops: Teaching Your Program to Repeat

What computers do best is repeat things tirelessly. Adding up 1 to 100, printing a multiplication table, searching ten thousand records for a target—all of these need loops. C has three loop statements: while, do-while, and for.

6.1 The while loop

c
while (condition) {
    loop body
}

How it runs: first the condition is tested. If it's true, the loop body runs once, and then control goes back to the top to test the condition again. This repeats until the condition is false, which ends the loop.

text
     ┌──▶ Is the condition true? ──no──▶ end the loop
     │              │
     │             yes
     │              ▼
     └───── run the loop body

Example: compute 1 + 2 + 3 + … + 100.

c
#include <stdio.h>

int main(void)
{
    int i = 1;            // (1) initialize the loop variable
    int sum = 0;
    while (i <= 100) {    // (2) the loop condition
        sum += i;
        i++;              // (3) update the loop variable
    }
    printf("1 + 2 + ... + 100 = %d\n", sum);
    return 0;
}

Output:

text
1 + 2 + ... + 100 = 5050

A correct loop usually has three parts: initialization, a condition, and an update. If you forget i++, i stays 1 forever, the condition is always true, and the program is stuck in an infinite loop. To stop a program that's stuck in an infinite loop, press Ctrl + C in the terminal.

while is especially good when you "don't know how many times to loop, only when to stop." For example, here's how to count the digits of an integer and add them up:

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter a positive integer: ");
    scanf("%d", &n);

    int digits = 0, sum = 0;
    int temp = n;           // work on a copy so n keeps its original value
    while (temp > 0) {
        sum += temp % 10;   // take the last digit
        temp /= 10;         // drop the last digit
        digits++;
    }
    printf("%d has %d digits, which add up to %d\n", n, digits, sum);
    return 0;
}

Output:

text
Enter a positive integer: 2024
2024 has 4 digits, which add up to 8

6.2 The do-while loop

c
do {
    loop body
} while (condition);    // note the semicolon at the end

do-while runs the loop body once before testing the condition, so the body always runs at least once.

The digit-counting program above has a small flaw: for the input 0, the while condition is false from the start, so the program claims that "0 has 0 digits." do-while fixes this, because every integer has at least 1 digit:

c
do {
    sum += temp % 10;
    temp /= 10;
    digits++;
} while (temp != 0);

The classic use of do-while is input validation: let the user type something first, and if it's invalid, ask again. The version below handles both "that's not a number" and "the number is out of range":

c
#include <stdio.h>

int main(void)
{
    int score = -1;
    int ok;
    do {
        printf("Enter a score (0-100): ");
        ok = scanf("%d", &score);
        if (ok == EOF) {
            return 1;                  // input has ended, so just quit
        }
        int ch;
        while ((ch = getchar()) != '\n' && ch != EOF) {
            // discard the rest of the line
        }
    } while (ok != 1 || score < 0 || score > 100);

    printf("You entered %d\n", score);
    return 0;
}

Output:

text
Enter a score (0-100): abc
Enter a score (0-100): 120
Enter a score (0-100): 95
You entered 95

Without clearing the buffer, typing abc would make scanf fail and leave abc sitting in the buffer; in the next round, scanf would read it again and fail again... and the program would loop forever.

6.3 The for loop

The for loop puts the three parts—initialization, condition, and update—together in one place, which makes it the most compact loop:

c
for (initialization; condition; update) {
    loop body
}

It runs in this order:

text
(1) initialize -> (2) test the condition -> (3) run the body -> (4) update -> (2) test -> (3) -> (4) -> ... until the condition is false

The initialization runs only once. Example: computing n factorial, n! = 1 × 2 × … × n. Factorials grow extremely fast—13! is already beyond the range of int—so we use long long:

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter n (0-20): ");
    scanf("%d", &n);

    long long result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    printf("%d! = %lld\n", n, result);
    return 0;
}

Output:

text
Enter n (0-20): 20
20! = 2432902008176640000

Defining a variable in the initialization part, as in for (int i = 2; ...), was introduced in C99. A variable defined this way exists only inside the loop and is gone as soon as the loop ends.

Any of the three parts of a for can be left out (but not the two semicolons). for (;;) is an infinite loop with no condition, equivalent to while (1).

for loops often use the comma operator to control two variables at once, for example to move inward from both ends:

c
for (int i = 0, j = 9; i < j; i++, j--) {
    printf("(%d, %d) ", i, j);   // prints (0, 9) (1, 8) (2, 7) (3, 6) (4, 5)
}

6.4 break and continue

  • `break`: immediately ends the whole loop and jumps to the statement after it;
  • `continue`: skips the rest of the current pass and moves straight on to the next one (in a for loop, the "update" part runs first).
c
#include <stdio.h>

int main(void)
{
    // continue: print the numbers from 1 to 20 that aren't multiples of 3
    for (int i = 1; i <= 20; i++) {
        if (i % 3 == 0) {
            continue;
        }
        printf("%d ", i);
    }
    printf("\n");

    // break: find the first multiple of 7 greater than 100
    int n = 101;
    while (1) {
        if (n % 7 == 0) {
            break;
        }
        n++;
    }
    printf("The first multiple of 7 greater than 100 is %d\n", n);
    return 0;
}

Output:

text
1 2 4 5 7 8 10 11 13 14 16 17 19 20
The first multiple of 7 greater than 100 is 105

while (1) plus break is a very common pattern: when the loop's exit condition can only be checked in the middle of the body, it's the most natural way to write the loop.

Caution
break exits only the loop (or `switch`) that directly contains it. In nested loops, a break in the inner loop doesn't end the outer loop.

6.5 Nested loops

A loop body can contain another loop; this is called a nested loop. Each time the outer loop runs once, the inner loop runs all the way through.

Example 1: a multiplication table. The outer loop handles the rows (row i), and the inner loop prints the products in each row (row i has i of them):

c
#include <stdio.h>

int main(void)
{
    for (int i = 1; i <= 9; i++) {
        for (int j = 1; j <= i; j++) {
            printf("%d*%d=%-3d", j, i, i * j);
        }
        printf("\n");
    }
    return 0;
}

Output:

text
1*1=1
1*2=2  2*2=4
1*3=3  2*3=6  3*3=9
1*4=4  2*4=8  3*4=12 4*4=16
1*5=5  2*5=10 3*5=15 4*5=20 5*5=25
1*6=6  2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7  2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8  2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9  2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81

%-3d prints each product left-aligned in a field 3 characters wide, which keeps the columns lined up.

Example 2: printing a pyramid. The general method for pattern-printing problems is to first work out how the number of spaces and characters in row i depends on i. For a pyramid with 5 rows:

Row iSpacesStars
141
233
325
417
509

The pattern is: spaces = n − i, and stars = 2i − 1.

c
#include <stdio.h>

int main(void)
{
    int n = 5;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n - i; j++) {
            printf(" ");
        }
        for (int j = 1; j <= 2 * i - 1; j++) {
            printf("*");
        }
        printf("\n");
    }
    return 0;
}

Output:

text
    *
   ***
  *****
 *******
*********

6.6 Classic algorithms

Example 1: testing for primes. A prime is an integer greater than 1 whose only divisors are 1 and itself. To test whether n is prime, you only need to check for divisors between 2 and √n: if n = a × b, then one of a and b must be no larger than √n.

c
#include <stdio.h>
#include <stdbool.h>

int main(void)
{
    int n;
    printf("Enter a positive integer: ");
    scanf("%d", &n);

    bool is_prime = (n >= 2);          // numbers below 2 aren't prime
    for (int i = 2; i * i <= n; i++) {  // i * i <= n instead of i <= sqrt(n) avoids floating point
        if (n % i == 0) {
            is_prime = false;
            break;                     // found a divisor, so there's no need to keep checking
        }
    }
    printf("%d %s prime\n", n, is_prime ? "is" : "is not");
    return 0;
}

Output:

text
Enter a positive integer: 97
97 is prime

Example 2: finding the greatest common divisor with the Euclidean algorithm. This is one of the oldest known algorithms: the greatest common divisor (GCD) of two numbers equals the GCD of the smaller number and the remainder left when one is divided by the other. Keep replacing the numbers with the remainder until the remainder becomes 0.

For 48 and 18:

xyx % y
481812
18126
1260
60Stop: the answer is 6
c
#include <stdio.h>

int main(void)
{
    int a, b;
    printf("Enter two positive integers: ");
    scanf("%d %d", &a, &b);

    int x = a, y = b;
    while (y != 0) {
        int r = x % y;
        x = y;
        y = r;
    }
    printf("The GCD of %d and %d is %d\n", a, b, x);
    return 0;
}

Output:

text
Enter two positive integers: 48 18
The GCD of 48 and 18 is 6

Example 3: a number-guessing game. The program secretly picks a number from 1 to 100 and the player guesses it; the program answers "too high" or "too low" until the guess is right.

c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    srand((unsigned int)time(NULL));   // seed the random number generator with the current time
    int target = rand() % 100 + 1;     // a random integer from 1 to 100
    int guess;
    int count = 0;

    printf("I'm thinking of a number from 1 to 100. Can you guess it?\n");
    do {
        printf("Your guess: ");
        if (scanf("%d", &guess) != 1) {
            printf("Invalid input. Game over.\n");
            return 1;
        }
        count++;
        if (guess > target) {
            printf("Too high!\n");
        } else if (guess < target) {
            printf("Too low!\n");
        }
    } while (guess != target);

    printf("You got it! The number was %d, and it took you %d guesses.\n", target, count);
    return 0;
}

A possible run:

text
I'm thinking of a number from 1 to 100. Can you guess it?
Your guess: 50
Too high!
Your guess: 25
Too low!
Your guess: 37
Too low!
Your guess: 43
You got it! The number was 43, and it took you 4 guesses.

About random numbers:

  • rand() (in <stdlib.h>) returns a "pseudo-random" number between 0 and RAND_MAX. It's computed with a formula, so as long as the starting point (called the seed) is the same, the sequence it produces is exactly the same.
  • srand(seed) sets the seed. Using the current time, time(NULL) (in <time.h>), as the seed usually gives each run of the program a different seed, and therefore different results (time typically counts whole seconds, so two runs started within the same second get exactly the same sequence). Call `srand` only once in the whole program; never put it inside a loop.
  • To get a random integer in the range [min, max], use the formula rand() % (max - min + 1) + min.
Tip
If you always guess the middle of the remaining range (a strategy called binary search), you're guaranteed to find any number from 1 to 100 in at most 7 guesses, because 2⁷ = 128 > 100. The binary search algorithm in Chapter 8 is based on exactly this idea.

6.7 The goto statement (Good to know)

goto makes the program jump to a position marked with a label in the same function. It makes a program's flow tangled and hard to follow, so it's generally discouraged. One of its few "legitimate" uses is breaking out of several nested loops at once:

c
#include <stdio.h>

int main(void)
{
    for (int i = 1; i < 10; i++) {
        for (int j = 1; j < 10; j++) {
            if (i * j == 42) {
                printf("Found it: %d * %d = 42\n", i, j);
                goto found;
            }
        }
    }
found:
    printf("Search finished\n");
    return 0;
}

Output:

text
Found it: 6 * 7 = 42
Search finished

Another use is releasing resources in one central place when an error occurs, which Chapter 17 covers.

6.8 Choosing a loop, and common mistakes

SituationRecommended loop
The number of passes is known (such as "repeat 10 times" or "go from 1 to n")for
The number of passes is unknown; you only know when to stopwhile
The body must run at least once (such as reading input and then checking it)do-while

Each of the three loops can be rewritten as either of the others; choose the one that best expresses your intent.

Common loop mistakes:

  1. Off-by-one errors: i < n and i <= n differ by one pass. After writing a loop, walk through the smallest case (such as n = 1) in your head.
  2. A stray semicolon after the loop header: the body of for (i = 0; i < 10; i++); is an empty statement, and the code in the braces below runs just once.
  3. Changing a `for` loop's variable inside the body: this easily muddles the logic, so avoid it.
  4. Using floating-point numbers to count passes: for (double x = 0; x != 1.0; x += 0.1) never ends! Because of floating-point error, x never becomes exactly 1.0. Use integers to count loop passes.

Exercises

  1. Compute the sum of all the odd numbers from 1 to 100.
  2. Narcissistic numbers. A three-digit number is called narcissistic (or an Armstrong number) if the sum of the cubes of its digits equals the number itself; for example, 153 = 1³ + 5³ + 3³. Print all the narcissistic numbers.
  3. Print a diamond. Read n and print a diamond whose upper half has n rows. For n = 3:
text
     *
    ***
   *****
    ***
     *
  1. Reverse an integer. Read a positive integer and print its digits in reverse order, along with how many digits it has. For the input 12345, print Reversed: 54321 (5 digits).
  2. Compute 1! + 2! + 3! + … + 10!. (Hint: within a single loop, you can use the previous factorial to compute the current one.)

Part II: The Core

Goal of this part: master the four central topics of C—functions, arrays, strings, and pointers. Functions teach you to "divide and conquer," arrays and strings let you handle data in bulk, and pointers are the soul of C and what sets it apart from most high-level languages. This part is the heart of the guide, so take your time and make sure you truly understand every example.

Chapter 7: Functions: The Building Blocks of Programs

7.1 Why functions?

Suppose you're writing a program that has to check whether a number is prime in three different places. Copying the prime-checking code three times isn't just long-winded: if you later find a bug in it, you have to fix it in three places, and missing one leaves a hidden problem behind.

A function packages a piece of code that does a specific job and gives it a name; whenever you need that job done, you "call" the function by name. Think of a function as a machine: you feed in raw materials (the arguments), the machine runs, and out comes a finished product (the return value). People who use the machine don't need to know how it's built inside, only what it does.

Using functions brings several benefits:

  • Reuse: write once, use everywhere;
  • Divide and conquer: split a big problem into small ones, each solved by its own function;
  • Readability and easy maintenance: if (is_prime(n)) makes sense at a glance, and a change only needs to be made in one place.

In fact, you've been using functions all along: main, printf, scanf, and sqrt are all functions.

7.2 Defining and calling functions

The general form of a function definition is:

c
return_type function_name(parameter_list)
{
    function body
}

Example: a function that returns the larger of two integers.

c
#include <stdio.h>

// Returns the larger of a and b
int max(int a, int b)
{
    if (a > b) {
        return a;
    }
    return b;
}

int main(void)
{
    int x = 3, y = 7;
    int m = max(x, y);                              // call the max function
    printf("The larger one is %d\n", m);
    printf("The larger one is %d\n", max(10, max(4, 12)));  // calls can be nested
    return 0;
}

Output:

text
The larger one is 7
The larger one is 12

Some terminology:

  • Parameters (formal parameters): the names in the function definition, such as a and b in max. They act as local variables inside the function;
  • Arguments (actual arguments): the values passed in when the function is called, such as x and y in max(x, y);
  • Return value: the value handed back to the caller with a return statement. Its type should match the function's return type; if it doesn't, it's converted to the return type automatically, just as in an assignment.

Here's what happens when max(x, y) is called:

  1. The arguments are evaluated (3 and 7) and their values are copied into the parameters a and b;
  2. main is paused, and execution jumps into the body of max;
  3. When return b; runs, the function ends and carries 7 back to where it was called;
  4. Back in main, the expression max(x, y) has the value 7, which is assigned to m.

Functions that return nothing. If a function just performs an action and has no result to hand back, its return type is void. The function body can have no return at all, or it can use a plain return; to end early:

c
void print_line(int length)
{
    for (int i = 0; i < length; i++) {
        putchar('-');
    }
    putchar('\n');
}

Functions with no parameters. Write void as the parameter list, as in int get_input(void).

Caution
Before C23, int f() and int f(void) meant different things: the former meant "the parameters aren't specified," so the compiler didn't check the arguments of a call at all, while only the latter explicitly meant "no parameters." For clarity and safety, write void when there are no parameters.

A few rules:

  • Once a return statement runs, the function ends immediately; the statements after it don't run;
  • A function can return only one value (to "return" several values, you'll use pointers, in Chapter 10);
  • In a function whose return type isn't void, every path through the function must reach a return; otherwise, the caller gets an indeterminate value;
  • C doesn't allow you to define a function inside another function.

7.3 Function declarations

A C compiler reads code from top to bottom. If a function is called before the compiler has "seen" it, you get an error (or a warning). The example above defines max before main, so there's no problem. If you'd rather put main first, you need to write a declaration of the function (also called a function prototype) beforehand, telling the compiler the function's name, parameter types, and return type:

c
#include <stdio.h>

double circle_area(double r);   // a function declaration, ending with a semicolon

int main(void)
{
    printf("Area: %.2f\n", circle_area(2.0));
    return 0;
}

double circle_area(double r)     // the function definition
{
    return 3.14159265358979 * r * r;
}

Output:

text
Area: 12.57

In a declaration you can leave out the parameter names and write double circle_area(double);, but keeping the names makes it easier to read. In fact, the job of #include <stdio.h> is exactly to bring in the declarations of printf, scanf, and friends—headers consist mostly of declarations. In Chapter 15, when we cover multi-file programs, you'll write headers of your own.

7.4 Passing arguments: pass by value

The function below tries to swap the values of two variables. Do you think it will work?

c
#include <stdio.h>

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
    printf("Inside swap: a = %d, b = %d\n", a, b);
}

int main(void)
{
    int x = 1, y = 2;
    swap(x, y);
    printf("After swap:  x = %d, y = %d\n", x, y);
    return 0;
}

Output:

text
Inside swap: a = 2, b = 1
After swap:  x = 1, y = 2

The swap failed. The reason is that C passes arguments by value: when a function is called, the values of the arguments are copied into the parameters. a and b are just "photocopies" of x and y, and changing the photocopies inside the function leaves the originals untouched.

text
 variables in main              variables in swap
+-------+-------+   copy    +-------+-------+
| x = 1 | y = 2 |  ─────▶   | a = 1 | b = 2 |   after swapping -> a = 2, b = 1
+-------+-------+           +-------+-------+
 the originals are untouched     the copies are changed

So how do you write a function that really swaps two variables? The answer is to give the function the addresses of the variables, which is exactly what pointers in Chapter 10 are for.

7.5 Scope: where a variable is valid

Scope is the region of a program in which a name can be used.

  • Local variables: variables defined inside a function or inside a block (a pair of braces) are valid only in the block where they're defined. A function's parameters are local variables too. Local variables with the same name in different functions have nothing to do with each other.
  • Global variables: variables defined outside all functions can be used anywhere from the point of definition to the end of the file.
c
#include <stdio.h>

int g_count = 0;          // a global variable

void add(void)
{
    g_count++;            // any function can use a global variable
}

int main(void)
{
    int x = 10;           // a local variable, valid only inside main
    {
        int x = 20;       // the x in the inner block "shadows" the outer x
        printf("inner x = %d\n", x);
    }
    printf("outer x = %d\n", x);

    add();
    add();
    printf("g_count = %d\n", g_count);
    return 0;
}

Output:

text
inner x = 20
outer x = 10
g_count = 2

When an inner block defines a variable with the same name as one in an outer block, the outer variable is "shadowed" inside the inner block. This is confusing, so avoid it.

Tip
Use global variables sparingly. Any function can modify a global variable, and once a program grows, it becomes very hard to find out "who changed it." Pass data between functions with parameters and return values whenever possible. If you really do need a global variable, you can mark it with a g_ prefix, as above, so that it stands out.

7.6 Storage duration and static

Besides "where is it valid" (scope), a variable also has a "how long does it live" property, called its storage duration.

  • Automatic storage duration: an ordinary local variable is created when the program enters its block and destroyed when the program leaves the block. Each call to a function gets brand-new local variables.
  • Static storage duration: global variables and local variables marked static are created when the program starts and destroyed only when it ends, and they're initialized only once. A static variable without an explicit initializer is automatically initialized to 0.
c
#include <stdio.h>

void counter(void)
{
    static int count = 0;   // a static local variable: initialized once, and it survives after the function returns
    int normal = 0;         // an ordinary local variable: created and initialized again on every call
    count++;
    normal++;
    printf("count = %d, normal = %d\n", count, normal);
}

int main(void)
{
    counter();
    counter();
    counter();
    return 0;
}

Output:

text
count = 1, normal = 1
count = 2, normal = 1
count = 3, normal = 1

A static local variable combines two traits: it's as long-lived as a global variable, yet, like a local variable, it can be used only inside its function. It's well suited to recording state such as "how many times has this function been called?"

static can also be applied to global variables and functions, making them visible only within the current source file; the related keyword extern also comes into play there. Both are closely tied to multi-file programs and are covered in Chapter 15.

7.7 Recursion: a function that calls itself

When a function calls itself from within its own body, that's recursion. Recursion suits problems that "can be broken down into smaller problems of the same kind."

Take the factorial: n! = n × (n−1)!, and 1! = 1. The problem of computing n! turns into the problem of computing (n−1)!:

c
#include <stdio.h>

long long factorial(int n)
{
    if (n <= 1) {                    // the base case
        return 1;
    }
    return n * factorial(n - 1);     // the recursive case
}

int main(void)
{
    for (int i = 0; i <= 5; i++) {
        printf("%d! = %lld\n", i, factorial(i));
    }
    return 0;
}

Output:

text
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120

Running factorial(4) first "dives in" level by level, and once it reaches the base case, it "returns" level by level:

text
factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * 1))          <- base case reached; now return level by level
= 4 * (3 * 2)
= 4 * 6
= 24

Every recursive function needs two ingredients:

  1. A base case: the simplest situation, where the answer can be given directly without recursing;
  2. A recursive case: turns the problem into a smaller problem of the same kind, and each recursive call must move closer to the base case.

If the base case is missing, or the recursion can never reach it, the function calls itself endlessly until the program crashes.

The cost of recursion. Recursive code is often very concise, but it isn't always efficient. Take the Fibonacci sequence (1, 1, 2, 3, 5, 8, ..., where each term from the third on is the sum of the two before it):

c
#include <stdio.h>

long long g_calls = 0;   // counts how many times the recursive function is called

long long fib_recursive(int n)
{
    g_calls++;
    if (n <= 2) {
        return 1;
    }
    return fib_recursive(n - 1) + fib_recursive(n - 2);
}

long long fib_loop(int n)
{
    long long a = 1, b = 1;
    for (int i = 3; i <= n; i++) {
        long long c = a + b;
        a = b;
        b = c;
    }
    return b;
}

int main(void)
{
    int n = 40;
    printf("Loop version:      fib(%d) = %lld\n", n, fib_loop(n));
    printf("Recursive version: fib(%d) = %lld\n", n, fib_recursive(n));
    printf("The recursive version made %lld function calls\n", g_calls);
    return 0;
}

Output:

text
Loop version:      fib(40) = 102334155
Recursive version: fib(40) = 102334155
The recursive version made 204668309 function calls

The loop version finishes instantly, while the recursive version makes over two hundred million function calls, and you can feel a clear pause when it runs. The cause is a huge amount of repeated computation, which you can see in the call tree for fib(5):

text
                    fib(5)
                  /        \
             fib(4)          fib(3)
            /      \         /     \
        fib(3)    fib(2)  fib(2)  fib(1)
        /    \
    fib(2)  fib(1)

fib(3) is computed twice and fib(2) three times. The larger n gets, the worse the repetition becomes, and the number of calls grows exponentially with n.

The Tower of Hanoi. Some problems are very natural to think about recursively. The Tower of Hanoi puzzle goes like this: there are three pegs, A, B, and C, and peg A holds n disks stacked from largest at the bottom to smallest at the top. The goal is to move all of them to C, moving one disk at a time and never placing a larger disk on top of a smaller one.

The recursive idea: to move n disks from A to C, you only need to—

  1. Move the top n−1 disks from A to B (using C as the spare);
  2. Move the largest disk, disk n, from A to C;
  3. Move the n−1 disks on B to C (using A as the spare).

And "moving n−1 disks" is itself a smaller problem of the same kind!

c
#include <stdio.h>

// Move n disks from peg "from" to peg "to", using peg "via" as the spare
void hanoi(int n, char from, char via, char to)
{
    if (n == 1) {
        printf("Move disk 1 from %c to %c\n", from, to);
        return;
    }
    hanoi(n - 1, from, to, via);
    printf("Move disk %d from %c to %c\n", n, from, to);
    hanoi(n - 1, via, from, to);
}

int main(void)
{
    hanoi(3, 'A', 'B', 'C');
    return 0;
}

Output:

text
Move disk 1 from A to C
Move disk 2 from A to B
Move disk 1 from C to B
Move disk 3 from A to C
Move disk 1 from B to A
Move disk 2 from B to C
Move disk 1 from A to C

Moving n disks takes 2ⁿ − 1 moves. The legendary 64-disk tower would need about 1.8 × 10¹⁹ moves; even at one move per second, that would take about 580 billion years.

Tip
The secret to understanding recursion is trust: when you write hanoi(n - 1, ...), don't try to unfold every level of calls in your head. Trust that it correctly does the job of "moving n−1 disks," and think only about what the current level has to do.

7.8 The call stack

How does a program remember "where to go back to" when a function finishes? The answer is the call stack.

Every time a function is called, the system sets aside a stack frame for it in a memory region called the stack. The frame holds the function's parameters, local variables, and return address, and it's destroyed when the function returns. A stack is "last in, first out," like a pile of plates: the last plate placed on top is the first one taken off.

Here's the call stack at the deepest point of the recursion in factorial(3):

text
          +------------------------+
top       | frame of factorial(1)  |  <- running now
          +------------------------+
          | frame of factorial(2)  |  waiting for factorial(1) to return
          +------------------------+
          | frame of factorial(3)  |  waiting for factorial(2) to return
          +------------------------+
bottom    | frame of main          |  waiting for factorial(3) to return
          +------------------------+

This also explains two things:

  • Why local variables disappear once their function returns: the stack frame that held them has been destroyed;
  • Why recursion that goes too deep crashes: stack space is limited (usually just a few MB), and every level of recursion takes up a stack frame. With too many levels, the stack runs out of space; this is called a stack overflow.

7.9 Using standard library functions

The C standard library provides a large number of ready-made functions. To use them, include the corresponding header:

HeaderPurposeCommon functions
<stdio.h>Input and outputprintf, scanf, getchar, fopen
<stdlib.h>General utilitiesrand, srand, abs, exit, malloc, qsort
<string.h>String handlingstrlen, strcpy, strcmp
<math.h>Mathematicssqrt, pow, fabs, sin
<ctype.h>Character classificationisdigit, isalpha, toupper
<time.h>Date and timetime, clock

Here's <math.h> as an example:

c
#include <stdio.h>
#include <math.h>

int main(void)
{
    printf("sqrt(2)     = %.4f\n", sqrt(2.0));    // square root
    printf("pow(2, 10)  = %.0f\n", pow(2, 10));   // power
    printf("fabs(-3.5)  = %.1f\n", fabs(-3.5));   // absolute value
    printf("ceil(2.1)   = %.0f\n", ceil(2.1));    // round up
    printf("floor(2.9)  = %.0f\n", floor(2.9));   // round down
    printf("round(2.5)  = %.0f\n", round(2.5));   // round to nearest (halves away from zero)
    printf("log10(1000) = %.1f\n", log10(1000));  // base-10 logarithm
    return 0;
}

Output:

text
sqrt(2)     = 1.4142
pow(2, 10)  = 1024
fabs(-3.5)  = 3.5
ceil(2.1)   = 3
floor(2.9)  = 2
round(2.5)  = 3
log10(1000) = 3.0

Note that most functions in <math.h> take and return double; for the absolute value of an integer, use abs from <stdlib.h>.

7.10 How to design good functions

  1. A function should do one thing. If you find it hard to sum up what a function does in a single sentence, it probably should be split up.
  2. Give it a good name. Function names are usually verbs or verb phrases, such as print_report, is_valid, or calculate_average. Functions that return true or false often start with is_ or has_.
  3. Keep functions short. A common guideline is no more than one screen (about 50 lines).
  4. Don't take too many parameters. With more than 4 or 5 parameters, consider bundling them into a structure (Chapter 11).
  5. Avoid relying on global variables. Functions that get their input from parameters and deliver their results through return values are the easiest to understand and test.

Exercises

  1. A prime-checking function. Write a function int is_prime(int n) that returns 1 if n is prime and 0 otherwise. Use it to print all the primes below 100, 10 per line.
  2. GCD and LCM. Write the functions int gcd(int a, int b) and int lcm(int a, int b), then read two positive integers and print their greatest common divisor and least common multiple.
  3. Recursive power. Write a recursive function double power(double x, int n) that computes xⁿ (n ≥ 0). Going further: use the idea that xⁿ = (x^(n/2))² to cut the number of recursive calls from n down to about log₂n.
  4. Printing binary recursively. Write a recursive function void print_binary(int n) that prints the binary representation of a non-negative integer n. For example, print_binary(13) prints 1101.

Chapter 8: Arrays: Working with Data in Bulk

8.1 Why arrays?

To process the grades of a class of 50 students, would you really define 50 variables named score1, score2, ..., score50? Obviously not. An array is a structure for storing a group of data of the same type: it consists of a number of elements stored one after another, all sharing a single name and told apart by an index.

c
int scores[50];    // defines 50 int "slots" in one go

8.2 Defining and initializing one-dimensional arrays

You define an array as type name[length];. The length is usually a constant, as in int scores[50]; or double prices[N]; (where N is a macro constant defined with #define).

There are several ways to initialize an array:

c
int a[5] = {1, 2, 3, 4, 5};         // full initialization
int b[5] = {1, 2};                  // partial initialization; the rest become 0: {1, 2, 0, 0, 0}
int c[5] = {0};                     // all zeros; the most common way to zero an array
int d[] = {10, 20, 30};             // length omitted; it's set by the number of values, here 3
int e[10] = {[2] = 5, [7] = 9};     // C99 designated initializers: e[2] is 5, e[7] is 9, the rest 0
Caution
If an array defined inside a function isn't initialized at all (as in int f[5];), every element holds an indeterminate "garbage value." But as soon as the initializer list contains even one value, all the remaining elements are set to 0.

Finding an array's length. Divide the size of the whole array in bytes by the size of one element:

c
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]);   // 32 / 4 = 8

Arrays can't be assigned as a whole. int x[3], y[3] = {1, 2, 3}; x = y; is an error, and the compiler complains: assignment to expression with array type. To copy an array, copy it element by element with a loop (or use memcpy, introduced in Chapter 9).

8.3 Accessing array elements

You access an element as name[index], and indexes start at 0: an array of length n has indexes 0 through n−1. Looping over an array is the most common operation of all:

c
#include <stdio.h>

#define N 5

int main(void)
{
    int scores[N];
    printf("Enter %d scores: ", N);
    for (int i = 0; i < N; i++) {
        scanf("%d", &scores[i]);
    }

    int sum = 0;
    int max_index = 0;             // remembers the index of the highest score
    for (int i = 0; i < N; i++) {
        sum += scores[i];
        if (scores[i] > scores[max_index]) {
            max_index = i;
        }
    }

    printf("Total: %d\n", sum);
    printf("Average: %.2f\n", (double)sum / N);
    printf("Highest: %d (score #%d)\n", scores[max_index], max_index + 1);
    return 0;
}

Output:

text
Enter 5 scores: 78 92 85 66 90
Total: 411
Average: 82.20
Highest: 92 (score #2)
Common mistake
Going out of bounds. For int a[5], the valid indexes are 0 to 4, and a[5] is already out of bounds. C doesn't check whether an index is in range: reading out of bounds gives unpredictable values, and writing out of bounds quietly overwrites the memory of other variables. The program might crash, or it might "run normally" and produce wrong results. This is one of the most common and most dangerous errors in C programs. Be especially careful with loop bounds: for (i = 0; i < 5; i++) is right, while for (i = 0; i <= 5; i++) goes out of bounds.

8.4 What arrays look like in memory

All the elements of an array are stored contiguously in memory. The program below prints the address of each element:

c
#include <stdio.h>

int main(void)
{
    int a[5] = {10, 20, 30, 40, 50};
    for (int i = 0; i < 5; i++) {
        printf("a[%d] = %d, address: %p\n", i, a[i], (void *)&a[i]);
    }
    return 0;
}

A possible run (the addresses may differ every time you run it):

text
a[0] = 10, address: 0x7ffd5e3c8a10
a[1] = 20, address: 0x7ffd5e3c8a14
a[2] = 30, address: 0x7ffd5e3c8a18
a[3] = 40, address: 0x7ffd5e3c8a1c
a[4] = 50, address: 0x7ffd5e3c8a20

The addresses of neighboring elements differ by exactly 4, the size of one int:

text
   a[0]     a[1]     a[2]     a[3]     a[4]
+--------+--------+--------+--------+--------+
|   10   |   20   |   30   |   40   |   50   |
+--------+--------+--------+--------+--------+
 ...8a10  ...8a14  ...8a18  ...8a1c  ...8a20

Because the elements are stored contiguously, the computer can find any element instantly from the address of the first element and the index: the address of a[i] = the starting address + i × the element size. That's why arrays support "random access" to any element, and why that access is so fast. It also explains why indexes start at 0: an index is really "the distance from the first element."

8.5 Common array operations

1. Reversing in place. Move two indexes inward from both ends, swapping the elements they point to:

c
for (int i = 0, j = n - 1; i < j; i++, j--) {
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

2. Counting. Using indexes to stand for data values and elements to record how many times each value occurs is a very practical technique. For example, here's how to count how many scores fall in each range:

c
#include <stdio.h>

int main(void)
{
    int scores[] = {95, 82, 67, 45, 78, 88, 91, 100, 59, 73, 84, 66};
    int n = sizeof(scores) / sizeof(scores[0]);
    int counts[11] = {0};   // counts[k] counts scores from k*10 to k*10+9; counts[10] counts 100s

    for (int i = 0; i < n; i++) {
        counts[scores[i] / 10]++;
    }

    printf("90-100: %d\n", counts[9] + counts[10]);
    for (int k = 8; k >= 6; k--) {
        printf("%d-%d:  %d\n", k * 10, k * 10 + 9, counts[k]);
    }
    int fail = 0;
    for (int k = 0; k < 6; k++) {
        fail += counts[k];
    }
    printf("Below 60: %d\n", fail);
    return 0;
}

Output:

text
90-100: 3
80-89:  3
70-79:  2
60-69:  2
Below 60: 2

The single line counts[scores[i] / 10]++ both works out which range a score belongs to and counts it, with no need for a long chain of if-elses.

8.6 Arrays as function parameters

To pass an array to a function, write the parameter as type name[], and always pass the length as an additional argument:

c
#include <stdio.h>

void print_array(const int arr[], int n)
{
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

void double_all(int arr[], int n)
{
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;
    }
}

int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int n = sizeof(a) / sizeof(a[0]);
    print_array(a, n);
    double_all(a, n);
    print_array(a, n);
    return 0;
}

Output:

text
1 2 3 4 5
2 4 6 8 10

Two things happen here that are completely different from ordinary variables:

  1. The function modified the original array. Chapter 7 said that C passes arguments by value, so a function can't change its arguments. But when you pass an array, what's actually passed is the address of its first element, and the function works directly on the original array's memory through that address. Chapter 10 explains this in detail.
  2. Inside the function, `sizeof` can't tell you the array's length. Inside the function, arr is really a pointer, so sizeof(arr) gives the size of a pointer (8 on common 64-bit systems), not the size of the whole array. GCC warns about this: 'sizeof' on array function parameter 'arr' will return size of 'const int *'. That's why the caller has to pass in the length.

The const in front of the parameter means "this function only reads the array and doesn't modify it." It's a good habit: it prevents the function from modifying the data by accident, and it tells anyone reading the code at a glance what the function intends.

8.7 Sorting

Sorting is one of the most fundamental and important kinds of algorithms. Here are two simple sorting algorithms.

Bubble sort. Walk from the beginning to the end, comparing each pair of neighboring elements and swapping them if they're in the wrong order. After one pass, the largest element has "bubbled up" to the end. Repeat the process on the remaining elements.

Taking {5, 2, 9, 1, 7, 3} as an example (the square brackets mark the part that's already sorted):

text
start:    5  2  9  1  7  3
pass 1:   2  5  1  7  3 [9]
pass 2:   2  1  5  3 [7  9]
pass 3:   1  2  3 [5  7  9]
pass 4:   1  2  3  5  7  9     <- no swaps in this pass: already sorted, so stop early
c
#include <stdio.h>
#include <stdbool.h>

void bubble_sort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++) {           // at most n-1 passes
        bool swapped = false;
        for (int j = 0; j < n - 1 - i; j++) {   // the last i elements are already in place
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        if (!swapped) {
            break;
        }
    }
}

void print_array(const int arr[], int n)
{
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void)
{
    int a[] = {5, 2, 9, 1, 7, 3};
    int n = sizeof(a) / sizeof(a[0]);
    printf("Before: ");
    print_array(a, n);
    bubble_sort(a, n);
    printf("After:  ");
    print_array(a, n);
    return 0;
}

Output:

text
Before: 5 2 9 1 7 3
After:  1 2 3 5 7 9

Selection sort. In each pass, select the smallest element of the unsorted part and move it to the front of the unsorted part:

c
void selection_sort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        int min_index = i;                 // assume the current position holds the minimum
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_index]) {
                min_index = j;
            }
        }
        if (min_index != i) {              // swap the minimum into position i
            int temp = arr[i];
            arr[i] = arr[min_index];
            arr[min_index] = temp;
        }
    }
}

In the worst case, both of these algorithms make roughly n²/2 comparisons, so we say their time complexity is O(n²). With n = 100,000, that's about 5 billion comparisons—slow. In practice, you'll usually use the standard library's qsort function (introduced in Chapter 10), which uses a more efficient algorithm.

8.8 Searching

Linear search: compare the elements one by one from the beginning. It's simple but slow: in the worst case it takes n comparisons.

c
int linear_search(const int arr[], int n, int target)
{
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;        // found it: return the index
        }
    }
    return -1;               // not found: return -1
}

Binary search: works only on sorted arrays. Each step compares the middle element with the target: if they're equal, you've found it; if the target is larger, search the right half; if it's smaller, search the left half. Every comparison cuts the search range in half, so even a million elements need at most 20 comparisons (2²⁰ ≈ one million). This is exactly the "always guess the middle" strategy from the number-guessing game.

c
#include <stdio.h>

int binary_search(const int arr[], int n, int target)
{
    int low = 0, high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;   // same as (low + high) / 2, but can't overflow
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            low = mid + 1;                  // the target is in the right half
        } else {
            high = mid - 1;                 // the target is in the left half
        }
    }
    return -1;
}

int main(void)
{
    int a[] = {3, 8, 15, 23, 42, 57, 68, 91};
    int n = sizeof(a) / sizeof(a[0]);
    printf("Index of 42: %d\n", binary_search(a, n, 42));
    printf("Index of 50: %d\n", binary_search(a, n, 50));
    return 0;
}

Output:

text
Index of 42: 4
Index of 50: -1

Here's how the search for 42 proceeds:

lowhighmida[mid]Comparison
0732323 < 42, so search the right half
4755757 > 42, so search the left half
44442Found it!
Tip
mid = (low + high) / 2 overflows when low + high exceeds the range of int; writing low + (high - low) / 2 avoids the problem. This very bug lurked in the binary search of Java's standard library for nearly a decade before anyone noticed.

8.9 Two-dimensional arrays

Data in the form of a table—several students' grades in several courses, a matrix, a game board—is best represented with a two-dimensional array:

c
int scores[3][4];   // 3 rows and 4 columns: 12 elements in all

Think of a two-dimensional array as "an array of arrays": scores is an array of 3 elements, and each element is itself an array of 4 ints. You access the element in row i, column j as scores[i][j]; row and column indexes both start at 0.

c
#include <stdio.h>

#define STUDENTS 3
#define COURSES 3

int main(void)
{
    int scores[STUDENTS][COURSES] = {
        {85, 92, 78},     // student 0's scores in three courses
        {90, 88, 95},
        {72, 65, 80}
    };

    // each student's total: fix the row, walk across the columns
    for (int i = 0; i < STUDENTS; i++) {
        int total = 0;
        for (int j = 0; j < COURSES; j++) {
            total += scores[i][j];
        }
        printf("Total for student %d: %d\n", i + 1, total);
    }

    // each course's average: fix the column, walk down the rows
    for (int j = 0; j < COURSES; j++) {
        int sum = 0;
        for (int i = 0; i < STUDENTS; i++) {
            sum += scores[i][j];
        }
        printf("Average for course %d: %.2f\n", j + 1, (double)sum / STUDENTS);
    }
    return 0;
}

Output:

text
Total for student 1: 255
Total for student 2: 273
Total for student 3: 217
Average for course 1: 82.33
Average for course 2: 81.67
Average for course 3: 84.33

A two-dimensional array is stored row by row in memory (row-major order). int m[2][3] is laid out like this:

text
  m[0][0]  m[0][1]  m[0][2]  m[1][0]  m[1][1]  m[1][2]
+--------+--------+--------+--------+--------+--------+
|        |        |        |        |        |        |
+--------+--------+--------+--------+--------+--------+
 \________ row 0 ________/  \________ row 1 ________/

When a two-dimensional array is a function parameter, you must specify the number of columns (the number of rows can be omitted), because the compiler needs to know how many elements each row has in order to work out where m[i][j] is:

c
void print_matrix(int m[][3], int rows)
{
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%4d", m[i][j]);
        }
        printf("\n");
    }
}

8.10 Variable-length arrays (Good to know)

C99 lets you use a variable as the length of an array; this is called a variable-length array (VLA):

c
int n;
scanf("%d", &n);
int a[n];    // the length isn't known until run time

But VLAs come with plenty of limitations. Starting with C11 they became an optional feature that compilers may choose not to support, and Visual Studio's C compiler doesn't support them. They're allocated on the stack, so if n is large, the program crashes with a stack overflow, and there's no way to detect or handle that failure. So for arrays whose size is decided at run time, use the dynamic memory allocation described in Chapter 12.

Exercises

  1. Rotate right. Read n (n ≤ 100) integers and a non-negative integer k, rotate the array right by k positions, and print it. For example, rotating 1 2 3 4 5 right by 2 gives 4 5 1 2 3.
  2. The second largest. Read 10 integers and find the second largest (values equal to the maximum don't count). If there isn't one (for example, when all the numbers are equal), print a message saying so. Go through the array only once.
  3. Removing elements. Read n integers and an integer x, remove every element equal to x (shifting the later elements forward), and print the resulting array.
  4. Pascal's triangle. Use a two-dimensional array to print the first 10 rows of Pascal's triangle. Each row begins and ends with 1, and every other number is the sum of the number "above and to the left" and the number "directly above":
text
   1
   1 1
   1 2 1
   1 3 3 1
   1 4 6 4 1

Chapter 9: Strings

Handling text is one of the things programs do most often: usernames, file paths, a sentence, an entire article... In C, all of these are represented as strings.

9.1 Strings in C

Unlike many languages, C has no dedicated string type. In C, a string is simply an array of characters that ends with the null character `'\0'`.

c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "Hello";
    printf("s = %s\n", s);
    printf("sizeof(s) = %zu\n", sizeof(s));   // bytes taken by the array, including the final '\0'
    printf("strlen(s) = %zu\n", strlen(s));   // length of the string, not counting '\0'

    for (int i = 0; s[i] != '\0'; i++) {
        printf("s[%d] = '%c'\n", i, s[i]);
    }
    return 0;
}

Output:

text
s = Hello
sizeof(s) = 6
strlen(s) = 5
s[0] = 'H'
s[1] = 'e'
s[2] = 'l'
s[3] = 'l'
s[4] = 'o'

"Hello" has only 5 characters, yet the array takes 6 bytes, because the compiler automatically adds a '\0' at the end:

text
  s[0]  s[1]  s[2]  s[3]  s[4]  s[5]
+-----+-----+-----+-----+-----+-----+
| 'H' | 'e' | 'l' | 'l' | 'o' | '\0'|
+-----+-----+-----+-----+-----+-----+

'\0' is the character whose ASCII code is 0 (note that it's not the character '0', whose ASCII code is 48). Every string function relies on it to know where a string ends, and so does the for loop above. Without a `'\0'`, it isn't a valid string: printf and the other functions would keep reading until they happened to hit a 0 somewhere in memory, printing garbage along the way or even crashing the program.

Here are several ways to initialize a character array:

c
char a[6] = {'H', 'e', 'l', 'l', 'o', '\0'};  // character by character: you must write the '\0' yourself
char b[6] = "Hello";      // from a string: the compiler adds the '\0' automatically
char c[] = "Hello";       // the length is worked out automatically as 6
char d[20] = "Hello";     // all the remaining elements are '\0'
char e[5] = "Hello";      // legal, but there's no room for '\0': e is NOT a valid string!

So when you define a character array, make its length at least one more than the longest string it will hold.

9.2 Reading and printing strings

To print a string, use %s with printf, or the puts function; puts(s) automatically adds a newline after the string.

To read a string, there are two common ways:

c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char name[20];
    printf("What's your name? ");
    scanf("%19s", name);                   // an array name already stands for an address: no &

    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF) {
        // clear the newline that scanf left in the buffer
    }

    char line[100];
    printf("Say something: ");
    if (fgets(line, sizeof(line), stdin) != NULL) {
        line[strcspn(line, "\n")] = '\0';  // remove the newline that fgets keeps
        printf("Hi, %s! You said: [%s]\n", name, line);
    }
    return 0;
}

Output:

text
What's your name? Tom
Say something: I love C programming
Hi, Tom! You said: [I love C programming]

How `scanf("%s", ...)` behaves:

  • It skips any whitespace at the start, then reads until the next whitespace character (space, tab, newline). So it can read only one word: for the input Tom Smith, it reads just Tom.
  • No `&` goes in front of the variable name, because an array's name already stands for the address of its first element (Chapter 10 explains this in detail).
  • Always limit how much it reads. name has 20 bytes, room for at most 19 characters plus a '\0', so we write %19s. With plain %s, if the user types more than 19 characters, the extra characters are written past the end of the array and corrupt other memory. This is the infamous buffer overflow, the root cause of countless security holes in history.

How `fgets(array, size, stdin)` behaves:

  • It reads an entire line, spaces included;
  • It reads at most "size − 1" characters and then adds a '\0' itself, so it never overflows;
  • If the line fits within the limit, the newline `'\n'` is read in as well, and you usually need to remove it. strcspn(line, "\n") returns the position of the first newline in line (or the string's length if there's no newline); writing '\0' at that position removes the newline;
  • It returns NULL if reading fails or the input has ended.
Common mistake
The gets function found in older textbooks has no way to limit how much it reads, which made it a notorious source of buffer overflows; it was removed in the C11 standard. Never use gets; use fgets instead.
Caution
When you mix scanf and fgets, scanf leaves a newline in the buffer, and the fgets that follows reads that newline and finishes immediately, giving you an empty string. That's why the program above clears the buffer between them.

9.3 String functions

<string.h> provides a large number of functions for working with strings:

FunctionWhat it does
strlen(s)Returns the length of the string (not counting '\0')
strcpy(dest, src)Copies src into dest
strncpy(dest, src, n)Copies at most n characters
strcat(dest, src)Appends src to the end of dest
strncat(dest, src, n)Appends at most n characters and always adds a '\0'
strcmp(s1, s2)Compares in dictionary order: returns a negative number if s1 comes first, 0 if they're equal, and a positive number if s1 comes later
strncmp(s1, s2, n)Compares only the first n characters
strchr(s, c)Finds the first occurrence of the character c
strstr(s, sub)Finds the first occurrence of the substring sub
strcspn(s, set)Returns the length of the initial part of s that contains no characters from set
memset(p, c, n)Sets n bytes starting at p to c
memcpy(dest, src, n)Copies n bytes
c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char dest[50] = "Hello";
    char src[] = "World";

    printf("strlen(dest) = %zu\n", strlen(dest));

    strcat(dest, ", ");
    strcat(dest, src);
    printf("After strcat: %s\n", dest);

    char copy[50];
    strcpy(copy, dest);
    printf("After strcpy: %s\n", copy);

    if (strcmp("apple", "banana") < 0) {
        printf("apple comes before banana\n");
    }
    if (strcmp(dest, copy) == 0) {
        printf("dest and copy have the same contents\n");
    }

    char *pos = strstr(dest, "World");   // returns a pointer to where the substring starts, or NULL
    if (pos != NULL) {
        printf("\"World\" starts at index %d\n", (int)(pos - dest));
    }
    return 0;
}

Output:

text
strlen(dest) = 5
After strcat: Hello, World
After strcpy: Hello, World
apple comes before banana
dest and copy have the same contents
"World" starts at index 7

strstr returns a pointer (of type char *), and pos - dest works out how many characters from the start of the string it points to. Pointers are covered in Chapter 10; for now, just use this as shown.

Keep these points in mind when using these functions:

  1. Compare strings with `strcmp`, never with `==`. if (s1 == s2) compares the addresses of the two arrays, not their contents, so it's almost always "not equal."
  2. You can't assign a string with `=`. char s[20]; s = "Hello"; is an error (arrays can't be assigned as a whole); use strcpy(s, "Hello"); instead.
  3. `strcpy` and `strcat` don't check whether the destination is big enough. The destination array must be large enough, or you get a buffer overflow.
  4. `strncpy` has a catch: if the source string's length is n or more, it does not add a '\0' at the end.

So how do you copy a string safely? A simple, reliable way is to use snprintf (introduced in Section 9.6). It always adds a '\0' and never writes past the space you give it:

c
char small[6];
snprintf(small, sizeof(small), "%s", copy);   // writes at most 5 characters plus one '\0'
printf("%s\n", small);                         // prints: Hello

Splitting a string with `strtok`. strtok can cut a string into pieces at the separators you specify:

c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char text[] = "apple,banana;cherry,,date";
    char *token = strtok(text, ",;");       // on the first call, pass the string
    while (token != NULL) {
        printf("[%s]\n", token);
        token = strtok(NULL, ",;");         // on later calls, pass NULL to keep working on the same string
    }
    return 0;
}

Output:

text
[apple]
[banana]
[cherry]
[date]

Note that strtok modifies the original string (it replaces the separators with '\0'), so it can't be used on string literals. It also skips runs of consecutive separators, which means empty fields are ignored.

9.4 Writing string functions yourself

The best way to understand strings is to implement a few string functions yourself. They all boil down to one idea: process the characters one by one until you reach `'\0'`.

c
#include <stdio.h>

// Returns the length of a string
size_t my_strlen(const char s[])
{
    size_t len = 0;
    while (s[len] != '\0') {
        len++;
    }
    return len;
}

// Copies a string
void my_strcpy(char dest[], const char src[])
{
    int i = 0;
    while (src[i] != '\0') {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';      // don't forget to copy the end marker
}

// Reverses a string in place
void reverse(char s[])
{
    int len = (int)my_strlen(s);
    for (int i = 0, j = len - 1; i < j; i++, j--) {
        char temp = s[i];
        s[i] = s[j];
        s[j] = temp;
    }
}

int main(void)
{
    char a[] = "Hello, C!";
    char b[20];

    printf("Length: %zu\n", my_strlen(a));
    my_strcpy(b, a);
    printf("Copy: %s\n", b);
    reverse(b);
    printf("Reversed: %s\n", b);
    return 0;
}

Output:

text
Length: 9
Copy: Hello, C!
Reversed: !C ,olleH
Caution
reverse deliberately converts the length to int. If you used size_t (an unsigned type) directly and wrote j = len - 1, then for an empty string len - 1 wouldn't be -1: it would "wrap around" to a huge positive number, and the loop would access memory outside the array. This is a classic trap of unsigned types.

9.5 Character functions

<ctype.h> provides a set of functions for testing and converting single characters:

FunctionWhat it does
isalpha(c)Is it a letter?
isdigit(c)Is it a digit character '0' to '9'?
isalnum(c)Is it a letter or a digit?
isspace(c)Is it whitespace (space, tab, newline, and so on)?
isupper(c) / islower(c)Is it an uppercase / lowercase letter?
toupper(c) / tolower(c)Converts to uppercase / lowercase (other characters are returned unchanged)
ispunct(c)Is it a punctuation character?

The testing functions return a nonzero value when the answer is yes and 0 when it's no. Example: swapping the case of every letter in a string.

c
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char s[] = "Hello World 2026!";
    for (int i = 0; s[i] != '\0'; i++) {
        unsigned char c = (unsigned char)s[i];
        if (isupper(c)) {
            s[i] = (char)tolower(c);
        } else if (islower(c)) {
            s[i] = (char)toupper(c);
        }
    }
    printf("%s\n", s);
    return 0;
}

Output:

text
hELLO wORLD 2026!
Caution
The functions in <ctype.h> require their argument to be a value in the range of unsigned char (or EOF). The bytes of non-ASCII characters such as é may be negative when stored in a char, and passing them in directly is undefined behavior, which is why the code above converts each character to unsigned char first.

9.6 Converting between strings and numbers

Strings to numbers. atoi (to int) and atof (to double), from <stdlib.h>, are the simplest to use: atoi("123") gives 123. But they have a drawback: they return 0 when the conversion fails, so you can't tell whether the input was "0" or "abc". When you need to check whether input is valid, use strtol (to long) and strtod (to double) instead:

c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char input[] = "78xyz";
    char *end;                                 // strtol points end at the first character it didn't convert
    long value = strtol(input, &end, 10);      // 10 means base 10

    if (end == input) {
        printf("\"%s\" is not a number\n", input);
    } else if (*end != '\0') {
        printf("Converted %ld, but found extra characters \"%s\"\n", value, end);
    } else {
        printf("Converted: %ld\n", value);
    }
    return 0;
}

Output:

text
Converted 78, but found extra characters "xyz"

This code uses pointers; it'll make more sense when you come back to it after Chapter 10. The capstone project in Chapter 18 uses exactly this technique to check user input.

Numbers to strings. Use sprintf, or the safer snprintf. They're used just like printf, except that the result is "printed" into a character array instead of onto the screen:

c
char buf[50];
int year = 2026;
double score = 95.5;
snprintf(buf, sizeof(buf), "Score in %d: %.1f", year, score);
puts(buf);    // prints: Score in 2026: 95.5

The second argument of snprintf is the size of the array, which guarantees that what it writes won't overflow the array. sprintf has no such argument, so it risks overflowing.

Parsing data out of a string. sscanf is used just like scanf, except that it reads from a string:

c
const char *date = "2026-09-26";
int y, m, d;
if (sscanf(date, "%d-%d-%d", &y, &m, &d) == 3) {
    printf("Year %d, month %d, day %d\n", y, m, d);    // prints: Year 2026, month 9, day 26
}

9.7 Arrays of strings

Several strings can be stored in a two-dimensional character array, one string per row:

c
#include <stdio.h>
#include <string.h>

#define COUNT 5
#define LEN 20

int main(void)
{
    char names[COUNT][LEN] = {"Tom", "Alice", "Bob", "Diana", "Charlie"};

    // sort the names into dictionary order with bubble sort
    for (int i = 0; i < COUNT - 1; i++) {
        for (int j = 0; j < COUNT - 1 - i; j++) {
            if (strcmp(names[j], names[j + 1]) > 0) {
                char temp[LEN];
                strcpy(temp, names[j]);
                strcpy(names[j], names[j + 1]);
                strcpy(names[j + 1], temp);
            }
        }
    }

    for (int i = 0; i < COUNT; i++) {
        printf("%s\n", names[i]);
    }
    return 0;
}

Output:

text
Alice
Bob
Charlie
Diana
Tom

names[i] is row i, that is, the i-th string. Because strings can't be assigned directly, swapping two of them takes strcpy. Chapter 10 introduces a more efficient approach: storing strings in an array of pointers, so that swapping means exchanging pointers only.

9.8 A note on non-English text

Characters outside ASCII take more than one byte in UTF-8: an accented letter like é takes 2 bytes, a Chinese character like 你 takes 3, and an emoji takes 4. So in a UTF-8 environment, strlen("café") is 5, not 4, and reversing a string byte by byte scrambles such characters into garbage. The string functions in this chapter all work on bytes, which is fine for English letters and digits. Processing text character by character in other languages requires understanding character encodings and wide characters (wchar_t), which go beyond the scope of this guide.

Exercises

  1. Character counts. Read a line of text (which may contain spaces) and count its letters, digits, spaces, and other characters separately.
  2. Implement strcmp. Write a function int my_strcmp(const char s1[], const char s2[]) whose return value follows the same rules as strcmp (negative if the first string comes first, 0 if they're equal, positive otherwise).
  3. Capitalize each word. Read a line of English text, change the first letter of every word to uppercase, and print the result. For example, hello world from c becomes Hello World From C.
  4. Palindromes. Read a string and decide whether it's a palindrome (it reads the same forward and backward), ignoring case. For example, Level and noon are palindromes, but hello isn't.

Chapter 10: Pointers: The Soul of C

Pointers are C's most powerful feature, and they're famously hard for beginners. But don't worry: at heart, a pointer is a very simple thing—a pointer is an address. Once "memory" and "addresses" are clear in your mind, and you get into the habit of drawing pictures, pointers lose all their mystery.

10.1 Memory and addresses

Chapter 2 compared memory to a long row of lockers, where each locker (1 byte) has a number: its address. When you define a variable, the system gives it a run of consecutive bytes, and the variable's address is the address of the first byte it occupies.

The address-of operator & gives you a variable's address, and %p prints an address:

c
#include <stdio.h>

int main(void)
{
    int a = 10;
    double b = 3.14;
    char c = 'X';
    printf("a = %d, address: %p\n", a, (void *)&a);
    printf("b = %.2f, address: %p\n", b, (void *)&b);
    printf("c = %c, address: %p\n", c, (void *)&c);
    return 0;
}

A possible run:

text
a = 10, address: 0x7ffe3b1a5c3c
b = 3.14, address: 0x7ffe3b1a5c40
c = X, address: 0x7ffe3b1a5c3b

The actual addresses may be different every time you run the program, and that doesn't matter. %p expects an argument of type void *, which is why (void *) converts each address first; this chapter explains what void * means later on.

An analogy: a variable is like a hotel room, and its address is the room number. If you write the room number on a slip of paper and hand it to someone, they can find the room—that slip of paper with the room number on it is a pointer.

10.2 Pointer variables

A pointer variable is a variable made specifically for holding addresses. You define one by putting * in front of the variable name:

c
int *p;      // p is a pointer variable: it can hold the address of an int variable

Basic pointer use involves just two operators:

  • &variable: takes the address of a variable;
  • *pointer: accesses the variable the pointer points to; this is called dereferencing.
c
#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;          // p holds a's address: we say "p points to a"

    printf("a  = %d\n", a);
    printf("&a = %p\n", (void *)&a);
    printf("p  = %p\n", (void *)p);
    printf("*p = %d\n", *p);

    *p = 20;              // change the variable p points to, which is a
    printf("After *p = 20, a = %d\n", a);
    return 0;
}

A possible run:

text
a  = 10
&a = 0x7ffc8a2e4b5c
p  = 0x7ffc8a2e4b5c
*p = 10
After *p = 20, a = 20

As a picture:

text
      variable a (address 0x...4b5c)         variable p
     +------------------+          +------------------+
     |        10        |  ◀────── |  0x7ffc8a2e4b5c  |
     +------------------+          +------------------+

p holds the address of a, so *p is a: reading *p reads a, and writing *p writes a.

One `, two meanings.** In a definition like int p`, the ` says "p is a pointer"; in an expression like p`, the `` means "dereference." Beginners easily mix the two up, so keep them apart.

What's the point of a pointer's type? On common 64-bit systems, every kind of pointer is 8 bytes (because all addresses are 64 bits), so sizeof(int *), sizeof(double *), and sizeof(char *) are all 8. (The C standard itself doesn't require all pointer types to have the same size; this describes common platforms.) Why, then, distinguish int * from double *? Because a pointer's type determines two things:

  1. How many bytes a dereference reads or writes: for an int *, *p reads or writes 4 bytes; for a double *, 8 bytes;
  2. The step size of pointer arithmetic: how many bytes p + 1 moves forward (Section 10.5).
Common mistake
int* p, q; defines a pointer p and an ordinary integer q, because the * applies only to the variable name right after it. To define two pointers, write int *p, *q;. To avoid confusion, write the * next to the variable name, or define only one pointer per line.

10.3 Null pointers, wild pointers, and dangling pointers

Null pointers. A pointer whose value is NULL is a null pointer, meaning "it points to nothing." NULL is defined in headers such as <stdio.h> and <stdlib.h>. Dereferencing a null pointer crashes the program (on Linux you usually see Segmentation fault). So before using a pointer that might be null, check it first:

c
if (p != NULL) {
    printf("%d\n", *p);
}

Wild pointers. An uninitialized pointer holds a random value that points to some unknown place in memory; this is a wild pointer. Dereferencing a wild pointer at best reads a garbage value; at worst it corrupts data or crashes the program:

c
int *p;      // a wild pointer: p holds a random address
*p = 10;     // dangerous! writes to an unknown address

A good habit: initialize a pointer as soon as you define it, and if you don't yet know where it should point, set it to `NULL`.

Dangling pointers. A pointer that once pointed to a valid object that no longer exists is a dangling pointer. The classic example is returning the address of a local variable:

c
int *bad_function(void)
{
    int x = 10;
    return &x;      // wrong! x is destroyed when the function returns, so this address is no longer valid
}

Remember the call stack from Chapter 7: when a function returns, its stack frame is destroyed, and its local variables go with it. GCC warns about this: function returns address of local variable. Another common source of dangling pointers is dynamically allocated memory that has already been freed, which you'll meet in Chapter 12.

10.4 Pointers and functions: a swap that works

In Chapter 7, the swap function failed, because the function received only copies of its arguments. Now that we have pointers, we can pass the addresses of the variables to the function and let it operate directly on the original variables through those addresses:

c
#include <stdio.h>

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void)
{
    int x = 1, y = 2;
    swap(&x, &y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}

Output:

text
x = 2, y = 1
text
    main's stack frame                 swap's stack frame
  +-------+-------+                +---------+---------+
  | x = 1 | y = 2 |                | a = &x  | b = &y  |
  +-------+-------+                +---------+---------+
      ▲       ▲                         │         │
      │       └─────────────────────────┼─────────┘
      └─────────────────────────────────┘
       through *a and *b, swap changes main's x and y directly

Arguments are still passed by value—it's just that this time, the values copied are addresses. A copy of an address points to the same place as the original, so the original variable can be changed through the copy. Now you can also see why scanf("%d", &n) needs the &: scanf has to know the address of n in order to store the value it reads there.

"Returning" several values through pointers. A function's return can hand back only one value, but pointer parameters can carry several results back to the caller:

c
#include <stdio.h>

// Computes the quotient and remainder of a divided by b
void div_mod(int a, int b, int *quotient, int *remainder)
{
    *quotient = a / b;
    *remainder = a % b;
}

int main(void)
{
    int q, r;
    div_mod(17, 5, &q, &r);
    printf("17 / 5 = %d remainder %d\n", q, r);
    return 0;
}

Output:

text
17 / 5 = 3 remainder 2

Pointer parameters used to "bring results back" like this are often called output parameters.

10.5 Pointer arithmetic

Pointers support a small set of operations:

OperationMeaning
p + n, p - nMoves the pointer forward (backward) by n elements, not n bytes
p++, p--Moves the pointer by one element
p - qHow many elements apart two pointers are (both must point into the same array)
p == q, p < q, etc.Compares the positions of two pointers

Pointers can't be added together, multiplied, or divided—adding two addresses makes no sense.

c
#include <stdio.h>

int main(void)
{
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;                  // same as int *p = &arr[0];

    printf("p     = %p, *p       = %d\n", (void *)p, *p);
    printf("p + 1 = %p, *(p + 1) = %d\n", (void *)(p + 1), *(p + 1));
    printf("p + 2 = %p, *(p + 2) = %d\n", (void *)(p + 2), *(p + 2));

    char str[] = "abc";
    char *q = str;
    printf("q     = %p\n", (void *)q);
    printf("q + 1 = %p\n", (void *)(q + 1));

    int *last = &arr[4];
    printf("last - p = %td\n", last - p);
    return 0;
}

A possible run:

text
p     = 0x7ffd4f6c1e20, *p       = 10
p + 1 = 0x7ffd4f6c1e24, *(p + 1) = 20
p + 2 = 0x7ffd4f6c1e28, *(p + 2) = 30
q     = 0x7ffd4f6c1e1c
q + 1 = 0x7ffd4f6c1e1d
last - p = 4

Adding 1 to an int * increases the address by 4 (the size of one int); adding 1 to a char * increases it by just 1. That's what it means for a pointer's type to determine its "step size." The result of subtracting two pointers has type ptrdiff_t, whose format specifier is %td.

Caution
Pointer arithmetic is meaningful only inside an array. The result of p + n or p - n must still point to an element of the same array, or exactly one past its last element (that "one past the end" position may be used in comparisons and subtraction but must not be dereferenced); going beyond that range is undefined behavior even if you never access the memory. p - q and ordering comparisons such as < and > likewise work only on two pointers into the same array. == and !=, on the other hand, can compare any two pointers of the same type, and any pointer can be compared with NULL.

10.6 Pointers and arrays

Pointers and arrays are closely related; this is one of C's defining features. At its core is a single rule:

In most expressions, an array's name is automatically converted into a pointer to its first element.

So int *p = arr; and int *p = &arr[0]; are exactly the same. It follows that:

text
arr[i]  ≡  *(arr + i)  ≡  *(p + i)  ≡  p[i]

In fact, C defines the subscript operation a[i] as *(a + i).

c
#include <stdio.h>

int main(void)
{
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;

    // four equivalent ways to access an element
    printf("%d %d %d %d\n", arr[2], *(arr + 2), p[2], *(p + 2));

    // walking through an array with a pointer
    for (int *q = arr; q < arr + 5; q++) {
        printf("%d ", *q);
    }
    printf("\n");

    printf("sizeof(arr) = %zu\n", sizeof(arr));
    printf("sizeof(p)   = %zu\n", sizeof(p));
    return 0;
}

Output (on a common 64-bit system):

text
30 30 30 30
10 20 30 40 50
sizeof(arr) = 20
sizeof(p)   = 8

An array name and a pointer aren't exactly the same thing. They differ in these ways:

  1. An array name can't be assigned to or changed. arr = p; and arr++ are both errors, whereas a pointer variable can be pointed anywhere you like.
  2. `sizeof` gives different results. sizeof(arr) is the size of the whole array (20 bytes); sizeof(p) is the size of the pointer itself (8 bytes).
  3. `&arr` has a different type. &arr has the same numeric value as arr, but its type is "pointer to the whole array," so &arr + 1 skips over the entire array (moving 20 bytes forward), while arr + 1 moves only 4 bytes.

To sum up: when an array is the operand of sizeof or & (or of typeof, new in C23), its name isn't converted into a pointer. Also, when a double-quoted string (a string literal) initializes a character array, as in char s[] = "hello";, the literal isn't converted into a pointer either; its characters are copied into the array instead (the next section has the details). Everywhere else, an array name "decays" into a pointer to its first element.

The truth about array parameters. Now we can explain the two puzzles from Chapter 8. When an array is passed to a function, what's passed is the address of its first element. So a function parameter written int arr[] is really int *arr; the two are exactly equivalent. That's why the function can change the original array, and why sizeof(arr) inside the function gives only 8.

What does `p++ mean?** You'll see p++` all the time in code that processes arrays and strings. Because postfix `++` has higher precedence than `, it's equivalent to *(p++): it gets the value p currently points to, and then moves p` to the next element. Compare the variations:

ExpressionMeaning
*p++Gets the value of *p, then moves p forward by one element
(*p)++Gets the value of *p, then adds 1 to the value p points to; p doesn't move
*++pMoves p forward by one element first, then gets the value it points to
++*pAdds 1 to the value p points to first, then gets that value

10.7 Pointers and strings

There are two common ways to represent a string, and they're fundamentally different:

c
#include <stdio.h>

int main(void)
{
    char s[] = "hello";          // a character array initialized with the contents of a string literal: modifiable
    const char *p = "hello";     // a character pointer to a read-only string literal

    s[0] = 'H';                  // fine: this modifies the array's own copy
    // p[0] = 'H';               // wrong: string literals can't be modified
    p = "world";                 // fine: the pointer can be pointed at another string
    // s = "world";              // wrong: an array name can't be assigned to

    printf("%s %s\n", s, p);
    return 0;
}

Output:

text
Hello world
text
 array s on the stack (modifiable)     read-only data area (not modifiable)
+---+---+---+---+---+----+          +---+---+---+---+---+----+
| H | e | l | l | o | \0 |          | h | e | l | l | o | \0 |   <- the literal "hello"
+---+---+---+---+---+----+          +---+---+---+---+---+----+
                                    +---+---+---+---+---+----+
   pointer p ──────────────────────▶ | w | o | r | l | d | \0 |   <- the literal "world"
                                    +---+---+---+---+---+----+

String literals (such as "hello") live in the program's read-only data area, and modifying a string literal is undefined behavior that crashes the program on most systems. So a pointer to a string literal should be declared as const char *; then any attempt to modify the literal is caught by the compiler as an error.

Pointers make string processing very concise. Here are strlen and strcpy written with pointers:

c
size_t my_strlen(const char *s)
{
    const char *p = s;
    while (*p != '\0') {
        p++;
    }
    return (size_t)(p - s);      // the distance between the two pointers is the string's length
}

char *my_strcpy(char *dest, const char *src)
{
    char *start = dest;
    while ((*dest++ = *src++) != '\0') {
        // empty loop body: the copying and moving all happen in the condition
    }
    return start;
}

(*dest++ = *src++) != '\0' is a classic C idiom: it assigns *src to *dest, moves both pointers forward, and checks whether the character just copied was '\0', stopping if it was. That way the '\0' gets copied too.

10.8 const and pointers

When const is combined with a pointer, its meaning depends on where it goes:

Written asCan you change the value through p?Can you point p somewhere else?
const int *p (same as int const *p)NoYes
int *const pYesNo
const int *const pNoNo

How to remember it: *look at whether `const` is to the left or right of the `.** A const to the left of the ` applies to the data being pointed to (the data is read-only); a `const` to the right of the `` applies to the pointer itself (where it points is fixed).

c
int a = 1, b = 2;

const int *p1 = &a;
// *p1 = 10;       // wrong: can't modify the data through p1
p1 = &b;           // fine: p1 can point somewhere else

int *const p2 = &a;
*p2 = 10;          // fine: can modify the data through p2
// p2 = &b;        // wrong: p2 can't point somewhere else

The most common use of const int * is in function parameters, to say "this function only reads the data; it won't modify it," as in the standard library's size_t strlen(const char *s);. Whenever a function doesn't need to modify the data a pointer parameter points to, add `const` to that parameter; it's one of the hallmarks of professional C code.

10.9 Pointers to pointers

A pointer variable is itself a variable with its own address, so you can have a "pointer to a pointer," also called a double pointer:

c
#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;       // p points to a
    int **pp = &p;     // pp points to p

    printf("a = %d, *p = %d, **pp = %d\n", a, *p, **pp);
    **pp = 20;         // same as *p = 20, which is a = 20
    printf("a = %d\n", a);
    return 0;
}

Output:

text
a = 10, *p = 10, **pp = 10
a = 20
text
      pp                  p                  a
+-----------+       +-----------+       +---------+
|  &p   ●───┼─────▶ |  &a   ●───┼─────▶ |   10    |
+-----------+       +-----------+       +---------+

The most important use of a double pointer is letting a function change the caller's pointer variable. The reasoning is exactly the same as for swap: to change an int in a function, you pass an int *; to change an int * in a function, you pass an int **.

c
#include <stdio.h>

// Makes the caller's pointer *pmax point to the largest element of the array
void point_to_max(int *arr, int n, int **pmax)
{
    *pmax = &arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > **pmax) {
            *pmax = &arr[i];
        }
    }
}

int main(void)
{
    int a[] = {3, 9, 4, 7};
    int *pm = NULL;
    point_to_max(a, 4, &pm);
    printf("The largest value is %d, at index %td\n", *pm, pm - a);
    *pm = 0;                       // change the array's largest element directly through pm
    printf("Now the array is: %d %d %d %d\n", a[0], a[1], a[2], a[3]);
    return 0;
}

Output:

text
The largest value is 9, at index 1
Now the array is: 3 0 4 7

Double pointers will come up again in Chapter 12, when a function allocates memory, and in Chapter 13, when a function changes a linked list's head pointer.

10.10 Arrays of pointers and pointers to arrays

These two names sound alike but are completely different things:

  • An array of pointers is first and foremost an array, and each of its elements is a pointer. For example, const char *names[3];.
  • A pointer to an array is first and foremost a pointer, and it points to an entire array. For example, int (*p)[4];.

The key to telling them apart is operator precedence: [] has higher precedence than *. In int *p[4], p binds to [4] first, so p is an array (whose elements have type int *); in int (*p)[4], the parentheses make p bind to * first, so p is a pointer (to an array of 4 ints).

c
#include <stdio.h>

int main(void)
{
    // an array of pointers: each element points to a string
    const char *weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
    for (int i = 0; i < 7; i++) {
        printf("%s ", weekdays[i]);
    }
    printf("\n");

    // a pointer to an array: points to one row of a two-dimensional array
    int m[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    int (*row)[4] = m;             // m decays into a pointer to row 0
    printf("row[1][2] = %d\n", row[1][2]);
    row++;                         // move to the next row (skipping 4 ints)
    printf("(*row)[0] = %d\n", (*row)[0]);
    return 0;
}

Output:

text
Mon Tue Wed Thu Fri Sat Sun
row[1][2] = 7
(*row)[0] = 5

Storing a group of strings in an array of pointers is more flexible than the two-dimensional character array of Chapter 9: the strings can have different lengths without wasting space, and sorting them means swapping pointers only, not copying whole strings.

text
weekdays (an array of pointers)
+-------+       +---+---+---+----+
|   ●───┼──────▶| M | o | n | \0 |
+-------+       +---+---+---+----+
|   ●───┼──────▶| T | u | e | \0 |
+-------+       +---+---+---+----+
|  ...  |

Two-dimensional arrays and pointers. A two-dimensional array m decays into a pointer to row 0, so m + i points to row i. *(m + i) is row i itself (a one-dimensional array, which in turn decays into a pointer to that row's first element). Therefore:

text
m[i][j]  ≡  *(*(m + i) + j)

10.11 The parameters of main

The main function has another standard form that can receive command-line arguments:

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("There are %d command-line arguments:\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

After compiling, run it from the command line like this:

text
$ ./args hello 123 "C language"
There are 4 command-line arguments:
argv[0] = ./args
argv[1] = hello
argv[2] = 123
argv[3] = C language
  • argc (argument count) is the number of arguments, and the program's name counts as one;
  • argv (argument vector) is an array of pointers; each element points to one argument string, and argv[0] is the program's name;
  • Arguments are separated by spaces, and an argument containing spaces must be wrapped in quotes;
  • Arguments always arrive as strings; if you need numbers, convert them yourself (for example, with strtol).

Many command-line tools (such as gcc hello.c -o hello) receive their arguments this way. The file-copying program in Chapter 14 uses this too.

10.12 Function pointers

Functions live in memory too, so they also have addresses. A function pointer is a pointer that holds a function's address, and through it you can call the function indirectly.

c
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int main(void)
{
    int (*op)(int, int);    // op is a pointer to a function that takes two ints and returns an int
    op = add;               // a function's name stands for the function's address
    printf("add: %d\n", op(6, 3));
    op = mul;
    printf("mul: %d\n", op(6, 3));

    // an array of function pointers: pick the function to call by index
    int (*ops[])(int, int) = {add, sub, mul};
    const char *symbols[] = {"+", "-", "*"};
    for (int i = 0; i < 3; i++) {
        printf("6 %s 3 = %d\n", symbols[i], ops[i](6, 3));
    }
    return 0;
}

Output:

text
add: 9
mul: 18
6 + 3 = 9
6 - 3 = 3
6 * 3 = 18

The declaration syntax for function pointers is rather convoluted, but you can give the type an alias with typedef:

c
typedef int (*BinaryOp)(int, int);   // BinaryOp stands for "pointer to this kind of function"
BinaryOp op = add;

Callbacks and qsort. The most important use of function pointers is the callback: you pass one function as an argument to another, and the second one calls it at the right moment. The standard library's sorting function qsort is the classic example. Its declaration is:

c
void qsort(void *base, size_t count, size_t size,
           int (*compare)(const void *, const void *));

Its four parameters are the address of the array's first element, the number of elements, the size of each element, and a comparison function. qsort knows how to sort, but it doesn't know how to compare your data; you tell it the rule through the comparison function. The comparison function receives the addresses of two elements and returns a negative number if the first should come first, a positive number if it should come later, and 0 if they're equal.

c
#include <stdio.h>
#include <stdlib.h>

int compare_asc(const void *a, const void *b)
{
    int x = *(const int *)a;     // convert the void * to an int * first, then dereference
    int y = *(const int *)b;
    return (x > y) - (x < y);    // 1 if x > y, -1 if x < y, 0 if they're equal
}

int compare_desc(const void *a, const void *b)
{
    return compare_asc(b, a);    // swapping the arguments gives descending order
}

void print_array(const int *arr, int n)
{
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void)
{
    int a[] = {42, 7, 19, 88, 3, 56};
    int n = sizeof(a) / sizeof(a[0]);

    qsort(a, n, sizeof(a[0]), compare_asc);
    printf("Ascending:  ");
    print_array(a, n);

    qsort(a, n, sizeof(a[0]), compare_desc);
    printf("Descending: ");
    print_array(a, n);
    return 0;
}

Output:

text
Ascending:  3 7 19 42 56 88
Descending: 88 56 42 19 7 3
Caution
Many resources write the comparison function as return x - y;. When x and y are far apart (say, one is close to INT_MAX and the other is negative), the subtraction overflows and the sort comes out wrong. Writing (x > y) - (x < y) can never overflow.

10.13 void pointers

void * is a "generic pointer": it can hold the address of data of any type, any object pointer can be assigned to it directly, and it can be assigned directly to any other type of object pointer. But it doesn't know what type of data it points to, so you can't dereference it directly or do arithmetic on it; you have to convert it to a pointer of a specific type first, just like *(const int *)a in the comparison function above.

qsort, memcpy, memset, and malloc (Chapter 12) all use void *, which lets them work with data of any type.

10.14 Keys to mastering pointers

  1. Draw pictures! Draw a box for every variable and write its value inside; for a pointer, draw an arrow to its target. Once the picture is drawn, the vast majority of pointer problems become clear.
  2. Always keep two things apart: the value of the pointer variable itself (an address), and the value it points to (*p).
  3. Remember the common pointer mistakes:
MistakeConsequencePrevention
Using an uninitialized (wild) pointerReads and writes random memory; crashes or corrupts dataInitialize pointers when you define them; use NULL if you don't know where they should point yet
Dereferencing a null pointerThe program crashes (segmentation fault)Check for NULL before using the pointer
Returning the address of a local variableA dangling pointer; the data can be overwritten at any momentNever return the address of a local variable
Going out of an array's bounds through a pointerSame as going out of bounds with an indexCheck boundary conditions carefully
Modifying a string literalUndefined behavior; usually a crashPoint to string literals with const char *
Mismatched pointer typesBoth the number of bytes accessed and their interpretation are wrongTake the compiler's type warnings seriously, and be careful with casts

Exercises

  1. Minimum and maximum. Write a function void min_max(const int *arr, int n, int *min, int *max) that "returns" both the smallest and the largest value of an array through pointer parameters.
  2. Reverse with pointers. Write a function void reverse_array(int *arr, int n) that reverses an array using only pointer arithmetic (no [] subscripts).
  3. Analyze the output. Without running it, write down what the following code prints, then run it to check:
c
   int arr[] = {10, 20, 30, 40};
   int *p = arr;
   printf("%d ", *p++);
   printf("%d ", *p);
   printf("%d ", (*p)++);
   printf("%d ", *p);
   printf("%d ", *++p);
   printf("%d\n", (int)(p - arr));
   printf("%d %d %d %d\n", arr[0], arr[1], arr[2], arr[3]);
  1. Sorting strings. Use qsort to sort the array of pointers const char *words[] = {"banana", "apple", "cherry", "date", "fig"}; into dictionary order, and print the result. (Hint: the array's elements have type const char *, so the comparison function actually receives the addresses of const char * values.)

Part III: Going Further

Goal of this part: learn to organize complex data (structures), request memory on demand while the program runs (dynamic memory), build your first data structure (linked lists), save data to disk (files), and master engineering skills such as multi-file programs, bitwise operations, debugging, and coding conventions. By the end of this part, you'll be able to write genuinely useful programs.

Chapter 11: Structures, Unions, and Enumerations

11.1 Why structures?

Describing a student takes several pieces of information: an ID (an integer), a name (a string), an age (an integer), a score (a floating-point number), and so on. They have different types, so they can't go into a single array; and if you define them as separate variables or arrays, it's hard to show that "these pieces of information belong to the same person."

A structure (struct) lets us combine several pieces of data of different types into one unit, in effect defining a new data type.

11.2 Defining and using structures

c
#include <stdio.h>
#include <string.h>

struct Student {        // defines a new type named struct Student
    int id;             // member: student ID
    char name[20];      // member: name
    int age;            // member: age
    double score;       // member: score
};                      // note: the semicolon at the end is required

int main(void)
{
    struct Student s1 = {1001, "Alice", 19, 88.5};     // initialized in member order
    struct Student s2 = {.name = "Bob", .id = 1002};   // C99 designated initializers; members not listed become 0

    s2.age = 20;                     // use "." to access a member
    s2.score = 92.0;
    strcpy(s1.name, "Alicia");       // an array member can't be assigned with =

    printf("%d %s %d %.1f\n", s1.id, s1.name, s1.age, s1.score);
    printf("%d %s %d %.1f\n", s2.id, s2.name, s2.age, s2.score);

    struct Student s3 = s1;          // structures can be assigned as a whole: every member, arrays included, is copied
    s3.id = 1003;
    printf("%d %s %d %.1f\n", s3.id, s3.name, s3.age, s3.score);
    return 0;
}

Output:

text
1001 Alicia 19 88.5
1002 Bob 20 92.0
1003 Alicia 19 88.5

The key points:

  • struct Student { ... }; only defines a type, like a blank form template; it takes up no memory. The variables defined with it (s1, s2) are what actually occupy memory.
  • Members are accessed with the member operator ., as in s1.score. Each member can be used exactly like an ordinary variable of the same type; for example, you read one with scanf as scanf("%lf", &s1.score);.
  • Structure variables can be assigned as a whole (unlike arrays), but can't be compared with `==`; to compare two structures, you have to compare them member by member.

11.3 Simplifying type names with typedef

Writing struct Student every time is a bit tedious, so you can give the type an alias with typedef:

c
typedef struct {
    int id;
    char name[20];
    double score;
} Student;              // Student is now the name of this structure type

Student s = {1001, "Alice", 88.5};   // no need to write struct anymore

typedef gives an existing type a new name, and it works with other types too, as in typedef unsigned int uint;. In the code for the rest of this guide, structure type names are usually capitalized (such as Student) to set them apart from variable names.

11.4 Pointers to structures and the arrow operator

Pointers to structures are used all the time. Accessing a member through a pointer could be written (*p).score (the parentheses are required, because . has higher precedence than *), but that's so clumsy that C provides the arrow operator ->: p->score is exactly the same as (*p).score.

Structures as function parameters. A structure can be passed by value, but then the whole structure is copied, which costs a lot when the structure is large, and the function can't modify the original. So you usually pass a pointer to the structure, adding const if the function doesn't need to modify it.

c
#include <stdio.h>

typedef struct {
    char name[20];
    int age;
    double score;
} Student;

// Pointer + const: efficient, and guarantees the data won't be modified
void print_student(const Student *s)
{
    printf("Name: %s, age: %d, score: %.1f\n", s->name, s->age, s->score);
}

// Pointer: can modify the caller's structure
void add_bonus(Student *s, double bonus)
{
    s->score += bonus;
    if (s->score > 100) {
        s->score = 100;
    }
}

// By value: the function gets a copy, so its changes don't affect the original
void try_modify(Student s)
{
    s.age = 99;
    printf("Age inside try_modify: %d\n", s.age);
}

int main(void)
{
    Student stu = {"Carol", 18, 95.0};
    Student *p = &stu;

    printf("%s %d\n", (*p).name, p->age);
    add_bonus(&stu, 8);
    try_modify(stu);
    print_student(&stu);
    return 0;
}

Output:

text
Carol 18
Age inside try_modify: 99
Name: Carol, age: 18, score: 100.0

add_bonus successfully changed the score. try_modify did set the age to 99 inside the function, but it changed only a copy; the age in the original structure is still 18.

11.5 Arrays of structures

An array of structures can hold a set of records, like a table: each element is a row, and each member is a column. The program below uses qsort to sort players by score, from highest to lowest:

c
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char name[20];
    int score;
} Player;

int compare_by_score_desc(const void *a, const void *b)
{
    const Player *pa = a;      // a void * can be assigned directly to other pointer types
    const Player *pb = b;
    return (pb->score > pa->score) - (pb->score < pa->score);
}

int main(void)
{
    Player players[] = {
        {"Alice", 820},
        {"Bob", 950},
        {"Carol", 760},
        {"David", 910},
        {"Eve", 880}
    };
    int n = sizeof(players) / sizeof(players[0]);

    qsort(players, n, sizeof(Player), compare_by_score_desc);

    printf("Leaderboard:\n");
    for (int i = 0; i < n; i++) {
        printf("%d. %-6s %d\n", i + 1, players[i].name, players[i].score);
    }
    return 0;
}

Output:

text
Leaderboard:
1. Bob    950
2. David  910
3. Eve    880
4. Alice  820
5. Carol  760

qsort can sort an array of any type; all you have to do is tell it the comparison rule in the comparison function. To sort by name instead, for example, change the comparison function to return strcmp(pa->name, pb->name);.

Caution
qsort isn't guaranteed to be "stable": when two elements compare as equal, the order they end up in is unspecified. If you need "players with equal scores sorted by name," write that rule into the comparison function.

11.6 Nested structures

A member of a structure can itself be another structure:

c
typedef struct {
    int year;
    int month;
    int day;
} Date;

typedef struct {
    char name[20];
    Date birthday;          // this member is another structure
} Person;

Person p = {"David", {2005, 3, 15}};
printf("%s was born in %d\n", p.name, p.birthday.year);   // use . level by level

11.7 Structure alignment in memory

The memory a structure takes up isn't necessarily the sum of its members' sizes:

c
#include <stdio.h>
#include <stddef.h>

struct A {
    char c1;
    int i;
    char c2;
};

struct B {
    char c1;
    char c2;
    int i;
};

int main(void)
{
    printf("sizeof(struct A) = %zu\n", sizeof(struct A));
    printf("sizeof(struct B) = %zu\n", sizeof(struct B));
    printf("Member offsets in A: c1 = %zu, i = %zu, c2 = %zu\n",
           offsetof(struct A, c1), offsetof(struct A, i), offsetof(struct A, c2));
    return 0;
}

Output (on mainstream 64-bit systems):

text
sizeof(struct A) = 12
sizeof(struct B) = 8
Member offsets in A: c1 = 0, i = 4, c2 = 8

The members are identical and only their order differs, yet the sizes differ by 4 bytes. The reason is memory alignment: a CPU reads data most efficiently when the data's address is a multiple of its size (for example, a 4-byte int at an address that's a multiple of 4), and on some platforms this is even mandatory. So the compiler inserts padding bytes between members. The usual rules are:

  1. Each member's offset must be a multiple of that member's alignment requirement. For basic types such as char, int, and double, the alignment requirement is usually the type's size; an array has the same alignment requirement as its element type, so char name[20], for example, needs only 1-byte alignment;
  2. The structure's total size must be a multiple of the largest alignment requirement among its members (so that every element of an array of such structures is aligned too).

Here's the layout of struct A (.. marks padding bytes):

text
offset:  0    1    2    3    4    5    6    7    8    9   10   11
       +----+----+----+----+----+----+----+----+----+----+----+----+
       | c1 | .. | .. | .. | i  | i  | i  | i  | c2 | .. | .. | .. |
       +----+----+----+----+----+----+----+----+----+----+----+----+

After c1, 3 bytes of padding make i start at a multiple of 4; after c2, 3 more bytes of padding make the total size a multiple of 4. In struct B, the two chars sit side by side, and only 2 bytes of padding are needed after them.

Practical advice: when defining a structure, order its members from largest to smallest to reduce padding and save memory. The offsetof macro (in <stddef.h>) tells you a member's offset.

11.8 Enumerations

An enumeration (enum) defines a set of named integer constants. It's ideal for representing "one of a limited number of values," such as days of the week, colors, or states:

c
#include <stdio.h>

enum Weekday { MON = 1, TUE, WED, THU, FRI, SAT, SUN };   // TUE is 2, and each after that adds 1

int main(void)
{
    enum Weekday today = SAT;
    printf("Today is day %d of the week\n", today);

    switch (today) {
    case SAT:
    case SUN:
        printf("Enjoy your weekend!\n");
        break;
    default:
        printf("It's a weekday. Keep going!\n");
        break;
    }
    return 0;
}

Output:

text
Today is day 6 of the week
Enjoy your weekend!
  • If you don't specify values, the first enumeration constant is 0 and each one after it is 1 greater; you can also give any constant a specific value.
  • Enumeration constants are really just integers: they can take part in arithmetic and be used as case labels in a switch.
  • Compared with a group of macro constants defined with #define, an enumeration keeps related constants together, makes their meaning clearer, and lets a debugger show the constants' names.

You can shorten an enumeration's type name with typedef as well: typedef enum { RED, GREEN, BLUE } Color;.

11.9 Unions

A union is written much like a structure, but all of its members share the same block of memory, and its size is that of its largest member (plus any alignment padding). At any moment, only the member written last holds a meaningful value.

c
#include <stdio.h>

union Data {
    int i;
    float f;
    char str[8];
};

int main(void)
{
    union Data d;
    printf("sizeof(union Data) = %zu\n", sizeof(d));

    d.i = 42;
    printf("d.i = %d\n", d.i);
    d.f = 3.14f;                   // writing f overwrites the memory i lives in
    printf("d.f = %.2f\n", d.f);
    printf("d.i = %d (overwritten)\n", d.i);
    return 0;
}

Output:

text
sizeof(union Data) = 8
d.i = 42
d.f = 3.14
d.i = 1078523331 (overwritten)

After d.f is written, those 4 bytes hold the binary representation of the floating-point number 3.14; read them back as an integer and you get a "baffling" number.

A typical use of unions is together with an enumeration, forming a "tagged union": an enumeration member records which kind of data is currently stored, and the code uses it to decide which member to access. That way, one variable can hold any one of several types of data:

c
#include <stdio.h>

typedef enum { SHAPE_CIRCLE, SHAPE_RECT } ShapeType;

typedef struct {
    ShapeType type;                       // the tag: records which kind of shape this is
    union {
        struct { double radius; } circle;
        struct { double width, height; } rect;
    } data;
} Shape;

double area(const Shape *s)
{
    switch (s->type) {
    case SHAPE_CIRCLE:
        return 3.14159265358979 * s->data.circle.radius * s->data.circle.radius;
    case SHAPE_RECT:
        return s->data.rect.width * s->data.rect.height;
    }
    return 0.0;
}

int main(void)
{
    Shape shapes[2];
    shapes[0].type = SHAPE_CIRCLE;
    shapes[0].data.circle.radius = 1.0;
    shapes[1].type = SHAPE_RECT;
    shapes[1].data.rect.width = 3.0;
    shapes[1].data.rect.height = 4.0;

    for (int i = 0; i < 2; i++) {
        printf("Area of shape %d: %.2f\n", i + 1, area(&shapes[i]));
    }
    return 0;
}

Output:

text
Area of shape 1: 3.14
Area of shape 2: 12.00

11.10 Bit-fields (Good to know)

In memory-sensitive settings such as embedded development, you can use bit-fields to specify that a structure member occupies only a certain number of bits:

c
struct Flags {
    unsigned int is_read  : 1;   // takes just 1 bit: the value is 0 or 1
    unsigned int is_write : 1;
    unsigned int level    : 4;   // takes 4 bits: the value is 0-15
};

The exact memory layout of bit-fields is up to the compiler and isn't portable between platforms. For most programs, the "bitwise operations + flags" approach in Chapter 16 is the more general choice.

11.11 Putting it together: dealing cards

Here's a small program that shuffles and deals a deck of cards, using enumerations, structures, arrays, and random numbers:

c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef enum { SPADES, HEARTS, CLUBS, DIAMONDS } Suit;

typedef struct {
    Suit suit;
    int rank;          // 1-13, standing for A, 2-10, J, Q, K
} Card;

const char *suit_name(Suit s)
{
    switch (s) {
    case SPADES:   return "Spades";
    case HEARTS:   return "Hearts";
    case CLUBS:    return "Clubs";
    case DIAMONDS: return "Diamonds";
    }
    return "?";
}

void print_card(Card c)
{
    const char *ranks[] = {"", "A", "2", "3", "4", "5", "6", "7",
                           "8", "9", "10", "J", "Q", "K"};
    printf("%s of %s", ranks[c.rank], suit_name(c.suit));
}

int main(void)
{
    Card deck[52];
    int k = 0;
    for (int s = SPADES; s <= DIAMONDS; s++) {
        for (int r = 1; r <= 13; r++) {
            deck[k].suit = (Suit)s;
            deck[k].rank = r;
            k++;
        }
    }

    // Fisher-Yates shuffle: going from the back, swap each card with a random card at or before it
    srand((unsigned int)time(NULL));
    for (int i = 51; i > 0; i--) {
        int j = rand() % (i + 1);
        Card temp = deck[i];
        deck[i] = deck[j];
        deck[j] = temp;
    }

    printf("Your hand: ");
    for (int i = 0; i < 5; i++) {
        if (i > 0) {
            printf(", ");
        }
        print_card(deck[i]);
    }
    printf("\n");
    return 0;
}

A possible run:

text
Your hand: 7 of Hearts, K of Spades, A of Diamonds, 10 of Clubs, Q of Hearts

The Fisher-Yates algorithm guarantees that each of the 52! possible orderings is equally likely (assuming the random numbers are good enough), which makes it the standard way to shuffle.

Exercises

  1. Day of the year. Define a date structure Date (with year, month, and day), and write a function int day_of_year(const Date *d) that works out which day of the year the date is (watch out for leap years). For example, March 1, 2024, is day 61.
  2. Complex numbers. Define a structure Complex for complex numbers (with real and imaginary parts), and write functions that add and multiply two complex numbers. Hint: (a+bi)(c+di) = (ac−bd) + (ad+bc)i.
  3. Sorting books. Define a book structure (title, author, price), initialize the information for 5 books in your program, sort them by price from highest to lowest, and print them.
  4. Something to think about. On a common 64-bit system, what are the sizeof values of the two structures below? Work them out yourself first, then write a program to check.
c
   struct X { char a; double b; int c; };
   struct Y { double b; int c; char a; };

Chapter 12: Dynamic Memory Management

So far, the sizes of all our variables and arrays have had to be decided when writing the code. But often, the amount of data isn't known until the program runs: how many numbers will the user enter? How many records are in the file? Dynamic memory management lets a program request memory as it needs it while running, and give it back when it's done.

12.1 A program's memory layout

The memory of a running C program is divided roughly into the following regions (this is a simplified, typical layout; the details vary between systems):

text
high addresses +--------------------------+
               |          stack           |  locals and parameters; managed automatically; small
               |            ↓             |
               |                          |
               |            ↑             |
               |           heap           |  malloc and friends; managed by hand; large
               +--------------------------+
               |    global/static data    |  globals and statics; live for the whole run
               +--------------------------+
               |      read-only data      |  string literals and other constants
               +--------------------------+
               |           code           |  the program's machine instructions
low addresses  +--------------------------+

The stack and the heap are the two most important regions. Here's how they compare:

StackHeap
What's stored thereLocal variables, function parametersMemory requested with malloc and friends
Who manages itThe compiler, which allocates and frees it automaticallyThe programmer, who requests and frees it by hand
LifetimeEnds when the function it belongs to returnsFrom the request until free is called
SizeFairly small (usually 8 MB on Linux, 1 MB on Windows)Large, limited by the memory available in the system
When the size is decidedUsually at compile time (variable-length arrays are the exception)At run time

You need heap memory when:

  • The size of the data isn't known until the program runs;
  • The data needs to outlive the function that creates it;
  • There's a lot of data, too much to fit on the stack (such as an array of ten million elements).

12.2 malloc and free

The malloc function in <stdlib.h> requests memory, and the free function releases it:

c
void *malloc(size_t size);   // requests size bytes; returns the starting address on success, NULL on failure
void free(void *ptr);        // releases the memory ptr points to, which must have come from malloc, calloc, or realloc
c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int n;
    printf("How many integers? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Invalid input\n");
        return 1;
    }

    int *arr = malloc(n * sizeof(int));   // request enough memory for n ints
    if (arr == NULL) {                    // the request can fail, so always check
        printf("Out of memory!\n");
        return 1;
    }

    printf("Enter %d integers: ", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);             // use it just like an ordinary array
    }

    printf("In reverse: ");
    for (int i = n - 1; i >= 0; i--) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);                            // release it when you're done
    arr = NULL;                           // then set the pointer to NULL to prevent accidental use
    return 0;
}

Output:

text
How many integers? 5
Enter 5 integers: 1 2 3 4 5
In reverse: 5 4 3 2 1

Key points for using malloc and free:

  1. Get the number of bytes right. malloc takes a number of bytes, not a number of elements, so multiply by the size of each element. An even better way to write it is malloc(n * sizeof *arr): using *arr instead of the type name means that if the type of arr ever changes, this line doesn't have to change with it.
  2. Always check whether the return value is `NULL`. When memory runs out, malloc returns NULL, and using it directly crashes the program.
  3. Newly allocated memory isn't initialized; it contains garbage values.
  4. What you borrow, you must return. Every successful malloc must be matched by exactly one free. Forgetting to free memory causes a memory leak.
  5. After freeing, set the pointer to `NULL`. After free, the pointer variable still holds the old address, but that memory no longer belongs to you (this is the dangling pointer from Chapter 10). If the pointer is NULL, any accidental use crashes the program immediately, which is much easier to notice than quietly reading or writing invalid memory. Also, free(NULL) is safe; it does nothing.
  6. In C, the void * returned by malloc can be assigned directly to any object pointer, so you don't need to write (int *)malloc(...) (in C++, the cast is required).

12.3 calloc and realloc

`calloc(count, size_of_each)`: requests memory and sets every byte to 0.

c
int *scores = calloc(100, sizeof *scores);   // 100 ints, all 0

`realloc(old_pointer, new_size_in_bytes)`: resizes a block of memory that was already allocated. When growing a block, it tries to extend it in place; if there isn't enough room, it finds a new block that's big enough, copies the old data over, and frees the old block. So the address realloc returns may differ from the original.

realloc comes with a classic trap:

c
p = realloc(p, new_size);          // dangerous!

If realloc fails, it returns NULL, but the original memory is not freed. Written this way, p becomes NULL, the address of the original memory is lost, and you have a memory leak. The correct way is to receive the result in a temporary pointer first:

c
int *temp = realloc(p, new_size);
if (temp == NULL) {
    // growing failed, but p is still valid: keep using it, or free it
    free(p);
    return 1;
}
p = temp;                           // update p only after success

Also, realloc(NULL, size) is equivalent to malloc(size).

12.4 In practice: a dynamic array that grows by itself

Combining a structure with realloc, we can implement an array that "grows up on its own": there's no longer a limit on the number of elements, and it expands automatically when it fills up.

c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int *data;          // points to the elements, stored on the heap
    size_t size;        // how many elements there are now
    size_t capacity;    // current capacity (how many elements fit)
} IntVector;

void vec_init(IntVector *v)
{
    v->data = NULL;
    v->size = 0;
    v->capacity = 0;
}

// Appends an element at the end; returns true on success, false if memory runs out
bool vec_push(IntVector *v, int value)
{
    if (v->size == v->capacity) {                     // full: time to grow
        size_t new_cap = (v->capacity == 0) ? 4 : v->capacity * 2;
        int *new_data = realloc(v->data, new_cap * sizeof *new_data);
        if (new_data == NULL) {
            return false;                             // growing failed; the existing data is still intact
        }
        v->data = new_data;
        v->capacity = new_cap;
        printf("  [grow] capacity is now %zu\n", new_cap);
    }
    v->data[v->size] = value;
    v->size++;
    return true;
}

void vec_free(IntVector *v)
{
    free(v->data);
    vec_init(v);                                      // back to the empty state
}

int main(void)
{
    IntVector v;
    vec_init(&v);

    printf("Enter some integers, ending with 0:\n");
    int x;
    while (scanf("%d", &x) == 1 && x != 0) {
        if (!vec_push(&v, x)) {
            printf("Out of memory!\n");
            break;
        }
    }

    long long sum = 0;
    for (size_t i = 0; i < v.size; i++) {
        sum += v.data[i];
    }
    printf("Read %zu numbers; their sum is %lld\n", v.size, sum);
    if (v.size > 0) {
        printf("Their average is %.2f\n", (double)sum / v.size);
    }

    vec_free(&v);
    return 0;
}

Output:

text
Enter some integers, ending with 0:
3 8 1 9 4 7 2 6 5 10 0
  [grow] capacity is now 4
  [grow] capacity is now 8
  [grow] capacity is now 16
Read 10 numbers; their sum is 55
Their average is 5.50

Each time it grows, the capacity doubles rather than increasing by 1. Every growth step may copy all the data, so if the capacity grew by 1 each time, adding n elements would take about n²/2 copies. With doubling, growth happens only rarely, and on average each added element costs only a constant number of copies. C++'s vector, Java's ArrayList, and Python's list all use a similar "grow by a factor" strategy internally.

This example also shows a good way to organize code: keep the data (the structure) together with the functions that operate on it (vec_init, vec_push, vec_free), so that users only call the functions and never need to know the internal details. The capstone project in Chapter 18 follows the same style.

12.5 Allocating memory inside a function

Chapter 10 said you must never return the address of a local variable, because local variables are destroyed when their function returns. But the lifetime of heap memory isn't tied to any function: it stays valid until it's freed. So a function can allocate heap memory and hand its address to the caller. There are two common ways to do this:

c
// Way 1: hand over the newly allocated memory through the return value
int *create_array(size_t n, int init_value)
{
    int *arr = malloc(n * sizeof *arr);
    if (arr != NULL) {
        for (size_t i = 0; i < n; i++) {
            arr[i] = init_value;
        }
    }
    return arr;          // the caller checks for NULL, and frees the memory when done
}

// Way 2: hand over the newly allocated memory through a double-pointer parameter,
// and use the return value to report success or failure
bool create_zero_array(size_t n, int **out)
{
    *out = calloc(n, sizeof **out);
    return *out != NULL;
}

To call the second one, pass the address of a pointer variable: int *p; if (create_zero_array(10, &p)) { ... free(p); }.

Here's a common mistake:

c
void wrong_alloc(int *p, size_t n)
{
    p = malloc(n * sizeof *p);   // changes only the parameter p; the caller's pointer is unaffected
}                                // and this block's address is lost: a memory leak

This fails for exactly the same reason swap failed in Chapter 7: to change the caller's pointer variable in a function, you must pass its address, that is, a double pointer.

Tip
When a function allocates memory and hands it to the caller to free, always say "the caller is responsible for freeing it" in the function's comments or documentation. Being clear about "who allocates and who frees" is the key to avoiding memory leaks.

12.6 Dynamic two-dimensional arrays

When you need a two-dimensional array whose size isn't known until run time, the simplest approach is to allocate one contiguous block of rows × cols elements and use i * cols + j to work out the index of the element in row i, column j:

c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int rows = 3, cols = 4;
    int *m = malloc((size_t)rows * cols * sizeof *m);
    if (m == NULL) {
        return 1;
    }

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            m[i * cols + j] = i * 10 + j;    // row i, column j
        }
    }
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%4d", m[i * cols + j]);
        }
        printf("\n");
    }

    free(m);
    return 0;
}

Output:

text
   0   1   2   3
  10  11  12  13
  20  21  22  23

This approach needs just one malloc and one free, and the memory is contiguous, which is efficient. Another approach is to first allocate an "array of pointers" and then allocate each row separately; that lets you access elements as m[i][j], but it takes several allocations and frees. It's left as an exercise for this chapter.

12.7 Common memory errors

Dynamic memory is where C programs' bugs cluster. Burn these errors into your memory:

ErrorExampleConsequence
Memory leakAllocating and forgetting to freeThe program uses more and more memory; a long-running program eventually runs out
Losing the pointerp = malloc(...); p = malloc(...);The first block's address is lost and can never be freed
Using freed memoryfree(p); printf("%d", *p);Undefined behavior: you may read garbage or corrupt other data
Freeing twicefree(p); free(p);Undefined behavior; usually a crash
Out-of-bounds accessAllocating 5 elements and accessing a 6thCorrupts the heap's internal bookkeeping; often crashes in strange ways much later
Freeing memory not from the heapint a; free(&a);Undefined behavior; usually a crash
Freeing an address other than the startfree(p + 1)Undefined behavior; usually a crash. free accepts only the exact address returned by malloc and friends, and it can't free just part of a block
Miscalculated sizemalloc(n) or malloc(sizeof(p))Not enough space was allocated, leading to out-of-bounds access
Not checking for NULLUsing malloc's return value directlyThe program crashes when memory runs out

Good habits for avoiding these errors:

  • Check for NULL immediately after allocating;
  • As you write the malloc, decide where the matching free will go;
  • Set the pointer to NULL right after free;
  • Keep "allocate" and "free" in pairs at the same level of the code (like vec_init and vec_free);
  • Let tools check for you (see the next section).

12.8 Memory-checking tools

Memory errors often don't crash the program right away. Instead they lie in wait, then cause bizarre problems in unrelated places, and they're very hard to track down by eye. Fortunately, there are tools made for exactly this.

AddressSanitizer (ASan) is a memory-error detector built into GCC and Clang; to use it, just add -fsanitize=address to the compile command. Take this program, which writes out of bounds:

c
#include <stdlib.h>

int main(void)
{
    int *arr = malloc(5 * sizeof *arr);
    if (arr == NULL) {
        return 1;
    }
    for (int i = 0; i <= 5; i++) {    // wrong: i reaches 5, which is out of bounds
        arr[i] = i;
    }
    free(arr);
    return 0;
}
text
$ gcc -g -fsanitize=address overflow.c -o overflow
$ ./overflow
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000024 ...
WRITE of size 4 at 0x503000000024 thread T0
    #0 0x... in main overflow.c:10
    ...
0x503000000024 is located 0 bytes after 20-byte region [0x503000000010,0x503000000024)
allocated by thread T0 here:
    #0 0x... in malloc ...
    #1 0x... in main overflow.c:5

ASan pinpoints the problem precisely: a heap buffer overflow on line 10, a 4-byte write, just past the end of the 20-byte block allocated on line 5. On Linux, ASan also reports memory leaks when the program finishes.

Valgrind is another commonly used memory checker on Linux. It doesn't require recompiling (though -g is recommended so that it can show line numbers):

text
$ valgrind --leak-check=full ./program

It reports detailed information about illegal reads and writes (Invalid read, Invalid write) and memory leaks (definitely lost and so on).

Tip
While you're learning, get into the habit of compiling a debug build with -fsanitize=address,undefined (undefined also detects some kinds of undefined behavior, such as signed integer overflow). ASan works out of the box on Linux and macOS, and Visual Studio on Windows supports it too, through the /fsanitize=address option.

Exercises

  1. A dynamic average. First read how many integers there will be (n), then use malloc to allocate room for n integers. After reading them, compute their average and how many of them are above the average, and finally free the memory.
  2. Implement strdup. Write a function char *my_strdup(const char *s) that allocates a block of memory just big enough to hold the string s, copies s into it, and returns the new string's address (the caller is responsible for freeing it).
  3. Dynamic two-dimensional arrays. Read a number of rows m and columns n, and create an m × n two-dimensional int array by allocating "an array of pointers plus each row separately," so that elements can be accessed as a[i][j]. Fill in some data (such as a[i][j] = i * j), print it, and finally free all the memory correctly.
  4. Spot the bugs. Find all the memory errors in the code below, explain their consequences, and fix them:
c
   #include <stdio.h>
   #include <stdlib.h>
   #include <string.h>

   char *make_greeting(const char *name)
   {
       char *buf = malloc(strlen("Hello, ") + strlen(name));
       strcpy(buf, "Hello, ");
       strcat(buf, name);
       return buf;
   }

   int main(void)
   {
       char *s = make_greeting("Tom");
       printf("%s\n", s);
       free(s);
       printf("%s\n", s);

       int *nums = malloc(10 * sizeof(int));
       nums = malloc(20 * sizeof(int));
       free(nums);
       free(nums);
       return 0;
   }

Chapter 13: Linked Lists: Your First Data Structure

With structures, pointers, and dynamic memory in your toolbox, you can build real data structures. The linked list is the most basic and classic dynamic data structure, and it's the starting point for learning stacks, queues, trees, graphs, and more.

13.1 The limits of arrays

Arrays have two obvious limitations:

  1. Their size is fixed. You have to decide the size when you define an array: too big wastes memory, too small isn't enough. The dynamic array from Chapter 12 can grow, but every growth step may copy all the data.
  2. Inserting and deleting are expensive. To insert an element at the front of an array, you first have to shift every other element back one position, and deleting works the same way in reverse. The bigger the array, the more it costs.

13.2 What is a linked list?

A linked list is made of a series of nodes, each with two parts: a data field that holds the data, and a pointer field that points to the next node. The nodes don't have to be next to each other in memory; the pointers "link" them together. A head pointer remembers where the first node is, and the last node's pointer field is NULL, marking the end of the list.

text
 head
  │
  ▼
+----+---+     +----+---+     +----+------+
| 10 | ●─┼───▶ | 20 | ●─┼───▶ | 30 | NULL |
+----+---+     +----+---+     +----+------+
 data  pointer

A linked list is like a train: each car is hooked to the next. To add a car, you just unhook one connection and hook things up again, without moving any other cars. But to find the 100th car, you have no choice but to start at the engine and count the cars one by one.

ArrayLinked list
MemoryContiguousScattered, connected by pointers
Accessing the i-th elementFast: the address is calculated directly, O(1)Slow: you must walk from the start, O(n)
Inserting or deleting at a known positionSlow: later elements must be shifted, O(n)Fast: just change some pointers, O(1) (for a singly linked list, provided you already have a pointer to the node just before that position)
SizeFixed, or must grow as a wholeNodes can be added or removed at any time
Extra overheadNoneEvery node stores an extra pointer

Neither structure is better in every way; choose according to what you need: if you often access elements by position, use an array; if you often insert and delete, use a linked list.

13.3 Defining a node

c
typedef struct Node {
    int data;             // the data field
    struct Node *next;    // the pointer field: points to the next node
} Node;

Notice that inside the structure we write struct Node *next, not Node *next. Inside the structure, the alias Node created by typedef doesn't exist yet, so you have to use the full tagged name struct Node. A structure can contain a pointer to a structure of its own type (but not a structure of its own type, which would have to be infinitely large). Such structures are called self-referential structures.

An empty list is simply a head pointer that's NULL:

c
Node *head = NULL;

13.4 Basic linked-list operations

1. Traversal. Start a pointer cur at the first node, and follow next to the following node each time, until you reach NULL:

c
for (const Node *cur = head; cur != NULL; cur = cur->next) {
    printf("%d -> ", cur->data);
}

Almost every linked-list operation is built on this pattern.

2. Inserting at the front. This takes two steps:

text
before:    head ──▶ [10|●]──▶ [20|●]──▶ NULL

(1) node->next = head;     make the new node point to the current first node

           node ──▶ [5|●]──┐
                           ▼
           head ──▶ [10|●]──▶ [20|●]──▶ NULL

(2) head = node;           make the head pointer point to the new node

           head ──▶ [5|●]──▶ [10|●]──▶ [20|●]──▶ NULL

The two steps can't be done in the opposite order. If you did head = node first, the address of the original first node would be lost, and the whole list along with it.

Because inserting at the front has to change the caller's head pointer, the function's parameter is Node **head (a double pointer). This is exactly what Chapter 10 said: "to change a pointer inside a function, pass the address of that pointer."

3. Inserting at the back. First walk along the list to find the last node (the one whose next is NULL), then make it point to the new node. If the list is empty, the new node becomes the first node, and again the head pointer has to change.

4. Inserting after a given node. Same idea as inserting at the front: connect first, then disconnect:

c
node->next = pos->next;   // (1) the new node first points to the node after pos
pos->next = node;         // (2) then pos points to the new node

5. Deleting a node. To delete a node, you need to find the node before it, make that node "skip over" it, and then free it:

text
delete 20: prev points to 10, cur points to 20

   head ──▶ [10|●]──▶ [20|●]──▶ [30|●]──▶ NULL
             prev       cur

prev->next = cur->next;  free(cur);

   head ──▶ [10|●]──────────────▶ [30|●]──▶ NULL

If the node to delete is the first node (so there's no node before it), change the head pointer instead: *head = cur->next;.

6. Freeing the whole list. Free the nodes one at a time. You must save the address of the next node before freeing the current one, because once a node is freed, you can't access its next anymore.

13.5 The complete program

Let's put these operations together into one complete program:

c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Creates a new node
Node *create_node(int value)
{
    Node *node = malloc(sizeof *node);
    if (node == NULL) {
        fprintf(stderr, "Out of memory\n");
        exit(EXIT_FAILURE);             // memory is exhausted and we can't go on, so end the program
    }
    node->data = value;
    node->next = NULL;
    return node;
}

// Inserts at the front
void push_front(Node **head, int value)
{
    Node *node = create_node(value);
    node->next = *head;
    *head = node;
}

// Inserts at the back
void push_back(Node **head, int value)
{
    Node *node = create_node(value);
    if (*head == NULL) {                // empty list: the new node is the first node
        *head = node;
        return;
    }
    Node *cur = *head;
    while (cur->next != NULL) {         // find the last node
        cur = cur->next;
    }
    cur->next = node;
}

// Inserts a new node after the node pos
void insert_after(Node *pos, int value)
{
    Node *node = create_node(value);
    node->next = pos->next;
    pos->next = node;
}

// Finds the first node whose value is value; returns NULL if there isn't one
Node *find(Node *head, int value)
{
    for (Node *cur = head; cur != NULL; cur = cur->next) {
        if (cur->data == value) {
            return cur;
        }
    }
    return NULL;
}

// Deletes the first node whose value is value; returns true on success
bool remove_value(Node **head, int value)
{
    Node *prev = NULL;
    Node *cur = *head;
    while (cur != NULL && cur->data != value) {
        prev = cur;
        cur = cur->next;
    }
    if (cur == NULL) {                  // not found
        return false;
    }
    if (prev == NULL) {                 // deleting the first node
        *head = cur->next;
    } else {
        prev->next = cur->next;
    }
    free(cur);
    return true;
}

// Prints the list
void print_list(const Node *head)
{
    for (const Node *cur = head; cur != NULL; cur = cur->next) {
        printf("%d -> ", cur->data);
    }
    printf("NULL\n");
}

// Frees the whole list
void free_list(Node **head)
{
    Node *cur = *head;
    while (cur != NULL) {
        Node *next = cur->next;         // save the address of the next node first
        free(cur);                      // then free the current node
        cur = next;
    }
    *head = NULL;
}

int main(void)
{
    Node *head = NULL;

    for (int i = 1; i <= 5; i++) {
        push_back(&head, i * 10);
    }
    printf("push_back 10..50:   ");
    print_list(head);

    push_front(&head, 5);
    printf("push_front 5:       ");
    print_list(head);

    Node *p = find(head, 30);
    if (p != NULL) {
        insert_after(p, 35);
    }
    printf("insert 35 after 30: ");
    print_list(head);

    remove_value(&head, 5);
    remove_value(&head, 50);
    printf("remove 5 and 50:    ");
    print_list(head);

    free_list(&head);
    printf("after free_list:    ");
    print_list(head);
    return 0;
}

Output:

text
push_back 10..50:   10 -> 20 -> 30 -> 40 -> 50 -> NULL
push_front 5:       5 -> 10 -> 20 -> 30 -> 40 -> 50 -> NULL
insert 35 after 30: 5 -> 10 -> 20 -> 30 -> 35 -> 40 -> 50 -> NULL
remove 5 and 50:    10 -> 20 -> 30 -> 35 -> 40 -> NULL
after free_list:    NULL

Try compiling and running this program with -fsanitize=address to confirm there are no memory errors or leaks. Then deliberately change the body of the loop in free_list to free(cur); cur = cur->next; (free first, then read next) and see what AddressSanitizer reports.

Tip
When writing linked-list code, always think through these edge cases: an empty list, a list with just one node, an operation on the first node, and an operation on the last node. Most linked-list bugs hide in these spots. When in doubt, draw the nodes and arrows on paper and simulate the pointer changes step by step.

13.6 Lists with a dummy head node (Good to know)

In the implementation above, inserting or deleting the first node needs special handling (changing the head pointer). A common trick is to put a "dummy head node" (also called a sentinel node) at the very front of the list. It stores no real data, the head pointer always points to it, and the first real data node is head->next. Now every data node has a node in front of it, so insertion and deletion no longer need special cases. Many data structures textbooks use lists with a dummy head node by default.

13.7 Variations on the linked list

  • Doubly linked lists: each node has two pointers, one to the previous node and one to the next. You can traverse in both directions, and deleting a node no longer requires hunting for its predecessor; the cost is one more pointer per node.
text
  NULL ◀──[● | 10 | ●]◀──▶[● | 20 | ●]◀──▶[● | 30 | ●]──▶ NULL
  • Circular linked lists: the last node's next points back to the first node, forming a ring. They suit problems about "taking turns" or "counting off in a circle" (such as the Josephus problem).

Linked lists are also the foundation for other data structures: if you allow insertion and deletion only at one end, you have a stack (last in, first out); if you allow insertion at one end and deletion at the other, you have a queue (first in, first out). These belong to a data structures course, and Chapter 19 suggests how to continue learning them.

Exercises

  1. List length. Write functions, one with a loop and one with recursion, that count the nodes in a list.
  2. Reverse a list. Write a function Node *reverse_list(Node *head) that reverses a list in place (without creating new nodes) and returns the new head pointer. For example, 1 -> 2 -> 3 -> NULL becomes 3 -> 2 -> 1 -> NULL. This is one of the most frequently asked linked-list interview questions.
  3. Merging sorted lists. Given two lists whose elements are in increasing order, write a function that merges them into a single sorted list (by relinking the existing nodes, without creating new ones).
  4. The middle node. Write a function that returns the middle node of a list, walking through the list only once. When there's an even number of nodes, return the second of the two middle nodes. (Hint: use two pointers, one that moves one step at a time and one that moves two.)

Chapter 14: Working with Files

While a program runs, its data lives in memory, and when the program ends, the data is gone. To make data "live" longer—saving a game's progress, recording students' grades, writing a log—you need to store it on disk in files.

14.1 Files and streams

Text files and binary files. Files come in two kinds, depending on how their contents are stored:

  • Text files: the contents are characters that you can view directly in a text editor such as Notepad; .txt, .c, and .csv files, for example. The integer 12345 stored as text is the 5 characters '1' '2' '3' '4' '5', taking 5 bytes.
  • Binary files: store data exactly as it's represented in memory, and look like gibberish in a text editor; images, music, and executable programs, for example. The integer 12345 stored in binary is simply the 4 bytes of an int.

Streams. C treats every input and output device (keyboard, screen, file) as a stream: an ordered sequence of bytes. A program works with a stream through a pointer of type FILE *. When a program starts, three standard streams are opened automatically:

Standard streamMeaningDefault device
stdinStandard inputThe keyboard
stdoutStandard outputThe screen
stderrStandard errorThe screen

printf(...) is really shorthand for fprintf(stdout, ...). It's best to send error messages to stderr: that way, even if the normal output is redirected to a file, the error messages still show up on the screen.

Line-ending differences. Linux and macOS end lines with \n, while Windows text files end lines with the two characters \r\n. When you read or write a file in text mode on Windows, the C library converts automatically: writing \n produces \r\n, and reading converts it back to \n. In binary mode, no conversion happens.

14.2 Opening and closing files

The basic workflow for using a file is: open → read/write → close.

c
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("hello.txt", "w");      // open for writing; create the file if it doesn't exist
    if (fp == NULL) {
        perror("Could not open the file");
        return 1;
    }
    fprintf(fp, "Hello, File!\n");
    fprintf(fp, "Today is %d-%02d-%02d\n", 2026, 9, 26);
    fclose(fp);                               // close the file

    fp = fopen("hello.txt", "r");             // reopen it for reading
    if (fp == NULL) {
        perror("Could not open the file");
        return 1;
    }
    char line[100];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("Read: %s", line);
    }
    fclose(fp);
    return 0;
}

Output:

text
Read: Hello, File!
Read: Today is 2026-09-26

After it runs, a new file named hello.txt appears in the current working directory (usually the folder you ran the program from; see "About file paths" below); you can open it in a text editor to have a look.

`fopen(filename, mode)` opens a file. On success it returns a FILE * pointer; on failure (for example, if the file doesn't exist or you don't have permission) it returns NULL. Always check the return value after opening a file. The common modes are:

ModeMeaningIf the file doesn't existIf the file exists
"r"Read onlyOpening failsReading starts at the beginning
"w"Write onlyA new file is createdIts contents are erased!
"a"AppendA new file is createdWriting continues at the end
"r+"Read and writeOpening failsReading and writing start at the beginning
"w+"Read and writeA new file is createdIts contents are erased!
"a+"Read and appendA new file is createdYou can read; writes always go at the end

Adding b to a mode (as in "rb" or "wb") opens the file in binary mode.

Common mistake
Opening an existing file with mode "w" erases its contents immediately, even if you never write a single byte. If you just want to add to the end of the file, use mode "a".

`fclose(fp)` closes a file. Data written to a file is often held in a buffer in memory first and written to disk in batches; fclose writes out whatever is left in the buffer and releases the resources associated with the file. Forgetting to close a file can lose data, and there's also a limit to how many files one program can have open at once.

`perror(message)` prints your message followed by the reason for the error as reported by the system. For example, trying to open a file that doesn't exist prints:

text
Could not open the file: No such file or directory

About file paths. A relative path such as "hello.txt" is relative to the program's current working directory when it runs, which isn't necessarily the directory containing the source code. When you run a program from an IDE, the working directory is usually the project directory or the directory containing the executable; if a file can't be found, check this first. On Windows, a backslash in a path must be written twice inside a string, as in "C:\\data\\scores.txt", or you can simply use forward slashes: "C:/data/scores.txt".

14.3 Reading and writing characters: fgetc and fputc

fgetc(fp) reads one character from a file and returns EOF at the end of the file; fputc(c, fp) writes one character to a file. They're the file versions of getchar and putchar.

The program below counts how many bytes and how many lines are in the hello.txt created in the previous section:

c
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("hello.txt", "r");
    if (fp == NULL) {
        perror("hello.txt");
        return 1;
    }

    int ch;                             // must be an int to compare correctly with EOF
    long bytes = 0, lines = 0;
    while ((ch = fgetc(fp)) != EOF) {
        bytes++;
        if (ch == '\n') {
            lines++;
        }
    }
    fclose(fp);

    printf("%ld bytes, %ld lines\n", bytes, lines);
    return 0;
}

Output:

text
33 bytes, 2 lines

while ((ch = fgetc(fp)) != EOF) is the standard way to read a file character by character: read a character, assign it to ch, and check whether it's EOF. Note that ch must be an int. If it were a char, then on platforms where char is unsigned it would never equal EOF (which is negative), and the loop would never end; on platforms where char is signed, a byte with the value 0xFF in the file would usually become -1 when stored in a char and be mistaken for EOF (which is -1 in common implementations), ending the loop early.

14.4 Reading and writing lines: fgets and fputs

fgets(buf, size, fp) reads one line from a file (at most size − 1 characters), as introduced in Chapter 9; fputs(s, fp) writes a string to a file (unlike puts, it doesn't add a newline).

The standard way to process a text file line by line is:

c
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
    // process this line
}

If a line is longer than the buffer, fgets reads it in several pieces; either take that into account when programming, or make the buffer big enough.

14.5 Formatted reading and writing: fprintf and fscanf

fprintf and fscanf are used exactly like printf and scanf, except that they take a file pointer as their first argument. They're ideal for reading and writing structured text data:

c
#include <stdio.h>

typedef struct {
    char name[20];
    int age;
    double score;
} Student;

int main(void)
{
    Student students[] = {
        {"Alice", 19, 88.5},
        {"Bob", 20, 92.0},
        {"Carol", 18, 76.5}
    };
    int n = sizeof(students) / sizeof(students[0]);

    FILE *fp = fopen("students.txt", "w");
    if (fp == NULL) {
        perror("students.txt");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        fprintf(fp, "%s %d %.1f\n", students[i].name, students[i].age, students[i].score);
    }
    fclose(fp);

    fp = fopen("students.txt", "r");
    if (fp == NULL) {
        perror("students.txt");
        return 1;
    }
    Student s;
    int count = 0;
    while (fscanf(fp, "%19s %d %lf", s.name, &s.age, &s.score) == 3) {
        count++;
        printf("Record %d: %s, age %d, score %.1f\n", count, s.name, s.age, s.score);
    }
    fclose(fp);
    return 0;
}

Output:

text
Record 1: Alice, age 19, score 88.5
Record 2: Bob, age 20, score 92.0
Record 3: Carol, age 18, score 76.5

The students.txt it creates looks like this, and people can read and edit it directly:

text
Alice 19 88.5
Bob 20 92.0
Carol 18 76.5

The loop condition fscanf(...) == 3 means "all 3 items were read successfully." The loop ends when it reaches the end of the file or runs into data in the wrong format.

14.6 Binary reading and writing: fread and fwrite

fwrite and fread copy bytes from memory to a file, or from a file into memory, exactly as they are:

c
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);

Starting at address ptr, they write or read count blocks of size bytes each, and they return the number of blocks actually written or read.

c
#include <stdio.h>

typedef struct {
    char name[20];
    int age;
    double score;
} Student;

int main(void)
{
    Student out[3] = {{"Alice", 19, 88.5}, {"Bob", 20, 92.0}, {"Carol", 18, 76.5}};

    FILE *fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        perror("students.dat");
        return 1;
    }
    size_t written = fwrite(out, sizeof(Student), 3, fp);   // write the whole array in one go
    fclose(fp);
    printf("Wrote %zu records of %zu bytes each\n", written, sizeof(Student));

    Student in[3];
    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        perror("students.dat");
        return 1;
    }
    size_t count = fread(in, sizeof(Student), 3, fp);
    fclose(fp);

    for (size_t i = 0; i < count; i++) {
        printf("%s %d %.1f\n", in[i].name, in[i].age, in[i].score);
    }
    return 0;
}

Output:

text
Wrote 3 records of 32 bytes each
Alice 19 88.5
Bob 20 92.0
Carol 18 76.5

Text and binary formats each have their strengths and weaknesses:

Text formatBinary format
ReadabilityGood: can be viewed and edited directlyPoor: needs a dedicated program to read
SpeedSlower: numbers must be converted to and from charactersFast: memory is copied directly
PrecisionFloating-point values can lose precision (depending on how many digits are written)Exact
PortabilityGoodPoor: type sizes, byte order (Chapter 16), and structure padding may differ between platforms

As a rule of thumb, data that people need to read, or that must be exchanged between different programs and platforms, belongs in a text format (such as CSV or JSON); data that must be fast and is read and written only by the same program can use a binary format.

14.7 Moving around in a file

Every open file has a "current position." Reads and writes start there and move it forward. These functions query and change the current position:

FunctionWhat it does
ftell(fp)Returns the current position. For a file opened in binary mode, it's the number of bytes from the start of the file; for a file opened in text mode, the value is only guaranteed to work with fseek to return to the same position, and you can't do arithmetic with it as a byte count
fseek(fp, offset, origin)Moves the current position to "origin + offset"
rewind(fp)Goes back to the start of the file

The origin of fseek can be one of three values: SEEK_SET (the start of the file), SEEK_CUR (the current position), or SEEK_END (the end of the file). For example, here's how to find the size of a file:

c
FILE *fp = fopen("students.dat", "rb");
if (fp != NULL) {
    fseek(fp, 0, SEEK_END);          // move to the end of the file
    long size = ftell(fp);           // the position now is the file's size
    printf("File size: %ld bytes\n", size);   // prints: File size: 96 bytes
    fclose(fp);
}

For a file opened in text mode, because of things like the line-ending conversion described in Section 14.1, the offset passed to fseek must be either 0 or a value returned by an earlier call to ftell (and in that case the origin must be SEEK_SET). So whenever you need to calculate positions in bytes, open the file in binary mode, as the example above does. (Strictly speaking, the C standard doesn't guarantee that a file opened in binary mode supports seeking relative to SEEK_END either, but on common systems such as Windows, Linux, and macOS, the code above is the usual way to find a file's size.)

In a binary file whose records all have the same size, fseek lets you jump straight to the k-th record and read it, without reading all the records before it. This is called random access, and databases are built on similar principles to store and retrieve data quickly.

14.8 How to detect the end of a file correctly

Beginners often write their file-reading loops like this:

c
while (!feof(fp)) {                      // the wrong way!
    fgets(line, sizeof(line), fp);
    printf("%s", line);
}

Written this way, the last line of the file is often printed twice. The reason is that feof checks the file's end-of-file flag, and that flag is set only when a read operation runs into the end of the file; feof can't "predict" whether the next read will fail. The last line of a text file usually ends with a newline, and fgets stops at that newline without running into the end of the file, so the flag isn't set and the loop runs once more; this time fgets reads nothing and returns NULL, but line still holds the previous line, and it gets printed again. (If the last line has no newline at the end, fgets runs into the end of the file while reading it, and then nothing is repeated—the same code behaves differently depending on whether the file ends with a newline, which is itself a sign that it's unreliable.) If the file is empty, the loop body still runs once and prints whatever line held before (unpredictable garbage, if line was never initialized).

The right way is to use the return value of the reading function as the loop condition:

  • fgetc: while ((ch = fgetc(fp)) != EOF)
  • fgets: while (fgets(buf, size, fp) != NULL)
  • fscanf: while (fscanf(fp, ...) == number_of_items_expected)
  • fread: while ((n = fread(buf, size, count, fp)) > 0)

After the loop, if you need to tell "reached the end of the file normally" apart from "a read error occurred," call feof(fp) or ferror(fp).

14.9 Putting it together: a file-copying tool

Here's a simple file-copying tool that uses command-line arguments. It reads and writes in binary mode, so it can copy any kind of file (text, images, archives...):

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "Usage: %s SOURCE DEST\n", argv[0]);
        return 1;
    }

    FILE *src = fopen(argv[1], "rb");
    if (src == NULL) {
        perror(argv[1]);
        return 1;
    }
    FILE *dst = fopen(argv[2], "wb");
    if (dst == NULL) {
        perror(argv[2]);
        fclose(src);                      // don't forget to close the file that's already open
        return 1;
    }

    char buffer[4096];                    // read and write at most 4 KB at a time
    size_t n;
    long long total = 0;
    while ((n = fread(buffer, 1, sizeof(buffer), src)) > 0) {
        if (fwrite(buffer, 1, n, dst) != n) {
            perror("Write error");
            fclose(src);
            fclose(dst);
            return 1;
        }
        total += (long long)n;
    }
    if (ferror(src)) {
        perror("Read error");
        fclose(src);
        fclose(dst);
        return 1;
    }

    fclose(src);
    if (fclose(dst) != 0) {               // some data may reach the disk only when the file is closed, so check this too
        perror(argv[2]);
        return 1;
    }
    printf("Copied %lld bytes\n", total);
    return 0;
}

Compile and run it:

text
$ gcc -Wall -Wextra mycp.c -o mycp
$ ./mycp students.dat backup.dat
Copied 96 bytes
$ ./mycp nothing.txt a.txt
nothing.txt: No such file or directory

A few details worth noticing: reading and writing a whole block at a time (rather than one byte at a time) is far more efficient; fread returns the number of bytes it actually read, and the last block is usually smaller than 4096 bytes, so the write uses n, not sizeof(buffer); and before returning because of an error, the program closes the files it has already opened.

Tip
On the command line, standard input and output can also be "redirected" to files: ./program < input.txt makes the program read its input from input.txt (as if someone were typing the file's contents on the keyboard for you), and ./program > output.txt writes the output to output.txt. When testing a program that needs a lot of input, write the test data into a file and redirect it, instead of typing it in by hand over and over.

Exercises

  1. Word count. Write a program that counts the lines, words, and bytes in a text file given as a command-line argument (a word is a run of non-whitespace characters separated by whitespace). This is what the Linux wc command does.
  2. Convert to uppercase. Write a program upper SOURCE DEST that converts every lowercase letter in the source file to uppercase and writes the result to the destination file.
  3. Random access. Using the students.dat created in Section 14.6, read a record number k (starting from 1), use fseek to jump directly to the k-th record, read it, and display it. Print a message if the number is out of range.
  4. Statistics from a data file. The text file numbers.txt contains some integers separated by spaces or newlines (how many is unknown). Write a program that reads all of them and prints their count, sum, average, and maximum.

Chapter 15: The Preprocessor and Multi-File Programs

As programs grow longer, cramming all the code into a single file makes it hard to maintain. This chapter first covers the preprocessing directives that start with #, then shows how to split a program into several files—an essential skill for real projects.

15.1 What is the preprocessor?

As Chapter 1 explained, the first step of compilation is preprocessing. Before the real compilation starts, the preprocessor makes some purely textual changes to the source code: it copies in the contents of headers, replaces macros with their text, and keeps or removes certain pieces of code depending on conditions.

Preprocessing directives all start with #, each takes up a line of its own, and there's no semicolon at the end (they aren't C statements). To see what the code looks like after preprocessing, run gcc -E filename.c.

15.2 #include: including headers

What #include does is very simple: it copies the entire contents of the named file into the current position, unchanged. It comes in two forms:

  • #include <stdio.h>: angle brackets search the system's standard header directories; use them for standard library headers;
  • #include "stats.h": double quotes search the directory of the current source file first, then the system directories; use them for headers you write yourself.

15.3 #define: macros

Object-like macros define constants, as you saw in Chapter 2:

c
#define MAX_STUDENTS 100
#define PI 3.14159265358979

Function-like macros take parameters and look like functions, but they're nothing more than plain text substitution, which leads to some classic traps:

c
#include <stdio.h>

#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
#define DOUBLE_BAD(x) (x) + (x)
#define DOUBLE(x) ((x) + (x))

int main(void)
{
    printf("SQUARE_BAD(1 + 2)  = %d\n", SQUARE_BAD(1 + 2));
    printf("SQUARE(1 + 2)      = %d\n", SQUARE(1 + 2));
    printf("10 * DOUBLE_BAD(3) = %d\n", 10 * DOUBLE_BAD(3));
    printf("10 * DOUBLE(3)     = %d\n", 10 * DOUBLE(3));
    return 0;
}

Output:

text
SQUARE_BAD(1 + 2)  = 5
SQUARE(1 + 2)      = 9
10 * DOUBLE_BAD(3) = 33
10 * DOUBLE(3)     = 60
  • SQUARE_BAD(1 + 2) is replaced by 1 + 2 * 1 + 2, which, because * has higher precedence than +, means 1 + (2 * 1) + 2: the result is 5, not 9;
  • 10 * DOUBLE_BAD(3) is replaced by 10 * (3) + (3), which gives 33, not 60.

So when writing function-like macros, follow two rules: put parentheses around every parameter, and around the whole expression.

Even with all the parentheses in place, one problem can't be avoided: a parameter may be evaluated more than once. SQUARE(i++) is replaced by ((i++) * (i++)), which modifies i twice—undefined behavior. So never pass an expression with side effects (such as `i++` or a function call) to a macro.

In modern C, most function-like macros can be replaced by `inline` functions (C99). They get a function's type checking and evaluate each argument only once, and the compiler can expand their code right at the call site, saving the overhead of a function call just as a macro does (inline is only a suggestion: the compiler decides whether to expand the code, and GCC, for example, usually doesn't when optimization is turned off):

c
static inline int square(int x)
{
    return x * x;
}

Macros also have two special operators: # turns a parameter into a string, and ## glues two tokens together into one. A macro that spans several lines continues each line with a backslash \. A macro containing several statements is conventionally wrapped in do { ... } while (0), so that it can be used anywhere as if it were a single ordinary statement (even inside an if-else without braces, it won't misbehave):

c
#include <stdio.h>

#define PRINT_INT(x) printf(#x " = %d\n", x)          // #x turns the argument's text into a string
#define SWAP_INT(a, b) do { int t_ = (a); (a) = (b); (b) = t_; } while (0)

int main(void)
{
    int width = 3, height = 5;
    PRINT_INT(width * height);
    SWAP_INT(width, height);
    PRINT_INT(width);
    PRINT_INT(height);
    return 0;
}

Output:

text
width * height = 15
width = 5
height = 3

#undef name removes a macro's definition. By convention, macro names are written entirely in capitals to set them apart from functions and variables, as a reminder to users: "this is a macro—be careful."

15.4 Conditional compilation

Conditional compilation lets the preprocessor decide, based on conditions, which code to keep and which to remove.

DirectiveMeaning
#if constant_expressionKeep the code that follows if the expression is true
#elif constant_expressionLike else if
#elseOtherwise
#endifEnds the conditional block
#ifdef NAMEIf this macro is defined
#ifndef NAMEIf this macro isn't defined
defined(NAME)Tests inside #if whether a macro is defined, as in #if defined(A) && !defined(B)

Use 1: a debugging switch. Print debugging information during development, and turn it all off for release with one flip:

c
#include <stdio.h>

#ifdef DEBUG
    #define LOG(msg) fprintf(stderr, "[DEBUG] %s:%d: %s\n", __FILE__, __LINE__, msg)
#else
    #define LOG(msg) ((void)0)
#endif

int main(void)
{
    LOG("program started");
    int sum = 0;
    for (int i = 1; i <= 10; i++) {
        sum += i;
    }
    LOG("sum computed");
    printf("sum = %d\n", sum);
    return 0;
}

In a normal build, LOG is replaced by ((void)0), which does nothing, so the program prints only its result:

text
$ gcc log_demo.c -o log_demo
$ ./log_demo
sum = 55

Define the DEBUG macro at compile time with the -D option (which has the same effect as writing #define DEBUG at the top of the code), and the debugging information appears:

text
$ gcc -DDEBUG log_demo.c -o log_demo
$ ./log_demo
[DEBUG] log_demo.c:11: program started
[DEBUG] log_demo.c:16: sum computed
sum = 55

Use 2: cross-platform code. Different operating systems predefine different macros, such as _WIN32 on Windows, __linux__ on Linux, and __APPLE__ on macOS. With them, you can write different code for different platforms:

c
#ifdef _WIN32
    #define CLEAR_SCREEN "cls"
#else
    #define CLEAR_SCREEN "clear"
#endif

Use 3: temporarily "commenting out" a large block of code. Just wrap it in #if 0 ... #endif; unlike /* */, this isn't disturbed by comments already in the code.

15.5 Predefined macros

The compiler predefines some useful macros:

MacroMeaning
__FILE__The name of the current source file (a string)
__LINE__The current line number (an integer)
__DATE__, __TIME__The date and time of compilation (strings)
__STDC_VERSION__The C standard version being compiled against: 199901L for C99, 201112L for C11, 201710L for C17, 202311L for C23

C99 also provides __func__, the name of the current function (strictly speaking, it's a predefined identifier rather than a macro). This information is ideal for logs and error reports; the LOG macro above uses __FILE__ and __LINE__.

15.6 Why split a program into several files?

Real projects easily run to tens of thousands or even millions of lines of code, which can't possibly all live in one file. Splitting a program into several files has many advantages:

  • Clear structure: each file handles one self-contained feature, which makes code easier to find and change;
  • Easier teamwork: different people can edit different files at the same time;
  • Easier reuse: a finished module can be dropped straight into another project;
  • Faster builds: after you change a file, only that file needs to be recompiled, not the whole project.

15.7 Headers and source files

Multi-file C programs follow a convention: `.h` headers hold "declarations," and `.c` source files hold "definitions." A header is like a module's "user manual," telling others what the module offers; the source file is where those features are actually implemented.

Let's turn a set of statistics functions into a standalone module. The project consists of three files:

stats.h (the module's interface):

c
#ifndef STATS_H
#define STATS_H

double stats_mean(const double *data, int n);
double stats_max(const double *data, int n);
double stats_min(const double *data, int n);
int stats_call_count(void);

#endif

stats.c (the module's implementation):

c
#include "stats.h"

static int call_count = 0;       // a static global variable: visible only in this file

// A static function: for use inside this file only; other files can't call it
static double sum_of(const double *data, int n)
{
    double sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += data[i];
    }
    return sum;
}

double stats_mean(const double *data, int n)
{
    call_count++;
    return sum_of(data, n) / n;
}

double stats_max(const double *data, int n)
{
    call_count++;
    double m = data[0];
    for (int i = 1; i < n; i++) {
        if (data[i] > m) {
            m = data[i];
        }
    }
    return m;
}

double stats_min(const double *data, int n)
{
    call_count++;
    double m = data[0];
    for (int i = 1; i < n; i++) {
        if (data[i] < m) {
            m = data[i];
        }
    }
    return m;
}

int stats_call_count(void)
{
    return call_count;
}

main.c (uses the module):

c
#include <stdio.h>
#include "stats.h"

int main(void)
{
    double scores[] = {88.5, 92.0, 76.5, 95.0, 81.0};
    int n = sizeof(scores) / sizeof(scores[0]);

    printf("Average: %.2f\n", stats_mean(scores, n));
    printf("Highest: %.1f\n", stats_max(scores, n));
    printf("Lowest:  %.1f\n", stats_min(scores, n));
    printf("The statistics functions were called %d times\n", stats_call_count());
    return 0;
}

Compile and run:

text
$ gcc -Wall -Wextra main.c stats.c -o stats_demo
$ ./stats_demo
Average: 86.60
Highest: 95.0
Lowest:  76.5
The statistics functions were called 3 times

Several key points in this example:

1. Header guards. The contents of stats.h are wrapped in #ifndef STATS_H ... #endif. The first time the file is included, STATS_H isn't defined yet, so it gets defined and all the contents are kept. If the same source file includes it again for some reason (through an indirect include, for example), STATS_H is already defined, and the whole contents are skipped, avoiding duplicate-definition errors. Every header should have a header guard. Many compilers also support the more concise #pragma once; it isn't part of the C standard, but all the major compilers support it.

2. A source file should include its own header too. stats.c includes stats.h at the top, so the compiler can check that the declarations and definitions of the functions agree.

3. Using `static` to hide internal details. The function sum_of and the variable call_count are both marked static, so they're visible only inside stats.c, and main.c can't touch them directly. The only way for outside code to learn the call count is through the stats_call_count() function. This is information hiding: a module exposes only the interface it needs to, and its internals can change at any time without affecting its users. Giving all the public functions a common prefix (such as stats_) also keeps them from colliding with other modules' functions.

15.8 extern: sharing a global variable between files

If you really do need a global variable shared by several files, the right way is: define it in one `.c` file, and declare it with `extern` in a header.

c
// config.h
extern int g_verbose;       // a declaration: tells the compiler "this variable is defined elsewhere"; no memory is allocated

// config.c
#include "config.h"
int g_verbose = 0;          // the definition: actually allocates memory; it may appear only once in the whole program

Any other file that includes config.h can then use g_verbose.

The difference between a declaration and a definition is the key to understanding multi-file programs: a declaration just tells the compiler "this thing exists," and it can appear many times; a definition actually creates the thing (allocating memory for a variable, providing a function's body), and it may appear only once in the entire program. So don't define variables or functions in headers (static inline functions excepted); otherwise, every .c file that includes the header gets its own copy of the definition, and the linker reports a "multiple definition" error.

15.9 Compiling, linking, and common errors

A multi-file project can be compiled with a single command: gcc main.c stats.c -o stats_demo. It actually compiles each .c file separately and then links the results together. You can also do the steps by hand:

bash
$ gcc -Wall -Wextra -c main.c      # compile main.c into the object file main.o
$ gcc -Wall -Wextra -c stats.c     # compile stats.c into the object file stats.o
$ gcc main.o stats.o -o stats_demo # link the two object files into an executable

The advantage of compiling in steps is that after changing stats.c, you only need to recompile stats.c and relink; main.o can be reused as is.

The two most common link errors in multi-file programs are:

Error messageCommon cause
undefined reference to 'stats_mean'The function is declared but never defined: you forgot to add stats.c to the compile command, or the function names don't match
multiple definition of 'g_verbose'The same variable or function is defined more than once, usually because a variable was defined in a header

Both of these errors happen at link time (the messages often mention ld, the linker), not at compile time: each .c file compiles fine on its own, and the problem only shows up when they're combined.

15.10 Building automatically with a Makefile

Once there are more than a few files, typing a long string of compile commands by hand every time gets tedious, and you also have to remember which files changed and need recompiling. The make tool does all of this automatically, based on a file named Makefile:

makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -g

stats_demo: main.o stats.o
	$(CC) $(CFLAGS) -o stats_demo main.o stats.o

main.o: main.c stats.h
	$(CC) $(CFLAGS) -c main.c

stats.o: stats.c stats.h
	$(CC) $(CFLAGS) -c stats.c

clean:
	rm -f stats_demo *.o

A Makefile is made up of rules, each of which has the form:

text
target: dependencies
<Tab>command that builds the target

Take main.o: main.c stats.h as an example. It means: "main.o depends on main.c and stats.h; if either of those files is newer than main.o (that is, it has been modified), run the command on the next line to rebuild main.o."

Type make in the project directory, and it builds the first target, stats_demo, working out automatically which files need recompiling; type make clean to delete all the generated files.

Caution
In a Makefile, the indentation in front of a command must be a single Tab character, not spaces; otherwise you get the error missing separator. On Windows, you can install make in MSYS2 with pacman (in the MinGW toolchain, the command may be named mingw32-make). Also, the Windows Command Prompt has no rm command, so use del there instead.

For larger projects that need to build on several platforms, the industry standard is CMake, which generates build files for you; CLion, Visual Studio, and VS Code all support it well. Once you understand the basic ideas behind Makefiles, CMake will be easy to pick up.

Exercises

  1. Macro traps. Write the macros MAX(a, b) and ABS(x), using parentheses correctly. Then think: after running int i = 5, j = 3; int m = MAX(i++, j);, what are i and m? Why is this usage dangerous? Would there still be a problem with an inline function?
  2. An assertion macro. Write a macro CHECK(cond) that, when the condition cond is false, prints the file name, the line number, and the text of the condition itself (for example, Check failed: main.c:12: x > 0), but doesn't end the program. (Hint: use the # operator to turn the argument into a string.)
  3. A multi-file project. Turn the is_prime, gcd, and lcm functions from the Chapter 7 exercises into a module: declarations in mymath.h, implementations in mymath.c, and calls to them in main.c. Write out the commands to compile it step by step, and write a Makefile for it.

Chapter 16: Bitwise Operations and a Look Under the Hood

One of C's defining features is that it can work directly with the individual bits of data. This is used everywhere: in embedded development (to control hardware registers), network protocols, encryption and compression, graphics, and algorithm optimization. This chapter looks at bitwise operations in depth, and uses them to peek at what data really looks like in memory.

16.1 The six bitwise operators

Chapter 4 gave you a first look at the bitwise operators; here's a systematic tour. Taking a = 12 and b = 10 as examples, and showing 8 bits to make them easy to read:

c
#include <stdio.h>

void print_bits8(unsigned int x)          // prints the low 8 bits of x
{
    for (int i = 7; i >= 0; i--) {
        putchar(((x >> i) & 1u) ? '1' : '0');
    }
}

void show(unsigned int x, const char *label)
{
    print_bits8(x);
    printf("  %3u  %s\n", x & 0xFFu, label);
}

int main(void)
{
    unsigned int a = 12, b = 10;
    show(a, "a");
    show(b, "b");
    show(a & b, "a & b  (AND)");
    show(a | b, "a | b  (OR)");
    show(a ^ b, "a ^ b  (XOR)");
    show(~a, "~a     (NOT; low 8 bits shown)");
    show(a << 2, "a << 2 (shift left by 2)");
    show(a >> 2, "a >> 2 (shift right by 2)");
    return 0;
}

Output:

text
00001100   12  a
00001010   10  b
00001000    8  a & b  (AND)
00001110   14  a | b  (OR)
00000110    6  a ^ b  (XOR)
11110011  243  ~a     (NOT; low 8 bits shown)
00110000   48  a << 2 (shift left by 2)
00000011    3  a >> 2 (shift right by 2)

The rules for each operator:

OperatorRuleAn intuitive way to think of it
& ANDThe result bit is 1 only if both bits are 1"Filters" bits: AND with 0 gives 0; AND with 1 leaves the bit unchanged
| ORThe result bit is 1 if either bit is 1"Turns on" bits: OR with 1 gives 1; OR with 0 leaves the bit unchanged
^ XORThe result bit is 1 if the bits differ, 0 if they're the same"Flips" bits: XOR with 1 flips the bit; XOR with 0 leaves it unchanged
~ NOT0 becomes 1 and 1 becomes 0Flips every bit
<< Left shiftShifts all bits left, filling with 0s on the rightShifting left by n multiplies by 2ⁿ (when it doesn't overflow)
>> Right shiftShifts all bits rightFor non-negative numbers, shifting right by n divides by 2ⁿ (rounding down)

Things to watch out for with bitwise operations:

  1. Use unsigned types for bitwise operations whenever possible. Left-shifting a negative number is undefined behavior, and when a negative number is shifted right, whether 0s or copies of the sign bit fill in from the left is up to the compiler (the major compilers copy the sign bit). Unsigned types avoid these problems, which is why all the code in this chapter uses unsigned types and puts a u after its constants.
  2. The shift count can't be negative, and it can't be greater than or equal to the number of bits in the type. For a 32-bit unsigned int, for example, x << 32 is undefined behavior, and the result isn't necessarily 0.
  3. Watch the precedence. Shifts have lower precedence than addition and subtraction, and &, ^, and | have lower precedence than the comparison operators, so x & 1 == 0 actually means x & (1 == 0). When using bitwise operators, add plenty of parentheses.

16.2 Common bit tricks

Let k be the number of a bit (the lowest bit is bit 0):

OperationExpression
Test for odd or evenn & 1u: 1 means odd, 0 means even
Get bit k(n >> k) & 1u
Set bit k to 1n | (1u << k)
Clear bit k to 0n & ~(1u << k)
Flip bit kn ^ (1u << k)
Remove the lowest 1 bitn & (n - 1)
Keep only the lowest 1 bitn & (~n + 1)
Test whether n is a power of 2n != 0 && (n & (n - 1)) == 0
c
#include <stdio.h>
#include <stdbool.h>

void print_bits8(unsigned int x)
{
    for (int i = 7; i >= 0; i--) {
        putchar(((x >> i) & 1u) ? '1' : '0');
    }
}

void show(unsigned int x, const char *label)
{
    print_bits8(x);
    printf("  %3u  %s\n", x & 0xFFu, label);
}

bool is_power_of_two(unsigned int n)
{
    return n != 0 && (n & (n - 1)) == 0;
}

int main(void)
{
    unsigned int n = 44;
    show(n, "n itself");
    show(n | (1u << 0), "set bit 0:      n | (1u << 0)");
    show(n & ~(1u << 5), "clear bit 5:    n & ~(1u << 5)");
    show(n ^ (1u << 7), "flip bit 7:     n ^ (1u << 7)");
    show(n & (n - 1), "drop lowest 1:  n & (n - 1)");
    show(n & (~n + 1), "keep lowest 1:  n & (~n + 1)");
    printf("Bit 3 of n is %u\n", (n >> 3) & 1u);
    printf("Is 44 a power of 2? %s\n", is_power_of_two(44) ? "yes" : "no");
    printf("Is 64 a power of 2? %s\n", is_power_of_two(64) ? "yes" : "no");
    return 0;
}

Output:

text
00101100   44  n itself
00101101   45  set bit 0:      n | (1u << 0)
00001100   12  clear bit 5:    n & ~(1u << 5)
10101100  172  flip bit 7:     n ^ (1u << 7)
00101000   40  drop lowest 1:  n & (n - 1)
00000100    4  keep lowest 1:  n & (~n + 1)
Bit 3 of n is 1
Is 44 a power of 2? no
Is 64 a power of 2? yes

Why does n & (n - 1) remove the lowest 1 bit? Because n - 1 turns the lowest 1 of n into a 0 and all the 0s to its right into 1s, while leaving the higher bits alone. ANDing the two clears the lowest 1 and everything to its right and leaves the other bits unchanged. A power of 2 has exactly one 1 in binary, and removing it leaves 0; that's how the power-of-2 test works.

16.3 In practice: permissions as flags

An integer has 32 bits, and each bit can represent an "on/off" state, so a single integer can record 32 Boolean values at once. That saves space and makes the whole set easy to pass around and compare. This technique is called flags or a bit mask, and you'll see it everywhere in operating systems, graphics libraries, and hardware programming.

c
#include <stdio.h>

#define PERM_READ  (1u << 2)    // 100: read
#define PERM_WRITE (1u << 1)    // 010: write
#define PERM_EXEC  (1u << 0)    // 001: execute

void print_perm(unsigned int perm)
{
    printf("%c%c%c\n",
           (perm & PERM_READ)  ? 'r' : '-',
           (perm & PERM_WRITE) ? 'w' : '-',
           (perm & PERM_EXEC)  ? 'x' : '-');
}

int main(void)
{
    unsigned int perm = PERM_READ | PERM_WRITE;   // both read and write permission
    print_perm(perm);

    perm |= PERM_EXEC;                            // add execute permission
    print_perm(perm);

    perm &= ~PERM_WRITE;                          // remove write permission
    print_perm(perm);

    if (perm & PERM_READ) {                       // check for read permission
        printf("Has read permission\n");
    }
    return 0;
}

Output:

text
rw-
rwx
r-x
Has read permission

Use | to combine or add flags, & ~ to remove a flag, and & to test a flag—commit these three operations to memory. File permissions on Linux work exactly this way: in chmod 755 file, the 7 (binary 111) gives the owner read, write, and execute permission, and each 5 (binary 101) gives the group and everyone else read and execute permission. Read, write, and execute correspond to 4, 2, and 1, exactly as in the program above.

16.4 The magic of XOR

XOR has some elegant properties: a ^ a = 0, a ^ 0 = a, and it's both commutative and associative. These properties solve some interesting problems.

Finding the number that appears only once. In an array where every number appears exactly twice except for one number that appears once, find that one:

c
int find_single(const int *arr, int n)
{
    int result = 0;
    for (int i = 0; i < n; i++) {
        result ^= arr[i];      // pairs of equal numbers cancel out to 0
    }
    return result;             // for {4, 1, 2, 1, 2}, the result is 4
}

Swapping two integers without a temporary variable: a ^= b; b ^= a; a ^= b;. This trick is widely circulated, but don't use it in real code: it's hard to read, it's no faster than using a temporary variable, and when a and b are the same variable (say, two pointers to the same memory), the result becomes 0.

16.5 Fixed-width integer types

Chapter 2 mentioned that the sizes of types such as int and long can vary between platforms. Where you need precise control over the size of data—embedded programming, network communication, file formats—use the fixed-width integer types that C99 provides in <stdint.h>:

TypeMeaning
int8_t, int16_t, int32_t, int64_tSigned integers of exactly 8/16/32/64 bits
uint8_t, uint16_t, uint32_t, uint64_tUnsigned integers of exactly 8/16/32/64 bits
intptr_t, uintptr_tInteger types that can hold a pointer's value

To print these types, use the format macros defined in <inttypes.h>:

c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void)
{
    int32_t a = -123456;
    uint8_t b = 255;
    uint64_t c = UINT64_MAX;
    printf("a = %" PRId32 "\n", a);
    printf("b = %" PRIu8 "\n", b);
    printf("c = %" PRIu64 "\n", c);
    return 0;
}

Output:

text
a = -123456
b = 255
c = 18446744073709551615

Macros such as PRId32 are replaced by the correct format string for the platform (such as "d" or "ld"), which is then automatically joined with the strings around it. It looks a little odd, but it's the standard way to guarantee correctness on every platform.

16.6 Byte order: big-endian and little-endian

An int takes 4 bytes—but in what order are those 4 bytes stored in memory? Take 0x12345678; there are two possibilities:

text
                 address:  low  ───────────────▶  high
big-endian:                 12    34    56    78     the most significant byte is at the lowest address
little-endian:              78    56    34    12     the least significant byte is at the lowest address

The x86 computers we use every day, and the vast majority of ARM devices (phones included), are little-endian. You can check with a program:

c
#include <stdio.h>

int main(void)
{
    unsigned int x = 0x12345678;
    unsigned char *p = (unsigned char *)&x;    // look at x one byte at a time through a byte pointer

    printf("x = 0x%X\n", x);
    for (size_t i = 0; i < sizeof(x); i++) {
        printf("byte %zu: 0x%02X\n", i, p[i]);
    }
    printf("This machine is %s-endian\n", (p[0] == 0x78) ? "little" : "big");
    return 0;
}

Output on an x86 computer:

text
x = 0x12345678
byte 0: 0x78
byte 1: 0x56
byte 2: 0x34
byte 3: 0x12
This machine is little-endian

This program also shows what a pointer's type does: the same address, read through an unsigned int *, is a 4-byte integer; read through an unsigned char *, it's a series of individual bytes. Looking at the bytes of any object through a pointer to a character type is something the C standard explicitly allows.

Byte order matters a great deal for network communication and binary files: if a little-endian computer sends an integer's memory as is, a big-endian computer that receives it will interpret it wrongly. That's why network protocols standardize on big-endian (known as network byte order), and both senders and receivers convert. It's also one of the reasons Chapter 14 said binary files "don't travel well between platforms."

16.7 How floating-point numbers look in binary (Optional)

Modern computers almost all store floating-point numbers according to the IEEE 754 standard. A 32-bit float has three parts:

text
 31  30        23 22                                    0
+---+------------+---------------------------------------+
| S |  exponent  |           fraction (23 bits)          |
|   |  (8 bits)  |                                       |
+---+------------+---------------------------------------+

It represents the value (−1)^S × 1.fraction × 2^(exponent − 127); the idea is simply scientific notation in binary.

Take 5.75 as an example: 5.75 in binary is 101.11, which in scientific notation is 1.0111 × 2². So the sign bit S = 0, the exponent = 2 + 127 = 129 = 10000001, and the fraction is the part after the binary point, 0111, padded with 0s.

The program below shows each part of a float:

c
#include <stdio.h>
#include <stdint.h>
#include <string.h>

void print_field(uint32_t value, int bits)
{
    for (int i = bits - 1; i >= 0; i--) {
        putchar(((value >> i) & 1u) ? '1' : '0');
    }
}

void print_float_bits(float f)
{
    uint32_t bits;
    memcpy(&bits, &f, sizeof bits);      // copy the float's 4 bytes into an integer, unchanged

    printf("%-6g sign ", f);
    print_field(bits >> 31, 1);
    printf(" | exponent ");
    print_field((bits >> 23) & 0xFFu, 8);
    printf(" | fraction ");
    print_field(bits & 0x7FFFFFu, 23);
    printf("\n");
}

int main(void)
{
    print_float_bits(5.75f);
    print_float_bits(-1.0f);
    print_float_bits(0.1f);
    return 0;
}

Output:

text
5.75   sign 0 | exponent 10000001 | fraction 01110000000000000000000
-1     sign 1 | exponent 01111111 | fraction 00000000000000000000000
0.1    sign 0 | exponent 01111011 | fraction 10011001100110011001101

Look at the fraction of 0.1: 1001 1001 1001 ... is an endlessly repeating binary fraction that can't fit in 23 bits, so it has to be rounded. That's the root cause of 0.1 + 0.2 != 0.3 from Chapter 2. A double has a 52-bit fraction and much higher precision, but it can't represent 0.1 exactly either.

The program uses memcpy to copy the bytes of the float into an integer, which is the safe way to inspect a floating-point number's internal representation. Writing *(uint32_t *)&f directly also happens to work with many compilers, but it breaks C's "strict aliasing" rule and is undefined behavior.

Exercises

  1. Counting 1 bits. Write a function int count_ones(unsigned int n) that counts how many 1s there are in the binary representation of n. Implement it two ways: by checking every bit, and by using n & (n - 1) (so that the loop runs as many times as there are 1s).
  2. Powers of 2. Write a function that finds the smallest power of 2 that's no less than n (n ≥ 1). For example, 5 gives 8 and 16 gives 16.
  3. Reversing bits. Write a function uint32_t reverse_bits(uint32_t n) that reverses the order of the bits in a 32-bit unsigned integer (bit 0 swaps with bit 31, bit 1 with bit 30, and so on). For example, 0x00000001 reversed is 0x80000000.
  4. A bit set. Use a uint32_t to represent a subset of the set {0, 1, ..., 31} (bit k is 1 if k is in the set). Implement functions to add an element, remove an element, test whether an element is present, and print all the elements of the set. Then use | and & to find the union and intersection of two sets.

Chapter 17: Debugging, Error Handling, and Coding Style

Writing code is only the first step. Being able to find and fix errors quickly, and to write code that's robust and easy to read, is what makes a capable programmer. This chapter covers methods and tools for debugging and the basic patterns of error handling, and it sums up the conventions of good C style.

17.1 Make friends with your compiler: take warnings seriously

Compiler warnings are your cheapest and quickest "code reviewer." Look at the code below. It compiles, yet it hides several problems:

c
#include <stdio.h>

int square(int x)
{
    int result = x * x;
}

int main(void)
{
    int n;
    double avg = 3.5;
    unsigned int count = 10;
    int unused = 0;

    if (n = 5) {
        printf("%d\n", avg);
    }
    for (int i = 0; i < count; i++) {
    }
    printf("%d\n", square(3));
    return 0;
}

Compile it with gcc -Wall -Wextra and you get the following warnings (with the source-code excerpts left out):

text
warn.c: In function 'square':
warn.c:5:9: warning: unused variable 'result' [-Wunused-variable]
warn.c: In function 'main':
warn.c:15:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
warn.c:16:18: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
warn.c:18:23: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
warn.c:13:9: warning: unused variable 'unused' [-Wunused-variable]
warn.c: In function 'square':
warn.c:6:1: warning: control reaches end of non-void function [-Wreturn-type]

Every one of them points to a real problem: square computes its result but forgets to return it; if (n = 5) writes an assignment where a comparison was meant; printf prints a double with %d; a signed number is compared with an unsigned one... Without warnings, you'd only find these problems when the program misbehaved, and tracking them down then would be much harder.

Recommendations:

  • Always compile with -Wall -Wextra, and aim for zero warnings;
  • Consider adding -Werror, which turns every warning into an error and forces you to fix it;
  • Turn on the equivalent warning options in your IDE too (in Visual Studio, you can set the warning level to /W4).

17.2 Kinds of errors

KindWhen it happensTypical examplesHow it shows up
Compile errorAt compile timeSyntax errors, misspellings, mismatched typesThe compiler reports an error
Link errorAt link timeUndefined functions, duplicate definitionsThe linker reports an error, often mentioning ld
Run-time errorWhile runningSegmentation faults, division by zero, memory errorsThe program crashes, or a checking tool catches it
Logic errorWhile runningA wrong algorithm, wrong boundary conditionsThe results are wrong. Hardest to find; needs testing and debugging

Compile and link errors come with the compiler telling you where they are, so they're relatively easy to fix (Appendix F lists the common error messages). The real test of skill is run-time and logic errors.

17.3 Debugging with printf

The simplest and most widely used debugging method is to print the values of variables at key points, and see where the program's actual behavior departs from what you expected:

c
fprintf(stderr, "[debug] i = %d, sum = %d\n", i, sum);

It's a good idea to send debugging output to stderr, for two reasons: it's easy to tell apart from the program's normal output, and stderr is unbuffered by default. Output to stdout may be held in a buffer for a while, so if the program crashes shortly after printing, the buffered output is lost and you'd wrongly conclude that the program never reached that line. Combined with the conditional compilation from Chapter 15, a single DEBUG macro can switch all the debugging output on or off.

Debugging with printf is simple and direct, but it means editing and recompiling the code over and over. For complicated problems, a debugger is a much more powerful tool.

17.4 Using the GDB debugger

A debugger lets you pause a program at chosen places, then run it one line at a time while inspecting the values of its variables. GDB is the most widely used debugger on Linux (it's also included in MSYS2's MinGW toolchain; on macOS, the equivalent debugger is LLDB, which works in a similar way).

Take this factorial program, which prints the wrong result:

c
#include <stdio.h>

long long factorial(int n)
{
    long long result = 0;
    for (int i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}

int main(void)
{
    int n = 5;
    long long f = factorial(n);
    printf("%d! = %lld\n", n, f);
    return 0;
}

Output:

text
5! = 0

Let's use GDB to find out why. First, compile with -g (to generate debugging information) and -O0 (to turn off optimization, so that the program runs in exactly the order of the source code):

text
$ gcc -g -O0 fact.c -o fact
$ gdb ./fact
(gdb) break factorial
Breakpoint 1 at 0x1154: file fact.c, line 5.
(gdb) run
Breakpoint 1, factorial (n=5) at fact.c:5
5           long long result = 0;
(gdb) next
6           for (int i = 1; i <= n; i++) {
(gdb) print result
$1 = 0
(gdb) display result
1: result = 0
(gdb) next
7               result *= i;
1: result = 0
(gdb) next
6           for (int i = 1; i <= n; i++) {
1: result = 0
(gdb) info locals
i = 1
result = 0

After the first multiplication (result *= 1), result is still 0. The problem is plain to see: result starts at 0, and 0 times anything is 0. Its initial value should be 1.

Common GDB commands:

Command (short form)What it does
break location (b)Sets a breakpoint; the location can be a function name, a line number (such as b 12), or "file:line"
run (r)Starts the program, pausing at breakpoints
next (n)Runs the next line, without stepping into function calls
step (s)Runs the next line, stepping into function calls
continue (c)Keeps running until the next breakpoint
finishRuns until the current function returns
print expression (p)Shows the value of a variable or expression, such as p arr[2], p *ptr, or p x + y
display expressionShows the value of this expression automatically every time the program pauses
info localsShows the values of all the local variables in the current function
backtrace (bt)Shows the call stack: which function you're in, and who called it
watch variableSets a watchpoint: pauses whenever the variable's value changes
list (l)Shows the source code around the current position
quit (q)Exits GDB

Finding where a program crashes with GDB. When a program crashes (with a segmentation fault, for example), run it in GDB: GDB stops where the crash happened, and backtrace then shows the call stack, telling you where the program crashed and through which chain of calls it got there:

c
#include <stdio.h>
#include <string.h>

size_t count_chars(const char *s)
{
    return strlen(s);
}

int main(void)
{
    const char *names[] = {"Alice", NULL, "Bob"};
    for (int i = 0; i < 3; i++) {
        printf("%zu\n", count_chars(names[i]));
    }
    return 0;
}
text
$ gcc -g -O0 crash.c -o crash
$ gdb ./crash
(gdb) run
5

Program received signal SIGSEGV, Segmentation fault.
__strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex-base.S:81
(gdb) backtrace
#0  __strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex-base.S:81
#1  0x00005555555551a5 in count_chars (s=0x0) at crash.c:6
#2  0x00005555555551fb in main () at crash.c:13

Read the call stack from the bottom up: main called count_chars on line 13, count_chars called strlen on line 6, and the argument it passed, s=0x0, is a null pointer—strlen dereferenced the null pointer and crashed. Frame #0 is inside the standard library, which is usually not where the problem is; focus on frames #1 and #2, which are in your own code.

Graphical debugging. IDEs such as Visual Studio, VS Code, and CLion all offer graphical debugging interfaces: click to the left of a line number to set a breakpoint, use the toolbar buttons to step through the code, and watch variables' values in the "Watch" or "Variables" window. Underneath, they work exactly like GDB, so once you understand GDB's ideas, you'll be all the more effective with a graphical debugger.

Tip
The core idea of debugging is to narrow things down. First find out which part of the code the problem is in (binary search: set a breakpoint in the middle and see whether the problem shows up before or after it), then watch that part line by line until you find the first place where a variable's actual value differs from what you expected.

17.5 Assertions with assert

assert(condition), from <assert.h>, checks conditions that "by rights must always be true." When the condition is false, it prints the file, line, function, and text of the condition, and ends the program immediately:

c
#include <stdio.h>
#include <assert.h>

double average(const int *arr, int n)
{
    assert(arr != NULL);
    assert(n > 0);                        // the caller must guarantee n > 0
    long long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return (double)sum / n;
}

int main(void)
{
    int scores[] = {90, 85, 77};
    printf("%.2f\n", average(scores, 3));
    printf("%.2f\n", average(scores, 0)); // breaks the precondition
    return 0;
}

Output:

text
84.00
assert_demo: assert_demo.c:7: average: Assertion `n > 0' failed.
Aborted (core dumped)

Assertions expose an error at the earliest possible point, instead of letting it surface much later in some strange form. Defining the NDEBUG macro when compiling (-DNDEBUG) turns off all assertions, so assertions are only for catching programmers' mistakes (such as a violated precondition of a function). Never use them to handle user input errors, missing files, or other situations that can legitimately happen at run time; those must be handled carefully with if statements.

17.6 The basics of error handling

C has no "exception" mechanism like other languages do; error handling relies mainly on checking return values.

1. Check every function call that can fail. fopen and malloc can return NULL, scanf can fail to read, and even fclose and fwrite can fail. At the very least, check the calls whose failure would break the operations that follow.

2. Use `errno` to find out why. When many standard library functions fail, they store an error code in the global variable errno (in <errno.h>). perror prints a description based on it, and strerror(errno) (in <string.h>) returns the description as a string, which you can use in error messages you write yourself.

3. When designing your own functions, report errors clearly. A common convention is to return 0 for success and a nonzero (or negative) value for failure, and to hand back the real result through a pointer parameter. For example:

c
// Returns 0 on success and puts the result in *result; returns -1 on failure
int parse_score(const char *text, int *result);

4. Release resources in one place when an error occurs. When a function acquires several resources in turn (opening files, allocating memory...), a failure at any step must release everything acquired before it, and it's easy to miss something. A widely accepted pattern in C is to use goto to jump to a single cleanup section at the end of the function; the Linux kernel uses this pattern extensively:

c
int process_file(const char *in_path, const char *out_path)
{
    int status = -1;          // assume failure
    FILE *in = NULL;
    FILE *out = NULL;
    char *buffer = NULL;

    in = fopen(in_path, "r");
    if (in == NULL) {
        perror(in_path);
        goto cleanup;
    }
    out = fopen(out_path, "w");
    if (out == NULL) {
        perror(out_path);
        goto cleanup;
    }
    buffer = malloc(4096);
    if (buffer == NULL) {
        fprintf(stderr, "Out of memory\n");
        goto cleanup;
    }

    // ... the normal processing goes here ...
    status = 0;               // everything succeeded

cleanup:
    free(buffer);             // free(NULL) is safe
    if (out != NULL) {
        fclose(out);
    }
    if (in != NULL) {
        fclose(in);
    }
    return status;
}

Every resource pointer starts out as NULL, so the cleanup section can check and release them all the same way, without caring which step failed.

5. Ending the program. In main, you can end the program with return; in any other function, calling exit(status) (in <stdlib.h>) ends the whole program immediately. For the status, you can use EXIT_SUCCESS and EXIT_FAILURE, defined in <stdlib.h>.

17.7 A list of undefined behaviors

In earlier chapters we ran into many kinds of undefined behavior. What they have in common is that the C standard places no requirements on the result: the program may crash, may print wrong results, or may "seem perfectly fine"—until you switch compilers, change the optimization level, or feed it different input. Here are the most common kinds of undefined behavior in one place:

Undefined behaviorExample
Accessing an array out of boundsint a[5]; a[5] = 1;
Dereferencing a null or wild pointerint *p = NULL; *p = 1;
Using memory that has been freedfree(p); *p = 1;
Freeing the same memory twicefree(p); free(p);
Using the value of an uninitialized variableint x; printf("%d", x);
Signed integer overflowint x = INT_MAX; x + 1;
Integer division by 0int x = 5 / 0;
Modifying a string literalchar *s = "hi"; s[0] = 'H';
Modifying a variable in an expression while also modifying or reading it somewhere that isn't ordered with respect to that modificationi = i++; (two modifications), a[i] = i++; (one modification, one read)
Shifting by a negative amount, or by at least the number of bits in the type1 << 32 (with a 32-bit int)
A format specifier that doesn't match its argument's typeprintf("%d", 3.14);
Using the return value of a function that didn't execute a returnA non-void function reaches its end without a return, and the caller uses the value
Returning the address of a local variable and then using itint *f(void) { int x; return &x; }

Your weapons against undefined behavior: turn on compiler warnings; test with -fsanitize=address,undefined; and keep this table in mind.

17.8 Coding conventions and good habits

Code is written for people to read, and only incidentally for machines to run. Good coding style greatly reduces the chance of mistakes, and it makes the code easier for others (including you, a few months from now) to understand. Here are some widely accepted conventions:

Naming

  • Use lowercase words separated by underscores for variables and functions (such as student_count and find_max);
  • Write macros and constants in all capitals (such as MAX_SIZE);
  • Capitalize the names of your own types (such as Student and IntVector);
  • Choose names that say what they mean; avoid names like a, b, or temp1 (loop variables i and j excepted);
  • Start the names of functions that return a Boolean with is_ or has_.

Formatting

  • Indent consistently (usually 4 spaces), and don't mix tabs and spaces;
  • Use one brace style consistently;
  • Always use braces for the bodies of `if`, `for`, and `while`, even when they're only one line;
  • Write one statement per line;
  • Put spaces around operators and after commas, to give the code room to breathe;
  • Tools such as clang-format can format code automatically.

Structure

  • Keep functions short, each doing one thing;
  • Avoid "magic numbers"; use named constants instead;
  • Use global variables as little as possible;
  • Define variables where they're first used, and initialize them immediately;
  • Add const to pointer parameters whose data the function doesn't modify.

Robustness

  • Check every function call that can fail;
  • Validate user input;
  • Always release the resources you acquire (memory, files);
  • Never use gets; limit the length when reading strings with scanf; prefer functions with length limits, such as fgets and snprintf;
  • Watch out for integer overflow and array bounds.

Comments

  • Comments should explain "why," not repeat "what" the code does;
  • Before each function, describe what it does, what its parameters mean, what it returns, and anything the caller should watch out for (for example, "the caller must free the returned memory");
  • When you change code, update its comments too—outdated comments are worse than no comments at all.

A comparison. The code below runs correctly, but it's nearly impossible to read:

c
#include <stdio.h>
int main(){int a[100],n,i,j,t;scanf("%d",&n);
for(i=0;i<n;i++)scanf("%d",&a[i]);
for(i=0;i<n-1;i++)for(j=0;j<n-1-i;j++)if(a[j]>a[j+1]){t=a[j];a[j]=a[j+1];a[j+1]=t;}
for(i=0;i<n;i++)printf("%d ",a[i]);return 0;}

Rewritten according to the conventions:

c
#include <stdio.h>

#define MAX_COUNT 100

void bubble_sort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main(void)
{
    int numbers[MAX_COUNT];
    int count;

    if (scanf("%d", &count) != 1 || count < 1 || count > MAX_COUNT) {
        fprintf(stderr, "The count must be between 1 and %d\n", MAX_COUNT);
        return 1;
    }
    for (int i = 0; i < count; i++) {
        if (scanf("%d", &numbers[i]) != 1) {
            fprintf(stderr, "Invalid input\n");
            return 1;
        }
    }

    bubble_sort(numbers, count);

    for (int i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    return 0;
}

The rewritten code is a bit longer, but its structure is clear at a glance, and it fixes a hidden danger in the original: the original never checks whether n exceeds 100, so entering a large number makes it write past the end of the array.

Exercises

  1. Debugging practice. The function below is supposed to return the index of the largest value in an array, but it sometimes gets it wrong. First step through it with GDB (or your IDE's debugger), watching how the variables change, then find and fix all the mistakes. You can test it with the two data sets {5, 1, 2} and {-5, -2, -8}.
c
   int max_index(const int arr[], int n)
   {
       int max = 0;
       int index = 0;
       for (int i = 1; i < n; i++) {
           if (arr[i] > max) {
               max = arr[i];
               index = i;
           }
       }
       return index;
   }
  1. Spotting undefined behavior. Which of the code fragments below contain undefined behavior? Explain why.
c
   // (a)
   int a[3] = {1, 2, 3};
   for (int i = 0; i <= 3; i++) printf("%d ", a[i]);

   // (b)
   unsigned int u = 0;
   u = u - 1;

   // (c)
   int x = 2147483647;
   x = x + 1;

   // (d)
   char s[] = "hello";
   s[0] = 'H';

   // (e)
   char *t = "hello";
   t[0] = 'H';

   // (f)
   int i = 1;
   int b = i++ + i++;
  1. Refactoring. Rewrite the code below according to this chapter's conventions: give things meaningful names, eliminate the magic numbers, add the necessary input checks and braces, and move repeated code into functions.
c
   #include <stdio.h>
   int main(){double s[5],t=0,m=0;int i;
   for(i=0;i<5;i++){scanf("%lf",&s[i]);t+=s[i];}
   for(i=0;i<5;i++)if(s[i]>m)m=s[i];
   printf("%.2f %.2f\n",t/5,m);
   for(i=0;i<5;i++)if(s[i]>=60)printf("%d pass\n",i+1);else printf("%d fail\n",i+1);}

Part IV: Putting It All Together

Goal of this part: apply everything you've learned to build a small but practical system from start to finish—from requirements analysis and overall design to coding and testing—and then map out where to go after C.

Chapter 18: Capstone Project: A Student Grade Manager

Learning to program ultimately comes down to "making something." In this chapter we build a command-line student grade manager from scratch. It isn't large (about 600 lines, many of them comments), yet it uses nearly everything you've learned so far, and it walks through the basic workflow of real software development: requirements analysis → overall design → coding → testing.

18.1 What the project covers

Working through this project, you'll put the following knowledge to use:

TopicHow the project uses it
Structures and typedef (Chapter 11)Student represents one student, and StudentList represents the list of students
Dynamic memory (Chapter 12)The student list is a dynamic array that grows automatically
Pointers and const (Chapter 10)Functions pass data to one another through pointers to structures
String handling (Chapter 9)Reading, comparing, and searching IDs and names; splitting CSV lines
Input validation (Chapters 3, 6, and 9)Whole lines are read with fgets, then checked strictly with strtol and strtod
Function pointers (Chapter 10)qsort sorts by different rules
Files (Chapter 14)Data is saved to and loaded from a CSV file
Branches, loops, and functions (Chapters 5–7)The menu loop and every feature module

18.2 Requirements analysis

Before writing any code, get clear on "what it has to do." The system must provide these features:

OptionFeatureDescription
1Add a studentEnter an ID, a name, and scores in three courses (Math, English, and Science). IDs must be unique, and scores must be between 0 and 100
2List allShow every student's course scores, total, and average in a table
3SearchLook up a student by exact ID, or by any part of the name (typing Smith finds every student whose name contains Smith)
4Update scoresFind a student by ID and enter all of their scores again
5Delete a studentDelete a student by ID, after asking for confirmation
6SortSort by total (highest first) or by ID (ascending)
7StatisticsThe average, highest, lowest, and pass rate for each course, plus the student with the highest total
8SaveSave the data to the file students.csv
0ExitIf there are unsaved changes, ask whether to save them

There are also a few "unstated requirements" that matter just as much:

  • Load the data automatically at startup, and start with an empty list if the file doesn't exist;
  • Be robust: no invalid input (letters, out-of-range numbers, overly long text, input that ends early) may crash the program or send it into an infinite loop;
  • Keep the data file human-readable, so that it can be opened in a text editor or a spreadsheet program such as Excel;
  • Leak no memory: checking with AddressSanitizer should produce "zero reports."

18.3 Overall design

1. Data structures

c
typedef struct {
    char id[ID_LEN];                    // student ID
    char name[NAME_LEN];                // name
    double scores[COURSE_COUNT];        // score in each course
} Student;

typedef struct {
    Student *data;                      // points to the array of students on the heap
    size_t size;                        // current number of students
    size_t capacity;                    // current capacity
    bool modified;                      // are there changes that haven't been saved?
} StudentList;

The student list uses the dynamic array from Chapter 12 rather than the linked list from Chapter 13, because the number of students is usually modest, an array can be handed straight to qsort, and access by index is convenient. (Switching to a linked list is one of this chapter's extension challenges.)

The maximum lengths of IDs and names and the number of courses are all macro constants. Changing the length of IDs or names takes just one edit; if you change the number of courses, COURSE_COUNT, you also need to update the course names in COURSE_NAMES, and adjust any existing data file to match.

2. Data file format

The data is stored in CSV (comma-separated values) format, one student per line: the ID, the name, and then the score in each course:

text
2024001,Alice Smith,85.50,92.50,78.00
2024002,Bob Johnson,90.00,88.00,95.00

CSV is a universal text format that people can read directly and spreadsheet programs such as Excel can open. Because the comma is the separator, IDs and names can't contain commas.

3. Modules

Following the principle that "a function should do one thing," the program is divided into the following groups of functions:

ModuleFunctionsResponsibility
Input helpersread_line, input_text, input_int, input_score, confirmRead each kind of input safely and reject invalid input
Student listlist_init, list_free, list_append, list_remove_at, list_find_by_idManage the dynamic array
Displaystudent_total, print_header, print_studentCompute totals and format the output
Featuresadd_student, show_all, search_student, modify_student, delete_student, sort_students, show_statisticsImplement each item on the menu
File I/Osave_to_file, split_fields, parse_line, load_from_fileSave and load the CSV file
Main programprint_menu, mainThe menu loop

The modules call one another in layers: the features call the input helpers, the student list, and the display module, and the main program calls the features and the file I/O module. Lower-level modules never depend on higher-level ones.

4. Program flow

text
start ──▶ load the data file ──▶ show the menu ──▶ read a choice ──▶ run that feature ──┐
                                      ▲                                                 │
                                      └─────────────────────────────────────────────────┘
choose 0 ──▶ unsaved changes? ──yes──▶ ask whether to save ──▶ free memory ──▶ end

18.4 The complete code

Create a file named student_manager.c and enter the code below. It's fairly long, so type it in one module at a time, following the module list above, and compile after finishing each part.

c
/*
 * Student Grade Manager  student_manager.c
 *
 * Adds, lists, searches, updates, deletes, sorts, and summarizes student grades.
 * The data is stored in a CSV file.
 * Build: gcc -Wall -Wextra -std=c11 student_manager.c -o student_manager
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <stdbool.h>

#define ID_LEN        16                /* max length of a student ID (including '\0') */
#define NAME_LEN      32                /* max length of a name (including '\0') */
#define COURSE_COUNT  3                 /* number of courses */
#define LINE_LEN      256               /* buffer size for one line of input */
#define PASS_SCORE    60.0              /* passing score */
#define DATA_FILE     "students.csv"    /* name of the data file */

/* Course names: each is shown in a column 8 characters wide, so keep them short */
static const char *COURSE_NAMES[COURSE_COUNT] = {"Math", "English", "Science"};

/* ======================== Data structures ======================== */

typedef struct {
    char id[ID_LEN];                    /* student ID */
    char name[NAME_LEN];                /* name */
    double scores[COURSE_COUNT];        /* score in each course */
} Student;

typedef struct {
    Student *data;                      /* points to the array of students on the heap */
    size_t size;                        /* current number of students */
    size_t capacity;                    /* current capacity */
    bool modified;                      /* are there changes that haven't been saved? */
} StudentList;

/* ======================== Input helpers ======================== */

/* Returns true if the string consists only of whitespace (an empty string counts too) */
bool is_blank(const char *s)
{
    while (*s != '\0') {
        if (!isspace((unsigned char)*s)) {
            return false;
        }
        s++;
    }
    return true;
}

/*
 * Reads one line of input and removes the newline at the end.
 * Returns 1 on success, 0 if input has ended, and -1 if the line was too long
 * (the rest of it has been discarded).
 */
int read_line(char *buf, size_t size)
{
    if (fgets(buf, (int)size, stdin) == NULL) {
        return 0;
    }
    size_t len = strcspn(buf, "\n");
    if (buf[len] == '\n') {
        buf[len] = '\0';
        return 1;
    }
    /* No newline was read, so the line is too long: read and discard the rest of it */
    int ch;
    bool discarded = false;
    while ((ch = getchar()) != '\n' && ch != EOF) {
        discarded = true;
    }
    return discarded ? -1 : 1;
}

/* Reads a non-empty string that contains no commas. Returns false if input has ended */
bool input_text(const char *prompt, char *buf, size_t size)
{
    while (true) {
        printf("%s", prompt);
        int result = read_line(buf, size);
        if (result == 0) {
            return false;
        }
        if (result < 0) {
            printf("Too long (at most %zu characters). Please try again.\n", size - 1);
        } else if (is_blank(buf)) {
            printf("This can't be empty. Please try again.\n");
        } else if (strchr(buf, ',') != NULL) {
            printf("Commas aren't allowed. Please try again.\n");
        } else {
            return true;
        }
    }
}

/* Reads an integer in the range [min, max]. Returns false if input has ended */
bool input_int(const char *prompt, int min, int max, int *out)
{
    char buf[LINE_LEN];
    while (true) {
        printf("%s", prompt);
        int result = read_line(buf, sizeof buf);
        if (result == 0) {
            return false;
        }
        if (result > 0) {
            char *end;
            errno = 0;
            long value = strtol(buf, &end, 10);
            if (end != buf && is_blank(end) && errno == 0 && value >= min && value <= max) {
                *out = (int)value;
                return true;
            }
        }
        printf("Please enter a whole number from %d to %d.\n", min, max);
    }
}

/* Reads a score from 0 to 100. Returns false if input has ended */
bool input_score(const char *prompt, double *out)
{
    char buf[LINE_LEN];
    while (true) {
        printf("%s", prompt);
        int result = read_line(buf, sizeof buf);
        if (result == 0) {
            return false;
        }
        if (result > 0) {
            char *end;
            errno = 0;
            double value = strtod(buf, &end);
            if (end != buf && is_blank(end) && errno == 0 && value >= 0.0 && value <= 100.0) {
                *out = value;
                return true;
            }
        }
        printf("A score must be a number from 0 to 100. Please try again.\n");
    }
}

/* Asks the user to confirm; y or Y means yes */
bool confirm(const char *prompt)
{
    char buf[LINE_LEN];
    printf("%s (y/n): ", prompt);
    if (read_line(buf, sizeof buf) != 1) {
        return false;
    }
    return buf[0] == 'y' || buf[0] == 'Y';
}

/* Returns "" for 1 and "s" for anything else, for printing plurals */
const char *plural(size_t n)
{
    return n == 1 ? "" : "s";
}

/* ======================== The student list (a dynamic array) ======================== */

void list_init(StudentList *list)
{
    list->data = NULL;
    list->size = 0;
    list->capacity = 0;
    list->modified = false;
}

void list_free(StudentList *list)
{
    free(list->data);
    list_init(list);
}

/* Appends a student at the end, growing the array when it's full. Returns false if memory runs out */
bool list_append(StudentList *list, const Student *s)
{
    if (list->size == list->capacity) {
        size_t new_cap = (list->capacity == 0) ? 8 : list->capacity * 2;
        Student *new_data = realloc(list->data, new_cap * sizeof *new_data);
        if (new_data == NULL) {
            return false;
        }
        list->data = new_data;
        list->capacity = new_cap;
    }
    list->data[list->size] = *s;
    list->size++;
    return true;
}

/* Removes the student at the given index; the students after it move forward */
void list_remove_at(StudentList *list, size_t index)
{
    for (size_t i = index; i + 1 < list->size; i++) {
        list->data[i] = list->data[i + 1];
    }
    list->size--;
}

/* Finds a student by ID and returns the index, or -1 if there's no such student */
long list_find_by_id(const StudentList *list, const char *id)
{
    for (size_t i = 0; i < list->size; i++) {
        if (strcmp(list->data[i].id, id) == 0) {
            return (long)i;
        }
    }
    return -1;
}

/* ======================== Display ======================== */

double student_total(const Student *s)
{
    double total = 0.0;
    for (int c = 0; c < COURSE_COUNT; c++) {
        total += s->scores[c];
    }
    return total;
}

/* Widths such as %-16s and %8.1f keep the columns lined up (all the text is ASCII) */
void print_header(void)
{
    printf("%-16s%-20s", "ID", "Name");
    for (int c = 0; c < COURSE_COUNT; c++) {
        printf("%8s", COURSE_NAMES[c]);
    }
    printf("%8s%8s\n", "Total", "Average");
}

void print_student(const Student *s)
{
    double total = student_total(s);
    printf("%-16s%-20s", s->id, s->name);
    for (int c = 0; c < COURSE_COUNT; c++) {
        printf("%8.1f", s->scores[c]);
    }
    printf("%8.1f%8.2f\n", total, total / COURSE_COUNT);
}

/* ======================== Features ======================== */

/* Reads the score for each course in turn */
bool input_scores(Student *s)
{
    for (int c = 0; c < COURSE_COUNT; c++) {
        char prompt[64];
        snprintf(prompt, sizeof prompt, "%s score: ", COURSE_NAMES[c]);
        if (!input_score(prompt, &s->scores[c])) {
            return false;
        }
    }
    return true;
}

void add_student(StudentList *list)
{
    Student s;
    if (!input_text("ID: ", s.id, sizeof s.id)) {
        return;
    }
    if (list_find_by_id(list, s.id) >= 0) {
        printf("ID %s already exists. Nothing was added.\n", s.id);
        return;
    }
    if (!input_text("Name: ", s.name, sizeof s.name) || !input_scores(&s)) {
        return;
    }
    if (!list_append(list, &s)) {
        printf("Out of memory. Nothing was added.\n");
        return;
    }
    list->modified = true;
    printf("Student added.\n");
}

void show_all(const StudentList *list)
{
    if (list->size == 0) {
        printf("No students yet.\n");
        return;
    }
    print_header();
    for (size_t i = 0; i < list->size; i++) {
        print_student(&list->data[i]);
    }
    printf("%zu student%s in total.\n", list->size, plural(list->size));
}

/* Exact match on the ID, or a partial match on the name (the name contains the keyword) */
void search_student(const StudentList *list)
{
    char key[NAME_LEN];
    if (!input_text("ID or name to search for: ", key, sizeof key)) {
        return;
    }
    size_t found = 0;
    for (size_t i = 0; i < list->size; i++) {
        const Student *s = &list->data[i];
        if (strcmp(s->id, key) == 0 || strstr(s->name, key) != NULL) {
            if (found == 0) {
                print_header();
            }
            print_student(s);
            found++;
        }
    }
    if (found == 0) {
        printf("No matching students.\n");
    }
}

void modify_student(StudentList *list)
{
    char id[ID_LEN];
    if (!input_text("ID of the student to update: ", id, sizeof id)) {
        return;
    }
    long index = list_find_by_id(list, id);
    if (index < 0) {
        printf("There's no student with ID %s.\n", id);
        return;
    }
    Student *s = &list->data[index];
    print_header();
    print_student(s);
    printf("Enter the new scores:\n");
    Student updated = *s;               /* edit a copy first; write it back only if every input succeeds */
    if (!input_scores(&updated)) {
        return;
    }
    *s = updated;
    list->modified = true;
    printf("Scores updated.\n");
}

void delete_student(StudentList *list)
{
    char id[ID_LEN];
    if (!input_text("ID of the student to delete: ", id, sizeof id)) {
        return;
    }
    long index = list_find_by_id(list, id);
    if (index < 0) {
        printf("There's no student with ID %s.\n", id);
        return;
    }
    print_header();
    print_student(&list->data[index]);
    if (confirm("Delete this student?")) {
        list_remove_at(list, (size_t)index);
        list->modified = true;
        printf("Student deleted.\n");
    } else {
        printf("Nothing was deleted.\n");
    }
}

int compare_by_total_desc(const void *a, const void *b)
{
    double ta = student_total(a);
    double tb = student_total(b);
    return (ta < tb) - (ta > tb);       /* higher totals come first */
}

int compare_by_id(const void *a, const void *b)
{
    const Student *sa = a;
    const Student *sb = b;
    return strcmp(sa->id, sb->id);
}

void sort_students(StudentList *list)
{
    if (list->size == 0) {
        printf("No students yet.\n");
        return;
    }
    int choice;
    if (!input_int("Sort by (1 = total, highest first; 2 = ID, ascending): ", 1, 2, &choice)) {
        return;
    }
    qsort(list->data, list->size, sizeof(Student),
          choice == 1 ? compare_by_total_desc : compare_by_id);
    list->modified = true;
    show_all(list);
}

void show_statistics(const StudentList *list)
{
    if (list->size == 0) {
        printf("No students yet.\n");
        return;
    }
    printf("%-10s%9s%9s%9s%11s\n", "Course", "Average", "Highest", "Lowest", "Pass rate");
    for (int c = 0; c < COURSE_COUNT; c++) {
        double sum = 0.0;
        double max = list->data[0].scores[c];
        double min = max;
        size_t passed = 0;
        for (size_t i = 0; i < list->size; i++) {
            double score = list->data[i].scores[c];
            sum += score;
            if (score > max) {
                max = score;
            }
            if (score < min) {
                min = score;
            }
            if (score >= PASS_SCORE) {
                passed++;
            }
        }
        printf("%-10s%9.2f%9.1f%9.1f%10.1f%%\n", COURSE_NAMES[c],
               sum / list->size, max, min, 100.0 * passed / list->size);
    }

    size_t best = 0;
    for (size_t i = 1; i < list->size; i++) {
        if (student_total(&list->data[i]) > student_total(&list->data[best])) {
            best = i;
        }
    }
    printf("Top student: %s (%s), total %.1f\n", list->data[best].name,
           list->data[best].id, student_total(&list->data[best]));
}

/* ======================== Reading and writing the file ======================== */

/* Saves in CSV format, one student per line: ID,name,score1,score2,score3 */
bool save_to_file(StudentList *list, const char *path)
{
    FILE *fp = fopen(path, "w");
    if (fp == NULL) {
        perror(path);
        return false;
    }
    for (size_t i = 0; i < list->size; i++) {
        const Student *s = &list->data[i];
        fprintf(fp, "%s,%s", s->id, s->name);
        for (int c = 0; c < COURSE_COUNT; c++) {
            fprintf(fp, ",%.2f", s->scores[c]);
        }
        fprintf(fp, "\n");
    }
    if (fclose(fp) != 0) {
        perror(path);
        return false;
    }
    list->modified = false;
    printf("Saved %zu record%s to %s.\n", list->size, plural(list->size), path);
    return true;
}

/*
 * Splits line into fields at the commas (modifying line); an empty field still counts.
 * Stores the start of each field in fields and returns the number of fields,
 * or -1 if there are more than max
 */
int split_fields(char *line, char *fields[], int max)
{
    int count = 0;
    char *p = line;
    while (true) {
        if (count == max) {
            return -1;                  /* too many fields */
        }
        fields[count] = p;
        count++;
        char *comma = strchr(p, ',');
        if (comma == NULL) {
            return count;               /* this is the last field */
        }
        *comma = '\0';                  /* end the current field at the comma */
        p = comma + 1;                  /* the next field starts after the comma */
    }
}

/* Parses one CSV line (modifying line in the process); returns true on success */
bool parse_line(char *line, Student *s)
{
    char *fields[2 + COURSE_COUNT];
    int count = split_fields(line, fields, 2 + COURSE_COUNT);
    if (count != 2 + COURSE_COUNT
        || is_blank(fields[0]) || is_blank(fields[1])
        || strlen(fields[0]) >= ID_LEN || strlen(fields[1]) >= NAME_LEN) {
        return false;
    }
    strcpy(s->id, fields[0]);
    strcpy(s->name, fields[1]);
    for (int c = 0; c < COURSE_COUNT; c++) {
        char *end;
        s->scores[c] = strtod(fields[2 + c], &end);
        if (end == fields[2 + c] || !is_blank(end)
            || !(s->scores[c] >= 0.0 && s->scores[c] <= 100.0)) {   /* written this way, it also rejects nan */
            return false;
        }
    }
    return true;
}

bool load_from_file(StudentList *list, const char *path)
{
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        printf("Data file %s not found; starting with an empty list.\n", path);
        return false;
    }
    char line[LINE_LEN];
    int line_no = 0;
    size_t loaded = 0;
    while (fgets(line, sizeof line, fp) != NULL) {
        line_no++;
        line[strcspn(line, "\r\n")] = '\0';     /* remove the line ending (Windows \r\n included) */
        if (is_blank(line)) {
            continue;
        }
        Student s;
        if (!parse_line(line, &s)) {
            printf("Warning: line %d is malformed and was skipped.\n", line_no);
            continue;
        }
        if (list_find_by_id(list, s.id) >= 0) {
            printf("Warning: line %d repeats ID %s and was skipped.\n", line_no, s.id);
            continue;
        }
        if (!list_append(list, &s)) {
            printf("Out of memory; stopped loading.\n");
            break;
        }
        loaded++;
    }
    fclose(fp);
    printf("Loaded %zu record%s from %s.\n", loaded, plural(loaded), path);
    return true;
}

/* ======================== Main menu ======================== */

void print_menu(void)
{
    printf("\n=========== Student Grade Manager ===========\n");
    printf("  1. Add a student      2. List all\n");
    printf("  3. Search             4. Update scores\n");
    printf("  5. Delete a student   6. Sort\n");
    printf("  7. Statistics         8. Save\n");
    printf("  0. Exit\n");
    printf("=============================================\n");
}

int main(void)
{
    StudentList list;
    list_init(&list);
    load_from_file(&list, DATA_FILE);

    while (true) {
        print_menu();
        int choice;
        if (!input_int("Choose an option (0-8): ", 0, 8, &choice)) {
            choice = 0;                         /* input has ended: treat it as Exit */
        }

        switch (choice) {
        case 1:
            add_student(&list);
            break;
        case 2:
            show_all(&list);
            break;
        case 3:
            search_student(&list);
            break;
        case 4:
            modify_student(&list);
            break;
        case 5:
            delete_student(&list);
            break;
        case 6:
            sort_students(&list);
            break;
        case 7:
            show_statistics(&list);
            break;
        case 8:
            save_to_file(&list, DATA_FILE);
            break;
        default:                                /* 0: exit */
            if (list.modified && confirm("You have unsaved changes. Save them?")) {
                save_to_file(&list, DATA_FILE);
            }
            list_free(&list);
            printf("Goodbye!\n");
            return 0;
        }
    }
}

18.5 A closer look at the key code

1. Why not just read input with scanf?

As Chapters 3 and 6 explained, when scanf meets invalid input it leaves it in the buffer, and if you don't handle that properly, the program loops forever; and as Chapter 9 explained, reading strings with it also risks overflow. So this project uses a more reliable approach: read a whole line with `fgets` first, then parse and check that line.

read_line reads one line and uses its return value to distinguish three cases: success (1), end of input (0), and a line that was too long (-1; the excess has been read and thrown away, so it won't disturb the next read). Built on top of it:

  • input_int parses an integer with strtol and checks four conditions: at least one digit was converted (end != buf), only whitespace follows the number (is_blank(end)), there was no overflow (errno == 0), and the value is in the allowed range. If any condition fails, the user is asked to try again. input_score works the same way with strtod.
  • Every input function returns false when input ends (when read_line returns 0), and its caller then abandons the current operation. When reading a menu choice fails, main treats it as "Exit," so even if input ends early (say, the user pressed Ctrl + D), the program won't loop forever.

You can reuse this set of input helpers in any command-line program you write from now on.

2. Growing the dynamic array

list_append is essentially the same as vec_push from Chapter 12: when the array is full, realloc doubles its capacity, and the result of realloc goes into a temporary pointer first, so the existing data survives if it fails. list_remove_at removes an element by shifting every element after it forward by one position.

3. Editing a copy when updating scores

modify_student doesn't change the student in the list directly. It first makes a copy, updated, enters the new scores into the copy, and writes the copy back only when every input has succeeded. That way, if input ends halfway through, the original data isn't left "half new, half old." This idea of "either everything succeeds, or nothing changes" is called atomicity in the world of databases.

4. Choosing the sort order with function pointers

c
qsort(list->data, list->size, sizeof(Student),
      choice == 1 ? compare_by_total_desc : compare_by_id);

The last argument of qsort is a pointer to the comparison function, and here the conditional operator picks one of the two comparison functions to pass in. compare_by_total_desc compares two doubles, again using the form (a < b) - (a > b) (notice that the order is the reverse of ascending order, which makes it descending).

5. Loading the file, and tolerating errors

load_from_file reads the file line by line with fgets, and removes the line ending with strcspn(line, "\r\n"); removing \r as well keeps it compatible with files edited on Windows. parse_line first calls split_fields to split the line at the commas, then checks the number of fields, that the ID and name aren't blank, the lengths of the strings, and the range of each score; if any check fails, the line is judged invalid. It deliberately doesn't use strtok from Chapter 9, because strtok treats a run of consecutive commas as a single separator and silently skips empty fields: split with strtok, a line with an extra empty field, such as 2024001,,Tom,85,90,95, yields just 5 fields and would be accepted as valid data. split_fields finds the commas one at a time with strchr, so an empty field still counts as a field, and the field-count check can be trusted. The score check is written as !(score >= 0.0 && score <= 100.0) rather than score < 0.0 || score > 100.0, because strtod also accepts the text nan ("not a number"), and every <, <=, >, >=, or == comparison involving NaN is false: the second form would let it through, while the first rejects it. Invalid lines and lines with duplicate IDs are skipped with a warning, rather than making the whole load fail: people may have edited the data file by hand, so the program should be as "forgiving" as it can.

6. Lining up the table

Widths in printf, such as %-16s, %-20s, and %8.1f, keep the table's columns aligned; the ID column is 16 characters wide, so even the longest allowed ID (15 characters) is followed by at least one space. This works because everything in the table is ASCII, where each character is one byte and takes up one column on screen. A name containing non-ASCII characters such as é has more bytes than columns, so its row would drift out of line (Section 3.1 mentioned this), and a name longer than 20 characters pushes the rest of its row to the right. For a console program, those are acceptable trade-offs.

7. The unsaved-changes reminder

The modified flag in StudentList records whether the data has changed since it was last saved: adding, updating, deleting, and sorting set it to true, and a successful save sets it back to false. On exit, the user is asked about saving only when there really are unsaved changes. The helper function plural returns an "s" whenever a count isn't 1, so the messages read "1 record" but "4 records."

18.6 Building and running

bash
$ gcc -Wall -Wextra -std=c11 student_manager.c -o student_manager
$ ./student_manager

While developing and testing, it's a good idea to add AddressSanitizer:

bash
$ gcc -Wall -Wextra -std=c11 -g -fsanitize=address,undefined student_manager.c -o student_manager

Suppose students.csv already contains the data for three students:

text
2024001,Alice Smith,85.50,92.50,78.00
2024002,Bob Johnson,90.00,88.00,95.00
2024003,Carol Lee,72.00,58.00,66.00

Here's one run of the program (to save space, the menu is shown only the first time; the later repetitions are left out):

text
Loaded 3 records from students.csv.

=========== Student Grade Manager ===========
  1. Add a student      2. List all
  3. Search             4. Update scores
  5. Delete a student   6. Sort
  7. Statistics         8. Save
  0. Exit
=============================================
Choose an option (0-8): 2
ID              Name                    Math English Science   Total Average
2024001         Alice Smith             85.5    92.5    78.0   256.0   85.33
2024002         Bob Johnson             90.0    88.0    95.0   273.0   91.00
2024003         Carol Lee               72.0    58.0    66.0   196.0   65.33
3 students in total.

Choose an option (0-8): 1
ID: 2024004
Name: David Smith
Math score: abc
A score must be a number from 0 to 100. Please try again.
Math score: 101
A score must be a number from 0 to 100. Please try again.
Math score: 59.5
English score: 70.5
Science score: 80
Student added.

Choose an option (0-8): 7
Course      Average  Highest   Lowest  Pass rate
Math          76.75     90.0     59.5      75.0%
English       77.25     92.5     58.0      75.0%
Science       79.75     95.0     66.0     100.0%
Top student: Bob Johnson (2024002), total 273.0

Choose an option (0-8): 6
Sort by (1 = total, highest first; 2 = ID, ascending): 1
ID              Name                    Math English Science   Total Average
2024002         Bob Johnson             90.0    88.0    95.0   273.0   91.00
2024001         Alice Smith             85.5    92.5    78.0   256.0   85.33
2024004         David Smith             59.5    70.5    80.0   210.0   70.00
2024003         Carol Lee               72.0    58.0    66.0   196.0   65.33
4 students in total.

Choose an option (0-8): 0
You have unsaved changes. Save them? (y/n): y
Saved 4 records to students.csv.
Goodbye!
Tip
Because the data file is plain CSV, you can open students.csv in a spreadsheet program to look at it. On Windows, though, Excel may lock a file while it has it open, so close the file in Excel before choosing Save in the program; otherwise fopen fails and the program prints an error message instead of saving.

18.7 A test checklist

Once the program is written, act like a "picky user" and deliberately try all kinds of unusual operations. Here's a checklist to go through item by item:

TestExpected result
At the menu, enter letters, -1, 9, or an empty line"Please enter a whole number from 0 to 8." and a new prompt
Add a student whose ID already existsA message that the ID already exists; nothing is added
Enter abc, -5, 101, or 85pts as a scoreA new prompt for the score
Enter an ID longer than 15 charactersA message that it's too long, and a new prompt
Enter a name containing a commaA message that commas aren't allowed, and a new prompt
Search for part of a name, such as SmithEvery student whose name contains Smith is listed
List, sort, or show statistics when the list is empty"No students yet."
Search for, update, or delete an ID that doesn't existA message that there's no such student
Answer n when asked to confirm a deletionNothing is deleted, and the data is unchanged
The data file contains malformed lines or duplicate IDsThose lines are skipped with a warning, and the rest load normally
A line in the data file has an empty field or an extra comma (such as 2024001,,Tom,85,90,95)The line is judged malformed and skipped with a warning
The data file doesn't existA message, then an empty list to start with
Press Ctrl + D in the middle of input (on Windows, Ctrl + Z and then Enter)The program exits normally instead of looping forever
Do all of the above with a build compiled with -fsanitize=addressNo memory errors or leaks are reported
Tip
You can write your test input into a text file and run it all at once with input redirection (Chapter 14): ./student_manager < test_input.txt. Run it again after every code change, and any new bug you introduce shows up right away. This is the idea behind regression testing.

18.8 Extension challenges

Once the basic version works, try some of these extensions, listed roughly from easiest to hardest:

  1. Score bands. Add to the statistics the number of students in each score band (such as 90–100, 80–89, 70–79, 60–69, and below 60), and draw a simple horizontal histogram with *.
  2. Split into several files. Following Chapter 15, split the program into input.h/.c (input helpers), student.h/.c (the student list and features), storage.h/.c (file I/O), and main.c, and write a Makefile for it.
  3. Automatic backups. Before saving, use the rename function from <stdio.h> to rename the old data file to students.csv.bak.
  4. A linked-list version. Replace the dynamic array with a linked list from Chapter 13 for storing students, and implement sorting yourself with a linked-list sorting algorithm.
  5. Configurable courses. Put the course names on the first line of the data file, and have the program decide the number and names of the courses from it (you'll need to allocate each student's scores dynamically).
  6. Undo. Use a "stack" to record the most recent changes, and implement "undo the last step."
  7. Logins and permissions. Add two roles, teacher and student: teachers can change any data, while students can only look up their own grades.

Chapter 19: Where to Go Next

19.1 Looking back: what you've learned

If you've worked carefully through the earlier chapters and finished the exercises and the capstone project, you can now:

  • Set up a C development environment, and understand the whole journey from source code to executable;
  • Use all the basic syntax: data types, operators, branches, loops, functions, arrays, and strings;
  • Understand memory, addresses, and pointers, and draw diagrams of a program's memory;
  • Organize data with structures, allocate memory on demand, and implement a data structure such as a linked list;
  • Read and write files, build multi-file projects, and track down problems with a debugger and memory-checking tools;
  • Complete, on your own, a small system of about six hundred lines with a clear structure, robust input handling, and no memory leaks.

That's a very solid starting point. Where you go from here depends on your interests and goals.

19.2 What to learn next

1. Data structures and algorithms (the most recommended next step)

Whatever direction you take, data structures and algorithms are the core of computer science. They're central to technical interviews and to computer science exams. With C and linked lists under your belt, you can move on to stacks, queues, trees (binary trees, binary search trees, balanced trees), heaps, hash tables, and graphs, along with algorithms such as quicksort, merge sort, binary search, recursion and divide-and-conquer, greedy algorithms, and dynamic programming. As you learn, implement every data structure yourself in C, and practice extensively on problem-solving sites.

2. Computer systems

If you want to understand "how programs really run on a computer"—how data is represented in the machine, how C code becomes machine instructions, what linking is all about, the memory hierarchy and caches, processes and virtual memory—read Computer Systems: A Programmer's Perspective (CS:APP) and do its accompanying labs. The book explains everything in C, so it picks up right where this guide leaves off.

3. Operating systems and Linux systems programming

C is the "native language" of operating systems. Learning the systems programming interfaces for processes, threads, file systems, inter-process communication, and network sockets lets you write programs that truly work hand in hand with the operating system.

4. Embedded development

If hardware interests you, start with a microcontroller board such as an Arduino, a Raspberry Pi Pico, an STM32, or an ESP32, and use C to control LEDs, sensors, and motors—building things you can see and touch. This path also calls for some knowledge of digital circuits and hardware.

5. Other programming languages

With a foundation in C, other languages come much more easily:

  • C++: adds object-oriented programming, templates, and a powerful standard library (the STL) on top of C; widely used in games, graphics, high-performance systems, and more;
  • Rust: a modern systems programming language focused on memory safety, whose compiler stops errors common in C, such as dangling pointers and data races, at compile time. Having learned C first, you'll deeply appreciate why Rust is designed the way it is;
  • Python, Go, Java, and others: used for web development, data analysis, artificial intelligence, and many other application areas, with high development productivity.
TitleAuthor(s)LevelNotes
C Primer PlusStephen PrataBeginnerDetailed explanations and plenty of examples; long, suited to systematic study
C Programming: A Modern ApproachK. N. KingBeginner to intermediateComprehensive and rigorous, with excellent exercises; regarded by many as one of the best C textbooks
The C Programming LanguageBrian W. Kernighan, Dennis M. RitchieOnce you have a foundationThe classic co-written by C's creator, Dennis Ritchie: thin but dense, and worth rereading
Pointers on CKenneth A. ReekIntermediateAn in-depth, thorough treatment of pointers
C Traps and PitfallsAndrew KoenigIntermediateVery short; devoted entirely to the places where C trips people up
Expert C Programming: Deep C SecretsPeter van der LindenIntermediateStories from C's history and its inner workings; highly entertaining
Computer Systems: A Programmer's PerspectiveRandal E. Bryant, David R. O'HallaronIntermediateUnderstanding computer systems from a programmer's point of view; essential reading for computer science students
Data Structures and Algorithm Analysis in CMark Allen WeissIntermediateA classic data structures textbook, with everything implemented in C

19.4 Online resources

  • cppreference (en.cppreference.com): the most authoritative and convenient reference for the C standard library; the first place to look up how a function works.
  • Compiler Explorer (godbolt.org): write C code in your browser and see the assembly it compiles to in real time—a great tool for understanding what happens under the hood.
  • CS50 (cs50.harvard.edu): Harvard's introductory computer science course, free online. The first half uses C, the lectures are lively, and the problem sets are fun.
  • Beej's Guide to C Programming (beej.us/guide/bgc): a free, friendly online book that covers C from the basics through the standard library.
  • Practice sites: LeetCode (leetcode.com) focuses on interview-style algorithm problems; Exercism's C track (exercism.org/tracks/c) offers bite-sized exercises checked by automated tests; Codeforces (codeforces.com) runs regular programming contests; Project Euler (projecteuler.net) has math-flavored problems that are great for practicing loops and algorithms.
  • Read well-written open-source code: cJSON (a small JSON parsing library whose core is a single .c file and a single .h file), the Lua interpreter, Redis, and SQLite are all known for high-quality code. Start with the small projects and learn how experienced programmers organize their code.

19.5 Project ideas

"Use it as soon as you learn it" is the best way to make knowledge stick. Here are some projects well suited to C:

DifficultyProjectWhat it exercises
BeginnerAddress book, library manager, expense trackerStructures, files, menus (the structure of Chapter 18 is a good model)
BeginnerAn expression calculator that handles parentheses and precedenceStacks, string parsing
IntermediateCommand-line games: Snake, Minesweeper, 2048, GomokuTwo-dimensional arrays, game loops, random numbers
IntermediateA text file compressor (Huffman coding)Trees, priority queues, bitwise operations, binary files
IntermediateA simple shell (command-line interpreter)String parsing, process management (requires learning systems programming)
ChallengeA text editor (see the open-source project kilo, a usable editor in about a thousand lines of C)Terminal control, data structure design
ChallengeA JSON parser, a simple interpreterLexical analysis, parsing, recursive descent
ChallengeAn HTTP server; your own memory allocator (malloc)Network programming, systems programming, memory management

19.6 Final words

There are no shortcuts in learning to program, but there is a method: write lots of code, read lots of code, debug a lot, and reflect a lot. Don't get discouraged when things get hard. Every bug that drives you up the wall deepens your understanding of computers a little more, and every segmentation fault you track down on your own is real, solid growth.

C is a language that "lets you see everything." It hides no details from you, and it takes no responsibility off your hands—and precisely because of that, once you've mastered C, you hold a key to understanding the whole world of computing.

Here's to going far on your programming journey!

Appendices

Appendix A: Answers to Exercises

Most programming exercises can be solved correctly in more than one way; the answers here are just one possibility. If your program gives correct results and its logic is clear, it's a good answer. Try each exercise on your own first, then compare, paying most attention to differences in approach. To save space, some answers show only the key function or code fragment.

Chapter 1

1. Business card

c
#include <stdio.h>

int main(void)
{
    printf("====================\n");
    printf("Name: Jane Doe\n");
    printf("Major: Computer Science\n");
    printf("Motto: Talk is cheap. Show me the code.\n");
    printf("====================\n");
    return 0;
}

2. Making mistakes on purpose

With GCC, you'll see roughly the following errors (the wording may vary slightly between versions):

  • Deleting the semicolon: error: expected ';' before 'return'. GCC marks the position at the end of the line that's missing the semicolon; the return in the message is the first word of the next line;
  • Changing printf to Printf: implicit declaration of function 'Printf'; did you mean 'printf'?, followed at link time by undefined reference to 'Printf';
  • Deleting the closing brace: error: expected declaration or statement at end of input;
  • Using curly quotes: error: stray '\342' in program.

3. Star triangle

c
#include <stdio.h>

int main(void)
{
    printf("   *\n");
    printf("  ***\n");
    printf(" *****\n");
    printf("*******\n");
    return 0;
}

Chapter 2

1. The program is the same as the one in Section 2.3. On 64-bit Windows, long is 4 bytes; long double is usually 16 bytes with MinGW and 8 bytes with Visual Studio.

2. The valid ones are _count, Int, total_sum, and score3. The invalid ones are 2nd_place (starts with a digit), my-name (contains a minus sign), int (a keyword), and a b (contains a space). Note that although _count is valid, some names that start with an underscore are reserved for the compiler and the standard library, so it's best not to start your own names with one. Int is valid because C is case-sensitive, which makes it different from the keyword int, but a name like that invites confusion.

3.

c
#include <stdio.h>

int main(void)
{
    char c = 'm';
    printf("Uppercase: %c\n", c - 'a' + 'A');       // c - 32 also works
    printf("Position in the alphabet: %d\n", c - 'a' + 1);
    printf("Three letters later: %c\n", c + 3);
    return 0;
}

Output:

text
Uppercase: M
Position in the alphabet: 13
Three letters later: p

4. The output is 3 3.0 3.5 3 4294967295. 7 / 2 is integer division, giving 3; b also starts out as the integer 3 and then becomes 3.0; 7.0 / 2 is floating-point division, giving 3.5; (int)3.99 discards the fractional part, giving 3; and -1 in two's complement is 32 ones, which read as an unsigned int is 4294967295 (assuming a 32-bit int).

Chapter 3

1. Circumference and area

c
#include <stdio.h>

#define PI 3.14159265358979

int main(void)
{
    double r;
    printf("Enter the radius: ");
    scanf("%lf", &r);
    printf("Circumference: %.2f\n", 2 * PI * r);
    printf("Area: %.2f\n", PI * r * r);
    return 0;
}

Output:

text
Enter the radius: 3
Circumference: 18.85
Area: 28.27

2. Sum and average

c
#include <stdio.h>

int main(void)
{
    int a, b, c;
    printf("Enter three integers: ");
    scanf("%d %d %d", &a, &b, &c);
    int sum = a + b + c;
    printf("Sum: %d\n", sum);
    printf("Average: %.2f\n", sum / 3.0);   // 3.0 rather than 3, to avoid integer division
    return 0;
}

3. Uppercase to lowercase

c
#include <stdio.h>

int main(void)
{
    char ch;
    printf("Enter an uppercase letter: ");
    scanf(" %c", &ch);
    char lower = ch - 'A' + 'a';          // ch + 32 also works
    printf("Lowercase: %c, ASCII code: %d\n", lower, lower);
    return 0;
}

4. Bases and formats

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter a non-negative integer: ");
    scanf("%d", &n);
    printf("Decimal: %d\n", n);
    printf("Octal: %o\n", n);
    printf("Hexadecimal: %x\n", n);
    printf("[%10d]\n", n);
    printf("[%-10d]\n", n);
    printf("[%010d]\n", n);
    return 0;
}

Output for the input 255:

text
Enter a non-negative integer: 255
Decimal: 255
Octal: 377
Hexadecimal: ff
[       255]
[255       ]
[0000000255]

%o and %x expect an unsigned integer, which is why the program asks for a non-negative number. (If you enter a negative number anyway, in practice it's printed as an unsigned value: with a 32-bit int, -1 comes out as 37777777777 in octal and ffffffff in hexadecimal.)

Chapter 4

1. Splitting a three-digit number

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter a three-digit number: ");
    scanf("%d", &n);
    int hundreds = n / 100;
    int tens = n / 10 % 10;
    int ones = n % 10;
    printf("hundreds %d, tens %d, ones %d\n", hundreds, tens, ones);
    return 0;
}

2. Converting seconds

c
#include <stdio.h>

int main(void)
{
    int total;
    printf("Enter a number of seconds: ");
    scanf("%d", &total);
    int hours = total / 3600;
    int minutes = total % 3600 / 60;
    int seconds = total % 60;
    printf("%02d:%02d:%02d\n", hours, minutes, seconds);
    return 0;
}

3. The first line prints 1 2 -1 -2: integer division truncates toward zero, so -5 / 3 is -1, and the result of % has the sign of the dividend, so -5 % 3 is -2. The second line prints 0 1 0: x++ > 0 compares using the old value of x, 0, so it's false, and then x becomes 1. Because the left side of && is false, evaluation short-circuits: the y++ on the right never runs, so y stays 0.

4. Largest of three

c
#include <stdio.h>

int main(void)
{
    int a, b, c;
    printf("Enter three integers: ");
    scanf("%d %d %d", &a, &b, &c);
    int max = (a > b) ? a : b;       // first, the larger of a and b
    max = (max > c) ? max : c;       // then compare it with c
    printf("Largest: %d\n", max);
    return 0;
}

You could also write it as a single nested expression, (a > b) ? (a > c ? a : c) : (b > c ? b : c), but the two-step version is easier to read.

Chapter 5

1. Sorting three numbers

c
#include <stdio.h>

int main(void)
{
    int a, b, c, temp;
    printf("Enter three integers: ");
    scanf("%d %d %d", &a, &b, &c);

    if (a > b) {            // make sure a <= b
        temp = a;
        a = b;
        b = temp;
    }
    if (a > c) {            // make sure a is the smallest
        temp = a;
        a = c;
        c = temp;
    }
    if (b > c) {            // make sure b <= c
        temp = b;
        b = c;
        c = temp;
    }
    printf("Smallest to largest: %d %d %d\n", a, b, c);
    return 0;
}

2. Days in a month

c
#include <stdio.h>

int main(void)
{
    int year, month, days;
    printf("Enter a year and a month: ");
    scanf("%d %d", &year, &month);

    switch (month) {
    case 1: case 3: case 5: case 7: case 8: case 10: case 12:
        days = 31;
        break;
    case 4: case 6: case 9: case 11:
        days = 30;
        break;
    case 2:
        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
            days = 29;
        } else {
            days = 28;
        }
        break;
    default:
        printf("Invalid month\n");
        return 1;
    }
    printf("Month %d of %d has %d days\n", month, year, days);
    return 0;
}

3. Tiered electricity pricing

c
#include <stdio.h>

int main(void)
{
    double kwh, bill;
    printf("Enter this month's usage (kWh): ");
    if (scanf("%lf", &kwh) != 1 || kwh < 0) {
        printf("Invalid input\n");
        return 1;
    }

    if (kwh <= 200) {
        bill = kwh * 0.5;
    } else if (kwh <= 400) {
        bill = 200 * 0.5 + (kwh - 200) * 0.55;
    } else {
        bill = 200 * 0.5 + 200 * 0.55 + (kwh - 400) * 0.8;
    }
    printf("Bill: %.2f\n", bill);
    return 0;
}

4. Character classes

c
#include <stdio.h>

int main(void)
{
    char ch;
    printf("Enter a character: ");
    scanf("%c", &ch);

    if (ch >= 'A' && ch <= 'Z') {
        printf("Uppercase letter\n");
    } else if (ch >= 'a' && ch <= 'z') {
        printf("Lowercase letter\n");
    } else if (ch >= '0' && ch <= '9') {
        printf("Digit\n");
    } else {
        printf("Other character\n");
    }
    return 0;
}

After learning <ctype.h> in Chapter 9, you can also use isupper, islower, and isdigit for these tests.

Chapter 6

1. The answer is 2500.

c
#include <stdio.h>

int main(void)
{
    int sum = 0;
    for (int i = 1; i <= 100; i += 2) {    // start at 1 and add 2 each time, so only odd numbers are visited
        sum += i;
    }
    printf("Sum of the odd numbers from 1 to 100: %d\n", sum);
    return 0;
}

2. Narcissistic numbers: the answer is 153, 370, 371, and 407.

c
#include <stdio.h>

int main(void)
{
    for (int n = 100; n <= 999; n++) {
        int a = n / 100;          // hundreds digit
        int b = n / 10 % 10;      // tens digit
        int c = n % 10;           // ones digit
        if (a * a * a + b * b * b + c * c * c == n) {
            printf("%d\n", n);
        }
    }
    return 0;
}

3. Printing a diamond

c
#include <stdio.h>

void print_row(int spaces, int stars)
{
    for (int j = 0; j < spaces; j++) {
        putchar(' ');
    }
    for (int j = 0; j < stars; j++) {
        putchar('*');
    }
    putchar('\n');
}

int main(void)
{
    int n;
    printf("Enter n: ");
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {          // the upper half (including the widest row in the middle)
        print_row(n - i, 2 * i - 1);
    }
    for (int i = n - 1; i >= 1; i--) {      // the lower half
        print_row(n - i, 2 * i - 1);
    }
    return 0;
}

This answer borrows functions from Chapter 7 a little early, moving the code that prints one row into a function so the upper and lower halves don't repeat it. Without a function, just write the contents of print_row into each of the two loops.

4. Reversing an integer

c
#include <stdio.h>

int main(void)
{
    int n;
    printf("Enter a positive integer: ");
    scanf("%d", &n);

    long long reversed = 0;
    int digits = 0;
    int temp = n;
    do {
        reversed = reversed * 10 + temp % 10;   // append the last digit of temp to the end of reversed
        temp /= 10;
        digits++;
    } while (temp != 0);
    printf("Reversed: %lld (%d digits)\n", reversed, digits);
    return 0;
}

reversed is a long long because reversing a number such as 1999999999 would exceed the range of int. Also, a number that ends in 0 (such as 120) loses its leading 0 when reversed, giving 21.

5. The answer is 4037913.

c
#include <stdio.h>

int main(void)
{
    long long factorial = 1, sum = 0;
    for (int i = 1; i <= 10; i++) {
        factorial *= i;         // reuse the previous result: i! = (i-1)! * i
        sum += factorial;
    }
    printf("1! + 2! + ... + 10! = %lld\n", sum);
    return 0;
}

Chapter 7

1. A prime-checking function

c
#include <stdio.h>

int is_prime(int n)
{
    if (n < 2) {
        return 0;
    }
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}

int main(void)
{
    int count = 0;
    for (int n = 2; n < 100; n++) {
        if (is_prime(n)) {
            printf("%4d", n);
            count++;
            if (count % 10 == 0) {
                printf("\n");
            }
        }
    }
    printf("\n");
    return 0;
}

Output:

text
   2   3   5   7  11  13  17  19  23  29
  31  37  41  43  47  53  59  61  67  71
  73  79  83  89  97

2. GCD and LCM

c
#include <stdio.h>

int gcd(int a, int b)
{
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}

int lcm(int a, int b)
{
    return a / gcd(a, b) * b;    // divide first, then multiply, to reduce the risk of overflow
}

int main(void)
{
    int a, b;
    printf("Enter two positive integers: ");
    scanf("%d %d", &a, &b);
    printf("GCD: %d, LCM: %d\n", gcd(a, b), lcm(a, b));
    return 0;
}

gcd can also be written recursively, very concisely: return b == 0 ? a : gcd(b, a % b);.

3. Recursive power

c
#include <stdio.h>

double power(double x, int n)          // recurses n times
{
    if (n == 0) {
        return 1.0;
    }
    return x * power(x, n - 1);
}

double fast_power(double x, int n)     // recurses about log2(n) times
{
    if (n == 0) {
        return 1.0;
    }
    double half = fast_power(x, n / 2);
    if (n % 2 == 0) {
        return half * half;            // x^n = (x^(n/2))^2
    }
    return half * half * x;            // when n is odd, multiply by one more x
}

int main(void)
{
    printf("%.0f %.0f\n", power(2, 10), fast_power(2, 10));   // prints: 1024 1024
    return 0;
}

Each recursive call of fast_power halves n, so it recurses only about log₂n times. Computing x¹⁰⁰⁰, for example, takes just over ten levels of recursion, while power needs a thousand. This algorithm is known as fast exponentiation (or exponentiation by squaring).

4. Printing binary recursively

c
#include <stdio.h>

void print_binary(int n)
{
    if (n >= 2) {
        print_binary(n / 2);    // print the higher bits first (the quotient when divided by 2)
    }
    printf("%d", n % 2);        // then the lowest bit (the remainder when divided by 2)
}

int main(void)
{
    print_binary(13);           // prints: 1101
    printf("\n");
    return 0;
}

This is exactly "divide by 2 repeatedly and read the remainders in reverse": the recursion first dives down to the highest bit, then prints the bits one by one on the way back out, which produces precisely the reversed order.

Chapter 8

1. Rotating right

The most direct approach uses a temporary array: b[(i + k) % n] = a[i]. Here's a cleverer method, the triple reversal, which needs no extra array: reverse the whole array, then reverse the first k elements and the remaining n−k elements separately. With 1 2 3 4 5 and k = 2: 5 4 3 2 1 → 4 5 3 2 1 → 4 5 1 2 3.

c
#include <stdio.h>

#define MAX_N 100

void reverse(int arr[], int left, int right)
{
    while (left < right) {
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
        left++;
        right--;
    }
}

int main(void)
{
    int a[MAX_N];
    int n, k;
    printf("Enter n (1-%d): ", MAX_N);
    if (scanf("%d", &n) != 1 || n < 1 || n > MAX_N) {
        printf("Invalid input\n");
        return 1;
    }
    printf("Enter %d integers: ", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }
    printf("Enter k: ");
    if (scanf("%d", &k) != 1 || k < 0) {
        printf("Invalid input\n");
        return 1;
    }

    k %= n;                 // rotating by n positions is the same as not rotating at all
    reverse(a, 0, n - 1);
    reverse(a, 0, k - 1);
    reverse(a, k, n - 1);

    for (int i = 0; i < n; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");
    return 0;
}

2. The second largest

c
#include <stdio.h>
#include <stdbool.h>

#define N 10

int main(void)
{
    int a[N];
    printf("Enter %d integers: ", N);
    for (int i = 0; i < N; i++) {
        scanf("%d", &a[i]);
    }

    int first = a[0];
    int second = 0;
    bool has_second = false;
    for (int i = 1; i < N; i++) {
        if (a[i] > first) {                 // a new maximum: the old maximum moves down to second place
            second = first;
            has_second = true;
            first = a[i];
        } else if (a[i] < first && (!has_second || a[i] > second)) {
            second = a[i];                  // smaller than the maximum, but larger than the current second
            has_second = true;
        }
    }

    if (has_second) {
        printf("The second largest is %d\n", second);
    } else {
        printf("There is no second largest number\n");
    }
    return 0;
}

3. Removing elements

The most naive method shifts all the later elements forward every time an x is found, but in the worst case that takes about n² moves. A better method uses two indexes: i walks through the array, and k marks "where the next element to keep should go."

c
#include <stdio.h>

#define MAX_N 100

int main(void)
{
    int a[MAX_N];
    int n, x;
    printf("Enter n (1-%d): ", MAX_N);
    if (scanf("%d", &n) != 1 || n < 1 || n > MAX_N) {
        printf("Invalid input\n");
        return 1;
    }
    printf("Enter %d integers: ", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }
    printf("Number to remove: ");
    scanf("%d", &x);

    int k = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] != x) {
            a[k] = a[i];      // keep this element
            k++;
        }
    }
    n = k;                    // the number of elements left

    for (int i = 0; i < n; i++) {
        printf("%d ", a[i]);
    }
    printf("\n");
    return 0;
}

4. Pascal's triangle

c
#include <stdio.h>

#define ROWS 10

int main(void)
{
    int t[ROWS][ROWS];
    for (int i = 0; i < ROWS; i++) {
        t[i][0] = 1;                             // the first number in each row
        t[i][i] = 1;                             // the last number in each row
        for (int j = 1; j < i; j++) {
            t[i][j] = t[i - 1][j - 1] + t[i - 1][j];
        }
    }

    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j <= i; j++) {
            printf("%d ", t[i][j]);
        }
        printf("\n");
    }
    return 0;
}

The last row printed is 1 9 36 84 126 126 84 36 9 1.

Chapter 9

1. Character counts

c
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char line[256];
    printf("Enter a line of text: ");
    if (fgets(line, sizeof(line), stdin) == NULL) {
        return 1;
    }

    int letters = 0, digits = 0, spaces = 0, others = 0;
    for (int i = 0; line[i] != '\0' && line[i] != '\n'; i++) {
        unsigned char c = (unsigned char)line[i];
        if (isalpha(c)) {
            letters++;
        } else if (isdigit(c)) {
            digits++;
        } else if (c == ' ') {
            spaces++;
        } else {
            others++;
        }
    }
    printf("Letters: %d, digits: %d, spaces: %d, other: %d\n", letters, digits, spaces, others);
    return 0;
}

Note: a non-ASCII character such as é takes more than one byte in UTF-8 (two, for é), so it's counted as that many "other" characters.

2. Implementing strcmp

c
int my_strcmp(const char s1[], const char s2[])
{
    int i = 0;
    while (s1[i] != '\0' && s1[i] == s2[i]) {   // skip the part that's the same
        i++;
    }
    return (unsigned char)s1[i] - (unsigned char)s2[i];
}

The loop stops at the first character that differs, or when s1 ends; the sign of the difference between the two characters there decides the result. If the two strings are identical, both stop at '\0', and the difference is 0. Converting to unsigned char matches the behavior of the standard strcmp (the standard says characters are compared as unsigned).

3. Capitalizing each word

c
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>

int main(void)
{
    char line[256];
    printf("Enter a line of English text: ");
    if (fgets(line, sizeof(line), stdin) == NULL) {
        return 1;
    }

    bool in_word = false;                 // are we currently inside a word?
    for (int i = 0; line[i] != '\0'; i++) {
        unsigned char c = (unsigned char)line[i];
        if (isspace(c)) {
            in_word = false;
        } else {
            if (!in_word) {               // the first letter of a word
                line[i] = (char)toupper(c);
            }
            in_word = true;
        }
    }
    printf("%s", line);
    return 0;
}

Keeping a Boolean variable that records "are we inside a word?" is a common technique for word-related problems; you'll use it again to count words in Exercise 1 of Chapter 14.

4. Palindromes

c
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>

bool is_palindrome(const char s[])
{
    int i = 0;
    int j = (int)strlen(s) - 1;
    while (i < j) {
        if (tolower((unsigned char)s[i]) != tolower((unsigned char)s[j])) {
            return false;
        }
        i++;
        j--;
    }
    return true;
}

int main(void)
{
    char s[100];
    printf("Enter a string: ");
    if (scanf("%99s", s) != 1) {
        return 1;
    }
    printf("%s\n", is_palindrome(s) ? "It's a palindrome" : "It's not a palindrome");
    return 0;
}

Chapter 10

1. Minimum and maximum

c
#include <stdio.h>

void min_max(const int *arr, int n, int *min, int *max)
{
    *min = arr[0];
    *max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < *min) {
            *min = arr[i];
        }
        if (arr[i] > *max) {
            *max = arr[i];
        }
    }
}

int main(void)
{
    int a[] = {7, -3, 12, 5, 0};
    int lo, hi;
    min_max(a, 5, &lo, &hi);
    printf("Minimum %d, maximum %d\n", lo, hi);   // prints: Minimum -3, maximum 12
    return 0;
}

2. Reversing with pointers

c
void reverse_array(int *arr, int n)
{
    if (n <= 0) {
        return;
    }
    int *left = arr;
    int *right = arr + n - 1;
    while (left < right) {
        int temp = *left;
        *left = *right;
        *right = temp;
        left++;
        right--;
    }
}

The check at the beginning can't be left out: when n is 0, arr + n - 1 would point before the start of the array, and the C standard says that even computing such a pointer is undefined behavior.

3. The output is:

text
10 20 20 21 30 2
10 21 30 40

Statement by statement: *p++ gets the value of arr[0], 10, and then p points to arr[1]; *p is 20; (*p)++ first gets 20, then adds 1 to arr[1], making it 21, while p stays put; *p is 21; *++p first moves p to arr[2], then gets 30. At this point p points to arr[2], so p - arr is 2.

4. Sorting strings

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int compare_str(const void *a, const void *b)
{
    // the array's elements have type const char *, so a and b are really addresses of const char *
    const char *const *pa = a;
    const char *const *pb = b;
    return strcmp(*pa, *pb);
}

int main(void)
{
    const char *words[] = {"banana", "apple", "cherry", "date", "fig"};
    int n = sizeof(words) / sizeof(words[0]);

    qsort(words, n, sizeof(words[0]), compare_str);

    for (int i = 0; i < n; i++) {
        printf("%s ", words[i]);
    }
    printf("\n");                // prints: apple banana cherry date fig
    return 0;
}

A common mistake is writing the comparison function as return strcmp(a, b);: a and b point to the pointers in the array, not to the strings themselves, so they have to be dereferenced once first.

Chapter 11

1. Day of the year

c
#include <stdio.h>
#include <stdbool.h>

typedef struct {
    int year;
    int month;
    int day;
} Date;

bool is_leap(int year)
{
    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}

int day_of_year(const Date *d)
{
    static const int days_in_month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int total = d->day;
    for (int m = 1; m < d->month; m++) {
        total += days_in_month[m - 1];
    }
    if (d->month > 2 && is_leap(d->year)) {
        total++;                    // a leap year, and February is over: add one more day
    }
    return total;
}

int main(void)
{
    Date d = {2024, 3, 1};
    printf("%d-%02d-%02d is day %d of the year\n", d.year, d.month, d.day, day_of_year(&d));
    return 0;
}

Output:

text
2024-03-01 is day 61 of the year

2. Complex numbers

c
#include <stdio.h>
#include <math.h>

typedef struct {
    double re;    // real part
    double im;    // imaginary part
} Complex;

Complex complex_add(Complex a, Complex b)
{
    Complex r;
    r.re = a.re + b.re;
    r.im = a.im + b.im;
    return r;
}

Complex complex_mul(Complex a, Complex b)
{
    Complex r;
    r.re = a.re * b.re - a.im * b.im;
    r.im = a.re * b.im + a.im * b.re;
    return r;
}

void print_complex(Complex c)
{
    printf("%.2f %c %.2fi\n", c.re, (c.im < 0) ? '-' : '+', fabs(c.im));
}

int main(void)
{
    Complex a = {1, 2};     // 1 + 2i
    Complex b = {3, -4};    // 3 - 4i
    print_complex(complex_add(a, b));   // prints: 4.00 - 2.00i
    print_complex(complex_mul(a, b));   // prints: 11.00 + 2.00i
    return 0;
}

Complex holds just two doubles, so passing and returning it by value costs very little and keeps the code simple. Only when a structure is large do you need to switch to passing pointers.

3. Sorting books

c
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char title[40];
    char author[20];
    double price;
} Book;

int compare_price_desc(const void *a, const void *b)
{
    const Book *x = a;
    const Book *y = b;
    return (x->price < y->price) - (x->price > y->price);
}

int main(void)
{
    Book books[] = {
        {"Data Structures", "Smith", 45.0},
        {"Operating Systems", "Johnson", 59.0},
        {"Computer Networks", "Williams", 52.5},
        {"Compilers", "Brown", 68.0},
        {"Discrete Mathematics", "Davis", 38.0}
    };
    int n = sizeof(books) / sizeof(books[0]);

    qsort(books, n, sizeof(Book), compare_price_desc);

    for (int i = 0; i < n; i++) {
        printf("%6.2f  %-22s %s\n", books[i].price, books[i].title, books[i].author);
    }
    return 0;
}

Output:

text
 68.00  Compilers              Brown
 59.00  Operating Systems      Johnson
 52.50  Computer Networks      Williams
 45.00  Data Structures        Smith
 38.00  Discrete Mathematics   Davis

4. sizeof(struct X) is 24, and sizeof(struct Y) is 16.

struct X: a is at offset 0; a double needs 8-byte alignment, so 7 bytes of padding follow and b is at offset 8; c is at offset 16 and takes 4 bytes; the total size must be a multiple of 8, so another 4 bytes of padding go at the end, for 24 bytes in all.

struct Y: b is at offset 0; c is at offset 8; a is at offset 12; 3 bytes of padding go at the end, for 16 bytes in all. Ordering the members from largest to smallest saves 8 bytes.

Chapter 12

1. A dynamic average

c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int n;
    printf("How many integers? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Invalid input\n");
        return 1;
    }

    int *nums = malloc(n * sizeof *nums);
    if (nums == NULL) {
        printf("Out of memory\n");
        return 1;
    }

    long long sum = 0;
    printf("Enter %d integers: ", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &nums[i]);
        sum += nums[i];
    }

    double avg = (double)sum / n;
    int above = 0;
    for (int i = 0; i < n; i++) {
        if (nums[i] > avg) {
            above++;
        }
    }
    printf("Average: %.2f; above average: %d\n", avg, above);

    free(nums);
    return 0;
}

2. Implementing strdup

c
#include <stdlib.h>
#include <string.h>

// Returns a copy of s, or NULL on failure. The caller is responsible for freeing the returned memory
char *my_strdup(const char *s)
{
    size_t len = strlen(s);
    char *copy = malloc(len + 1);        // +1 leaves room for the '\0'
    if (copy != NULL) {
        memcpy(copy, s, len + 1);        // copy the '\0' along with everything else
    }
    return copy;
}

POSIX systems have provided a strdup function for a long time, and C23 finally added it to <string.h> as well.

3. Dynamic two-dimensional arrays

c
#include <stdio.h>
#include <stdlib.h>

void free_matrix(int **m, int rows)
{
    if (m == NULL) {
        return;
    }
    for (int i = 0; i < rows; i++) {
        free(m[i]);                     // free each row first
    }
    free(m);                            // then the array of pointers itself
}

int **create_matrix(int rows, int cols)
{
    int **m = malloc(rows * sizeof *m);          // step 1: allocate the array of pointers
    if (m == NULL) {
        return NULL;
    }
    for (int i = 0; i < rows; i++) {
        m[i] = malloc(cols * sizeof **m);        // step 2: allocate each row
        if (m[i] == NULL) {
            free_matrix(m, i);                   // failed partway: free the i rows allocated so far
            return NULL;
        }
    }
    return m;
}

int main(void)
{
    int rows, cols;
    printf("Enter the number of rows and columns: ");
    if (scanf("%d %d", &rows, &cols) != 2 || rows <= 0 || cols <= 0) {
        printf("Invalid input\n");
        return 1;
    }

    int **a = create_matrix(rows, cols);
    if (a == NULL) {
        printf("Out of memory\n");
        return 1;
    }
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            a[i][j] = i * j;                     // used just like an ordinary two-dimensional array
            printf("%4d", a[i][j]);
        }
        printf("\n");
    }
    free_matrix(a, rows);
    return 0;
}

Here's the memory layout: a points to an array of pointers, and each of those pointers points to one row of data. Freeing happens in the reverse order of allocation: first the rows, then the array of pointers.

text
  a
  │     +-------+      +----+----+----+----+
  └───▶ |   ●───┼────▶ |    |    |    |    |   row 0
        +-------+      +----+----+----+----+
        |   ●───┼────▶ |    |    |    |    |   row 1
        +-------+      +----+----+----+----+
        |   ●───┼────▶ |    |    |    |    |   row 2
        +-------+      +----+----+----+----+

4. Spot the bugs

There are five errors:

  1. malloc(strlen("Hello, ") + strlen(name)) leaves no room for the '\0', so strcat writes 1 byte past the end; it needs to add 1;
  2. The return value of malloc isn't checked for NULL;
  3. After free(s), the code calls printf("%s\n", s), using memory that has already been freed;
  4. When nums is assigned the second time, the address of the first block is lost, which leaks memory;
  5. free(nums) runs twice, freeing the same memory twice.

Incidentally, if you compile this code with a recent GCC (12 or later) and -Wall, the compiler warns used after 'free' about errors 3 and 5. That shows once again why it pays to turn on warnings—but keep in mind that a compiler can't catch every memory error.

The corrected code:

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *make_greeting(const char *name)
{
    char *buf = malloc(strlen("Hello, ") + strlen(name) + 1);
    if (buf == NULL) {
        return NULL;
    }
    strcpy(buf, "Hello, ");
    strcat(buf, name);
    return buf;
}

int main(void)
{
    char *s = make_greeting("Tom");
    if (s != NULL) {
        printf("%s\n", s);
        free(s);
        s = NULL;
    }

    int *nums = malloc(10 * sizeof(int));
    // ... use nums ...
    free(nums);
    nums = malloc(20 * sizeof(int));   // free the old memory before allocating new memory
    // ... use nums ...
    free(nums);
    nums = NULL;
    return 0;
}

Chapter 13

The functions below all use the Node type from Section 13.5; to test them, put them together with the program from Section 13.5.

1. List length

c
int length_iterative(const Node *head)
{
    int count = 0;
    for (const Node *cur = head; cur != NULL; cur = cur->next) {
        count++;
    }
    return count;
}

int length_recursive(const Node *head)
{
    if (head == NULL) {                        // an empty list has length 0
        return 0;
    }
    return 1 + length_recursive(head->next);   // 1 + the length of the rest
}

2. Reversing a list

c
Node *reverse_list(Node *head)
{
    Node *prev = NULL;
    Node *cur = head;
    while (cur != NULL) {
        Node *next = cur->next;   // (1) remember the next node first
        cur->next = prev;         // (2) point the current node back at the previous one
        prev = cur;               // (3) move prev and cur one step forward
        cur = next;
    }
    return prev;                  // when the loop ends, prev points to what was the last node
}

Taking 1 -> 2 -> 3 as an example, each pass through the loop "turns around" one arrow:

text
start:         NULL   1 -> 2 -> 3 -> NULL
after pass 1:  NULL <- 1    2 -> 3 -> NULL
after pass 2:  NULL <- 1 <- 2    3 -> NULL
after pass 3:  NULL <- 1 <- 2 <- 3        (returns 3)

3. Merging sorted lists

c
Node *merge_sorted(Node *a, Node *b)
{
    Node dummy;                   // a sentinel node: no special case for "the result is empty"
    dummy.next = NULL;
    Node *tail = &dummy;          // tail always points to the last node of the result

    while (a != NULL && b != NULL) {
        if (a->data <= b->data) {
            tail->next = a;
            a = a->next;
        } else {
            tail->next = b;
            b = b->next;
        }
        tail = tail->next;
    }
    tail->next = (a != NULL) ? a : b;   // attach whatever is left, all at once
    return dummy.next;
}

This uses the "sentinel node" trick mentioned in Section 13.6: dummy is a local variable, and its next is the head of the resulting list.

4. The middle node

c
Node *middle_node(Node *head)
{
    Node *slow = head;
    Node *fast = head;
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;          // the slow pointer moves one step at a time
        fast = fast->next->next;    // the fast pointer moves two steps at a time
    }
    return slow;
}

The fast pointer moves twice as fast as the slow one, so when the fast pointer reaches the end of the list, the slow pointer is exactly in the middle. The "fast and slow pointers" technique is very important in linked-list problems; it can also be used to detect whether a list contains a cycle.

Chapter 14

1. Word count

c
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s FILE\n", argv[0]);
        return 1;
    }
    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        perror(argv[1]);
        return 1;
    }

    long lines = 0, words = 0, bytes = 0;
    bool in_word = false;
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        bytes++;
        if (ch == '\n') {
            lines++;
        }
        if (isspace(ch)) {
            in_word = false;
        } else if (!in_word) {      // going from whitespace to non-whitespace: a new word begins
            in_word = true;
            words++;
        }
    }
    fclose(fp);

    printf("%ld lines, %ld words, %ld bytes\n", lines, words, bytes);
    return 0;
}

Run on the hello.txt created in Section 14.2, it prints 2 lines, 5 words, 33 bytes.

2. Converting to uppercase

c
#include <stdio.h>
#include <ctype.h>

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "Usage: %s SOURCE DEST\n", argv[0]);
        return 1;
    }
    FILE *in = fopen(argv[1], "r");
    if (in == NULL) {
        perror(argv[1]);
        return 1;
    }
    FILE *out = fopen(argv[2], "w");
    if (out == NULL) {
        perror(argv[2]);
        fclose(in);
        return 1;
    }

    int ch;
    while ((ch = fgetc(in)) != EOF) {
        fputc(toupper(ch), out);
    }

    fclose(in);
    fclose(out);
    return 0;
}

3. Random access

c
#include <stdio.h>

typedef struct {
    char name[20];
    int age;
    double score;
} Student;

int main(void)
{
    FILE *fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        perror("students.dat");
        return 1;
    }

    fseek(fp, 0, SEEK_END);
    long count = ftell(fp) / (long)sizeof(Student);    // the number of records in the file

    long k;
    printf("There are %ld records. Which one do you want to see? ", count);
    if (scanf("%ld", &k) != 1 || k < 1 || k > count) {
        printf("Record number out of range\n");
        fclose(fp);
        return 1;
    }

    Student s;
    fseek(fp, (k - 1) * (long)sizeof(Student), SEEK_SET);  // jump straight to record k
    if (fread(&s, sizeof(Student), 1, fp) == 1) {
        printf("Record %ld: %s, age %d, score %.1f\n", k, s.name, s.age, s.score);
    }
    fclose(fp);
    return 0;
}

4. Statistics from a data file

c
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("numbers.txt", "r");
    if (fp == NULL) {
        perror("numbers.txt");
        return 1;
    }

    int x, max = 0, count = 0;
    long long sum = 0;
    while (fscanf(fp, "%d", &x) == 1) {
        if (count == 0 || x > max) {
            max = x;
        }
        sum += x;
        count++;
    }
    fclose(fp);

    if (count == 0) {
        printf("The file contains no integers\n");
    } else {
        printf("Count: %d, sum: %lld, average: %.2f, maximum: %d\n",
               count, sum, (double)sum / count, max);
    }
    return 0;
}

Chapter 15

1. Macro traps

c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))

MAX(i++, j) expands to ((i++) > (j) ? (i++) : (j)): first 5 > 3 is compared (after which i becomes 6); the condition is true, so i++ runs again, yielding 6, and then i becomes 7. In the end m is 6 and i is 7—i was incremented twice, which is completely different from what the user expected (m being 5 and i being 6). Worse still, nothing at the point of the call hints at the problem. With an inline function, each argument is evaluated exactly once before the call, so the problem disappears.

2. An assertion macro

c
#include <stdio.h>

#define CHECK(cond)                                                          \
    do {                                                                     \
        if (!(cond)) {                                                       \
            fprintf(stderr, "Check failed: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
        }                                                                    \
    } while (0)

int main(void)
{
    int x = -1;
    CHECK(x > 0);             // prints something like: Check failed: main.c:13: x > 0
    CHECK(x == -1);           // the condition holds, so nothing is printed
    printf("The program keeps running\n");
    return 0;
}

3. A multi-file project

mymath.h:

c
#ifndef MYMATH_H
#define MYMATH_H

int is_prime(int n);
int gcd(int a, int b);
int lcm(int a, int b);

#endif

mymath.c:

c
#include "mymath.h"

int is_prime(int n)
{
    if (n < 2) {
        return 0;
    }
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}

int gcd(int a, int b)
{
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}

int lcm(int a, int b)
{
    return a / gcd(a, b) * b;
}

main.c:

c
#include <stdio.h>
#include "mymath.h"

int main(void)
{
    printf("Is 97 prime? %s\n", is_prime(97) ? "yes" : "no");
    printf("gcd(48, 18) = %d\n", gcd(48, 18));
    printf("lcm(4, 6) = %d\n", lcm(4, 6));
    return 0;
}

The commands to compile it step by step:

bash
$ gcc -Wall -Wextra -c mymath.c
$ gcc -Wall -Wextra -c main.c
$ gcc main.o mymath.o -o app

The Makefile (the command lines start with a Tab):

makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -g

app: main.o mymath.o
	$(CC) $(CFLAGS) -o app main.o mymath.o

main.o: main.c mymath.h
	$(CC) $(CFLAGS) -c main.c

mymath.o: mymath.c mymath.h
	$(CC) $(CFLAGS) -c mymath.c

clean:
	rm -f app *.o

Chapter 16

1. Counting 1 bits

c
int count_ones_simple(unsigned int n)
{
    int count = 0;
    while (n != 0) {
        count += n & 1u;     // check the lowest bit
        n >>= 1;             // shift right by one to check the next bit
    }
    return count;
}

int count_ones_fast(unsigned int n)
{
    int count = 0;
    while (n != 0) {
        n &= n - 1;          // remove the lowest 1 bit each time
        count++;
    }
    return count;
}

The first method loops once for each significant binary digit of n (the position number of the highest 1 bit, plus 1); the second loops exactly as many times as there are 1 bits.

2. The smallest power of 2 no less than n

c
unsigned int next_power_of_two(unsigned int n)
{
    unsigned int p = 1;
    while (p < n) {
        p <<= 1;             // multiply by 2 each time
    }
    return p;
}

This assumes n doesn't exceed 2³¹; otherwise the result can't be represented in a 32-bit unsigned integer.

3. Reversing bits

c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

uint32_t reverse_bits(uint32_t n)
{
    uint32_t result = 0;
    for (int i = 0; i < 32; i++) {
        result = (result << 1) | (n & 1u);   // take n's lowest bit and put it into result's lowest bit
        n >>= 1;
    }
    return result;
}

int main(void)
{
    printf("0x%08" PRIX32 "\n", reverse_bits(0x00000001u));   // prints: 0x80000000
    printf("0x%08" PRIX32 "\n", reverse_bits(0x0000000Fu));   // prints: 0xF0000000
    return 0;
}

4. A bit set

c
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

uint32_t set_add(uint32_t set, int k)
{
    return set | (UINT32_C(1) << k);
}

uint32_t set_remove(uint32_t set, int k)
{
    return set & ~(UINT32_C(1) << k);
}

bool set_contains(uint32_t set, int k)
{
    return (set >> k) & 1u;
}

void set_print(uint32_t set)
{
    printf("{ ");
    for (int k = 0; k < 32; k++) {
        if (set_contains(set, k)) {
            printf("%d ", k);
        }
    }
    printf("}\n");
}

int main(void)
{
    uint32_t a = 0, b = 0;
    a = set_add(a, 1);
    a = set_add(a, 3);
    a = set_add(a, 5);
    b = set_add(b, 3);
    b = set_add(b, 4);
    b = set_add(b, 5);
    b = set_remove(b, 4);

    set_print(a);        // { 1 3 5 }
    set_print(b);        // { 3 5 }
    set_print(a | b);    // union: { 1 3 5 }
    set_print(a & b);    // intersection: { 3 5 }
    return 0;
}

UINT32_C(1) is a macro from <stdint.h> that gives the integer constant 1 corresponding to uint_least32_t (strictly speaking, its type is uint_least32_t after the integer promotions, which isn't necessarily exactly uint32_t; on common platforms it's the unsigned int constant 1u). Shifting it gives the right result for every k from 0 to 31, and avoids the undefined behavior of shifting a signed 1 left by 31 places.

Chapter 17

1. Debugging practice

The problem is the initial value of max, 0. It assumes that at least one element is greater than 0, and it ignores arr[0] altogether. For {5, 1, 2}, both 1 and 2 are greater than 0, so the function returns the index of 2, and the real maximum, 5, is never compared at all; for {-5, -2, -8}, no element is greater than 0, so the function returns 0, while the right answer is 1. The fix is to use the first element as the starting point:

c
int max_index(const int arr[], int n)
{
    int index = 0;                     // assume the first element is the largest
    for (int i = 1; i < n; i++) {
        if (arr[i] > arr[index]) {
            index = i;
        }
    }
    return index;                      // the caller must ensure n >= 1
}

2. Spotting undefined behavior

  • (a) Yes: the loop condition is i <= 3, which accesses the out-of-bounds a[3];
  • (b) No: unsigned arithmetic wraps around modulo 2ⁿ, so u becomes UINT_MAX, which is behavior the standard defines explicitly;
  • (c) Yes: signed integer overflow (assuming a 32-bit int);
  • (d) No: s is an array initialized from a string, and the code modifies the array's own contents;
  • (e) Yes: t points to a string literal, and modifying a string literal is undefined behavior;
  • (f) Yes: i is modified twice in the same expression, with no defined order between the two modifications.

3. Refactoring

c
#include <stdio.h>

#define STUDENT_COUNT 5
#define PASS_SCORE 60.0

double average(const double scores[], int n)
{
    double total = 0.0;
    for (int i = 0; i < n; i++) {
        total += scores[i];
    }
    return total / n;
}

double highest(const double scores[], int n)
{
    double max = scores[0];
    for (int i = 1; i < n; i++) {
        if (scores[i] > max) {
            max = scores[i];
        }
    }
    return max;
}

int main(void)
{
    double scores[STUDENT_COUNT];

    printf("Enter the scores of %d students: ", STUDENT_COUNT);
    for (int i = 0; i < STUDENT_COUNT; i++) {
        if (scanf("%lf", &scores[i]) != 1) {
            fprintf(stderr, "Invalid score\n");
            return 1;
        }
    }

    printf("Average: %.2f, highest: %.2f\n",
           average(scores, STUDENT_COUNT), highest(scores, STUDENT_COUNT));

    for (int i = 0; i < STUDENT_COUNT; i++) {
        const char *result = (scores[i] >= PASS_SCORE) ? "pass" : "fail";
        printf("Student %d: %s\n", i + 1, result);
    }
    return 0;
}

Besides improving the formatting, the refactoring fixes a hidden logic flaw in the original: the original started the maximum at 0, so if every score were negative (unlikely for scores, but possible if the function were reused elsewhere), the result would be wrong.

Appendix B: ASCII Table

ASCII has 128 characters (0–127). Codes 0–31 and 127 are non-printing control characters; the commonly used ones are:

DecHexNameWritten in CMeaning
00x00NUL'\0'The null character, which marks the end of a string
70x07BEL'\a'Alert (bell)
80x08BS'\b'Backspace
90x09HT'\t'Horizontal tab
100x0ALF'\n'Line feed (newline)
130x0DCR'\r'Carriage return
270x1BESC'\x1b'Escape; often used to control terminal colors and the cursor
1270x7FDEL'\x7f'Delete

Codes 32–126 are printable characters:

DecHexCharDecHexCharDecHexCharDecHexChar
320x20(space)560x388800x50P1040x68h
330x21!570x399810x51Q1050x69i
340x22"580x3A:820x52R1060x6Aj
350x23#590x3B;830x53S1070x6Bk
360x24$600x3C<840x54T1080x6Cl
370x25%610x3D=850x55U1090x6Dm
380x26&620x3E>860x56V1100x6En
390x27'630x3F?870x57W1110x6Fo
400x28(640x40@880x58X1120x70p
410x29)650x41A890x59Y1130x71q
420x2A*660x42B900x5AZ1140x72r
430x2B+670x43C910x5B[1150x73s
440x2C,680x44D920x5C\1160x74t
450x2D-690x45E930x5D]1170x75u
460x2E.700x46F940x5E^1180x76v
470x2F/710x47G950x5F_1190x77w
480x300720x48H960x60 ``1200x78x
490x311730x49I970x61a1210x79y
500x322740x4AJ980x62b1220x7Az
510x333750x4BK990x63c1230x7B{
520x344760x4CL1000x64d1240x7C|
530x355770x4DM1010x65e1250x7D}
540x366780x4EN1020x66f1260x7E~
550x377790x4FO1030x67g

A few patterns worth remembering: the digits '0' to '9' are 48–57, the uppercase letters 'A' to 'Z' are 65–90, the lowercase letters 'a' to 'z' are 97–122, and the uppercase and lowercase forms of a letter differ by 32.

Appendix C: Operator Precedence

A smaller level number means higher precedence. Operators at the same level are grouped in the direction given in the "Associativity" column. Note that precedence and associativity decide only how an expression is grouped, not the order in which its operands are evaluated (see Section 4.11).

LevelOperatorMeaningAssociativity
1()Function callLeft to right
[]Array subscript
. ->Member access, member access through a pointer
++ --Postfix increment and decrement
2++ --Prefix increment and decrementRight to left
+ -Unary plus and minus
! ~Logical NOT, bitwise NOT
(type)Cast
* &Dereference, address-of
sizeofSize of
3* / %Multiplication, division, remainderLeft to right
4+ -Addition, subtractionLeft to right
5<< >>Left shift, right shiftLeft to right
6< <= > >=Relational comparisonsLeft to right
7== !=Equal, not equalLeft to right
8&Bitwise ANDLeft to right
9^Bitwise XORLeft to right
10|Bitwise ORLeft to right
11&&Logical ANDLeft to right
12||Logical ORLeft to right
13?:ConditionalRight to left
14= += -= *= /= %= <<= >>= &= ^= |=Assignment and compound assignmentRight to left
15,CommaLeft to right

The combinations that trip people up most often:

ExpressionWhat it actually meansWhat was usually intended
*p++*(p++)Get the value, then advance the pointer (usually exactly what was intended)
x & 1 == 0x & (1 == 0)(x & 1) == 0
a << 1 + 1a << (1 + 1)(a << 1) + 1
*p.x*(p.x)(*p).x, that is, p->x
int *a[10]An array of 10 pointersFor a "pointer to an array," write int (*a)[10]
c = getchar() != EOFc = (getchar() != EOF)(c = getchar()) != EOF

Appendix D: Standard Library Quick Reference

`<stdio.h>`: input and output

FunctionPurpose
printf / fprintf / snprintfFormatted output to the screen / a file / a character array
scanf / fscanf / sscanfFormatted input from the keyboard / a file / a string
getchar / putcharRead / write one character
fgets / fputs / putsRead a line / write a string / write a string and a newline
fopen / fcloseOpen / close a file
fgetc / fputcRead / write one character from or to a file
fread / fwriteRead and write binary data in blocks
fseek / ftell / rewindSet / get / reset the file position
feof / ferrorTest whether an earlier read ran into the end of the file / whether an error has occurred
fflushWrite out the contents of an output buffer immediately
perrorPrint an error message
remove / renameDelete / rename a file

`<stdlib.h>`: general utilities

FunctionPurpose
malloc / calloc / realloc / freeAllocate, allocate zeroed, resize, and free dynamic memory
atoi / atofConvert a string to int / double (no error checking)
strtol / strtoul / strtodConvert a string to long / unsigned long / double (with error checking)
rand / srandGenerate a pseudo-random number / set the random seed
abs / labsAbsolute value of an integer
qsort / bsearchGeneric sorting / binary search
exitEnd the program
getenvGet the value of an environment variable

`<string.h>`: strings and memory

FunctionPurpose
strlenLength of a string
strcpy / strncpyCopy a string
strcat / strncatAppend a string
strcmp / strncmpCompare strings
strchr / strrchrFind the first / last occurrence of a character
strstrFind a substring
strtokSplit a string at separators
strspn / strcspnLength of the initial part made only of / free of the characters in a set
memcpy / memmoveCopy memory (memmove allows the source and destination to overlap)
memsetSet every byte in a block of memory to a value
memcmpCompare two blocks of memory
strerrorGet the description string for an error code

`<ctype.h>`: character handling

FunctionPurpose
isalpha / isdigit / isalnumIs it a letter / a digit / a letter or digit?
isupper / islowerIs it an uppercase / lowercase letter?
isspace / ispunct / isxdigitIs it whitespace / punctuation / a hexadecimal digit?
toupper / tolowerConvert to uppercase / lowercase

`<math.h>`: mathematical functions (on Linux with GCC, you may need -lm at the end of the compile command)

FunctionPurpose
sqrt / cbrtSquare root / cube root
pow / expPower / e raised to a power
log / log10 / log2Natural / base-10 / base-2 logarithm
fabsAbsolute value of a floating-point number
ceil / floor / round / truncRound up / round down / round to nearest / discard the fractional part
fmodFloating-point remainder
sin / cos / tan / atan2Trigonometric functions (arguments in radians)
hypotLength of the hypotenuse of a right triangle

`<time.h>`: time

FunctionPurpose
timeGet the current time (on common systems such as Linux, macOS, and Windows, the number of seconds since January 1, 1970; the C standard itself doesn't specify the encoding)
clockGet the processor time used by the program; divide by CLOCKS_PER_SEC to get seconds, handy for rough timing
difftimeCompute the difference between two times
localtime / strftimeConvert to local time / format a time as a string

Other common headers

HeaderMain contents
<assert.h>The assert macro
<stdbool.h>bool, true, false (keywords as of C23)
<stdint.h> / <inttypes.h>Fixed-width integer types such as int32_t and uint64_t, and their format macros
<limits.h> / <float.h>The ranges of the types, such as INT_MAX and DBL_MAX
<stddef.h>size_t, ptrdiff_t, NULL, offsetof
<errno.h>The errno error code

For the complete list of functions and how to use them, see cppreference.

Appendix E: C Keywords

The 32 keywords of C89/C90, grouped by purpose

CategoryKeywords
Basic typeschar short int long float double void
Type modifierssigned unsigned
User-defined typesstruct union enum typedef
Storage classesauto static extern register
Type qualifiersconst volatile
Branchingif else switch case default
Loopsfor while do
Jumpsbreak continue goto return
Operatorsizeof

Keywords added by later standards

StandardNew keywords
C99inline restrict _Bool _Complex _Imaginary
C11_Alignas _Alignof _Atomic _Generic _Noreturn _Static_assert _Thread_local
C23alignas alignof bool constexpr false nullptr static_assert thread_local true typeof typeof_unqual _BitInt _Decimal32 _Decimal64 _Decimal128

Most of the keywords added in C99 and C11 start with an underscore followed by a capital letter, to avoid clashing with names in existing programs (names of that form are reserved for the language itself). C23 then gave several of the commonly used ones more natural spellings, such as bool for _Bool and static_assert for _Static_assert. C23 also changed the meaning of auto, allowing it to deduce a variable's type the way it does in C++.

Appendix F: Common Error Messages

The messages below are GCC's (Clang's wording is similar). When you run into an error message you don't recognize, you can also paste it into a search engine.

Compile errors and warnings

Error messageMeaningCommon causes and fixes
expected ';' before ...A semicolon is missingLook first at the end of the line where the error is reported (GCC usually marks the missing ; right where it belongs); if the error points to the start of a line, check the end of the line above
'x' undeclared (first use in this function)The variable x hasn't been declaredA misspelled variable name, or you forgot to define the variable
implicit declaration of function 'f'A function was used without being declaredYou forgot to include a header, misspelled the function name, or defined the function after calling it without writing a declaration
stray '\342' in program (or another stray '\3xx')There's an illegal character in the programNon-ASCII characters in the code, most often curly quotes “ ” ‘ ’ or dashes pasted from a word processor or web page, or an invisible non-breaking space (stray '\302'). Retype them as plain ASCII characters
missing terminating " characterA string is missing its closing quoteCheck that the double quotes are paired
expected declaration or statement at end of inputThe file ended unexpectedlyUnmatched braces: a } is missing
'else' without a previous 'if'An else has no matching ifThere's a stray semicolon after the if, or several statements aren't enclosed in braces
break statement not within loop or switchbreak is in the wrong placebreak can be used only inside a loop or a switch
duplicate case valueA switch has two identical case valuesCheck the constants after each case
redefinition of 'x'Something was defined twiceTwo variables or functions with the same name were defined in the same scope
conflicting types for 'f'A function's declaration and definition don't matchCheck the parameters and return type of the declaration and definition; or the function was called before it was declared
too few arguments to function 'f'A function was called with too few argumentsCheck the number of arguments against the function's definition
assignment to expression with array typeAn array was assigned as a wholeArrays can't be assigned with =; copy strings with strcpy
lvalue required as left operand of assignmentThe left side of an assignment isn't something that can be assigned toFor example, a + b = 5, or some cases where == was mistyped as =
format '%d' expects argument of type 'int', but argument 2 has type 'double'A format specifier doesn't match its argument's typeUse the right specifier (%f for a double)
format '%d' expects argument of type 'int *', but argument 2 has type 'int'An argument to scanf isn't an addressYou forgot the &: write scanf("%d", &n)
passing argument 1 of 'f' makes pointer from integer without a castAn integer was passed where a pointer is neededCommon when a character is passed as if it were a string to functions such as strlen or strcpy (for example strlen(c), where c is a char). Newer versions of GCC (14 and later) treat it as an error outright
initialization of 'int *' from incompatible pointer type 'double *'The pointer types don't matchCheck that the pointer has the right type
comparison between pointer and integerA pointer is compared with an integerCommon in if (c == "a"); a character needs single quotes: 'a'
control reaches end of non-void functionA function that returns a value may finish without a returnCheck that every path through the function has a return
suggest parentheses around assignment used as truth valueAn assignment is used as a conditionUsually == was mistyped as =
unused variable 'x'A variable was defined but never usedDelete the extra variable, or check whether you used the wrong variable name somewhere

Link errors

Error messageMeaningCommon causes and fixes
undefined reference to 'f'The definition of f can't be foundA misspelled function name; a .c file left out of the build; or, when using math functions, a missing -lm
undefined reference to 'main'There's no main functionmain is misspelled, or none of the compiled files contains a main
multiple definition of 'x'The same name is defined more than onceUsually a variable or function defined in a header; declare it in the header and define it in exactly one .c file instead

Run-time errors

SymptomMeaningCommon causes
Segmentation faultMemory that may not be accessed was accessedDereferencing a null or wild pointer, going out of an array's bounds, forgetting & in scanf, or recursion so deep that the stack overflows
Floating point exceptionAn arithmetic errorInteger division by 0 or remainder by 0 (despite the name, it's usually caused by integer arithmetic)
free(): double free detectedMemory was freed twiceThe same pointer was passed to free twice
*** stack smashing detected ***A stack array was written past its endA character array that's too small, or an overly long string written with strcpy or scanf("%s")
The program's exit code is -1073741819 (that is, 3221225477, or hex 0xC0000005)An access violation on Windows, the equivalent of a segmentation faultSame as a segmentation fault
The program's exit code is -1073741571 (that is, 3221225725, or hex 0xC00000FD)A stack overflow on WindowsRecursion that's too deep, or a very large local array defined in a function
The program's exit code is -1073741676 (that is, 3221225620, or hex 0xC0000094)Integer division by 0 on WindowsSame as Floating point exception

Visual Studio only

Error messageFix
C4996: 'scanf': This function or variable may be unsafePut #define _CRT_SECURE_NO_WARNINGS on the first line of the source file, or turn off "SDL checks" in the project properties (see Section 3.2)