Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Структуровані типи даних у C Масиви.docx
Скачиваний:
2
Добавлен:
01.04.2025
Размер:
1 Мб
Скачать
☆

Упражнение 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. Але в цьому випадку будуть зчитуватися тільки символи до першого пробілу. Вирішити цю проблему можна такими способами: