C Header Files: Declarations, Definitions, and Modular Code

Understand the declaration-versus-definition split that C header (.h) and source (.c) files embody, write correctly guarded headers, and see why this exact pattern is the direct foundation for how every Objective-C class is organized.

As C programs grow beyond a single file, you need a way to let one file use a function or a type that's actually defined somewhere else, without pasting the whole implementation everywhere it's needed. C solves this with a strict split between declaring something (telling the compiler 'this exists, here's its shape') and defining it (actually providing its full body or storage). Header files (.h) hold declarations; source files (.c) hold definitions; #include stitches them together at compile time. This lesson is not a side note before Objective-C — it is the direct blueprint for it. Every single Objective-C class you will ever write splits into exactly this same .h/.m pair: the interface (declarations) lives in the header, the implementation (definitions) lives in the .m file, and the reason that split exists is precisely the reasoning covered here.

Declaration vs. Definition

A declaration tells the compiler that something exists and describes its shape — a function's name, return type, and parameter types, or a type's name — without providing its actual body or allocating storage for it. int add(int a, int b); is a declaration: it promises a function called add exists somewhere, taking two ints and returning an int, but shows no implementation. A definition is the real thing: int add(int a, int b) { return a + b; } both declares and provides the actual executable body. For variables, extern int counter; is a declaration (no storage allocated here, it exists elsewhere), while int counter; at file scope is a definition (storage is allocated right here).

The compiler only needs a declaration to compile code that calls a function or uses a type — it needs to know the shape (so it can check argument types and generate the right calling code), not the implementation. The actual definition only needs to exist somewhere in the final program by the time the linker (covered in the next lesson) assembles everything together. This is exactly what makes splitting code across files possible: file A can call a function whose declaration it has seen, even though the function's definition lives in a completely different file that A never even reads.

• Declaration: describes the shape (name, types) — 'this exists'. • Definition: provides the actual body or allocates storage — 'here it is'. • The compiler only needs a declaration to check and compile a call site. • extern int x; is a declaration; int x; at file scope is a definition.

The Header/Source Pattern: .h Declares, .c Defines

By convention, C projects put declarations in a header file (.h) and the corresponding definitions in a source file (.c) of the same base name — math_utils.h declares what math_utils.c implements. Any other .c file that wants to use those functions writes #include "math_utils.h" at the top, which pastes in the declarations (via the preprocessor, from the previous lesson), giving the compiler everything it needs to type-check calls to those functions without ever seeing — or needing — their implementations.

This is the core diagram to hold in your head for the rest of this course: Header (.h) holds a Declaration → gets #included into a Source file (.c, or in Objective-C, .m) → which contains the matching Definition. The header is the public promise; the source file is the private fulfillment of that promise. Multiple source files can #include the same header and all call the declared functions, while only one source file actually defines each one — the linker's job (next lesson) is to make sure every declared-but-not-locally-defined symbol gets matched up with exactly one real definition somewhere in the build.

• math_utils.h: declarations only (function prototypes, type definitions, constants). • math_utils.c: #includes math_utils.h, then provides the actual function bodies. • other_file.c: #includes math_utils.h to call those functions, without ever seeing their implementation. • Diagram: Header (.h) → Declaration → #include → Source (.c/.m) → Definition.

Include Guards: Preventing Double Inclusion

Because #include is literal text-pasting, a header that gets included twice into the same translation unit — directly, or indirectly because two different headers you included both include it — pastes its declarations (and any type definitions) in twice, which the compiler rejects as a redefinition error. Include guards solve this using the conditional-compilation directives from the previous lesson: wrap the entire header body in #ifndef HEADER_NAME_H, #define HEADER_NAME_H, and a closing #endif. The first time the header is included, HEADER_NAME_H is not yet defined, so the body is processed (and the guard macro gets defined). Any subsequent inclusion in the same translation unit sees HEADER_NAME_H already defined and skips the entire body — the header effectively becomes a no-op the second time.

Most modern compilers, including clang, also support a simpler non-standard alternative: #pragma once as the very first line of the header, which tells the compiler directly 'only ever include this file once per translation unit' without needing a manually chosen macro name. It's shorter and eliminates the (rare, but real) risk of two different headers accidentally choosing the same guard macro name and clashing. Traditional #ifndef guards remain extremely common in production code, especially in cross-platform or older codebases, because #pragma once is not part of the official C standard, even though virtually every compiler in practical use supports it.

• #ifndef NAME_H / #define NAME_H / ... body ... / #endif — the classic, portable include guard. • #pragma once — simpler, single-line, non-standard but supported virtually everywhere. • Without a guard, double inclusion causes 'redefinition' compiler errors on anything but plain function prototypes.

Modularity: Public vs. Private Headers

Splitting declarations from definitions isn't just bookkeeping — it's what makes modular design possible in C. A well-designed header exposes only what other files genuinely need (a public interface): the functions, types, and constants that make up the module's contract with the rest of the program. Implementation details that other files have no business touching — internal helper functions, private state — are kept entirely inside the .c file and never declared in the header at all, often marked static so they aren't even visible to the linker outside that one file. This lets you freely rewrite the internals of math_utils.c without breaking anything that includes math_utils.h, as long as the public declarations don't change — a caller only ever depended on the promise the header made, never on how it was fulfilled.

Larger C projects sometimes go further and maintain a genuinely private header (used only inside a small cluster of related .c files, declaring internals shared between them but not meant for the rest of the program) alongside a public header (the one external code is meant to #include). This public/private split — one header as the real public contract, everything else kept as an implementation detail — is precisely the same idea Objective-C formalizes with public headers, and later, with access-control conventions for properties and methods.

• Public header: the module's contract with the rest of the program — only what callers need. • Implementation details stay inside the .c file, often marked static to hide them even from the linker. • You can freely change internals without breaking callers, as long as the public header's promises don't change. • Some large projects add private headers shared only among a cluster of related source files.

This Is the Blueprint for Every Objective-C Class

Hold on to this lesson, because you will see its exact shape again immediately once Objective-C syntax is introduced. Every Objective-C class is written as a pair of files: a .h file containing the class's @interface — the declarations of its properties and methods, the public promise of what the class can do — and a .m file containing the @implementation — the actual method bodies, the definitions that fulfill that promise. Any other file that wants to use the class writes #import "ClassName.h" (the Objective-C-flavored #include from the previous lesson) and gets exactly the declarations it needs to call the class's methods, without ever seeing — or needing — the implementation.

Every reason you just learned for splitting C code into .h/.c pairs applies unchanged to why Objective-C classes are split into .h/.m pairs: callers only depend on the public interface, not the implementation; you can rewrite a method's internals freely as long as its declared signature doesn't change; and the header acts as the module's contract with the rest of the program. When the next module introduces @interface and @implementation, you should recognize them immediately as the Objective-C names for exactly the declaration/definition split you just practiced in plain C.

• Objective-C .h file = @interface = declarations (the C header pattern, renamed). • Objective-C .m file = @implementation = definitions (the C source file pattern, renamed). • #import "ClassName.h" is the same include-and-declare pattern as #include "math_utils.h". • Every argument for .h/.c modularity in C carries over unchanged to .h/.m modularity in Objective-C.

Code Examples

The #ifndef/#define/#endif guard ensures that even if math_utils.h is #included multiple times (directly or indirectly) in the same translation unit, its declarations are only processed once. Only prototypes appear here — no function bodies.

math_utils.c includes its own header (good practice — it lets the compiler verify the definitions match the declared prototypes) and provides the real bodies. main.c only includes the header, never math_utils.c, yet can call both functions because the header gave the compiler everything it needed to check the calls.

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

#endif /* MATH_UTILS_H */
/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("%d\n", add(2, 3));
    printf("%d\n", multiply(2, 3));
    return 0;
}
/* config.h */
#pragma once

#define APP_VERSION "1.0.0"
#define MAX_CONNECTIONS 10

The Header/Source Split

The same declaration-in-header / definition-in-source pattern reappears, renamed, as @interface (.h) / @implementation (.m) in Objective-C.

math_utils.h                    math_utils.c
 ┌───────────────────┐   #include ┌───────────────────────┐
 │ int add(int,int);  │◀──────────│ #include "math_utils.h"│
 │   (Declaration)     │           │ int add(int a,int b){  │
 └───────────────────┘           │   return a + b;        │
          ▲                       │ } (Definition)          │
          │ #include              └───────────────────────┘
 ┌───────────────────┐
 │ main.c calls add() │
 │ (only needs decl.)  │
 └───────────────────┘

 Objective-C, later: ClassName.h (@interface) --> ClassName.m (@implementation)

Important Rules

Headers declare; source files define. Keep implementation bodies out of headers (except for small inline/static helpers where intentional).

Every header should be protected with an include guard (#ifndef/#define/#endif) or #pragma once.

A .c file that defines functions declared in its own .h file should #include that header itself, so the compiler checks they match.

Expose only what callers need in the public header; keep internal helpers static and undeclared there.

Remember this exact .h/.c shape — it becomes .h/.m for every Objective-C class you write.

From C Modules to Objective-C Classes

Cocoa and Cocoa Touch frameworks are organized exactly along these lines at scale: a class like NSString ships a public .h header declaring the methods you can call, while its actual implementation is compiled separately and hidden from you entirely. When you later write your own Objective-C classes, you will write a Person.h with @interface Person declaring properties and methods, and a Person.m with @implementation Person providing the bodies — the direct descendant of the math_utils.h/math_utils.c pattern from this lesson.

/* utils.h */ int square(int x) { return x * x; } /* utils.h */ int square(int x); /* utils.c */ #include "utils.h" int square(int x) { return x * x; }

Putting a full function body directly in a header that gets #included into multiple .c files causes the function to be defined multiple times across the program, leading to linker errors (covered in the next lesson) — headers should hold only the declaration.

/* config.h, no guard */ #define VERSION "1.0" /* config.h */ #ifndef CONFIG_H #define CONFIG_H #define VERSION "1.0" #endif

Without an include guard, a header that ends up #included twice into the same translation unit (often indirectly, through two other headers) re-pastes its content and can trigger redefinition errors on anything beyond simple #define constants.

What is the key difference between a declaration and a definition in C? A. There is no real difference; the terms are interchangeable B. A declaration describes a function or variable's shape without providing its body/storage; a definition provides the actual body or allocates storage C. Declarations only exist in .c files; definitions only exist in .h files D. A definition is always faster to compile than a declaration

B. A declaration describes a function or variable's shape without providing its body/storage; a definition provides the actual body or allocates storage — A declaration (e.g. int add(int,int);) tells the compiler something exists and its shape; a definition (the function body, or int x; at file scope) actually provides the implementation or allocates storage.

Why does a header file need an include guard (#ifndef/#define/#endif) or #pragma once? A. To make the file compile faster B. To prevent the same declarations from being pasted in more than once into a single translation unit, which would cause redefinition errors C. It's required by the C language grammar for all headers D. To hide the header's contents from other files

B. To prevent the same declarations from being pasted in more than once into a single translation unit, which would cause redefinition errors — Since #include performs literal text pasting, a header included twice (directly or via two other headers) would paste its content twice; the guard makes every inclusion after the first a no-op.

In the C header/source pattern, what does math_utils.c gain by #including its own math_utils.h? A. Nothing — it's unnecessary and should be avoided B. It makes the program run faster at runtime C. The compiler can verify that the definitions in the .c file actually match the prototypes declared in the .h file D. It automatically links the file into the final executable

C. The compiler can verify that the definitions in the .c file actually match the prototypes declared in the .h file — Including its own header lets the compiler cross-check that math_utils.c's function definitions match the signatures promised in math_utils.h, catching mismatches at compile time rather than link time.

How does this C header/source pattern relate to Objective-C classes? A. It doesn't — Objective-C classes use a completely unrelated file structure B. Objective-C classes are written as a .h file (@interface, the declarations) and a .m file (@implementation, the definitions) — the same declaration/definition split renamed C. Objective-C classes only ever need a single .m file, with no header D. The .h file in Objective-C contains the full implementation, and .m is optional

B. Objective-C classes are written as a .h file (@interface, the declarations) and a .m file (@implementation, the definitions) — the same declaration/definition split renamed — Every Objective-C class splits into a .h file holding @interface (declarations of properties/methods) and a .m file holding @implementation (the actual method bodies) — directly mirroring the .h/.c declaration/definition split from plain C.

Create a header string_utils.h (properly guarded) declaring a function int string_length(const char *s), and a matching string_utils.c that includes the header and implements the function by counting characters until the null terminator. Write a short main.c that includes only the header and calls the function.

/* string_utils.h */ #ifndef STRING_UTILS_H #define STRING_UTILS_H int string_length(const char *s); #endif /* string_utils.c */ #include "string_utils.h" int string_length(const char *s) { int count = 0; while (s[count] != '\0') { count++; } return count; } /* main.c */ #include <stdio.h> #include "string_utils.h" int main(void) { printf("%d\n", string_length("hello")); return 0; }

Remember the guard macro name should be unique to this header, and string_utils.c should #include its own header so the compiler checks the definition against the declaration.