Declarations, Definitions, and Identifiers
This chapter distinguishes declarations from definitions, explains how identifiers are formed, and shows how the two halves of the C name system fit together. This is the foundation for scope, linkage, and multi-file programs.
Why This Matters
The declaration/definition split is the reason C can compile multiple files separately and link them later. Confusing the two leads directly to duplicate definitions, missing definitions, and "undefined reference" linker errors.
Prerequisites
c.core.2— objects and the abstract machine.
Core Concept
Identifiers
An identifier is a name you give to an object, function, type, label, struct member, or macro. It is a sequence of letters, digits, and underscores, not beginning with a digit. a, count, _private, and MAX_SIZE are all identifiers.
C is case-sensitive: count, Count, and COUNT are three different names.
Reserved identifiers you must not use (in a hosted program, unless the standard grants them to you):
- Identifiers beginning with
_followed by an uppercase letter or another
underscore (_Foo, __x) are reserved for the implementation.
- Identifiers beginning with
_at file scope are reserved for the
implementation (even if followed by a lowercase letter).
- Identifiers in the standard library are reserved when the corresponding
header is included.
Declarations
A declaration introduces a name and associates it with a type. It tells the compiler "there is a thing with this name and this type" without necessarily reserving storage or producing code.
extern int counter; /* declaration only: no storage reserved */
int max(int a, int b); /* function declaration (prototype) */
Definitions
A definition is a declaration that *also* reserves storage (for an object) or provides the body (for a function).
int counter = 0; /* definition: reserves storage */
int max(int a, int b) { /* definition: provides the body */
return a > b ? a : b;
}
Every definition is also a declaration, but not every declaration is a definition.
The one-definition rule (in practice)
Across an entire program, an identifier with external linkage may have exactly one definition (the "one definition rule," though C states it as a constraint that there be no more than one external definition). It may have many declarations, but they must all be compatible.
Syntax
Object declaration forms
int x; /* tentative definition (file scope) */
extern int y; /* declaration only */
int z = 5; /* definition with initializer */
At file scope, int x; without extern and without an initializer is a tentative definition: if no other definition appears in the translation unit, it becomes a definition initialized to 0. If a definition with an initializer appears later, the tentative definition is just a declaration.
Function declaration forms
int f(int a, int b); /* prototype: names optional but useful */
int f(int, int); /* prototype: names omitted */
int f(); /* old-style: unspecified arguments (avoid) */
The prototype form int f(int, int); tells the compiler the parameter types, enabling type checking and argument conversions.
Examples
Declaration vs. definition
/* header.h */
extern int shared_count; /* declaration */
int compute(int x); /* declaration */
/* impl.c */
int shared_count = 0; /* definition */
int compute(int x) { /* definition */
return x * 2;
}
Every translation unit that includes header.h sees the declaration. Exactly one translation unit (impl.c) provides the definition.
Tentative definition
int value; /* tentative definition */
int main(void)
{
return value; /* returns 0 (static storage is zero-initialized) */
}
Because value is never given an explicit initializer, the tentative definition becomes a definition initialized to 0.
How It Works
Declarations give the compiler enough type information to generate correct references to a name. Definitions give the compiler (or the linker, for external symbols) the actual storage or code. The linker's job is to match every external reference (from a declaration) to its single definition.
Variations
Typedefs are declarations, not definitions
typedef unsigned long size_t;
A typedef introduces a name for a type but reserves no storage and produces no code. It is a declaration of an alias.
Struct declarations vs. definitions
struct Node; /* declaration of an incomplete type */
struct Node { int v; }; /* definition (completes the type) */
A struct *type* can be declared incomplete and defined later. This enables opaque types and forward references (c.struct.decl).
Common Mistakes
- Putting a definition in a header included by multiple files → duplicate
definition linker error. Use extern declarations in headers and define in one .c file (unless inline/static changes linkage, c.func.inline).
- Forgetting to define a function that is declared → "undefined reference"
linker error.
- Using reserved identifiers (
_Foo,__x). - Confusing
extern int x;(declaration) withint x;at file scope
(tentative definition).
Undefined Behavior
- Declaring the same identifier with incompatible types across translation
units is a constraint violation and, if undiagnosed, leads to UB.
- Using a name reserved by the implementation can cause UB or undefined
behavior depending on context.
Portability
- The exact set of reserved identifiers is standard and portable, but some
implementations reserve additional names (e.g., __attribute__ on GCC).
- Old-style function declarations (
int f();) have different rules and are
obsolescent; avoid them.
Under the Hood
The compiler records declarations in a symbol table for the translation unit. External declarations produce entries the linker must resolve; definitions produce definitions in the object file's symbol table. Linkage (c.lang.linkage) determines which symbols are visible across translation units.
Practical Usage
- Headers contain declarations; source files contain definitions.
- Use include guards in headers (
c.pp.conditional). - Use
externfor globals shared across files, and prefer functions over
shared mutable globals.
Exercises
1. Write a header declaring a function and a source file defining it, plus a main that calls it. Build and run. 2. Deliberately create a duplicate-definition error by defining a global in a header included twice, and observe the linker diagnostic. 3. Explain what a tentative definition is and demonstrate it with a program that reads an uninitialized file-scope int.
Deep Challenge
Explain why the following produces a linker error, and identify the exact diagnostic category:
/* a.c */
int helper(void) { return 1; }
/* b.c */
extern int helper(void);
int main(void) { return helper(); }
Then add a second definition of helper in a third file and explain what the standard says about multiple external definitions.
Related Concepts
c.lang.scope/c.lang.storage-duration— where names are visible and
how long objects live.
c.lang.linkage— how definitions connect across files.c.core.39— multi-file programs and headers.
References
- ISO/IEC 9899:2018 §6.2.1 (scopes of identifiers), §6.7 (declarations),
§6.9 (external definitions), §7.1.3 (reserved identifiers).
Verification
- The declaration/definition distinction is standard.
VERIFIED - Tentative definition rules are standard.
VERIFIED - Reserved identifier categories are standard.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Identifiers and reserved names
- [ ] Declarations
- [ ] Definitions
- [ ] Tentative definitions
- [ ] Typedef as declaration
- [ ] Incomplete struct declarations
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.lang.ident | 0 | 4 |
| c.lang.decl | 0 | 5 |
| c.lang.def | 0 | 5 |