Why this matters
A char is the C++ type for one character, such as a letter, a digit or a punctuation mark. A program that reads text works with char values one at a time. It might check whether a character is a lower-case letter, change an upper-case letter to lower case, count how many times each letter appears, or move each letter forward by a fixed amount in a simple secret code.
These tasks use arithmetic on characters. The idea that makes them work is simple: a char is also a small whole number. The letter a is stored as the number 97, the letter b as 98, and so on. Once you know this, adding 1 to a letter gives the next letter, and subtracting one letter from another gives the distance between them. This lesson shows how to use that fact safely.
The lesson uses the number and letter codes of the usual character set, which is ASCII. Every letter, digit and punctuation mark on a common keyboard has a fixed number in ASCII. The examples were run with the GNU C++ compiler, g++ 15.2, on 64-bit Linux. Compile a file with:
g++ -std=c++20 -Wall -o demo demo.cpp
./demoThe lesson about literals and number bases in this module explains how the spellings such as 0x41 relate to these numbers. Overflow, where a value is too big for its type, is covered in the two's complement lesson.
A char is a small number
A character literal is a single character written between single quotes, such as 'a'. The compiler stores each character literal as a number, which is called its character code. The character code is the position of the character in the character set. The program below shows the code of each character in a short word.
The output is:
H has code 72
i has code 105
! has code 33The for (char ch : word) loop visits each character of the string in turn. The static_cast<int> part converts a char to an int, so that the program prints the number instead of the letter. The static_cast keyword is the way to ask for a conversion in C++ when you want it written in the code.
The codes are not random. Digits come first, then the upper-case letters, then the lower-case letters, with a gap between the last upper-case letter and the first lower-case letter. The table shows a few codes from the usual character set.
| Character | Code | Notes |
|---|---|---|
' ' | 32 | The space character. |
'0' | 48 | The first digit. The digits '0' to '9' have codes 48 to 57. |
'A' | 65 | The first upper-case letter. |
'a' | 97 | The first lower-case letter. |
Two facts about char matter for the rest of this lesson. First, sizeof('a') is 1 in C++, so a char is always one byte. Second, a char is an integer type, and you can use it in arithmetic like any other integer. Whether a plain char can hold negative numbers depends on the compiler. On this system it can, which is shown in the mistakes section.
Adding to a char
Because a char is a number, you can add to it. The next program starts with the letter a and shows what adding 1 does.
The output is:
letter + 1 = 98
as a letter: b
after letter++: bThe first line prints 98, not b. The expression letter + 1 is an int. C++ converts a char to an int before doing arithmetic on it, which is called integer promotion. The sum is the number 98, and std::cout prints a number as digits. To get the letter back, the second line converts the result with static_cast<char>. The third line uses letter++, which adds 1 to the variable itself and stores the result. Afterwards the variable holds b.
The same idea works with any offset. The next example builds a letter from a number. The letter A has code 65, so adding 25 gives code 90, which is Z.
The output is:
Z
mThe second line uses a fact about the character set. In ASCII, each lower-case letter has a code 32 larger than the matching upper-case letter. So 'M' + 32 is the code of m. This works for ASCII, but a program that must run on every system should use the standard library function std::tolower from the <cctype> header, which does not depend on the exact codes.
The same idea changes the case of a whole string. The next program walks through every character, and changes only the letters that are in the range it is looking for. The first loop makes the text upper case. The second loop makes it lower case again.
The output is:
HELLO, WORLD 2026!
hello, world 2026!The loop variable is char&, a reference. The ampersand means the loop changes the character inside the string, not a copy of it. The digits, the comma, the space and the exclamation mark fail the range test, so they are left as they are. The expression ch - 'a' + 'A' moves a lower-case letter to the matching upper-case letter, because it takes the offset from 'a' and adds it to 'A'. The second loop does the reverse by adding the difference between 'a' and 'A', which is 32.
Comparing chars
A character test compares a char with another character. In ASCII the letters are in order. The lower-case letters run from 'a' to 'z' with no gaps, and the upper-case letters run from 'A' to 'Z'. Because of this, a letter is in the range from 'a' to 'z' when its code is at least the code of 'a' and at most the code of 'z'. The check is written with the comparison operators.
The output is:
A lower 0, upper 1, digit 0
b lower 1, upper 0, digit 0
3 lower 0, upper 0, digit 1
? lower 0, upper 0, digit 0
z lower 1, upper 0, digit 0Each check is a plain comparison between two char values. There is no need to look up the codes. The expression ch >= '0' && ch <= '9' is true for every digit character, because the digits '0' to '9' are also in order.
The standard library has ready-made tests in the <cctype> header, such as std::isalpha and std::islower. They return a non-zero number when the test is true. Their argument must be a value that fits in unsigned char, so the usual form is std::isalpha(static_cast<unsigned char>(ch)). The hand-written comparisons above show what those tests do, and they are useful when you need the exact range.
Counting letters with offsets from 'a'
The most common use of char arithmetic is counting. The program needs one counter for each letter. Make an array of 26 counters, and let the position in the array stand for the letter. Position 0 counts a, position 1 counts b, and position 25 counts z.
To find the position of a letter, subtract 'a' from it. The code of 'a' is 97, so 'c' minus 'a' is 99 minus 97, which is 2. Position 2 is the counter for c, which is the third letter. This is the offset from 'a', the distance of a letter from the first letter of the alphabet.
The next program counts the letters in a sentence. Upper-case and lower-case letters both go into the same counter, so H and h count together.
The output is:
d: 1
e: 1
h: 1
l: 3
o: 2
r: 1
w: 1The two if branches are needed because upper-case and lower-case letters have different codes. Each branch subtracts the code of its own first letter, 'a' or 'A', so both give a position from 0 to 25. The digits 1, 2 and 3, the comma, the space and the exclamation mark fail both tests and are ignored.
The second loop turns a position back into a letter. The expression 'a' + i gives the code of the letter at position i, and static_cast<char> turns that code into a character for printing. The loop prints only the letters that appeared, so the output is sorted alphabetically.
Offsets from 'a' are also useful for shifting letters. The next program moves each letter forward by a fixed number of places and wraps back to a after z. This is the idea behind the Caesar cipher, a very old and very weak secret code. The wrap-around uses the remainder operator %.
The output is:
abc
aThe expression c - 'a' gives the offset of the letter, from 0 to 25. Adding k moves the offset forward. The remainder % 26 brings any result from 26 or more back into the range from 0 to 25. Then 'a' + turns the offset back into a letter. The letter z has offset 25, and adding 3 gives 28. The remainder after dividing by 26 is 2, which is c. The letter x shifted by 3 gives a, and y gives b.
The same method turns text into numbers. A digit character such as '7' has the code 55, and '0' has the code 48. The difference '7' - '0' is 7. A loop can build a whole number one digit at a time, which is the way a program reads a number that the user typed.
The output is:
2026
2027For each character, the loop multiplies the running value by 10 to make room for a new digit, then adds the digit. For "2026" the value goes 2, then 20, then 202, then 2026. The const std::string& parameter means the function reads the string without copying it.
Worked example: palindromes that ignore case and punctuation
A palindrome is a word or phrase that reads the same forwards and backwards, such as level. Phrases often break the rule only because of capital letters, spaces and punctuation. This program checks a phrase after skipping everything that is not a letter, and treating upper-case and lower-case letters as equal. It combines the range tests and the case change from this lesson.
It uses two indexes, left and right, that start at the two ends of the string and move toward the middle. Each step skips non-letters, and compares the two letters that remain.
The output is:
1
0
1The function prints 1 for true and 0 for false, because std::cout writes a bool as a number. The phrase "Hello" gives 0, because its first letter H does not match its last letter o. The other two phrases give 1.
The text.size() function returns an unsigned number. The program converts it to int before subtracting 1, so an empty string gives -1 and the loop does not run.
Common mistakes
Printing a sum of two chars without a cast. The expression 'a' + 1 is an int, so std::cout prints the number 98, not the letter b. Wrap the result in static_cast<char> when the goal is a letter. A correct program prints b after the cast, and the mistake shows up only as a wrong-looking number on the screen.
Comparing a char with a string. Single quotes make a character, and double quotes make a string. The comparison if (c == "a") does not compare a character with a letter. The compiler reports this error for g++ 15.2:
error: ISO C++ forbids comparison between pointer and integer [-fpermissive]Write c == 'a' with single quotes.
Using the letter itself as an index. The counting program uses counts[ch - 'a'], not counts[ch]. The character 'q' has code 113, so counts[ch] asks for position 113 in an array with 26 places. This is outside the array. With the GNU standard library on this Ubuntu g++ build, the program stops with an assertion message (a run-time check that failed) such as this one:
/usr/include/c++/15/array:210: ... std::array<_Tp, _Nm>::operator[](size_type) [with _Tp = int; long unsigned int _Nm = 26; ...]: Assertion '__n < this->size()' failed.The program ends with exit code 134. Other compilers or settings may not check the index at all, and then the program writes to memory it does not own. Always subtract the first letter first.
Storing a too-large value in a char. A char on this system holds values from -128 to 127. The expression 'z' + 200 is 322, which is too big. The compiler warns when it sees a constant like this:
warning: overflow in conversion from ‘int’ to ‘char’ changes value from ‘322’ to ‘66’ [-Woverflow]When the value is not a constant, the compiler may not warn, and the stored value is still wrong. Check the range before you store a result in a char.
Assuming that char is unsigned. On this system, std::is_signed_v<char> is true, so a char can hold negative values. The code 200 does not fit in a signed char. Converting it gives -56:
std::cout << static_cast<int>(static_cast<char>(200)) << '\n'; // prints -56Some systems use an unsigned char instead. For the character tests in <cctype>, convert to unsigned char first, as shown earlier. This keeps the value in the range the function expects.
Turning a digit string into a number with the string itself. A program that writes "7" - '0' is trying to subtract a character from a string. The compiler rejects it:
error: invalid conversion from ‘const char*’ to ‘int’ [-fpermissive]Use the character '7', with single quotes, to get the value 7.
Practice
- What does this line print?
std::cout << 'd' - 'a' << '\n';Show the answer
It prints 3. The code of 'd' is 100 and the code of 'a' is 97, so the difference is 3. Subtraction of two char values gives an int, so the result is a number, not a letter.
- What does this program print?
char c = 'A' + 25;
std::cout << c << '\n';Show the answer
It prints Z. The letter A has offset 0, so adding 25 gives the letter at offset 25, which is Z. The result is stored in a char, so std::cout prints the letter.
- Write a function
int count_lower(const std::string& text)that returns how many lower-case letters the text contains. Use the range test from this lesson.
Show the answer
int count_lower(const std::string& text) {
int count = 0;
for (char ch : text) {
if (ch >= 'a' && ch <= 'z') {
count++;
}
}
return count;
}For the text "a1B2c" the function returns 2, because a and c are lower-case and B is upper-case.
- Using the
shift_letterfunction from this lesson, what doesshift_letter('y', 3)return?
Show the answer
It returns 'b'. The letter y has offset 24. Adding 3 gives 27, and the remainder after dividing by 26 is 1, which is the offset of b.
- Write a function
char next_letter(char c)that returns the next letter of the alphabet, and returns'a'when given'z'. Test it with'm'and'z'.
Show the answer
char next_letter(char c) {
return static_cast<char>('a' + (c - 'a' + 1) % 26);
}The call next_letter('m') returns n, and next_letter('z') returns a. The remainder operator makes the letter after z wrap back to a.
Summary
- A
charholds a character code, which is a small whole number, so you can use it in arithmetic. - Arithmetic on a
chargives anint. Usestatic_cast<char>to turn the result back into a letter. - Letters, digits and upper-case letters are in order in the usual character set, so range comparisons such as
ch >= 'a' && ch <= 'z'work. - Subtracting
'a'from a lower-case letter gives its position from 0 to 25, which is the usual way to index an array of 26 counters. - Subtracting
'0'from a digit character gives its value, and a loop can build a number from a string of digits. - Compare a
charwith a character in single quotes, and never with a string in double quotes.