Manipulation and Memory Management of std::string

A practical guide covering the most useful methods and operators for working with std::string objects and their memory
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we’ll finish off our exploration of the std::string type by investigating:

  • The most useful std::string methods and operators for manipulating the string’s contents
  • How to use generic standard library algorithms with std::string objects, drastically increasing our options
  • The std::basic_string class, which allows us to customize the character type our strings use
  • How memory is managed within standard library strings, and how we can intervene in that process to get some performance improvements

Appending Content

We can append content to the end of an existing string, using the append() method, or the += operator:

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hello"};
  Greeting.append(" World");
  Greeting += '!';

  std::cout << Greeting;
}
Hello World!

The append() method also has some alternative signatures, giving us more options.

For example, we can append a sequence of characters:

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hello"};
  Greeting.append(10, '!');
  std::cout << Greeting;
}
Hello!!!!!!!!!!

Or we can append a substring by passing the starting position in the string to append, and the number of characters to append:

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hello "};
  std::string Options{"World,Everyone,Friends"};

  // Starting from position 15, append 7 characters
  Greeting.append(Options, 15, 7);

  std::cout << Greeting;
}
Hello Friends

append_range()

We can also append a range to a string using the append_range() method. This will typically be the case when what we want to append is in a different type of container, or the result of an algorithm:

#include <iostream>
#include <string>
#include <array>

int main(){
  std::string Greeting{"Hello "};
  std::array Characters{
    'W', 'o', 'r', 'l', 'd'};

  Greeting.append_range(Characters);

  std::cout << Greeting;
}
Hello World

Note: append_range() was added to the specification in C++23, so may not yet be available on all compilers.

Inserting Content

If we need to insert content into the middle of a string, we have two main options: insert() and insert_range()

The most common use of insert() involves passing the insertion position, and the string to insert:

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hello!"};
  Greeting.insert(5, " World");
  std::cout << Greeting;
}
Hello World!

insert_range()

Since C++23, we also have the insert_range() method, which allows us to insert content from any range-based container. Unlike the basic insert(), the insert_range() requires an iterator to specify the insertion position.

std::string objects come with the usual suite of iterators which we can generate from methods like begin():

#include <iostream>
#include <string>
#include <array>

int main(){
  std::string Greeting{"Hello!"};
  std::array Characters{
    ' ', 'W', 'o', 'r', 'l', 'd'};

  Greeting.insert_range(Greeting.begin() + 5,
                        Characters);

  std::cout << Greeting;
}
Hello World!

Note: insert_range() was added to the specification in C++23, so may not yet be available on all compilers.

We cover iterators in more detail in our earlier chapter, covering containers:

Erasing Contents

The erase() method gives us the ability to remove all or some of the characters in our string.

In its most basic usage, the erase() method accepts a starting position, and the quantity of characters to erase.

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hello World!"};

  // Starting from index 5, erase 6 characters
  Greeting.erase(5, 6);

  std::cout << Greeting;
}
Hello!

Both arguments to erase() are optional. By default, the first argument is 0, ie the beginning of the string, and the second argument is the end of the string:

// Erase all characters
MyString.erase();

// Erase everything except the first 5 characters
MyString.erase(5);

// Erase the first 6 characters
MyString.erase({}, 6);

Replacing Contents

Finally, we can replace parts of our string, effectively combining both an erase() and an insert().

The most common usage of the replace() function involves replacing part of the calling string with new content. This involves passing three arguments:

  • The position of the first character in the string to replace
  • The number of characters to replace
  • The string to replace it with

The number of characters to replace does not need to be the same as the number of characters we’re replacing it with. The string will shrink or grow as needed:

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hello World!"};

  // Starting from index 6, replace 5 characters
  Greeting.replace(6, 5, "Everyone");

  std::cout << Greeting;
}
Hello Everyone!

replace_with_range()

Since C++23, we also have the replace_with_range() function. It behaves similarly but allows the replacement content to be sourced from a range. Instead of a starting index and a count, we provide an iterator pair representing the range of characters we want to replace in the original string:

#include <iostream>
#include <string>
#include <array>

int main(){
  std::string Greeting{"Hello World!"};
  std::array Characters{
    'E', 'v', 'e', 'r', 'y', 'o', 'n', 'e'};

  Greeting.replace_with_range(
    Greeting.begin() + 6, Greeting.begin() + 11,
    Characters);

  std::cout << Greeting;
}
Hello Everyone!

Note: replace_with_range() was added to the specification in C++23, so may not yet be available on all compilers.

Iterators and Algorithms

Like many standard library collections, std::string includes the usual suite of iterators. This allows traversal through its characters in a standardized way.

The most basic implication of this is that we use a range-based for loop to iterate over the characters of a std::string:

#include <iostream>
#include <string>

int main(){
  std::string Greeting{"Hey"};

  for (const char& Character : Greeting) {
    std::cout << Character << '\n';
  }
}
H
e
y

These iterators also allow std::strings to be compatible with iterator-based algorithms.

In this example, we use the std::reverse() algorithm to reverse a std::string:

#include <iostream>
#include <string>
#include <algorithm>

int main(){
  std::string String{"12345"};
  std::reverse(String.begin(), String.end());
  std::cout << String;
}
54321

Since std::string objects are also ranges, we can use all the range-based features we covered in the earlier algorithms chapter, such as views, projection, and range-based algorithms.

Here, we approximate the number of words in a string by using the std::ranges::count() algorithm to return the number of spaces it has:

#include <algorithm>
#include <iostream>
#include <string>

int main(){
  std::string String{"The quick brown fox"};

  std::cout << "The string has "
    << std::ranges::count(String, ' ') + 1
    << " words";
}
The string has 4 words

Setting the Character Type

Each character in a std::string has the basic char type, but we can change this.

In reality, std::string is nothing more than an alias for another type: std::basic_string<char>.

That means it is simply defining the template type of a std::basic_string, but we can do that ourselves:

#include <iostream>
#include <string>

int main(){
  std::basic_string<char> String{"Hello\n"};

  std::cout << String;

  if constexpr (std::same_as<
    decltype(String), std::string>) {
    std::cout << "That was a std::string";
  }
}
Hello
That was a std::string

As such, we can also use any other character type, not just a basic char. Here, we create a string of "wide" characters, using the wchar_t type.

Before we do this, there are two things to note. First, we cannot initialize "wide" character strings from regular character strings, like a std::string, a char*, or a char[].

Instead, we need to use wide character strings, such as a wchar_t* or wchar_t[]. The language provides a literal for creating these: L"" which we’re using below:

std::basic_string<wchar_t> String{L"Hello"};

Secondly, we also can’t send wide characters to a stream intended for narrow characters. We need to send them to a stream that accepts wide characters. std::wcout is the wide variation of std::cout

#include <iostream>
#include <string>

int main(){
  std::basic_string<wchar_t> String{L"Hello"};
  std::wcout << String;
}
Hello

We covered "wide" characters in our earlier lesson on Characters:

As an aside, similar to std::string being an alias, most of the std::basic_string variations using other built-in characters have also been aliased to shorter names.

For example, std::basic_string<wchar_t> is available through the alias std::wstring

Memory Management

At this point, the similarities between a std::string and a dynamic array are hopefully clear. The std::string container manages memory in much the same way as a resizable array, such as std::vector.

It occupies a block of memory in which to store its characters.

If that memory is too small to accommodate an action we take, such as appending a load of additional characters, the std::string class takes care of that for us behind the scenes.

It finds a new, larger block of memory to occupy, and it copies all the characters to this new location and then releases its previous memory.

In most cases, we can just ignore that mechanism, but it’s worth spending a moment to understand what’s going on, especially as we can get some performance benefits from intervening.

Similar to std::vector, the std::basic_string types have three methods to investigate the current state of the string’s memory situation:

  • size() returns the number of characters that are currently in the string. This is synonymous with the length() method.
  • capacity() returns how many characters can be stored before the string needs to reallocate more memory and move to a bigger home
  • max_size() is the maximum size the string can grow to, in the current environment. Most actions that would take the string’s size beyond this will throw a std::length_error exception
#include <iostream>
#include <string>

int main(){
  std::string String{"Hello"};

  std::cout << "Size: " << String.size();
  std::cout << "\nCapacity: "
    << String.capacity();
  std::cout << "\nMax Size: "
    << String.max_size();
}
Size: 5
Capacity: 15
Max Size: 9223372036854775807

Manipulating the Memory Allocation

Occasionally, we may want to intervene in this process. There are three main methods for doing this

reserve()

The resizing process can be a performance drain. Reallocating memory and moving the characters to the new location takes time.

If we know a string is going to grow through the duration of our program, it can be helpful to just reserve enough space right from the start, reducing the number of reallocations needed.

We can do this by calling the reserve() method, passing a number representing how many characters we want to reserve space for. Our string will be moved to a location that can accommodate that capacity or slightly more:

#include <iostream>
#include <string>

int main(){
  std::string String{"Hello"};

  std::cout << "Capacity: "
    << String.capacity();

  String.reserve(1'000);
  std::cout << "\nNew Capacity: "
    << String.capacity();
}
Capacity: 15
New Capacity: 1007

As of C++20, if the argument we pass to reserve() is lower than the current capacity, the function call has no effect.

With older compilers, it may reduce the capacity() of the string, but not below the size(). That is, it will only reduce excess capacity - existing characters will not be removed.

resize()

The resize() method changes the current size of the string. If the argument we pass to resize() is smaller than the current size, excess characters are pruned:

#include <iostream>
#include <string>

int main(){
  std::string String{"Hello World"};
  String.resize(5);

  std::cout << String;
}
Hello

If the argument is larger than the current size, we fill the surplus with additional characters. These will be null characters (\0) by default. However, we can pass a second argument to resize() to specify what we want the surplus to be filled with:

#include <iostream>
#include <string>

int main(){
  std::string String{"Hello"};
  String.resize(10, '!');

  std::cout << String;
}
Hello!!!!!

shrink_to_fit()

The shrink_to_fit() function requests that the capacity of the string be reduced to match the size(), or marginally more. This is useful if we no longer expect our string to grow, and we want to release the surplus memory:

#include <iostream>
#include <string>

int main(){
  std::string String;
  String.reserve(1'000);

  for (int i = 0; i < 100; ++i) {
    String += "Hello";
  }

  std::cout << "Size: " << String.
    size();
  std::cout << "\nCapacity: " << String.
    capacity();

  String.shrink_to_fit();

  std::cout << "\n\nSize: " << String.
    size();
  std::cout << "\nCapacity: " << String.
    capacity();
}
Size: 500
Capacity: 1007

Size: 500
Capacity: 511

Summary

In this lesson, we've explored more of the capabilities of std::string, covering everything from basic modifications to advanced memory management techniques.

Main Points Learned

  • How to append content to a std::string using append() and the += operator.
  • The use of insert() and insert_range() for adding content at specific positions within a string.
  • Removing characters from a string with erase() and its different overloads.
  • Replacing parts of a string using replace() and understanding the replace_with_range() function.
  • Utilizing std::string iterators and standard library algorithms to manipulate and analyze string content.
  • Changing the character type of a std::string to wide characters using std::basic_string<wchar_t>.
  • Managing std::string memory with methods like reserve(), resize(), and shrink_to_fit() to optimize performance.

Was this lesson useful?

Next Lesson

Regular Expressions

An introduction to regular expressions, and how to use them in C++ with the standard library's regex, regex_match, and regex_search
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Strings and Streams
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Regular Expressions

An introduction to regular expressions, and how to use them in C++ with the standard library's regex, regex_match, and regex_search
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved