Syntax
The syntax of C is defined by ISO/IEC 9899 as a sequence of translation phases that take source text and produce a sequence of tokens for the compiler proper. The lexical surface — the characters, keywords, identifiers, literals, and punctuators that may legally appear in a source file — is small enough to enumerate. The grammatical surface that combines those tokens into declarations, statements, and expressions is similarly modest. The brevity of the grammar is one of the language’s defining properties: an experienced reader can hold the entire syntax of C in their head, which is part of why C compilers are comparatively small and why the language has been ported to nearly every architecture in commercial production.
A complete program
A conforming hosted program defines main and includes the headers that declare the library functions it uses:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "usage: %s <name>\n", argv[0]);
return EXIT_FAILURE;
}
printf("Hello, %s.\n", argv[1]);
return EXIT_SUCCESS;
}
The file is a translation unit. After preprocessing — #include directives are replaced by the contents of the named files, macros are expanded, conditionals are resolved — the compiler sees a sequence of tokens in which every external dependency has been textually substituted in place. The resulting tokens are parsed against the grammar specified in §6 of ISO/IEC 9899:2018, type-checked, and emitted as object code.
Source character set
The standard distinguishes the source character set, in which the program is written, from the execution character set, in which characters appear at runtime. The basic source set consists of the upper- and lower-case letters of the English alphabet, the decimal digits, and twenty-nine graphic characters: ! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~. Whitespace consists of the space, the horizontal tab, the vertical tab, the form feed, and the new-line. Implementations that accept a wider source set — most do, supporting UTF-8 — process the additional characters according to the universal character names mechanism (\uXXXX and \UXXXXXXXX), defined in §6.4.3.
Comments
The language supports two comment forms:
/* a block comment, possibly spanning multiple lines */
// a line comment, terminated by the end of the line (since C99)
Block comments do not nest. The line-comment form was admitted in C99, having been a common extension before that. In contemporary code the line form predominates for short remarks; the block form remains conventional for file headers and for prose paragraphs that may need to be displayed within the source.
Identifiers and keywords
An identifier consists of a non-digit character followed by any number of digit and non-digit characters. The non-digit characters are the upper- and lower-case letters, the underscore, and any universal character name designating a character permitted by Annex D. The implementation must distinguish at least the first 63 characters of an internal identifier and the first 31 characters of an external identifier, although every modern implementation accepts considerably more.
The keywords are reserved and may not be used as identifiers. As of C23 the set is:
alignas else struct
alignof enum switch
auto extern thread_local
bool false true
break float typedef
case for typeof
char goto typeof_unqual
const if union
constexpr inline unsigned
continue int void
default long volatile
do nullptr while
double register _Atomic
_BitInt
_Decimal128
_Decimal32
_Decimal64
static_assert
static
short
signed
sizeof
return
restrict
Identifiers beginning with an underscore followed by an upper-case letter, or by a second underscore, are reserved for the implementation; they may appear in standard headers but not in user code.
Declarations and definitions
A declaration introduces an identifier and gives it a type; a definition additionally allocates storage for it (for objects) or supplies a body (for functions):
extern int errno; // declaration; storage defined elsewhere
int counter; // definition; reserves storage at file scope
int square(int x); // function declaration (a *prototype*)
int square(int x) { return x * x; } // function definition
A declaration consists of declaration specifiers — type specifiers (int, struct point), type qualifiers (const, volatile), storage-class specifiers (static, extern, auto, register), and function specifiers (inline, _Noreturn) — followed by one or more declarators, each of which names an object or function and combines the base type with pointer, array, and function modifiers. Declarators read outward from the identifier:
int *p; // p is a pointer to int
int a[10]; // a is an array of 10 ints
int *ap[10]; // ap is an array of 10 pointers to int
int (*pa)[10]; // pa is a pointer to an array of 10 ints
int (*fp)(int); // fp is a pointer to a function taking int and returning int
int *f(int); // f is a function taking int and returning a pointer to int
The convention by which declarations mirror the syntax of expressions — declaration follows use — is one of the design choices that requires acclimation. Reading the more elaborate forms is straightforward once it is internalised: read the identifier first, then the surrounding modifiers from the closest binding outward.
Statements
The statement grammar is small. The principal forms are:
expression; // expression statement
{ /* statements */ } // compound statement (a block)
if (cond) statement // selection
if (cond) statement else statement
switch (expr) { /* cases */ }
while (cond) statement // iteration
do statement while (cond);
for (init; cond; step) statement
goto label; // unconditional transfer
continue;
break;
return expr;
Every statement is terminated by a semicolon or, in the case of compound statements, by a closing brace. The grammar admits no statements that are evaluated for both their side effects and their result; the comma expression is a separate construction at the expression level.
Type qualifiers
Three type qualifiers modify the type of the qualified object:
const— the object’s value may not be modified through the qualified lvalue.volatile— the object may be modified by means outside the program’s control; the implementation may not optimise accesses to it.restrict— applied to a pointer, asserts that the object the pointer designates will be accessed through that pointer (and pointers based on it) for the duration of the access. Permits the compiler to assume non-aliasing.
C11 added _Atomic as a fourth qualifier, treated as a qualifier in declarations but with a richer specification involving the <stdatomic.h> library.
const int answer = 42;
volatile int *hardware_register;
void copy(int * restrict dst, const int * restrict src, size_t n);
Storage-class specifiers
Five storage-class specifiers determine the lifetime, visibility, and uniqueness of objects:
| Specifier | Effect |
|---|---|
auto | Automatic storage duration; the default at block scope. Rarely written explicitly. |
register | A hint that the object should be kept in a register; the address-of operator may not be applied. Largely vestigial. |
static | Static storage duration. At file scope, restricts linkage to the translation unit. At block scope, makes the object persist across function calls. |
extern | Indicates that the declaration refers to an object or function defined elsewhere; gives external linkage. |
thread_local | Static storage duration with one instance per thread (since C11; spelt _Thread_local before C23). |
The interaction of storage class with scope and linkage is the subject of Scope and linkage.
Expressions
An expression is a sequence of operators and operands that designates a value, designates an object, generates side effects, or some combination. C is an expression-oriented language: assignment, function call, the conditional operator, and increment all yield values and may be embedded within larger expressions. The full operator and precedence table is the subject of Operators.
The compiler is permitted to reorder evaluation of subexpressions within the limits set by the sequence point rules. Two unsequenced modifications of the same scalar object yield undefined behaviour; the canonical example is i = i++;, the meaning of which the standard does not define.
A note on undefined behaviour
The standard distinguishes three classes of behaviour that are not portable:
- Implementation-defined — the implementation must choose a behaviour and document it (the size of
int, the result of right-shifting a negative integer). - Unspecified — the implementation may choose any of several behaviours and need not document the choice (the order in which function arguments are evaluated).
- Undefined — the standard imposes no requirements; the program may produce any output, including none, or fail to compile (dereferencing a null pointer, signed-integer overflow, accessing an object outside its lifetime).
Undefined behaviour is the contract by which C compilers obtain their performance: the compiler is free to assume that no program ever invokes it. Consequently, code that does invoke undefined behaviour may compile without diagnostic and execute in ways that depend on optimisation level, compiler version, and surrounding context. Avoiding undefined behaviour is the chief discipline of writing correct C.