What Are C-Style Strings?

A C-style string is a C-style array whose element type is char or const char, holding text terminated by a null terminator, the character '\0'.

That terminator is doing something important. A std::string knows its own length; a C-style string is just a run of characters in memory with no length attached, so the only way any function can tell where the text stops is to walk forward until it meets '\0'. Every peculiarity below follows from that one design decision.

You will meet these constantly in older code and in C libraries, which is why they are worth understanding even though modern code should reach for std::string or std::string_view.

Defining One

Initialize a char array from a string literal and the compiler sizes the array and appends the terminator for you:

#include <cstring>
#include <iostream>
#include <iterator>

int main()
{
    const char callsign[]{ "Kilo Delta" };

    std::cout << callsign << '\n';
    std::cout << "std::size:   " << std::size(callsign) << '\n';
    std::cout << "std::strlen: " << std::strlen(callsign) << '\n';

    return 0;
}

Output:

Kilo Delta
std::size:   11
std::strlen: 10

The two length functions disagree, and the difference is the whole lesson in miniature:

  • std::size() reports the array's length, 11, because the array holds ten visible characters plus the terminator.
  • std::strlen() reports the string's length, 10, by walking from the start and counting until it reaches '\0'.

std::size() is a compile-time property of the type and costs nothing. std::strlen() has to inspect memory at run time, so it is proportional to the length of the text.

They Decay

Being C-style arrays, C-style strings decay to a pointer in most expressions, which is why every function that takes one takes a const char* and receives no length information whatsoever. It relies entirely on the terminator being there.

#include <cstring>
#include <iostream>

void announce(const char* badge)
{
    std::cout << "Now boarding: " << badge << '\n';
}

int main()
{
    char badge[16]{ "Kilo Delta" };

    announce(badge);

    std::strcpy(badge, "Mike Echo");
    announce(badge);

    return 0;
}

Output:

Now boarding: Kilo Delta
Now boarding: Mike Echo

Printing Depends Entirely on the Terminator

When operator<< is given a char*, it does not print a pointer value. It assumes it has been handed a C-style string and prints characters until it finds '\0'.

That assumption is load-bearing. Overwrite the terminator and there is no longer anything telling the stream to stop, so it keeps reading past the end of the array through whatever memory follows. That is undefined behavior: it may print garbage, it may print for a long time, it may crash. Nothing warns you, because the array itself looks fine.

Reading Input Safely

The danger with input is buffer overflow: writing more characters into the array than it can hold, which corrupts whatever sits after it in memory.

Extracting straight into a char array with >> gives no protection, because the array cannot tell the stream how big it is. std::cin.getline() can, if you tell it:

char badge[32]{};

std::cin.getline(badge, std::size(badge));

The second argument caps how much will be written, including the terminator, so passing std::size(badge) ties the limit to the actual array. Read the number from the array rather than typing 32 again, and the two can never drift apart.

Modifying Them

You can change the contents, but you cannot assign a new value with =:

#include <iostream>

int main()
{
    char callsign[]{ "Kilo Delta" };

    callsign = "Mike Echo";

    std::cout << callsign << '\n';

    return 0;
}

This program is shown to demonstrate the error, and does not compile:

s.cpp: In function 'int main()':
s.cpp:7:14: error: incompatible types in assignment of 'const char [10]' to 'char [11]'
    7 |     callsign = "Mike Echo";
      |     ~~~~~~~~~^~~~~~~~~~~~~

An array is not a reseatable handle to characters; it is the characters. Assignment would have to mean copying, and C++ does not do that for arrays. To change the text you copy into it, as the std::strcpy call in the earlier example does, and it is your job to be certain the destination is large enough.

The Rest of the C String Library

<cstring> provides the traditional operations: std::strlen for length, std::strcpy to copy, std::strcat to append, and std::strcmp to compare. Every one of them relies on correct terminators and none of them knows how big your destination is, which is why this family has such a long history of buffer overflows.

Prefer std::string

Best Practice
Avoid non-const C-style string objects. Use std::string when you need to own or modify text, and std::string_view when you only need to read it.

std::string tracks its own length, resizes itself, assigns with =, and cannot overflow a fixed buffer because there is no fixed buffer. The one place C-style strings remain natural is string literals and interfaces to C libraries, and both of those are const.

Summary

What they are: C-style arrays of char or const char, ending in a null terminator '\0', which is the only marker of where the text stops.

Two different lengths: std::size() gives the array length including the terminator and costs nothing; std::strlen() counts characters up to the terminator and scans at run time.

They decay: functions receive a const char* with no length, so correctness depends entirely on the terminator being present.

Printing: operator<< prints characters until it finds '\0'. Without one, it reads past the end of the array, which is undefined behavior.

Input: use std::cin.getline(buffer, std::size(buffer)) so the limit comes from the array itself, and avoid buffer overflow from writing more than the array holds.

Assignment: = does not work on a C-style string after initialization. Copy into it with std::strcpy and make sure the destination is big enough.

Library: <cstring> offers std::strlen, std::strcpy, std::strcat, and std::strcmp, none of which know your buffer size.

In modern code: prefer std::string for owned or modified text and std::string_view for read-only access, leaving C-style strings to literals and C interfaces.