Read from the name outward, right to left

char * const argv[]
            argv[]   → argv is an array
      * const        → of const pointers
char                 → to char

Rules of thumb:

  • [] and () next to the name bind first (“array of”, “function returning”).
  • Then read leftward: each * is “pointer to”.
  • A const applies to whatever is immediately to its left; if nothing is to its left, it applies to the thing on its right.
DeclarationCan change the chars?Can repoint?
char *pyesyes
const char *p (same as char const *p)noyes
char *const pyesno
const char *const pnono

The duplicate-const trap

static const char const *v[];   // looks like "const pointers to const chars"

Both consts are left of the *, so both apply to char: you’ve made the chars const twice and the pointers not at all. GCC says:

error: duplicate 'const' declaration specifier [-Werror=duplicate-decl-specifier]

What was meant: const char *const v[]. The const for the pointer goes after the *.

Why execv takes char *const argv[]

int execv(const char *path, char *const argv[]);

exec promises not to repoint the slots, but not to leave the chars alone, even though it never writes to them. The fully-const signature would have been const char *const argv[], but C won’t implicitly convert char ** to const char *const *, so every existing caller would have needed a cast. POSIX kept the old signature and documents that the strings aren’t modified (man 3p exec, RATIONALE).

The other direction bites if you write const-correct code:

error: passing argument 2 of 'execv' from incompatible pointer type [-Wincompatible-pointer-types]
note: expected 'char * const*' but argument is of type 'const char * const*'

The usual answer is an explicit cast at the call, (char *const *)argv, which is safe because of the POSIX guarantee. (In GCC 14+ incompatible pointer types are an error by default, even without -Werror.)

References: man 3 exec, man 3p exec; cdecl.org to check your reading.