22,3 — std::string length and capacity

Po utworzeniu ciągów znaków często warto wiedzieć, jak długie są. Tutaj w grę wchodzą operacje na długości i pojemności. Omówimy także różne sposoby konwersji std::string z powrotem na ciągi w stylu C, dzięki czemu można ich używać z funkcjami, które oczekują ciągów typu char*.

Długość ciągu

Długość ciągu jest dość prosta — jest to liczba znaków w ciągu. Istnieją dwie identyczne funkcje do określania długości łańcucha:

size_type string::length() const
size_type string::size() const

  • Obie te funkcje zwracają bieżącą liczbę znaków w ciągu, z wyłączeniem terminatora zerowego.

Przykładowy kod:

std::string s { "012345678" };
std::cout << s.length() << '\n';

Wyjście:

9

Chociaż możliwe jest użycie funkcjilength() do określenia, czy ciąg znaków zawiera jakieś znaki, czy nie, bardziej efektywne jest użyj funkcji pusty():

bool string::empty() const

  • Zwraca wartość true, jeśli ciąg znaków nie zawiera znaków, w przeciwnym razie false.

Przykładowy kod:

std::string string1 { "Not Empty" };
std::cout << (string1.empty() ? "true" : "false") << '\n';
std::string string2; // empty
std::cout << (string2.empty() ? "true" : "false")  << '\n';

Wyjście:

false
true

Jest jeszcze jedna funkcja związana z rozmiarem, której prawdopodobnie nigdy nie użyjesz, ale uwzględnimy ją tutaj dla kompletności:

size_type string::max_size() const

  • Zwraca maksymalną liczbę znaków, jaką może zawierać ciąg.
  • Ta wartość będzie się różnić w zależności od systemu operacyjnego i architektury systemu.

Przykładowy kod:

std::string s { "MyString" };
std::cout << s.max_size() << '\n';

Wyjście:

4294967294

Pojemność ciągu

Pojemność ciągu odzwierciedla ilość pamięci przydzielonej przez ciąg do przechowywania jego zawartości. Wartość ta jest mierzona w znakach ciągu, z wyłączeniem terminatora NULL. Na przykład ciąg znaków o pojemności 8 może pomieścić 8 znaków.

size_type string::capacity() const

  • Zwraca liczbę znaków, które ciąg może pomieścić bez zmiany alokacji.

Przykładowy kod:

std::string s { "01234567" };
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

Wyjście:

Length: 8
Capacity: 15

Pamiętaj, że pojemność jest większa niż długość łańcucha! Chociaż nasz ciąg miał długość 8, faktycznie przydzielił on wystarczającą ilość pamięci na 15 znaków! Dlaczego to zrobiono?

Ważną rzeczą, o której należy pamiętać, jest to, że jeśli użytkownik chce umieścić w ciągu znaków więcej znaków, niż jest w stanie pomieścić ciąg, należy ponownie przydzielić ciąg do większej pojemności. Na przykład, jeśli ciąg znaków miał zarówno długość, jak i pojemność 8, dodanie do niego jakichkolwiek znaków wymusiłoby ponowną alokację. Zwiększając pojemność niż rzeczywisty ciąg, daje to użytkownikowi trochę miejsca w buforze na rozwinięcie ciągu, zanim konieczna będzie ponowna alokacja.

Jak się okazuje, ponowna alokacja jest zła z kilku powodów:

Po pierwsze, ponowna alokacja ciągu jest stosunkowo kosztowna. Najpierw należy przydzielić nową pamięć. Następnie każdy znak ciągu należy skopiować do nowej pamięci. Może to zająć dużo czasu, jeśli ciąg jest duży. Na koniec należy zwolnić przydział starej pamięci. Jeśli wykonujesz wiele ponownych alokacji, proces ten może znacznie spowolnić program.

Po drugie, za każdym razem, gdy łańcuch jest ponownie alokowany, zawartość ciągu zmienia się na nowy adres pamięci. Oznacza to, że wszystkie odniesienia, wskaźniki i iteratory do łańcucha stają się nieprawidłowe!

Zauważ, że nie zawsze jest tak, że ciągi będą przydzielane z pojemnością większą niż długość. Rozważmy następujący program:

std::string s { "0123456789abcde" };
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

Ten program wyprowadza:

Length: 15
Capacity: 15

(Wyniki mogą się różnić w zależności od kompilatora).

Dodajmy jeden znak do łańcucha i obserwujmy zmianę pojemności:

std::string s("0123456789abcde");
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

// Now add a new character
s += "f";
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

Daje to wynik:

Length: 15
Capacity: 15
Length: 16
Capacity: 31

void string::reserve()
void string::reserve(size_type unSize)

  • Drugi wariant tej funkcji ustawia pojemność ciągu na co najmniej unSize (może być większa). Należy pamiętać, że może to wymagać ponownej alokacji.
  • Jeśli zostanie wywołana pierwsza wersja funkcji lub druga wersja z wartością unSize mniejszą niż bieżąca pojemność, funkcja spróbuje zmniejszyć pojemność, aby dopasować ją do długości. To żądanie zmniejszenia wydajności może zostać zignorowane, w zależności od implementacji.

Przykładowy kod:

std::string s { "01234567" };
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

s.reserve(200);
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

s.reserve();
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

Wyjście:

Length: 8
Capacity: 15
Length: 8
Capacity: 207
Length: 8
Capacity: 207

Ten przykład pokazuje dwie interesujące rzeczy. Po pierwsze, chociaż poprosiliśmy o pojemność 200, w rzeczywistości otrzymaliśmy pojemność 207. Zawsze gwarantujemy, że pojemność będzie co najmniej tak duża, jak wymagana jest Twoja prośba, ale może być większa. Następnie poprosiliśmy o zmianę pojemności w celu dopasowania ciągu. To żądanie zostało zignorowane, ponieważ pojemność nie uległa zmianie.

Jeśli wiesz z góry, że będziesz konstruować duży ciąg znaków, wykonując wiele operacji na łańcuchach, które zwiększą jego rozmiar, możesz uniknąć wielokrotnej ponownej alokacji łańcucha, rezerwując od początku wystarczającą pojemność:

#include <iostream>
#include <string>
#include <cstdlib> // for rand() and srand()
#include <ctime> // for time()

int main()
{
    std::srand(std::time(nullptr)); // seed random number generator

    std::string s{}; // length 0
    s.reserve(64); // reserve 64 characters

    // Fill string up with random lower case characters
    for (int count{ 0 }; count < 64; ++count)
        s += 'a' + std::rand() % 26;

    std::cout << s;
}

Wynik działania tego programu będzie się zmieniać za każdym razem, ale oto wynik jednego wykonania:

wzpzujwuaokbakgijqdawvzjqlgcipiiuuxhyfkdppxpyycvytvyxwqsbtielxpy

Zamiast konieczności reallocate s wielokrotnie, ustawiamy pojemność raz, a następnie wypełniamy ciąg. Może to mieć ogromne znaczenie w wydajności podczas konstruowania dużych ciągów poprzez konkatenację.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
52 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze