Б. Страуструп - Язык программирования С++ (1119446), страница 55
Текст из файла (страница 55)
Примеры использования операций ++ и -- дляитераций можно найти в $$8.8.197Бьерн Страуструп.Язык программирования С++7.11 Строковый классТеперь можно привести более осмысленный вариант класса string. В нем подсчитывается число ссылокна строку, чтобы минимизировать копирование, и используются как константы стандартные строки C++.#include <iostream.h>#include <string.h>class string {struct srep {char* s;// указатель на строкуint n;// счетчик числа ссылокsrep() { n = 1; }};srep *p;public:string(const char *);// string x = "abc"string();// string x;string(const string &);// string x = string ...string& operator=(const char *);string& operator=(const string &);~string();char& operator[](int i);friend ostream& operator<<(ostream&, const string&);friend istream& operator>>(istream&, string&);friend int operator==(const string &x, const char *s){ return strcmp(x.p->s,s) == 0; }friend int operator==(const string &x, const string &y){ return strcmp(x.p->s,y.p->s) == 0; }friend int operator!=(const string &x, const char *s){ return strcmp(x.p->s,s) != 0; }friend int operator!=(const string &x, const string &y){ return strcmp(x.p->s,y.p->s) != 0; }};Конструкторы и деструкторы тривиальны:string::string(){p = new srep;p->s = 0;}string::string(const string& x){x.p->n++;p = x.p;}string::string(const char* s){p = new srep;p->s = new char[ strlen(s)+1 ];strcpy(p->s, s);}string::~string(){if (--p->n == 0) {delete[] p->s;delete p;}198Бьерн Страуструп.Язык программирования С++}Как и всегда операции присваивания похожи на конструкторы.
В них нужно позаботиться об удалениипервого операнда, задающего левую часть присваивания:string& string::operator=(const char* s){if (p->n > 1) {// отсоединяемся от старой строкиp->n--;p = new srep;}else// освобождаем строку со старым значениемdelete[] p->s;p->s = new char[ strlen(s)+1 ];strcpy(p->s, s);return *this;}string& string::operator=(const string& x){x.p->n++;// защита от случая ``st = st''if (--p->n == 0) {delete[] p->s;delete p}p = x.p;return *this;}Операция вывода показывает как используется счетчик числа ссылок. Она сопровождает как эхокаждую введенную строку (ввод происходит с помощью операции << , приведенной ниже):ostream& operator<<(ostream& s, const string& x){return s << x.p->s << " [" << x.p->n << "]\n";}Операция ввода происходит с помощью стандартной функции ввода символьной строки ($$10.3.1):istream& operator>>(istream& s, string& x){char buf[256];s >> buf;// ненадежно: возможно переполнение buf// правильное решение см.
в $$10.3.1x = buf;cout << "echo: " << x << '\n';return s;}Операция индексации нужна для доступа к отдельным символам. Индекс контролируется:void error(const char* p){cerr << p << '\n';exit(1);}char& string::operator[](int i){if (i<0 || strlen(p->s)<i) error("недопустимое значение индекса");return p->s[i];}199Бьерн Страуструп.Язык программирования С++В основной программе просто даны несколько примеров применения строковых операций. Слова извходного потока читаются в строки, а затем строки печатаются. Это продолжается до тех пор, пока небудет обнаружена строка done, или закончатся строки для записи слов, или закончится входной поток.Затем печатаются все строки в обратном порядке и программа завершается.int main(){string x[100];int n;cout << " здесь начало \n";for ( n = 0; cin>>x[n]; n++) {if (n==100) {error("слишком много слов");return 99;}string y;cout << (y = x[n]);if (y == "done") break;}cout << "теперь мы идем по словам в обратном порядке \n";for (int i=n-1; 0<=i; i--) cout << x[i];return 0;}7.12 Друзья и членыВ заключении можно обсудить, когда при обращении в закрытую часть пользовательского типа стоитиспользовать функции-члены, а когда функции-друзья.
Некоторые функции, например конструкторы,деструкторы и виртуальные функции ($$R.12), обязаны быть членами, но для других есть возможностьвыбора. Поскольку, описывая функцию как член, мы не вводим нового глобального имени, приотсутствии других доводов следует использовать функции-члены.Рассмотрим простой класс X:class X {// ...X(int);int m1();int m2() const;friend int f1(X&);friend int f2(const X&);friend int f3(X);};Вначале укажем, что члены X::m1() и X::m2() можно вызывать только для объектов класса X.Преобразование X(int) не будет применяться к объекту, для которого вызваны X::m1() или X::m2():void g(){1.m1();1.m2();}// ошибка: X(1).m1() не используется// ошибка: X(1).m2() не используетсяГлобальная функция f1() имеет то же свойство ($$4.6.3), поскольку ее параметр - ссылка безспецификации const.
С функциями f2() и f3() ситуация иная:void h(){f1(1);f2(1);f3(1);}// ошибка: f1(X(1)) не используется// нормально: f2(X(1));// нормально: f3(X(1));200Бьерн Страуструп.Язык программирования С++Следовательно операция, изменяющая состояние объекта класса, должна быть членом или глобальнойфункцией с параметром-ссылкой без спецификации const. Операции над основными типами, которыетребуют в качестве операндов адреса (=, *, ++ и т.д.), для пользовательских типов естественноопределять как члены.Обратно, если требуется неявное преобразование типа для всех операндов некоторой операции, тореализующая ее функция должна быть не членом, а глобальной функцией и иметь параметр типассылки со спецификацией const или нессылочный параметр. Так обычно обстоит дело с функциями,реализующими операции, которые для основных типов не требуют адресов в качестве операндов (+, -, ||и т.д.).Если операции преобразования типа не определены, то нет неопровержимых доводов в пользуфункции-члена перед функцией-другом с параметром-ссылкой и наоборот.
Бывает, что программиступросто одна форма записи вызова нравится больше, чем другая. Например, многим для обозначенияфункции обращения матрицы m больше нравится запись inv(m), чем m.inv(). Конечно, если функцияinv() обращает саму матрицу m, а не возвращает новую, обратную m, матрицу, то inv() должна бытьчленом.При всех прочих равных условиях лучше все-таки остановиться на функции-члене. Можно привеститакие доводы.
Нельзя гарантировать, что когда-нибудь не будет определена операция обращения.Нельзя во всех случаях гарантировать, что будущие изменения не повлекут за собой изменения всостоянии объекта. Запись вызова функции-члена ясно показывает программисту, что объект можетбыть изменен, тогда как запись с параметром-ссылкой далеко не столь очевидна. Далее, выражениядопустимые в функции-члене могут быть существенно короче эквивалентных выражений в глобальнойфункции. Глобальная функция должна использовать явно заданные параметры, а в функции-членеможно неявно использовать указатель this.
Наконец, поскольку имена членов не являются глобальнымиименами, они обычно оказываются короче, чем имен глобальных функций.7.13 ПредостереженияКак и всякое другое языковое средство, перегрузка операций может использоваться разумно инеразумно. В частности, возможностью придавать новый смысл обычным операциям можновоспользоваться так, что программа будет совершенно непостижимой. Представьте, каково будетчитателю, если в своей программе вы переопределили операцию + так, чтобы она обозначалавычитание. Описанный здесь механизм перегрузки будет защищать программиста и пользователя оттаких безрассудств.
Поэтому программист не может изменить ни смысл операций над основнымитипами данных, такими, как int, ни синтаксис выражений и приоритеты операций для них.По всей видимости перегрузку операций имеет смысл использовать для подражания традиционномуиспользованию операций. Запись с обычным вызовом функции можно использовать в тех случаях,когда традиционной записи с базовой операцией не существует, или, когда набор операций, которыедопускают перегрузку, не достаточен, чтобы записать с его помощью нужные действия.7.14 Упражнения1.(*2) Определите итератор для класса string.
Определите операцию конкатенации + и операцию += ,значащую "добавить в конец строки". Какие еще операции вы хотели бы и смогли определить дляэтого класса?2.(*1.5) Определите для строкового класса операцию выделения подстроки с помощью перегрузки ().3.(*3) Определите класс string таким образом, чтобы операцию выделения подстроки можно былоприменять к левой части присваивания. Вначале напишите вариант, в котором строку можноприсваивать подстроке той же длины, а затем вариант с различными длинами строк.4.(*2) Разработайте класс string таким образом, чтобы объекты его трактовались при передачепараметров и присваивании как значения, т.е.
чтобы в классе string копировались самипредставления строк, а не только управляющие структуры.5.(*3) Измените класс string из предыдущего упражнения так, чтобы строки копировались только принеобходимости. Это значит, что нужно хранить одно общее представления двух одинаковых строк201Бьерн Страуструп.Язык программирования С++до тех пор, пока одна из них не изменится. Не пытайтесь задать операцию выделения подстроки,которую одновременно можно применять и к левой части присваивания.6.(*4) Определите класс string, обладающий перечисленными в предыдущих упражненияхсвойствами: объекты его трактуются как значения, копирование является отложенным (т.е.происходит только при необходимости) и операцию выделения подстроки можно применять к левойчасти присваивания.7.(*2) Какие преобразования типа используются в выражениях следующей программы?struct X {int i;X(int);operator+(int);};struct Y {int i;Y(X);operator+(X);operator int();};extern X operator*(X,Y);extern int f(X);X x = 1;Y y = x;int i = 2;int main(){i + 10;x + y + i;f(y);}y + 10;x * X +i;y + y;y + 10 * y;f(7);106 + y;Определите X и Y как целые типы.
Измените программу так, чтобы ее можно было выполнить и онанапечатала значения всех правильных выражений.8.(*2) Определите класс INT, который будет эквивалентен типу int. Подсказка: определите функциюINT::operator int().9.(*1) Определите класс RINT, который будет эквивалентен типу int, за исключением того, чтодопустимыми будут только операции: + (унарный и бинарный), - (унарный и бинарный), *, / и %.Подсказка: не надо определять RINT::operator int().10.