Include Guards in C and C++: #ifndef, #define and #pragma once

A guard stops a header being pasted in twice. It does not stop it being defined twice. Tested examples, captured errors, and the pragma once myths.

One arrow blocked before reaching a file and others passing freely, illustrating that an include guard works within a single translation unit

An include guard stops a header being pasted into the same source file twice. It does not stop the header’s contents appearing in every source file that includes it — and the difference between those two sentences is where most header bugs live.

This guide covers the guard pattern itself, the two things it genuinely protects you from, the one thing people wrongly believe it protects them from, how #pragma once actually compares once you measure rather than repeat the folklore, and where C++20 modules leave both of them. Every program on this page was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 and GCC 14.2, using -std=c17 -Wall -Wextra unless noted; the examples this refresh replaces demonstrated #ifndef without ever showing a header file, so every code block here has been rewritten and run. Every output block, warning and linker error is captured verbatim.

What Are #ifndef and #define?

#ifndef is a preprocessor directive that tests whether a macro name has not been defined, and compiles the code up to the matching #endif if so. #define creates that macro name. Wrapping a header’s entire contents in #ifndef NAME / #define NAME / #endif produces an include guard: the first #include defines the macro and the body is processed, and any later #include of the same header in the same source file finds the macro already defined and skips the body. This all happens before compilation, in the preprocessor, and it is scoped to one translation unit.

That last clause is the one to hold on to. A translation unit is one .c or .cpp file plus everything it includes. The guard macro is created while that file is being preprocessed and is gone before the compiler starts on the next one.

A guard works per file, not per program One source file, header included twice a.c #include "counter.h" #include "counter.h" #ifndef COUNTER_H #define COUNTER_H second copy skipped Compiles. This is the whole job of a guard. The macro lives in one translation unit and is forgotten when the compiler moves to the next file. Two source files, header included once in each a.c → counter.h b.c → counter.h int counter = 0; Two definitions. The guard never sees the clash. ld: multiple definition of `counter'; first defined here Fix the header, not the guard: declare with extern and define once in a .c file. Captured from gcc 13.3, -std=c17 -Wall -Wextra. Guards stop repeated inclusion; they do not stop repeated definition across the translation units that make up your program.
An include guard is scoped to one translation unit: it stops the same header being pasted in twice while compiling a single .c file, and that is all it stops. Two source files each including the header once both receive whatever it defines, so a definition in a header still collides at link time. Declare with extern in the header and define once in a .c file.

The Include Guard Pattern

Here is a header with a guard, which is the shape you will write a thousand times:

/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H

struct Counter {
    int value;
};

void counter_bump(struct Counter *c);

#endif /* COUNTER_H */

And a source file that includes it twice, which happens constantly in real projects through indirect includes rather than by anyone writing it out:

/* a.c */
#include "counter.h"
#include "counter.h"

int main(void) { return 0; }
a.c compiles cleanly

Without the guard, struct Counter would be defined twice in one translation unit and the compiler would reject it. That is the guard’s job, and it does it well.

Three details worth getting right:

  • The #endif comment is optional but earns its place. In a 400-line header, #endif on its own tells you nothing about which block it closes.
  • Nothing goes outside the guard. Not includes, not comments that matter, and definitely not code. Anything above #ifndef or below #endif is processed on every inclusion.
  • The guard name must be unique across your entire project, not just the directory. The Guard Names section below shows what happens when it is not.

What a Guard Does Not Protect Against

This is the misconception worth an entire section, because it is stated confidently all over the internet: include guards do not prevent multiple definition. They prevent multiple inclusion, which is a different thing.

Put a definition in a guarded header:

/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H

int counter = 0;        /* a DEFINITION, not a declaration */

#endif

Include it once in each of two source files — not twice in one, just once each — and link them:

/usr/bin/ld: /tmp/ccq1kYCs.o:(.bss+0x0): multiple definition of `counter';
             /tmp/cc45UbJV.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

The guard did its job perfectly in both translation units. Each one included the header exactly once. The problem is that “exactly once per file” times two files equals two definitions of counter in the finished program, and the linker is the first thing to see them together. No preprocessor directive can fix that, because the preprocessor has finished before the linker starts.

The fix is in the header’s contents, not its guard:

/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H

extern int counter;     /* declaration: says it exists somewhere */

#endif
/* counter.c */
#include "counter.h"
int counter = 0;        /* definition: exactly one, in one .c file */

The rule for headers: put things there that are meant to be included in many files, and keep ordinary object definitions out. Function prototypes, struct and union definitions, typedefs, enums and macros are all fine in a header because they are declarations or type definitions rather than objects. Variables need extern in the header and a definition in exactly one source file. Templates, inline functions and inline variables (C++17), constexpr entities and C’s static inline functions are all designed to live in headers: the language permits them to be defined in every translation unit that includes them, provided the definitions are identical, and the implementation makes those definitions refer to one entity. Ordinary variables and non-inline functions have no such permission, which is what the linker error above is telling you.

#pragma once vs Include Guards

#pragma once is a non-standard directive that every mainstream compiler supports. One line at the top of the header replaces the three-line guard:

/* counter.h */
#pragma once

struct Counter { int value; };
void counter_bump(struct Counter *c);

Two claims usually settle this argument. I tested both, and neither survived.

Myth 1: #pragma once compiles faster

The reasoning goes that a guarded header must be opened and re-read every time, while #pragma once lets the compiler skip it. In practice GCC has had a multiple-include optimisation for decades: when it sees a header whose entire body is wrapped in a guard, it remembers the macro and skips the file on later inclusions.

How this was measured

SettingValue
Date testedSeptember 2026
MachineSingle-core Linux VM, Ubuntu 24.04
CompilerGCC 13.3, -std=c17, no optimisation flags
Workload300 leaf headers, 60 mid-level headers each including 30 leaves, one .c including all 60 — 1,800 include edges, acyclic
Iterations3 warm-up runs discarded, then mean of 10
CheckedBoth variants exit 0; identical header contents apart from the guard style
Not capturedNo median, no percentiles, no CPU-frequency control, no file-open counts
StyleMean compile time
Include guards14 ms
#pragma once15 ms

Three runs of ten: guards 14, 14, 14 ms; #pragma once 15, 16, 15 ms. One millisecond in fourteen, on a graph with 1,800 include edges — that is noise, not a reason to choose. Note what this measures: total compile time, not file opens or preprocessing operations. A compiler may still stat a header it has decided not to reparse, and none of that was counted. If your build is slow, the fix is including fewer headers, not changing how you deduplicate the ones you include.

I should be straight about the first version of this benchmark, because it produced a dramatic result and the result was wrong. I built 200 headers that each included all the others, which looks like a stress test and is actually a cycle: the #pragma once variant blew past GCC’s maximum include depth and spent its time emitting thousands of errors. I had redirected stderr and never checked the exit status, so it looked like #pragma once was 2× slower. It was failing. The numbers above come from an acyclic graph where both variants compile successfully.

Myth 2: #pragma once breaks when a header is reachable by two paths

The usual warning is that #pragma once identifies files by path, so the same header reached through a symlink, a hard link or a copy gets included twice. I tested a hard link and a byte-identical copy, with a struct definition in the header so any double inclusion would be a compile error:

Same header reached viaGCC 13.3MSVC 19.44
Hard link (same inode)DeduplicatedIncluded twice
SymlinkDeduplicatedIncluded twice
Byte-identical copyDeduplicatedIncluded twice

GCC deduplicated every case, including the copy, which has a different inode and a different path. MSVC deduplicated none of them, failing with a struct redefinition each time:

C:\guardtest\hard.h(2): error C2011: 'Tag': 'struct' type redefinition
C:\guardtest\real.h(2): note: see declaration of 'Tag'

So the folklore is real — it just depends which compiler you ask. GCC compares more than the path; MSVC, on this version, appears not to. Neither behaviour is required by any standard, because #pragma once is not in any standard. That is the whole argument in one table: a guard’s behaviour is defined by the macro and identical everywhere, while #pragma once is whatever your compiler decided to implement.

So which should you use?

PropertyInclude guards#pragma once
In the standardYes, C and C++No — universally supported, but an extension
Lines of boilerplateThreeOne
Can be typo’d or collideYes — see belowNo name to collide
Behaviour across odd filesystemsDefined by the macro, alwaysGCC deduplicates aliased paths; MSVC 19.44 did not
Compile speed here14 ms15 ms

Guards are portable by definition and carry a naming hazard. #pragma once removes the naming hazard and relies on a behaviour no standard requires. Both are defensible; pick one and be consistent, because a codebase using both gets the drawbacks of each. If you need to satisfy a strict standards-conformance requirement, guards are the only answer.

Guard Names: The Silent Failure

A guard name that collides with another header’s is the one failure mode in this whole topic that does not announce itself. Two headers, both using UTILS_H:

/* utils.h */          /* net/utils.h */
#ifndef UTILS_H        #ifndef UTILS_H
#define UTILS_H        #define UTILS_H
int from_utils(void);  int from_net_utils(void);
#endif                 #endif
#include "utils.h"
#include "net/utils.h"      /* body skipped: UTILS_H is already defined */

The second header is silently discarded. Under C17 you get a warning that is easy to lose in a busy build:

main.c:6:5: warning: implicit declaration of function 'from_net_utils';
            did you mean 'from_utils'? [-Wimplicit-function-declaration]

Under C23 and in C++ the same code is a hard error, which is a real improvement:

main.c:6:5: error: implicit declaration of function 'from_net_utils'

But notice what the diagnostic says. It complains about the function, not about the header being skipped. Nothing anywhere says “your guard names collided”, and if the second header had only defined types rather than declared functions, you would have got no diagnostic at all until something failed much later.

Name guards after the project and the path, not just the file: MYPROJ_NET_UTILS_H. Some teams append a random suffix or a UUID, which is ugly and completely reliable.

One rule the compiler will not enforce for you: identifiers beginning with an underscore followed by a capital, and any identifier containing a double underscore, are reserved to the implementation. _UTILS_H and UTILS__H are both reserved, both extremely common in the wild, and both compile without a murmur:

(no diagnostic: the compiler does not police this)

These spellings are reserved to the implementation, which is free to use them for its own purposes. Your program should not define them even where the compiler accepts them without complaint. Start guard names with a letter.

#ifndef Beyond Include Guards

Include guards are the main use but not the only one. The other common pattern is a default that a build can override:

#include <stdio.h>

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 4096
#endif

int main(void) {
    printf("Buffer size is %d bytes.\n", BUFFER_SIZE);
    return 0;
}
Buffer size is 4096 bytes.

Compile with -DBUFFER_SIZE=8192 and the #ifndef sees the macro already defined, skips its body, and the command-line value wins. This is how build systems configure code without editing it.

C23 adds #elifdef and #elifndef, which spell #elif defined(X) more directly. Check your compiler’s flag name before reaching for them — the mode is spelled differently across GCC releases:

Compiler and flagResult
GCC 13.3, -std=c2xAccepted, prints cpu
GCC 13.3, -std=c23error: unrecognized command-line option '-std=c23'
GCC 14.2, -std=c2xAccepted, prints cpu
GCC 14.2, -std=c23Accepted, prints cpu

Be more careful still about mixing them into older code. Compiled with -std=c17, the same program printed neither — it silently took the #else branch instead of the #elifdef one, with zero diagnostics on both compilers even under -Wall -Wextra -Wpedantic. An unrecognised conditional directive does not fail loudly; it changes which branch you compile. The full directive list is on cppreference’s conditional inclusion page.

Where C++20 Modules Leave All This

Modules are the long-term answer to the problem guards exist to patch. import std; or import mylib; gives the compiler a pre-parsed representation instead of pasting text, so there is nothing to deduplicate, no macro leakage across the boundary, and no include order to get wrong.

They are genuinely better, and they are also not yet a default you can assume. Support across GCC, Clang and MSVC has been landing unevenly for years, build-system integration is the harder half of the problem, and the standard library module has arrived more recently than the language feature. Most C++ written in 2026 still uses headers, and all C does — C has no standardised module system, so if you write C, include guards are not a transitional technology. They are the answer.

The practical position: keep writing guards, write them correctly, and treat modules as something to adopt when your compiler, your build system and your dependencies all agree rather than as a reason to defer learning the thing you will use this week.

Key Takeaways

  • An include guard prevents a header being processed twice in one translation unit. That is its entire job, and it does it reliably.
  • Guards do not prevent multiple definition across source files. A variable defined in a guarded header still produces multiple definition at link time. Declare with extern in the header, define once in a .c file.
  • Keep ordinary object definitions out of headers. Prototypes, types, typedefs, enums, macros, templates and inline or static inline functions all belong there. A plain variable does not.
  • #pragma once is not measurably faster. On a 1,800-edge include graph here, 15 ms against 14 ms for guards. Choose on portability and naming, not speed.
  • Guard name collisions fail silently. The second header is skipped and the diagnostic, if any, blames something else. Name guards after the project and path.
  • Never start a guard name with an underscore or include a double underscore — those identifiers are reserved, and no compiler will warn you.
  • C++20 modules offer a different model; C has no standardised equivalent. Headers and modules will coexist for years, so guards remain working knowledge in C++ and the permanent answer in C.

Frequently Asked Questions

Conclusion

The include guard is three lines of boilerplate that almost every C and C++ programmer types without thinking, and the thinking is worth doing once. Its scope is one translation unit. It protects against text being pasted twice, and against nothing else. Everything that goes wrong around headers — the linker complaining about a variable, the header that mysteriously has no effect, the macro that leaks into a file that never asked for it — comes from expecting it to do more.

Keep ordinary definitions out of headers and most of those problems never start. The rest of our C programming guides take the same approach, and if you are working through the language systematically, the complete guide to C++ programming covers where the header model is heading.

What I Could Not Verify

Everything here was compiled on one machine: a single-core Ubuntu 24.04 VM running GCC 13.3 and GCC 14.2 with libstdc++ and glibc 2.39. GCC and MSVC were tested; Clang was not. The #pragma once path results differ between the two compilers tested, which is itself the finding, but a third implementation could differ again. rather than guaranteed. The #pragma once deduplication of hard links and copies is GCC behaviour, not a standard requirement — Clang and MSVC use their own heuristics and may differ, which is itself the argument for guards where conformance matters. The compile-time comparison is one workload on one machine with cold caches unmanaged; the millisecond figures will not transfer, and the finding that the gap is negligible is an expectation for ordinary include graphs rather than something these three runs establish for all of them. I did not count file opens to confirm the mechanism behind that result, so the explanation of GCC’s multiple-include optimisation is documented behaviour I am citing, not something I measured. The statement that C++20 module support is still maturing is a general characterisation of the ecosystem rather than a test of any particular toolchain.

Scroll to Top