“Read from right to left” is not a reliable complete method for complex C declarations. A stronger approach follows the standard grammar: split a declaration into its declaration specifiers and declarator, start at the identifier, process adjacent array or function suffixes, then process pointer layers; parentheses change the grouping. Finally, combine the base type, qualifiers, and storage class.
The rules here follow the WG14 C23 draft. The examples compile in C17 mode so they remain reproducible on widely deployed toolchains. Syntax and type constraints come from C; object layout, pointer representation, calling convention, and symbol rules come from a particular implementation and ABI. Do not conflate them.
Table of Contents
The two main parts of a declaration
Consider:
static const unsigned long *samples[8];
static const unsigned longis the declaration-specifier sequence:staticis a storage-class specifier,constis a type qualifier, andunsigned longprovides the type specifiers.samples[8]is the declarator.samples[8]groups first, sosamplesis an eight-element array; thesays that each element is a pointer.- Together:
samplesis “an array of eight pointers toconst unsigned long.”
A declarator’s shape resembles an expression: combinations such as a[i], f(), and *p are reflected in declarations. That is only a reading aid; the declarator grammar remains the authority.
A repeatable reading procedure
- Find the identifier in the current declarator. If it is parenthesized, begin inside the parentheses.
- Read suffixes directly attached to the identifier or grouped declarator first:
[]means “array of,” and()means “function returning.” - Then read the
at the left of that layer. Aconst,volatile,restrict, or_Atomicimmediately following an individualqualifies that pointer layer. - Leave the parentheses and repeat the suffix and pointer steps until the declarator is exhausted.
- Finally, combine the base type and qualifiers from the declaration specifiers.
- Treat a
typedefname as one complete type name, not as a textual macro expansion.
“Right before left” can remind you that suffixes bind tightly, but it does not replace grouping and grammar. It is especially easy to fail on function pointers, pointers to arrays, and typedef names.
Matrix: parentheses change the type
| Declaration | Reading | Decisive grouping |
|---|---|---|
int *items[4] | items is an array of four pointers to int | [] groups with items first |
int (*row)[4] | row is a pointer to an array of four int | Parentheses group *row first |
int *make(void) | make is a no-argument function returning int * | make(void) groups first |
int (*callback)(void) | callback is a pointer to a no-argument function returning int | Parentheses group *callback first |
int (*handlers[3])(double) | handlers is an array of three function pointers; each function takes double and returns int | Array, then pointer, then function |
int (producer)(void) | producer is a function pointer; the function takes no arguments and returns int * | Pointer, then function, then returned pointer |
C does not permit a function to return an array type or function type, and an array element cannot have function type. Pointers to those types are the legal form.
One compile-clean combined example
This file covers arrays and pointers, function pointers, a function-type typedef, a pointer typedef, and const placement:
#include <stddef.h>
typedef char *char_ptr;
typedef int comparator(const void *, const void *);
static int compare_ints(const void *left, const void *right)
{
const int a = *(const int *)left;
const int b = *(const int *)right;
return (a > b) - (a < b);
}
static int *make_value(void)
{
static int value = 7;
return &value;
}
static void declarations(void)
{
char buffer[8] = {0};
const char *read_only = buffer;
char *const fixed = buffer;
const char *const both = buffer;
char_ptr const alias_fixed = buffer;
int *items[4] = {0};
int matrix[3][4] = {{0}};
int (*row)[4] = &matrix[0];
comparator *cmp = compare_ints;
int *(*producer)(void) = make_value;
(void)read_only;
fixed[0] = 'A';
(void)both;
alias_fixed[1] = 'B';
(void)items;
(*row)[0] = cmp(&matrix[0][0], &matrix[0][1]);
(void)producer;
}
int main(void)
{
declarations();
return 0;
}
comparator is a function type, not a pointer-to-function type; comparator cmp is the pointer. producer is written directly as a pointer to a function returning int .
Which layer does const qualify?
const char p and char const p are equivalent: p is an ordinary pointer, and the pointed-to char cannot be modified through this expression. In char const p, const follows the , so p cannot be reassigned, while its char can be modified. const char *const p qualifies both layers.
The three declarations in the historical note read precisely as follows:
| Declaration | p itself | Object pointed to by p | Innermost character |
|---|---|---|---|
const char **p | Reassignable pointer | Reassignable const char * pointer | Read-only through this type |
char const p | Reassignable pointer | Non-reassignable char * pointer | Writable |
char **const p | Non-reassignable pointer | Reassignable char * pointer | Writable |
“Cannot modify through a const-qualified lvalue” does not mean the underlying object can never change anywhere. If the object was not originally defined with a const-qualified type, another unqualified alias may still modify it legally. const also says nothing about ownership, lifetime, or thread safety.
const is not automatically transitive through nested pointers
Implicitly treating char as const char violates a constraint. Otherwise the callee could store a pointer to a genuinely const char through the latter, then let the original char * attempt to modify that constant object.
This is an intentionally invalid diagnostic case:
static void rejected_conversion(void)
{
char *mutable = 0;
const char **slot = &mutable;
(void)slot;
}
Do not cast away the diagnostic. Depending on the interface intent, copy to a one-level const char * view, redesign the parameter nesting, or make caller and callee use genuinely compatible types.
typedef can hide the declarator shape
A typedef creates a type name; it is not a text-substitution macro:
typedef char *char_ptr;
char buffer[8] = {0};
char_ptr const fixed = buffer;
Here const qualifies the complete pointer type denoted by char_ptr, so fixed is char const, not const char . Similarly:
typedef int operation(double);
operation *handler;
operation is a function type and handler is a function pointer. During review, locate the typedef definition first. Clear naming for pointer aliases can reduce misreading, but it does not change the language rules.
Array and function parameters are adjusted
Only in a function parameter declaration, a parameter written with array type is adjusted to a pointer, and a parameter written with function type is adjusted to a function pointer. This rule does not turn an ordinary array object into a pointer.
#include <stddef.h>
static int double_value(int value)
{
return value * 2;
}
static int apply(int operation(int), int value)
{
return operation(value);
}
static int sum4(const int values[static 4])
{
return values[0] + values[1] + values[2] + values[3];
}
static void clear_first(size_t rows, int matrix[static rows][4])
{
matrix[0][0] = 0;
}
int main(void)
{
int matrix[2][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8}
};
const int total = sum4(matrix[0]);
const int doubled = apply(double_value, 3);
clear_first(2, matrix);
return total == 10 && doubled == 6 && matrix[0][0] == 0 ? 0 : 1;
}
- As a parameter,
int operation(int)adjusts toint (*operation)(int). Writing the pointer form explicitly is usually clearer in a public API. const int values[static 4]adjusts to a pointer toconst int;static 4additionally requires each call to provide access to at least four elements.- The outermost array in
int matrix[static rows][4]adjusts to a pointer to an array of fourint; the inner extent of four remains part of the pointed-to type. - Qualifiers inside a parameter’s square brackets qualify the adjusted pointer; an element qualifier is written on the base type.
- Because the parameter has been adjusted,
sizeofon it inside the function yields the pointer size, not the caller’s array size. Pass lengths explicitly.
An array expression also converts to a pointer to its first element in most expression contexts, but that is a related and distinct rule from parameter-type adjustment.
_Atomic: use it only when concurrency semantics require it
_Atomic belongs to the type-qualification system, but it is not decoration for a declarator exercise. This example uses it only to show levels:
#include <stdatomic.h>
static _Atomic(int) counter;
static int * _Atomic head;
static _Atomic(int) *pointer_to_atomic;
int main(void)
{
int value = 0;
atomic_store(&counter, 1);
atomic_store(&head, &value);
pointer_to_atomic = &counter;
return atomic_load(pointer_to_atomic) == 1
&& *atomic_load(&head) == 0
? 0
: 1;
}
counter is an atomic int object; head is an atomic pointer to ordinary int; pointer_to_atomic is an ordinary pointer to atomic int. Real concurrent code must also define synchronization, memory order, and object lifetime and check whether the target implementation is lock-free. Placing _Atomic in a declaration does not complete that design.
restrict and volatile also have their own semantic constraints: restrict is an access-association optimization contract, and volatile is not a thread-synchronization primitive. Do not treat them as variants of const.
Language constraints are not ABI guarantees
The standard grammar can tell you that int (*callback)(void) is a function pointer. It does not guarantee:
- the concrete size, alignment, or representation of
int, object pointers, or function pointers; - structure layout, padding, byte order, or bit-field layout;
- a platform calling convention, register passing, name decoration, or dynamic-linking rules;
- that arbitrary object pointers and function pointers are interchangeable;
- a third-party library’s ownership, nullability, buffer-length, or callback-lifetime contract.
An FFI, plugin boundary, driver interface, or network/on-disk format requires the target ABI and library documentation too. Do not infer a portable guarantee from one local sizeof, one successful link, or a cast. Where layout is fixed, use types and compile-time checks defined by the target specification and validate every supported target.
Why generated declarations still require review
Declaration generators, header generators, and compiler ASTs can help confirm parenthesis structure. They cannot decide:
- whether the selected dialect is C17, C23, or a compiler-extended mode;
- which declaration survives macros and conditional compilation;
- whether a
typedefhides a pointer, function, or target-dependent type; - whether calling convention, visibility, packing attributes, and ABI match;
- whether
const,restrict, array bounds, ownership, lifetime, and threading contracts express the real interface; - whether the generated declaration matches the library binary, header version, and target triple.
Put generated output into a minimal header and call site, parse it with the target compiler, and review its semantics. Successful compilation proves that one compiler accepts it in one mode; it does not prove API correctness or cross-ABI portability.
Reproduce the compile checks in a disposable directory
The three complete examples and one expected-failure case were tested with GCC 13.3.0. The valid files used:
cc --version
cc -std=c17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only declarators.c
cc -std=c17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only parameter-adjustment.c
cc -std=c17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only atomic-declaration.c
The expected-failure file used:
if cc -std=c17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only bad-const.c; then
echo "unexpected success"
exit 1
fi
echo "expected constraint diagnostic observed"
-std=c17 fixes the language mode, -Wpedantic requests relevant ISO-required and compiler-supplied diagnostics, -Werror turns warnings into errors, and -fsyntax-only emits no object file. The GCC manual also states that -Wpedantic is not proof that every non-ISO construct has been found. If the project targets C23, run -std=c23 separately on a compiler that fully supports the features in use and record the compiler version and target.
Review checklist
- Mark declaration specifiers and the declarator separately.
- Start from the identifier, honor parentheses, and read adjacent suffixes before pointer layers.
- Record the qualifier following each individual
*. - Expand the concept, not the text, of a
typedef; determine whether its alias denotes an object, pointer, array, or function type. - Distinguish a pointer to an array from an array of pointers, and a function returning a pointer from a function pointer.
- For parameters, record array/function adjustment, minimum-length contracts, and inner extents that remain.
- Do not use a cast to bypass incompatible nested-pointer qualification.
- Document ownership, lifetime, nullability, buffer length, and thread semantics outside the type.
- Fix the compiler dialect and enable strict diagnostics; check public boundaries with at least two target compilers or platforms.
- Validate the ABI separately for FFI and binary interfaces; C syntax validation is insufficient.
Standards and compiler references
- WG14 N3096: ISO/IEC 9899:2023 working draft, especially the clauses on declarations, type qualifiers, declarators, array declarators, function declarators, and type definitions.
- WG14 N1570: C11 committee draft, useful for cross-checking
_Atomicand widely deployed C11/C17 rules. - GCC language standards
- GCC warning options
- Clang User’s Manual: language modes and diagnostics
Historical source archive (provenance only)
The complete visible body from
source_exportfollows verbatim. Nothing was removed, rewritten, whitespace-normalized, or redacted for privacy or safety. Its “read from right to left” rule is only a limited mnemonic and cannot replace the standard declarator grammar. The whole block is inert plain text; do not treat its explanation as the complete current rule.
例如:
const char **p;
char const p;
char **const p;
阅读变量声明,要从右往左阅读。例如其中的
char const p
char *const *p前面有*表示p是一个指针,char *const *p表示指针p指向的内容为const型的。char *const *p表示const *p所指向的内容为指针,char *const *p 表示‘指针p’指向的’const型指针’所指向的内容为char型的。
