<cstring>
C++ header providing null-terminated string manipulation and raw memory operation functions inherited from C's <string.h>.
<cstring>since C++98The <cstring> header is the C++ form of C's <string.h>, declaring functions for null-terminated byte string manipulation and raw memory operations in namespace std.
Overview
<cstring> exposes two categories of routines: functions that operate on null-terminated byte strings (NTBS) β whose correctness depends on finding a '\0' terminator within bounds β and lower-level memory primitives that work on arbitrary byte ranges with no terminator requirement.
Before std::string (also C++98) and std::string_view (C++17) became idiomatic, C-style strings were the only option. Today <cstring> still surfaces in:
- interoperability with C APIs and OS/platform interfaces that deal in
char* - performance-critical paths where
memcpy/memsetare the lowest-overhead tools available - implementing custom string types, serialisers, or memory allocators
- proven hot paths where the overhead of
std::stringallocation is measurable
All identifiers live in namespace std when <cstring> is included. The older <string.h> header also works in C++ (identifiers in the global namespace, possibly duplicated in std), but including it has been deprecated since C++98.
Syntax
#include <cstring>
// String length and copy
std::size_t std::strlen (const char* s);
char* std::strcpy (char* dest, const char* src);
char* std::strncpy(char* dest, const char* src, std::size_t n);
// String concatenation
char* std::strcat (char* dest, const char* src);
char* std::strncat(char* dest, const char* src, std::size_t n);
// String comparison
int std::strcmp (const char* s1, const char* s2);
int std::strncmp(const char* s1, const char* s2, std::size_t n);
// String search
char* std::strchr (const char* s, int c);
char* std::strrchr(const char* s, int c);
char* std::strstr (const char* haystack, const char* needle);
char* std::strtok (char* str, const char* delimiters); // not thread-safe
// Error description
char* std::strerror(int errnum); // not thread-safe
// Memory primitives (no null-terminator assumption)
void* std::memcpy (void* dest, const void* src, std::size_t n); // regions must not overlap
void* std::memmove(void* dest, const void* src, std::size_t n); // handles overlap safely
void* std::memset (void* dest, int byte_value, std::size_t n);
int std::memcmp (const void* s1, const void* s2, std::size_t n);
void* std::memchr (const void* s, int c, std::size_t n);C++14 const-correctness: strchr, strrchr, strstr, and memchr were given two overloads β one for const input returning a const pointer, one for non-const input returning a non-const pointer. Before C++14 the standard specified a single signature returning a non-const pointer regardless of input constness, which was technically UB when used to mutate a string literal.
Examples
Core string operations
#include <cstring>
#include <cassert>
#include <iostream>
void string_ops() {
char buf[64];
std::strcpy(buf, "Hello"); // copies "Hello\0" into buf
std::strcat(buf, ", world"); // appends ", world\0"
std::size_t len = std::strlen(buf); // 12 β null not counted
std::cout << buf << " (" << len << " chars)\n";
// strcmp returns 0 on equality, <0 or >0 otherwise β do NOT compare to 1/-1
assert(std::strcmp("abc", "abc") == 0);
assert(std::strcmp("abc", "abd") < 0);
// strncmp limits comparison to n characters
assert(std::strncmp("abcXXX", "abcYYY", 3) == 0);
}Bounded, always-null-terminated copy
strcpy does not check destination buffer size β if src is longer than dest, it writes past the end: undefined behaviour and a classic buffer overflow vector. A safe idiom:
#include <cstring>
#include <stdexcept>
// Copies src into dest[0..dest_size-1], always null-terminates, throws on overflow.
void checked_copy(char* dest, std::size_t dest_size, const char* src) {
std::size_t src_len = std::strlen(src);
if (src_len >= dest_size)
throw std::length_error("destination buffer too small");
std::memcpy(dest, src, src_len + 1); // +1 carries the '\0'
}strncpy is a poor substitute: it pads the destination with '\0' up to n bytes (O(n) even for short strings) and β crucially β does not null-terminate when strlen(src) >= n. Explicitly setting dest[n-1] = '\0' is always required.
memcpy vs memmove vs std::copy
These three tools have different guarantees and appropriate use-cases. Choosing the wrong one introduces either undefined behaviour or unnecessary overhead.
memcpy is the fastest raw-byte copy: it is permitted to assume the source and destination do not overlap, which lets the compiler (or libc) use the widest possible SIMD loads without worrying about direction. The moment the regions do overlap, the behaviour is undefined.
memmove gives the same observable result but handles overlap by either using a temporary buffer internally or copying in reverse order when dest > src. On most modern hardware with auto-vectorisation, the performance gap is negligible.
std::copy from <algorithm> works on any pair of InputIterator/OutputIterator, including raw pointers. For raw pointers to trivially copyable types, a quality implementation will lower to memmove (not memcpy) under the hood. It is the right choice when you are working in a template context or when the element type is not a byte.
#include <cstring>
#include <algorithm>
#include <vector>
// --- memcpy: safe only for non-overlapping regions ---
void bulk_copy(char* dest, const char* src, std::size_t n) {
std::memcpy(dest, src, n); // UB if ranges overlap
}
// --- memcpy overlap bug: classic mistake when shifting elements right ---
void bad_shift_right(char* buf, std::size_t len, std::size_t shift) {
// BUG: dest (buf+shift) overlaps src (buf) when shift < len
std::memcpy(buf + shift, buf, len); // undefined behaviour
}
// --- memmove fix: identical call, defined behaviour ---
void good_shift_right(char* buf, std::size_t len, std::size_t shift) {
std::memmove(buf + shift, buf, len); // copies back-to-front internally
std::memset(buf, '\0', shift); // zero the vacated prefix
}
// --- std::copy_backward: the C++ way to shift elements right in a container ---
#include <algorithm> // C++98
void shift_vector_right(std::vector<int>& v, std::size_t shift) {
// copy_backward copies from end-to-start, which is safe for rightward overlap
std::copy_backward(v.begin(), v.end() - shift, v.end());
}Best Practice. When overlap cannot be statically ruled out, default to memmove (for byte buffers) or std::copy_backward (for element arrays). Reserve memcpy for situations where the non-overlap property is provably guaranteed and the extra speed matters.
strcpy/strcat security dangers and safer alternatives
strcpy is one of the most exploited functions in the history of C and C++. It copies bytes from src until it hits a null terminator, writing every byte β including the null β into dest with no length check. A source longer than the destination silently overwrites adjacent stack or heap memory.
#include <cstring>
void dangerous() {
char buf[8];
std::strcpy(buf, "this string is way too long");
// Writes 28 bytes into an 8-byte buffer β stack smash, undefined behaviour.
}strncpy(dst, src, n) looks safer but has a hidden trap: if strlen(src) >= n, no null terminator is written. The resulting byte array is not a valid C string.
#include <cstring>
void strncpy_null_termination_bug() {
char buf[4];
std::strncpy(buf, "hello", 4);
// buf == {'h', 'e', 'l', 'l'} β no '\0'
// Any subsequent strlen(buf) or puts(buf) reads past buf: UB.
// Canonical fix: always force-terminate after strncpy.
buf[4 - 1] = '\0'; // buf[n-1] = '\0'
}strncat is slightly better: it always writes a null terminator. However the n argument is the maximum number of characters to append, not the size of the destination buffer, which makes it easy to miscalculate.
#include <cstring>
void strncat_correct(char* dest, std::size_t dest_capacity, const char* src) {
std::size_t dest_len = std::strlen(dest);
if (dest_len >= dest_capacity) return; // already full
// n must be capacity - current_length - 1 (reserve space for '\0')
std::strncat(dest, src, dest_capacity - dest_len - 1);
// strncat always null-terminates the result
}The portable safe alternative is snprintf. It always null-terminates, accepts a buffer size, and returns the number of characters that would have been written β letting you detect truncation:
#include <cstdio>
bool safe_copy(char* dest, std::size_t dest_size, const char* src) {
int written = std::snprintf(dest, dest_size, "%s", src);
return written >= 0 && static_cast<std::size_t>(written) < dest_size;
// returns false if truncation occurred
}
bool safe_append(char* dest, std::size_t dest_size, const char* suffix) {
std::size_t len = std::strlen(dest);
int written = std::snprintf(dest + len, dest_size - len, "%s", suffix);
return written >= 0 && static_cast<std::size_t>(written) < dest_size - len;
}POSIX defines strlcpy(dst, src, size) and strlcat(dst, src, size) which always null-terminate and return the length that would have been produced (truncation detectable when retval >= size). These are available on BSD, macOS, and glibc 2.38+, but are not standard C or C++ β use snprintf for portable code.
memcpy vs memmove for type-punning
Before C++20, the sanctioned way to reinterpret bit patterns between unrelated trivially copyable types was memcpy:
#include <cstring>
#include <cstdint>
#include <type_traits>
// Defined behaviour; compiler typically optimises to a register move.
float bits_to_float(uint32_t bits) {
static_assert(std::is_trivially_copyable_v<float>); // C++11 trait
static_assert(sizeof(float) == sizeof(uint32_t));
float result;
std::memcpy(&result, &bits, sizeof(result));
return result;
}
// C++20: prefer std::bit_cast<float>(bits) from <bit> β constexpr-capable,
// no memcpy required.String searching
#include <cstring>
#include <iostream>
void parse_log_line(const char* line) {
// strchr: first occurrence of a character
const char* colon = std::strchr(line, ':');
if (!colon) return;
// strstr: first occurrence of a substring
const char* err = std::strstr(line, "ERROR");
if (err)
std::cout << "Error token at byte offset " << (err - line) << '\n';
// strrchr: *last* occurrence β handy for extension or basename extraction
const char* path = "/usr/local/lib/libfoo.so.2";
const char* dot = std::strrchr(path, '.');
if (dot)
std::cout << "suffix: " << dot << '\n'; // ".2"
}strtok thread safety and alternatives
strtok maintains an internal static pointer between calls β the pointer to the "rest of the string" after the last token. This makes it fundamentally non-reentrant: two threads calling strtok on different strings simultaneously will corrupt each other's state. Even on a single thread, calling any function that internally uses strtok while your own tokenisation loop is in progress will break it.
#include <cstring>
#include <cstdio>
// DANGEROUS in multithreaded code or any context where the
// call stack may re-enter strtok with a different string.
void single_threaded_only(char* line) {
char* token = std::strtok(line, " ,;");
while (token) {
std::printf("token: %s\n", token);
token = std::strtok(nullptr, " ,;"); // nullptr = continue from saved state
}
}POSIX strtok_r (available on Linux/macOS) and C11 Annex K strtok_s (available on MSVC) take an explicit char** state pointer, making them reentrant and thread-safe:
#include <cstring> // strtok_r on POSIX
// POSIX-only: strtok_r is thread-safe because state is caller-owned.
void thread_safe_tokenise(char* line) {
char* saveptr = nullptr;
char* token = strtok_r(line, " ,;", &saveptr);
while (token) {
// process token ...
token = strtok_r(nullptr, " ,;", &saveptr);
}
}In modern C++ the cleanest approach is to work with std::string_view and avoid modifying the input string at all:
#include <string_view>
#include <vector>
// C++17: tokenise without modifying the source string.
std::vector<std::string_view> tokenise(std::string_view input,
std::string_view delimiters) {
std::vector<std::string_view> tokens;
std::size_t start = 0;
while (start < input.size()) {
std::size_t end = input.find_first_of(delimiters, start);
if (end == std::string_view::npos) {
tokens.push_back(input.substr(start));
break;
}
if (end != start)
tokens.push_back(input.substr(start, end - start));
start = end + 1;
}
return tokens;
}In C++20, std::views::split from <ranges> provides a lazy, range-based tokeniser that avoids allocation entirely:
#include <ranges> // C++20
#include <string_view>
#include <iostream>
void split_example() {
std::string_view csv = "alpha,beta,gamma,delta";
for (auto token : csv | std::views::split(',')) {
// token is a subrange of chars; construct a string_view cheaply
std::string_view sv(token.begin(), token.end());
std::cout << sv << '\n';
}
}memset for zero-initialisation vs value initialisation
memset(ptr, 0, size) sets every byte in a region to zero. For POD types with an all-bits-zero representation equivalent to zero (integers, pointers on all common platforms, IEEE 754 floats where 0.0 is all-zero bits), this produces the expected zero value. For types with padding bytes, it zeros those too β which is the desired behaviour when preparing data for serialisation or comparison.
#include <cstring>
#include <cstdint>
struct PacketHeader {
uint32_t magic;
uint16_t version;
uint8_t flags[6];
// compiler may insert padding
};
PacketHeader make_header() {
PacketHeader h;
std::memset(&h, 0, sizeof(h)); // zeros every byte including padding
h.magic = 0xCAFEBABE;
h.version = 2;
return h;
}memset interprets its second argument as an unsigned char and repeats that byte pattern. Using it to set multi-byte integers to non-zero values produces implementation-defined results on exotic word layouts β keep non-zero patterns to unsigned char arrays or to well-known byte values like 0xFF where the bit pattern is unambiguous.
When memset is NOT appropriate. For types with virtual functions, non-trivial constructors, or pointers to member functions, memset will corrupt the vtable pointer or invariants. For non-POD types, always use value initialisation (= {}) or std::fill:
#include <cstring>
#include <algorithm>
#include <array>
struct Trivial { int x; double y; };
struct NonTrivial { virtual ~NonTrivial() {} int x; };
void zero_init_examples() {
// POD: memset is fine
Trivial t;
std::memset(&t, 0, sizeof(t));
// POD: value-initialisation is equivalent and more idiomatic in C++
Trivial t2 = {}; // C++98 β zero-initialises all members
Trivial t3{}; // C++11 β same
// Non-POD: NEVER use memset β would zero the vtable pointer
NonTrivial nt{}; // correct: calls constructor
// Arrays of POD: std::fill is type-safe and equally fast
std::array<int, 256> buf;
std::fill(buf.begin(), buf.end(), 0); // C++11
// or simply: std::array<int, 256> buf{}; // value-initialises to all zeros
}memcmp for POD equality
memcmp compares memory byte-by-byte and returns zero if the two regions are identical. For trivially copyable types with no padding, it provides a fast equality check that the compiler can lower to a sequence of wide comparisons or SIMD instructions.
#include <cstring>
#include <cstdint>
struct Vector3 {
float x, y, z;
// No padding on any common platform (4-byte floats, packed)
};
bool vectors_equal(const Vector3& a, const Vector3& b) {
return std::memcmp(&a, &b, sizeof(Vector3)) == 0;
}Pitfall: padding bytes are uninitialized. When a struct has padding (compiler-inserted bytes for alignment), those bytes are not guaranteed to be initialised to any particular value. Two objects with identical field values can have different padding contents, causing memcmp to report inequality:
#include <cstring>
struct Padded {
char c; // 1 byte
// 3 bytes of padding inserted by the compiler for alignment
int i; // 4 bytes
};
bool padded_memcmp_bug() {
Padded a{}, b{}; // value-init zeros the fields...
a.c = 'X'; a.i = 42;
b.c = 'X'; b.i = 42;
// ...but the 3 padding bytes between c and i may still differ
// if the objects were ever manipulated without value-initialisation.
return std::memcmp(&a, &b, sizeof(Padded)) == 0;
// MAY return false even though a and b are logically equal!
}The safe approaches are:
- Zero-initialise both objects with
= {}ormemsetbefore assigning fields β this guarantees padding bytes are zero in both. - Provide
operator==that compares only the named fields. - Use
static_assertwithstd::has_unique_object_representations_v<T>(C++17) to check at compile time that the type has no padding.
#include <type_traits> // C++17
template <typename T>
bool safe_memcmp_equal(const T& a, const T& b) {
static_assert(std::is_trivially_copyable_v<T>,
"memcmp requires trivially copyable type");
static_assert(std::has_unique_object_representations_v<T>,
"memcmp is unreliable for types with padding");
return std::memcmp(&a, &b, sizeof(T)) == 0;
}strerror and thread safety
strerror(errno) returns a pointer to a human-readable string describing the error code. The returned pointer points into a static buffer β on most implementations, a single global array. A concurrent call from another thread, or even a second call from the same thread, can overwrite that buffer before you have finished reading it.
#include <cstring>
#include <cerrno>
#include <cstdio>
// NOT thread-safe: two threads can race on the static buffer.
void bad_error_print(int err) {
std::fprintf(stderr, "Error: %s\n", std::strerror(err));
}POSIX strerror_r solves this by writing into a caller-supplied buffer. There are two incompatible signatures (XSI and GNU); the GNU version (default on Linux when _GNU_SOURCE is defined) returns a char* that may or may not point at the supplied buffer:
#include <cstring>
#include <cerrno>
// POSIX XSI strerror_r β portable on macOS, BSD, musl.
void thread_safe_strerror_xsi(int err) {
char buf[256];
if (strerror_r(err, buf, sizeof(buf)) == 0)
// buf now contains the message
;
}In C++11 and later, std::system_error with std::generic_category() is the idiomatic replacement. It allocates a std::string internally, is thread-safe, and integrates with the <system_error> error-code infrastructure:
#include <system_error> // C++11
#include <cerrno>
#include <iostream>
void modern_error_message(int posix_errno) {
// Constructs a std::system_error and extracts its .what() string.
std::error_code ec(posix_errno, std::generic_category());
std::string msg = ec.message(); // thread-safe, heap-allocated copy
std::cerr << "Error: " << msg << '\n';
}
// Or capture it directly after a failing system call:
void report_last_error() {
std::error_code ec(errno, std::generic_category());
throw std::system_error(ec, "open failed");
}Modern C++ Replacements
Every <cstring> function has a C++ equivalent that is safer, more composable, or both. The table below maps legacy functions to their preferred modern replacements.
<cstring> function | Modern C++ alternative | Notes |
|---|---|---|
strlen | std::string_view::size() | No O(n) scan; size tracked at construction |
strcpy | std::string assignment (s = src) | Manages allocation; throws on OOM |
strncpy | std::string(src, n) or snprintf | std::string constructor copies exactly n chars safely |
strcat | std::string::operator+= | Appends without overflow risk |
strncat | s.append(src, n) | Explicit length; no silent truncation |
strcmp | std::string::compare() or <=> (C++20) | Three-way comparison; works with string_view |
strncmp | s.compare(0, n, t) or strncmp | Still acceptable for prefix checks in hot paths |
strchr | std::string_view::find_first_of(c) | Returns position, not raw pointer |
strstr | std::string::find() / std::string_view::find() | Returns npos instead of nullptr |
strtok | std::views::split (C++20) or string_view loop | Thread-safe; non-destructive |
strerror | std::error_code::message() (C++11) | Thread-safe; returns std::string |
memcpy | std::copy / std::memcpy | memcpy still optimal for POD bulk copies |
memmove | std::copy / std::copy_backward | Prefer for typed arrays; compilers lower to memmove |
memset | Value initialisation = {} or std::fill | Type-safe; works for non-POD types |
memcmp | operator== | Handles padding correctly; inlinable |
Note on performance. std::memcpy and std::memmove remain the right tools for bulk copying raw byte buffers (network packets, file I/O staging buffers, serialisation). The std::string and std::vector alternatives introduce heap allocation that is inappropriate in many embedded or latency-sensitive contexts. Use the modern alternatives as the default and drop to the C functions only when profiling justifies it.
Best Practices
- Default to
std::string/std::string_viewfor new code. They carry their own length, manage allocation, and eliminate the entire class of null-terminator bugs. Reach for<cstring>at C API boundaries or in demonstrably hot paths. - Always compute destination capacity before calling
strcpyorstrcat. The length of the source is a runtime value β validate it against the destination size before writing. - Use
snprintfinstead ofstrcpy/strncpyfor safe bounded copies. It always null-terminates and returns the number of characters that would have been written, making truncation detectable. - Use
memcpyfor bulk copying of trivially copyable objects, not pointer casts followed by dereference. The latter violates the strict aliasing rule. - Never use
strtokin multithreaded or reentrant code. Preferstrtok_r(POSIX),strtok_s(MSVC/C11), or astd::string_view-based tokeniser. - Never use
strerrorin multithreaded code. Usestrerror_r, or better,std::error_code::message()from<system_error>. - Zero sensitive buffers explicitly and durably. A plain
memseton memory about to go out of scope may be optimised away by the compiler. UseSecureZeroMemory(Windows) orexplicit_bzero/memset_s(C11) for key material.
Common Pitfalls
Off-by-one on buffer sizes. strcpy(dest, src) writes strlen(src) + 1 bytes. A buffer of exactly strlen(src) bytes is one byte too small.
strncpy does not guarantee null-termination.
char buf[4];
std::strncpy(buf, "hello", 4); // buf = {'h','e','l','l'} β no '\0'!
buf[3] = '\0'; // required manuallymemcpy on overlapping regions is undefined behaviour. Even if it works on your platform today, it can silently break under link-time optimisation or a new compiler version. Use memmove when overlap is possible.
memcmp on structs with padding gives wrong results. Padding bytes are indeterminate; two logically equal structs can differ in those bytes. Either zero-initialise both before use or provide operator==. Use std::has_unique_object_representations_v<T> (C++17) as a compile-time guard.
Pointer comparison instead of value comparison. ptr1 == ptr2 compares addresses. Use std::strcmp(ptr1, ptr2) == 0 to compare the strings they point to.
strerror is not thread-safe. It returns a pointer into a static buffer that another thread or a subsequent call can overwrite. Use strerror_r (POSIX) or capture the result immediately into a std::string, or switch to std::error_code::message().
strlen on uninitialised arrays. An array that has been declared but not written to may contain no '\0' within its bounds; strlen will scan into adjacent memory, producing undefined behaviour and potential security issues.
memset on non-POD types corrupts the vtable. Zeroing an object with a virtual function table pointer via memset overwrites that pointer. Only ever use memset on trivially copyable types.
See Also
<string>βstd::stringandstd::basic_string; the idiomatic C++ replacement for most<cstring>use cases<string_view>(C++17) β non-owning view with built-in length tracking; eliminatesstrlenat API boundaries<charconv>(C++17) β locale-independent, allocation-free integer and floating-point β string conversions; supersedesatoi/strtol/strtod<bit>(C++20) βstd::bit_castreplaces thememcpytype-punning idiom with aconstexpr-capable, type-safe alternative<system_error>(C++11) βstd::error_codeandstd::system_error; the thread-safe, structured replacement forstrerror<algorithm>βstd::copy,std::copy_backward,std::fill; type-safe alternatives tomemcpy/memset<ranges>(C++20) βstd::views::split; lazy, non-destructive tokenisation replacingstrtok<cstdlib>βatoi,strtol,strtodfor C-style numeric parsing fromchar*