CodeRigor

FREE LESSON

C strings, terminators & bounded adapters

Keep byte length, buffer capacity and NUL termination distinct; use memchr to inspect bounded storage and copy text without silent truncation.

A C string is a byte sequence terminated by NUL. A char pointer alone carries no length or capacity. A string_view carries length but need not have a terminator at its end. When these representations meet, the adapter must define whether binary bytes and truncation are acceptable.

Text length is not buffer capacity
The first NUL ends the C string; the remaining bytes are unused capacity, not more text.
Text length is not buffer capacity
The first NUL ends the C string; the remaining bytes are unused capacity, not more text.

Keep three quantities separate

For the text "cat", the text length is three, its complete C-string representation occupies four bytes, and the destination may have a capacity larger than either. strlen requires a reachable terminator and reports the text length; it does not validate available storage. A bounded scan can use memchr on the supplied nonempty range and return no result when that range contains no NUL.

The supplied span is the readable boundary even if the backing allocation is larger. A terminator just after the span must not count as a successful bounded scan. Never call strlen first to decide whether that scan would have been safe.

Follow one complete example

Run this program, then change one boundary while keeping the API preconditions valid. Predict its output before pressing Run.

C++
#include <cstdio>
#include <cstring>
#include <string_view>
int main() {
    char storage[8]={'c','a','t','!','!','!','!','!'};
    std::string_view view(storage,3);
    char out[4]{};
    std::memcpy(out,view.data(),view.size());
    out[view.size()]='\0';
    std::printf("%s %zu\n",out,std::strlen(out));
}
Reject before mutation
A complete text conversion reserves its terminator before copying.
Reject before mutation
A complete text conversion reserves its terminator before copying.

Define the conversion policy before copying

This lesson rejects embedded NULs instead of silently converting "a\0b" to the C-visible text "a". It also rejects insufficient capacity instead of truncating. These are wrapper policies, not hidden guarantees from strcpy or strncpy. Check them both before changing any destination byte.

strncpy is not a universal safe string copy: when no NUL appears in its first n source bytes, it writes no terminator. When the source is shorter, it pads the destination with additional zero bytes. strcpy and strcat require enough storage and do not make overlapping ranges valid. strnlen and strlcpy have platform/standard-version distinctions; this portable baseline uses memchr and explicit lengths instead.

Choose the representation
A length and a terminator answer different questions.
Choose the representation
A length and a terminator answer different questions.

Handle borrowed storage deliberately

Our adapter permits overlap. Copy the payload with memmove, then write NUL at the source length. Writing the terminator first can corrupt a source byte when views overlap. Preserve every byte beyond the new terminator, and preserve the entire destination on a rejected request.

Prefer std::string for owned C++ text and std::string_view for bounded borrowing. Use this adapter at an actual C interface boundary. A returned pointer into a string still depends on that string lifetime and on whether later operations invalidate its storage. NUL termination does not grant ownership or extend lifetime.

Read the return value

Predict this smaller case. The check item later changes the values, so explain the rule as well as the output.

C++
#include <cstdio>
#include <cstring>
int main(){char a[]={'a','\0','b','\0'}; auto p=static_cast<const char*>(std::memchr(a,'b',sizeof a)); std::printf("%zu %td\n",std::strlen(a),p-a);}

Complete the faded example

State the missing precondition or result check before filling the code. The fill exercise supplies the rest of the implementation.

C++
// Reject an embedded NUL before changing destination.
// Require one byte beyond the payload.
if (!source.empty()) std::memmove(destination.data(), source.data(), source.size());
// Fill: establish the terminator invariant.

Check your model

An empty source and a zero-capacity output are different from an empty source and one writable byte. Which one can represent a C string?

Practice the boundary

Implement two borrowed-buffer adapters using C string/memory functions.

c_text_length(std::span<const char>) -> std::optional<std::size_t> returns the first NUL byte's index, or nullopt if there is no terminator inside the supplied span. It must never scan past that bound. An empty span has no terminator.

copy_c_text(std::span<char> destination, std::string_view source) -> bool copies the entire source and appends one NUL. Reject embedded NULs and insufficient room (including room for the terminator), leaving the destination unchanged on failure. Empty text requires one byte. Overlap is supported, including a source slice inside the destination. Bytes after the terminator remain unchanged. No allocation is required.

Read → six checks → two bug hunts → fill → build. A successful graded submission is evidence; reading or watching alone does not grant mastery. Review cards reinforce API choices separately.

Watch and verify

The Watch & read panel links a free lecture for the relevant background and primary API references. The lecture notes say which parts are background; the tested contract here supplies the exact wrapper policy.

C standard-library course · C library review cards · Unix/Linux systems

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →