Skip to content
VLSI Mentor

C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data

C Strings and Null Termination

Nothing in the three preceding chapters depended on what an array held. The rules about contiguity, about the length living in the type, about decay — all of them are the same for ints, doubles and anything else. One element type is different, and not because the language treats it differently. When a char array is used to hold text, a convention attaches to it that the type system knows nothing about: the text is understood to stop at the first zero byte, wherever that happens to be. That single convention is what makes C strings work at all, since it means a function can be handed nothing but an address and still find the end. It is also the reason this chapter exists, because the convention introduces a second length. The array has a size, fixed in its type and known at compile time; the text has a length, discovered at run time by looking for the zero. Those two numbers are usually related and nothing enforces any relationship between them, so the interesting question is not what a C string is but what happens when they disagree — and the answer is a short catalogue of ways the terminating byte fails to be where everybody assumed it was.

Foundation16 min readC++Part IFundamentalsStrings

Part I · Module 9 · Chapter 9.4 · C Strings and Null Termination

1. A Convention, Not a Type

There is no C string type. There is a char array and an agreement about what is in it:

A C string is a sequence of char ending in a zero byte. The zero byte is part of the data, and it is how every function that reads the string knows where to stop.

Nothing in the type system records that agreement. char buf[8] is an array of eight chars whether or not any of them is zero; it is only the functions you pass it to that interpret it as text.

That is worth stating before anything else, because every problem in this chapter is a consequence of it. The compiler checks types, and "has a zero byte somewhere in it" is not a type.

2. Two Lengths

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const char greeting[] = "idle";

That declaration creates an array of five chars — the four you wrote plus a zero the compiler appends. So there are two numbers:

QuestionAnswerKnown
How big is the array?sizeof(greeting)at compile time, from the type
How long is the text?std::strlen(greeting)at run time, by scanning for the zero

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
cstr.cpp — complete program
#include <cstring>
#include <iostream>

int main()
{
    const char greeting[] = "idle";          // 4 characters + a terminator

    std::cout << "sizeof(greeting) = " << sizeof(greeting) << "   (includes the '\\0')\n";
    std::cout << "strlen(greeting) = " << std::strlen(greeting) << "   (does not)\n\n";

    for (std::size_t i = 0; i < sizeof(greeting); ++i)
        std::cout << "  [" << i << "] = " << static_cast<int>(greeting[i])
                  << (greeting[i] == '\0' ? "   <- the terminator\n" : "\n");
    return 0;
}

Measured output, zero warnings:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(greeting) = 5   (includes the '\0')
strlen(greeting) = 4   (does not)

  [0] = 105
  [1] = 100
  [2] = 108
  [3] = 101
  [4] = 0   <- the terminator

Five bytes, four characters. The off-by-one between sizeof and strlen is not a quirk to memorise — it is the terminator, which is in the array and not in the text.

And the bytes are numbers. 105, 100, 108, 101 are 'i', 'd', 'l', 'e'. Chapter 3.6 established that a char holds a small integer; printing them as int is that fact made visible. The terminator is the integer 0, written '\0'.

3. What strlen Actually Does

std::strlen is not reading a stored length. There is nowhere to store one — Chapter 9.1 §3 measured that an array has no header.

It scans: start at the address, look at each byte, stop when you find a zero, return how many you passed. Which means:

It is O(n), not O(1). Calling strlen in a loop condition walks the string on every iteration — a classic performance mistake in C code, and one reason Chapter 9.5's std::string stores its size.

It requires the terminator to exist. If there is no zero in the array, strlen keeps scanning past the end. §7 measures what that does.

And it can be given a pointer. strlen takes a const char*, so Chapter 9.2's decay is exactly how it is normally called — the array arrives as an address with no length, and the terminator is the only thing that makes the function possible at all.

The convention exists so that a bare address is enough. That is its whole justification, and §5–§7 are its whole cost.

4. Writing One

Three forms, with different amounts of room for error:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const char a[] = "idle";        // size deduced: 5. Always correct.
    const char b[5] = "idle";       // explicit and correct: 4 + terminator
    const char c[4] = "idle";       // ERROR -- no room for the terminator

The third is rejected, which is the language helping:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: initializer-string for char array is too long, array size is 4 but initializer has size 5 (including the null terminating character)

The message spells out the arithmetic, including the parenthetical that is the whole point of §2.

const char a[] = "idle" is the form to write. Letting the compiler count means the size is right by construction, and Chapter 9.1 §2's advice about deduced sizes applies with extra force here because the count includes a byte you did not type.

A string literal is itself an array. "idle" has type const char[5] — measured with decltype, and sizeof("idle") is 5. And Chapter 7.3 §8 established that its characters have static storage duration — which is why returning one from a function is safe where returning a local array is not.

5. Where the Terminator Goes Missing: the Exact Fit

Now the catalogue. The first case is an array filled exactly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const char exact[4] = {'i', 'd', 'l', 'e'};     // NO room for '\0'

This is legal and it is not a string. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exact[4] from 4 chars: sizeof = 4   last byte = 101   (not 0)

The last byte is 101, which is 'e'. There is no zero anywhere in the array.

Zero warnings at the standing flags. The compiler has no basis to object: an array of four chars initialized with four chars is exactly what was asked for. Only the intent was a string, and intent is not in the type.

This is the form that catches people out because it looks careful. Writing the characters individually feels more explicit than a string literal, and it silently loses the one byte that mattered.

6. Where It Goes Missing: strncpy

The second case is the one with a reputation, and it deserves it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    char dst[4];
    std::strncpy(dst, "idle", sizeof dst);     // copies 4 chars, no room for '\0'

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after strncpy(dst,"idle",4): last byte = 101   terminated? NO

strncpy does not guarantee a terminator. Its contract is "copy at most n characters", and if the source is n or more characters long it copies n of them and stops — leaving no room for the zero. The function was designed for fixed-width fields, not for strings, and its name has misled people for forty years.

Zero warnings at the standing flags.

The two safe forms, both measured:

Azvya Education Pvt. Ltd.VLSI Mentor
noterm.cpp — the safe half of the program
#include <cstdio>
#include <cstring>
#include <iostream>

int main()
{
    const char ok1[] = "idle";                       // size deduced: 5
    char ok2[5];
    std::snprintf(ok2, sizeof ok2, "%s", "idle");     // always terminates
    std::cout << "ok1 sizeof = " << sizeof(ok1) << "   terminated? " << (ok1[4] == '\0' ? "yes" : "no") << '\n';
    std::cout << "ok2 via snprintf: \"" << ok2 << "\"   terminated? " << (ok2[4] == '\0' ? "yes" : "no") << '\n';
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok1 sizeof = 5   terminated? yes
ok2 via snprintf: "idle"   terminated? yes

snprintf always writes a terminator when given a non-zero buffer size, truncating the text if necessary. Measured with a buffer two characters too small:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
"id"  terminated? yes

It wrote what fitted and terminated it. That makes it the right tool when you must build text into a fixed buffer — you lose characters rather than losing the terminator — and it is why it is the usual recommendation in C code that cannot use std::string.

7. What Reading an Unterminated Array Does

The third case is the consequence of the first two, and it is the one Chapter 9.3 promised:

Azvya Education Pvt. Ltd.VLSI Mentor
printnoterm.cpp — complete program. Undefined behaviour.
#include <iostream>
int main()
{
    const char bad[4] = {'i', 'd', 'l', 'e'};   // no terminator
    std::cout << bad << '\n';                    // reads until it finds a zero -- UB
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic printnoterm.cpp -o printnoterm
$                                            ← zero warnings

Under AddressSanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
==56461==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016d24e804
SUMMARY: AddressSanitizer: stack-buffer-overflow constexpr_c_functions.h:63 in std::__1::__constexpr_strlen<char>(char const*)

Read where the sanitizer puts the blame. Not in main, and not on the line with std::cout. Inside the standard library's own strlen.

That is the mechanism stated as precisely as it can be: printing a const char* makes the stream call strlen to find out how much to write; strlen scans for a zero; there is no zero in the array; so it keeps reading past the end. The program's defect is in main and the overflow happens in library code that was behaving correctly given what it was handed.

Three things follow.

The compiler cannot see this. The array is fine, the print is fine, and the relationship between them is a convention. Chapter 9.1 §6's split applies — there is no constant for it to check.

The symptom is remote from the cause, which is Chapter 8.8 §7's property in a new setting. A stack trace points at the library, and a reader who has not met this will start by suspecting the library.

It usually appears to work. Whatever follows the array in memory may well contain a zero byte soon, in which case the program prints idle plus some garbage, or prints idle exactly, and nobody notices.

8. What to Actually Do

Use std::string for text you own and std::string_view for text you only read — Chapters 9.5 and 9.6. This chapter is for reading, not for writing new code.

Write const char a[] = "..." and let the compiler count, when you do need an array.

Remember sizeof includes the terminator and strlen does not — the two numbers differ by exactly one for a correct string.

Never use strncpy expecting a terminator. Use snprintf, which always writes one.

Never fill a char array exactly with the characters you want as text.

Do not call strlen in a loop condition. It rescans every time.

Expect no diagnostics for a missing terminator, and run a sanitizer — which reports it from inside the library.

Treat a char* from a C interface as read-only unless its documentation says otherwise, and convert to std::string at the boundary.

One topic this chapter has deliberately not raised: a char is a byte, and text in the real world is not one byte per character. Encodings, UTF-8 and the wide character types are a subject of their own, out of scope for Part I, and everything above is about bytes rather than about characters in any human sense.

9. Mistakes Worth Making Once

Filling a char array exactly. Measured: const char exact[4] = {'i','d','l','e'} has no zero and zero warnings.

Trusting strncpy to terminate. Measured: it did not, and nothing warned.

Confusing sizeof with strlen. Measured: 5 and 4 for the same array.

Calling strlen in a loop condition. It is O(n) per call — there is no stored length.

Expecting a warning for an unterminated string. Measured: zero, because the convention is not a type.

Reading a stack trace that points into the library and blaming the library. Measured: ASan reported the overflow inside __constexpr_strlen; the defect was in main.

Assuming it works because it printed correctly. Whatever follows in memory may contain a zero.

Writing const char c[4] = "idle". error: initializer-string for char array is too long.

10. Interview Relevance

"What is a C string?" A char array ending in a zero byte — and the sharper half is that there is no C string type, only a convention that functions honour. Adding that this is why a bare const char* is enough to pass a string shows you know what the convention is for.

"What is the difference between sizeof and strlen?" Compile-time array size versus run-time scan to the terminator; 5 and 4 for "idle". If you add that strlen is O(n) because there is nowhere to store a length, you have given the reason rather than the fact.

"What is wrong with strncpy?" It does not guarantee a terminator when the source fills the buffer. The expected follow-up is what to use instead — snprintf, or better, std::string.

"Why is a missing terminator hard to debug?" The compiler cannot see it, the overflow happens inside whichever library function scans for the zero, and it frequently appears to work because some later byte is zero.

11. What You Should Be Able to Say Now

  1. There is no C string type — only a char array and the convention that the text ends at the first zero byte.
  2. The convention is not in the type system, which is why nothing in this chapter can be checked by the compiler.
  3. Two lengths: sizeof from the type at compile time, strlen by scanning at run time.
  4. Measured: sizeof("idle") array is 5, strlen is 4 — they differ by exactly the terminator.
  5. Measured: the bytes are 105, 100, 108, 101, 0 — Chapter 3.6's "a char is a small integer", visible.
  6. strlen scans, so it is O(n) and it requires the zero to exist.
  7. The convention exists so that a bare address is enough — that is its entire justification.
  8. "idle" is itself an array of type const char[5], with static storage duration (Chapter 7.3 §8).
  9. Write const char a[] = "..." and let the compiler count; const char c[4] = "idle" is an error.
  10. Measured — the exact fit: four characters in a char[4] leaves no zero, with zero warnings.
  11. Measured — strncpy: copying 4 characters into a char[4] left it unterminated, with zero warnings.
  12. snprintf always writes a terminator — measured, and it is the right tool for a fixed buffer.
  13. Measured — reading an unterminated array: zero warnings, and ASan reports stack-buffer-overflow inside the library's strlen.
  14. So the symptom is remote from the cause (Chapter 8.8 §7's property), and it frequently appears to work.

12. Exercises

Exercise 1 — the two numbers. For const char s[] = "reset"; give sizeof(s), strlen(s), the index of the terminator, and the value of the byte at that index.

Exercise 2 — is it a string? For each, say whether it is a valid C string and why: (a) const char a[] = "ok"; (b) const char b[2] = {'o','k'}; (c) const char c[3] = {'o','k','\0'}; (d) const char d[3] = "ok"; (e) const char e[2] = "ok";.

Exercise 3 — predict the diagnostic. Say what the compiler reports for each, and what a sanitizer would report at run time: (a) const char c[4] = "idle"; (b) const char c[4] = {'i','d','l','e'}; std::cout << c; (c) char d[4]; std::strncpy(d, "idle", 4); std::cout << d;.

Exercise 4 — explain the trace. A stack trace shows a buffer overflow inside std::__constexpr_strlen. Say where the defect actually is, how it got there, and why the library is not at fault.

Exercise 5 — fix the copy. char name[8]; std::strncpy(name, source, sizeof name); may leave name unterminated. Give two repairs, and say which you would ship.

Exercise 6 — why O(n). Explain why strlen cannot be O(1), referring to what Chapter 9.1 §3 measured about an array's storage.

Exercise 7 — read the interface. int open(const char* path, int flags); takes a C string. Say what the function must do to find the end of path, what happens if you pass an unterminated array, and how you would call it given a std::string.

13. Summary

  • No C string type — a char array plus the convention that text ends at the first zero byte.
  • The convention is not a type, so none of this chapter's failures can be compile-checked.
  • Two lengths: sizeof at compile time, strlen by scanning. Measured: 5 and 4.
  • Measured: the bytes are 105, 100, 108, 101, 0.
  • strlen is O(n) and needs the zero; the convention exists so a bare address suffices.
  • "idle" is a const char[5] with static storage duration.
  • Measured — the exact fit leaves no terminator, zero warnings.
  • Measured — strncpy left the buffer unterminated, zero warnings; snprintf always terminates.
  • Measured — printing it draws no warning and ASan blames the library's strlen.
  • Write std::string; read C strings because every OS and vendor interface is one.

Both halves of the problem this chapter described — the length that has to be rediscovered by scanning, and the terminator that can be absent — come from the same source: the text and its length are not stored together. The next chapter is the type that stores them together. It knows its own size without scanning, it cannot lose its terminator because it maintains one for you, it grows when you append to it, and it owns its characters so that copying it copies the text. That last property is the one Chapter 6.5 promised and could not demonstrate, because until now there has been no type in this curriculum whose copy costs anything measurable.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.