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
- 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.
- 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.
- 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.
- 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)
| Part | Chapters | Core topics | Suggested time |
|---|---|---|---|
| Part I: The Basics | 0–6 | Tools, data types, input and output, operators, branches, loops | About 2 weeks |
| Part II: The Core | 7–10 | Functions, arrays, strings, pointers | About 2–3 weeks |
| Part III: Going Further | 11–17 | Structures, dynamic memory, linked lists, files, multi-file programs, bitwise operations, debugging | About 2–3 weeks |
| Part IV: Putting It All Together | 18–19 | Capstone project, where to go next | About 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:
10111000 00000101 00000000 00000000 00000000The 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:
mov eax, 5Assembly 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:
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:
| Standard | Published | Main changes |
|---|---|---|
| C89 / C90 | 1989 / 1990 | The first official standard (often called ANSI C); ironed out the differences between compilers |
| C99 | 1999 | // single-line comments, long long, a Boolean type, variable-length arrays, declarations in the middle of code, for (int i = 0; ...) |
| C11 | 2011 | Multithreading support, _Generic, _Static_assert, anonymous structures; removed the dangerous gets function |
| C17 | 2018 | Defect fixes only, no new features (sometimes called C18) |
| C23 | 2024 | bool/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?
- 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.
- 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.
- 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. - 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:
| Strengths | Weaknesses |
|---|---|
| Fast, and uses few resources | You manage memory by hand |
| Close to the hardware; you can work with memory directly | Almost no safety checks: out-of-bounds array accesses, dangling pointers, and similar errors aren't caught automatically |
| Portable: nearly every platform has a C compiler | The standard library is small, so you implement many things yourself |
| Small, simple syntax with few keywords | Some 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
- Practice first. Programming is a craft; reading without practicing teaches you nothing. Type in and run every example in this guide yourself.
- 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.
- 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.
- Draw pictures. With arrays, pointers, and linked lists, sketching memory cells and arrows on paper often solves the problem by itself.
- 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.
- 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).
- 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:
write compile run
hello.c (text file) ──────────────▶ hello.exe (executable) ──────────────▶ output on screen1.2 Installing a development environment
Pick the option that matches your operating system.
Windows option 1: Visual Studio Community (the easiest)
- Download Visual Studio Community (free for individual learners) from Microsoft's website.
- During installation, select the "Desktop development with C++" workload (it includes the C compiler as well).
- 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++. - When your code is ready, press Ctrl + F5 (Start Without Debugging) to run it.
CautionVisual Studio reports error C4996 when you usescanf; see Section 3.2 for the fix.
Windows option 2: VS Code + MinGW-w64 (lightweight and flexible)
- Download and install MSYS2 from msys2.org. The default install location is
C:\msys64. - 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
Yto confirm:
pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain- Add
C:\msys64\ucrt64\binto thePathenvironment variable (Settings → System → About → Advanced system settings → Environment Variables). - Open a new Command Prompt window and type
gcc --version. If you see version information, the installation worked. - 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:
xcode-select --installClick 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:
# Ubuntu / Debian
sudo apt update
sudo apt install build-essential gdb
# Fedora
sudo dnf install gcc gdb
# Arch Linux
sudo pacman -S base-devel gdbAfterward, 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.
TipDon'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:
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}Output:
Hello, World!These seven short lines contain the basic skeleton of every C program. Let's go through them one at a time:
| Code | Meaning |
|---|---|
#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.
printfcan't be writtenPrintf, andmaincan't be writtenMain. - Declarations and ordinary statements end with a semicolon. Statements such as
printf(...);andreturn 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 headerint 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 inint 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.)
CautionSome older textbooks write the main function asvoid main(). That's not standard C, and some compilers reject it or warn about it. Always useint 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:
$ gcc hello.c -o hello
$ ./hello
Hello, World!- The first command runs GCC to compile
hello.c;-o hellonames the resulting programhello. Without-o, the default name isa.outon Linux and macOS anda.exeon Windows. - The second command runs the program. In the Windows Command Prompt, type
hello(orhello.exe); in PowerShell, type.\hello.exe.
For everyday learning, use this "enhanced" compile command instead:
$ gcc -Wall -Wextra -std=c11 -g hello.c -o hello| Option | Effect |
|---|---|
-Wall -Wextra | Turn on the common warnings and extra warnings. Many hidden mistakes show up early as warnings |
-std=c11 | Compile according to the C11 standard |
-g | Include debugging information so you can investigate problems with a debugger (Chapter 17) |
-o hello | Name the output program |
TipTreat 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:
hello.c ──preprocess──▶ hello.i ──compile──▶ hello.s ──assemble──▶ hello.o ──link──▶ hello
source expanded source assembly code object file executable- Preprocessing: handles the directives that start with
#. For example,#include <stdio.h>is replaced by the entire contents ofstdio.h, so the preprocessed file often runs to hundreds or thousands of lines. - Compilation: translates the C code into assembly code. Syntax errors are caught in this step.
- Assembly: translates the assembly code into machine instructions, producing an object file (a
.ofile). - Linking: the machine code for
printfisn't in yourhello.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:
$ 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 executableOpen 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:
// 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:
i = i + 1; // add 1 to i <- useless: the code already says this
i = i + 1; // skip the header line <- useful: explains the intentTipWhile debugging, you can use comments to temporarily "switch off" code. In most editors, select the lines and pressCtrl + /(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:
file:line:column: error: descriptionHere are the errors beginners hit most often (shown as GCC 13 prints them; the wording varies slightly between versions):
| What went wrong | Compiler message | Cause 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 page | error: stray '\342' in program | Non-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 " character | The quotes around a string don't match |
Missing } at the end | error: expected declaration or statement at end of input | The 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:
- Fix the first error first. One mistake often triggers a cascade of follow-on errors; fix the first, and the rest may disappear.
- 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 65001in 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
- 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. - Make mistakes on purpose. Introduce each of these errors into the Hello World program: delete the semicolon, change
printftoPrintf, 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. - Star triangle. Use
printfto print the pattern below (use onlyprintf, no loops):
*
***
*****
*******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:
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.
| Decimal | Binary | Hex | Decimal | Binary | Hex | |
|---|---|---|---|---|---|---|
| 0 | 0000 | 0 | 8 | 1000 | 8 | |
| 1 | 0001 | 1 | 9 | 1001 | 9 | |
| 2 | 0010 | 2 | 10 | 1010 | A | |
| 3 | 0011 | 3 | 11 | 1011 | B | |
| 4 | 0100 | 4 | 12 | 1100 | C | |
| 5 | 0101 | 5 | 13 | 1101 | D | |
| 6 | 0110 | 6 | 14 | 1110 | E | |
| 7 | 0111 | 7 | 15 | 1111 | F |
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:
#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:
Age: 18
Height: 1.75 m
Grade: A
Age next year: 19The %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:
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:
scoreandScoreare two different variables; - They can't be C keywords, such as
int,return, orif(Appendix E has the complete list).
| Name | Valid? | Notes |
|---|---|---|
score | Yes | |
total_score | Yes | Underscores separate the words; recommended |
score2 | Yes | Digits are fine anywhere except at the start |
2score | No | Starts with a digit |
total-score | No | Contains a minus sign |
my score | No | Contains a space |
return | No | It'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.
TipIn 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):
| Type | Typical size | Typical range | Used for | printf format |
|---|---|---|---|---|
char | 1 byte | -128 to 127 | Characters, very small integers | %c / %d |
short | 2 bytes | -32768 to 32767 | Smaller integers | %hd |
int | 4 bytes | About ±2.1 billion | The most common integer type | %d |
long | 4 or 8 bytes | Depends on the platform | Larger integers | %ld |
long long | 8 bytes | About ±9.2×10¹⁸ | Very large integers | %lld |
float | 4 bytes | About 6–7 significant digits | Single-precision floating point | %f |
double | 8 bytes | About 15–16 significant digits | Double-precision floating point; the usual choice | %f |
long double | 8–16 bytes | Depends on the platform | Even 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:
#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:
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) = 16The 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.
CautionWhether plaincharis signed or unsigned is up to the compiler and platform (it's usually signed on x86 and unsigned on some ARM platforms). If you usecharto store small integers, writesigned charorunsigned charexplicitly.
The headers <limits.h> and <float.h> define the ranges of the various types:
#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:
int range: -2147483648 to 2147483647
unsigned int max: 4294967295
long long range: -9223372036854775808 to 9223372036854775807
float significant digits: 6
double significant digits: 15Overflow. 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.
TipUndefined 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:
+5 in sign-magnitude: 0000 0101
-5 in sign-magnitude: 1000 0101But 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.
+5: 0000 0101
invert: 1111 1010
add 1: 1111 1011 <- this is -5 in two's complementLet'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).
TipThe 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:
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 floatA 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:
#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:
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:
- 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). - Prefer `double`.
floathas only 6–7 significant digits, so its errors pile up quickly;doubleis much more precise and, on modern computers, just about as fast. - Don't use floating point for money. Store amounts as integers, in cents.
TipOn Linux, when you compile with GCC a program that uses functions from<math.h>, you may need to add the-lmoption at the end of the command to link the math library, as ingcc 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.
#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:
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 7The 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 acharvariable; 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 typeint, sosizeof('A')equalssizeof(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 singlecharvariable.
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:
#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 as | Meaning |
|---|---|
42 | A decimal integer |
052 | An octal integer (it starts with 0), equal to decimal 42 |
0x2A | A hexadecimal integer, equal to decimal 42 |
0b101010 | A binary integer (standard in C23; GCC and Clang have supported it for a long time) |
42u, 42L, 42LL | Of type unsigned int, long, and long long, respectively |
3.14, 3.14f, 1.5e3 | Floating-point numbers (double by default, float with the f suffix) |
'A' | A character |
"Hello" | A string |
Common mistakeNever pad an integer with leading zeros to "line it up"! Inint x = 010;,xis 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.
#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:
Area of a circle with radius 2.0: 12.57
A year has about 52 weeksUsing 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 ascharandshort, are converted tointbefore they take part in arithmetic (this is called integer promotion; strictly speaking, ifintcan't represent every value of the original type, the value is promoted tounsigned intinstead, but on common platforms they all becomeint); - 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
intand adouble, theintis converted todoublefirst.
3. Explicit conversion (a cast): write (type) to convert a value by hand, as in (double)sum.
#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:
n = 3
avg1 = 3.0, avg2 = 3.5, avg3 = 3.5Some 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:
#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
- Use
sizeofto print the sizes ofchar,short,int,long,long long,float,double, andlong doubleon your computer, and compare them with the table in this chapter. - 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. - Given
char c = 'm';, and without looking at an ASCII table, write expressions for: the uppercase version ofc; the position ofcin the alphabet (withaas 1); and the letter 3 places afterc. Print them withprintfto check. - Without running it, write down what the following code prints, then run it to check:
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:
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:
| Specifier | Meaning | Example | Output |
|---|---|---|---|
%d | Decimal integer (int) | printf("%d", 42) | 42 |
%u | Unsigned decimal integer | printf("%u", 42u) | 42 |
%ld / %lld | long / long long integer | printf("%lld", 10LL) | 10 |
%f | Decimal notation, 6 digits after the point by default | printf("%f", 3.14) | 3.140000 |
%e | Scientific notation | printf("%e", 1500.0) | 1.500000e+03 |
%g | 6 significant digits by default; uses %f or %e style depending on the magnitude of the number, and drops trailing zeros | printf("%g", 3.14) | 3.14 |
%c | A single character | printf("%c", 'A') | A |
%s | A string | printf("%s", "Hi") | Hi |
%x / %X | Hexadecimal (lowercase / uppercase letters) | printf("%x", 255) | ff |
%o | Octal | printf("%o", 8) | 10 |
%p | A pointer (an address) | See Chapter 10 | |
%zu | size_t (such as the result of sizeof) | printf("%zu", sizeof(int)) | 4 |
%% | A literal percent sign | printf("100%%") | 100% |
Controlling width, precision, and alignment. You can add modifiers between the % and the letter:
#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:
[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:
#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:
Item Qty Price
Apple 3 4.50
Banana 12 0.75
Cherry 100 25.80CautionWidths 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%.2frounds the value, but because floating-point numbers are approximations, the result occasionally doesn't "look" rounded. For example,2.675is actually stored as 2.67499999..., soprintf("%.2f", 2.675)prints2.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)prints0.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 sequence | Meaning |
|---|---|
\n | Newline |
\t | Tab; handy for simple alignment |
\\ | A backslash \ |
\" | A double quote |
\' | A single quote |
\0 | The null character (ASCII code 0), which marks the end of a string |
\r | Carriage return (moves the cursor back to the start of the line) |
\a | Alert (some terminals beep) |
For example, to print He said: "C is fun!", write printf("He said: \"C is fun!\"\n");.
Common mistakeFormat specifiers must match the types of their arguments.printf("%d", 3.14)prints adoublewith%dand 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:
#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:
Enter two integers: 3 5
3 + 5 = 8Why 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 type | scanf format | printf 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.
#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:
Enter an integer: 25
scanf read 1 item(s), n = 25Enter an integer: abc
scanf read 0 item(s), n = 0Once 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,scanftriggers compile error C4996:'scanf': This function or variable may be unsafe. Consider using scanf_s instead.Microsoft recommends its ownscanf_sfunction, but not every compiler supportsscanf_s. The fix: put#define _CRT_SECURE_NO_WARNINGSon 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:
#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:
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:
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 mistakeSome online resources usefflush(stdin)to clear the input buffer. The C standard defines whatfflushdoes 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:
#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:
Enter a lowercase letter: b
Its ASCII code is 98, and its uppercase form is BNotice 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.
#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:
Enter your weight (kg): 65
Enter your height (m): 1.75
Your BMI is 21.2After 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
- Circumference and area. Read a circle's radius and print its circumference and area, rounded to 2 decimal places.
- Sum and average. Read three integers and print their sum, plus their average rounded to 2 decimal places. (Hint: watch out for integer division.)
- Uppercase to lowercase. Read an uppercase letter and print the corresponding lowercase letter and its ASCII code.
- 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
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ | Addition | 7 + 2 | 9 |
- | Subtraction | 7 - 2 | 5 |
* | Multiplication | 7 * 2 | 14 |
/ | Division | 7 / 2 | 3 (integer division) |
% | Remainder (modulo) | 7 % 2 | 1 |
Division and remainder need special care:
#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:
7 / 2 = 3
-7 / 2 = -3
7 % 3 = 1
-7 % 3 = -1
7 % -3 = 1
7.0 / 2 = 3.5- Integer division throws away the fractional part (it truncates toward zero), so
7 / 2is 3 and-7 / 2is -3. If at least one operand is a floating-point number, the division is done in floating point. - `%` works only on integers, and the result has the same sign as the dividend (the left operand).
- 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:
| Expression | Purpose | With n = 1234 |
|---|---|---|
n % 10 | Get the last digit | 4 |
n / 10 | Remove the last digit | 123 |
n % 2 == 0 | Test whether n is even | 1 (true) |
n % 7 == 0 | Test whether n is divisible by 7 | 0 (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 as | Equivalent to |
|---|---|
x += 5 | x = x + 5 |
x -= 5 | x = x - 5 |
x *= 5 | x = x * 5 |
x /= 5 | x = x / 5 |
x %= 5 | x = x % 5 |
Let's use these operators in a "making change" program that pays an amount with as few bills as possible:
#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:
Enter an amount in dollars: 388
$100 x 3, $50 x 1, $20 x 1, $10 x 1, $5 x 1, $1 x 34.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 toifirst, and the expression's value isiafter the increment; - Postfix
i++: the expression's value isibefore the increment, and theniis incremented.
#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:
a = 5, i = 6
b = 7, i = 7
c = 7, d = 5, i = 5When i++; or ++i; stands alone as a statement, the two do exactly the same thing: both add 1 to i.
Common mistakeDon't modify a variable and then modify or read it again elsewhere in the same expression, as ini = i++;,a = i++ + i++;, orprintf("%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 doesi++ + ++iequal?" 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):
| Operator | Meaning | Example (a = 5, b = 3) | Result |
|---|---|---|---|
> | Greater than | a > b | 1 |
< | Less than | a < b | 0 |
>= | Greater than or equal to | a >= 5 | 1 |
<= | Less than or equal to | b <= 2 | 0 |
== | Equal to | a == 5 | 1 |
!= | Not equal to | a != b | 1 |
Common mistakeTest for equality with two equals signs,==.if (x = 5)doesn't check whetherxequals 5; it assigns 5 tox, 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 mistakeYou can't copy the math notation1 < x < 10straight into C. It's read as(1 < x) < 10:1 < xis 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 isx > 1 && x < 10.
4.6 Logical operators
Logical operators combine conditions:
| Operator | Meaning | Rule |
|---|---|---|
&& | Logical AND ("and") | True only if both sides are true |
|| | Logical OR ("or") | True if at least one side is true |
! | Logical NOT | True becomes false, and false becomes true |
Truth table (1 means true, 0 means false):
a | b | a && b | a || b | !a |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 |
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, ifAis false, the result must be false, soBisn't evaluated; - For
A || B, ifAis true, the result must be true, soBisn't evaluated.
#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:
r = 0
y = 1, x = 1Short-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:
#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:
Enter a year: 2024
Is 2024 a leap year? 1Try 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:
condition ? expression1 : expression2If 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:
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:
int a, b;
a = (b = 3, b + 2); // first b = 3 runs, then b + 2 is computed; a is 5Its 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):
| Operator | Name | Rule | Example | Result |
|---|---|---|---|---|
& | Bitwise AND | A bit is 1 only if both bits are 1 | a & b | 1000, i.e., 8 |
| | Bitwise OR | A bit is 1 if either bit is 1 | a | b | 1110, i.e., 14 |
^ | Bitwise XOR | A bit is 1 if the two bits differ, 0 if they're the same | a ^ b | 0110, i.e., 6 |
~ | Bitwise NOT | Every 0 becomes 1 and every 1 becomes 0 | ~a | -13 (for an int) |
<< | Left shift | Shifts all bits left, filling with 0s on the right | a << 2 | 110000, i.e., 48 |
>> | Right shift | Shifts all bits right | a >> 2 | 11, 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:
int n = 10;
printf("%zu %zu %zu\n", sizeof(int), sizeof(n), sizeof n); // prints: 4 4 4When 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):
| Level | Operators | Associativity |
|---|---|---|
| 1 (highest) | () [] -> . postfix ++ -- | Left to right |
| 2 | ! ~ prefix ++ -- unary + - * (dereference) & (address-of) sizeof casts | Right 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 operators | Right 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 == 0is meant to "test whether the lowest bit ofxis 0," but==has higher precedence than&, so it's actually read asx & (1 == 0), that is,x & 0, which is always 0. Write(x & 1) == 0instead.a << 1 + 1meansa << 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.
TipNobody 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
- Split a three-digit number. Read a three-digit integer and print its hundreds, tens, and ones digits. For the input
365, printhundreds 3, tens 6, ones 5. - 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, print01:02:05. - Predict the output. Without running it, write down what the following code prints, then run it to check and explain the results:
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);- Largest of three. Read three integers and find the largest one using only the conditional operator
?:(noif).
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:
if (condition) {
statements // run when the condition is true
}Here's the flow:
Is the condition true?
/ \
yes no
| |
run the statements |
\ /
continue with what comes nextFor example, here's how to find the absolute value of an integer:
#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:
Enter an integer: -7
Its absolute value is 7The 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:
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."
TipAlways use braces, however many statements the block contains. Many companies' coding standards make this a hard rule.
Common mistakeDon't put a semicolon afterif (...). Inif (x > 0);, the semicolon is itself an "empty statement": theifcontrols that empty statement, and the code in the braces below runs unconditionally.
5.3 The if-else statement
if-else chooses between two alternatives:
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:
#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:
Enter an integer: 17
17 is odd5.4 else if: choosing among many
When there are several possibilities, you can chain if-else statements together:
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:
#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:
Enter a score (0-100): 85
Grade: BTwo things to notice:
- The order of the conditions matters. The second branch says only
score >= 80, notscore < 90, because reaching that branch means the first condition was false, soscoremust be below 90. But ifscore >= 60came first, a 95 would be graded D. - The input check relies on short-circuiting in
||: ifscanffails, the comparisons after it are never made. Returning a nonzero value frommainsignals 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:
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:
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:
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.
#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:
Enter an expression (e.g. 3 + 4): 10 / 4
10 / 4 = 2.5Rules for using switch:
- The expression in parentheses after
switchmust have an integer type (charcounts as an integer); it can't be a floating-point number or a string; - Each
casemust 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; defaultis 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:
#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:
Level 2
Level 3
OtherAfter 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:
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:
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;
}TipIf you need to define a variable inside acase, wrap thatcase's statements in braces, as incase 1: { int t = 0; ... break; }. For one thing, standards before C23 don't allow a variable definition immediately after acaselabel (some compilers report an error); for another, without braces the variable's scope extends into thecases that follow, and when execution jumps there from anothercase, 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.
#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:
Enter the coefficients a b c of ax^2 + bx + c = 0: 1 -3 2
Two distinct real roots: x1 = 2.00, x2 = 1.00Enter the coefficients a b c of ax^2 + bx + c = 0: 1 2 1
One repeated real root: x1 = x2 = -1.00Enter 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.00iThis 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
- 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;) - 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. - 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.
- 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
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.
┌──▶ Is the condition true? ──no──▶ end the loop
│ │
│ yes
│ ▼
└───── run the loop bodyExample: compute 1 + 2 + 3 + … + 100.
#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:
1 + 2 + ... + 100 = 5050A 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:
#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:
Enter a positive integer: 2024
2024 has 4 digits, which add up to 86.2 The do-while loop
do {
loop body
} while (condition); // note the semicolon at the enddo-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:
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":
#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:
Enter a score (0-100): abc
Enter a score (0-100): 120
Enter a score (0-100): 95
You entered 95Without 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:
for (initialization; condition; update) {
loop body
}It runs in this order:
(1) initialize -> (2) test the condition -> (3) run the body -> (4) update -> (2) test -> (3) -> (4) -> ... until the condition is falseThe 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:
#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:
Enter n (0-20): 20
20! = 2432902008176640000Defining 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:
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
forloop, the "update" part runs first).
#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:
1 2 4 5 7 8 10 11 13 14 16 17 19 20
The first multiple of 7 greater than 100 is 105while (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.
Cautionbreakexits only the loop (or `switch`) that directly contains it. In nested loops, abreakin 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):
#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:
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 i | Spaces | Stars |
|---|---|---|
| 1 | 4 | 1 |
| 2 | 3 | 3 |
| 3 | 2 | 5 |
| 4 | 1 | 7 |
| 5 | 0 | 9 |
The pattern is: spaces = n − i, and stars = 2i − 1.
#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:
*
***
*****
*******
*********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.
#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:
Enter a positive integer: 97
97 is primeExample 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:
| x | y | x % y |
|---|---|---|
| 48 | 18 | 12 |
| 18 | 12 | 6 |
| 12 | 6 | 0 |
| 6 | 0 | Stop: the answer is 6 |
#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:
Enter two positive integers: 48 18
The GCD of 48 and 18 is 6Example 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.
#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:
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 andRAND_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 (timetypically 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 formularand() % (max - min + 1) + min.
TipIf 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:
#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:
Found it: 6 * 7 = 42
Search finishedAnother use is releasing resources in one central place when an error occurs, which Chapter 17 covers.
6.8 Choosing a loop, and common mistakes
| Situation | Recommended 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 stop | while |
| 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:
- Off-by-one errors:
i < nandi <= ndiffer by one pass. After writing a loop, walk through the smallest case (such as n = 1) in your head. - 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. - Changing a `for` loop's variable inside the body: this easily muddles the logic, so avoid it.
- Using floating-point numbers to count passes:
for (double x = 0; x != 1.0; x += 0.1)never ends! Because of floating-point error,xnever becomes exactly 1.0. Use integers to count loop passes.
Exercises
- Compute the sum of all the odd numbers from 1 to 100.
- 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.
- Print a diamond. Read n and print a diamond whose upper half has n rows. For n = 3:
*
***
*****
***
*- 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, printReversed: 54321 (5 digits). - 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:
return_type function_name(parameter_list)
{
function body
}Example: a function that returns the larger of two integers.
#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:
The larger one is 7
The larger one is 12Some terminology:
- Parameters (formal parameters): the names in the function definition, such as
aandbinmax. They act as local variables inside the function; - Arguments (actual arguments): the values passed in when the function is called, such as
xandyinmax(x, y); - Return value: the value handed back to the caller with a
returnstatement. 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:
- The arguments are evaluated (3 and 7) and their values are copied into the parameters
aandb; mainis paused, and execution jumps into the body ofmax;- When
return b;runs, the function ends and carries 7 back to where it was called; - Back in
main, the expressionmax(x, y)has the value 7, which is assigned tom.
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:
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).
CautionBefore C23,int f()andint 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, writevoidwhen there are no parameters.
A few rules:
- Once a
returnstatement 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 areturn; 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:
#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:
Area: 12.57In 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?
#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:
Inside swap: a = 2, b = 1
After swap: x = 1, y = 2The 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.
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 changedSo 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.
#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:
inner x = 20
outer x = 10
g_count = 2When 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.
TipUse 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 ag_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
staticare 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.
#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:
count = 1, normal = 1
count = 2, normal = 1
count = 3, normal = 1A 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)!:
#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:
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120Running factorial(4) first "dives in" level by level, and once it reaches the base case, it "returns" level by level:
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
= 24Every recursive function needs two ingredients:
- A base case: the simplest situation, where the answer can be given directly without recursing;
- 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):
#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:
Loop version: fib(40) = 102334155
Recursive version: fib(40) = 102334155
The recursive version made 204668309 function callsThe 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):
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—
- Move the top n−1 disks from A to B (using C as the spare);
- Move the largest disk, disk n, from A to C;
- 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!
#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:
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 CMoving 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.
TipThe secret to understanding recursion is trust: when you writehanoi(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):
+------------------------+
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:
| Header | Purpose | Common functions |
|---|---|---|
<stdio.h> | Input and output | printf, scanf, getchar, fopen |
<stdlib.h> | General utilities | rand, srand, abs, exit, malloc, qsort |
<string.h> | String handling | strlen, strcpy, strcmp |
<math.h> | Mathematics | sqrt, pow, fabs, sin |
<ctype.h> | Character classification | isdigit, isalpha, toupper |
<time.h> | Date and time | time, clock |
Here's <math.h> as an example:
#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:
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.0Note 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
- 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.
- Give it a good name. Function names are usually verbs or verb phrases, such as
print_report,is_valid, orcalculate_average. Functions that return true or false often start withis_orhas_. - Keep functions short. A common guideline is no more than one screen (about 50 lines).
- Don't take too many parameters. With more than 4 or 5 parameters, consider bundling them into a structure (Chapter 11).
- 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
- 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. - GCD and LCM. Write the functions
int gcd(int a, int b)andint lcm(int a, int b), then read two positive integers and print their greatest common divisor and least common multiple. - 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. - 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)prints1101.
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.
int scores[50]; // defines 50 int "slots" in one go8.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:
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 0CautionIf an array defined inside a function isn't initialized at all (as inint 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:
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]); // 32 / 4 = 8Arrays 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:
#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:
Enter 5 scores: 78 92 85 66 90
Total: 411
Average: 82.20
Highest: 92 (score #2)Common mistakeGoing out of bounds. Forint a[5], the valid indexes are 0 to 4, anda[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, whilefor (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:
#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):
a[0] = 10, address: 0x7ffd5e3c8a10
a[1] = 20, address: 0x7ffd5e3c8a14
a[2] = 30, address: 0x7ffd5e3c8a18
a[3] = 40, address: 0x7ffd5e3c8a1c
a[4] = 50, address: 0x7ffd5e3c8a20The addresses of neighboring elements differ by exactly 4, the size of one int:
a[0] a[1] a[2] a[3] a[4]
+--------+--------+--------+--------+--------+
| 10 | 20 | 30 | 40 | 50 |
+--------+--------+--------+--------+--------+
...8a10 ...8a14 ...8a18 ...8a1c ...8a20Because 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:
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:
#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:
90-100: 3
80-89: 3
70-79: 2
60-69: 2
Below 60: 2The 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:
#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:
1 2 3 4 5
2 4 6 8 10Two things happen here that are completely different from ordinary variables:
- 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.
- Inside the function, `sizeof` can't tell you the array's length. Inside the function,
arris really a pointer, sosizeof(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):
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#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:
Before: 5 2 9 1 7 3
After: 1 2 3 5 7 9Selection sort. In each pass, select the smallest element of the unsorted part and move it to the front of the unsorted part:
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.
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.
#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:
Index of 42: 4
Index of 50: -1Here's how the search for 42 proceeds:
| low | high | mid | a[mid] | Comparison |
|---|---|---|---|---|
| 0 | 7 | 3 | 23 | 23 < 42, so search the right half |
| 4 | 7 | 5 | 57 | 57 > 42, so search the left half |
| 4 | 4 | 4 | 42 | Found it! |
Tipmid = (low + high) / 2overflows whenlow + highexceeds the range ofint; writinglow + (high - low) / 2avoids 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:
int scores[3][4]; // 3 rows and 4 columns: 12 elements in allThink 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.
#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:
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.33A two-dimensional array is stored row by row in memory (row-major order). int m[2][3] is laid out like this:
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:
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):
int n;
scanf("%d", &n);
int a[n]; // the length isn't known until run timeBut 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
- 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 5right by 2 gives4 5 1 2 3. - 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.
- 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.
- 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":
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1Chapter 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'`.
#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:
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:
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:
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:
#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:
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 justTom. - 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.
namehas 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 inline(or the string's length if there's no newline); writing'\0'at that position removes the newline; - It returns
NULLif reading fails or the input has ended.
Common mistakeThegetsfunction 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 usegets; usefgetsinstead.
CautionWhen you mixscanfandfgets,scanfleaves a newline in the buffer, and thefgetsthat 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:
| Function | What 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 |
#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:
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 7strstr 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:
- 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." - You can't assign a string with `=`.
char s[20]; s = "Hello";is an error (arrays can't be assigned as a whole); usestrcpy(s, "Hello");instead. - `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.
- `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:
char small[6];
snprintf(small, sizeof(small), "%s", copy); // writes at most 5 characters plus one '\0'
printf("%s\n", small); // prints: HelloSplitting a string with `strtok`. strtok can cut a string into pieces at the separators you specify:
#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:
[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'`.
#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:
Length: 9
Copy: Hello, C!
Reversed: !C ,olleHCautionreversedeliberately converts the length toint. If you usedsize_t(an unsigned type) directly and wrotej = len - 1, then for an empty stringlen - 1wouldn'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:
| Function | What 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.
#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:
hELLO wORLD 2026!CautionThe functions in<ctype.h>require their argument to be a value in the range ofunsigned char(orEOF). The bytes of non-ASCII characters such asémay be negative when stored in achar, and passing them in directly is undefined behavior, which is why the code above converts each character tounsigned charfirst.
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:
#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:
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:
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.5The 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:
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:
#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:
Alice
Bob
Charlie
Diana
Tomnames[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
- Character counts. Read a line of text (which may contain spaces) and count its letters, digits, spaces, and other characters separately.
- Implement strcmp. Write a function
int my_strcmp(const char s1[], const char s2[])whose return value follows the same rules asstrcmp(negative if the first string comes first, 0 if they're equal, positive otherwise). - 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 cbecomesHello World From C. - Palindromes. Read a string and decide whether it's a palindrome (it reads the same forward and backward), ignoring case. For example,
Levelandnoonare palindromes, buthelloisn'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:
#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:
a = 10, address: 0x7ffe3b1a5c3c
b = 3.14, address: 0x7ffe3b1a5c40
c = X, address: 0x7ffe3b1a5c3bThe 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:
int *p; // p is a pointer variable: it can hold the address of an int variableBasic 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.
#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:
a = 10
&a = 0x7ffc8a2e4b5c
p = 0x7ffc8a2e4b5c
*p = 10
After *p = 20, a = 20As a picture:
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:
- How many bytes a dereference reads or writes: for an
int *,*preads or writes 4 bytes; for adouble *, 8 bytes; - The step size of pointer arithmetic: how many bytes
p + 1moves forward (Section 10.5).
Common mistakeint* p, q;defines a pointerpand an ordinary integerq, because the*applies only to the variable name right after it. To define two pointers, writeint *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:
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:
int *p; // a wild pointer: p holds a random address
*p = 10; // dangerous! writes to an unknown addressA 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:
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:
#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:
x = 2, y = 1 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 directlyArguments 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:
#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:
17 / 5 = 3 remainder 2Pointer parameters used to "bring results back" like this are often called output parameters.
10.5 Pointer arithmetic
Pointers support a small set of operations:
| Operation | Meaning |
|---|---|
p + n, p - n | Moves the pointer forward (backward) by n elements, not n bytes |
p++, p-- | Moves the pointer by one element |
p - q | How 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.
#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:
p = 0x7ffd4f6c1e20, *p = 10
p + 1 = 0x7ffd4f6c1e24, *(p + 1) = 20
p + 2 = 0x7ffd4f6c1e28, *(p + 2) = 30
q = 0x7ffd4f6c1e1c
q + 1 = 0x7ffd4f6c1e1d
last - p = 4Adding 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.
CautionPointer arithmetic is meaningful only inside an array. The result ofp + norp - nmust 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 - qand 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 withNULL.
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:
arr[i] ≡ *(arr + i) ≡ *(p + i) ≡ p[i]In fact, C defines the subscript operation a[i] as *(a + i).
#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):
30 30 30 30
10 20 30 40 50
sizeof(arr) = 20
sizeof(p) = 8An array name and a pointer aren't exactly the same thing. They differ in these ways:
- An array name can't be assigned to or changed.
arr = p;andarr++are both errors, whereas a pointer variable can be pointed anywhere you like. - `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). - `&arr` has a different type.
&arrhas the same numeric value asarr, but its type is "pointer to the whole array," so&arr + 1skips over the entire array (moving 20 bytes forward), whilearr + 1moves 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:
| Expression | Meaning |
|---|---|
*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 |
*++p | Moves p forward by one element first, then gets the value it points to |
++*p | Adds 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:
#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:
Hello world 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:
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 as | Can you change the value through p? | Can you point p somewhere else? |
|---|---|---|
const int *p (same as int const *p) | No | Yes |
int *const p | Yes | No |
const int *const p | No | No |
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).
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 elseThe 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:
#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:
a = 10, *p = 10, **pp = 10
a = 20 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 **.
#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:
The largest value is 9, at index 1
Now the array is: 3 0 4 7Double 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).
#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:
Mon Tue Wed Thu Fri Sat Sun
row[1][2] = 7
(*row)[0] = 5Storing 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.
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:
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:
#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:
$ ./args hello 123 "C language"
There are 4 command-line arguments:
argv[0] = ./args
argv[1] = hello
argv[2] = 123
argv[3] = C languageargc(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, andargv[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.
#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:
add: 9
mul: 18
6 + 3 = 9
6 - 3 = 3
6 * 3 = 18The declaration syntax for function pointers is rather convoluted, but you can give the type an alias with typedef:
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:
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.
#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:
Ascending: 3 7 19 42 56 88
Descending: 88 56 42 19 7 3CautionMany resources write the comparison function asreturn x - y;. Whenxandyare far apart (say, one is close toINT_MAXand 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
- 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.
- Always keep two things apart: the value of the pointer variable itself (an address), and the value it points to (
*p). - Remember the common pointer mistakes:
| Mistake | Consequence | Prevention |
|---|---|---|
| Using an uninitialized (wild) pointer | Reads and writes random memory; crashes or corrupts data | Initialize pointers when you define them; use NULL if you don't know where they should point yet |
| Dereferencing a null pointer | The program crashes (segmentation fault) | Check for NULL before using the pointer |
| Returning the address of a local variable | A dangling pointer; the data can be overwritten at any moment | Never return the address of a local variable |
| Going out of an array's bounds through a pointer | Same as going out of bounds with an index | Check boundary conditions carefully |
| Modifying a string literal | Undefined behavior; usually a crash | Point to string literals with const char * |
| Mismatched pointer types | Both the number of bytes accessed and their interpretation are wrong | Take the compiler's type warnings seriously, and be careful with casts |
Exercises
- 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. - Reverse with pointers. Write a function
void reverse_array(int *arr, int n)that reverses an array using only pointer arithmetic (no[]subscripts). - Analyze the output. Without running it, write down what the following code prints, then run it to check:
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]);- Sorting strings. Use
qsortto sort the array of pointersconst char *words[] = {"banana", "apple", "cherry", "date", "fig"};into dictionary order, and print the result. (Hint: the array's elements have typeconst char *, so the comparison function actually receives the addresses ofconst 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
#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:
1001 Alicia 19 88.5
1002 Bob 20 92.0
1003 Alicia 19 88.5The 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 ins1.score. Each member can be used exactly like an ordinary variable of the same type; for example, you read one withscanfasscanf("%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:
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 anymoretypedef 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.
#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:
Carol 18
Age inside try_modify: 99
Name: Carol, age: 18, score: 100.0add_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:
#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:
Leaderboard:
1. Bob 950
2. David 910
3. Eve 880
4. Alice 820
5. Carol 760qsort 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);.
Cautionqsortisn'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:
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 level11.7 Structure alignment in memory
The memory a structure takes up isn't necessarily the sum of its members' sizes:
#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):
sizeof(struct A) = 12
sizeof(struct B) = 8
Member offsets in A: c1 = 0, i = 4, c2 = 8The 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:
- Each member's offset must be a multiple of that member's alignment requirement. For basic types such as
char,int, anddouble, the alignment requirement is usually the type's size; an array has the same alignment requirement as its element type, sochar name[20], for example, needs only 1-byte alignment; - 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):
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:
#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:
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
caselabels in aswitch. - 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.
#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:
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:
#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:
Area of shape 1: 3.14
Area of shape 2: 12.0011.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:
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:
#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:
Your hand: 7 of Hearts, K of Spades, A of Diamonds, 10 of Clubs, Q of HeartsThe 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
- Day of the year. Define a date structure
Date(with year, month, and day), and write a functionint 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. - Complex numbers. Define a structure
Complexfor 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. - 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.
- Something to think about. On a common 64-bit system, what are the
sizeofvalues of the two structures below? Work them out yourself first, then write a program to check.
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):
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:
| Stack | Heap | |
|---|---|---|
| What's stored there | Local variables, function parameters | Memory requested with malloc and friends |
| Who manages it | The compiler, which allocates and frees it automatically | The programmer, who requests and frees it by hand |
| Lifetime | Ends when the function it belongs to returns | From the request until free is called |
| Size | Fairly small (usually 8 MB on Linux, 1 MB on Windows) | Large, limited by the memory available in the system |
| When the size is decided | Usually 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:
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#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:
How many integers? 5
Enter 5 integers: 1 2 3 4 5
In reverse: 5 4 3 2 1Key points for using malloc and free:
- Get the number of bytes right.
malloctakes a number of bytes, not a number of elements, so multiply by the size of each element. An even better way to write it ismalloc(n * sizeof *arr): using*arrinstead of the type name means that if the type ofarrever changes, this line doesn't have to change with it. - Always check whether the return value is `NULL`. When memory runs out,
mallocreturnsNULL, and using it directly crashes the program. - Newly allocated memory isn't initialized; it contains garbage values.
- What you borrow, you must return. Every successful
mallocmust be matched by exactly onefree. Forgetting to free memory causes a memory leak. - 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 isNULL, 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. - In C, the
void *returned bymalloccan 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.
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:
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:
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 successAlso, 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.
#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:
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.50Each 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:
// 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:
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 leakThis 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.
TipWhen 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:
#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:
0 1 2 3
10 11 12 13
20 21 22 23This 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:
| Error | Example | Consequence |
|---|---|---|
| Memory leak | Allocating and forgetting to free | The program uses more and more memory; a long-running program eventually runs out |
| Losing the pointer | p = malloc(...); p = malloc(...); | The first block's address is lost and can never be freed |
| Using freed memory | free(p); printf("%d", *p); | Undefined behavior: you may read garbage or corrupt other data |
| Freeing twice | free(p); free(p); | Undefined behavior; usually a crash |
| Out-of-bounds access | Allocating 5 elements and accessing a 6th | Corrupts the heap's internal bookkeeping; often crashes in strange ways much later |
| Freeing memory not from the heap | int a; free(&a); | Undefined behavior; usually a crash |
| Freeing an address other than the start | free(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 size | malloc(n) or malloc(sizeof(p)) | Not enough space was allocated, leading to out-of-bounds access |
Not checking for NULL | Using malloc's return value directly | The program crashes when memory runs out |
Good habits for avoiding these errors:
- Check for
NULLimmediately after allocating; - As you write the
malloc, decide where the matchingfreewill go; - Set the pointer to
NULLright afterfree; - Keep "allocate" and "free" in pairs at the same level of the code (like
vec_initandvec_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:
#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;
}$ 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:5ASan 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):
$ valgrind --leak-check=full ./programIt reports detailed information about illegal reads and writes (Invalid read, Invalid write) and memory leaks (definitely lost and so on).
TipWhile you're learning, get into the habit of compiling a debug build with-fsanitize=address,undefined(undefinedalso 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=addressoption.
Exercises
- A dynamic average. First read how many integers there will be (n), then use
mallocto 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. - Implement strdup. Write a function
char *my_strdup(const char *s)that allocates a block of memory just big enough to hold the strings, copiessinto it, and returns the new string's address (the caller is responsible for freeing it). - Dynamic two-dimensional arrays. Read a number of rows m and columns n, and create an m × n two-dimensional
intarray by allocating "an array of pointers plus each row separately," so that elements can be accessed asa[i][j]. Fill in some data (such asa[i][j] = i * j), print it, and finally free all the memory correctly. - Spot the bugs. Find all the memory errors in the code below, explain their consequences, and fix them:
#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:
- 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.
- 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.
head
│
▼
+----+---+ +----+---+ +----+------+
| 10 | ●─┼───▶ | 20 | ●─┼───▶ | 30 | NULL |
+----+---+ +----+---+ +----+------+
data pointerA 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.
| Array | Linked list | |
|---|---|---|
| Memory | Contiguous | Scattered, connected by pointers |
| Accessing the i-th element | Fast: the address is calculated directly, O(1) | Slow: you must walk from the start, O(n) |
| Inserting or deleting at a known position | Slow: 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) |
| Size | Fixed, or must grow as a whole | Nodes can be added or removed at any time |
| Extra overhead | None | Every 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
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:
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:
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:
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|●]──▶ NULLThe 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:
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 node5. 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:
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|●]──▶ NULLIf 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:
#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:
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: NULLTry 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.
TipWhen 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.
NULL ◀──[● | 10 | ●]◀──▶[● | 20 | ●]◀──▶[● | 30 | ●]──▶ NULL- Circular linked lists: the last node's
nextpoints 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
- List length. Write functions, one with a loop and one with recursion, that count the nodes in a list.
- 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 -> NULLbecomes3 -> 2 -> 1 -> NULL. This is one of the most frequently asked linked-list interview questions. - 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).
- 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.csvfiles, 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 stream | Meaning | Default device |
|---|---|---|
stdin | Standard input | The keyboard |
stdout | Standard output | The screen |
stderr | Standard error | The 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.
#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:
Read: Hello, File!
Read: Today is 2026-09-26After 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:
| Mode | Meaning | If the file doesn't exist | If the file exists |
|---|---|---|---|
"r" | Read only | Opening fails | Reading starts at the beginning |
"w" | Write only | A new file is created | Its contents are erased! |
"a" | Append | A new file is created | Writing continues at the end |
"r+" | Read and write | Opening fails | Reading and writing start at the beginning |
"w+" | Read and write | A new file is created | Its contents are erased! |
"a+" | Read and append | A new file is created | You 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 mistakeOpening 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:
Could not open the file: No such file or directoryAbout 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:
#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:
33 bytes, 2 lineswhile ((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:
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:
#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:
Record 1: Alice, age 19, score 88.5
Record 2: Bob, age 20, score 92.0
Record 3: Carol, age 18, score 76.5The students.txt it creates looks like this, and people can read and edit it directly:
Alice 19 88.5
Bob 20 92.0
Carol 18 76.5The 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:
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.
#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:
Wrote 3 records of 32 bytes each
Alice 19 88.5
Bob 20 92.0
Carol 18 76.5Text and binary formats each have their strengths and weaknesses:
| Text format | Binary format | |
|---|---|---|
| Readability | Good: can be viewed and edited directly | Poor: needs a dedicated program to read |
| Speed | Slower: numbers must be converted to and from characters | Fast: memory is copied directly |
| Precision | Floating-point values can lose precision (depending on how many digits are written) | Exact |
| Portability | Good | Poor: 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:
| Function | What 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:
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:
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...):
#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:
$ 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 directoryA 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.
TipOn the command line, standard input and output can also be "redirected" to files:./program < input.txtmakes the program read its input frominput.txt(as if someone were typing the file's contents on the keyboard for you), and./program > output.txtwrites the output tooutput.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
- 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
wccommand does. - Convert to uppercase. Write a program
upper SOURCE DESTthat converts every lowercase letter in the source file to uppercase and writes the result to the destination file. - Random access. Using the
students.datcreated in Section 14.6, read a record number k (starting from 1), usefseekto jump directly to the k-th record, read it, and display it. Print a message if the number is out of range. - Statistics from a data file. The text file
numbers.txtcontains 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:
#define MAX_STUDENTS 100
#define PI 3.14159265358979Function-like macros take parameters and look like functions, but they're nothing more than plain text substitution, which leads to some classic traps:
#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:
SQUARE_BAD(1 + 2) = 5
SQUARE(1 + 2) = 9
10 * DOUBLE_BAD(3) = 33
10 * DOUBLE(3) = 60SQUARE_BAD(1 + 2)is replaced by1 + 2 * 1 + 2, which, because*has higher precedence than+, means1 + (2 * 1) + 2: the result is 5, not 9;10 * DOUBLE_BAD(3)is replaced by10 * (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):
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):
#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:
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.
| Directive | Meaning |
|---|---|
#if constant_expression | Keep the code that follows if the expression is true |
#elif constant_expression | Like else if |
#else | Otherwise |
#endif | Ends the conditional block |
#ifdef NAME | If this macro is defined |
#ifndef NAME | If 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:
#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:
$ gcc log_demo.c -o log_demo
$ ./log_demo
sum = 55Define 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:
$ 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 = 55Use 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:
#ifdef _WIN32
#define CLEAR_SCREEN "cls"
#else
#define CLEAR_SCREEN "clear"
#endifUse 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:
| Macro | Meaning |
|---|---|
__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):
#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);
#endifstats.c (the module's implementation):
#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):
#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:
$ 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 timesSeveral 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.
// 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 programAny 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:
$ 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 executableThe 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 message | Common 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:
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 *.oA Makefile is made up of rules, each of which has the form:
target: dependencies
<Tab>command that builds the targetTake 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.
CautionIn a Makefile, the indentation in front of a command must be a single Tab character, not spaces; otherwise you get the errormissing separator. On Windows, you can install make in MSYS2 withpacman(in the MinGW toolchain, the command may be namedmingw32-make). Also, the Windows Command Prompt has normcommand, so usedelthere 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
- Macro traps. Write the macros
MAX(a, b)andABS(x), using parentheses correctly. Then think: after runningint i = 5, j = 3; int m = MAX(i++, j);, what areiandm? Why is this usage dangerous? Would there still be a problem with aninlinefunction? - An assertion macro. Write a macro
CHECK(cond)that, when the conditioncondis 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.) - A multi-file project. Turn the
is_prime,gcd, andlcmfunctions from the Chapter 7 exercises into a module: declarations inmymath.h, implementations inmymath.c, and calls to them inmain.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:
#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:
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:
| Operator | Rule | An intuitive way to think of it |
|---|---|---|
& AND | The result bit is 1 only if both bits are 1 | "Filters" bits: AND with 0 gives 0; AND with 1 leaves the bit unchanged |
| OR | The result bit is 1 if either bit is 1 | "Turns on" bits: OR with 1 gives 1; OR with 0 leaves the bit unchanged |
^ XOR | The 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 |
~ NOT | 0 becomes 1 and 1 becomes 0 | Flips every bit |
<< Left shift | Shifts all bits left, filling with 0s on the right | Shifting left by n multiplies by 2ⁿ (when it doesn't overflow) |
>> Right shift | Shifts all bits right | For non-negative numbers, shifting right by n divides by 2ⁿ (rounding down) |
Things to watch out for with bitwise operations:
- 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
uafter its constants. - 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 << 32is undefined behavior, and the result isn't necessarily 0. - Watch the precedence. Shifts have lower precedence than addition and subtraction, and
&,^, and|have lower precedence than the comparison operators, sox & 1 == 0actually meansx & (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):
| Operation | Expression |
|---|---|
| Test for odd or even | n & 1u: 1 means odd, 0 means even |
| Get bit k | (n >> k) & 1u |
| Set bit k to 1 | n | (1u << k) |
| Clear bit k to 0 | n & ~(1u << k) |
| Flip bit k | n ^ (1u << k) |
| Remove the lowest 1 bit | n & (n - 1) |
| Keep only the lowest 1 bit | n & (~n + 1) |
| Test whether n is a power of 2 | n != 0 && (n & (n - 1)) == 0 |
#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:
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? yesWhy 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.
#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:
rw-
rwx
r-x
Has read permissionUse | 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:
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>:
| Type | Meaning |
|---|---|
int8_t, int16_t, int32_t, int64_t | Signed integers of exactly 8/16/32/64 bits |
uint8_t, uint16_t, uint32_t, uint64_t | Unsigned integers of exactly 8/16/32/64 bits |
intptr_t, uintptr_t | Integer types that can hold a pointer's value |
To print these types, use the format macros defined in <inttypes.h>:
#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:
a = -123456
b = 255
c = 18446744073709551615Macros 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:
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 addressThe x86 computers we use every day, and the vast majority of ARM devices (phones included), are little-endian. You can check with a program:
#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:
x = 0x12345678
byte 0: 0x78
byte 1: 0x56
byte 2: 0x34
byte 3: 0x12
This machine is little-endianThis 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:
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:
#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:
5.75 sign 0 | exponent 10000001 | fraction 01110000000000000000000
-1 sign 1 | exponent 01111111 | fraction 00000000000000000000000
0.1 sign 0 | exponent 01111011 | fraction 10011001100110011001101Look 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
- 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 usingn & (n - 1)(so that the loop runs as many times as there are 1s). - 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.
- 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,0x00000001reversed is0x80000000. - A bit set. Use a
uint32_tto 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:
#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):
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
| Kind | When it happens | Typical examples | How it shows up |
|---|---|---|---|
| Compile error | At compile time | Syntax errors, misspellings, mismatched types | The compiler reports an error |
| Link error | At link time | Undefined functions, duplicate definitions | The linker reports an error, often mentioning ld |
| Run-time error | While running | Segmentation faults, division by zero, memory errors | The program crashes, or a checking tool catches it |
| Logic error | While running | A wrong algorithm, wrong boundary conditions | The 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:
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:
#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:
5! = 0Let'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):
$ 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 = 0After 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 |
finish | Runs 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 expression | Shows the value of this expression automatically every time the program pauses |
info locals | Shows 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 variable | Sets 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:
#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;
}$ 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:13Read 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.
TipThe 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:
#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:
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:
// 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:
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 behavior | Example |
|---|---|
| Accessing an array out of bounds | int a[5]; a[5] = 1; |
| Dereferencing a null or wild pointer | int *p = NULL; *p = 1; |
| Using memory that has been freed | free(p); *p = 1; |
| Freeing the same memory twice | free(p); free(p); |
| Using the value of an uninitialized variable | int x; printf("%d", x); |
| Signed integer overflow | int x = INT_MAX; x + 1; |
| Integer division by 0 | int x = 5 / 0; |
| Modifying a string literal | char *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 modification | i = 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 type | 1 << 32 (with a 32-bit int) |
| A format specifier that doesn't match its argument's type | printf("%d", 3.14); |
Using the return value of a function that didn't execute a return | A 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 it | int *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_countandfind_max); - Write macros and constants in all capitals (such as
MAX_SIZE); - Capitalize the names of your own types (such as
StudentandIntVector); - Choose names that say what they mean; avoid names like
a,b, ortemp1(loop variablesiandjexcepted); - Start the names of functions that return a Boolean with
is_orhas_.
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-formatcan 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
constto 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 withscanf; prefer functions with length limits, such asfgetsandsnprintf; - 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:
#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:
#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
- 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}.
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;
}- Spotting undefined behavior. Which of the code fragments below contain undefined behavior? Explain why.
// (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++;- 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.
#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:
| Topic | How 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:
| Option | Feature | Description |
|---|---|---|
| 1 | Add a student | Enter 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 |
| 2 | List all | Show every student's course scores, total, and average in a table |
| 3 | Search | Look up a student by exact ID, or by any part of the name (typing Smith finds every student whose name contains Smith) |
| 4 | Update scores | Find a student by ID and enter all of their scores again |
| 5 | Delete a student | Delete a student by ID, after asking for confirmation |
| 6 | Sort | Sort by total (highest first) or by ID (ascending) |
| 7 | Statistics | The average, highest, lowest, and pass rate for each course, plus the student with the highest total |
| 8 | Save | Save the data to the file students.csv |
| 0 | Exit | If 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
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:
2024001,Alice Smith,85.50,92.50,78.00
2024002,Bob Johnson,90.00,88.00,95.00CSV 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:
| Module | Functions | Responsibility |
|---|---|---|
| Input helpers | read_line, input_text, input_int, input_score, confirm | Read each kind of input safely and reject invalid input |
| Student list | list_init, list_free, list_append, list_remove_at, list_find_by_id | Manage the dynamic array |
| Display | student_total, print_header, print_student | Compute totals and format the output |
| Features | add_student, show_all, search_student, modify_student, delete_student, sort_students, show_statistics | Implement each item on the menu |
| File I/O | save_to_file, split_fields, parse_line, load_from_file | Save and load the CSV file |
| Main program | print_menu, main | The 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
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 ──▶ end18.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.
/*
* 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_intparses an integer withstrtoland 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_scoreworks the same way withstrtod.- Every input function returns
falsewhen input ends (whenread_linereturns 0), and its caller then abandons the current operation. When reading a menu choice fails,maintreats it as "Exit," so even if input ends early (say, the user pressedCtrl + 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
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
$ gcc -Wall -Wextra -std=c11 student_manager.c -o student_manager
$ ./student_managerWhile developing and testing, it's a good idea to add AddressSanitizer:
$ gcc -Wall -Wextra -std=c11 -g -fsanitize=address,undefined student_manager.c -o student_managerSuppose students.csv already contains the data for three students:
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.00Here's one run of the program (to save space, the menu is shown only the first time; the later repetitions are left out):
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!TipBecause the data file is plain CSV, you can openstudents.csvin 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; otherwisefopenfails 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:
| Test | Expected 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 exists | A message that the ID already exists; nothing is added |
Enter abc, -5, 101, or 85pts as a score | A new prompt for the score |
| Enter an ID longer than 15 characters | A message that it's too long, and a new prompt |
| Enter a name containing a comma | A message that commas aren't allowed, and a new prompt |
Search for part of a name, such as Smith | Every 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 exist | A message that there's no such student |
Answer n when asked to confirm a deletion | Nothing is deleted, and the data is unchanged |
| The data file contains malformed lines or duplicate IDs | Those 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 exist | A 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=address | No memory errors or leaks are reported |
TipYou 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:
- 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
*. - 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), andmain.c, and write a Makefile for it. - Automatic backups. Before saving, use the
renamefunction from<stdio.h>to rename the old data file tostudents.csv.bak. - 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.
- 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).
- Undo. Use a "stack" to record the most recent changes, and implement "undo the last step."
- 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.
19.3 Recommended books
| Title | Author(s) | Level | Notes |
|---|---|---|---|
| C Primer Plus | Stephen Prata | Beginner | Detailed explanations and plenty of examples; long, suited to systematic study |
| C Programming: A Modern Approach | K. N. King | Beginner to intermediate | Comprehensive and rigorous, with excellent exercises; regarded by many as one of the best C textbooks |
| The C Programming Language | Brian W. Kernighan, Dennis M. Ritchie | Once you have a foundation | The classic co-written by C's creator, Dennis Ritchie: thin but dense, and worth rereading |
| Pointers on C | Kenneth A. Reek | Intermediate | An in-depth, thorough treatment of pointers |
| C Traps and Pitfalls | Andrew Koenig | Intermediate | Very short; devoted entirely to the places where C trips people up |
| Expert C Programming: Deep C Secrets | Peter van der Linden | Intermediate | Stories from C's history and its inner workings; highly entertaining |
| Computer Systems: A Programmer's Perspective | Randal E. Bryant, David R. O'Hallaron | Intermediate | Understanding computer systems from a programmer's point of view; essential reading for computer science students |
| Data Structures and Algorithm Analysis in C | Mark Allen Weiss | Intermediate | A 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
.cfile and a single.hfile), 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:
| Difficulty | Project | What it exercises |
|---|---|---|
| Beginner | Address book, library manager, expense tracker | Structures, files, menus (the structure of Chapter 18 is a good model) |
| Beginner | An expression calculator that handles parentheses and precedence | Stacks, string parsing |
| Intermediate | Command-line games: Snake, Minesweeper, 2048, Gomoku | Two-dimensional arrays, game loops, random numbers |
| Intermediate | A text file compressor (Huffman coding) | Trees, priority queues, bitwise operations, binary files |
| Intermediate | A simple shell (command-line interpreter) | String parsing, process management (requires learning systems programming) |
| Challenge | A text editor (see the open-source project kilo, a usable editor in about a thousand lines of C) | Terminal control, data structure design |
| Challenge | A JSON parser, a simple interpreter | Lexical analysis, parsing, recursive descent |
| Challenge | An 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
#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; thereturnin the message is the first word of the next line; - Changing
printftoPrintf:implicit declaration of function 'Printf'; did you mean 'printf'?, followed at link time byundefined 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
#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.
#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:
Uppercase: M
Position in the alphabet: 13
Three letters later: p4. 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
#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:
Enter the radius: 3
Circumference: 18.85
Area: 28.272. Sum and average
#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
#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
#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:
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
#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
#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
#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
#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
#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
#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
#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.
#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.
#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
#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
#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.
#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
#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:
2 3 5 7 11 13 17 19 23 29
31 37 41 43 47 53 59 61 67 71
73 79 83 89 972. GCD and LCM
#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
#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
#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.
#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
#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."
#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
#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
#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
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
#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
#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
#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
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:
10 20 20 21 30 2
10 21 30 40Statement 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
#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
#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:
2024-03-01 is day 61 of the year2. Complex numbers
#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
#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:
68.00 Compilers Brown
59.00 Operating Systems Johnson
52.50 Computer Networks Williams
45.00 Data Structures Smith
38.00 Discrete Mathematics Davis4. 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
#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
#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
#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.
a
│ +-------+ +----+----+----+----+
└───▶ | ●───┼────▶ | | | | | row 0
+-------+ +----+----+----+----+
| ●───┼────▶ | | | | | row 1
+-------+ +----+----+----+----+
| ●───┼────▶ | | | | | row 2
+-------+ +----+----+----+----+4. Spot the bugs
There are five errors:
malloc(strlen("Hello, ") + strlen(name))leaves no room for the'\0', sostrcatwrites 1 byte past the end; it needs to add 1;- The return value of
mallocisn't checked forNULL; - After
free(s), the code callsprintf("%s\n", s), using memory that has already been freed; - When
numsis assigned the second time, the address of the first block is lost, which leaks memory; 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:
#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
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
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:
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
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
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
#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
#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
#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
#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
#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
#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:
#ifndef MYMATH_H
#define MYMATH_H
int is_prime(int n);
int gcd(int a, int b);
int lcm(int a, int b);
#endifmymath.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:
#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:
$ gcc -Wall -Wextra -c mymath.c
$ gcc -Wall -Wextra -c main.c
$ gcc main.o mymath.o -o appThe Makefile (the command lines start with a Tab):
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 *.oChapter 16
1. Counting 1 bits
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
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
#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
#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:
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-boundsa[3]; - (b) No: unsigned arithmetic wraps around modulo 2ⁿ, so
ubecomesUINT_MAX, which is behavior the standard defines explicitly; - (c) Yes: signed integer overflow (assuming a 32-bit
int); - (d) No:
sis an array initialized from a string, and the code modifies the array's own contents; - (e) Yes:
tpoints to a string literal, and modifying a string literal is undefined behavior; - (f) Yes:
iis modified twice in the same expression, with no defined order between the two modifications.
3. Refactoring
#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:
| Dec | Hex | Name | Written in C | Meaning |
|---|---|---|---|---|
| 0 | 0x00 | NUL | '\0' | The null character, which marks the end of a string |
| 7 | 0x07 | BEL | '\a' | Alert (bell) |
| 8 | 0x08 | BS | '\b' | Backspace |
| 9 | 0x09 | HT | '\t' | Horizontal tab |
| 10 | 0x0A | LF | '\n' | Line feed (newline) |
| 13 | 0x0D | CR | '\r' | Carriage return |
| 27 | 0x1B | ESC | '\x1b' | Escape; often used to control terminal colors and the cursor |
| 127 | 0x7F | DEL | '\x7f' | Delete |
Codes 32–126 are printable characters:
| Dec | Hex | Char | Dec | Hex | Char | Dec | Hex | Char | Dec | Hex | Char |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 32 | 0x20 | (space) | 56 | 0x38 | 8 | 80 | 0x50 | P | 104 | 0x68 | h |
| 33 | 0x21 | ! | 57 | 0x39 | 9 | 81 | 0x51 | Q | 105 | 0x69 | i |
| 34 | 0x22 | " | 58 | 0x3A | : | 82 | 0x52 | R | 106 | 0x6A | j |
| 35 | 0x23 | # | 59 | 0x3B | ; | 83 | 0x53 | S | 107 | 0x6B | k |
| 36 | 0x24 | $ | 60 | 0x3C | < | 84 | 0x54 | T | 108 | 0x6C | l |
| 37 | 0x25 | % | 61 | 0x3D | = | 85 | 0x55 | U | 109 | 0x6D | m |
| 38 | 0x26 | & | 62 | 0x3E | > | 86 | 0x56 | V | 110 | 0x6E | n |
| 39 | 0x27 | ' | 63 | 0x3F | ? | 87 | 0x57 | W | 111 | 0x6F | o |
| 40 | 0x28 | ( | 64 | 0x40 | @ | 88 | 0x58 | X | 112 | 0x70 | p |
| 41 | 0x29 | ) | 65 | 0x41 | A | 89 | 0x59 | Y | 113 | 0x71 | q |
| 42 | 0x2A | * | 66 | 0x42 | B | 90 | 0x5A | Z | 114 | 0x72 | r |
| 43 | 0x2B | + | 67 | 0x43 | C | 91 | 0x5B | [ | 115 | 0x73 | s |
| 44 | 0x2C | , | 68 | 0x44 | D | 92 | 0x5C | \ | 116 | 0x74 | t |
| 45 | 0x2D | - | 69 | 0x45 | E | 93 | 0x5D | ] | 117 | 0x75 | u |
| 46 | 0x2E | . | 70 | 0x46 | F | 94 | 0x5E | ^ | 118 | 0x76 | v |
| 47 | 0x2F | / | 71 | 0x47 | G | 95 | 0x5F | _ | 119 | 0x77 | w |
| 48 | 0x30 | 0 | 72 | 0x48 | H | 96 | 0x60 | `` | 120 | 0x78 | x |
| 49 | 0x31 | 1 | 73 | 0x49 | I | 97 | 0x61 | a | 121 | 0x79 | y |
| 50 | 0x32 | 2 | 74 | 0x4A | J | 98 | 0x62 | b | 122 | 0x7A | z |
| 51 | 0x33 | 3 | 75 | 0x4B | K | 99 | 0x63 | c | 123 | 0x7B | { |
| 52 | 0x34 | 4 | 76 | 0x4C | L | 100 | 0x64 | d | 124 | 0x7C | | |
| 53 | 0x35 | 5 | 77 | 0x4D | M | 101 | 0x65 | e | 125 | 0x7D | } |
| 54 | 0x36 | 6 | 78 | 0x4E | N | 102 | 0x66 | f | 126 | 0x7E | ~ |
| 55 | 0x37 | 7 | 79 | 0x4F | O | 103 | 0x67 | g |
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).
| Level | Operator | Meaning | Associativity |
|---|---|---|---|
| 1 | () | Function call | Left to right |
[] | Array subscript | ||
. -> | Member access, member access through a pointer | ||
++ -- | Postfix increment and decrement | ||
| 2 | ++ -- | Prefix increment and decrement | Right to left |
+ - | Unary plus and minus | ||
! ~ | Logical NOT, bitwise NOT | ||
(type) | Cast | ||
* & | Dereference, address-of | ||
sizeof | Size of | ||
| 3 | * / % | Multiplication, division, remainder | Left to right |
| 4 | + - | Addition, subtraction | Left to right |
| 5 | << >> | Left shift, right shift | Left to right |
| 6 | < <= > >= | Relational comparisons | Left to right |
| 7 | == != | Equal, not equal | Left to right |
| 8 | & | Bitwise AND | Left to right |
| 9 | ^ | Bitwise XOR | Left to right |
| 10 | | | Bitwise OR | Left to right |
| 11 | && | Logical AND | Left to right |
| 12 | || | Logical OR | Left to right |
| 13 | ?: | Conditional | Right to left |
| 14 | = += -= *= /= %= <<= >>= &= ^= |= | Assignment and compound assignment | Right to left |
| 15 | , | Comma | Left to right |
The combinations that trip people up most often:
| Expression | What it actually means | What was usually intended |
|---|---|---|
*p++ | *(p++) | Get the value, then advance the pointer (usually exactly what was intended) |
x & 1 == 0 | x & (1 == 0) | (x & 1) == 0 |
a << 1 + 1 | a << (1 + 1) | (a << 1) + 1 |
*p.x | *(p.x) | (*p).x, that is, p->x |
int *a[10] | An array of 10 pointers | For a "pointer to an array," write int (*a)[10] |
c = getchar() != EOF | c = (getchar() != EOF) | (c = getchar()) != EOF |
Appendix D: Standard Library Quick Reference
`<stdio.h>`: input and output
| Function | Purpose |
|---|---|
printf / fprintf / snprintf | Formatted output to the screen / a file / a character array |
scanf / fscanf / sscanf | Formatted input from the keyboard / a file / a string |
getchar / putchar | Read / write one character |
fgets / fputs / puts | Read a line / write a string / write a string and a newline |
fopen / fclose | Open / close a file |
fgetc / fputc | Read / write one character from or to a file |
fread / fwrite | Read and write binary data in blocks |
fseek / ftell / rewind | Set / get / reset the file position |
feof / ferror | Test whether an earlier read ran into the end of the file / whether an error has occurred |
fflush | Write out the contents of an output buffer immediately |
perror | Print an error message |
remove / rename | Delete / rename a file |
`<stdlib.h>`: general utilities
| Function | Purpose |
|---|---|
malloc / calloc / realloc / free | Allocate, allocate zeroed, resize, and free dynamic memory |
atoi / atof | Convert a string to int / double (no error checking) |
strtol / strtoul / strtod | Convert a string to long / unsigned long / double (with error checking) |
rand / srand | Generate a pseudo-random number / set the random seed |
abs / labs | Absolute value of an integer |
qsort / bsearch | Generic sorting / binary search |
exit | End the program |
getenv | Get the value of an environment variable |
`<string.h>`: strings and memory
| Function | Purpose |
|---|---|
strlen | Length of a string |
strcpy / strncpy | Copy a string |
strcat / strncat | Append a string |
strcmp / strncmp | Compare strings |
strchr / strrchr | Find the first / last occurrence of a character |
strstr | Find a substring |
strtok | Split a string at separators |
strspn / strcspn | Length of the initial part made only of / free of the characters in a set |
memcpy / memmove | Copy memory (memmove allows the source and destination to overlap) |
memset | Set every byte in a block of memory to a value |
memcmp | Compare two blocks of memory |
strerror | Get the description string for an error code |
`<ctype.h>`: character handling
| Function | Purpose |
|---|---|
isalpha / isdigit / isalnum | Is it a letter / a digit / a letter or digit? |
isupper / islower | Is it an uppercase / lowercase letter? |
isspace / ispunct / isxdigit | Is it whitespace / punctuation / a hexadecimal digit? |
toupper / tolower | Convert to uppercase / lowercase |
`<math.h>`: mathematical functions (on Linux with GCC, you may need -lm at the end of the compile command)
| Function | Purpose |
|---|---|
sqrt / cbrt | Square root / cube root |
pow / exp | Power / e raised to a power |
log / log10 / log2 | Natural / base-10 / base-2 logarithm |
fabs | Absolute value of a floating-point number |
ceil / floor / round / trunc | Round up / round down / round to nearest / discard the fractional part |
fmod | Floating-point remainder |
sin / cos / tan / atan2 | Trigonometric functions (arguments in radians) |
hypot | Length of the hypotenuse of a right triangle |
`<time.h>`: time
| Function | Purpose |
|---|---|
time | Get 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) |
clock | Get the processor time used by the program; divide by CLOCKS_PER_SEC to get seconds, handy for rough timing |
difftime | Compute the difference between two times |
localtime / strftime | Convert to local time / format a time as a string |
Other common headers
| Header | Main 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
| Category | Keywords |
|---|---|
| Basic types | char short int long float double void |
| Type modifiers | signed unsigned |
| User-defined types | struct union enum typedef |
| Storage classes | auto static extern register |
| Type qualifiers | const volatile |
| Branching | if else switch case default |
| Loops | for while do |
| Jumps | break continue goto return |
| Operator | sizeof |
Keywords added by later standards
| Standard | New keywords |
|---|---|
| C99 | inline restrict _Bool _Complex _Imaginary |
| C11 | _Alignas _Alignof _Atomic _Generic _Noreturn _Static_assert _Thread_local |
| C23 | alignas 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 message | Meaning | Common causes and fixes |
|---|---|---|
expected ';' before ... | A semicolon is missing | Look 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 declared | A misspelled variable name, or you forgot to define the variable |
implicit declaration of function 'f' | A function was used without being declared | You 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 program | Non-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 " character | A string is missing its closing quote | Check that the double quotes are paired |
expected declaration or statement at end of input | The file ended unexpectedly | Unmatched braces: a } is missing |
'else' without a previous 'if' | An else has no matching if | There's a stray semicolon after the if, or several statements aren't enclosed in braces |
break statement not within loop or switch | break is in the wrong place | break can be used only inside a loop or a switch |
duplicate case value | A switch has two identical case values | Check the constants after each case |
redefinition of 'x' | Something was defined twice | Two 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 match | Check 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 arguments | Check the number of arguments against the function's definition |
assignment to expression with array type | An array was assigned as a whole | Arrays can't be assigned with =; copy strings with strcpy |
lvalue required as left operand of assignment | The left side of an assignment isn't something that can be assigned to | For 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 type | Use 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 address | You forgot the &: write scanf("%d", &n) |
passing argument 1 of 'f' makes pointer from integer without a cast | An integer was passed where a pointer is needed | Common 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 match | Check that the pointer has the right type |
comparison between pointer and integer | A pointer is compared with an integer | Common in if (c == "a"); a character needs single quotes: 'a' |
control reaches end of non-void function | A function that returns a value may finish without a return | Check that every path through the function has a return |
suggest parentheses around assignment used as truth value | An assignment is used as a condition | Usually == was mistyped as = |
unused variable 'x' | A variable was defined but never used | Delete the extra variable, or check whether you used the wrong variable name somewhere |
Link errors
| Error message | Meaning | Common causes and fixes |
|---|---|---|
undefined reference to 'f' | The definition of f can't be found | A 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 function | main is misspelled, or none of the compiled files contains a main |
multiple definition of 'x' | The same name is defined more than once | Usually 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
| Symptom | Meaning | Common causes |
|---|---|---|
Segmentation fault | Memory that may not be accessed was accessed | Dereferencing 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 exception | An arithmetic error | Integer division by 0 or remainder by 0 (despite the name, it's usually caused by integer arithmetic) |
free(): double free detected | Memory was freed twice | The same pointer was passed to free twice |
*** stack smashing detected *** | A stack array was written past its end | A 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 fault | Same as a segmentation fault |
The program's exit code is -1073741571 (that is, 3221225725, or hex 0xC00000FD) | A stack overflow on Windows | Recursion 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 Windows | Same as Floating point exception |
Visual Studio only
| Error message | Fix |
|---|---|
C4996: 'scanf': This function or variable may be unsafe | Put #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) |