Header-File Design and Usage Guidelines

As is well known, compared with object-oriented C++, procedural C has no concept of encapsulation, inheritance, or polymorphism, let alone interfaces. In C programs, using the principle of separate compilation, header files (.h or .hpp) serve to provide declarations and interfaces, while source files (.c) provide implementations, thereby achieving modular engineering design and ensuring high cohesion and low coupling. This is undoubtedly very important.

Preface

Preface 1. Disclaimer

The content of this article is largely derived from “C — Principles, Rules, and Recommendations for Header-File Inclusion”, but that article has poor formatting, so the content has been reformatted and excerpted here.

Preface 2. On Declarations and Definitions

In C programs, using the principle of separate compilation, header files (.h or .hpp) serve to provide declarations and interfaces, while source files (.c) provide implementations. The C compiler typically treats source files as compilation units. During compilation, identifiers are validated against declarations; only at the linking stage are the actual implementations located and linked together into a single whole.

Additionally, to avoid ambiguity, the principle for declarations and definitions is “multiple declarations (for external variables), a single definition.” In a C project, to avoid ambiguity, there must be only one definition. Multiple declarations are permitted to satisfy the requirement of declaring before defining within the same file, or to be used in other files as compilation units.

  • Definitions typically involve memory allocation, while declarations do not;

  • A definition is simultaneously a declaration, for example:

    1
    
    int v_g; // 既是定义,也是声明;(Both a definition and a declaration)
    
  • Declarations typically use the extern keyword:

    1
    
    extern int v_g; // 使用同文件中后面的外部变量或其它文件中定义的外部变量;(Uses an external variable defined later in the same file or in another file)
    
  • If an extern declaration is accompanied by initialization, it becomes a definition:

    1
    
    extern const int vc_g = 28; // const默认为内部链接,显式声明为extern后为外部链接;(const is internal linkage by default; explicitly declaring it as extern makes it external linkage)
    

Source files implement the definitions of variables and functions and specify their linkage scope. Header files contain the declarations of global variables, functions, data types, and macros that need to be accessible externally.

Preface 3. Common Extension

Unix compilers and linkers commonly use a “common extension” that permits multiple definitions, provided that at most one definition is initialized.

This approach is called a “common extension” by the ANSI C standard. Some older systems may require explicit initialization to distinguish definitions from external declarations.

The common extension is explained in Computer Systems: A Programmer’s Perspective as follows: among multiply-defined symbols, at most one strong symbol is allowed. Functions and initialized global variables are strong symbols; uninitialized global variables are weak symbols. The Unix linker uses the following rules to process multiply-defined symbols:

  • Rule 1: Multiple strong symbols are not allowed. Global variables defined within header files that are included by multiple source files will be defined multiple times (during the preprocessor stage, the header file contents are expanded into the source files). If these definitions are explicitly assigned values (initialized), this rule is violated.
  • Rule 2: If there is one strong symbol and multiple weak symbols, the strong symbol is chosen.
  • Rule 3: If there are multiple weak symbols, any one of them is chosen.

When global variables of the same name are defined in different files (even if their types and meanings differ), the variable shares the same memory block (same address). If the variable definitions are all initialized, a “multiple definition” linker error will occur. If a variable definition is uninitialized, there will be no linker error, though a compiler warning such as size of symbol 'XXX' changed may appear if the different types result in different sizes.

In the worst case, compilation and linking succeed normally, but different files reading and writing the same-named global variable will interfere with each other, causing very elusive problems. This risk is especially prominent when using third-party libraries whose source code cannot be accessed.

Preface 4. Origin of This Article

C language projects typically use files to achieve modularity. The requirement for modularity is “high cohesion, low coupling”, from which a series of header-file inclusion principles, rules, and recommendations can be derived.

I once believed that a “.c file” (unless otherwise specified, .c and .cpp files are collectively referred to as source files) should correspond to a single “.h file” (unless otherwise specified, .h and .hpp files are collectively referred to as header files), and that a source file only needs to include its own header file. If a source file uses content from other files, it should include the needed headers in its header file. This approach seemed workable, and when the project had few files, no problems were apparent. However, as the number of project files grew, I discovered a flaw in this thinking: header files including each other led to a situation where I assumed that if a macro was declared, it would take effect, but in actual testing, some declared macros failed to work after compilation. (The header files used the extern "C" idiom to prevent duplicate inclusion.)

This is actually an incorrect programming habit. The correct approach is: a source file’s header file should only include other necessary header files — no unnecessary headers should be included. Meanwhile, source files should include all headers they use. This way, even if a source file ends up with duplicate inclusions, it causes no harm.

Note: The Google C++ Style Guide requires that C++ header files be Self-contained. In plain language, header files should be self-sufficient — they should work as the first header included, without depending on other headers being included first.

For C, header-file planning largely reflects the soundness of the overall system design. Poor header-file design is one of the root causes of excessively long compilation times.

Note: For example, if x.h depends on y.h, and y.h depends on z.h, dependency transitivity occurs. All source files that include x.h transitively depend on z.h through y.h. If any of these three header files changes, all source files including x.h must be recompiled. Moreover, overly long dependency chains increase header-file parsing time, even though most of the content may never be used. A simple chain dependency already causes such problems — let alone the complications of diamond dependencies.

Fortunately, some well-established design methods have been developed for planning header files rationally.

Preface 5. Table of Contents

  1. Principles

    • Header files are suitable for declaring interfaces, not for containing implementations.
    • Header files should have a single responsibility.
    • Header-file partitioning principles
    • Hierarchical semantic principle and use of common headers
    • Semantic relevance principle for header files
    • Include order within source files should go from most specific to most general.
    • Minimization principle: Use forward declarations and extern function declarations; avoid including headers whenever possible.
  2. Rules

    • Every .c file should have a corresponding .h file that declares the interfaces intended for external use.
    • Circular header-file dependencies are forbidden. Reduce nesting and cross-references, and try to avoid ordering dependencies.
    • Prefer including headers in source files rather than in header files. .c/.h files must not include unused headers.
    • Header files should be self-contained and self-sufficient.
    • Which headers a header file includes should depend only on itself, not on the source files that include it.
    • Always write include guards (#define guards).
    • Defining variables in header files is forbidden.
    • Other .c files’ interfaces may only be used by including their header files; using extern to access external functions or variables directly in .c files is forbidden.
    • Including header files inside extern "C" blocks is forbidden.
  3. Recommendations

    • A module typically contains multiple .c files, which should be placed in the same directory. The directory name should be the module name. For the convenience of external users, it is recommended that each module provide a single .h file named after the directory.
    • If a module contains multiple sub-modules, it is recommended that each sub-module provide a public .h file named after the sub-module.
    • Header files should not use non-standard file extensions such as .inc.
    • Within a single product, the header-file inclusion order should be consistent.

1. Principles

  • Header files are suitable for declaring interfaces, not for containing implementations. Header files serve as the external interface of a module or unit. They should contain declarations intended for external use, such as function declarations, macro definitions, type definitions, and so on.

    Declarations of internally used functions (equivalent to private methods in a class) should not be placed in header files.

    Internally used macros, enumerations, and struct definitions should not be placed in header files.

    Variable definitions should not be placed in header files; they should be placed in source files.

    Variable declarations should, as much as possible, not be placed in header files — that is, global variables should generally not be used as interfaces. Variables are internal implementation details of a module or unit; they should not be directly exposed externally by declaring them in header files. Instead, they should be exposed through function interfaces. Even when global variables must be used, they should be defined only in source files, with the header file merely declaring the variable as global.

    Header files must not define variables or functions. They may only contain macros, types (typedef/struct/union/enum, etc.), and declarations of variables and functions. In special cases, basic-type global variables may be declared extern in a header file, allowing source files to access the global variable by including that header. However, header files should not use extern to declare custom-type (e.g., struct) global variables, as this would force source files that do not need to access the variable to include the header file that defines the custom type.

「Principles for Using Global Variables」

  1. If a global variable is accessed only within a single source file, it should be changed to a static global variable within that file;

  2. If a global variable is accessed only by a single function, it should be changed to a static local variable within that function;

  3. Avoid using extern to declare global variables; it is better to provide functions to access these variables. Directly exposing global variables is unsafe, as external users may not fully understand the meaning of these variables.

  4. When designing and calling functions that access dynamic global variables, static global variables, or static local variables, reentrancy issues must be considered.