8 C-Strings, Strings, and Vectors
Hussam Ghunaim, Ph.D.
Learning Objectives
At the end of reading this chapter, you will be able to :
- Implement C-strings in your programs
- Solve real-world problems using string class in your programs.
- Understand the differences between vectors and arrays.
C-Strings
C-strings have been used in C++ for a long time to handle strings. Recently, it has been replaced with a more advanced and flexible string class that will be discussed in the next section. Although C-strings are outdated, they are still in common use. The reason for that is many legacy systems are built by implementing the C-strings. This means that programmers are still required to be familiar with and comfortable working with them. In some applications, C-strings might be favored for their reduced overhead and enhanced performance.
C-strings are actually arrays of characters delimited with the null character ‘\0‘ to determine the end of the stored string. For example, executing the code char variable[15] = “C++ Course”; creates the following character array in the memory:
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] | [8] | [9] | [10] | [11] | [12] | [13] | [14] |
C | + | + | C | o | u | r | s | e | \0 | ? | ? | ? | ? |
The null character is crucial when working with C-strings. Since it is just a character, it can be mistakenly deleted. Therefore, we must always check for the null character when reading the contents of C-string variables to know when to stop. If the null character is lost, we might continue reading through uninitialized areas of the array or, even worse, reading beyond the boundaries of the array.
It is acceptable to declare and initialize a C-string variable without specifying the desired size. The previous example can be rewritten as char variable[ ] = “C++ Course”; In this syntax, the system will create an array with 11 memory locations to accommodate the string and the null character. However, this syntax char var[ ] = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’}; will create a normal array of characters, NOT a C-string type, which means the null character will not be created and added to the array. Therefore, it is recommended that you avoid using this syntax.
Example 01
Line 6 will create a C-string variable to store the string “cString” followed by automatically adding the null character ‘\0’ located at index 7. The string characters are stored at indexes from 0 up to 6. When running the code, the while loop will print the cStringVar contents up to the null character when the loop terminates, avoiding reading the rest of the array that includes uninitialized array locations.
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] | [8] | [9] |
C | S | t | r | i | n | g | \0 | ? | ? |
Let us now experiment with what would happen if we accidentally lose ‘\0’.
Experiment 1: Add the below line of code just below line 6 to replace ‘\0’ with any other character like ‘a’ and run the code again. Explain what happens.
cStringVar[7] = ‘a’;
The exact behavior will depend on the specific compiler you are using. Running this code on Visual Studio on a Windows system, the code still printed the string correctly, followed by the added ‘a’ character without any complications. This happened because the compiler’s automated operation happened behind the scenes. The array will be automatically initialized to its base type at the time of declaration. This means that although the code did not explicitly initialize the memory locations at indexes [8] and [9], the system automatically initialized them for us based on the array base type, which is the null character ‘\0’.
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] | [8] | [9] |
C | S | t | r | i | n | g | a | \0 | \0 |
Experiment 2: Now let us examine another scenario. Change the size of the string to 8 instead of 10. In this case, the ‘\0’ character will be the last character in the array. Therefore, when you replace the null character with any other character, the array will no longer behave as a C-string array. Run the code and explain the results.
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] |
C | S | t | r | i | n | g | a |
The code now will print all characters stored in the array in addition to undefined contents from outside the array boundary. To avoid reading and writing to memory locations outside of the array boundary, we can add additional protection to the while loop condition like this:
const int SIZE = 8;
while (cStringVar[index] != ‘\0’ && (index < SIZE) ) { …..
This change guarantees that the loop terminates at either reaching the null character or reaching the end of the array.
Operators’ Constraints with C-Strings
One of the challenges we face when working with C-strings is that they cannot be used directly with many common operators, such as the assignment operator (=) and comparison operators (e.g., == , < , > ). This is because C-strings are essentially arrays of characters, and arrays in C++ cannot be assigned or compared using these operators by default. For example, these expressions are illegal,
char CString[15], anotherCString[15];
CString = “C++ Strings”; //illegal
anotherCString = “another C++ Strings”; //illegal
CString == anotherCString; //illegal
The second and the third lines are illegal because C-style strings are fixed-size character arrays and cannot be reassigned after declaration. You cannot use the = operator to assign a new string literal to a C-string. Similarly, C-style strings cannot be compared using the == operator. This operator only checks if the two strings are the same array, not if they contain the same characters. Therefore, the following comparison always produces false, although the two strings have the same contents because they are indeed two different arrays.
char str1[] = “string”;
char str2[] = “string”;
bool stringsComp = (str1 == str2);
To address these limitations, the <cstring> library provides several functions to manipulate C-strings. Table 01 lists some of these functions along with their descriptions.
Function | Description | Example |
strlen(string) | Returns the number of characters in a string (not counting the \0 null terminator). | strlen(“Hello”) → 5 |
strcpy_s(dest, destsz, source) | Copies the string source into dest (overwrite the contents of dest). | char dest[10]; strcpy_s(dest, 10, “Hello”); → dest contains “Hello” |
strncpy_s(dest, destsz, source, n) | Copies up to n characters from source to dest. | char dest[10]; strncpy_s(dest, 10, “Hello”, 2); → dest contains “He” |
strcmp(string1, string2) | Compares string1 and string2. Returns 0 if they are the same, a positive number if string1 is lexicographically greater, and a negative if string1 is lexicographically less. | strcmp(“apple”, “banana”) → -1 (because “apple” is lexicographically less than “banana”) |
strncmp(string1, string2, n) | Compares up to n characters of string1 and string2. | strncmp(“hello”, “helium”, 3) → 0 (because the first 3 characters “hel” are the same in both strings) |
strcat_s(dest, destsz, source) | Adds the characters from the source to the end of the dest (concatenates them). | char dest[20] = “Hello”; strcat_s(dest, 20, ” World”); → dest contains “Hello World” |
strncat_s(dest, destsz, source, n) | Adds up to n characters from source to the end of dest. | char dest[20] = “Hello”; strncat_s(dest, 20, ” World”, 3); → dest contains “Hello Wor” |
.
Example02
Run the code and explain the results. The main purpose of this example is to illustrate the usage of some of the C-string functions. In lines 7 to 10, we declare and populate C-string variables. As always, we have a concern about overflowing C-string variables; it is a good idea to check their sizes using the strlen function from the <cstring> library, lines 13 and 14. strlen function returns the number of characters saved in a C-string variable – excluding the null character – not the declared size. That is, if a C-string variable is declared to have the size of 10 and then used to save 5 characters value, strlen will return 5.
Because the full name variable was declared to be of a specific size, we want to check if the combined first and last names will fit. If not, we must warn the user utilizing the if … else statement in line 17. If the size of both first and last names + one space between them + the null character ‘\0’ fit the fullName variable size, then we can run the else block. Firstly, we copy the firstName variable into the fullName using the strcpy_s function. The ‘_s’ part denotes the safe version of this function introduced in C++ 11 in comparison with the older version. Many of the <cstring> functions have been modified and have the ‘_s” part to signify this fact. strcpy_s function takes three arguments: the destination variable, the size of the destination variable, and the source variable, respectively. Again, it is critical to check that the destination variable size is large enough to hold the source variable contents. Line 23 adds a space to separate the two names, and line 24 adds the lastName contents into the fullName variable using the strcat_s. The cat part of this function name denotes the concatenate action, which means adding the second string to the end of the first string. This is in contrast to the strcpy_s function behavior, in which the contents of the source string overwrite the contents of the destination variable. Finally, line 26 prints out the fullName contents to the console.
It is very important to remember that all of the mentioned functions must handle the null character appropriately for every operation. That is, strlen returns the size of the string, excluding the null character. Both the strcpy_s and strcat_s will ensure that the destination variable is always delimited with the null character appropriately.
Example03
To better understand how to work with C-strings, let us do a couple of tests on example 02 code. The exact behavior depends on your compiler and system. To run all the code in this book, I used Visual Studio IDE and Windows 11.
Test 1: Change the firstNameSize to 4 and enter a large first name like John, which will not leave a space for the null character. When running the code, an exception is thrown saying, “Stack around the variable ‘firstName’ was corrupted,” which prevents running the remaining of the code. Similar results will occur if the lastNamesize is changed in the same manner.
Test 2: Change firstNameSize, lastNameSize, and fullNameSize back to 10. This time, be sure the first and last names you entered fit within the allowed limit; however, the combined names will not fit the full name size allowance, such as John Smith. Of course, in this case, the if-statement will detect the problem and terminate the execution safely.
Test 3: Keep all changes in test 2 and change the if-condition to become if (strlen(firstName) + strlen(lastName) + 2 > 20) . We do this only to trick the if-condition and allow the else block to execute. This time, by using the same example, ‘John Smith,’ the if-statement will fail to detect the problem, and the code will continue executing the else block. Lines 22 and 23 will execute normally as the size of the lastName will fit both the first name and the additional space. However, line 24 will generate the exception again because it causes a memory overflow.
Exercise 01
Assume strlen function is not implemented in the <cstring> library. Implement a function called stringLength that counts how many characters are in a C-string, excluding the null character. The function should be able to count up to 100 characters.
Note: In your stringLength implementation, it is ok to return an integer number for the string length. However, the strlen member function returns the string length in size_t type, which is an unsigned integer type. The unsigned types are discussed in Chapter 4.
Exercise 02
Write a program that asks a user to enter their first and last names. Then, it extracts the first 3 characters from each name to create a username. Finally, attach the created username to the email domain ‘@example.com’. The program must utilize the strcpy_s and strcat_s functions.
To further increase the safety of the strcpy_s and strcat_s functions, other versions exist, strncpy_s and strncat_s, table 1. In these versions, a fourth parameter is designated to pass the integer n, which specifies the number of characters from the source string that must either be copied or concatenated with the destination string.
Example 04
Run the code and explain the results. In this example, we are testing how the strncpy_s and strncat_s functions can provide additional safety when working with C-Strings. The constants in lines 7 to 10 provide reasonable string sizes to run the code without issues. However, we are going to modify these sizes to test the behavior of both strncpy_s and strncat_s functions. Note that we used the getline member function of the input stream cin to read an entire line of text.
Test 1: change the constant MAX_LABEL_LENGTH size to 5 and provide a large first name like ‘Smith’. When you run the code, the strncpy_s function at line 24 will cause the run-time error “Debug Assertion Failed!” to terminate the execution gracefully after failing to assert that the source size fits the destination size.
Test 2: keep the same MAX_LABEL_LENGTH size and enter a four-character first name like ‘John’. This time the strncpy_s function at line 24 will run correctly, however, the function strncat_s at line 27 will now complain about the source size – which is only 1 character – being larger than the destination size.
Test 3: as an exercise, make similar changes to test functions at lines 30, 33, and 36. Comment on the results.
Comparing C-Strings
We use the strcmp function to compare two C-strings. This function returns 0 if both strings are equal, a negative number if the first string is lexicographically less than the second, and a positive number if the first string is lexicographically greater. Remember that all characters are encoded using the ASCII encoding scheme discussed in chapter 02 – figure 02.
Example 05
In this example, we will test what values are returned by the strcmp for various scenarios. For clarity, first, test the code using single-character strings. Then, you can use larger strings. Compare and explain the results.
Test 1: str1 = a str2 = b Test 2: str1 = b str2 = a Test 3: str1 = a str2 = A Test 4: str1 = B str2 = b Test 5: str1 = a str2 = a Test 6: str1 = A str2 = A Test 7: str1 = abc str2 = abc Test 8: str1 = abca str2 = abcb Test 9: str1 = abcb str2 = abca Test 10: str1 = abca str2 = abcA
Another version of the strcmp function is strncmp. This variant allows you to compare strings up to a specified number of characters, rather than comparing the entire strings. Example 06 demonstrates two of the scenarios where this feature is desirable.
Example 06
In the provided code, line 16, we are only interested to know whether the entered command starts with the word help or not. This can be part of a larger system where users can enter a large number of commands and this information can be useful to optimize the system performance. To test this functionality, you can enter something like help ls, help /s/d, clear dir, hellp/ cd/, or anything else.
Similarly, in line 28, we want to know the type of username entered, assuming there are many different types of usernames. For testing the code, you can enter something like admin_master, admin level 1, user_1, admn-user, or anything else.
Exercise 03
Modify example 04 to create a name badge instead of the shipping label.
Requirements:
- Add a title (Mr./Ms./Mrs./Dr., etc.) before the name with a maximum length of 4 characters
- Add a job title after the name with a maximum length of 20 characters
- The name badge format should be: Title FirstName LastName – JobTitle
- Total badge length should not exceed 40 characters
- Validate that no buffer overflow occurs
Exercise 04
Write a program that captures and displays book details. The program should:
1) Prompt the user to enter the book name, author’s name, publication date, and ISBN.
2) Safely copy and concatenate these details into a single string using the strncpy_s and strncat_s functions.
3) Display the combined book details in a formatted manner.
Example Output:
Enter the book name (max 49 characters):
Enter the author’s name (max 49 characters):
Enter the publication date (max 49 characters):
Enter the ISBN (max 49 characters):
Book Details:
[The Very Hungry Caterpillar] | Author: Eric Carle | Publication Date: June 3, 1969 | ISBN: 978-0399226908
Exercise 05
Write a C++ program that compares two strings. The program should:
1) Prompt the user to enter two strings.
2) Ensure the maximum length for each string is 49 characters.
3) Check if the entire strings are identical using strcmp. If not, find the maximum number of identical characters at the beginning of both strings using strncmp.
4) Display the results to the user.
Example Output:
Enter the first string (max 49 characters): HelloWorld
Enter the second string (max 49 characters): HelloUniverse
The strings are not completely identical.
The maximum number of identical characters at the beginning of both strings is: 5
Strings
Strings are a fundamental part of programming in C++. They allow us to work with sequences of characters, making it easier to handle text. In this section, we will experiment with how working with strings is a lot easier and safer than working with CStrings discussed in the earlier section. In C++, the string class is defined in the <string> library. Thus, to use strings, you must include this library. Strings are first introduced in chapter two. We discuss this topic again here to easily compare strings’ functionality with CStrings.
Example 07
Run the attached code and comment on the results. This example shows how easy to declare, initialize, and concatenate strings. You might want to compare how you did these operations with CStrings. In line 7, we declared four string variables in one line, initializing the first variable and leaving others without initialization. After reading two strings from the console in line 9, line 10 concatenates them together using the + operator. Compilers are smart enough to tell what needs to be done in this statement. If you are using the + operator between two numerical values, it will be understood that the required operation is to add the two numerical values and return their addition. However, if the + operator happens to be placed between two strings, it will be understood that the required operation is to concatenate the two strings into one string. If you try to place the + operator between a string and a numerical value, you will get an error as this operator is not defined. Line 11 shows how easy it is to print out string variables contents in any desired format.
Member Functions
The string class is a powerful class that contains many member functions. In this section, we will discuss a couple of them.
Example 08
This example code tests whether a given text can be read exactly the same in the reversed direction. For example, the text racecar can be read the same from right to left. Similarly, the sentence “No lemon, no melon.” This wordplay is called Palindrome. To simplify the code, the given text must be stripped of all punctuation, including spaces. Additionally, upper-case letters need to be converted to lowercase.
The provided code has two functions: cleanString and isPalindrome. The program starts execution from line 27 in the main function when a user is prompted to enter a sentence and save it into the sentence variable. Then, the isPalindrome function is called in line 30, where it returns either true or false. The isPalindrome function takes the entered text as a parameter passed by reference to make the parameter accessible to all functions. The const modifier is used to protect this parameter from being changed unintentionally. The first line in the isPalindrome function calls the cleanString function, line18. As the name suggests, cleanString function job is to remove all punctuations and convert upper-case characters to lower-case. The first task can be achieved by calling the predefined isalnum function, line 10. This function returns true for only letters and numbers. The tolower function converts upper-case letters to lower-case. cleanString function uses the for-each loop in line 9 that reads the text saved in the str variable character by character and saves it in the c variable.
When the cleanString function returns the cleaned string to the isPalindrome function in line 18, a for loop is used to reverse it. Note how the for loop is written to loop backward from the end of the string to its beginning. Although the length() function returns values in size_t type, which is an unsigned integer type, we used int instead. The reason is in this implementation i, must become negative to terminate the loop, which is not possible with size_t type. Additionally, although string variables can be used as whole objects, it is allowable to access particular characters using the square brackets syntax resembling arrays’ indexes, line 21. Line 23 tests whether the actual entered text by the user cleanStr equals the reversed text. Thus, isPalindrome returns either true or false.
Example 09
Run the attached code and comment on the results. In this example, we use the find member function that searches a string for a specific character. If the character is found, find returns its integer position starting from 0. If not, find returns a special constant defined in the string class called npos, which means ‘no position’.
The code counts how many vowels are there in any given text. This is done by the countVowels function that takes the entered text by a user as a parameter. Then, it compares every character with a predefined list of vowels, including both upper and lower case characters, as in line 8.
The for-each loop is used to iterate over all characters stored in the str variable one by one. The if-statement condition returns true only if the character is found in the vowels list. Otherwise, it returns npos constant where the condition returns false, skipping counting that particular character.
We use the referencing operator :: when we need to tell the compiler where to find a specific definition, thus we wrote string::npos to say that the constant npos is defined in the string class.
To clarify how the find function works, add the below line inside the if-statement block. This line prints what find function returns for every found character c .
cout << “The character (” << c << “) position is ” << vowels.find(c) << endl;
Example 10
Run the provided code several times with different input texts, then comment on the results.
In this example, we want to create two copies of the user input text, vowelGroup and consonantGroup. The first loop modifies the vowelGroup by replacing every consonant letter with _ ignoring punctuation and numbers. The member function replace – line 17 – takes three arguments: the index of the character in the string, the number of characters to replace starting at the first argument index, and the characters to replace the existing characters in the string. The first part of the if condition checks whether a given character exists in the vowels variable defined in line 10. As explained in example 09, the find member function returns npos constant if a character is not found. This means the condition vowels.find(vowelGroup.at(i)) == string::npos returns true for all non-vowel characters. The second part checks whether the character is alpha to ignore punctuation and numbers. isalpha member function defined in the <cctype> library returns true only for upper and lower case letters.
Note in this example, we used the string class member function at( ) to read a specific character in the string rather than using square bracket syntax. Both notations return the same result; however, using at( ) is safer because it checks whether the provided index is a legal index or not. If not, at( ) returns an exception. Using square brackets does not perform this check. Risking reading areas out of the string boundaries. Both notations are popular when working with strings.
The second loop works with similar logic. However, the if condition does not call isalpha because once we know that a character is a vowel, it is certainly an alpha character.
Exercise 06
1) Explain how the for-each loop works without specifying the boundaries of the string.
2) Explain how the isPalindrome function reverses the cleaned string.
3) In the isPalindrome function, the for-loop copies the contents of the cleanStr from end to start, that is, backward:
for (int i = cleanStr.length() – 1; i >= 0; –i) {
reversed += cleanStr[i]; }
Modify only the for-loop to copy the contents of the cleanStr from start to end, that is, forward.
Exercise 07
Modify example 09 to count vowels, consonants, numbers, spaces, punctuation, and any other characters in a given text. Use this sample text to test your code:
Hello, World! This is a test text to include all types of characters: 1) Vowels: a, e, i, o, u (both uppercase and lowercase) 2) Consonants: b, c, d, f, g, h, j, k, l, m, n, p, q, r, s, t, v, w, x, y, z (both uppercase and lowercase) 3) Numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 4) Spaces: (spaces between words) 5) Punctuation: . , ; : ! ? ‘ ” ( ) [ ] { } – _ + = / \ | @ # \$ % ^ & * ~ ` < > 6) Special Symbols: © ® ™ § ¶ 7) Control Characters: \n (newline), \t (tab), \r (carriage return) 8) Extended ASCII/Unicode:
Exercise 08
Write a program that replaces a desired text with another. You must use find and replace member functions in your solution.
Hint: find has an overloaded version that lets you search starting at any desired position. find (wordToFind, position)
Sample run 1:
Sample run 2:
String Comparison
Strings can be compared using comparison operators like ==, !=, <, >, <=, and >=. This is possible because characters are encoded according to the ASCII scheme. When comparing strings, you are actually comparing their ASCII codes character by character until a difference is found or the end of the string is reached. In the following example, we will demonstrate using == and < operators. It should be clear how to sue all other operators in the same manner.
Example 11
Run the provided code and comment on the results. This is a simple word battle game where two players are prompted to enter a single word of their choice. Then, inside the for-loop, the two words will be compared, and the user who chooses a word that comes before the opponent word will be the winner. Of course, to make the game interesting, you can request that only real words be used.
Exercise 09
Implement the linear search algorithm for strings. The linear search algorithm is discussed in Chapter 6.
Sample run1:
Sample run2:
Converting Strings to Numbers and Vice Versa
In some scenarios, it is safer or more convenient to read numerical input as a string and then convert it to a numerical type like int or double. For example, if we ask users to enter a monetary value, some users might include symbols, such as \$45,000. Clearly, you will get an error if you try to read this input directly as an int or double. Similarly, if we ask users to enter dates, users might choose different formats, such as 4/14/2025 or 4-14-2025. The following example clarifies how we can handle such scenarios.
Example 12
Run the attached code and comment on the results. The code will first ask users to enter a monetary value. Because we do not know how the value will be entered, the entered value is initially saved as a string. Then, the for loop will remove all non-digit characters from the entered value. Note how the erase member function of the string class is used to achieve this goal. Lastly, the stod is used to convert the string to double and makes it ready to be included in any desired calculations. stod function throws an exception if its argument contains non-digit characters. There are other similar functions that convert strings to numerical types like stoi (string to integer) , stof (string to float), and others.
The second part of the code is about handling dates. To avoid confusion caused by which delimiter users might use, like / or \ or – , the code will focus on reading the numerical values for month, day, and year, ignoring any other non-digit characters. for-each loop is used to iterate over all characters stored in the string input. Each character is tested, whether being a digit or not, utilizing the isdigit function. Note how the part variable is utilized to separate month, day, and year values. The += operator is overloaded to work with both strings and characters by concatenating the character at the left side to the end of the string at the right side. If month, day, and year strings need to be further processed to check if they fit within a plausible range, they must be converted to integers first. See exercise 10.
Exercise 10
Modify example 12 to check the entered date fits within a plausible range like months from 1 to 12, days from 1 to 31 and year 1900 to 2100 (or any similar range).
On the other hand, there are some scenarios where we want to convert strings to numbers. For example, to format numbers in a certain way or to prevent overflow exceptions. All numerical types have specific range limits to what numbers they can save, such as int or double; review Table 1 in Chapter 2. If a user enters numbers outside of the expected range, an overflow exception might be thrown, breaking the execution of the code. To avoid this problem, it is useful to first read numbers as strings and then test them before converting them into a numerical type. In example 13, we will examine the formatting scenario where converting numbers to string type can be desirable.
Example 13
Run the given code and test it with these numbers: 100999.87888, 123888456, -129900.654, etc. Comment on the results.
The provided code formates the entered numbers by adding a comma after every three digits, rounding decimal numbers to only two decimal places, and adding a \$ sign. The code works by first asking a user to enter any number to format; then, it calls the formatNumber function. The first thing this function does is round numbers to two decimal places, line 34. If a user enters an integer, this line adds two zeros decimal places to format all numbers consistently. To simplify the code, line 35 converts negative numbers to positive numbers using the absolute value fabs function from <cmath> library. The to_string from <string> library converts numbers to strings. to_string function returns numbers with six decimal places. For example, if you entered 123 or 123.45, to_string returns 123.000000 or 123.450000. Therefore, the rounded number stored in the number variable will have four extra 0s added to it. Line 36 trims those extra four 0s utilizing subStr member function of the string class. This function returns part of a string by specifying the start and the end of the returned sub-string.
Now, we are ready to add commas to make reading large numbers easier. The insertCommas function called in line 38 takes the numStr formatted so far as an argument. To add commas at the correct places, a for-loop is used to iterate through the entire numStr string in reverse order. The idea here is to read characters in the numStr from right to left and to save them into a new string variable formattedNumber using the insert member function of the string class. This function takes two arguments: the position where we want to insert a string and the contents of this string. In line 15, we read characters one by one, starting at the end of the numStr and adding them at the beginning of the formattedNumber variable. Thus, although we are reading the numStr reversely, the formattedNumber will be in the correct order at the end of the for-loop.
To accomplish our goal, we have to add commas after detecting the decimal point and reading each digit from right to left in sets of three. To understand how the code works, let’s trace through an example, like 1234567.89. At the very first iteration of the for-loop, the if-statement returns false. Then, the else if part returns false as well, allowing moving forward and reading the second character, which will have the same result. The third read character will be detected as the decimal point character by the if-statement setting pastDecimal to true and count to 0. At the following iteration, the else if block will be selected, incrementing the count variable and testing whether its value equals 3, which is not at this point. After two more iterations and when count equals 3, a comma is inserted at the beginning of the formattedNumber variable. This process repeats until reading the entire numStr string.
Exercise 11
In example 12, change the first regular for loop into a for-each loop. Similarly, change the second for-each loop into a regular for loop.
Exercise 12
After studying example 13, write a program that formats phone numbers as (xxx) xxx – xxxx. The code must return an error message if a 10-digit number is not entered.
Converting CStrings to Strings and Vice Versa
While strings offer a more powerful and user-friendly interface for string manipulation, CStrings are still widely used. CStrings are rooted in the C/C++ programming languages, which means they are widely used in legacy codebases and systems that require compatibility with C libraries. Understanding CStrings is essential for maintaining and interfacing with such code, especially in embedded systems or performance-critical applications where C is prevalent. In certain scenarios, CStrings can be more efficient in terms of memory usage and performance. For example, CStrings do not have the overhead associated with the dynamic memory management that string employs. This can lead to faster execution in situations where string manipulation is minimal and predictable. CStrings provide a lower-level interface to string data, which can be beneficial when you need fine-grained control over memory allocation and string operations. This is particularly useful in systems programming or when working with hardware where resource constraints are a concern. Therefore, there are still valid reasons to learn how to convert strings to CStrings and vice versa.
Because strings are developed after the date of developing the CStrings, strings are designed to recognize CStrings and convert them to string type automatically. For, example,
char CString[] = “C-string”;
string str;
str = CString; // legal statement
The first line is illegal because CStrings does not recognize the =
operator. The second line is illegal because the strcpy function only accepts two arguments of CString type.
Vectors
We have seen previously how convenient it is to use arrays due to their ability to provide fast, direct access to elements through indexing. However, arrays have a notable limitation: fixed size. Once an array is declared with a specific size in C++, that size cannot be changed. This limitation can be restrictive, especially when the number of elements needed is not known in advance or may vary during runtime.
In response to this limitation, vectors were designed. Vectors are dynamic array-like containers from the Standard Template Library (STL). It is part of the C++ Standard Library and provides an efficient way to handle collections of data that can change in size during runtime. A vector can grow or shrink in size as needed. This makes vectors ideal for situations where the number of elements may change during execution, providing both flexibility and ease of use.
The syntax of declaring an empty vector with base type int is vector<int> vectorName; Vectors are template classes, which means they can accept and handle various types. Therefore, we must declare what type we are interested in when declaring the vector using the angel brackets syntax <typeName>.
After declaring the vector, we need to initialize it using one of its member functions called push_back(). This function adds elements to the end of the vector, increasing its size, for example:
vector<double> sample;
sample.push_back(0.0);
sample.push_back(-12.15);
sample.push_back(22.34);
Vector elements, once initialized, can be either read or written using a similar syntax to accessing array elements, using square brackets and indexes starting from 0. However, the square brackets syntax can NOT be used to initialize a vector element. This can be tricky because if you initialize a vector to ten values using the push_back() function, you can only access these initialized elements using the square brackets syntax. As we know, vectors can grow in size; however, if you try to assign values outside the initialized range, unexpected behavior can result.
Example 14
Run the given code and comment on the results. After declaring the vector called temperatures, the vector member function push_back() is used to initialize it. In the for loop, the vector elements are accessed using a syntax similar to the array syntax, temperatures[i]. The vector member function size() returns the number of elements currently stored in the vector, NOT the amount of space that has been allocated for the vector, which can be larger than the number of elements it currently holds. Note that the for loop variant is declared as size_t type, which is unsigned int to match the returned type by size(). The type casting (char)248 is used to print the degree symbol °
in the results. On my system, the ASCII code for this symbol is 248.
Alternate Vector Initialization
Using push_back() can be daunting. Therefore, other methods are available to do so. A list of elements can be assigned to a vector similar to the array syntax: vector<int> sample = {1, 2, 3, 4, 5}; Additionally, we can call a vector constructor that takes an integer argument and initializes that number of elements to 0s: vector<int> sample(10); If we call the size() on this vector, sample.size( ) would return 10. Now, [ ]’s can be used to assign elements 0 through 9. However, if you want to add more elements beyond index 9, you must use push_back() to do so.
Example 15
The attached code has 6 tests to help you understand the behavior of vectors. At the beginning, all code lines are commented out. Then, you will need to comment and uncomment portions of the code to run each test separately. Visual Studio and other similar IDEs provide buttons and keyboard shortcuts to do this easily.
Test 1: Uncomment lines 7 and 8 and run the code. aVector.size() function returns 0 as the size of a declared but not initialized vector.
Test 2: Comment out the previous lines and uncomment lines 11 to 14. After declaring the aVector and initializing the first 10 locations, the aVector.size() function returns 10 as the current size of the vector. Now, it is permissible to use the [ ] notation to read only the initialized locations. Notice that all values stored in the vector are 0s, which are the default values for the type int.
Test 3: Comment out the previous lines and uncomment lines 17 to 20. In this test, we set the for loop condition as aVector.size() + 1 to try accessing a location outside the initialized range. As you expected, an error message will pop up.
Test 4: Comment out the previous lines and uncomment lines 23 to 29. This test shows that after declaring a vector, we can use the square brackets [ ] notation to write to any of the initialized locations.
Test 5: Comment out the previous lines and uncomment lines 32 to 38. Similar to test 3, we try to write outside the initialized range of the vector. Of course, an error message will pop up.
Test 6: Comment out the previous lines and uncomment lines 41 to 49. In this test, we use the if .. else statement to check whether we are writing within the initialized range of the vector locations. If it is true, we can use the square brackets [ ] notation to add values, otherwise, we must use the push_back() function to do so.
What makes vectors powerful is the capability of growing and shrinking their size. You can do this by utilizing the vector class member functions like capacity(), reserve(), and resize().
Example 16
In this example, we will examine vectors’ behavior as they are designed to grow dynamically to meat programs needs. The code has 3 tests. You will need to comment out and comment on portions of the code as instructed.
Test 1: Uncomment lines 11 to 17 and run the code. From the results, we can notice that the capacity increases in a nonlinear manner. In the beginning, it grows slowly by adding a few locations. But later, it grows much faster to meet the program demands.
Test 2: Comment out the previous lines and uncomment lines 20 to 23. In the case that you know the needed vector size, you can reserve a specific amount using the reserve() member function. Using reserve() function can enhance the program performance by avoiding the excessive vector dynamic growing overhead. What happens behind the scenes is that every time the vector becomes full, a new vector is created with a larger capacity. Then, all stored data are copied from the old vector to the new vector. Lastly, the old vector is destroyed. These operations can affect the overall performance of programs.
Although executing line 20 will reserve 100 locations in the vector, line 21 shows that the current size is 0 because reserve() function does not actually initialize the reserved locations. Therefore, if line 23 is executed, an error message will pop up on the screen.
Test 3: Comment out the previous lines and uncomment lines 26 to 47. resize() is similar to size() in that it initializes the number of locations equal to the passed argument. However, resize() can set a new size to the vector either by increasing or decreasing its original size.
Running lines 26 to 31, resize the vector to 10. Then, it is ok to use the [ ] notation to add values to these locations because they are already initialized. Lines 34 to 37 resize the vector to a new size of 20 and print the contents of the entire vector. You will notice that after printing the first 10 values added in the previous step, 10 zeros are printed out as well. Those zeros are the default values added to the initialized locations in the vector after increasing its size from 10 to 20. Lines 41 to 45 replace zeros with values and then print the entire vector contents.
Test 4: Keep the commented-out lines from test 3 unchanged and uncomment lines 49 to 53. When you run these lines, an error message will pop up on the screen. The reason is that after resizing the vector down to 15, the last 5 locations were lost and cannot be accessed anymore.
Exercise 13
Write a program that has a function called enterExpenses that allows users to enter their daily expenses. From the main function, print out the entire list of entered values and calculate and print out the total and the average. As we do not know how many values the user will enter, using Vectros is the best choice. The program should be able to handle the case of an empty vector. To this end, the vector member function empty() can be helpful.
Exercise 14
Complete this table for the discussed member functions of the vector class: resize(), capacity(), reserve(), and empty().
Function | Purpose | Return Type | Key Features | Example |
---|---|---|---|---|
size() | Returns the number of elements currently in the vector. | size_t | Shows the number of elements stored. Does not change the vector contents. | vector v = {1, 2, 3}; cout << “Size: ” << v.size(); |
Exercise 15
Wrap up
This chapter provides an overview of handling C-strings, strings, and vectors. It begins by explaining C-strings, which are arrays of characters ending with a null character (\0
). Despite being outdated, C-strings are still used in legacy systems for their performance benefits. The chapter highlights common issues with C-strings, such as the potential loss of the null character, and discusses the limitations of using operators directly with C-strings. To address these limitations, functions from the <cstring> library, such as strlen, strcpy_s, and strcmp, are introduced.
The chapter then transitions to discuss the advantages of using the string class. Strings are easier and safer to use compared to C-strings, with simple initialization and concatenation using the +
operator. The string class also offers powerful member functions like find, substr, length, insert, and replace, which facilitate various string manipulations.
Vectors, a dynamic array-like container from the Standard Template Library (STL), are also covered. Vectors can grow or shrink in size, making them ideal for situations where the number of elements may change during execution. The chapter explains how to initialize vectors using member functions like push_back, size, capacity, reserve, and resize, and handle dynamic growth efficiently.
Finally, the chapter addresses converting between C-strings and strings, noting that strings can be converted to C-strings using the c_str member function. Practical examples and exercises are included throughout the chapter to reinforce learning and ensure safe and efficient string manipulation and dynamic array handling.
Answer Key
Exercise 01:
Exercise 02:
Exercise 03
Exercise 04:
Exercise 05:
Exercise 06:
1) In the cleanString function, the for-each loop is written as for (char c : str). This loop iterates over each character in the string str without explicitly specifying the boundaries. The for-each loop automatically handles the iteration from the beginning to the end of the string. Here’s how it works:
- Initialization: The loop initializes c to the first character of str .
- Condition: The loop continues as long as there are characters left in str .
- Iteration: After each iteration, the loop moves to the next character in str .
This makes the code concise and easy to read, as you don’t need to manually manage the loop boundaries.
2) In the isPalindrome function, the cleaned string cleanStr is reversed using the for loop.
for (int i = cleanStr.length() – 1; i >= 0; –i) {
reversed += cleanStr[i]; }
Here’s how this loop works:
- Initialization: The loop starts with i set to the last index of cleanStr, which is (cleanStr.length() – 1).
- Condition: The loop continues as long as i is greater than or equal to 0.
- Iteration: In each iteration, the character at index i of cleanStr is appended to the reversed string, and i is decremented by 1.
This effectively constructs the reversed string by appending characters from the end of cleanStr to the beginning.
3) This for loop copies the contents of the cleanStr from start to end, that is, forward.
for (int i = 0; i < cleanStr.length(); ++i) {
reversed = cleanStr[i] + reversed;
}
Exercise 07:
Exercise 08:
Exercise 09:
Exercise 10:
Add the following lines at the end of the example 12 code:
Exercise 11:
The first for loop can be modified as:
The second for loop can be modified as:
Exercise 12:
Exercise 13:
Exercise 14:
Function | Purpose | Return Type | Key Features | Example |
---|---|---|---|---|
size() | Returns the number of elements currently in the vector. | size_t | Shows the number of elements stored. Does not change the vector contents. | vector<int> v = {1, 2, 3}; cout << “Size: ” << v.size(); // Output: Size: 3 |
resize() | Changes the size of the vector by adding or removing memory locations. | void | Expands or shrinks the vector. Newly added memory locations are initialized to a default value. | vector<int> v = {1, 2, 3}; v.resize(5); cout << v.size(); // Output: 5 // v = {1, 2, 3, 0, 0} |
capacity() | Returns the total capacity (allocated memory) of the vector. | size_t | Shows how much memory has been allocated, which may be larger than size(). | vector<int> v; v.reserve(10); cout << v.capacity(); // Output: 10 |
reserve() | Allocates memory for the specified number of elements. | void | Pre-allocates memory to avoid frequent reallocations during push_back() operations. | vector<int> v; v.reserve(10); cout << v.capacity(); // Output: 10 |
empty() | Checks if the vector contains any elements. | bool | Returns true if the vector has no elements, otherwise returns false. | vector<int> v; cout << v.empty(); // Output: true v.push_back(1); cout << v.empty(); // Output: false |
Exercise 15:
1)
vector<int> numbers;
numbers.push_back(10); // Correct: Adds the value 10 to the vector
2)
vector<int> numbers;
numbers.resize(5); // Correct: Sets the size of the vector to 5
3)
vector<int> numbers;
numbers.reserve(10); // Correct: Preallocates memory for 10 elements
4)
vector<int> numbers = {1, 2, 3};
if (numbers.empty() == false) { // Correct: empty() is a member function
cout << “Vector is not empty.”;
}
5)
vector<int> numbers = {1, 2, 3};
cout << numbers.capacity(); // Correct: capacity() returns a value
End of Chapter Exercises
42. The push_back() function adds elements to the beginning of a vector.
43. The size() function returns the total capacity of the vector.
44. The reserve() function initializes the reserved locations in a vector.
45. Accessing a vector element outside its initialized range can result in unexpected behavior.
46. The resize() function can both increase and decrease the size of a vector.
47. The capacity() function returns the number of elements currently stored in the vector.
48. The push_back() function can be used to add elements to a vector only if the index is within the initialized range.
54. Write a program that replaces a selected word with synonyms. The program will:
- Ask the user to enter a sentence and a keyword (the word the user wants to replace with synonyms).
- Try to match the keyword with a predefined set of synonyms.
- If the keyword is found, print out different versions of the sentence using the synonyms.
- If the keyword is not found, display a message saying, “Keyword not found in the sentence.”


55. Write a program that allows users to enter multiple full names and store them in a vector. The program must continuously prompt users to enter names until they type “stop.” After entering names, the program must ask users if they want to continue adding more names. If a user chooses to continue, they can enter more names; otherwise, the program will display all the names entered. Additionally, the program should print the current size and capacity of the vector after each set of names is entered. In the end, all entered names must be printed out to the console.
Sample Run:
56. Write a program to read large text from the console and use a vector to save the text word by word separately. The vector will expand dynamically to save all words in the text. The easiest way to split the input text into separate words is by using the stringstream class from the <sstream> library. You can create a stringstream object like this: stringstream objectName(input);. The objectName can be any variable name you choose, and input is the variable that holds the entire input text. Then, you can use the statement objectName >> word to extract words from the objectName into the word variable. The program must print the list of all words extracted from the input text.
Projects
Project 1: Bubble Sort Algorithm
In Chapter 6, example 11, the bubble sort algorithm was implemented using arrays. Reimplement bubble sort using vectors.
Project 2: The Advantage of Using the reserve()
Function
Objective: Write a program to test how the reserve()
member function can enhance the overall performance of programs.
Instructions:
- Program Structure:
- Your program must have two blocks of code:
- One block using the
reserve()
function. - One block without using the
reserve()
function.
- One block using the
- Your program must have two blocks of code:
- Time Measurement:
- Measure the time before and after running each block.
- Calculate the execution time for each block by subtracting the start time from the end time.
- Implementation:
- Inside each block, write a loop to add items to a vector.
- To get reasonable results, test your code with a large number of items added to the vector, such as 1,000,000, 10,000,000, and 100,000,000.
Hints:
- In C++, you can measure the system time using the
now()
member function of thehigh_resolution_clock
class from the<chrono>
library. now() function is better than time() from the <ctime> library because it can measure time with very fine granularity (nanoseconds, microseconds, or milliseconds depending on the system). - The line auto variableName = high_resolution_clock::now(); captures the current time.
- The auto keyword is used to declare the variable type because the type returned by now() is complex (std::chrono::time_point). Using auto allows the system to deduce the variable type automatically.
- More details about using <chrono> and its classes can be found at: https://en.cppreference.com/w/cpp/chrono