Różnica między typami string i char [] w C ++

126

Znam trochę C, a teraz przyjrzę się C ++. Jestem przyzwyczajony do znakowania tablic do obsługi ciągów znaków C, ale kiedy patrzę na kod C ++, widzę przykłady używające zarówno typu ciągów, jak i tablic znaków:

#include <iostream>
#include <string>
using namespace std;

int main () {
  string mystr;
  cout << "What's your name? ";
  getline (cin, mystr);
  cout << "Hello " << mystr << ".\n";
  cout << "What is your favorite team? ";
  getline (cin, mystr);
  cout << "I like " << mystr << " too!\n";
  return 0;
}

i

#include <iostream>
using namespace std;

int main () {
  char name[256], title[256];

  cout << "Enter your name: ";
  cin.getline (name,256);

  cout << "Enter your favourite movie: ";
  cin.getline (title,256);

  cout << name << "'s favourite movie is " << title;

  return 0;
}

(oba przykłady z http://www.cplusplus.com )

Przypuszczam, że jest to często zadawane i (oczywiste?) Pytanie, na które udzielono odpowiedzi, ale byłoby miło, gdyby ktoś mógł mi powiedzieć, jaka jest dokładnie różnica między tymi dwoma sposobami radzenia sobie z ciągami znaków w C ++ (wydajność, integracja z API, sposób, w jaki każdy z nich jest lepszy, ...).

Dziękuję Ci.

ramosg
źródło
To może pomóc: C ++ char * vs std :: string
Wael Dalloul

Odpowiedzi:

187

Tablica znaków to po prostu tablica znaków:

  • Jeśli zostanie przydzielony na stosie (jak w Twoim przykładzie), zawsze będzie zajmował np. 256 bajtów bez względu na długość zawartego w nim tekstu
  • Jeśli zostanie przydzielony na stercie (za pomocą malloc () lub new char []), jesteś odpowiedzialny za późniejsze zwolnienie pamięci i zawsze będziesz mieć narzut alokacji sterty.
  • Jeśli skopiujesz tekst zawierający więcej niż 256 znaków do tablicy, może się on zawiesić, wygenerować brzydkie komunikaty potwierdzające lub spowodować niewyjaśnione (błędne) zachowanie w innym miejscu twojego programu.
  • Aby określić długość tekstu, tablicę należy przeskanować, znak po znaku, w poszukiwaniu znaku \ 0.

Łańcuch to klasa, która zawiera tablicę znaków, ale automatycznie zarządza nią za Ciebie. Większość implementacji ciągów ma wbudowaną tablicę 16 znaków (więc krótkie łańcuchy nie fragmentują sterty) i używają sterty dla dłuższych ciągów.

Możesz uzyskać dostęp do tablicy znaków ciągu w następujący sposób:

std::string myString = "Hello World";
const char *myStringChars = myString.c_str();

Ciągi C ++ mogą zawierać osadzone znaki \ 0, znają ich długość bez liczenia, są szybsze niż tablice znaków przydzielone na sterty dla krótkich tekstów i chronią przed przepełnieniem bufora. Ponadto są bardziej czytelne i łatwiejsze w użyciu.


Jednak ciągi C ++ nie są (bardzo) odpowiednie do użycia poza granicami DLL, ponieważ wymagałoby to od każdego użytkownika takiej funkcji DLL upewnienia się, że używa dokładnie tego samego kompilatora i implementacji środowiska wykonawczego C ++, aby nie ryzykować, że jego klasa łańcuchów zachowa się inaczej.

Zwykle klasa łańcuchowa zwalnia również swoją pamięć sterty na stercie wywołującym, więc będzie mogła ponownie zwolnić pamięć tylko wtedy, gdy używasz udostępnionej (.dll lub .so) wersji środowiska wykonawczego.

W skrócie: używaj ciągów C ++ we wszystkich wewnętrznych funkcjach i metodach. Jeśli kiedykolwiek napiszesz plik .dll lub .so, użyj ciągów C w funkcjach publicznych (dll / tak eksponowanych).

Cygon
źródło
4
Ponadto łańcuchy znaków mają kilka funkcji pomocniczych, które mogą być naprawdę fajne.
Håkon
1
Nie wierzę w trochę o ograniczenia DLL. W bardzo szczególnych okolicznościach może potencjalnie się zepsuć ((jedna biblioteka DLL jest statycznie łączona z inną wersją środowiska uruchomieniowego niż używana przez inne biblioteki DLL) i w takich sytuacjach prawdopodobnie wydarzy się najpierw gorsze rzeczy), ale w ogólnym przypadku, gdy wszyscy używają domyślnego udostępniona wersja standardowego środowiska uruchomieniowego (domyślna) to się nie stanie.
Martin York
2
Przykład: Rozpowszechniasz skompilowane przez VC2008SP1 pliki binarne biblioteki publicznej o nazwie libfoo, która ma std :: string & w swoim publicznym interfejsie API. Teraz ktoś pobiera twój libfoo.dll i przeprowadza kompilację debugowania. Jego std :: string może bardzo dobrze zawierać dodatkowe pola debugowania, powodując przesunięcie wskaźnika dla dynamicznych ciągów znaków.
Cygon
2
Przykład 2: W 2010 roku ktoś pobiera plik libfoo.dll i używa go w swojej aplikacji zbudowanej na VC2010. Jego kod ładuje plik MSVCP100.dll, a plik libfoo.dll nadal ładuje plik MSVCP90.dll -> otrzymujesz dwie sterty -> nie można zwolnić pamięci, błędy asercji w trybie debugowania, jeśli libfoo zmodyfikuje odniesienie do ciągu i poda std :: string z nowym wskaźnik z powrotem.
Cygon
1
Pozostanę tylko przy „Krótko mówiąc: używaj ciągów C ++ we wszystkich wewnętrznych funkcjach i metodach”. Próba zrozumienia twoich przykładów sprawiła, że ​​mój mózg pękł.
Stephen
12

Arkaitz ma rację, że stringjest to typ zarządzany. Oznacza to dla ciebie , że nigdy nie musisz martwić się o długość łańcucha, ani nie musisz martwić się o zwolnienie lub ponowne przydzielenie pamięci ciągu.

Z drugiej strony char[]notacja w powyższym przypadku ograniczyła bufor znaków do dokładnie 256 znaków. Jeśli próbowałeś zapisać więcej niż 256 znaków w tym buforze, w najlepszym przypadku nadpiszesz inną pamięć, którą "posiada" twój program. W najgorszym przypadku spróbujesz nadpisać pamięć, której nie posiadasz, a system operacyjny zabije Twój program na miejscu.

Konkluzja? Łańcuchy są dużo bardziej przyjazne programistom, a znaki char [] są dużo bardziej wydajne dla komputera.

Mark Rushakoff
źródło
4
W najgorszym przypadku inne osoby nadpisują pamięć i uruchamiają złośliwy kod na Twoim komputerze. Zobacz także przepełnienie bufora .
David Johnstone,
6

Cóż, typ string jest w pełni zarządzaną klasą dla ciągów znaków, podczas gdy char [] jest nadal tym, czym był w C, tablicą bajtów reprezentującą ciąg znaków dla ciebie.

Jeśli chodzi o API i bibliotekę standardową, wszystko jest zaimplementowane w postaci łańcuchów znaków, a nie znaków [], ale wciąż jest wiele funkcji z biblioteki libc, które otrzymują znak [], więc może być konieczne użycie go do tych celów, poza tym chciałbym zawsze używaj std :: string.

Jeśli chodzi o wydajność, oczywiście surowy bufor niezarządzanej pamięci prawie zawsze będzie szybszy dla wielu rzeczy, ale weź pod uwagę na przykład porównywanie ciągów znaków, std :: string ma zawsze rozmiar do sprawdzenia go jako pierwszy, podczas gdy z char [] ty trzeba porównywać znak po znaku.

Arkaitz Jimenez
źródło
5

Osobiście nie widzę żadnego powodu, dla którego ktoś chciałby używać char * lub char [] poza kompatybilnością ze starym kodem. std :: string nie jest wolniejsze niż użycie c-string, z wyjątkiem tego, że zajmie się ponownym przydzieleniem za Ciebie. Możesz ustawić jego rozmiar podczas tworzenia, a tym samym uniknąć ponownego przydzielania, jeśli chcesz. Jego operator indeksujący ([]) zapewnia stały dostęp do czasu (i jest w każdym znaczeniu tego słowa dokładnie tym samym, co użycie indeksatora łańcuchów c). Użycie metody at daje również sprawdzone granice bezpieczeństwa, coś, czego nie dostajesz z c-stringami, chyba że to napiszesz. Twój kompilator najczęściej optymalizuje użycie indeksatora w trybie wydania. Łatwo jest bawić się sznurkami c; rzeczy takie jak delete vs delete [], bezpieczeństwo wyjątków, a nawet jak ponownie przydzielić łańcuch c.

A kiedy masz do czynienia z zaawansowanymi koncepcjami, takimi jak posiadanie łańcuchów COW i nie-COW dla MT itp., Będziesz potrzebować std :: string.

Jeśli martwisz się o kopie, tak długo, jak używasz referencji i stałych referencji, gdziekolwiek możesz, nie będziesz mieć żadnych narzutów z powodu kopii i jest to to samo, co robisz z c-stringiem.

Abhay
źródło
+1 Chociaż nie rozważałeś problemów związanych z implementacją, takich jak kompatybilność DLL, masz COW.
co z tego, że wiem, że moja tablica znaków ma 12 bajtów? Jeśli utworzę w tym celu wystąpienie ciągu, może to nie być naprawdę wydajne, prawda?
David 天宇 Wong,
@David: Jeśli masz bardzo wrażliwy na perfekcję kod, to tak. Możesz rozważyć wywołanie ctor std :: string jako narzut oprócz inicjalizacji składowych std :: string. Pamiętaj jednak, że przedwczesna optymalizacja spowodowała, że ​​wiele baz kodu zostało niepotrzebnie stylizowanych na C, więc bądź ostrożny.
Abhay,
1

Ciągi znaków mają funkcje pomocnicze i automatycznie zarządzają tablicami znaków. Możesz łączyć łańcuchy, w przypadku tablicy znaków musisz skopiować ją do nowej tablicy, łańcuchy mogą zmieniać swoją długość w czasie wykonywania. Tablica znaków jest trudniejsza w zarządzaniu niż ciągiem, a niektóre funkcje mogą akceptować tylko ciąg jako dane wejściowe, co wymaga konwersji tablicy na ciąg. Lepiej jest użyć ciągów, zostały one stworzone, abyś nie musiał używać tablic. Gdyby tablice były obiektywnie lepsze, nie mielibyśmy łańcuchów.


źródło
0

Pomyśl o (char *) jako string.begin (). Zasadnicza różnica polega na tym, że (char *) jest iteratorem, a std :: string jest kontenerem. Jeśli trzymasz się podstawowych łańcuchów, a (char *) da ci to, co robi std :: string :: iterator. Możesz użyć (char *), jeśli chcesz skorzystać z iteratora, a także zgodności z C, ale to wyjątek, a nie reguła. Jak zawsze uważaj na unieważnienie iteratora. Kiedy ludzie mówią (char *) nie jest bezpieczny, to właśnie mają na myśli. Jest tak samo bezpieczny, jak każdy inny iterator C ++.

Samuel Danielson
źródło
0

Jedną z różnic jest zakończenie zerowe (\ 0).

W C i C ++, char * lub char [] przyjmą wskaźnik do pojedynczego znaku jako parametr i będą śledzić w pamięci aż do osiągnięcia wartości pamięci 0 (często nazywanej terminatorem zerowym).

Ciągi C ++ mogą zawierać osadzone znaki \ 0, znają ich długość bez liczenia.

#include<stdio.h>
#include<string.h>
#include<iostream>

using namespace std;

void NullTerminatedString(string str){
   int NUll_term = 3;
   str[NUll_term] = '\0';       // specific character is kept as NULL in string
   cout << str << endl <<endl <<endl;
}

void NullTerminatedChar(char *str){
   int NUll_term = 3;
   str[NUll_term] = 0;     // from specific, all the character are removed 
   cout << str << endl;
}

int main(){
  string str = "Feels Happy";
  printf("string = %s\n", str.c_str());
  printf("strlen = %d\n", strlen(str.c_str()));  
  printf("size = %d\n", str.size());  
  printf("sizeof = %d\n", sizeof(str)); // sizeof std::string class  and compiler dependent
  NullTerminatedString(str);


  char str1[12] = "Feels Happy";
  printf("char[] = %s\n", str1);
  printf("strlen = %d\n", strlen(str1));
  printf("sizeof = %d\n", sizeof(str1));    // sizeof char array
  NullTerminatedChar(str1);
  return 0;
}

Wynik:

strlen = 11
size = 11
sizeof = 32  
Fee s Happy


strlen = 11
sizeof = 12
Fee
Eswaran Pandi
źródło
„ze specyficznego, wszystkie znaki są usuwane” nie, nie są „usuwane”, wypisywanie wskaźnika znaku drukuje tylko do terminatora null. (ponieważ jest to jedyny sposób, w jaki znak * zna koniec) klasa string sama zna pełny rozmiar, więc po prostu go używa. jeśli znasz rozmiar swojego znaku *, możesz sam wydrukować / użyć wszystkich znaków.
Puddle