Implementacja klasy String
Pracuję nad projektem (to język) i dla tego projektu zdecydowałem się na styl C ++ niskiego poziomu, co oznacza tworzenie własnych typów danych. Niedawno skończyłem tworzyć własną implementację klasy ciągów. Właściwie to pierwszy raz, kiedy utworzyłem klasę stringów, więc może robię wiele rzeczy źle. O ile wiem, przetestowałem go i działa i robi to, do czego jest przeznaczony, ale mogę coś przeoczyć lub coś nie jest najlepszą praktyką.
Moim celem było tutaj stworzenie klasy string „niskiego poziomu C ++”, co oznacza, że stworzyłbym wszystko sam, bez używania żadnych nagłówków.
Mam wszystko w jednym .hpliku i wiem, że nie jest to najlepszy sposób na zrobienie tego, ale nie jestem wielkim fanem dzielenia kodu, gdy jest to tylko mała klasa.
Oto przegląd metod i tego, co robią, oraz dlaczego je wybrałem (zauważ, że to nie jest rzeczywisty .hplik, pokazuję to tylko, aby zapewnić kontekst i przegląd tego, co robię):
class string
{
public:
string();
string(const char* buffer);
string(const string& buffer);
~string();
public:
string& operator=(const char* buffer);
string& operator=(const string& buffer);
void operator+=(char buffer);
void operator+=(const char* buffer);
void operator+=(const string& buffer);
bool operator==(const char* buffer) const;
bool operator==(const string& buffer) const;
bool operator!=(const char* buffer) const;
bool operator!=(const string& buffer) const;
char operator[](int index) const;
char& operator[](int index);
public:
int length() const;
// returns the actual string
const char* get() const;
private:
int str_len(const char* buffer) const;
// given a block of memory 'dest', fill that with characters from 'buffer'
void str_cpy(char* dest, int dest_size, const char* buffer);
void str_cpy(char* dest, int dest_size, const string& buffer);
// allocate a given size of memory
char* str_alc(int size);
private:
int size;
char* str;
};
Jak więc widzisz, nie jest to nic specjalnego, tylko kilka podstawowych funkcji, które powinny wystarczyć dla mojego projektu. Kilka komentarzy do kodu:
Zdecydowałem się dodać get()metodę zamiast czegoś podobnego, operator const char*()ponieważ uważam, że przeciążenie operatora wystarczyłoby i chcę, aby dostęp do rzeczywistego ciągu był bardziej wyraźny.
Również uwaga na temat metod prywatnych, są one zasadniczo bardzo podobne do metod, które można znaleźć w <string.h>nagłówku, jak strncpy()i str_len().
Oto rzeczywisty string.hplik:
#pragma once
namespace night { // 'night' is the project I'm working on
class string
{
public:
string()
{
size = 0;
str = str_alc(1);
}
string(const char* buffer)
{
size = str_len(buffer);
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
}
string(const string& buffer)
{
size = buffer.size;
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
}
~string()
{
delete[] str;
}
public:
string& operator=(const char* buffer)
{
delete[] str;
size = str_len(buffer);
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
return *this;
}
string& operator=(const string& buffer)
{
delete[] str;
size = buffer.size;
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
return *this;
}
void operator+=(char buffer)
{
char* temp = str_alc(size + 2);
str_cpy(temp, size + 2, str);
temp[size] = buffer;
temp[size + 1] = '\0';
delete[] str;
size += 1;
str = temp;
}
void operator+=(const char* buffer)
{
size += str_len(buffer);
char* temp = str_alc(size + 1);
str_cpy(temp, size + 1, str);
str_cpy(temp, size + 1, buffer);
delete[] str;
str = temp;
}
void operator+=(const string& buffer)
{
size += buffer.size;
char* temp = str_alc(size + 1);
str_cpy(temp, size + 1, str);
str_cpy(temp, size + 1, buffer);
delete[] str;
str = temp;
}
bool operator==(const char* buffer) const
{
if (size != str_len(buffer))
return false;
for (int a = 0; a < size; ++a)
{
if (str[a] != buffer[a])
return false;
}
return true;
}
bool operator==(const string& buffer) const
{
return operator==(buffer.str);
}
bool operator!=(const char* buffer) const
{
return !operator==(buffer);
}
bool operator!=(const string& buffer) const
{
return !operator==(buffer.str);
}
char operator[](int index) const
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
char& operator[](int index)
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
public:
int length() const
{
return size;
}
const char* get() const
{
return str;
}
private:
int str_len(const char* buffer) const
{
int length = 0;
for (int a = 0; buffer[a] != '\0'; ++a)
length += 1;
return length;
}
void str_cpy(char* dest, int dest_size, const char* buffer)
{
int start = 0;
while (dest[start] != '\0')
start += 1;
if (dest_size - start < str_len(buffer))
throw "[fatal error] function 'void str_cpy(char* dest, const char* buffer)' does not have enough space";
for (int a = 0; a < str_len(buffer); ++a)
dest[start + a] = buffer[a];
dest[start + str_len(buffer)] = '\0';
}
void str_cpy(char* dest, int dest_size, const string& buffer)
{
int start = 0;
while (dest[start] != '\0')
start += 1;
if (dest_size - start < buffer.size)
throw "[fatal error] function 'void str_cpy(char* dest, const string& buffer)' does not have enough space";
for (int a = 0; a < buffer.size; ++a)
dest[start + a] = buffer.str[a];
dest[start + buffer.size] = '\0';
}
char* str_alc(int size)
{
char* buffer;
try {
// set the new string to contain null-terminators by default
buffer = new char[size]{ '\0' };
}
catch (...) {
throw "[fatal error] function 'char* str_alc(int size)' cannot allocate enough memory";
}
return buffer;
}
private:
int size;
char* str;
};
} // namespace night
Jako przykład, oto jak go użyjesz:
int main()
{
night::string test = "class";
test += ' ';
test += "string";
std::cout << test.get() << '\n';
night::string test1 = "string class";
test = test1;
test[0] = 'S';
test[7] = 'C';
std::cout << test.get() << '\n';
night::string test2 = "String Class";
std::cout << (test == test2) << '\n';
std::cout << (test != test2) << '\n';
}
Oto mój główny obszar zainteresowania:
Czy potrzebuję konstruktora przenoszenia i operatora przypisania przenoszenia? Wiem, że to nie jest konieczne, ale czy w tym przypadku zrobiłyby dużą różnicę?
Czy metody prywatne są skuteczne? Czy można je ulepszyć?
Czy metoda jest
str_alc()dobra? Czy jest dobrą praktyką zawijanienewinstrukcji try-catch? I czy powinienem\0domyślnie wypełnić ciąg s? Czy może to powoduje więcej szkody niż pożytku?
Mam też drobne pytanie, czy nazwa parametru bufferjest właściwym wyborem? Nie bardzo wiem, jak nazwać te parametry ...
Wszelkie inne opinie również są bardzo mile widziane!
Dzięki!
Odpowiedzi
Dlaczego konwertujesz wyjątek, który ma znaczenie, na bezsensowny ciąg?
char* str_alc(int size)
{
char* buffer;
try {
// set the new string to contain null-terminators by default
buffer = new char[size]{ '\0' };
}
catch (...) {
throw "[fatal error] function 'char* str_alc(int size)' cannot allocate enough memory";
}
return buffer;
}
Sposób zgłaszania błędu będzie ostatecznie zależeć od miejsca jego wykrycia. Nie powinieneś po prostu ponownie rzucać sznurka. Wyłap wyjątek w miejscu, w którym zgłaszasz błędy, i przekonwertuj na odpowiedni komunikat o błędzie w tym momencie. Lub zgłoś bardziej znaczący typ wyjątku (nie ciąg znaków).
Ponadto, jeśli zamierzasz to sprawdzić, po prostu użyj niezrzucającej wersji new, a następnie sprawdź, czy bufor nie ma wartości null i zgłoś nowy wyjątek.
Nie wymyślaj na nowo wychodzących funkcji:
int str_len(const char* buffer) const
Istnieje już funkcja C do tego i gwarantuję, że NIE jest wolniejsza niż twoja wersja i prawdopodobnie o rząd wielkości szybsza.
void str_cpy(char* dest, int dest_size, const char* buffer)
Znowu są już funkcje kopiujące C-String. Jeśli zamierzasz je wymyślić na nowo, użyj algorytmów C ++ do kopiowania bajtów, zamiast ręcznego pisania pętli.
Jeśli porównujesz dwa obiekty łańcuchowe. Poświęcasz się porównywaniu obiektu typu string z C-String jako najbardziej ogólny przypadek.
bool operator==(const char* buffer) const
{
if (size != str_len(buffer))
return false;
for (int a = 0; a < size; ++a)
{
if (str[a] != buffer[a])
return false;
}
return true;
}
bool operator==(const string& buffer) const
{
return operator==(buffer.str);
}
bool operator!=(const char* buffer) const
{
return !operator==(buffer);
}
bool operator!=(const string& buffer) const
{
return !operator==(buffer.str);
}
W rezultacie obliczasz długość łańcucha dla obiektu, dla którego już znasz długość ciągu!
Wdrożyłeś sprawdzone operator[]:
char operator[](int index) const
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
char& operator[](int index)
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
W C ++ operator[]jest zwykle odznaczane i używane w sytuacjach, gdy już ustaliłeś, że dostęp jest w granicach, a zatem sprawdzenie jest zbędne.
W C ++ zwykle udostępniamy również niezaznaczoną wersję, więc nie musisz wykonywać ręcznego sprawdzania. W C ++ nazywamy tę wersję at().
for(int loop = 0; loop < str. length(); ++loop) {
std::cout << str[loop]; // Why do I need the index
// checked here (every loop)
// I have already established that
// loop is within bounds by checking
// it against the length of the string.
}
Nie zaimplementowano semantyki przenoszenia.
Nie zaimplementowałeś rozmiaru rezerwowego. Istnieje różnica między bieżącą długością a maksymalną długością, zanim będzie wymagana zmiana rozmiaru.
Twój operator przypisania nie jest bezpieczny.
string& operator=(const char* buffer)
{
delete[] str; // you have modified the object here
size = str_len(buffer);
str = str_alc(size + 1); // This can throw. If it does
// your object is in a bad state
// the member str is pointing at
// memory that has been released
// back to the runtime. Any
// use of this will be broken.
//
// You have to hope that that exception
// is not caught and the application
// exits.
str_cpy(str, size + 1, buffer);
return *this;
}
Prawidłowym sposobem jest zaimplementowanie idiomu kopiowania i zamiany.
string& operator=(const char* buffer)
{
string tmp(buffer); // safely create a copy.
// Now that you have done the copy swap this with tmp
std::swap(size, tmp.size)
std::swap(buffer, tmp.buffer);
return *this;
}
// destructor of tmp is called here.
// it will release the buffer that you just placed into the object
Standardowa wersja biblioteki std::stringimplementuje ładną optymalizację krótkich ciągów znaków oprócz podstawowej wersji dynamicznej alokacji pamięci, którą zaimplementowałeś.
Mogę coś przeoczyć, ale dlaczego nie std::string? Nie rozumiem, dlaczego nie możesz używać STL ani mówić o innych bibliotekach open source. Podałeś jakieś wyjaśnienie, ale nie rozumiem. Wykorzystanie STL i bibliotek open source pozwoli Ci zaoszczędzić mnóstwo czasu na programowanie i debugowanie.
Dla Ciebie wdrożenie stringów - główne problemy:
Dodanie pojedynczego znaku powoduje realokację, która jest straszna pod względem pamięci i wydajności. Zwykle utrzymuje się rozmiar rezerwowy i zwiększa go wykładniczo (x2 lub x1,5 za każdym razem). Nie musisz więc stosować realokacji za każdym razem, gdy ktoś czasami dodaje jeden lub więcej znaków.
Brakuje optymalizacji krótkich ciągów. Gdy ciąg jest wystarczająco krótki, powiedzmy poniżej 32 znaków, nie należy tworzyć alokacji dynamicznej i zamiast tego przechowywać dane lokalnie. W tym celu prawdopodobnie będziesz potrzebować dodatkowego bufora w klasie string. Jest to ważne, ponieważ większość strun jest dość krótka.
Poza tym te problemy powinieneś obsługiwać mniej więcej te same funkcje, które std::stringobsługują. Spójrz na jego API na cppreference .
Mając swoje struny zarówno zakończony zerem i posiadające wyraźne rozmiar jest zły pomysł. C ++ std :: string, zupełnie nieprzypadkowo, nie robi tego.
Możesz przydzielić dodatkowy znak i ustawić go na zero, aby ułatwić konwersję na ciągi w stylu C. Konwertując z lub porównując ciągi C, możesz (i powinieneś) testować pod kątem terminatora null w łańcuchu C. Nigdy nie szukaj terminatora null w żadnym innym miejscu kodu. Użyj size.
Zapomniałeś również zaimplementować semantykę przenoszenia.