Strings
Since module 2 you have used a std::string as one whole value: you stored it, printed it, joined two with + and read one from the input. A string is made of smaller pieces, the characters in it, and each character has a position.
In this lesson you open the string up. You read one character, change one, visit all of them in a loop, build a new string character by character, and search a string for a piece.
One character
A character is one letter, digit, space or other sign. The type that holds exactly one character is char. A character is written between single quotes.
#include <iostream>
#include <string>
int main() {
char initial = 'K';
std::string name = "Kofi";
char gap = ' ';
std::cout << initial << "\n";
std::cout << name << "\n";
std::cout << initial << gap << name << "\n";
}
K
Kofi
K Kofi
'K' and "K" look alike when they are printed, and they are values of different types.
| Written | Type | Holds |
|---|---|---|
'K' |
char |
exactly one character |
"K" |
text | a run of characters, which here happens to have one |
"Kofi" |
text | a run of four characters |
"" |
text | a run of no characters |
Single quotes always hold exactly one character. There is no empty char, and ' ' is not empty: it is the space character.
The two types do not stand in for each other. A char variable cannot be given text in double quotes, even text of one character.
#include <iostream>
int main() {
char initial = "K";
std::cout << initial << "\n";
}
One character is written with two keys. '\n' is the line end you have printed since module 1. The backslash and the n together name one character.
Letters are numbers
The computer stores every character as a small whole number, called its code. A char is that number, with one extra rule: std::cout prints a char as the character that has this code.
Storing a char in an int shows the number.
#include <iostream>
int main() {
char letter = 'a';
int code = letter;
std::cout << letter << " " << code << "\n";
char next = letter + 1;
std::cout << next << "\n";
std::cout << letter + 1 << "\n";
int value = '7' - '0';
std::cout << value * 2 << "\n";
}
a 97
b
98
14
The codes were handed out in order, and that is the useful part.
| Characters | Codes |
|---|---|
'0' to '9' |
48 to 57 |
'A' to 'Z' |
65 to 90 |
'a' to 'z' |
97 to 122 |
' ' (the space) |
32 |
You do not need to remember the numbers. Remember three facts: the digits are in order, the capital letters are in order, and the small letters are in order, with all the capitals before all the small letters.
Because the letters are in order, letter + 1 is the next letter. Look at the two lines that print it.
char next = letter + 1;works out 98 and stores it in achar. Printed, acharwith the code 98 isb.std::cout << letter + 1prints98. Arithmetic on achargives anint, as arithmetic on anintand adoublegives adoublein module 2. Anintis printed as a number.
So the result of arithmetic on characters is a character again only after it has been stored in a char.
Subtraction answers two questions that come up all the time.
c - '0'is the value of a digit.'7' - '0'is 55 - 48, which is 7. The character'7'is not the number 7.c - 'a'is a small letter's place in the alphabet, counted from 0.'a' - 'a'is 0 and'd' - 'a'is 3.
Comparisons use the codes as well, so c >= 'a' && c <= 'z' is true exactly when c is a small letter. A char can also be a loop's counter.
#include <iostream>
int main() {
for (char c = 'a'; c <= 'e'; ++c) {
std::cout << c << " is letter number " << c - 'a' + 1 << "\n";
}
}
a is letter number 1
b is letter number 2
c is letter number 3
d is letter number 4
e is letter number 5
How long is a string?
A string knows how many characters it holds. Write the variable, a dot, and size().
#include <iostream>
#include <string>
int main() {
std::string town = "Leeds";
std::string street = "12 Mill Lane";
std::string nothing = "";
std::cout << town.size() << "\n";
std::cout << street.size() << "\n";
std::cout << nothing.size() << "\n";
town = town + "!";
std::cout << town.size() << "\n";
}
5
12
0
6
town.size() is a call, like the calls you know, with one difference: the function belongs to the string written before the dot, and it answers for that string. It needs no values, and the parentheses stay.
What is counted:
- Every character between the quotes counts, and that includes spaces.
"12 Mill Lane"has two digits, eight letters and two spaces. - A digit is a character like any other.
"12"has the size 2. The string does not know that it spells a number. - The quotes are not part of the string. They only mark where it starts and ends in the program.
- A string with nothing in it has the size 0. It is called the empty string, and it is a perfectly good string.
std::string line;with no value starts as the empty string, andgetlinegives one for an empty line of input.
The size is not fixed. After town = town + "!"; the string is one character longer, and size() says so.
The character at a position
The characters of a string are numbered, and the numbering starts at 0. The number of a character is its position. s[i] is the character at position i, and its type is char.
#include <iostream>
#include <string>
int main() {
std::string word = "planet";
std::cout << word[0] << "\n";
std::cout << word[1] << "\n";
std::cout << word[word.size() - 1] << "\n";
word[0] = 'P';
word[5] = 's';
std::cout << word << "\n";
}
p
l
t
Planes
| Position | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| Character | p |
l |
a |
n |
e |
t |
The string has 6 characters, and their positions are 0 to 5. The first character is at position 0. The last is at position size() - 1, never at position size().
word[0] = 'P'; is an assignment to one character. It replaces what was at that position. The string stays the same length, and the other characters stay where they are.
A position from size() upwards is not a character of the string. word[6] is not one of the six characters, and an empty string has none at all. C++ does not check the position for you: no message, no stop. Reading the position size() itself is defined: it gives the null character, a character with the code 0 that ends every string and is not one of its characters. Storing any other character there, or using a position beyond size(), is not defined: the program may print rubbish or change something else. Keeping every position below size() is your job, and it is the reason this lesson spends two sections on loops.
Counting from 0 has one pleasant result: a string of size n has the positions 0 to n - 1, which are exactly the values a loop for (int i = 0; i < n; ++i) gives i.
A count that cannot go below zero
size() does not give an int. It gives a value of a type called std::size_t, which is unsigned: it holds 0, 1, 2 and upwards, and it has no negative values at all.
That sounds harmless, because a size is never negative. The trouble starts with subtraction. Take 1 away from an unsigned 0 and there is no -1 to go to. The count wraps round to the largest value the type has, which is a very large number.
#include <iostream>
#include <string>
int main() {
std::string word = "";
std::cout << (word.size() - 1 < 0) << "\n";
std::cout << (word.size() - 1 > 1000000) << "\n";
int n = word.size();
std::cout << (n - 1 < 0) << "\n";
std::cout << (n - 1 > 1000000) << "\n";
}
0
1
1
0
The first two lines ask about word.size() - 1 for an empty string. It is not less than 0, and it is greater than a million. The whole of that expression is unsigned, because size() is, and so it cannot be -1.
The last two lines do the same sum after int n = word.size();. That statement stores the size in an int, and from then on the arithmetic is int arithmetic: 0 - 1 is -1, as you expect.
This matters in a loop. i < word.size() - 1 looks like "stop before the last character". For an empty string it means "stop before that very large number", and the loop sets off through positions that do not exist.
The rule for this course:
- Store the size in an
intfirst, and do all the arithmetic with thatint. Every loop over a string in this course starts withint n = s.size();. - Comparing two sizes with each other, as in
a.size() < b.size(), is safe. Nothing is subtracted. - If you do compare with
size()directly, add on the other side and never subtract on thesize()side:i + 1 < s.size()is safe wherei < s.size() - 1is not.
Visiting every character
To look at every character of a string, let a counter run through the positions. This is the pattern, and it is always the same three lines.
#include <iostream>
#include <string>
int main() {
std::string code = "Flat 3B, North Wing";
int n = code.size();
int capitals = 0;
for (int i = 0; i < n; ++i) {
if (code[i] >= 'A' && code[i] <= 'Z') {
++capitals;
}
}
std::cout << "Capitals: " << capitals << "\n";
for (int i = n - 1; i >= n - 4; --i) {
std::cout << code[i];
}
std::cout << "\n";
}
Capitals: 4
gniW
n is the size, stored in an int. i starts at 0 and the loop goes on while i < n, so i takes the values 0 to n - 1: every position, each once, and never one that is not there. For an empty string n is 0, the test is false at once, and the body does not run.
Inside the loop, code[i] is the character of this turn. Here it is compared with 'A' and 'Z', using the codes from the section before.
The second loop runs backwards over the last four positions. Going backwards needs i >= ... and --i, and it needs i and n to be int: the loop ends when i drops below its last value, and an unsigned counter could not drop below 0.
A string can be given to a function like any other value. The parameter's type is std::string.
#include <iostream>
#include <string>
int count_capitals(std::string line) {
int n = line.size();
int capitals = 0;
for (int i = 0; i < n; ++i) {
if (line[i] >= 'A' && line[i] <= 'Z') {
++capitals;
}
}
return capitals;
}
int main() {
std::string door = "Flat 3B, North Wing";
std::cout << count_capitals(door) << "\n";
std::cout << count_capitals("no capitals here") << "\n";
}
4
0
As with an int in module 6, the function gets a copy. line is a new string that starts with the same characters as door. If the function changed line[0], door would not change.
A character's code can also decide what is drawn. Here each letter of a word becomes a bar, and a later letter of the alphabet gets a taller bar.
#include "scimigo.h"
#include <string>
using namespace scimigo;
int main() {
canvas(600, 400);
std::string word = "planet";
int n = word.size();
for (int i = 0; i < n; ++i) {
int place = word[i] - 'a' + 1;
rectangle(60 + i * 80, 360 - place * 15, 60, place * 15, "teal");
}
}
The tallest bar is the last one: t is letter number 20. The shortest is the third, for a.
Building a string
A string can grow. Start with an empty one and add to its end, one piece at a time. This is the pattern of a running total from module 5, with text in place of a number.
#include <iostream>
#include <string>
int main() {
std::string phrase = "low tide at nine";
std::string squeezed;
int n = phrase.size();
for (int i = 0; i < n; ++i) {
if (phrase[i] != ' ') {
squeezed += phrase[i];
}
}
std::cout << squeezed << "\n";
std::cout << squeezed.size() << "\n";
std::string row;
for (int i = 0; i < 3; ++i) {
row += "ab";
row.push_back('-');
}
std::cout << row << "\n";
}
lowtideatnine
13
ab-ab-ab-
std::string squeezed; starts empty. On each turn of the loop one character of phrase is looked at, and if it is not a space it is added to the end of squeezed.
Two ways to add to the end:
s += piece;adds a character or a whole string. It is the short spelling ofs = s + piece;, astotal += 5;is for numbers.s.push_back(c);adds exactly one character. Likesize(), it is written after a dot, and it belongs to the string before the dot.
Neither changes the characters that are already there. The size goes up by the number of characters added.
+ also joins a string and a character, on either side: row + '!' is row with an exclamation mark after it, and '!' + row has it in front. The rule from module 2 still holds: one side of the + must be a std::string variable.
Building is how a function makes a new string to give back. It declares an empty string, adds to it in a loop, and returns it.
#include <iostream>
#include <string>
std::string spaced_out(std::string word) {
std::string result;
int n = word.size();
for (int i = 0; i < n; ++i) {
result += word[i];
if (i + 1 < n) {
result += ' ';
}
}
return result;
}
int main() {
std::cout << spaced_out("tide") << "\n";
std::cout << spaced_out("a") << "\n";
}
t i d e
a
Comparing strings
== and != work on strings: two strings are equal when they have the same characters in the same positions. < and > work too, and they put strings in the order of a dictionary, with one catch.
#include <iostream>
#include <string>
int main() {
std::string pear = "pear";
std::string plum = "plum";
std::string plums = "plums";
std::string capital = "Plum";
std::string thirty = "30";
std::string four = "4";
std::cout << (pear < plum) << "\n";
std::cout << (plum < plums) << "\n";
std::cout << (plum == capital) << "\n";
std::cout << (capital < pear) << "\n";
std::cout << (thirty < four) << "\n";
}
1
1
0
1
1
To compare two strings, C++ goes through both from position 0 and stops at the first position where they differ. The string with the smaller character there, by its code, is the smaller string. Nothing after that position is looked at.
| Comparison | Decided at | Because |
|---|---|---|
"pear" < "plum" |
position 1 | e comes before l |
"plum" < "plums" |
the end of plum |
no position differs, and the shorter string ends first |
"plum" == "Plum" |
position 0 | p and P are different characters |
"Plum" < "pear" |
position 0 | P has the code 80 and p has 112 |
"30" < "4" |
position 0 | the character 3 comes before the character 4 |
The catch is in the last two rows. The order is the order of the codes, and that is a dictionary's order only while all the letters are small, or all capital.
- Every capital letter comes before every small letter.
"Zoo" < "ant"is true. - Digits are compared as characters, not as numbers.
"30" < "4"is true although 30 is the larger number.
Use these comparisons with at least one std::string variable in them. Two pieces of quoted text compared directly, as in "pear" < "plum", are not compared as strings at all.
Finding a piece and taking it out
Two functions that belong to a string do most of the work of pulling text apart.
s.find(piece) searches s for piece, which may be a character or a string, and gives the position where the first one starts. s.substr(start, length) gives a new string: length characters of s, beginning at position start. s.substr(start), with one value, takes everything from start to the end.
#include <iostream>
#include <string>
int main() {
std::string entry = "colour=dark teal";
std::size_t at = entry.find('=');
std::string key = entry.substr(0, at);
std::string value = entry.substr(at + 1);
std::cout << at << "\n";
std::cout << key << "\n";
std::cout << value << "\n";
std::cout << entry.substr(7, 4) << "\n";
}
6
colour
dark teal
dark
The = is at position 6. entry.substr(0, at) is the 6 characters before it: positions 0 to 5. entry.substr(at + 1) starts one after the = and runs to the end. Neither includes the = itself. entry is not changed by any of this: substr copies characters out.
find gives a position as a std::size_t, the unsigned type that size() gives, so at is declared with that type. Adding to it is safe. Subtracting from it needs the care of the section on counts.
When the piece is not there, find has no position to give. It gives a special value instead, named std::string::npos. Compare with that name before you use the result. Do not print it and do not use it as a position: it is the largest value of the type, far beyond the end of any string.
find can also start its search further along: s.find(piece, start) ignores everything before position start. That finds the second occurrence, and the third.
#include <iostream>
#include <string>
int main() {
std::string line = "the cat sat on the mat";
int found = 0;
std::size_t at = line.find("at");
while (at != std::string::npos) {
++found;
std::cout << "at position " << at << "\n";
at = line.find("at", at + 2);
}
std::cout << found << " found\n";
if (line.find("dog") == std::string::npos) {
std::cout << "no dog\n";
}
}
at position 5
at position 9
at position 20
3 found
no dog
Each turn of the loop handles one occurrence and then searches again from just past it: at + 2, because "at" is two characters long. When nothing more is found, at becomes std::string::npos and the loop ends.
Questions about a character
The header <cctype> has functions that answer questions about one character, and two that convert one.
| Call | What it tells you |
|---|---|
std::isalpha(c) |
is c a letter, capital or small? |
std::isdigit(c) |
is c one of the digits 0 to 9? |
std::isspace(c) |
is c a space, a tab or a line end? |
std::toupper(c) |
the capital of c if c is a small letter; otherwise c unchanged |
std::tolower(c) |
the small letter of c if c is a capital; otherwise c unchanged |
#include <cctype>
#include <iostream>
#include <string>
int main() {
std::string label = "Room 12b, 3rd floor";
int n = label.size();
int letters = 0;
int digits = 0;
for (int i = 0; i < n; ++i) {
if (std::isalpha(label[i])) {
++letters;
}
if (std::isdigit(label[i])) {
++digits;
}
}
std::cout << letters << " letters, " << digits << " digits\n";
for (int i = 0; i < n; ++i) {
label[i] = std::tolower(label[i]);
}
std::cout << label << "\n";
std::cout << std::toupper('a') << "\n";
char capital = std::toupper('a');
std::cout << capital << "\n";
}
12 letters, 3 digits
room 12b, 3rd floor
65
A
Use the three questions as a condition, as in if (std::isalpha(c)). They answer with a number: 0 for no, and some number that is not 0 for yes. A condition treats any number that is not 0 as true. The number for yes is not always 1, so do not print it and do not add it to a count.
The two conversions hold the same kind of surprise. std::toupper and std::tolower return an int: the code of the character, not a char. That is why std::cout << std::toupper('a') prints 65. Store the result in a char, and it is a character again:
label[i] = std::tolower(label[i]);stores it back into the string, at the same position. This is the usual way to change the case of a whole string.char capital = std::toupper('a');stores it in a variable of its own.
Characters that are not letters pass through both conversions unchanged, so there is no need to test for a letter first. In the output above, the digits, the comma and the spaces of label are as they were.
To compare two characters without regard to case, convert both the same way and compare the results: std::tolower(a) == std::tolower(b). Two int codes compare as well as two characters do.