C++ Module 5 – Arrays, vectors and aggregate types
C-style strings and why std::string replaced them
How null-terminated C strings work, why strcpy overflows and == misleads, how to use strlen, strcmp and snprintf safely, and how to convert to std::string.
What you will learn
- Explain how null-terminated strings work
- Use strlen, strcmp and snprintf correctly
- Convert safely between C strings and std::string
Before you start
What you’ll learn · Before you start
What you will learn
- Explain how null-terminated strings work
- Use strlen, strcmp and snprintf correctly
- Convert safely between C strings and std::string
Before you start
On this page
A C-style string is an array of char whose end is marked by the null character '\0', a byte with the value
0. Nothing else records its length: std::strlen finds it by walking through the characters until it meets that
byte. The functions that copy and join such strings, std::strcpy and std::strcat, never check whether the
destination is big enough, and == compares where two strings are in memory, not what they say. Decades of C programs
have crashed, or been broken into, through exactly these gaps.
std::string replaced them because it closes every gap: it stores its length, owns and grows its memory, compares
text with ==, and joins strings with +. C strings have not gone away, though. Every string literal is one,
main receives its arguments as C strings, and C libraries and operating-system calls take them. This lesson shows
how they work, the three classic mistakes with recordings from Apple clang and GCC 16, and how to move safely
between the two kinds.
A C string is characters followed by ‘\0’
How a C string marks its end, and how std::string knows its length
Text description of the diagram
The diagram compares two ways of holding the text "Pune".
- char city[] = "Pune" is an array of five chars at indexes 0 to 4: 'P', 'u', 'n', 'e' and the null character '\0', whose value is 0. Nothing else records how long the text is.
- std::strlen(city) finds the length by walking from index 0 until it meets the first '\0', so it counts 4 characters. It has to do that walk every time it is called, and it never stops if the '\0' is missing.
- A std::string holding "Pune" stores its size, 4, beside the characters, so size() needs no search. It still keeps a '\0' after the characters, so that c_str() can hand them to a function that expects a C string.
// A C string is an array of char that ends with the null character '\0'.
#include <cstddef>
#include <cstring>
#include <print>
int main() {
char city[] = "Pune"; // five chars: 'P', 'u', 'n', 'e' and the terminator '\0'
std::println("sizeof(city) = {}, std::strlen(city) = {}", sizeof(city), std::strlen(city));
for (std::size_t i = 0; i < sizeof(city); ++i) {
if (city[i] == '\0') std::println("city[{}] = {:3} the terminator", i, static_cast<int>(city[i]));
else std::println("city[{}] = {:3} '{}'", i, static_cast<int>(city[i]), city[i]);
}
city[2] = '\0'; // a '\0' in the middle ends the string there
const char* text = city;
std::println("after city[2] = '\\0': std::strlen = {}, the string is \"{}\"", std::strlen(text), text);
std::println("std::strlen(\"café\") = {}: it counts bytes, and é takes two in UTF-8", std::strlen("café"));
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra layout.cpp -o layout
./layout
g++-16 -std=c++23 -Wall -Wextra layout.cpp -o layout-gcc
./layout-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra layout.cpp -o layout
$ ./layout
sizeof(city) = 5, std::strlen(city) = 4
city[0] = 80 'P'
city[1] = 117 'u'
city[2] = 110 'n'
city[3] = 101 'e'
city[4] = 0 the terminator
after city[2] = '\0': std::strlen = 2, the string is "Pu"
std::strlen("café") = 5: it counts bytes, and é takes two in UTF-8
$ g++-16 -std=c++23 -Wall -Wextra layout.cpp -o layout-gcc
$ ./layout-gcc
sizeof(city) = 5, std::strlen(city) = 4
city[0] = 80 'P'
city[1] = 117 'u'
city[2] = 110 'n'
city[3] = 101 'e'
city[4] = 0 the terminator
after city[2] = '\0': std::strlen = 2, the string is "Pu"
std::strlen("café") = 5: it counts bytes, and é takes two in UTF-8
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 layout_session.py
char city[] = "Pune";copies the literal into an array of fivechar: the four letters and the terminator. Sosizeof(city)is 5 whilestd::strlen(city)is 4.- Each
charholds a number: 80 is'P'in ASCII, and the terminator is 0. - Writing a
'\0'into the middle ends the string there:std::strlenstops at the first one it meets, and so does every function that prints or copies the string. std::strlencounts bytes, not letters:"café"is 5 bytes, because UTF-8 storeséin two.- Because the length is never stored, each
std::strlencall walks the whole string again. A loop such asfor (i = 0; i < strlen(s); ++i)walks it once per step.
String literals are read-only
A string literal such as "pune" is an array of const char, created before the program starts and kept for its
whole run, and the standard leaves the effect of changing one undefined. Since C++11, the standard no longer lets a
literal convert to a plain char*, although compilers still accept it, with a warning, for the sake of old code:
// A string literal is an array of const char, kept in memory that the program may not change.
#include <cstdio>
#include <print>
int main() {
char copy[] = "pune"; // an array initialised with a copy of the literal: yours to change
copy[0] = 'P';
std::println("{}", copy);
std::fflush(stdout); // print the line above now, before the crash below
char* p = "pune"; // not allowed since C++11: the literal is const
p[0] = 'P'; // writing into the literal: undefined behaviour
std::println("{}", p);
} from session import run
# Both compilers warn about char* p = "pune"; writing through p then crashes, because literals are read-only.
run("""
clang++ -std=c++23 -Wall -Wextra literal.cpp -o literal
./literal
g++-16 -std=c++23 -Wall -Wextra literal.cpp -o literal-gcc
./literal-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra literal.cpp -o literal
literal.cpp:11:15: warning: ISO C++11 does not allow conversion from string literal to 'char *' [-Wwritable-strings]
11 | char* p = "pune"; // not allowed since C++11: the literal is const
| ^
1 warning generated.
$ ./literal
Pune
[stopped by SIGBUS: a shell reports exit status 138]
$ g++-16 -std=c++23 -Wall -Wextra literal.cpp -o literal-gcc
literal.cpp: In function 'int main()':
literal.cpp:11:15: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
11 | char* p = "pune"; // not allowed since C++11: the literal is const
| ^~~~~~
$ ./literal-gcc
Pune
[stopped by SIGBUS: a shell reports exit status 138]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 literal_session.py
Both compilers warn about char* p = "pune"; (-Wwritable-strings in Clang, -Wwrite-strings in GCC), and both
programs crashed on the write with SIGBUS: the literal sits in memory that the program may only read. The copy in
char copy[] = "pune"; is an ordinary array, so changing it is fine. Point to literals with const char*, and copy
them into an array or a std::string when you need to change the text. The std::fflush(stdout) is there because
output that is still waiting in a buffer is lost when a program crashes.
Comparing: std::strcmp, not ==
C++ (compare.cpp) · compare.cpp
// Two arrays that hold the same text: == on pointers compares addresses, std::strcmp compares the characters.
#include <cstring>
#include <print>
int main() {
char typed[] = "pune";
char stored[] = "pune";
const char* a = typed;
const char* b = stored;
std::println("a == b: {}", a == b); // two arrays, two addresses
std::println("std::strcmp(a, b) == 0: {}", std::strcmp(a, b) == 0); // the same characters
std::println("std::strcmp(\"apple\", \"banana\") < 0: {}", std::strcmp("apple", "banana") < 0);
std::println("std::strcmp(\"Zoo\", \"apple\") < 0: {}", std::strcmp("Zoo", "apple") < 0); // 'Z' is 90, 'a' is 97
} C++ (compare_arrays.cpp) · compare_arrays.cpp
// == between two arrays: deprecated since C++20, an error in C++26.
#include <print>
int main() {
char typed[] = "pune";
char stored[] = "pune";
std::println("{}", typed == stored);
} from session import run
# compare.cpp with both compilers, then == between two arrays in C++23 and in C++26 mode (-std=c++2c).
run("""
clang++ -std=c++23 -Wall -Wextra compare.cpp -o compare
./compare
g++-16 -std=c++23 -Wall -Wextra compare.cpp -o compare-gcc
./compare-gcc
clang++ -std=c++23 -Wall -Wextra compare_arrays.cpp -o compare_arrays 2>&1 | grep -E "warning|error"
g++-16 -std=c++23 -Wall -Wextra compare_arrays.cpp -o compare_arrays-gcc 2>&1 | grep -E "warning|error"
clang++ -std=c++2c -Wall -Wextra compare_arrays.cpp -o compare_arrays 2>&1 | grep -E "warning|error"
g++-16 -std=c++2c -Wall -Wextra compare_arrays.cpp -o compare_arrays-gcc 2>&1 | grep -E "warning|error"
""") Output
$ clang++ -std=c++23 -Wall -Wextra compare.cpp -o compare
$ ./compare
a == b: false
std::strcmp(a, b) == 0: true
std::strcmp("apple", "banana") < 0: true
std::strcmp("Zoo", "apple") < 0: true
$ g++-16 -std=c++23 -Wall -Wextra compare.cpp -o compare-gcc
$ ./compare-gcc
a == b: false
std::strcmp(a, b) == 0: true
std::strcmp("apple", "banana") < 0: true
std::strcmp("Zoo", "apple") < 0: true
$ clang++ -std=c++23 -Wall -Wextra compare_arrays.cpp -o compare_arrays 2>&1 | grep -E "warning|error"
compare_arrays.cpp:7:30: warning: comparison between two arrays is deprecated; to compare array addresses, use unary '+' to decay operands to pointers [-Wdeprecated-array-compare]
compare_arrays.cpp:7:30: warning: array comparison always evaluates to false [-Wtautological-compare]
2 warnings generated.
$ g++-16 -std=c++23 -Wall -Wextra compare_arrays.cpp -o compare_arrays-gcc 2>&1 | grep -E "warning|error"
compare_arrays.cpp:7:30: warning: comparison between two arrays is deprecated in C++20 [-Warray-compare]
$ clang++ -std=c++2c -Wall -Wextra compare_arrays.cpp -o compare_arrays 2>&1 | grep -E "warning|error"
compare_arrays.cpp:7:30: error: comparison between two arrays is ill-formed in C++26; to compare array addresses, use unary '+' to decay operands to pointers [-Warray-compare-cxx26]
compare_arrays.cpp:7:30: warning: array comparison always evaluates to false [-Wtautological-compare]
1 warning and 1 error generated.
$ g++-16 -std=c++2c -Wall -Wextra compare_arrays.cpp -o compare_arrays-gcc 2>&1 | grep -E "warning|error"
compare_arrays.cpp:7:30: error: comparison between two arrays is not allowed in C++26 [-Warray-compare]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare_session.py
a == bon twoconst char*compares two addresses. The two arrays hold the same text in different places, so the answer isfalse.std::strcmp(a, b)compares the characters one by one and returns 0 when they are all equal, a negative number whenacomes first and a positive number whenbdoes. It compares the byte values, so every uppercase ASCII letter comes before every lowercase one:"Zoo"sorts before"apple".- Comparing two arrays directly,
typed == stored, has been deprecated since C++20, and both compilers warn. C++26 removes it: with-std=c++2c(C++26 mode), Clang and GCC both refuse to compile it. - With string literals the result of
==is not even predictable: the standard leaves it unspecified whether two identical literals are the same object, so"pune" == "pune"may betrueorfalse.
Copying and joining: where buffer overflows come from
std::strcpy(dest, src) copies characters until it has copied the '\0', and std::strcat(dest, src) appends the
same way. Neither knows how big dest is:
// std::strcat appends until it has copied the '\0', however small the destination is.
#include <cstring>
#include <print>
void greet(const char* name) {
char buffer[16];
std::strcpy(buffer, "Hello, "); // 7 characters and '\0': fits
std::strcat(buffer, name); // appends name: nothing checks that it fits
std::println("{}", buffer);
}
int main(int argc, char* argv[]) {
greet(argc > 1 ? argv[1] : "Asha");
} from session import run
# A short name fits; a 14-letter name overflows the 16-byte buffer. Without a checker the result depends on the
# compiler: Clang's build dies inside println, GCC's -O2 build seems to work. AddressSanitizer names the bug (grep
# keeps the key lines of its report, sed hides the addresses).
run("""
clang++ -std=c++23 -Wall -Wextra overflow.cpp -o overflow
./overflow Ravi
./overflow Venkataramanan
g++-16 -std=c++23 -O2 -Wall -Wextra overflow.cpp -o overflow-gcc
./overflow-gcc Venkataramanan
clang++ -std=c++23 -g -fsanitize=address overflow.cpp -o overflow-asan
ASAN_OPTIONS=symbolize=0 ./overflow-asan Venkataramanan 2>&1 | grep -E "ERROR|of size|overflows" | sed -E 's/==[0-9]+==//; s/0x[0-9a-f]+/0x…/g'
""") Output
$ clang++ -std=c++23 -Wall -Wextra overflow.cpp -o overflow
$ ./overflow Ravi
Hello, Ravi
$ ./overflow Venkataramanan
libc++abi: terminating due to uncaught exception of type std::bad_alloc: std::bad_alloc
[stopped by SIGABRT: a shell reports exit status 134]
$ g++-16 -std=c++23 -O2 -Wall -Wextra overflow.cpp -o overflow-gcc
$ ./overflow-gcc Venkataramanan
Hello, Venkataramanan
$ clang++ -std=c++23 -g -fsanitize=address overflow.cpp -o overflow-asan
$ ASAN_OPTIONS=symbolize=0 ./overflow-asan Venkataramanan 2>&1 | grep -E "ERROR|of size|overflows" | sed -E 's/==[0-9]+==//; s/0x[0-9a-f]+/0x…/g'
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x… at pc 0x… bp 0x… sp 0x…
WRITE of size 15 at 0x… thread T0
[32, 48) 'buffer' (line 6) <== Memory access at offset 48 overflows this variable
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 overflow_session.py
"Hello, Ravi" fits into 16 bytes. "Hello, Venkataramanan" needs 22, so strcat wrote 6 bytes past the end of
buffer, over whatever the compiler had placed there. The Clang build then died with a std::bad_alloc exception
thrown inside std::println, a message that says nothing about the real cause. The GCC -O2 build printed the whole
greeting and seemed fine: the overflow was still there, it just damaged nothing that this short program used
afterwards. AddressSanitizer names the bug, a
stack-buffer-overflow: a write of 15 bytes, the name and its terminator, that runs past the 16 bytes of buffer.
Overflows like this one are also a classic security hole: input chosen by an attacker can overwrite data the
program relies on, such as the place a function returns to.
When you must fill a fixed buffer, use a function that takes its size. std::snprintf writes at most the size you
give it, always ends the text with '\0', and returns the length the whole text would have had:
// std::snprintf writes at most the size it is given, always ends with '\0',
// and returns the length the whole text would have had.
#include <cstddef>
#include <cstdio>
#include <print>
int main() {
char line[12];
int needed = std::snprintf(line, sizeof line, "%s scored %d", "Asha", 74);
std::println("needed {}, room for {} plus '\\0': \"{}\"", needed, sizeof line - 1, line);
if (needed >= static_cast<int>(sizeof line)) std::println("the text was cut short");
char wide[32];
needed = std::snprintf(wide, sizeof wide, "%s scored %d", "Asha", 74);
std::println("with 32 bytes: needed {}, \"{}\"", needed, wide);
} from session import run
# GCC sees the truncation while compiling (-Wformat-truncation, part of -Wall); both programs print the same.
run("""
clang++ -std=c++23 -Wall -Wextra bounded.cpp -o bounded
./bounded
g++-16 -std=c++23 -Wall -Wextra bounded.cpp -o bounded-gcc
./bounded-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra bounded.cpp -o bounded
$ ./bounded
needed 14, room for 11 plus '\0': "Asha scored"
the text was cut short
with 32 bytes: needed 14, "Asha scored 74"
$ g++-16 -std=c++23 -Wall -Wextra bounded.cpp -o bounded-gcc
bounded.cpp: In function 'int main()':
bounded.cpp:9:31: warning: '%d' directive output truncated writing 2 bytes into a region of size 0 [-Wformat-truncation=]
9 | int needed = std::snprintf(line, sizeof line, "%s scored %d", "Asha", 74);
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bounded.cpp:9:31: note: 'snprintf' output 15 bytes into a destination of size 12
$ ./bounded-gcc
needed 14, room for 11 plus '\0': "Asha scored"
the text was cut short
with 32 bytes: needed 14, "Asha scored 74"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bounded_session.py
The text needed 14 characters and the buffer had room for 11, so snprintf stored "Asha scored" and returned 14:
a return value that is not smaller than the buffer’s size means the text was cut short. GCC even noticed the cut while
compiling, with its -Wformat-truncation warning. Avoid std::strncpy for this job: when the source is too long it
fills the destination without a terminator, so the next strlen runs off its end.
| Criterion | C string (char array) | std::string |
|---|---|---|
| Length | Found by walking to '\0', every time | Stored: size() takes one step |
| Memory | A fixed buffer that you size yourself | Grows by itself and frees itself |
| Copy and join | strcpy and strcat, unchecked | = and +, which never overflow |
| Compare text | strcmp(a, b) == 0 | a == b |
| A '\0' inside the text | Ends the string | An ordinary character |
| When to choose | Talking to C functions, and string literals | Every string your own code builds, keeps or changes |
Converting between C strings and std::string
Going from a C string to a std::string is easy and safe, because the string copies the characters. Going back
needs care:
C++ (convert.cpp) · convert.cpp
// From C strings to std::string and back.
#include <cstddef>
#include <cstring>
#include <print>
#include <string>
#include <string_view>
std::size_t c_api_length(const char* s) { return std::strlen(s); } // stands for any function that takes a C string
int main() {
const char* from_c = "Chennai";
std::string s = from_c; // copies the characters up to the '\0'
s += " Central"; // s owns its memory and grows as needed
std::println("{} ({} characters)", s, s.size());
std::println("a C function sees {} characters", c_api_length(s.c_str())); // c_str(): a '\0'-terminated copy-free view
std::string_view view = from_c; // no copy: a pointer and a length, found with strlen once
std::println("view {} has {} characters", view, view.size());
std::string with_zero("ab\0cd", 5); // a std::string can hold '\0' as an ordinary character
std::println("size {}, but std::strlen(c_str()) is {}", with_zero.size(), std::strlen(with_zero.c_str()));
} C++ (from_null.cpp) · from_null.cpp
// A std::string made from a null pointer: undefined behaviour before C++23, a compile error since.
#include <string>
int main() {
std::string s = nullptr;
return static_cast<int>(s.size());
} from session import run
# convert.cpp with both compilers, then a std::string made from nullptr, which C++23 refuses (grep keeps the error lines).
run("""
clang++ -std=c++23 -Wall -Wextra convert.cpp -o convert
./convert
g++-16 -std=c++23 -Wall -Wextra convert.cpp -o convert-gcc
./convert-gcc
clang++ -std=c++23 from_null.cpp -o from_null 2>&1 | grep "error:"
g++-16 -std=c++23 from_null.cpp -o from_null-gcc 2>&1 | grep "error:"
""") Output
$ clang++ -std=c++23 -Wall -Wextra convert.cpp -o convert $ ./convert Chennai Central (15 characters) a C function sees 15 characters view Chennai has 7 characters size 5, but std::strlen(c_str()) is 2 $ g++-16 -std=c++23 -Wall -Wextra convert.cpp -o convert-gcc $ ./convert-gcc Chennai Central (15 characters) a C function sees 15 characters view Chennai has 7 characters size 5, but std::strlen(c_str()) is 2 $ clang++ -std=c++23 from_null.cpp -o from_null 2>&1 | grep "error:" from_null.cpp:5:17: error: conversion function from 'std::nullptr_t' to 'std::string' (aka 'basic_string<char>') invokes a deleted function $ g++-16 -std=c++23 from_null.cpp -o from_null-gcc 2>&1 | grep "error:" from_null.cpp:5:21: error: use of deleted function 'std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::nullptr_t) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; std::nullptr_t = std::nullptr_t]'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 convert_session.py
std::string s = from_c;copies the characters up to the'\0'; after that,sowns its own memory.s.c_str()returns a pointer to the string’s characters followed by a'\0', ready for a function that expects a C string. It is not a copy, so it stays valid only whilesexists and is not changed.- A
std::string_viewbuilt from a C string runsstrlenonce and then remembers the length. Like a C string pointer, it does not own the characters. - A
std::stringmay contain'\0'as an ordinary character:with_zerohas 5 characters, but a C function given itsc_str()sees only the 2 before the first terminator. - Making a
std::stringfrom a null pointer used to compile, with undefined behaviour, usually a crash, at run time. Since C++23 the standard deletes that constructor, sostd::string s = nullptr;is a compile error in both compilers.
The pointer from c_str() is the one to watch. Here it outlives the string it points into:
// c_str() points into the string's own memory: valid only while the string lives and is not changed.
#include <cstring>
#include <print>
#include <string>
std::string full_name(const char* first, const char* last) {
return std::string(first) + " " + last;
}
int main() {
std::string city = "Bengaluru";
const char* p = city.c_str(); // fine: city is alive and unchanged
std::println("{} has {} characters", p, std::strlen(p));
const char* name = full_name("Venkata", "Ramanan Subramaniam").c_str();
// The string returned by full_name was a temporary: it was destroyed at the end of the line above.
std::println("{}", name);
} from session import run
# Clang warns while compiling; AddressSanitizer catches the read of the freed memory (key lines, addresses hidden).
run("""
clang++ -std=c++23 -g -Wall -Wextra -fsanitize=address lifetime.cpp -o lifetime
ASAN_OPTIONS=symbolize=0 ./lifetime 2>&1 | grep -E "ERROR|of size|located|freed by|allocated by" | sed -E 's/==[0-9]+==//; s/0x[0-9a-f]+/0x…/g'
""") Output
$ clang++ -std=c++23 -g -Wall -Wextra -fsanitize=address lifetime.cpp -o lifetime
lifetime.cpp:15:24: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
15 | const char* name = full_name("Venkata", "Ramanan Subramaniam").c_str();
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
$ ASAN_OPTIONS=symbolize=0 ./lifetime 2>&1 | grep -E "ERROR|of size|located|freed by|allocated by" | sed -E 's/==[0-9]+==//; s/0x[0-9a-f]+/0x…/g'
ERROR: AddressSanitizer: heap-use-after-free on address 0x… at pc 0x… bp 0x… sp 0x…
READ of size 2 at 0x… thread T0
0x… is located 0 bytes inside of 48-byte region [0x…,0x…)
freed by thread T0 here:
previously allocated by thread T0 here:
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifetime_session.py
full_name returns a temporary std::string, which is destroyed at the end of the line that called it, together
with its buffer. Clang warns while compiling (-Wdangling-gsl), and AddressSanitizer reports the later read as a
heap-use-after-free in a 48-byte region, the freed buffer of the 27-character name. GCC compiles the same file
without a warning, so do not rely on one. Keep the std::string in a variable for as long as you use its c_str(),
and call c_str() again after any change to the string.
Your turn: write strlen, strcmp and a bounded copy
Writing these three functions yourself is the quickest way to see the rules of this lesson from the inside: the walk
to '\0', the comparison of byte values and the terminator that must always be there.
// Exercise: three C-string functions. Write the bodies marked "your code here", then build and run this file:
// clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined cstring_exercise.cpp -o cstring_exercise
// ./cstring_exercise
// Every check must say "ok", and the sanitizers must report nothing. Do not call the <cstring> functions in your
// answers: the checks compare your functions with them.
#include <cstddef>
#include <cstring>
#include <print>
// The number of characters before the terminating '\0', like std::strlen.
std::size_t my_strlen(const char* s) {
return 0; // your code here
}
// Compares two C strings like std::strcmp: negative if a comes first, 0 if they are equal, positive if b comes first.
// Compare the characters as unsigned char values, as std::strcmp does.
int my_strcmp(const char* a, const char* b) {
return 0; // your code here
}
// Copies src into dest, which has room for size chars, and always ends dest with '\0': it copies at most size - 1
// characters, and with size 0 it writes nothing. Returns my_strlen(src), so a result >= size means the copy was cut.
std::size_t bounded_copy(char* dest, std::size_t size, const char* src) {
return 0; // your code here
}
int failures = 0;
void check(bool passed, const char* what) {
std::println("{} {}", passed ? "ok " : "FAILED", what);
if (!passed) ++failures;
}
int sign(int x) { return (x > 0) - (x < 0); }
int main() {
const char* words[]{"", "a", "Pune", "Thiruvananthapuram"};
bool lengths_ok = true;
for (const char* w : words) lengths_ok = lengths_ok && my_strlen(w) == std::strlen(w);
check(lengths_ok, "my_strlen agrees with std::strlen on \"\", \"a\", \"Pune\" and a long name");
const char* pairs[][2]{{"apple", "banana"}, {"pune", "pune"}, {"abc", "ab"}, {"Zoo", "apple"}, {"", "x"}};
bool order_ok = true;
for (auto& p : pairs) order_ok = order_ok && sign(my_strcmp(p[0], p[1])) == sign(std::strcmp(p[0], p[1]));
check(order_ok, "my_strcmp has the same sign as std::strcmp on five pairs");
check(my_strcmp("caf\xc3\xa9", "cafe") > 0, "bytes above 127 compare as unsigned: \"café\" after \"cafe\"");
char dest[8];
std::size_t n = bounded_copy(dest, sizeof dest, "Bengaluru");
check(n == 9 && std::strcmp(dest, "Bengalu") == 0, "\"Bengaluru\" into 8 bytes: \"Bengalu\", returns 9");
n = bounded_copy(dest, sizeof dest, "Pune");
check(n == 4 && std::strcmp(dest, "Pune") == 0, "\"Pune\" fits: copied whole, returns 4");
char one[1]{'x'};
n = bounded_copy(one, sizeof one, "Agra");
check(n == 4 && one[0] == '\0', "size 1: only the terminator is written");
char untouched[4]{'q', 'q', 'q', 'q'};
n = bounded_copy(untouched, 0, "Agra");
check(n == 4 && untouched[0] == 'q', "size 0: nothing is written");
std::println("{} of 7 checks failed", failures);
return failures == 0 ? 0 : 1;
} As it starts, every function returns 0, so every check fails:
from session import run
# The exercise as it starts: the functions return 0, so most checks fail.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined cstring_exercise.cpp -o cstring_exercise
./cstring_exercise
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined cstring_exercise.cpp -o cstring_exercise
cstring_exercise.cpp:11:35: warning: unused parameter 's' [-Wunused-parameter]
11 | std::size_t my_strlen(const char* s) {
| ^
cstring_exercise.cpp:17:27: warning: unused parameter 'a' [-Wunused-parameter]
17 | int my_strcmp(const char* a, const char* b) {
| ^
cstring_exercise.cpp:17:42: warning: unused parameter 'b' [-Wunused-parameter]
17 | int my_strcmp(const char* a, const char* b) {
| ^
cstring_exercise.cpp:23:32: warning: unused parameter 'dest' [-Wunused-parameter]
23 | std::size_t bounded_copy(char* dest, std::size_t size, const char* src) {
| ^
cstring_exercise.cpp:23:50: warning: unused parameter 'size' [-Wunused-parameter]
23 | std::size_t bounded_copy(char* dest, std::size_t size, const char* src) {
| ^
cstring_exercise.cpp:23:68: warning: unused parameter 'src' [-Wunused-parameter]
23 | std::size_t bounded_copy(char* dest, std::size_t size, const char* src) {
| ^
6 warnings generated.
$ ./cstring_exercise
FAILED my_strlen agrees with std::strlen on "", "a", "Pune" and a long name
FAILED my_strcmp has the same sign as std::strcmp on five pairs
FAILED bytes above 127 compare as unsigned: "café" after "cafe"
FAILED "Bengaluru" into 8 bytes: "Bengalu", returns 9
FAILED "Pune" fits: copied whole, returns 4
FAILED size 1: only the terminator is written
FAILED size 0: nothing is written
7 of 7 checks failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
You are done when it prints 0 of 7 checks failed and the sanitizers report nothing. Hints: in my_strcmp, stop at
the first position where the characters differ or where a ends, then subtract the two characters converted to
unsigned char; in bounded_copy, handle size == 0 first, and write the terminator after the last character you
copy.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · Why does strlen(“hello”) return 5 while sizeof(“hello”) is 6?
A string literal is an array of const char that holds its characters and then the terminating null character,
'\0'. "hello" is therefore an array of 6 chars, and sizeof measures the whole array: 6 bytes. std::strlen
does not know about arrays at all. It receives a pointer to the first character and counts characters until it
meets the first '\0', which gives 5. The two also differ after a change: write a '\0' into the middle of a char
array and strlen gets shorter, while sizeof stays the size of the array. And inside a function that receives the
string as a const char*, sizeof gives the size of the pointer, 8 on a 64-bit computer, which is why only
strlen can measure a C string there.
Key takeaways
- A C string is chars followed by
'\0'.std::strlenwalks to that byte every time it is called, and it counts bytes:"café"is 5. - String literals are arrays of
const charin read-only memory: point to them withconst char*, and writing into one crashed both builds. - Compare text with
std::strcmp(a, b) == 0.==compares addresses, and==between two arrays is an error in C++26. std::strcpyandstd::strcatnever check the destination’s size; a 22-byte greeting in a 16-byte buffer gave a crash with Clang, silent damage with GCC and a stack-buffer-overflow report with AddressSanitizer.std::snprintfnever overflows, always terminates the text and returns the length it needed.std::stringstores its length, manages its memory and compares with==;c_str()lends a C string that dies with the string or changes with it.
References
- Working Draft, Programming Languages: C++ (N4950): string literals, basic_string, <cstring> (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C (N3220): string handling and snprintf (ISO/IEC JTC1/SC22/WG14)
- C++ Core Guidelines, SL.str.1: Use std::string to own character sequences (Standard C++ Foundation)
- AddressSanitizer (Clang documentation) (LLVM Project)
- Diagnostic flags in Clang: -Wdangling-gsl (LLVM Project)
- Diagnostic flags in Clang: -Warray-compare-cxx26 (LLVM Project)
- Using the GNU Compiler Collection: Options to Request or Suppress Warnings (-Wformat-truncation, -Wwrite-strings) (GNU Project)
- P2865R6: Remove Deprecated Array Comparisons from C++26 (ISO/IEC JTC1/SC22/WG21)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress