- •1 Масиви
- •2 Рядки і символи
- •Реализация в языках программирования
- •[Править] в языке Си
- •Урок 6. Класс string
- •Класс string
- •Упражнение 3.15
- •Клас string: конструктори і присвоєння рядків, операції, відкладене копіювання Клас string: конструктори і присвоєння рядків, операції, відкладене копіювання
- •Упражнение 3.12
- •Упражнение 3.13
- •Упражнение 3.14
- •2 Рядки і символи
Упражнение 3.12
Найдите ошибки в приведенных ниже операторах:
(a) char ch = "The long and winding road";
(b) int ival = &ch;
(c) char *pc = &ival;
(d) string st( &ch );
(e) pc = 0; (i) pc = '0'; (f) st = pc; (j) st = &ival; (g) ch = pc[0]; (k) ch = *pc; (h) pc = st; (l) *pc = ival;
Упражнение 3.13
Объясните разницу в поведении следующих операторов цикла:
while ( st++ )
++cnt;
while ( *st++ ) ++cnt;
Упражнение 3.14
Даны две семантически эквивалентные программы. Первая использует встроенный строковый тип, вторая – класс string:
// ***** Реализация с использованием C-строк *****
#include <iostream>
#include <cstring>
int main() { int errors = 0; const char *pc = "a very long literal string";
for ( int ix = 0; ix < 1000000; ++ix ) { int len = strlen( pc ); char *pc2 = new char[ len + 1 ]; strcpy( pc2, pc ); if ( strcmp( pc2, pc )) ++errors;
delete [] pc2; } cout << "C-строки: " << errors << " ошибок.\n"; }
// ***** Реализация с использованием класса string *****
#include <iostream> #include <string>
int main() { int errors = 0; string str( "a very long literal string" );
for ( int ix = 0; ix < 1000000; ++ix ) { int len = str.size(); string str2 = str; if ( str != str2 ) } cout << "класс string: " << errors << " ошибок.\n; }
Что эти программы делают? Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?
Упражнение 3.15
Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения
Клас string: конструктори і присвоєння рядків, операції, відкладене копіювання Клас string: конструктори і присвоєння рядків, операції, відкладене копіювання
Найпоширенішим типом, що вживається практично у кожному проекті, є тип string . Його можна запрограмувати самому, але вживання власного типу string створить проблеми сумісності, утруднить вживання програми в інших умовах. Тому вживають стандартний клас string , що є компонентом стандартної бібліотеки С++. Але клас string сам по собі цікавий, щоб окремо вивчити його можливу реалізацію.
Почнемо з вимог до класу string . Він повинен забезпечувати:
Створення рядка символів.
Ініціалізацію рядка символів символьним масивом або іншим рядком.
Копіювання одного рядка до іншого (присвоєння).
Читання та запис окремих символів рядка.
Порівняння двох рядків на рівність.
Конкатенацію двох рядків.
Обрахунок довжини рядка.
Перевірку рядка на пустоту.
Знищення рядка символів.
Приклади використання
string st1; // створення порожнього рядка
//створення і ініціалізація
string st2( "Приклад ініціалізації" );
string st3 = st2; //теж ініціалізація
string st4 (st2); //ще одна ініціалізація
st1 = st2; //копіювання присвоєнням
cout << st1[1]; //читання символу
st1[1] = ’п’; //заміна символу
if (st3 == st2) //порівняння рядків на рівність
cout << “рівні”;
st1 = st2 + st3; //конкатенація
st2 = st3 + ”рядка”; //теж конкатенація
st2 += st3; //конкатенація з дописуванням в кінець рядка
cout << st.size(); //обчислення довжини рядка
if ( st1.empty()) //перевірка на пустоту
cout << ”рядок порожній”;
~string st1; //знищення рядка
Перетворення char* до string:
string s1;
const char *pc = "a character array";
s1 = pc; // правильно
Навпаки не працює:
char *str = s1; // помилка компіляції
//Перетворення виконує класна функція c_str ():
char *str = sl.c_str(); // майже вірно
const char *str = sl.c_str(); // зовсім вірно
Ось фрагмент специфікації класу string . Це дещо спрощений у запису текст заголовного файлу із стандартної бібліотеки. Особливих пояснень цей текст не потребує, оскільки призначення класу звичайне. Мінімальні пояснення до тексту дані в коментарях.
class string
{
public:
typedef unsigned long size_type;
typedef char* iterator;
// Максимально можлива довжину тексту визначимо як
// const string::size_type string::npos = -1;
// оскільки максимально можливе число — це
// npos = 4 294 967 295 (2**32-1)
static const size_type npos;
// Набір конструкторів і деструктор
string();
string(const string& _X);
string(const char* _S);
// Буде використано перших _N символів з рядка _S
string(const char* _S, size_type _N);
~string();
// Оператор індексування
string& operator[](size_type _P0);
// Варіанти присвоєння
string& operator=(const string& _X);
string& operator=(const char* _S);
string& operator=(const char _C);
// Присвоєння з конкатенацією
string& operator+=(const string& _X);
string& operator+=(const char* _S);
// Конкатенація
string operator+(const string& _X) const;
// Порівняння рядків
bool operator==(const string& _X) const;
bool operator!=(const string& _X) const;
bool operator>(const string& _X) const;
bool operator>=(const string& _X) const;
bool operator< (const string& _X) const;
bool operator<=(const string& _X) const;
// Ввід-вивід
friend operator>>(istream &is, string& _X);
friend operator>>(ostream &os, const string& _X)
// Перетворення string в const char*
const char* c_str() const;
// Властивості рядка (розмір, пустота)
size_type length() const;
size_type size() const;
size_type capacity() const;
bool empty() const;
// Очищення рядка
void clear();
// Перестановка
void swap(string& _X);
friend void swap(string& _X, string& _Y);
// Пошук підрядка
size_type find
(const string& _X, size_type _P = 0) const;
// Взяття підрядка
string substr
(size_type _P = 0, size_type _M = npos) const;
// Ітератори
iterator begin();
iterator end();
// Дописування в кінець
string& append(const string& _X);
string& append(const char* _S);
string& append(const char* _S, size_type _N);
// Функції присвоєння
string& assign(const string& _X);
string& assign
(const string& _X, size_type _P, size_type _M);
string& assign(const char* _S);
string& assign(const char* _S, size_type _N);
string& assign(const char _C);
// Лексикографічне порівняння частин рядків
bool compare(const string& _X) const;
bool compare(size_type _P, size_type _M,
const string& _X) const;
// Вставка підрядка
string& insert(size_type _P0, const string& _X);
string& insert(size_type _P0,
const string& _X, size_type _P, size_type _M);
string& insert(size_type _P0, const char* _S);
string& insert(size_type _P0,
const char* _S, size_type _M);
// Стирання підрядка
string& erase(size_type _P0 = 0,
size_type _M = npos);
// Заміна підрядка
string& replace(size_type _P0,
size_type _N0, const string& _X);
string& replace(size_type _P0,
size_type _N0, const char* _S);
string& replace(iterator _F,
iterator _L, const string& _X);
private:
char* _allocator;
size_type _len;
size_type _capacity;
};
Дуже корисною вправою була б спроба самостійної власної реалізації операцій і функцій над рядками. А ми розглянемо ще одну специфікацію рядків, а саме рядків з відкладеним копіюванням DeferredString , запропоновану Б.Страуструпом в Спеціальному виданні С++.
class DeferredString
{
private:
// Спільне зберігання однакових рядків різних об’єктів
struct Srep; //representation
Srep* rep;
public:
// Розрізнення індексування при читанні і запису
class Cref; //reference to char
// Генерування виключень при виході індексів за межі
class Range(); //errors
DeferredString();
DeferredString(const DeferredString&);
DeferredString(const char*);
~DeferredString();
DeferredString& operator=( const DeferredString&);
DeferredString& operator=( const char* );
// Перевірка виходу індексу за межі рядка
void check(int i) const;
char read(int i) const;
void write (int i, char c);
Cref operator[](int i);
char operator[](int i) const;
int size()const;
};
Нижче наведено реалізацію цього класу. Порожній рядок, як і будь-який новий, завжди викликає конструктор зображення:
DeferredString::DeferredString()
{
rep = new Srep(0,"");
}
DeferredString::DeferredString(const char* s)
{
rep=new Srep(strlen(s),s);
}
Копіювальний конструктор створює ще одне посилання на існуюче зображення
DeferredString::DeferredString(const DeferredString& x)
{
x.rep->n++;
rep=x.rep; //shared representation
}
Деструктор знищує об'єкт лише у тому випадку, коли на нього немає більше посилань. Інакше кількість посилань зменшується на одиницю
DeferredString::DeferredString(const DeferredString& x)
{
x.rep->n++;
rep=x.rep; //shared representation
}
Як завжди, присвоєння відрізняється від коструктора необхідністю розпорядитися старим значенням лівої частини
DeferredString& DeferredString::
operator=(const DeferredString &x)
{
x.rep->n++;
// якщо присвоєння виду st=st
// то одночасно із зменшенням rep->n
// зменшиться також x.rep->n
// Старе значення витирається лише тоді,
// коли воно більш ніким не використовується
if (--rep->n==0) delete rep;
rep=x.rep;
return *this;
}
DeferredString& DeferredString::operator=(const char* s)
{
// Перевіряємо, чи хтось користується старим значенням
if (rep->n == 1)
// Якщо ні, виконуємо присвоєння на місці
rep->assign(strlen(s),s);
else
{
// Зменшуємо лічильник посилань
rep->n--;
// Будуємо нове зображення
rep=new Srep(strlen(s),s);
}
return *this;
}
Реалізації перевірки виходу індексу за межі і читання символу за його номером очевидні
void DeferredString::check(int i) const
{
if (i<0||rep-<sz<=i)
{
throw Range();
}
}
char DeferredString::read(int i) const
{
return rep->s[i];
}
Запис нового символу приводить до створення нового зображення. Можна було б передбачити вдосконалення реалізації для випадку, коли новий символ співпадає зі старим
void DeferredString::write (int i, char c)
{
rep=rep->getOwnCopy();
rep->s[i]=c;
}
Оператор індексування тепер перевірить спочатку правильність індексу, а потім створить об'єкт Cref (про нього нижче)
DeferredString::Cref DeferredString::operator[](int i)
{
check(i);
return Cref(*this,i);
}
char DeferredString::operator[](int i) const
{
check(i);
return rep->s[i];
}
int DeferredString::size()const
{
return rep->sz;
}
Структура спільного зображення має досить зрозумілу специфікацію
struct DeferredString::Srep
{
char* s; //pointer to characters
int sz; //number of elements
int n; //number of references
Srep (int nsz, const char* p);
~Srep();
Srep* getOwnCopy();
void assign (int nsz, const char* p);
private:
Srep (const Srep&);
Srep& operator= (const Srep&);
};
Зверніть увагу на закриту частину структури. До неї внесені оператори, замовчуваними версіями яких можна було б ненароком зіпсувати всю систему відкладеного копіювання.
Конструктор створює нове зображення
Зверніть увагу на закриту частину структури. До неї внесені оператори, замовчуваними версіями яких можна було б ненароком зіпсувати всю систему відкладеного копіювання. Конструктор створює нове зображення
деструктор його знищує.
DeferredString::Srep::~Srep()
{
delete [] s;
}
Тут відбувається явне копіювання рядка із створенням нового зображення
DeferredString::Srep* DeferredString::Srep::getOwnCopy()
{
if (n==1) return this;
n--;
return new Srep(sz,s);
}
Найнижчий рівень реалізації присвоєння
void DeferredString::Srep::assign(int nsz, const char* p)
{
if (sz!=nsz)
{
delete [] s;
sz=nsz;
s = new char[sz+1];
}
strcpy(s,p);
}
Ось клас для розпізнавання читання і запису
class DeferredString::Cref
{
friend class DeferredString;
DeferredString & s;
int i;
Cref(DeferredString& ss, int ii): s(ss), i(ii){}
public:
operator char() const {return s.read(i);}
void operator=(char c) {s.write(i,c);}
};
Корисно провести детальне спостереження за поведінкою об'єктів з відкладеним копіюванням.
Введення/виведення рядків у стилі C++
Використовуються класи cin (метод getline) і cout.
Приклад
#include <iostream.h>
void main()
{
const int n=20;
char st[n];
cout<<"Ввести рядок="
cin.getline(st,n);
cout<<"Уведений рядок="<<st<<endl;
}
Пояснення: Метод getline() зчитує з вхідного потоку n-1 символів або менше (якщо символ пеходу рядка \n зустрінеться раніш) і записує їх у строкову змінну st. При цьому з вхідного потоку вилучається символ переходу рядка, а в рядок додається завершальний нуль-символ. Якщо у вхідному потоці більш n-1 символів, то при виконанні наступної операції введення будуть зчитуватися символи, що залишилися.
Для виведення рядка можна використовувати метод cout.
Функції для роботи з рядками
Об'єднання рядків
char* strcat(char* Рядок1, const char* Рядок2);
Поєднує рядки Рядок1 і Рядок2, результат заноситься в рядок Рядок1.
Копіювання рядка
char* strcpy(char *Рядок1, const char *Рядок2);
Копіює вміст рядка Рядок2 у рядок Рядок1, поки не зустрінеться нуль-символ.
char* strncpy(char *Рядок1, const char *Рядок2, n);
Копіює вміст рядка Рядок2 у рядок Рядок1, але не більш n символів.
Визначення довжини рядка
int strlen(const char *Рядок);
Повертає довжину рядка. Нуль-символ при цьому не враховується.
Порівняння рядків
int strcmp(const char *Рядок1, const char *Рядок2);
Виконує посимвольне порівняння рядків (порівнюються коди символів).
Повертає 0, якщо рядки рівні, негативне число, якщо Рядок1<Рядок2 і позитивне число, якщо Рядок1>Рядок2.
Заповнення рядка
char* strset(char *Рядок, char Символ);
Заповнює рядок заданим символом.
Пошук символу в рядку
char* strchr(const char *Рядок,int Символ);
Повертає вказівник на перший знайдений символ або null, якщо символ не знайдений.
Пошук підрядку в рядку
char* strstr(const char *Рядок1,const char *Рядок2);
Сканує рядок Рядок1 на предмет входження підрядку Рядок2. Повертає вказівник на символ з якого у рядку Рядок1 починається підрядок Рядок2 або null, якщо входження не знайдено.
Примітка – Для використання строкових функцій необхідний заголовний
файл <string.h>.
Приклад
#include <iostream.h>
#include <conio.h>
#include <string.h>
void main()
{
char s1[20];
char s2[15];
char s3[40];
int L1,L2,cm;
char *ch,*st;
clrscr();
//Введення двох рядків
//Примітка - Рядки вводити без пробілів!
cout<<"Ввести рядок s1=";
cin>>s1;
cout<<"Ввести рядок s2=";
cin>>s2;
//Визначення довжини рядків
L1=strlen(s1);
L2=strlen(s2);
cout<<"Довжина рядка s1="<<L1<<endl;
cout<<"Довжина рядка s2="<<L2<<endl;
//Копіювання рядка s1 у s3
strcpy(s3,s1);
cout<<"Рядок s3 після копіювання="<<s3<<endl;
//З'єднання рядків s3 і s2
strcat(s3,s2);
cout<<"Рядок s3 після з'єднання="<<s3<<endl;
//Порівняння рядків s1 і s2
cm=strcmp(s1,s2);
if (cm>0) cout<<"Рядок s1 більше s2"<<endl;
if (cm<0) cout<<"Рядок s2 більше s1"<<endl;
if (cm==0) cout<<"Рядки рівні"<<endl;
//Пошук символу
ch=strchr(s1,'s');
if (ch) cout<<"Символ s у позиції="<<ch-s1<<endl;
//Пошук підрядка
st=strstr(s1,"sdf");
if (st) cout<<"Підрядок sdf у позиції="<<st-s1<<endl;
}
Особливості введення/виведення рядків і символів
Введення рядка може здійснюватися також як і введення числових даних за допомогою функції scanf() або вхідного потоку cin. Але в цьому випадку будуть зчитуватися тільки символи до першого пробілу. Вирішити цю проблему можна такими способами:
