Why this matters
Counting letters is a small job that shows up in many programs. A spell checker counts letter patterns. A word game checks whether a word can be built from a set of letters. A text tool reports the most common letter in a paragraph. All of these need the same tool: a place to keep one count per letter, and a loop that adds to the right count for each character.
This lesson builds that tool in C++. It uses a fixed array with 26 slots, one for each letter of the English alphabet. You will count the letters of a word, compare two words, find the most common letter, and see what goes wrong with capital letters and with indexes that are out of range.
The loops here work on single characters. If you have not yet read how a char holds a small number, the first lesson in this module covers that.
A letter is a number
A char holds a small whole number. The most common table that maps characters to numbers is ASCII. In ASCII, the lowercase letters a to z are 26 numbers in a row, from 97 for a up to 122 for z.
Because the letters sit next to each other in the table, subtracting 'a' from a lowercase letter gives its position in the alphabet, counting from 0. The next program prints four such subtractions:
#include <iostream>
int main() {
std::cout << 'a' - 'a' << '\n';
std::cout << 'c' - 'a' << '\n';
std::cout << 'z' - 'a' << '\n';
std::cout << 'C' - 'a' << '\n';
}Output:
0
2
25
-30A letter minus itself is 0, so 'a' - 'a' is 0. The letter c is the third letter, and counting starts at 0 (a is 0, b is 1, c is 2), so 'c' - 'a' is 2. The letter z is 25.
The last line gives -30. Capital letters have their own numbers in ASCII, and they are not next to the lowercase ones. The uppercase C is 67, so 67 - 97 is -30. This is the first place where the trick breaks, and the mistakes section comes back to it.
One detail matters for the code that follows. When you subtract two char values, C++ turns them into int values first. The result is a normal int, so you can use it as an index into an array.
The idea: one box per letter
Picture 26 mailboxes in a row, labelled from a to z. For each letter you read, you drop one card into the matching box. When you finish, you count the cards in each box. Each box holds the count of one letter.
In an array, the boxes are numbered from 0. Box 0 holds the count for a, box 1 for b, and so on up to box 25 for z. To find the box for a letter, subtract 'a':
| Letter | Index (c - 'a') | Count after "hello" |
|---|---|---|
e | 4 | 1 |
h | 7 | 1 |
l | 11 | 2 |
o | 14 | 1 |
The word hello has five characters but only four different letters. The l appears twice, so its box holds 2. The other boxes that the word does not use stay at 0.
A useful side effect is that the boxes are already in alphabetical order. If you walk from index 0 to index 25, you list the letters from a to z without any sorting.
Declaring the array
The standard library has a fixed-size array type called std::array. You need the header <array> for it. Here is the declaration for 26 counts:
std::array<int, 26> counts{};The two values in angle brackets are the type of each slot (int, a whole number) and the number of slots (26). The empty braces {} set every slot to 0. Without them, the slots hold whatever numbers were already in that memory, which is a bug covered in the mistakes section.
A std::array knows its own size. You can ask it with counts.size(), which returns 26. It also has a method called at() that checks the index before it uses it. You will see at() used in the mistakes section to make a bad index visible. Real programs usually use counts[i], which does not check the index.
The plain C style, int counts[26] = {};, also works. It is shorter, but the array does not know its own size, so you have to remember 26 yourself.
Step 1: count the letters of one word
The first program counts the letters of hello. It has two loops. The first loop adds up the counts. The second loop prints the letters that appear at least once.
#include <array>
#include <iostream>
#include <string>
int main() {
std::string word = "hello";
std::array<int, 26> counts{};
for (char c : word) {
counts[c - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (counts[i] > 0) {
std::cout << static_cast<char>('a' + i) << ": " << counts[i] << '\n';
}
}
}Output:
e: 1
h: 1
l: 2
o: 1The first loop reads each character of word in turn. The name c holds the current character. For h, the expression c - 'a' is 7, so counts[7]++ adds one to the box for h. The ++ means "add one to this value".
The second loop goes from index 0 to index 25. Any box with a count above zero is a letter that appeared, so the loop prints it. The letter is printed with static_cast<char>('a' + i). The expression 'a' + i is a number such as 97 for i equal to 0. The cast turns that number back into a character so that a is printed instead of 97. The mistakes section shows what happens without the cast.
Notice that the output is already in alphabetical order. That comes from the layout of the boxes, not from any sorting step.
Step 2: compare two words
Two words have the same letter counts when every box holds the same number for both. For example, listen and silent each contain one i, one l, one s, one t, one e and one n. Their boxes match, so the words have equal counts.
To compare, we need the counts of each word. The next version puts the counting into a function. The function returns the whole array. C++ lets a function return an array object like this, and the caller gets a full copy.
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
bool same_letter_counts(const std::string& a, const std::string& b) {
return count_letters(a) == count_letters(b);
}
int main() {
std::cout << std::boolalpha;
std::cout << "listen vs silent: " << same_letter_counts("listen", "silent") << '\n';
std::cout << "apple vs pale: " << same_letter_counts("apple", "pale") << '\n';
std::cout << "aab vs abb: " << same_letter_counts("aab", "abb") << '\n';
std::cout << "Listen vs silent: " << same_letter_counts("Listen", "silent") << '\n';
}Output:
listen vs silent: true
apple vs pale: false
aab vs abb: false
Listen vs silent: falseSome details in this program:
using Counts = std::array<int, 26>;gives the long type a short name. Every later line can sayCounts.- The
if (c >= 'a' && c <= 'z')test skips anything that is not a lowercase letter. Spaces, digits, punctuation and capital letters are skipped. This is a choice. It is the right choice for a word game with only lowercase words, and the wrong one for text with capitals. The last line of output shows the effect.Listenhas a capitalL, which is skipped, so the function counts onlyisten, and that does not matchsilent. count_letters(a) == count_letters(b)compares twostd::arrayobjects with==. The comparison is true only when all 26 pairs of boxes match.- The parameters are
const std::string&. The&means the function receives the string without copying it, andconstmeans the function promises not to change it.
aab has two a and one b. abb has one a and two b. The boxes differ, so the answer is false.
The rule for capitals and punctuation is a decision you make before you write the function. Write it down. The exercises ask you to change the rule.
Step 3: find the most common letter
The third example uses a sentence that contains every letter of the alphabet. It finds the letter that appears most often and counts how many different letters the sentence uses.
#include <algorithm>
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
int main() {
std::string sentence = "the quick brown fox jumps over the lazy dog";
Counts counts = count_letters(sentence);
int best = 0;
for (int i = 1; i < 26; i++) {
if (counts[i] > counts[best]) {
best = i;
}
}
std::cout << "most common: " << static_cast<char>('a' + best) << " (" << counts[best] << ")\n";
int distinct = static_cast<int>(std::count_if(counts.begin(), counts.end(), [](int n) { return n > 0; }));
std::cout << "distinct letters: " << distinct << '\n';
}Output:
most common: o (4)
distinct letters: 26The search for the most common letter keeps a variable best, which is the index of the highest count seen so far. It starts at 0, which is box a. Each time a later box holds a larger count, best moves to that box. The test uses >, not >=, so when two letters tie, the earlier one in the alphabet keeps the title.
The count_if line counts how many boxes hold a number above zero. The [](int n) { return n > 0; } part is a lambda, which is a small unnamed function written inside the code. It takes one number and returns true when the number is above zero. std::count_if calls it once for each box. It needs the header <algorithm>.
The word distinct means "different". The sentence uses all 26 letters, so the answer is 26. A sentence like this, which uses every letter, is called a pangram. Exercise 2 asks you to write a check for it.
Common mistakes
Forgetting to set the counts to zero
This is the most common mistake with counting arrays. The program below leaves out the {}:
#include <array>
#include <iostream>
int main() {
std::array<int, 26> counts;
std::cout << "counts[0] = " << counts[0] << '\n';
}The first slot was never given a value. Its number comes from whatever the memory held before, so the output is a random-looking number. On one run the program printed:
counts[0] = 73744The number changes between runs and between computers. Compiled with g++ -std=c++20 -Wall, the program did not warn. Compiled with -O2, g++ reported this warning:
l08_uninit.cpp:6:49: warning: ‘counts’ is used uninitialized [-Wuninitialized]The fix is the {} after the name. Every counting array in this lesson has it for that reason.
Using a capital letter as an index
The counting loop assumes the input is lowercase. Here the word is Hello, with a capital H:
#include <array>
#include <iostream>
#include <string>
int main() {
std::array<int, 26> counts{};
std::string word = "Hello";
for (char c : word) {
counts.at(c - 'a')++;
}
std::cout << "done\n";
}The capital H is 72 in ASCII. Subtracting 'a', which is 97, gives -25. That is not a box. The at() method checks the index and stops the program with this message:
terminate called after throwing an instance of 'std::out_of_range'
what(): array::at: __n (which is 18446744073709551591) >= _Nm (which is 26)The number 18446744073709551591 is the value -25 seen as an unsigned number. An array index is an unsigned number in C++, so a negative index becomes a very large positive one. The message says the index is past the end of the array.
With counts[c - 'a']++, as in the earlier programs, nothing checks the index. The increment writes to memory that is not part of the array. C++ calls this undefined behaviour, which means the program may print nothing wrong, print wrong results, or crash later in a place far from the cause. The fix is to test for lowercase before using the index, as count_letters does, or to convert capitals to lowercase first (exercise 5).
Going one past the last box
There are 26 boxes numbered 0 to 25. Index 26 does not exist. This is the same off-by-one error that appears in array loops of every kind:
#include <array>
int main() {
std::array<int, 26> counts{};
counts.at(26) = 1;
}Output:
terminate called after throwing an instance of 'std::out_of_range'
what(): array::at: __n (which is 26) >= _Nm (which is 26)The message says that index 26 is not less than the size 26. The loops in this lesson use i < 26, not i <= 26, for that reason.
Printing the number instead of the letter
Without the cast, 'a' + i prints a number:
#include <array>
#include <iostream>
int main() {
std::array<int, 26> counts{};
counts[0] = 1;
std::cout << 'a' + 0 << ": " << counts[0] << '\n';
std::cout << static_cast<char>('a' + 0) << ": " << counts[0] << '\n';
}Output:
97: 1
a: 1The first line prints 97 because 'a' + 0 is an int, and std::cout prints an int as a number. The second line converts the number back to a character with static_cast<char>, so it prints a.
Practice
Exercise 1. Vowels. Write int count_vowels(const std::string& text) that returns how many of the letters a, e, i, o and u appear in the text. Test it with education (expected 5) and rhythm (expected 0). Use the count_letters function from Step 2.
Show the answer
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
int count_vowels(const std::string& text) {
Counts counts = count_letters(text);
return counts['a' - 'a'] + counts['e' - 'a'] + counts['i' - 'a'] + counts['o' - 'a'] + counts['u' - 'a'];
}
int main() {
std::cout << count_vowels("education") << '\n';
std::cout << count_vowels("rhythm") << '\n';
}Output: 5 then 0. Each vowel has its own box, so the function adds those five boxes together. Writing 'a' - 'a' instead of 0 shows which letter each box stands for.
Exercise 2. Pangram. A pangram uses every letter of the alphabet at least once. Write bool is_pangram(const std::string& text). Test it with the quick brown fox jumps over the lazy dog (expected true) and hello (expected false). Hint: std::all_of checks a condition on every element of a range.
Show the answer
#include <algorithm>
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
bool is_pangram(const std::string& text) {
Counts counts = count_letters(text);
return std::all_of(counts.begin(), counts.end(), [](int n) { return n > 0; });
}
int main() {
std::cout << std::boolalpha;
std::cout << is_pangram("the quick brown fox jumps over the lazy dog") << '\n';
std::cout << is_pangram("hello") << '\n';
}Output: true then false. The function returns true only when every one of the 26 boxes is above zero.
Exercise 3. Predict, then run. Before you run anything, write down the counts of a, b and n for the word banana. Then run this program and compare.
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
int main() {
Counts counts = count_letters("banana");
std::cout << "a: " << counts[0] << '\n';
std::cout << "b: " << counts[1] << '\n';
std::cout << "n: " << counts[13] << '\n';
}Show the answer
The output is:
a: 3
b: 1
n: 2a is box 0, b is box 1, and n is box 13 (n is the 14th letter, so its index is 13). The word has three a, one b and two n.
Exercise 4. Letters that appear once. Write std::string letters_once(const std::string& text). It returns the letters that appear exactly once, in alphabetical order. Test it with abcab (expected c) and aabbcd (expected cd). Print the results inside brackets so that an empty result is visible.
Show the answer
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
std::string letters_once(const std::string& text) {
Counts counts = count_letters(text);
std::string result;
for (int i = 0; i < 26; i++) {
if (counts[i] == 1) {
result += static_cast<char>('a' + i);
}
}
return result;
}
int main() {
std::cout << "[" << letters_once("abcab") << "]\n";
std::cout << "[" << letters_once("aabbcd") << "]\n";
}Output:
[c]
[cd]The loop walks the boxes in alphabetical order, so the letters come out sorted. The += operator adds one character to the end of the string.
Exercise 5. Capitals count. Change count_letters so that a capital letter counts as its lowercase form. Then Listen and silent should have equal counts. Use c - 'A' + 'a' to turn a capital into a lowercase letter.
Show the answer
#include <array>
#include <iostream>
#include <string>
using Counts = std::array<int, 26>;
Counts count_letters(const std::string& text) {
Counts counts{};
for (char c : text) {
if (c >= 'A' && c <= 'Z') {
c = static_cast<char>(c - 'A' + 'a');
}
if (c >= 'a' && c <= 'z') {
counts[c - 'a']++;
}
}
return counts;
}
bool same_letter_counts(const std::string& a, const std::string& b) {
return count_letters(a) == count_letters(b);
}
int main() {
std::cout << std::boolalpha;
std::cout << same_letter_counts("Listen", "silent") << '\n';
std::cout << same_letter_counts("aab", "abb") << '\n';
}Output: true then false. The first if moves a capital letter into the lowercase range, and the second if then counts it. The check for capitals comes first, so the converted letter is counted once.
Summary
- A
charis a number, and subtracting'a'from a lowercase letter gives its position from 0 to 25. - An array of 26 counts, indexed by
c - 'a', stores how many times each letter appears. - Initialise the array with
{}so every count starts at 0. - Two words have equal letter counts when their count arrays compare equal with
==. - Check the input before you use it as an index. Capitals and other characters give indexes outside the array.
- Use
static_cast<char>when you turn a number back into a letter for printing.