Cstring Functions
Header <cstring> provides null-terminated C-style string functions: strlen, strcpy, strcat, strcmp, and memory operations for interop with C code.
<cstring> Functionssince C++98Header <cstring> declares C-style string manipulation functions that operate on null-terminated character arrays, including length queries, copying, concatenation, and comparison.
Overview
The <cstring> header (C++ version of C's <string.h>) provides functions for working with C-style null-terminated strings. These functions assume strings end with a '\0' terminator that is not counted in logical length but is required for the functions to determine where a string ends.
Modern C++ strongly prefers std::string and std::string_view for most use cases, as they handle memory management and bounds automatically. However, C-style string functions remain essential when interfacing with C libraries, legacy code, or working with fixed-size buffers and performance-critical code where dynamic allocation must be minimized.
All functions in <cstring> are inherited from the C standard library and have been available since C++98.
Syntax
Key functions in <cstring>:
#include <cstring>
// Query length (reads until null terminator)
size_t strlen(const char* str);
// Copy string (no bounds checking; dangerous)
char* strcpy(char* destination, const char* source);
// Copy with length limit
char* strncpy(char* destination, const char* source, size_t count);
// Append string (no bounds checking; dangerous)
char* strcat(char* destination, const char* source);
// Append with length limit
char* strncat(char* destination, const char* source, size_t count);
// Compare strings (returns <0, 0, or >0)
int strcmp(const char* str1, const char* str2);
// Compare with length limit
int strncmp(const char* str1, const char* str2, size_t count);
// Fill memory with value
void* memset(void* ptr, int value, size_t count);
// Copy memory
void* memcpy(void* destination, const void* source, size_t count);Examples
Measuring and copying strings with bounds checking:
#include <cstring>
#include <iostream>
int main() {
const char* source = "Hello, World!";
size_t len = std::strlen(source);
// Safe copy: allocate room for string plus null terminator
char buffer[20];
if (len + 1 <= sizeof(buffer)) {
std::strcpy(buffer, source); // Now safe; buffer is large enough
std::cout << buffer << '\n';
}
return 0;
}Bounded copying with guaranteed null termination:
#include <cstring>
#include <iostream>
int main() {
const char* source = "This is a long string that might overflow";
char buffer[10];
// Copy only what fits; leave room for null terminator
std::strncpy(buffer, source, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Always ensure null termination
std::cout << buffer << '\n'; // Output: "This is a"
return 0;
}String comparison:
#include <cstring>
#include <iostream>
int main() {
const char* str1 = "apple";
const char* str2 = "apple";
if (std::strcmp(str1, str2) == 0) {
std::cout << "Strings are equal\n";
}
// Case-sensitive; "Hello" and "hello" are different
if (std::strcmp("Hello", "hello") != 0) {
std::cout << "Case matters in C-style strings\n";
}
return 0;
}Safe concatenation with capacity checks:
#include <cstring>
#include <iostream>
int main() {
char buffer[50] = "Hello";
const char* suffix = ", World!";
size_t buffer_size = sizeof(buffer);
size_t current_len = std::strlen(buffer);
size_t suffix_len = std::strlen(suffix);
// Check if concatenation fits
if (current_len + suffix_len < buffer_size) {
std::strncat(buffer, suffix,
buffer_size - current_len - 1);
}
std::cout << buffer << '\n'; // Output: "Hello, World!"
return 0;
}Best Practices
-
Always use bounded variants: Replace
strcpy()withstrncpy()andstrcat()withstrncat(). The count parameter prevents writing beyond the buffer. -
Account for the null terminator: When allocating space, reserve an extra byte. A 10-character string requires 11 bytes. When limiting operations, use
sizeof(buffer) - 1to leave room for'\0'. -
Guarantee null termination after
strncpy(): The function does not null-terminate if the source is too long. Always explicitly set the final byte to'\0'after callingstrncpy(). -
Prefer
std::stringin modern code: For new C++ projects, usestd::string, which handles allocation and bounds automatically. It is exception-safe and integrates with the standard library. -
Use
std::string_view(C++17+) for read-only references: When you need a non-owning string reference,std::string_viewis safer and more efficient thanconst char*. -
Understand buffer decay: When passing a character array to a function, it decays to a pointer; the function cannot determine the buffer size. Always pass the buffer size as a separate parameter.
Common Pitfalls
Buffer overflow from unbounded strcpy():
char buffer[5];
std::strcpy(buffer, "Hello, World!"); // UNDEFINED BEHAVIOR: writes past bufferMissing null termination after strncpy():
char buffer[5];
std::strncpy(buffer, "Hello, World!", 5);
// buffer contains 'H', 'e', 'l', 'l', 'o' — NO null terminator
std::cout << buffer; // UNDEFINED BEHAVIOR: reads past buffer looking for '\0'Confusing string length with buffer size: strlen() excludes the terminator. If you allocate strlen(s) bytes, you have no room for '\0'. Always allocate strlen(s) + 1.
Modifying string literals: String literals are read-only. Using them as destinations in strcpy() or other write functions produces undefined behavior:
const char* str = "Hello";
std::strcat(str, "!"); // UNDEFINED BEHAVIOR: literal is read-onlySize parameter ambiguity in strncat(): The count in strncat() is the maximum number of characters to append, not the buffer size. This differs from strncpy() and can lead to off-by-one errors.
The Danger Trio: strcpy, strcat, and gets
These three functions share a common flaw: they write to a destination buffer with no knowledge of how large that buffer is. All three have caused real-world security vulnerabilities — stack smashing exploits, heap overflows, and remote code execution bugs in production software.
strcpy
strcpy copies bytes from source to destination until it hits a null terminator, then copies the terminator. It has absolutely no length check.
#include <cstring>
// DANGEROUS: no bounds check
void process_name(const char* input) {
char buffer[16];
std::strcpy(buffer, input); // If input is 17+ chars: stack overflow
// An attacker controlling 'input' can overwrite the return address
}
// Safe replacement: check length first, then copy
void process_name_safe(const char* input) {
char buffer[16];
size_t input_len = std::strlen(input);
if (input_len >= sizeof(buffer)) {
// handle error — input is too long
return;
}
std::strcpy(buffer, input); // Now safe: we verified the length
}The safe idiom without a preliminary strlen call is to use snprintf (see below) or, on POSIX systems, strlcpy.
strcat
strcat appends source to the end of destination by scanning destination for its null terminator and then writing source bytes. It has the same missing-length-check problem as strcpy, compounded by the fact that it must first walk the entire destination string to find where to append — making it O(n) in the length of the existing content.
#include <cstring>
// DANGEROUS: both the initial value and the appended value can overflow
char buffer[16] = "Hello";
std::strcat(buffer, ", World! Extra!"); // Writes 21 chars into a 16-byte buffer
// Safe replacement: use strncat with remaining capacity
char buffer2[16] = "Hello";
size_t used = std::strlen(buffer2);
size_t remaining = sizeof(buffer2) - used - 1; // -1 for the null terminator
std::strncat(buffer2, ", World!", remaining); // Appends at most 'remaining' chars
// strncat always null-terminates when n > 0Note that strncat's third argument is the maximum number of characters to append — not the total buffer size. This is a common source of off-by-one errors when developers confuse it with strncpy.
gets (removed in C11 / C++14)
gets reads a line from stdin into a buffer. It provides no way to specify the buffer size at all — the function signature is simply char* gets(char* s). It was deprecated in C99, removed from C11, and removed from C++14. Any code still using it should be treated as critically unsafe.
// DO NOT USE — gets was removed from the standard
char buffer[64];
gets(buffer); // Removed in C++14; UB risk; replace unconditionally
// Replacement: fgets with explicit size
char buffer2[64];
if (std::fgets(buffer2, sizeof(buffer2), stdin) != nullptr) {
// fgets preserves the newline '\n'; strip it if needed
size_t len = std::strlen(buffer2);
if (len > 0 && buffer2[len - 1] == '\n') {
buffer2[len - 1] = '\0';
}
}The strncpy Pitfall: Null Termination Is Not Guaranteed
strncpy is often presented as the "safe" version of strcpy, but it has a subtle and dangerous behavior: if the source string is at least as long as the count parameter, no null terminator is written to the destination. The buffer is left without a terminator, and any subsequent string operation that reads until '\0' produces undefined behavior.
#include <cstring>
char dest[5];
std::strncpy(dest, "Hello, World!", 5);
// dest now contains: {'H', 'e', 'l', 'l', 'o'} — no null terminator
// Calling strlen(dest) or printing dest is undefined behavior
// The mandatory manual fix:
dest[sizeof(dest) - 1] = '\0'; // Always required after strncpy when source may be >= nA secondary quirk: when source is shorter than count, strncpy zero-pads the remaining bytes up to count. This is useful for fixed-width fields but wastes time on large buffers.
Safe Alternatives to strncpy
strlcpy (BSD / POSIX): Copies at most size - 1 characters and always null-terminates. Returns the total length of the source string (not the number of bytes written), so callers can detect truncation by comparing the return value to the buffer size.
// strlcpy is available on macOS, BSDs, and most Linux libc implementations
// Requires <string.h> or a compatibility header
char dest[16];
size_t src_len = strlcpy(dest, source, sizeof(dest));
if (src_len >= sizeof(dest)) {
// Truncation occurred; src_len is the length of the full source string
}strncpy_s (C11 Annex K / Windows MSVC): A bounds-checked variant that additionally accepts a destination buffer size and invokes a constraint handler (by default, terminates the program) if the destination is too small. It always null-terminates.
// strncpy_s is available on Windows (MSVC) and with __STDC_LIB_EXT1__ support
char dest[16];
errno_t err = strncpy_s(dest, sizeof(dest), source, _TRUNCATE);
// err == STRUNCATE if source was truncated; 0 on successFor portable C++ code that cannot rely on either extension, the idiom snprintf(dest, sizeof(dest), "%s", source) is widely available and always null-terminates.
memcpy vs memmove: Overlapping Regions
memcpy and memmove both copy a block of bytes, but they differ critically in how they handle overlapping source and destination regions.
memcpy
memcpy is permitted by the standard to assume source and destination do not overlap. Implementations often exploit this by copying in large chunks (SIMD registers, 64-bit words) in any direction or order that is fastest on the target hardware. If the regions overlap, the behavior is undefined.
#include <cstring>
char buf[20] = "Hello, World!";
// SAFE: non-overlapping regions
char dest[20];
std::memcpy(dest, buf, 14); // dest is entirely separate from buf
// UNDEFINED BEHAVIOR: overlapping copy — source and dest share memory
std::memcpy(buf + 2, buf, 10); // UB! Overlapping regionsmemmove
memmove is defined to behave correctly even when source and destination overlap. The standard library typically implements this by checking direction and copying from the tail when necessary to avoid clobbering bytes before they are read.
// SAFE: memmove handles overlap correctly
std::memmove(buf + 2, buf, 10); // Shifts "Hello, Wor" right by 2 — always correctPerformance: memcpy is often measurably faster than memmove because the implementation need not check for overlap or adjust copy direction. When you know definitively that the regions do not overlap (separate allocations, distinct stack variables), prefer memcpy. When shifting bytes within the same buffer or when the relationship between pointers is uncertain, use memmove.
A practical rule: in generic utility functions that receive arbitrary pointers, always use memmove. Reserve memcpy for hot paths where non-overlap is proven.
C++ Alternatives
Modern C++ code rarely needs <cstring> functions directly. The standard library provides safer, higher-level tools for every common use case.
std::string and std::string_view
std::string eliminates the entire class of C-string bugs: it owns its buffer, grows automatically, and every member function is bounds-checked in debug builds. std::string_view (C++17) provides a non-owning reference to any contiguous character sequence — a std::string, a literal, or a char array — without allocation.
#include <string>
#include <string_view>
// Replacing strcpy/strcat:
std::string greeting = "Hello";
greeting += ", World!"; // Automatic reallocation; no overflow possible
// Replacing strcmp:
if (greeting == "Hello, World!") { /* ... */ }
// Replacing strncpy for read-only inspection:
std::string_view sv = greeting;
std::string_view prefix = sv.substr(0, 5); // No allocation; zero-copy viewstd::copy and std::copy_n for Byte Arrays
When working with raw byte buffers (e.g., protocol parsing, binary serialization), std::copy and std::copy_n from <algorithm> replace memcpy with type-safe, iterator-based copies.
#include <algorithm>
#include <cstdint>
uint8_t src[64] = { /* ... */ };
uint8_t dst[64];
// Replace memcpy:
std::copy(std::begin(src), std::end(src), std::begin(dst));
// Replace memcpy with explicit count:
std::copy_n(src, 32, dst);std::copy does not produce UB for overlapping ranges (it calls memmove semantics internally when the iterator category permits), but it is not equivalent to memmove in all cases. When overlap is a concern with raw pointers, still prefer std::memmove.
std::span<char> for Non-Owning Buffers (C++20)
std::span (C++20) wraps a pointer and a size into a single, non-owning view. It is the idiomatic replacement for the (char* buf, size_t n) parameter pair that C-string APIs require.
#include <span>
#include <algorithm>
void fill_header(std::span<char> buf) {
// buf.size() is always available — no separate size parameter needed
std::fill(buf.begin(), buf.end(), '\0');
std::copy_n("HDR", 3, buf.data());
}
char buffer[128];
fill_header(buffer); // Array decays to span automaticallysnprintf vs sprintf, and std::format
sprintf
sprintf writes formatted output to a character buffer with no length limit. It is unsafe for the same reason as strcpy: if the formatted result is longer than the buffer, it overflows.
char buf[16];
std::sprintf(buf, "Value: %d", 1234567890); // May overflow buf silentlysnprintf
snprintf accepts an explicit buffer size. It writes at most size - 1 characters, always null-terminates (when size > 0), and returns the number of characters that would have been written had the buffer been large enough — not the number actually written. This return value is the key to the "pre-calculation" idiom:
#include <cstdio>
// Determine required size before allocating:
int needed = std::snprintf(nullptr, 0, "x=%d y=%d", x, y);
// needed does NOT include the null terminator
std::string result(needed, '\0');
std::snprintf(result.data(), needed + 1, "x=%d y=%d", x, y);
// Truncation detection:
char small[8];
int written = std::snprintf(small, sizeof(small), "long string %d", 99);
if (written >= static_cast<int>(sizeof(small))) {
// Output was truncated; 'written' tells us the full untruncated length
}Passing nullptr as the destination with size 0 is explicitly defined behavior — snprintf performs the formatting calculation without writing any bytes, returning the required length. This lets callers allocate exactly the right buffer before a second call.
std::format (C++20)
std::format replaces snprintf for new code. It is type-safe, never overflows, raises compile-time errors for mismatched format specifiers, and returns a std::string directly.
#include <format>
std::string s = std::format("x={} y={}", x, y); // No buffer size arithmetic
std::string hex = std::format("0x{:08X}", value); // Hex with zero-padding
std::string fixed = std::format("{:.3f}", 3.14159); // 3 decimal placesFor C++23, std::print and std::println write formatted output directly to a file or stdout without creating an intermediate std::string.
Locale-Dependent Functions and Character Classification
strcoll and strxfrm
strcmp compares strings by raw byte values (lexicographic order of char values). This is fast but wrong for natural language sorting — "café" will sort before "cable" on some encodings, and accented characters may sort inconsistently across locales.
strcoll compares strings according to the current locale's collation rules. strxfrm transforms a string into a form where strcmp produces the same result as strcoll would — useful when comparing the same string against many others (transform once, compare many times cheaply).
#include <cstring>
#include <clocale>
std::setlocale(LC_COLLATE, ""); // Use environment locale
int result = std::strcoll("café", "cable"); // Locale-aware comparison
// result < 0 if "café" sorts before "cable" in the current locale
// strxfrm for repeated comparisons:
char transformed[256];
std::strxfrm(transformed, "café", sizeof(transformed));
// Now strcmp(transformed, other_transformed) == strcoll("café", other_original)tolower / toupper: The Signed char UB
tolower and toupper from <cctype> take an int argument, but the standard specifies that the value must be representable as unsigned char or must equal EOF. On platforms where char is signed (the common case on x86), passing a char directly to these functions is undefined behavior for any character with a value above 127 — which includes all accented Latin characters.
#include <cctype>
// UNDEFINED BEHAVIOR on platforms where char is signed:
char c = '\xe9'; // 'é' in Latin-1, has value 233 as unsigned, -23 as signed char
std::tolower(c); // UB: value is negative, not EOF
// CORRECT: cast through unsigned char first
std::tolower(static_cast<unsigned char>(c)); // Always defined
// Common idiom for a whole string:
std::string s = "Héllo";
for (char& ch : s) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
}C++ Locale Alternatives
The C++ <locale> header provides std::locale and the std::ctype facet as type-safe, object-oriented replacements for the C locale functions. Unlike setlocale, which is global and not thread-safe, std::locale objects can be created per-thread or per-stream.
#include <locale>
#include <string>
std::locale loc("en_US.UTF-8");
const std::ctype<char>& ct = std::use_facet<std::ctype<char>>(loc);
char c = 'A';
char lower = ct.tolower(c); // Locale-aware, no UB, not global state
// Locale-aware collation via std::collate:
const std::collate<char>& coll = std::use_facet<std::collate<char>>(loc);
int order = coll.compare("café", "café" + 4, "cable", "cable" + 5);For Unicode-aware text processing, the <locale> facets are themselves limited to single-byte encodings unless combined with wide characters or a library such as ICU. For serious multilingual work, <locale> is a stepping stone, not a final destination.
See Also
<string>— Modernstd::stringclass with automatic memory managementstd::string_view— Non-owning read-only string reference (C++17)<vector>— Dynamic arrays as safer alternative to manual buffersstd::char_traits— Character type operations and comparison