The difference between C-strings and [[std::string]] in C++ is ownership and safety. A C-string is a plain array of [[char]] ended by a null character, with no idea of its own length. [[std::string]] owns its characters, knows how long it is, and grows when you add to it. The C-string form is how C did it; [[std::string]] is how C++ does it.
What a C-string actually is
A C-string is a null-terminated array of char. The terminator is a zero byte, and every function that works with C-strings relies on it to know where the text ends.
#include
int main() {
const char title[] = "Bread"; // six chars: five letters plus the terminator
std::cout << "title: " << title << '\n';
std::cout << "bytes: " << sizeof(title) << '\n';
for (int i = 0; title[i] != '\0'; ++i) {
std::cout << "title[" << i << "] = '" << title[i] << "'\n";
}
return 0;
}The output shows the terminator doing its job:
title: Bread
bytes: 6
title[0] = 'B'
title[1] = 'r'
title[2] = 'e'
title[3] = 'a'
title[4] = 'd'Note that sizeof(title) is 6 even though "Bread" has five letters. The array includes the terminating zero.
Why C-strings are dangerous
The danger is that nothing enforces the terminator. If a function writes past the end of the buffer, or reads a buffer that was not terminated, the result is .undefined behaviour
#include
#include
int main() {
char buffer[8];
std::strcpy(buffer, "Short"); // fits: 5 chars plus the terminator
std::cout << "buffer: " << buffer << '\n';
std::cout << "length: " << std::strlen(buffer) << '\n';
// buffer now holds "Short" and three spare bytes.
// Writing a longer string here would run off the end.
return 0;
}The output is what you would expect:
buffer: Short
length: 5But the same code with a longer string writes past the end of the array, and there is no compiler check for it. This is the single biggest reason std::string replaced C-strings in new C++ code.
std::string owns its data
std::string stores a length, a pointer and its characters, and it manages the memory for you.
#include
#include
int main() {
std::string title = "Bread";
title += " and Butter";
title += '!';
std::cout << "title: " << title << '\n';
std::cout << "length: " << title.length() << '\n';
std::cout << "empty: " << std::boolalpha << title.empty() << '\n';
return 0;
}The output shows the string growing without any buffer to size by hand:
title: Bread and Butter!
length: 17
empty: falseThere is no terminator to think about, no capacity to manage, and the object copies itself correctly when you assign it.
Converting between the two forms
Going from a C-string to a std::string is automatic: the constructor takes a const char*. Going the other way uses .c_str(), which returns a pointer to the internal null-terminated buffer.
#include
#include
#include
int main() {
std::string shelf = "Fiction";
const char* raw = shelf.c_str();
std::cout << "raw: " << raw << '\n';
std::cout << "length: " << std::strlen(raw) << '\n';
std::string copy(raw);
std::cout << "copy: " << copy << '\n';
return 0;
}The output shows the round trip:
raw: Fiction
length: 7
copy: FictionOne warning about .c_str(): the pointer it returns is valid only as long as the string is not modified or destroyed. If you keep a pointer to a string and then change the string, the pointer may dangle.
Comparing C-strings correctly
Comparing two C-strings is done with std::strcmp, not with ==. The == operator on two const char* compares *addresses*, not the text they point at.
#include
#include
int main() {
char first[] = "shelf";
char second[] = "shelf";
std::cout << "strcmp: " << std::strcmp(first, second) << '\n';
std::cout << "content eq: " << (std::strcmp(first, second) == 0) << '\n';
std::cout << "pointer eq: " << (&first[0] == &second[0]) << '\n';
return 0;
}The output shows the difference clearly:
strcmp: 0
content eq: 1
pointer eq: 0The two arrays hold identical text, so std::strcmp says they are equal, but they are different objects at different addresses, so the pointer comparison is false. The same applies to a C-string and a std::string: comparing them with == does not compare their contents.
When a raw char array is still right
A C-string is not always wrong. Use a raw char array when you are talking to a C API that expects one, when you are handling a fixed block of bytes such as a binary format, or when the data genuinely is an array of characters rather than a piece of text — a byte buffer, for example.
#include
#include
void describe_buffer(const char* data, std::size_t length) {
std::cout << "length: " << length << '\n';
std::cout << "text: ";
for (std::size_t i = 0; i < length; ++i) {
std::cout << data[i];
}
std::cout << '\n';
}
int main() {
char raw[16] = {0};
std::strncpy(raw, "Vintage", sizeof(raw) - 1);
describe_buffer(raw, std::strlen(raw));
return 0;
}This is the shape you need for any C library function that takes a buffer and a length. The output confirms both worked:
length: 7
text: VintageQuick reference
- A C-string is a
chararray ending in'\0';std::stringowns its characters. sizeofon a C-string includes the terminator, so "Bread" is six bytes.std::strlenwalks the string looking for the terminator, so an unterminated buffer reads out of bounds.std::stringknows its length, grows on demand and copies safely.- Constructing a
std::stringfrom aconst char*is automatic;.c_str()goes the other way. - A C-string needs a fixed buffer; a
std::stringgrows on demand.
One more habit: when you find yourself writing [[strlen]] on a [[std::string]], you have probably chosen the wrong type. The standard string knows its own length, and using it correctly means the length can never be out of step with the characters it describes.
Try it yourself: write a program that reads a line of text into a [[std::string]] and then copies it into a fixed [[char[32]]] buffer using [[std::strncpy]], printing the length of each. Then try a title longer than 32 characters and see how the two approaches differ. Finally, replace the raw buffer with a [[std::string]] and confirm the program no longer needs to think about buffer size at all.