Constants, Enumerations and Initialization
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C has four different ways to say "this value does not change" — const objects, enum constants, macro
constants and (new in C23) constexpr — and they are not interchangeable. This page covers all four, plus
every way to give an object its initial value.
const Objects
const means this object must not be modified through this name. It does not mean "compile-time constant":
#include <stdio.h>
int main(void)
{
const int limit = 100;
const double gravity = 9.80665;
// limit = 101; // error: read-only variable is not assignable
int table[100]; // fine at block scope, but this is a VLA if limit is used
printf("%d %g %zu\n", limit, gravity, sizeof table);
return 0;
}
The subtlety that makes const unlike other languages: const int limit = 100; is not a constant expression
in C, so before C23 it could not size a file-scope array or appear in a case label. That is exactly the gap
enum, macros and now constexpr fill.
const is at its most valuable on pointer parameters, where it documents and enforces "I will not write
through this":
#include <stddef.h>
// The pointee is const; the pointer itself is not.
size_t count_spaces(const char *text)
{
size_t n = 0;
for (const char *p = text; *p != '\0'; ++p) {
if (*p == ' ') {
++n;
}
}
return n;
}
See Pointers for const char * vs. char * const vs.
const char * const.
Enumerations
An enum declares a set of named integer constants. Unlike const, these are constant expressions:
#include <stdio.h>
enum Color {
COLOR_RED, // 0
COLOR_GREEN, // 1
COLOR_BLUE = 10, // explicit
COLOR_INDIGO, // 11 -- continues from the previous value
COLOR_VIOLET = 11 // duplicates are allowed
};
// The idiomatic pre-C23 way to declare a constant usable at compile time:
enum { BUFFER_SIZE = 4096 };
int main(void)
{
char buffer[BUFFER_SIZE]; // legal: BUFFER_SIZE is a constant expression
enum Color c = COLOR_BLUE;
switch (c) {
case COLOR_RED: puts("red"); break;
case COLOR_GREEN: puts("green"); break;
case COLOR_BLUE: puts("blue"); break;
default: puts("other"); break;
}
printf("%d %d %zu\n", (int)c, COLOR_INDIGO, sizeof buffer);
return 0;
}
Before C23, every enumeration constant had type int and the enumerated type had an
implementation-defined compatible integer type. Two C23 changes fix long-standing complaints:
-
Enumeration constants that do not fit in
intare allowed, taking a wider type. -
An enumeration may declare a fixed underlying type, which pins its size, its signedness and its constants' type:
#include <stdint.h>
#include <stdio.h>
enum Opcode : uint8_t { // C23: exactly one byte, unsigned
OP_NOP = 0x00,
OP_LOAD = 0x01,
OP_HALT = 0xFF
};
enum Flags : unsigned long long {
FLAG_NONE = 0ULL,
FLAG_HUGE = 1ULL << 40 // does not fit in int -- legal in C23
};
int main(void)
{
printf("%zu %u %llu\n", sizeof(enum Opcode), (unsigned)OP_HALT, (unsigned long long)FLAG_HUGE);
return 0;
}
A trailing comma after the last enumerator has been legal since C99 — keep it, so adding a member is a one-line diff.
Macros vs. constexpr
Three ways to write "the maximum number of items is 64", with different trade-offs:
#include <stdio.h>
#define MAX_ITEMS_MACRO 64 // 1. preprocessor: no type, no scope, no debugger symbol
enum { MAX_ITEMS_ENUM = 64 }; // 2. enum: type int, scoped, constant expression
constexpr int MAX_ITEMS_CONSTEXPR = 64; // 3. C23: typed, scoped, constant expression
int main(void)
{
int a[MAX_ITEMS_MACRO];
int b[MAX_ITEMS_ENUM];
int c[MAX_ITEMS_CONSTEXPR]; // C23: a real constant, so this is not a VLA
printf("%zu %zu %zu\n", sizeof a, sizeof b, sizeof c);
return 0;
}
| Form | When to use it |
|---|---|
|
Values needed by the preprocessor itself ( |
|
Integer constants on any C version — still the most portable typed-ish constant. |
|
C23 and later: the right default. It has a type (so |
constexpr in C is deliberately much narrower than in C++: it applies to objects, not functions, and its
initializer must be a constant expression.
|
|
constexpr double TAU = 6.283185307179586;
constexpr unsigned char MASK = 0xF0;
constexpr int DERIVED = 8 * 8; // constant expressions may be computed
// constexpr int bad = rand(); // error: not a constant expression
Initialization
An object that is not initialized has an indeterminate value — except for objects with static or thread storage duration, which are zero-initialized. Reading an indeterminate value is undefined behavior, so initialize at the point of declaration.
Scalars and Aggregates
#include <stdio.h>
struct Point { double x, y; };
int main(void)
{
int i = 7;
double d = 1.5;
const char *s = "text";
int primes[5] = { 2, 3, 5, 7, 11 };
int padded[5] = { 2, 3 }; // remaining elements are zero
int inferred[] = { 2, 3, 5 }; // size 3, deduced from the initializer
int grid[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
struct Point p = { 3.0, 4.0 }; // positional
char word[6] = "hello"; // 5 chars + NUL exactly fits
printf("%d %g %s %d %d %zu %d %g %s\n",
i, d, s, primes[4], padded[2], sizeof inferred / sizeof inferred[0],
grid[1][2], p.y, word);
return 0;
}
Designated Initializers (C99)
Naming the members makes the initializer order-independent and self-documenting — and everything not named is zero-initialized:
#include <stdio.h>
struct Config {
const char *host;
int port;
bool verbose;
int retries;
};
int main(void)
{
struct Config cfg = {
.host = "localhost",
.port = 8080,
.retries = 3, // .verbose is zero-initialized to false
};
int sparse[10] = { [0] = 1, [9] = 100 }; // array designators
int ranges[6] = { [1] = 2, 3, 4 }; // continues from index 1
printf("%s:%d verbose=%d retries=%d sparse=%d,%d ranges=%d\n",
cfg.host, cfg.port, (int)cfg.verbose, cfg.retries, sparse[0], sparse[9], ranges[3]);
return 0;
}
Prefer designated initializers for any struct with more than two members: adding a field then cannot silently shift the meaning of existing initializers.
Zero and Empty Initializers
#include <string.h>
struct Big { int a; char name[64]; double values[16]; };
int main(void)
{
struct Big zeroed = { 0 }; // classic: initializes the first member, zeroes the rest
struct Big empty = { }; // C23: empty initializer -- zero-initializes everything
struct Big cleared;
memset(&cleared, 0, sizeof cleared); // runtime alternative, no initializer needed
return zeroed.a + empty.a + cleared.a;
}
{ } (C23) says what it means and works for any type, including one whose first member is itself an aggregate.
Note that { 0 } and memset differ in principle: memset writes all-bits-zero, which is not guaranteed to be
a null pointer or a zero floating-point value on exotic targets.
Compound Literals (C99)
A compound literal creates an unnamed object of a given type, in place — useful for passing a temporary struct or array to a function:
#include <stdio.h>
struct Point { double x, y; };
static double distance_from_origin(struct Point p)
{
return p.x * p.x + p.y * p.y; // squared, to avoid needing -lm here
}
static int sum(const int *values, size_t n)
{
int total = 0;
for (size_t i = 0; i < n; ++i) {
total += values[i];
}
return total;
}
int main(void)
{
double d = distance_from_origin((struct Point){ .x = 3.0, .y = 4.0 });
int s = sum((int[]){ 1, 2, 3, 4 }, 4);
struct Point *heapless = &(struct Point){ .x = 1.0, .y = 2.0 }; // block-scope lifetime
printf("%g %d %g\n", d, s, heapless->x);
return 0;
}
The lifetime rule is the one to remember: a compound literal at block scope lives until the end of the
enclosing block, so returning a pointer to one is a dangling pointer. At file scope it has static storage
duration and lives forever. C23 adds constexpr-qualified compound literals and storage-class specifiers on
them.
static_assert
static_assert fails the compilation when an assumption about types or sizes is violated — the cheapest
possible test:
#include <limits.h>
#include <stdint.h>
static_assert(sizeof(int) >= 4, "this code assumes a 32-bit or wider int");
static_assert(CHAR_BIT == 8, "this code assumes 8-bit bytes");
static_assert(sizeof(uint32_t) * CHAR_BIT == 32, "uint32_t must be exactly 32 bits");
struct Header { uint16_t magic; uint16_t version; uint32_t length; };
static_assert(sizeof(struct Header) == 8, "Header must have no padding for the wire format");
int main(void)
{
static_assert(1 + 1 == 2, "arithmetic still works"); // block scope is fine too
return 0;
}
In C23 the message is optional (static_assert(cond);) and static_assert is a keyword; C11 spelled it
_Static_assert and required the message. Use it for every layout assumption a file makes — especially in
code that reads binary formats.
See Also
-
Basic Types and Values — the type each constant form has.
-
Structures, Unions and Type Aliases — what designated initializers initialize.
-
Preprocessor and Macros — when a macro is still the right answer.
-
Type-Generic Programming —
typeofwithconstexpr. -
C++: Constants, Enumerations, and Initialization — C++ adds
consteval/constinit, scoped enumerations, and list initialization.
References
-
WG14 N3220 — the C23 working draft (§6.7.3 "Type qualifiers", §6.7.2.2 "Enumeration specifiers", §6.7.11 "Initialization", §6.5.2.5 "Compound literals", §6.7.11 "Static assertions").