C++ String to Int, Int to String and Char Arithmetic
Converting text to numbers and back in C++: stoi and its exceptions, the non-throwing charconv parser, char digit arithmetic and the cctype unsigned char cast.
- Course: C++ study plan
- Module: Strings
- Kind: Lesson
- Reading time: 14 min
- Runtime: Clang 18 · C++20
How do you convert a string to an int in C++?
Use std::stoi(s) from <string>: it skips leading whitespace, reads an optional sign and digits, and returns the int. It throws std::invalid_argument when there are no digits and std::out_of_range when the value does not fit, and it ignores trailing text, so std::stoi("42abc") is 42. C++17's std::from_chars parses without exceptions and reports what it consumed.
Lesson
A char is a number that the stream prints as a letter. Hold on to that and everything here follows: why '7' - '0' is 7, why c + 1 is an int, why <cctype> needs an unsigned char cast most tutorials omit, and how text becomes a number and back — std::to_string, the std::stoi family that throws, and C++17's std::from_chars that does not. Parsing a token that might not be a number is where text programs fail their hidden cases; this lesson gives two correct ways to do it.
char is a small integer
char c = 'A';
std::cout << c << ' ' << static_cast<int>(c) << ' ' << c + 1 << '\n'; // A 65 66
std::cout << static_cast<char>(c + 1) << '\n'; // B
'A' is the integer 65 with type char. Arithmetic promotes a char to int (Module 2, Integers and bits), so c + 1 is the int 66 and prints as digits; cast back to char to print a letter. On this platform char is signed, −128 to 127, so bytes above 127 — present the moment input is not ASCII — are negative. That fact is behind the cast in the next section.
cctype and the unsigned char cast
| Function | True for |
|---|---|
std::isdigit(c) | '0'–'9' |
std::isalpha(c) | letters |
std::isalnum(c) | letters or digits |
std::isspace(c) | space, tab, newline, carriage return, form feed, vertical tab |
std::isupper(c) / std::islower(c) | capitals / lower case |
std::ispunct(c) | printable, not alphanumeric, not space |
std::toupper(c) / std::tolower(c) | returns the converted character (as int) |
Every function takes and returns an int, and the argument must be representable as unsigned char or be EOF. A negative char is neither, and passing one is undefined behaviour — a crash on some libraries, silently wrong on others. Cast first:
#include <cctype>
bool is_digit(char c) { return std::isdigit(static_cast<unsigned char>(c)) != 0; }
char upper(char c) { return static_cast<char>(std::toupper(static_cast<unsigned char>(c))); }
Two casts for the case functions: unsigned char on the way in, char on the way out because they return int. Wrap them once in helpers and the rest of the program stays clean. The classification functions return a non-zero int, not bool — hence the != 0.
Digit and letter arithmetic
int d = '7' - '0'; // 7 - the digits are consecutive codes 48..57
char c = static_cast<char>('0' + 3); // '3'
int index = 'k' - 'a'; // 10 - letters are consecutive too (in ASCII)
char shifted = static_cast<char>('a' + (index + 3) % 26); // 'n' - a Caesar shift
Digit characters have consecutive codes, so c - '0' converts a digit to its value and '0' + d converts back. The same holds for 'a'–'z' and 'A'–'Z', which is how a letter becomes an index into a 26-element count array (count[c - 'a']). Check with isdigit/isalpha before subtracting — 'x' - '0' is 72 and nobody will stop you using it.
Building a number by hand is the loop every parser reduces to:
long long value = 0;
for (char c : text) {
if (!is_digit(c)) break;
value = value * 10 + (c - '0');
}
Use long long for the accumulator: ten digits overflow int, and signed overflow is undefined behaviour (Module 2). The hand-rolled loop also shows why the library functions matter — sign, whitespace, overflow and trailing text all need handling.
Number to text: std::to_string
std::string a = std::to_string(42); // "42"
std::string b = std::to_string(-7LL); // "-7"
std::string c = std::to_string(3.5); // "3.500000" - always six decimals, like printf's %f
std::string d = std::to_string(1e20); // "100000000000000000000.000000"
Integers convert exactly as printed. For floating point std::to_string behaves like %f — six decimals, not adjustable, enormous for large values — so use std::format("{:.2f}", x) (lesson 5) or a stream with std::setprecision whenever the format matters.
Text to number: the stoi family
#include <string>
int i = std::stoi("42"); // 42
int j = std::stoi(" -17xyz"); // -17: leading whitespace skipped, parsing stops at 'x'
long l = std::stol("123456789012");
long long ll = std::stoll("-9000000000000000000");
double d = std::stod("2.5e3"); // 2500.0
std::size_t used = 0;
int k = std::stoi("123abc", &used); // k = 123, used = 3 - how many characters were consumed
std::stoi, stol, stoll, stoul, stoull, stof, stod and stold wrap C's strtol family: they skip leading whitespace, accept a sign, parse the longest valid prefix and ignore the rest. The optional second argument receives the count of characters consumed — the only way to notice trailing junk. Two things go wrong loudly:
try {
int v = std::stoi(token);
std::cout << v << '\n';
} catch (const std::invalid_argument&) { // no digits at all: "abc", "", " "
std::cout << "not a number\n";
} catch (const std::out_of_range&) { // digits, but the value does not fit an int
std::cout << "too big\n";
}
An uncaught exception ends the program with a non-zero exit code — a runtime error on the judge, not a wrong answer — so any stoi on input you do not control sits inside a try. Module 15, Errors and exceptions, covers the mechanism; here the two catch clauses are enough.
Text to number without exceptions: std::from_chars
#include <charconv>
#include <string>
#include <system_error>
bool parse_int(const std::string& text, int& out) {
const char* first = text.data();
const char* last = first + text.size();
auto [ptr, ec] = std::from_chars(first, last, out);
return ec == std::errc() && ptr == last; // no error, and the whole token was consumed
}
std::from_chars (C++17, <charconv>) parses a character range into a number and reports through a result struct: ptr points just past what it consumed, and ec is std::errc() on success, std::errc::invalid_argument when no number starts at first, or std::errc::result_out_of_range when the digits are valid but the value does not fit. It never throws, never allocates, skips no whitespace and accepts no + — only an optional - for signed targets. Checking ptr == last turns "starts with a number" into "is a number", the check the exercises need. It is the fastest conversion in the library, and because it takes a pointer range it works on a std::string_view (lesson 6) too, so a token sliced from a line parses without a copy. std::to_chars is the mirror image; std::format is usually more convenient.
std::stoi | std::from_chars | |
|---|---|---|
| Leading whitespace | skipped | error |
+ sign | accepted | error |
| Trailing text | ignored (see the pos argument) | reported through ptr |
| Not a number | throws invalid_argument | ec == errc::invalid_argument |
| Too large | throws out_of_range | ec == errc::result_out_of_range |
| Base | argument (default 10) | argument (default 10) |
Pitfalls
std::isdigit(c)on a plaincharis undefined behaviour for bytes over 127; cast tounsigned charfirst.std::toupperreturnsint; assigning to acharwithout a cast is a silent narrowing, so cast explicitly.std::stoi("42abc")is 42, not an error; check the consumed count or usefrom_charswithptr == last.std::stoi("")andstd::stoi("abc")throwstd::invalid_argument;std::stoi("99999999999")throwsstd::out_of_range; an uncaught throw is a runtime error on the judge.std::from_charsrejects" 42"and"+42"; trim first and decide whether a+is allowed.
Key takeaways
charis an integer; arithmetic promotes toint, and'0' + d/c - '0'convert between digit characters and values.- Every
<cctype>call takesstatic_cast<unsigned char>(c); the case functions returnint. std::to_stringis exact for integers and fixed at six decimals for floating point.- The
stoifamily skips whitespace, parses a prefix and throws on failure; catchinvalid_argumentandout_of_range. std::from_charsreports throughptrandec, never throws, andptr == lastmeans the whole token was a number.
Common questions
What is the difference between stoi and from_chars?
std::stoi skips leading whitespace, accepts a +, ignores trailing text and throws on failure. std::from_chars (C++17, <charconv>) skips nothing, accepts only a - sign, never throws or allocates, and reports through ptr and ec; checking ec == std::errc() and ptr == last confirms the whole token was a number.
How do you convert a char digit to an int in C++?
Subtract the character zero: '7' - '0' is 7, because the digit characters have consecutive codes, 48 to 57. The reverse is static_cast<char>('0' + d). Check with std::isdigit first — 'x' - '0' is 72, and nothing stops you using it.
Why cast to unsigned char before calling isdigit or toupper?
The <cctype> functions require an argument representable as unsigned char or equal to EOF. char is signed on x86-64 Linux, so any byte above 127 arrives as a negative number, and passing it is undefined behaviour. Write std::isdigit(static_cast<unsigned char>(c)), and cast std::toupper's int result back to char.
How do you convert an int to a string in C++?
std::to_string(42) returns "42" and is exact for every integer type. For floating point it always prints six decimals, like printf's %f — std::to_string(3.5) is "3.500000" — so use std::format("{:.2f}", x) or a stream with std::setprecision when the format matters.
Why does c + 1 print a number instead of a letter?
A char is a small integer, and arithmetic promotes it to int, so 'A' + 1 is the int 66 and prints as digits. Cast the result back to print a letter: static_cast<char>(c + 1) prints B.
Exercises
Classify and swap
Read one line. Using <cctype> with the unsigned char cast, count its letters (isalpha), digits (isdigit), spaces (isspace) and other characters, then print the line with every letter's case swapped (capitals become lower case and lower case becomes capitals); every other character is unchanged.
Input: one line (possibly empty, possibly with leading or trailing spaces). Output: letters=<a> digits=<b> spaces=<c> other=<d> on the first line, the swapped text on the second.
Example: input Hello, World 42! → output
letters=10 digits=2 spaces=2 other=2
hELLO, wORLD 42!Parse the numbers
Read lines until end of input. Each line holds one token, possibly with spaces before or after it; blank lines are skipped. Trim the line, then decide what the token is:
- a valid
int— an optional-followed only by digits, and a value that fits inint— prints<token> -> <value>and is added to a runninglong longsum; - digits that do not fit in an
intprint<token> -> out of range; - anything else (letters, a
+sign, a decimal point, a space inside the token, a lone-) prints<token> -> invalid.
After the last line print sum=<sum>. Use std::from_chars and require that it consumed the whole token (ptr == last); if you prefer std::stoi, reject the + sign and trailing text yourself and catch both exceptions — an uncaught one is a runtime error.
Input: any number of lines. Output: one line per non-blank input line, then the sum.
Example: input
42
-17
abc
2147483648
→ output
42 -> 42
-17 -> -17
abc -> invalid
2147483648 -> out of range
sum=25