C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
77: Introduction to CMake for C Projects
You've likely spent the last few lessons running a command like gcc main.c -o program. That works great when your project is a single file. But the moment you start splitting your code into modules—which you should be doing—the command line becomes a nightmare. I remember a project early in my career where I spent an hour debugging a "missing function" only to realize I'd just forgotten to add one .c file to my manual build string.
The "Undefined Reference" Headache
Imagine you've built a simple string utility library to help with your project. You have main.c, string_utils.c, and string_utils.h. You try to compile it like this:
gcc main.c -o my_app
The compiler doesn't complain about the header (because it's included), but the linker throws a fit:
/usr/bin/ld: /tmp/ccXyZ.o: in function 'main':
main.c:(.text+0x15): undefined reference to 'string_reverse'
collect2: error: ld returned 1 exit status
The problem is that gcc compiled main.c, but it has no idea that the actual machine code for string_reverse lives inside string_utils.c. To fix this manually, you'd have to run gcc main.c string_utils.c -o my_app. Now, imagine you have 20 files. Do you really want to type them all out every time, or maintain a fragile shell script that breaks the moment you add a new file?
Offloading the Heavy Lifting to CMake
This is where CMake comes in. It isn't a compiler; it's a build system generator. You describe your project in a file called CMakeLists.txt, and CMake generates the actual build instructions (like a Makefile) for whatever system you're on. It's the industry standard for C and C++ because it handles the dependencies and linking for you.
Here is how we'd solve the "undefined reference" problem by creating a CMakeLists.txt file in your project root:
cmake_minimum_required(VERSION 3.10)
project(StringApp)
# We tell CMake to create an executable called 'my_app'
# and list every source file that needs to be compiled and linked.
add_executable(my_app main.c string_utils.c)
Now, instead of fighting with gcc flags, you use a standard workflow. I always recommend "out-of-source" builds. This keeps your source directory clean of temporary build artifacts.
mkdir build
cd build
cmake ..
make
By running cmake .., CMake looks at the parent directory, finds your CMakeLists.txt, and figures out exactly how to invoke the compiler on your specific OS. When you run make, it executes those instructions.
Handling Include Directories
As your project grows, you'll probably start putting headers in a /include folder and source files in a /src folder. If you do that, the compiler will complain that it can't find your .h files.
You could add -I./include to every gcc call, but in CMake, you just add one line to your configuration:
target_include_directories(my_app PUBLIC include)
I love this approach because it explicitly ties the include directory to the my_app target. If you later add a second executable or a library to the same project, you can give it its own set of include paths without them leaking into each other.
📋 Practical Task
Building a Multi-File Geometry Calculator
Your task is to move away from manual compilation and set up a CMake build for a geometry project. You are provided with three files:
main.c: Calls functions to calculate the area of a circle and a rectangle.geom.c: Contains the logic forcalc_circle_area()andcalc_rect_area().geom.h: The function prototypes.
Requirements:
- Create a
CMakeLists.txtfile that sets the minimum version to 3.10 and names the project "GeoCalc". - Configure the build so that the resulting executable is named
geometry_tool. - Ensure that both
main.candgeom.care included in the build process so that no "undefined reference" errors occur. - Demonstrate the build process by listing the commands you would run in the terminal to create a
builddirectory and compile the project.
There are no comments for now.