Теперь можно привести более осмысленный вариант класса string.
копирование, и используются как константы стандартные строки C++.
#include
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;
}
}
Как и всегда операции присваивания похожи на конструкторы. В них
нужно позаботиться об удалении первого операнда, задающего левую
часть присваивания:
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.1
x = 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)
return p->s[i];
}
В основной программе просто даны несколько примеров применения
строковых операций. Слова из входного потока читаются в строки,
а затем строки печатаются. Это продолжается до тех пор, пока не
будет обнаружена строка 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;
}
В заключении можно обсудить, когда при обращении в закрытую часть
пользовательского типа стоит использовать функции-члены, а когда
функции-друзья. Некоторые функции, например конструкторы, деструкторы
и виртуальные функции ($$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(); // ошибка: X(1).m1() не используется
1.m2(); // ошибка: X(1).m2() не используется
}
Глобальная функция f1() имеет то же свойство ($$4.6.3), поскольку
ее параметр - ссылка без спецификации const. С функциями f2() и
f3() ситуация иная:
void h()
{
f1(1); // ошибка: f1(X(1)) не используется
f2(1); // нормально: f2(X(1));
f3(1); // нормально: f3(X(1));
}
Следовательно операция, изменяющая состояние объекта класса,
должна быть членом или глобальной функцией с параметром-ссылкой
без спецификации const. Операции над основными типами, которые
требуют в качестве операндов адреса (=, *, ++ и т.д.),
для пользовательских типов естественно определять как члены.
Обратно, если требуется неявное преобразование типа для всех
операндов некоторой операции, то реализующая ее функция должна
быть не членом, а глобальной функцией и иметь параметр типа ссылки
со спецификацией const или нессылочный параметр. Так обычно обстоит
дело с функциями, реализующими операции, которые для основных
типов не требуют адресов в качестве операндов (+, -, || и т.д.).
Если операции преобразования типа не определены, то нет
неопровержимых доводов в пользу функции-члена перед функцией-другом
с параметром-ссылкой и наоборот. Бывает, что программисту просто
одна форма записи вызова нравится больше, чем другая.
Например, многим для обозначения функции обращения матрицы m больше
нравится запись inv(m), чем m.inv(). Конечно, если функция
inv() обращает саму матрицу m, а не возвращает новую, обратную m,
матрицу, то inv() должна быть членом.
При всех прочих равных условиях лучше все-таки остановиться
на функции-члене. Можно привести такие доводы. Нельзя гарантировать,
что когда-нибудь не будет определена операция обращения. Нельзя во
всех случаях гарантировать, что будущие изменения не повлекут за
собой изменения в состоянии объекта. Запись вызова функции-члена
ясно показывает программисту, что объект может быть изменен, тогда
как запись с параметром-ссылкой далеко не столь очевидна. Далее,
выражения допустимые в функции-члене могут быть существенно
короче эквивалентных выражений в глобальной функции. Глобальная
функция должна использовать явно заданные параметры, а в
функции-члене можно неявно использовать указатель this. Наконец,
поскольку имена членов не являются глобальными именами, они обычно
оказываются короче, чем имен глобальных функций.
0>
Достарыңызбен бөлісу: