Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Бьярн Страуструп - Язык программирования C++

.pdf
Скачиваний:
1042
Добавлен:
17.03.2018
Размер:
3.01 Mб
Скачать

Бьерн Страуструп.

Язык программирования С++

void f()

 

{

 

string s1(10);

// инициализация, а не присваивание

string s2 = s1;

}

 

Теперь только один объект типа string строится конструктором string::string(int), а уничтожаться будет две строки. Дело в том, что пользовательская операция присваивания не применяется к неинициализированному объекту. Достаточно взглянуть на функцию string::operator(), чтобы понять причину этого: указатель p будет тогда иметь неопределенное, по сути случайное значение. Как правило, в операции присваивания предполагается, что ее параметры проинициализированы. Для инициализации типа той, что приведена в этом примере это не так по определению. Следовательно, чтобы справиться с инициализацией нужна похожая, но своя функция:

struct string {

 

char* p;

// размер вектора, на который указывает p

int size;

string(int size) { p = new char[size=sz]; }

~string() { delete p; }

string& operator=(const string&); string(const string&);

};

string::string(const string& a)

{

p=new char[size=sz]; strcpy(p,a.p);

}

Инициализация объекта типа X происходит с помощью конструктора X(const X&). Мы не перестаем повторять, что присваивание и инициализация являются разными операциями. Особенно это важно в тех случаях, когда определен деструктор. Если в классе X есть нетривиальный деструктор, например, производящий освобождение объекта в свободной памяти, вероятнее всего, в этом классе потребуется полный набор функций, чтобы избежать копирования объектов по членам:

class X {

 

// ...

// конструктор, создающий объект

X(something);

X(const X&);

// конструктор копирования

operator=(const X&);

// присваивание:

 

// удаление и копирование

~X();

// деструктор, удаляющий объект

};

 

Есть еще два случая, когда приходится копировать объект: передача параметра функции и возврат ею значения. При передаче параметра неинициализированная переменная, т.е. формальный параметр инициализируется. Семантика этой операции идентична другим видам инициализации. Тоже происходит и при возврате функцией значения, хотя этот случай не такой очевидный. В обоих случаях используется конструктор копирования:

string g(string arg)

{

return arg;

}

main()

{

string s = "asdf"; s = g(s);

}

Очевидно, после вызова g() значение s должно быть "asdf". Не трудно записать в параметр s копию

191

Бьерн Страуструп.

Язык программирования С++

значения s, для этого надо вызвать конструктор копирования для string. Для получения еще одной копии значения s по выходе из g() нужен еще один вызов конструктора string(const string&). На этот раз инициализируется временная переменная, которая затем присваивается s. Для оптимизации одну, но не обе, из подобных операций копирования можно убрать. Естественно, временные переменные, используемые для таких целей, уничтожаются надлежащим образом деструктором string::~string() (см. $$R.12.2).

Если в классе X операция присваивания X::operator=(const X&) и конструктор копирования X::X(const X&) явно не заданы программистом, недостающие операции будут созданы транслятором. Эти созданные функции будут копировать по членам для всех членов класса X. Если члены принимают простые значения, как в случае комплексных чисел, это, то, что нужно, и созданные функции превратятся в простое и оптимальное поразрядное копирование. Если для самих членов определены пользовательские операции копирования, они и будут вызываться соответствующим образом:

class Record {

string name, address, profession; // ...

};

void f(Record& r1)

{

Record r2 = r1;

}

Здесь для копирования каждого члена типа string из объекта r1 будет вызываться string::operator=(const string&). В нашем первом и неполноценном варианте строковый класс имеет член-указатель и деструктор. Поэтому стандартное копирование по членам для него почти наверняка неверно. Транслятор может предупреждать о таких ситуациях.

7.7 Индексация

Операторная функция operator[] задает для объектов классов интерпретацию индексации. Второй параметр этой функций (индекс) может иметь произвольный тип. Это позволяет, например, определять ассоциативные массивы. В качестве примера можно переписать определение из $$2.3.10, где ассоциативный массив использовался в небольшой программе, подсчитывающей число вхождений слов в файле. Там для этого использовалась функция. Мы определим настоящий тип ассоциативного массива:

class assoc {

 

struct pair {

 

char* name;

 

int val;

 

};

 

pair* vec;

 

int max;

 

int free;

// предотвращает копирование

assoc(const assoc&);

assoc& operator=(const assoc&);

// предотвращает копирование

public:

 

assoc(int);

 

int& operator[](const char*);

 

void print_all();

 

};

 

В объекте assoc хранится вектор из структур pair размером max. В переменной free хранится индекс первого свободного элемента вектора.

Чтобы предотвратить копирование объектов assoc, конструктор копирования и операция присваивания описаны как частные. Конструктор выглядит так:

assoc::assoc(int s)

{

192

Бьерн Страуструп.

Язык программирования С++

max = (s<16) ? 16 : s;

 

free = 0;

 

vec = new pair[max];

 

}

 

В реализации используется все тот же неэффективный алгоритм поиска, что и в $$2.3.10. Но теперь, если вектор переполняется, объект assoc увеличивается:

#include <string.h>

int& assoc::operator[](const char* p)

/*

работает с множеством пар (структур pair):

проводит поиск p, возвращает ссылку на целое значение из найденной пары, создает новую пару, если p не найдено

*/

{

register pair* pp;

for (pp=&vec[free-1]; vec<=pp; pp-- )

if (strcmp(p,pp->name) == 0) return pp->val;

if (free == max) { //переполнение: вектор увеличивается pair* nvec = new pair[max*2];

for (int i=0; i<max; i++) nvec[i] = vec[i]; delete vec;

vec = nvec; max = 2*max;

}

pp = &vec[free++];

pp->name = new char[strlen(p)+1];

strcpy(pp->name,p);

// начальное значение = 0

pp->val = 0;

return pp->val;

 

}

Поскольку представление объекта assoc скрыто от пользователя, нужно иметь возможность напечатать его каким-то образом. В следующем разделе будет показано как определить настоящий итератор для такого объекта. Здесь же мы ограничимся простой функцией печати:

void assoc::print_all()

{

for (int i = 0; i<free; i++)

cout << vec[i].name << ": " << vec[i].val << '\n';

}

Наконец, можно написать тривиальную программу:

main()

// подсчет числа

вхождений во входной

{

// поток каждого

слова

 

 

const MAX = 256; // больше длины самого длинного слова char buf[MAX];

assoc vec(512);

while (cin>>buf) vec[buf]++; vec.print_all();

}

Опытные программисты могут заметить, что второй комментарий можно легко опровергнуть. Решить возникающую здесь проблему предлагается в упражнении $$7.14 [20]. Дальнейшее развитие понятие ассоциативного массива получит в $$8.8.

Функция operator[]() должна быть членом класса. Отсюда следует, что эквивалентность x[y] == y[x] может не выполняться, если x объект класса. Обычные отношения эквивалентности, справедливые для операций со встроенными типами, могут не выполняться для пользовательских типов ($$7.2.2, см.

также $$7.9).

193

Бьерн Страуструп.

Язык программирования С++

7.8 Вызов функции

Вызов функции, т.е. конструкцию выражение(список-выражений), можно рассматривать как бинарную операцию, в которой выражение является левым операндом, а список-выражений - правым. Операцию вызова можно перегружать как и другие операции. В функции operator()() список фактических параметров вычисляется и проверяется по типам согласно обычным правилам передачи параметров. Перегрузка операции вызова имеет смысл прежде всего для типов, с которыми возможна только одна операция, а также для тех типов, одна из операций над которыми имеет настолько важное значение, что все остальные в большинстве случаев можно не учитывать.

Мы не дали определения итератора для ассоциативного массива типа assoc. Для этой цели можно определить специальный класс assoc_iterator, задача которого выдавать элементы из assoc в некотором порядке. В итераторе необходимо иметь доступ к данным, хранимым в assoc, поэтому он должен быть описан как friend:

class assoc {

friend class assoc_iterator; pair* vec;

int max; int free; public:

assoc(int);

int& operator[](const char*);

};

 

Итератор можно определить так:

 

class assoc_iterator {

// массив assoc

const assoc* cs;

int i;

// текущий индекс

public:

 

assoc_iterator(const assoc& s) { cs = &s; i = 0; } pair* operator()()

{ return (i<cs->free)? &cs->vec[i++] : 0; }

};

Массив assoc объекта assoc_iterator нужно инициализировать, и при каждом обращении к нему с помощью операторной функции () будет возвращаться указатель на новую пару (структура pair) из этого массива. При достижении конца массива возвращается 0:

main()

// подсчет числа вхождений во входной

{

// поток каждого слова

// больше длины самого длинного слова

const MAX = 256;

char buf[MAX];

 

assoc vec(512);

 

while (cin>>buf) vec[buf]++;

 

assoc_iterator next(vec);

 

pair* p;

while ( p = next(vec) )

cout << p->name << ": " << p->val << '\n';

}

Итератор подобного вида имеет преимущество перед набором функций, решающим ту же задачу: итератор может иметь собственные частные данные, в которых можно хранить информацию о ходе итерации. Обычно важно и то, что можно одновременно запустить сразу несколько итераторов одного типа.

Конечно, использование объектов для представления итераторов непосредственно никак не связано с перегрузкой операций. Одни предпочитают использовать тип итератора с такими операциями, как first(), next() и last(), другим больше нравится перегрузка операции ++ , которая позволяет получить итератор, используемый как указатель (см. $$8.8). Кроме того, операторная функция operator() активно используется для выделения подстрок и индексации многомерных массивов.

194

Бьерн Страуструп.

Язык программирования С++

Функция operator() должна быть функцией-членом.

7.9 Косвенное обращение

Операцию косвенного обращения к члену -> можно определить как унарную постфиксную операцию. Это значит, если есть класс

class Ptr { // ...

X* operator->();

};

объекты класса Ptr могут использоваться для доступа к членам класса X также, как для этой цели используются указатели:

void f(Ptr p)

 

{

 

p->m = 7;

// (p.operator->())->m = 7

}

 

Превращение объекта p в указатель p.operator->() никак не зависит от члена m, на который он указывает. Именно по этой причине operator->() является унарной постфиксной операцией. Однако, мы не вводим новых синтаксических обозначений, так что имя члена по-прежнему должно идти после -> :

void g(Ptr p)

{

X*

q1

=

p->;

//

синтаксическая ошибка

X*

q2

=

p.operator->();

//

нормально

}

Перегрузка операции -> прежде всего используется для создания "хитрых указателей", т.е. объектов, которые помимо использования как указатели позволяют проводить некоторые операции при каждом обращении к указуемому объекту с их помощью. Например, можно определить класс RecPtr для организации доступа к объектам класса Rec, хранимым на диске. Параметром конструктора RecPtr является имя, которое будет использоваться для поиска объекта на диске. При обращении к объекту с помощью функции RecPtr::operator->() он переписывается в основную память, а в конце работы деструктор RecPtr записывает измененный объект обратно на диск.

class RecPtr {

Rec* in_core_address; const char* identifier;

// ...

public:

RecPtr(const char* p)

: identifier(p) { in_core_address = 0; } ~RecPtr()

{ write_to_disc(in_core_address,identifier); } Rec* operator->();

};

Rec* RecPtr::operator->()

{

if (in_core_address == 0)

in_core_address = read_from_disc(identifier); return in_core_address;

}

Использовать это можно так:

main(int argc, const char* argv)

{

for (int i = argc; i; i--) {

RecPtr p(argv[i]);

195

Бьерн Страуструп.

Язык программирования С++

p->update();

}

}

На самом деле, тип RecPtr должен определяться как шаблон типа (см. $$8), а тип структуры Record будет его параметром. Кроме того, настоящая программа будет содержать обработку ошибок и взаимодействие с диском будет организовано не столь примитивно.

Для обычных указателей операция -> эквивалентна операциям, использующим * и []. Так, если описано

Y* p;

то выполняется соотношение

p->m == (*p).m == p[0].m

Как всегда, для определенных пользователем операций такие соотношения не гарантируются. Там, где все-таки такая эквивалентность требуется, ее можно обеспечить:

class X { Y* p; public:

Y* operator->() { return p; }

Y& operator*() { return *p; }

Y& operator[](int i) { return p[i]; }

};

Если в вашем классе определено более одной подобной операции, разумно будет обеспечить эквивалентность, точно так же, как разумно предусмотреть для простой переменной x некоторого класса, в котором есть операции ++, += = и +, чтобы операции ++x и x+=1 были эквивалентны x=x+1.

Перегрузка -> как и перегрузка [] может играть важную роль для целого класса настоящих программ, а не является просто экспериментом ради любопытства. Дело в том, что в программировании понятие косвенности является ключевым, а перегрузка -> дает ясный, прямой эффективный способ представления этого понятия в программе. Есть другая точка зрения на операцию ->, как на средство задать в С++ ограниченный, но полезный вариант понятия делегирования (см. $$12.2.8 и 13.9).

7.10 Инкремент и декремент

Если мы додумались до "хитрых указателей", то логично попробовать переопределить операции инкремента ++ и декремента -- , чтобы получить для классов те возможности, которые эти операции дают для встроенных типов. Такая задача особенно естественна и необходима, если ставится цель заменить тип обычных указателей на тип "хитрых указателей", для которого семантика остается прежней, но появляются некоторые действия динамического контроля. Пусть есть программа с распространенной ошибкой:

void f1(T a) // традиционное использование

{

 

 

T v[200];

 

T* p

= &v[10];

 

p--;

 

// Приехали: `p' настроен вне массива,

*p =

a;

++p;

 

// и это не обнаружено

 

// нормально

*p =

a;

}

Естественно желание заменить указатель p на объект класса CheckedPtrToT, по которому косвенное обращение возможно только при условии, что он действительно указывает на объект. Применять инкремент и декремент к такому указателю будет можно только в том случае, что указатель настроен на объект в границах массива и в результате этих операций получится объект в границах того же массива:

class CheckedPtrToT {

// ...

196

Бьерн Страуструп.

Язык программирования С++

};

void f2(T a) // вариант с контролем

{

T v[200];

CheckedPtrToT p(&v[0],v,200);

p--;

// динамическая ошибка:

*p

= a;

++p;

// `p' вышел за границы массива

// нормально

*p

= a;

}

Инкремент и декремент являются единственными операциями в С++, которые можно использовать как постфиксные и префиксные операции. Следовательно, в определении класса CheckedPtrToT мы должны предусмотреть отдельные функции для префиксных и постфиксных операций инкремента и декремента:

class CheckedPtrToT {

T* p;

T* array; int size; public:

//начальное значение `p'

//связываем с массивом `a' размера `s'

CheckedPtrToT(T* p, T* a, int s);

//начальное значение `p'

//связываем с одиночным объектом

CheckedPtrToT(T* p);

// префиксная

T* operator++();

T* operator++(int);

// постфиксная

T* operator--();

// префиксная

T* operator--(int);

// постфиксная

T& operator*();

// префиксная

};

 

Параметр типа int служит указанием, что функция будет вызываться для постфиксной операции. На самом деле этот параметр является искусственным и никогда не используется, а служит только для различия постфиксной и префиксной операции. Чтобы запомнить, какая версия функции operator++ используется как префиксная операция, достаточно помнить, что префиксной является версия без искусственного параметра, что верно и для всех других унарных арифметических и логических операций. Искусственный параметр используется только для "особых" постфиксных операций ++ и --. С помощью класса CheckedPtrToT пример можно записать так:

void f3(T a) // вариант с контролем

 

{

 

T v[200];

 

CheckedPtrToT p(&v[0],v,200);

 

p.operator--(1);

// динамическая ошибка:

p.operator*() = a;

p.operator++();

// `p' вышел за границы массива

// нормально

p.operator*() = a;

}

 

В упражнении $$7.14 [19] предлагается завершить определение класса CheckedPtrToT, а другим упражнением ($$9.10[2]) является преобразование его в шаблон типа, в котором для сообщений о динамических ошибках используются особые ситуации. Примеры использования операций ++ и -- для итераций можно найти в $$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)

{

 

// защита от случая ``st = st''

x.p->n++;

 

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];

//

ненадежно: возможно переполнение buf

s >>

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)<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(); // ошибка: 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));

}

200

Соседние файлы в предмете Программирование