2.2. String abstractions in C

In the C language, the abstract idea of a string is implemented with an array of characters.

// the char array must be null terminated
char a[] = {'h', 'e', 'l', 'l', 'o', '\0'};  // null == '\0'

char b[] = {'h', 'e', 'l', 'l', 'o', 0};     // null == 0 also

// a quoted literal is just a special case of a char array
char* c = "hello";

Arrays of char that are null terminated are commonly called byte strings or C strings. Given the byte string:

const char* howdy = "hi there!";

In memory, howdy is automatically transformed into:

digraph char_array { fontname = "Bitstream Vera Sans" label="Character array in memory" node [ fontname = "Courier" fontsize = 14 shape = "record" style=filled fillcolor=lightblue ] arr [ label = "{'h'|'i'|' '|'t'|'h'|'e'|'r'|'e'|'!'|'\\0'}"; ] idx [ color = white; fillcolor=white; label = "{howdy[0]|howdy[1]|howdy[2]|howdy[3]|howdy[4]|howdy[5]|howdy[6]|howdy[7]|howdy[8]|howdy[9]}"; ] }

The last character in the array, '\0' is the null character, and is used to indicate the end of the string. The null character is a char equal to 0.

 1#include <iostream>
 2
 3int main () {
 4   if (const char null1 = '\0', null2 = 0;             // C++17
 5       null1 == null2) {
 6        std::cout << "these values are the same\n";
 7    } else {
 8        std::cout << "not the same\n";
 9    }
10}

Note

Care must be taken to ensure that the array is large enough to hold all of the characters AND the null terminator. Forgetting to account for null, or having a 'off by one error' is one of the most common mistakes when working with C strings.

A character array may allocate more memory that the characters currently stored in it. An array declaration like this:

char hi[10] = "Hello";

results in an in-memory representation like this:

digraph c { rankdir=LR fontname = "Bitstream Vera Sans" label="Character array with reserve memory" node [ fontname = "Courier" fontsize = 14 shape = "record" style=filled fillcolor=lightblue ] arr [ label = "{H|e|l|l|o|\\0| | | | }" ] }

The array elements after the null are unused, but could be. So, an array of size 10 has space for 4 more characters, 9 total.

C strings have an advantage of being extremely lightweight and simple. Their main disadvantage is that they are too simple for many applications. Their simplicity makes them a pain to work with, which is why the Standard Template Library (STL) contains the string class.

2.3. Working with C strings

The C programming language has a set of functions implementing operations on strings (character strings and byte strings) in the standard library. Various operations, such as copying, concatenation, tokenization, and searching are supported.

The complete list of byte string functions is available from cppreference.com.

It's important to know how to work with byte strings in C++ because the C string functions that C++ inherits from C continue to provide a few capabilities not implemented elsewhere in the STL.

In addition, when the type you have is a byte string, it's just easier and more efficient to manipulate the byte string directly, rather than create a temporary std::string merely to perform an operation and then convert back. In general, you want to try to avoid these kinds of unnecessary type conversions.

 1#include <cstdio>   // printf
 2#include <cstring>  // for strcpy function
 3
 4// In C, a string is literally an array of char
 5//
 6// This is not the same as the string class from the STL.
 7
 8int main()
 9{
10  char a[] = {'h', 'e', 'l', 'l', 'o', '\0'};  // the char array must be null terminated
11  char b[] = {'h', 'e', 'l', 'l', 'o', 0};     // null == 0
12  const char* c = "hello";                     // a string is just a special case of a char array
13
14  for (int i = 0; a[i]; ++i) {                 // explain why this loop terminates
15    printf ("%c %c %c\n", a[i], b[i], c[i]);
16  }
17
18  // copying strings
19  char* d = 0;
20  d = a;      // we only copied a pointer here!
21  if (a == d) {
22    d[0] = 'H';
23  }
24  printf ("\na and d strings:\n  %s %s\n", a, d);
25
26  char e[sizeof(b)];
27  strcpy(e, b);
28  e[0] = 'H';
29  printf ("\nb and e strings:\n  %s %s\n", b, e);
30
31  // a = e;    // compile error.  C strings are not assignable
32
33  return 0;
34}

2.3.1. Change character case

Many languages provide utilities to change character case as part of the string class.

Not C++.

C++ uses the legacy null-terminated byte strings library to provide these features.

Changing character case is a common task and unless you choose to write your own version of these functions, these functions from the STL are the ones you should use.

Many string conversion functions are defined in the cctype header. These functions which C++ inherited from C are often perfectly acceptable, however, there are some notable exceptions, such as the toupper function.

toupper(std::locale())

This version of std::toupper function takes a single char, which can be any character type, is not modified, and returns a character of the same type as the character type provided.

Because of this, the C++ version that uses std::locale is preferred.

 1#include <iostream>
 2#include <cctype>    // C toupper
 3#include <locale>    // C++ toupper
 4
 5int main() {
 6  using std::cout;
 7  char eng[14] = "hello, world!";
 8
 9  // Use C version
10  for (const auto& c: eng) {
11    cout << std::toupper(c) << ' ';
12  }
13  cout << '\n';
14  // Use locale aware version - no conversion
15  for (const auto& c: eng) {
16    cout << "'" << std::toupper(c, std::locale())  << "' ";
17  }
18
19  return 0;
20}

toupper()

The std::toupper function takes a single char, which is not modified, and returns an int. The return value can be used as the upper case version of the input character.

Note

Use the right toupper!

The C version of toupper returns int values, not character values. This can cause unexpected behavior or conversions.

For these resaons, the std::locale() version of toupper is preferred.

See the previous tab for details.

toupper is defined in header cctypes.

This function uses the default C locale to replace the lowercase letters abcdefghijklmnopqrstuvwxyz with respective uppercase letters ABCDEFGHIJKLMNOPQRSTUVWXYZ. Non-ASCII characters are not handled.

Recall that char implicitly convert to int.

 1#include <cctype>
 2#include <iostream>
 3
 4int main() {
 5  char value[15] = "hello, world!";
 6  char& first = value[0];
 7  // failure to assign the return value of toupper
 8  // to a variable is a common source of error.
 9  first =  std::toupper(first);
10  std::cout << value << '\n';
11  return 0;
12}

2.3.2. Copying and comparing C strings

Unlike most of the types we work with in C++, byte strings are simple arrays. Arrays cannot be assigned to each other using operator=. Values in array must be copied one elements at a time, for example using a loop.

Similarly, if you compare two arrays for equivalence, operator== will only return true if both arrays share the same memory address.

This is not what we usually want.

Like copying, in order to check a pair of arrays for equivalence a loop is used to compare each element one at a time until a difference is found.

The copy and compare functions are defined in the cstring header.

strcpy()

The strcpy function takes two byte strings as parameters and copies the source character array including the null terminator, to the destination character array.

 1#include <cstring>
 2#include <iostream>
 3
 4int main()
 5{
 6    const char* src = "Take the test.";
 7
 8    // src[0] = 'M';        // can't modify string literal
 9    char dest[16];          // mutable destination
10    std::strcpy(dest, src);
11    dest[0] = 'M';
12    std::cout << src << '\n' << dest << '\n';
13}

Note the order of the arguments.

A common source of error is to swap the order of the arguments.

strncpy()

The strncpy function copies byte strings, but will copy at most a provided count number of characters.

 1#include <cstring>
 2#include <iostream>
 3
 4int main() {
 5    const char* src = "hi";
 6    char dest[6] = {'a', 'b', 'c', 'd', 'e', 'f'};
 7    std::strncpy(dest, src, 5);
 8
 9    std::cout << "The contents of dest are: ";
10    for (char c : dest) {
11        if (c) {
12            std::cout << c << ' ';
13        } else {
14            std::cout << "nul" << ' ';
15        }
16    }
17    std::cout << '\n';
18}

strcmp()

The strcmp function takes two byte strings as parameters and returns a 0 if every element in both arrays is equal.

If the first operand is greater than the second, then a positive value is returned. If the first operand is less than the second, then a negative value is returned.

 1#include <cstdlib>
 2#include <cstring>
 3#include <iostream>
 4
 5int main() {
 6  const int count = 3;
 7  const char* argv[count] = {"test_prog", "--value", "42"};
 8  int value = 0;
 9
10  for (int i=1; i < 3; ++i) {
11    if (strcmp(argv[i], "--value") == 0) {
12      ++i;
13      if (i < count) {
14        value = atoi(argv[i]);
15      } else {
16        std::cerr <<
17          "Error using '--value' argument: no value specified\n";
18        break;
19      }
20    } else {
21      std::cerr << "Unknown command received!";
22      break;
23    }
24  }
25  std::cout << "value: " << value << '\n';
26}

Note

These functions are not locale-aware.

If you need to make locale aware comparisons, then use strcoll.

strncmp()

The strncmp function takes two byte strings as parameters and returns a 0 if every element in both arrays is equal.

However, this function only compares the at most a specified number of characters.

 1#include <cstring>
 2#include <iostream>
 3
 4void demo(const char* lhs, const char* rhs, int sz) {
 5    int compare = std::strncmp(lhs, rhs, sz);
 6
 7    if(compare == 0) {
 8        std::cout << "First " << sz << " chars of ["
 9                  << lhs << "] equal [" << rhs << "]\n";
10    } else if(compare < 0) {
11        std::cout << "First " << sz << " chars of ["
12                  << lhs << "] precede [" << rhs << "]\n";
13    } else if(compare > 0) {
14        std::cout << "First " << sz << " chars of ["
15                  << lhs << "] follow [" << rhs << "]\n";
16   }
17}
18
19int main() {
20    demo("Hello, world!", "Hello, everybody!", 13);
21    demo("Hello, everybody!", "Hello, world!", 13);
22    demo("Hello, everybody!", "Hello, world!", 7);
23    demo("Hello, everybody!" + 12, "Hello, somebody!" + 11, 5);
24}

Note

These functions are not locale-aware.

If you need to make locale aware comparisons, then use strcoll.

Self Check

Q1

Given the following:

char text[32];
strcpy(text, "hello");
int len = strlen(text);

What is the value of len?

Q2

Fix the errors in the printf line below:

1#include <cstdio>
2#include <string>
3
4int main() {
5  std::string yazoo = "ritish alternative band";
6  char c = 'B';
7
8  printf ("%c%s\n",c, yazoo);
9}

Q3

Which #include is required to use functions such as std::atoi and std::atof?

Q4

Which #include is required to use functions such as std::stoi and std::stol?


More to Explore