Why std::string Exists
C had no string type. What it had was an array of char with a zero byte marking the end — the so-called null-terminated string. You allocated the array yourself, you had to know in advance how big it needed to be, and finding the length meant walking the array until you hit the zero. Almost every famous security vulnerability of the 1990s and 2000s traces back to somebody writing more characters into such an array than it had room for.
std::string is the fix. It is a class that owns a buffer of characters and manages that buffer for you. When you append to it and the buffer is full, it quietly allocates a bigger one, copies the contents across, and frees the old one. When the string goes out of scope, its destructor releases the memory. You never call malloc, you never call free, and you cannot forget to.
It also knows its own length, in constant time, because it stores it. That single fact turns a lot of O(n) operations into O(1) ones and removes a whole family of off-by-one errors. And it composes: strings can go inside a std::vector, be returned from functions, be compared with ==, and be sorted, all without you writing a single line of memory management.
You still meet C-style strings, because string literals like "hello" are one — their type is const char*. That is fine; std::string constructs from them implicitly, which is why std::string s = "hello"; just works. The rule to follow is simple: accept and store std::string, and only drop to const char* when an old C API demands it, using .c_str().
#include <iostream>
#include <string>
int main() {
std::string name = "Ananya"; // constructed from a literal
std::string greeting = "Hello, " + name + "!";
std::cout << greeting << '\n'; // Hello, Ananya!
std::cout << name.size() << '\n'; // 6 (length() is the same function)
std::cout << name.empty() << '\n'; // 0
std::cout << name.front() << name.back() << '\n'; // Aa
// Handing a std::string to an old C API
// std::FILE* f = std::fopen(path.c_str(), "r");
// The C way, for contrast — you manage everything
const char* literal = "Ananya"; // 7 bytes: 6 chars + a trailing zero byte
std::cout << literal << '\n';
} size()andlength()on astd::stringare the same function with two names.length()reads better for text;size()matches every other standard container, so generic code uses it.
Searching and Slicing
find returns the index where a substring starts. The important part is what it returns when the substring is not there: a special value called std::string::npos. Beginners frequently test if (pos == -1) or if (pos < 0), and both are wrong, because npos is an unsigned value — the largest one the type can hold. Compare against std::string::npos explicitly and the check is correct and self-documenting.
substr(pos, count) takes a starting index and a count of characters, not an end index. This trips up people coming from Python, where s[2:5] uses an end index. Leaving out the count gives you everything from pos to the end. If pos is past the end of the string, substr throws std::out_of_range; a count that runs off the end is fine and simply stops there.
For reading a single character you have two options with different personalities. s[i] does no bounds checking at all — an out-of-range index is undefined behaviour, which may print rubbish or crash. s.at(i) checks and throws std::out_of_range. Use at() when the index came from outside your control, such as user input or a file, and [] inside a loop you have already bounded, where the check would be pure cost.
Splitting a string on a delimiter comes up constantly — CSV rows, comma-separated tags, command input. There is no built-in split in the standard library, and the idiomatic answer is a std::istringstream with std::getline, shown below.
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
std::string full = "Ananya Sharma";
std::size_t pos = full.find("Sharma");
if (pos != std::string::npos) {
std::cout << "found at " << pos << '\n'; // found at 7
}
if (full.find("Gupta") == std::string::npos) {
std::cout << "not present\n";
}
std::cout << full.substr(0, 6) << '\n'; // Ananya (count, not end index)
std::cout << full.substr(7) << '\n'; // Sharma (to the end)
std::cout << full[0] << '\n'; // A — unchecked
std::cout << full.at(1) << '\n'; // n — throws if out of range
// Splitting on a delimiter
std::string csv = "maths,physics,chemistry";
std::vector<std::string> subjects;
std::istringstream ss(csv);
std::string token;
while (std::getline(ss, token, ',')) {
subjects.push_back(token);
}
std::cout << subjects.size() << '\n'; // 3
} - Because
nposis unsigned, the shortcutif (full.find(x) >= 0)is always true and the shortcutif (full.find(x) == -1)is a signed/unsigned comparison your compiler will warn about. There is no shortcut worth the risk here — write outstd::string::npos.
Modifying and Comparing
Strings are mutable. += and append add to the end, push_back adds one character, insert(pos, text) puts text at an index, erase(pos, count) removes a run of characters, and replace(pos, count, text) swaps a run for something else. All of these positions are the same zero-based indices find returns, so the two sets of functions fit together naturally.
There is a performance point hiding here that matters more than it looks. Every append may force a reallocation: a new, larger buffer, a copy of everything, and the old buffer freed. Standard implementations grow geometrically so this averages out well, but building a long string one character at a time inside a loop still does real work. If you know roughly how long the result will be, call reserve(n) first and the growth happens once.
Comparison is where std::string is much nicer than C. == compares contents, and < compares lexicographically, character by character by character code. That gives you dictionary order for plain ASCII text, so sorting a vector of names works with no extra effort. Note that this is case-sensitive and based on character codes, which in ASCII put every uppercase letter before every lowercase one — so "Zebra" < "apple" is true. If you want case-insensitive comparison, convert both sides to one case first.
The trap to avoid: in C, comparing strings with == compared pointers rather than text, which is why strcmp exists. If you ever store text as const char* and compare two of them with ==, you are back to comparing addresses, and it will appear to work for identical literals and then fail on runtime-built strings. Keeping everything in std::string removes the question entirely.
#include <algorithm>
#include <cctype>
#include <iostream>
#include <string>
int main() {
std::string name = "Ananya";
name += " Sharma"; // "Ananya Sharma"
name.insert(6, " R."); // "Ananya R. Sharma"
name.erase(6, 3); // "Ananya Sharma"
name.replace(7, 6, "Verma"); // "Ananya Verma"
name.push_back('!'); // "Ananya Verma!"
std::cout << name << '\n';
// Reserve when you know the rough size
std::string report;
report.reserve(1000);
for (int i = 0; i < 100; ++i) report += "row\n";
// Comparison compares contents
std::string a = "apple", b = "banana";
if (a < b) std::cout << a << " comes first\n";
if (a == "apple") std::cout << "exact match\n";
// Case-insensitive: normalise both sides first
std::string input = "YES";
std::transform(input.begin(), input.end(), input.begin(),
[](unsigned char c) { return std::tolower(c); });
if (input == "yes") std::cout << "confirmed\n";
} - The
(unsigned char)in thattolowerlambda is not decoration.std::toloweris inherited from C and its behaviour is undefined for negative arguments, which a plaincharcan be on many platforms for non-ASCII bytes. Converting tounsigned charfirst is the standard fix.
Reading Strings, and Converting To and From Numbers
There are two ways to read text, and choosing the wrong one is responsible for more confused beginners than any other input problem in C++. std::cin >> word reads a single whitespace-delimited token: it skips leading whitespace, reads until the next space or newline, and stops. std::getline(std::cin, line) reads everything up to and including the newline, giving you the whole line, spaces included. If you ask for a full name with >>, you get the first name only and the surname sits in the buffer confusing your next read.
The famous trap is mixing the two. std::cin >> age; reads the digits and leaves the newline in the buffer, because >> stops at whitespace rather than consuming it. The very next std::getline sees that leftover newline immediately, decides the line is over, and hands you an empty string without waiting for you to type anything. It looks exactly like the program skipped your input. The fix is to discard the rest of the line after a >> read, using std::cin.ignore with a large count and '\n' as the delimiter.
For converting text to numbers, std::stoi, std::stol, std::stoll and std::stod take a std::string and return the number. They throw rather than returning a silent zero, which is what you want: std::invalid_argument if the text does not begin with a number at all, and std::out_of_range if the value will not fit. One behaviour to know: they parse the leading numeric part and ignore whatever follows, so std::stoi("42kg") returns 42 without complaint. If you need to reject trailing junk, ask for the second parameter, which tells you how many characters were consumed.
Going the other way, std::to_string turns any built-in number into a string. It is convenient and slightly blunt — std::to_string(3.14) gives "3.140000", because it formats with six digits after the decimal point. When you need control over the formatting, build the string with a std::ostringstream and the manipulators from <iomanip> instead.
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
int main() {
int age = 0;
std::cout << "Enter age: ";
std::cin >> age;
// Throw away the leftover newline, or the getline below reads nothing
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::string fullName;
std::cout << "Enter full name: ";
std::getline(std::cin, fullName); // keeps the spaces
// Text to number — these throw on bad input
try {
int marks = std::stoi("87");
double rate = std::stod("3.5");
std::size_t used = 0;
int partial = std::stoi("42kg", &used); // 42, used == 2
std::cout << marks << ' ' << rate << ' ' << partial << '\n';
} catch (const std::invalid_argument&) {
std::cout << "that was not a number\n";
} catch (const std::out_of_range&) {
std::cout << "that number is too large\n";
}
// Number to text
std::string plain = std::to_string(3.14); // "3.140000"
std::ostringstream out;
out << std::fixed << std::setprecision(2) << 3.14159;
std::string neat = out.str(); // "3.14"
std::cout << plain << ' ' << neat << ' ' << fullName << ' ' << age << '\n';
} - If the user types letters where you asked for a number,
std::cin >> agefails, leavesageat zero and puts the stream into a failed state, after which every subsequent read is skipped and your loops spin forever. Recover withstd::cin.clear();followed by the sameignorecall, then ask again.
