Proven C Book←↑→

21 Expressions and constants — the things that become values

What to know first

chapter 6, Representing integers · the representation of integers
chapter 20, The structure of a program · where statements and expressions sit

Looking back

Chapters 6–7 taught how to hold numbers in the machine (two’s complement, IEEE 754). So when a number is written in source code — when you write 12, say — when does it become the machine’s bits?

A. At compile time. The 12 in the source is just two characters (in the terms of chapter 8, the characters 1 and 2), and the compiler reads it, translates it into a two’s-complement bit pattern and plants it in the executable. A notation for a value written in source is called a literal — meaning “as the letters say.” That the bridge between the world of letters and the world of bits is the compiler — chapter 17′s relay is at work here too.

The need for this chapter, and its context

The first step inside the skeleton is how to write a value down. Making this chapter the gathering place for constant notation is deliberate — scatter the integer, character, floating and string notations and there is nowhere to look them up later. The integer representation of chapter 6 turns here, for the first time, into characters written in source.

By the end of this chapter

We go inside the statement. How to write values in source code is half of it — the notations for integer, character, floating and string constants are gathered here in one place, so this is where to come back when “what was that notation again?” arises — and the other half is weaving values into calculations (operators and expressions) and the order of calculation (precedence and parentheses). As this book’s principle demands, only the minimum set of operators: addition, subtraction, multiplication and parentheses. Even that goes surprisingly far.

The questions this chapter answers

  1. Then if I write 0.1 in the source, does the machine hold exactly 0.1?
  2. Why the advice never to use a lowercase l?
  3. Why must a hex float have the p exponent, and why use one at all?
  4. So should every #define constant become a constexpr?
  5. Is it the same as C++‘s constexpr?
  6. Why was division put off? Leaving one of the four operations out feels odd.
  7. Is printf("hello") a value too? A function call is written in the position of an expression.

21.1 Literals — values written in source#

We have met literals several times already. The 0 of hello world is an integer literal, and "Hello, world!\n" is a string literal — the characters inside the quotation marks become a value “as they are.” Add a decimal point to an integer literal (3.5) and it becomes a floating-point value from chapter 7.

Q. Then if I write 0.1 in the source, does the machine hold exactly 0.1?

A. It does not — exactly as chapter 7 taught. The compiler translates the literal 0.1 into its nearest neighbour (3FB9 9999 9999 999A). A literal is “a notation writing the value as it is”, but if that value is not on the grid of representable numbers, the approximation has already happened at translation. Writing it in the source does not make it exact — chapter 7′s lesson holds in the world of source code too.

21.2 Writing constants — all of them at a glance#

Having met literals in the previous section, this is the place to gather every notation for writing a value in source at once. The standard scatters them through its lexical clauses (§6.4.4, §6.4.5), and what trips people in practice is nearly always one of three things: a prefix, a suffix or an escape.

This section is for reference. There is no need to memorise it now — take away the sense that these notations exist, and come back when you need them. Read the listing in that spirit too: skimming the output and matching notation to result is enough.

examples/ch20/constants.c

/* 상수를 적는 방법들 — 표기가 값과 타입을 어떻게 정하는가. */
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <uchar.h>

int main(void)
{
    puts("[integer constants - four bases and digit separators (C23)]");
    printf("  1234        = %d\n", 1234);          /* 10진 */
    printf("  0755        = %d   <- a leading 0 means octal\n", 0755);
    printf("  0xFF        = %d\n", 0xFF);
    printf("  0b1010      = %d   <- binary, new in C23\n", 0b1010);
    printf("  1'000'000   = %d   <- digit separators, new in C23\n", 1'000'000);
    printf("  0b11'10'11'01 = %d\n", 0b11'10'11'01);

    puts("\n[접미어가 타입을 정한다]");
    printf("  sizeof 1    = %zu, sizeof 1L  = %zu, sizeof 1LL = %zu\n",
           sizeof 1, sizeof 1L, sizeof 1LL);
    printf("  sizeof 1U   = %zu, sizeof 1wb = %zu  ← wb 는 C23 의 _BitInt\n",
           sizeof 1U, sizeof 1wb);

    puts("\n[문자 상수 — 접두어가 타입을 정한다]");
    printf("  'a'   크기 %zu, 값 %d      ← C 에서 문자 상수는 int 다\n",
           sizeof 'a', 'a');
    printf("  u8'a' 크기 %zu (char8_t)\n",  sizeof u8'a');
    printf("  u'a'  크기 %zu (char16_t)\n", sizeof u'a');
    printf("  U'a'  크기 %zu (char32_t)\n", sizeof U'a');
    printf("  L'a'  크기 %zu (wchar_t)\n",  sizeof L'a');
    /* 'ab' 같은 다중 문자 상수는 값이 구현 정의라 -Wmultichar 가 경고한다.
       여기서는 경고를 켠 채 두고, 값은 본문의 실측 표로만 보인다. */

    puts("\n[이스케이프 — 8진과 16진]");
    printf("  '\\101' = %d, '\\x41' = %d   ← 둘 다 'A'\n", '\101', '\x41');
    printf("  \"\\x41\" \"1\" = \"%s\"        ← 16진은 가장 긴 열을 먹는다.\n",
           "\x41" "1");
    puts("    그래서 \"\\x411\" 이 아니라 문자열을 쪼개 이어 붙인다.");

    puts("\n[부동소수점 상수]");
    printf("  3.14  1e3=%g  1.=%g  .5=%g\n", 1e3, 1., .5);
    printf("  0x1p-3 = %g            ← 16진 부동소수점. 지수부 p 는 필수다\n", 0x1p-3);
    printf("  sizeof 1.0 = %zu, 1.0f = %zu, 1.0L = %zu\n",
           sizeof 1.0, sizeof 1.0f, sizeof 1.0L);
    printf("  0.1 == 0.1f ? %s   ← 접미어가 다르면 값도 다르다\n",
           (double)0.1f == 0.1 ? "예" : "아니오");

    puts("\n[문자열 리터럴]");
    printf("  sizeof \"abc\" = %zu        ← NUL 이 한 칸 더 붙는다\n", sizeof "abc");
    printf("  \"hello, \" \"world\" = \"%s\"  ← 인접한 것은 하나로 이어진다\n",
           "hello, " "world");
    printf("  \"a\\0b\": strlen = %zu, sizeof = %zu  ← 안에 NUL 을 넣어도 배열은 남는다\n",
           strlen("a\0b"), sizeof "a\0b");
    printf("  sizeof u8\"가\" = %zu, sizeof u\"가\" = %zu, sizeof U\"가\" = %zu\n",
           sizeof u8"\uAC00", sizeof u"\uAC00", sizeof U"\uAC00");
    return 0;
}

Output

[integer constants - four bases and digit separators (C23)]
  1234        = 1234
  0755        = 493   <- a leading 0 means octal
  0xFF        = 255
  0b1010      = 10   <- binary, new in C23
  1'000'000   = 1000000   <- digit separators, new in C23
  0b11'10'11'01 = 237

[접미어가 타입을 정한다]
  sizeof 1    = 4, sizeof 1L  = 8, sizeof 1LL = 8
  sizeof 1U   = 4, sizeof 1wb = 1  ← wb 는 C23 의 _BitInt

[문자 상수 — 접두어가 타입을 정한다]
  'a'   크기 4, 값 97      ← C 에서 문자 상수는 int 다
  u8'a' 크기 1 (char8_t)
  u'a'  크기 2 (char16_t)
  U'a'  크기 4 (char32_t)
  L'a'  크기 4 (wchar_t)

[이스케이프 — 8진과 16진]
  '\101' = 65, '\x41' = 65   ← 둘 다 'A'
  "\x41" "1" = "A1"        ← 16진은 가장 긴 열을 먹는다.
    그래서 "\x411" 이 아니라 문자열을 쪼개 이어 붙인다.

[부동소수점 상수]
  3.14  1e3=1000  1.=1  .5=0.5
  0x1p-3 = 0.125            ← 16진 부동소수점. 지수부 p 는 필수다
  sizeof 1.0 = 8, 1.0f = 4, 1.0L = 16
  0.1 == 0.1f ? 아니오   ← 접미어가 다르면 값도 다르다

[문자열 리터럴]
  sizeof "abc" = 4        ← NUL 이 한 칸 더 붙는다
  "hello, " "world" = "hello, world"  ← 인접한 것은 하나로 이어진다
  "a\0b": strlen = 1, sizeof = 4  ← 안에 NUL 을 넣어도 배열은 남는다
  sizeof u8"가" = 4, sizeof u"가" = 4, sizeof U"가" = 8

21.3 Integer constants (§6.4.4.1)#

BaseNotationExampleNote
decimalstarts with 1~91234
octalstarts with 00755 = 493the commonest trap — 08 is an error
hexadecimal0x or 0X0xFF = 255
binary0b or 0B0b1010 = 10added in C23

Table 21.1 — The bases an integer constant can be written in

Letters can follow too — a suffix, as in 10L, 1U, 3ULL. It marks not the value but the type.

SuffixWhat it marks
u Uan unsigned type
l Llong or wider
ll LLlong long or wider
wb WB_BitInt (C23)

Table 21.2 — The suffixes of an integer constant

For now, that a suffix changes the type is all you need. The type names above, and the exact rule for which type the compiler picks when there is no suffix, come in chapter 28 once integers have been met properly, in “The type of an integer constant”. One thing in advance — writing the same value in decimal or in hexadecimal can give it a different type.

Q. Why the advice never to use a lowercase l?

A. Because in many fonts 1 (one) and l (ell) look almost identical. 10l being read as 101 has really happened. Suffixes in capitals — 10L, 1UL — is the long-standing convention, and this book follows it. The 0x prefix is conventionally lowercase, which looks like the opposite rule, but the reason is the same: whichever is easier to tell apart.

21.4 The C23 digit separator '#

A notation for breaking up long numbers arrived in C23. It has no effect on the value — in the standard’s words it is ignored when determining the value of the constant.

The standard’s own example shows the traps along with the feature. Measured:

WrittenResultWhy
12'341234a separator goes only between digits
0b11'10'11'01237binary takes it too
0x1'2'3'4AB'C'D305441741so does hexadecimal
0x'FFerror — “digit separator after base indicator”it may not follow 0x
'1'2errorread as the character constant '1' followed by 2

Table 21.3 — Where the C23 digit separator works and where it does not

The last row is this notation’s one danger. A separator is a separator only between two digits; at the front it is read as a single quote — the start of a character constant.

21.5 Character constants (§6.4.4.5)#

The type names read fully only after chapter 8′s character sets and chapter 28′s integers — for now just see that the prefix settles the type.

NotationTypeMeasured sizeNote
'a'int4★in C a character constant is not a char
u8'a'char8_t1C23. Must be one UTF-8 code unit
u'a'char16_t2one UTF-16 code unit
U'a'char32_t4one UTF-32 code unit
L'a'wchar_t4 (Linux), 2 (Windows)the wide literal encoding (chapter 8)
'ab'int4value implementation-defined. GCC warns with -Wmultichar

Table 21.4 — Character constant prefixes and their types

A common misconception. “'a' is a char, so its size is 1”

True in C++ and false in C. Measured, the very same sizeof('a') is 4 in C and 1 in C++.

The standard’s sentence is “an integer character constant has type int” (§6.4.4.5p11). Its value is “what results when an object of type char holding that character is converted to int”, so the value is what you expect and only the type is wider.

Where it shows is mostly sizeof, _Generic, and overloading on the C++ side. Write only C and the practical harm is near zero, but in a header used from both languages it must be known.

The escapes are exactly these (§6.4.4.5).

KindNotationNote
must be escaped\' \\the single quote and the backslash must take this form
optional\" \?inside a string \" is needed
non-graphic characters\a \b \f \n \r \t \vtheir meanings are defined in §5.2.3
octal\ + octal digitsat most three digits
hexadecimal\x + hex digits★there is no digit limit
universal character names\uXXXX \UXXXXXXXXnaming characters outside the basic set

Table 21.5 — The kinds of escape sequence

Counter-example. "\x411" — a hex escape eats the letter after it

The standard nails it: each octal or hexadecimal escape sequence is the longest sequence of characters that can constitute the escape sequence (§6.4.4.5p7). Octal stops at three digits; hexadecimal does not stop.

"\x411"      /* not 'A'(0x41) then '1', but a request for 0x411 */

Measured, GCC warns hex escape sequence out of range. The fix is to split the string and let it join — adjacent string literals concatenate (below), so "\x41" "1" is exactly "A1".

21.6 Floating constants (§6.4.4.3)#

The decimal form must have either a decimal point or an exponent part. So 1., .5 and 1e3 are all valid, while 1 is an integer constant.

Floating constants take suffixes too (what the types are comes in chapters 7 and 52).

SuffixTypeMeasured size
(none)double8
f Ffloat4
l Llong double16 (x86-64 Linux)
df dd dl_Decimal32 / _Decimal64 / _Decimal128 (C23)4 / 8 / 16

Table 21.6 — The suffixes of a floating constant

There are also hexadecimal floating constants (C99) — 0x1p-3 is exactly 0.125.

Q. Why must a hex float have the p exponent, and why use one at all?

A. Because e is unavailable: in hexadecimal e is the digit 14 and cannot start an exponent. So p, meaning a binary exponent, was given its own place, and p cannot be omitted — without it there is no telling where the significand ends. 0x1p-3 is “1 × 2−3”.

The reason to use one is exactness. As chapter 7 showed, decimal 0.1 does not sit exactly in binary, whereas the hex notation transcribes the binary representation itself, so no rounding happens in translation. Hence its use in floating-point tests’ expected values, in the standard library’s tables of constants, and in papers about floating point. printf’s %a prints in the same notation (appendix B).

Worth noting too that the suffix changes the value. Measured, (double)0.1f == 0.1 is false — 0.1f is the nearest value on the float grid and 0.1 the nearest on the double grid, and those are different numbers (chapters 7 and 52).

21.7 String literals (§6.4.5)#

NotationElement typeEncodingMeasured sizeof
"가"charthe literal encoding (chapter 8)4 (3 UTF-8 bytes + NUL)
u8"가"char8_talways UTF-84
u"가"char16_tUTF-164 (1 code unit + NUL)
U"가"char32_tUTF-328
L"가"wchar_tthe wide literal encoding8 (Linux)

Table 21.7 — String literal prefixes and their encodings

Four properties go together.

  1. A NUL is appended. sizeof "abc" is not 3 but 4.
  2. Adjacent literals join into one (translation phase 6). "hello, " "world" is one string. It is the standard way to split a long string across lines, and the way out of the \x trap above. But the prefixes must not be mixed — u"a" U"b" is a compile error (a constraint violation).
  3. A NUL inside does not cut the array short. "a\0b" has strlen 1 and sizeof 4 — the string functions stop, the data is all there.
  4. Modifying one is undefined behaviour. Measured, it usually dies at run time (it is placed in a read-only section). So take string literals as const char *.

Counter-example. char *s = "abc"; s[0] = 'X';

It compiles (in C the type of a string literal is char[N], not const), and it dies when run — SIGSEGV in the measurement.

C++ closed this off entirely (a string literal is const char[N] there, so the assignment is an error). C left it open for compatibility with old code, so the habit has to close it — take it as const char *s = "abc"; and the compiler catches it. Turning on -Wwrite-strings is another way.

21.8 Things that look like constants#

NotationWhat it really isMore
RED (an enumeration constant)an integer constant, of type intchapter 59 — it lives in the ordinary-identifier yard
nullptra keyword of type nullptr_t (C23)chapter 37
true falsekeywords yielding bool values (C23)chapter 31
(int[]){1,2,3} a compound literalnot a constant but an object — you can take its addresschapter 48
#define N 100not a constant but token replacementchapter 61
constexpr int n = 10;C23′s real constant — usable in a constant expressionthe very next section

Table 21.8 — What the things that look like constants really are

The last two rows pay off in practice. A macro has neither type nor scope (chapter 61), and a const int is not a constant expression in C — the place where C and C++ part. But “cannot be used” is less accurate than where it cannot be, so here it is, measured.

Given const int n = 10;, writingResult (GCC, C23)
int a[n]; inside a blockaccepted — but as a variable length array, not a constant one
int a[n]; at file scopeerror — variably modified 'a' at file scope
static int a[n]; inside a blockerror — storage size of 'a' isn't constant
case n:error — case label does not reduce to an integer constant

Table 21.9 — Where a const int is refused

That is, it fails wherever a real constant expression is required. C23′s constexpr came in to fill that place — which is the next section.

21.9 constexpr — the real constant C23 brought in#

For a long time C had four things worth calling a “constant”, and all four fell short somewhere.

WhatHas a type?A constant expression?What is missing
100 (a literal)yesyesno name — the same value gets written everywhere
#define N 100no(judged after replacement)no scope, no type, no name in the debugger (chapter 61)
enum { N = 100 }int onlyyescannot hold long long, double or a string
const int n = 100;yesnofails where a real constant is needed — array sizes, case labels

Table 21.10 — Four kinds of ‘constant’ and what each lacks

constexpr is the word that came in to fill the blanks in that table — a value with a type, a name and a scope, which is also a constant expression.

examples-en/ch20/constexpr.c

/* C23's constexpr - how far a "real constant" goes. */
#include <stdio.h>

constexpr int table_size = 4;          /* file scope: static storage duration, internal linkage */
constexpr double half = 0.5;
constexpr long long big = 1LL << 40;

int table[table_size];                 /* an array size - and not a variable length array */
static_assert(sizeof table / sizeof table[0] == 4, "table_size is a constant");

constexpr int doubled = table_size * 2;  /* a constant built from a constant */

struct limits { int low, high; };
constexpr struct limits range = { 1, 9 };
static_assert(range.high == 9, "a member of a constexpr struct is a constant");

/* the preprocessor knows nothing of constexpr - the program prints that fact */
#if table_size == 4
static const char *preproc = "the preprocessor saw table_size == 4";
#else
static const char *preproc = "the preprocessor never saw it: the name became 0";
#endif

static const char *classify(int x)
{
    switch (x) {
    case table_size:  return "exactly the table size";   /* a case label */
    case doubled:     return "twice the table size";
    default:          return "something else";
    }
}

struct packed {
    unsigned flags : table_size;       /* the width of a bit-field */
};

int main(void)
{
    static int copy = table_size + 1;  /* static initialization */
    enum { same_again = table_size };  /* the value of an enumeration constant */

    printf("[constexpr is a constant expression]\n");
    printf("  array size    : %zu\n", sizeof table / sizeof table[0]);
    printf("  case label    : %s\n", classify(4));
    printf("  case label    : %s\n", classify(8));
    printf("  static init   : %d\n", copy);
    printf("  enum value    : %d\n", (int)same_again);
    printf("  bit-field     : %d bits\n", table_size);
    printf("  struct member : range.high = %d (checked with static_assert)\n",
           range.high);

    printf("\n[it has a type, unlike a macro]\n");
    printf("  half  = %g (double)\n", half);
    printf("  big   = %lld (long long)\n", big);
    printf("  const is implicit: %s\n",
           _Generic(&table_size, const int *: "yes, &table_size is const int *",
                                 int *: "no", default: "?"));

    printf("\n[a block-scope constexpr is an ordinary object with an address]\n");
    constexpr int local = 7;
    const int *p = &local;
    printf("  local = %d, read through a pointer = %d\n", local, *p);

    printf("\n[but the preprocessor runs before any of this]\n");
    printf("  %s\n", preproc);
    printf("  #if and #define live in a different world (chapter 57)\n");

    /* every line below is a compile error - the value must be exactly representable.
           constexpr unsigned int m = -1;      the value does not fit
           constexpr float f = 0.1;            0.1 as a double is not exact as a float
           constexpr int *q = &copy;           a pointer initializer must be null
           constexpr const char *s = "abc";    the same reason
           constexpr volatile int v = 1;       volatile, restrict and atomic are banned
           constexpr int no_init;              it must be a definition with an initializer */
    return 0;
}

Output

[constexpr is a constant expression]
  array size    : 4
  case label    : exactly the table size
  case label    : twice the table size
  static init   : 5
  enum value    : 4
  bit-field     : 4 bits
  struct member : range.high = 9 (checked with static_assert)

[it has a type, unlike a macro]
  half  = 0.5 (double)
  big   = 1099511627776 (long long)
  const is implicit: yes, &table_size is const int *

[a block-scope constexpr is an ordinary object with an address]
  local = 7, read through a pointer = 7

[but the preprocessor runs before any of this]
  the preprocessor never saw it: the name became 0
  #if and #define live in a different world (chapter 57)

21.9.1 What it is you are writing#

constexpr goes where a storage-class specifier goes (the seven words of that slot are gathered in chapter 45). Its meaning is one sentence of the standard — “an object declared with a storage-class specifier constexpr has its value permanently fixed at translation time; if not yet present, a const-qualification is implicitly added to the object’s type. The declared identifier is considered a constant expression.”1

So it works in every place the demonstration showed.

Where it may be usedWith const int instead
an array size, int table[table_size];becomes a variable length array, or an error
case table_size:error
static int copy = table_size + 1;error
enum { same = table_size };error
static_assert(...)error
the width of a bit-field, unsigned f : table_size;error
the initializer of another constexprerror

Table 21.11 — Where constexpr may be used

Three things to remember about the grammar. 1. It must be a definition, with an initializer. constexpr int n; is an error (“constexpr requires an initialized data declaration”). 2. The initializer must be a constant expression. 3. The only storage-class specifiers it may be combined with are auto, register and static, so extern constexpr and thread_local constexpr are errors.

21.9.2 How it differs from const#

A common misconception. “constexpr is a stronger const”

They look alike, but the two words answer different questions.

  • const is a promise: “I will not change it through this name.” It says nothing about when the value is decided — it may be decided at run time.
  • constexpr is a fact: “this value is decided at translation time.” As a consequence const follows (the standard adds it implicitly).

The direction is reversed, in other words. So const int n = f(); is fine while constexpr int n = f(); is not — a function call is not a constant expression. The demonstration’s _Generic makes that implicit const visible: the type of &table_size is const int *.

The working test is one line — if the value will be used as an array size or a case label, constexpr; if it only means “I will not change it”, const.

21.9.3 The value has to fit exactly#

constexpr carries one constraint that no other declaration does: the value of the initializer shall be exactly representable in the target type, with no change of value.2 A value that an ordinary initialization would have quietly truncated is caught here.

Writtengcc 14clang 22
constexpr unsigned int m = -1;error — “constexpr initializer not representable in type of object”error — “not exactly representable”
constexpr unsigned int m = -1U;OKOK
constexpr float f = 0.1;error — 0.1 as a double is not on the float griderror
constexpr float f = (float)0.1;OK — say what you mean with a castOK
constexpr signed short s = 70000;warning (-Woverflow)error
constexpr unsigned char c = 300;warning (-Woverflow)error

Table 21.12 — What the two compilers do when the value does not fit exactly

The last two rows are what the measurement bought. A constraint violation requires a diagnostic but does not dictate its shape, so gcc’s warning does not break the rules. Still, unless warnings are raised to errors (-Werror), a quietly truncated value becomes a constant — which is where the habit of building with -Wall -Wextra pays (chapter 16).

Pointers carry a stronger constraint — they must be null.

constexpr int *p = nullptr;              /* OK */
constexpr const char *s = "abc";         /* error: "constexpr pointer initializer is not null" */
constexpr char s2[] = "abc";             /* OK — take it as an array */

Trying to name a string constant with constexpr is the wall everybody meets on day one. The answer is to declare an array instead of a pointer.

21.9.4 What happens at file scope#

constexpr is often put where a macro used to be, so whether it may go in a header becomes a practical question straight away. The standard’s answer: a constexpr object at file scope has static storage duration, and its identifier has internal linkage (like static). Every translation unit that sees the same textual definition gets its own object, so a header definition does not collide (chapter 56).

Inside a block it is an ordinary local object — automatic storage duration, and its address can be taken. The demonstration’s &local confirms it. It is a “constant” and yet an object, which is where it parts from a macro.

Q. So should every #define constant become a constexpr?

A. For naming a value, yes. It gains a type and a scope, it shows up in the debugger under its name, and the compiler catches mistakes.

But one job stays with the macro — the preprocessor’s own conditionals. The preprocessor runs before the compiler and knows nothing of a constexpr name, so inside #if that name becomes 0. The demonstration prints the fact: #if table_size == 4 is judged not true but false (0 == 4). No error, no warning — it just quietly goes that way.

So the rule splits like this — values you branch on with #if stay #define; constants used as values in code become constexpr. Why the preprocessor’s world is separate at all is chapter 61′s subject.

Q. Is it the same as C++‘s constexpr?

A. The name is the same, the reach is not. C++‘s constexpr also applies to functions, running them at compile time, but C23 has no constexpr functions — it applies to object definitions only. constexpr int f(void) { ... } really is an error (“constexpr can only be used in variable declarations”, clang).

There is a reason C brought it in that narrowly. Compile-time execution is a feature that doubles the size of a language, which would have to be traded against C’s character as a small, predictable language. The place where a value is needed right now is filled by this section’s constexpr, and nothing beyond it was added.

21.10 Expressions — the things that are evaluated into values#

Weave literals together with operators and you have an expression, as in 2 + 3 * 4. The definition takes one sentence — that which is calculated (evaluated) into a value. This property of “becoming a value” is the whole of an expression, and half of the eye for reading C from here on. Wherever a value is needed in code, an expression may go there — a single literal is the simplest expression of all.

This chapter has only four operators — plus +, minus -, times *, and parentheses ( ). Following this book’s spiral principle, the remaining operators join in the chapters where they become necessary (comparison in chapter 31, division in chapter 29 — why division was put off, in a moment).

Here is the demonstration. The %d in the example below is a mark meaning “print an integer value here in decimal”; its formal explanation is in chapter 23 — for now it is only a window for seeing results.

examples-en/ch20/expr.c

#include <stdio.h>

int main(void)
{
    printf("%d\n", 2 + 3 * 4);    /* the multiplication is computed first */
    printf("%d\n", (2 + 3) * 4);  /* the parentheses state the order */
    return 0;
}

Output

14
20

21.11 Order — precedence, and the practice of parentheses#

The first line of the demonstration is 14 because the multiplication was calculated before the addition. Exactly the convention of the mathematics lesson, and C has fixed such a ranking of “who goes first” — precedence — for every operator.

Here we state this book’s recommendation in advance. Do not memorise the precedence table; use parentheses. C has dozens of operators and the ranking table runs to more than fifteen rows — even people who have memorised it all get confused and cause accidents. Make the order explicit with parentheses, as in the second line, and there is nothing to memorise and nothing to misread. The full table is in the appendix as reference material; the text goes with “arithmetic by the mathematical convention, parentheses everywhere else.”

Q. Why was division put off? Leaving one of the four operations out feels odd.

A. Because integer division differs from division in mathematics. In C 7 / 2 is not 3.5 but 3 — division between integers is the quotient with the fractional part discarded, and it only makes sense understood as a set with its partner operator % (remainder). Including the exact rule of that discarding (which way it goes for negative numbers), it is a subject worth treating properly on top of chapter 6′s world of integers, so it has been given a place in chapter 29. Better to treat it squarely, all at once, than to introduce it half-heartedly now and create the misconception “I thought 7/2 was 3.5.”

Q. Is printf("hello") a value too? A function call is written in the position of an expression.

A. Good eye — it is. Calling a function is itself an expression, and therefore becomes a value. What that value is and where it goes is exactly the next chapter’s subject. That one question has already half-opened the next chapter’s door.

21.12 One caution planted early — the order in time may differ#

About precedence there is one caution to plant as a seed. Precedence is a rule of binding — who is whose material — not of what the machine calculates first in time. Which of the left and right materials of an addition is calculated first, for instance, is not fixed by the standard and may differ between compilers. In an expression like 2 + 3 it does not matter at all, but when calculating a material leaves a trace such as output, the order of those traces may differ.

Be reassured on one point, though — the order between statements is guaranteed absolutely. Chapter 20′s “one statement at a time, top to bottom” is a contract. What can waver is only the inside of a single statement, and the exact rules there (side effects, the notion of sequence points) are faced head on in chapter 34, once more material is in place. For now one practical rule suffices: do not cram order-sensitive work into one statement; split the statements.

To summarise — a notation writing a value in source is a literal, that which is evaluated into a value is an expression, and the order of binding is governed by arithmetic convention and parentheses, while the order in time inside one statement may differ. In the next chapter we learn the most important device that consumes and produces values — how to call a function. Hello world’s heart, printf(...), is finally treated head on.

Notes

  1. ISO/IEC 9899 (C23) 6.7.2, paragraph 16. Draft N3220. ↩
  2. ISO/IEC 9899 (C23) 6.7.2, paragraph 5. The standard’s own example is the pair constexpr unsigned int minusOne = -1; (a constraint violation) and constexpr unsigned int uint_max = -1U; (valid). ↩