Современная электронная библиотека ModernLib.Net

C++

ModernLib.Net / Программирование / Хилл Мюррей / C++ - Чтение (стр. 12)
Автор: Хилл Мюррей
Жанр: Программирование

 

 


      while (cin»»x) cout««x;
      Действие ввода cin»»x выше возвращает istream amp;. Это знчение неявно преобразуется к значению, которое указывает сотояние cin, а уже это значение может проверяться оператором while (см. #8.4.2). Однако определять преобразование из оного типа в другой так, что при этом теряется информация, обычно не стоит.

6.3.3 Неоднозначности

      Присваивание объекту (или инициализация объекта) класса X является допустимым, если или присваиваемое значение является X, или существует единственное преобразование присваивемого значения в тип X. В некоторых случаях значение нужного типа может быть построено с помощью нескольких применений конструкторов или операций преобразования. Это должно делаться явно; допустим только один уровень неявных преобразований, определенных пользователем. Иногда значение нужного типа может быть посроено более чем одним способом. Такие случаи являются недпустимыми. Например:
      class x (* /* ... */ x(int); x(char*); *); class y (* /* ... */ y(int); *); class z (* /* ... */ z(x); *);
      overload f; x f(x); y f(y);
      z g(z);
      f(1); // недопустимо: неоднозначность f(x(1)) или f(y(1)) f(x(1)); f(y(1)); g(«asdf»); // недопустимо: g(z(x(«asdf»))) не пробуется g(z(«asdf»));
      Определяемые пользователем преобразования рассматриваюся только в том случае, если без них вызов разрешить нельзя. Например:
      class x (* /* ... */ x(int); *) overload h(double), h(x); h(1);
      Вызов мог бы быть проинтерпретирован или как h(double(1)), или как h(x(1)), и был бы недопустим по правилу единственности. Но первая интерпретация использует только стандартное преобразование и она будет выбрана по правилам, приведенным в #4.6.7.
      Правила преобразования не являются ни самыми простыми для реализации и документации, ни наиболее общими из тех, кторые можно было бы разработать. Возьмем требование единтвенности преобразования. Более общий подход разрешил бы копилятору применять любое преобразование, которое он сможет найти; таким образом, не нужно было бы рассматривать все воможные преобразования перед тем, как объявить выражение дпустимым. К сожалению, это означало бы, что смысл программы зависит от того, какое преобразование было найдено. В резултате смысл программы неким образом зависел бы от порядка опсания преобразований. Поскольку они часто находятся в разных исходных файлах (написанных разными людьми), смысл программы будет зависеть от порядка компоновки этих частей вместе. Есть другой вариант – запретить все неявные преобразования. Нет ничего проще, но такое правило приведет либо к неэлегантным пользовательским интерфейсам, либо к бурному росту перегрженных функций, как это было в предыдущем разделе с complex.
      Самый общий подход учитывал бы всю имеющуюся информацию о типах и рассматривал бы все возможные преобразования. Напрмер, если использовать предыдущее описание, то можно было бы обработать aa=f(1), так как тип aa определяет единственность толкования. Если aa является x, то единственное, дающее в рзультате x, который требуется присваиванием, – это f(x(1)), а если aa – это y, то вместоэтого будет использоваться f(y(1)). Самый общий подход справился бы и с g(«asdf»), поскольку единственной интерпретацией этого может быть g(z(x(«asdf»))). Сложность этого подхода в том, что он требует расширенного нализа всего выражения для того, чтобы определить интерпретцию каждой операции и вызова функции. Это приведет к замедлнию компиляции, а также к вызывающим удивление интерпретацим и сообщениям об ошибках, если компилятор рассмотрит преоразования, определенные в библиотеках и т.п. При таком подхде компилятор будет принимать во внимание больше, чем, как можно ожидать, знает пишущий программу программист!

6.4 Константы

      Константы классового типа определить невозможно в том смысле, в каком 1.2 и 12e являются константами типа double. Вместо них, однако, часто можно использовать константы осноных типов, если их реализация обеспечивается с помощью фунций членов. Общий аппарат для этого дают конструкторы, полчающие один параметр. Когда конструкторы просты и подставляются inline, имеет смысл рассмотреть в качестве константы вызов конструктора. Если, например, в «comlpex.h» есть описание класса comlpex, то выражение zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум вызовам функций приведут две операции *, а операция + и конструктор, к которому обращаются для создания comlpex(3) и comlpex(1,2), будут расширены inline.

6.5 Большие Объекты

      При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копрование каждого double заметны, но с ними вполне можно примриться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:
      class matrix (* double m[4][4]; public: matrix(); friend matrix operator+(matrix amp;, matrix amp;); friend matrix operator*(matrix amp;, matrix amp;); *);
      Ссылки позволяют использовать выражения, содержащие обычные арифметические операции над большими объектами, без ненужного копирования. Указатели применять нельзя, потому что невозможно для применения к указателю смысл операции переоределить невозможно. Операцию плюс можно определить так:
      matrix operator+(matrix amp;, matrix amp;); (* matrix sum; for (int i=0; i«4; i++) for (int j=0; j«4; j++) sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j]; return sum; *)
      Эта operator+() обращается к операндам + через ссылки, но возвращает значение объекта. Возврат ссылки может оказатся более эффективным:
      class matrix (* // ... friend matrix amp; operator+(matrix amp;, matrix amp;);
      friend matrix amp; operator*(matrix amp;, matrix amp;); *);
      Это является допустимым, но приводит к сложности с выдлением памяти. Поскольку ссылка на результат будет передваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной. Поскольку часто оперция используется в выражении больше одного раза, результат не может быть и статической переменной. Как правило, его размщают в свободной памяти. Часто копирование возвращаемого знчения оказывается дешевле (по времени выполнения, объему кода и объему данных) и проще программируется.

6.6 Присваивание и Инициализация

      Рассмотрим очень простой класс строк string:
      struct string (* char* p; int size; // размер вектора, на который указывает p
      string(int sz) (* p = new char[size=sz]; *) ~string() (* delete p; *) *);
      Строка – это структура данных, состоящая из вектора сиволов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в #5.10, это может привести к неприятностям. Например:
      void f() (* string s1(10); string s2(20); s1 = s2; *)
      будет размещать два вектора символов, а присваивание s1= s2 будет портить указатель на один из них и дублировать дргой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разруштельными последствиями. Решение этой проблемы состоит в том, чтобы соответствующим образом определить присваивание объетов типа string:
      struct string (* char* p; int size; // размер вектора, на который указывает p
      string(int sz) (* p = new char[size=sz]; *) ~string() (* delete p; *) void operator=(string amp;) *);
      void string::operator=(string amp; a) (* if (this == amp;a) return; // остерегаться s=s; delete p; p=new char[size=a.size]; strcpy(p,a.p); *)
      Это определение string гарантирует,и что предыдущий прмер будет работать как предполагалось. Однако небольшое измнение f() приведет к появлению той же проблемы в новом облике:
 
      void f() (* string s1(10); s2 = s1; *)
      Теперь создается только одна строка, а уничтожается две. К неинициализированному объекту определяемая пользователем операция присваивания не применяется. Беглый взгляд на string::operator=() объясняет, почему было бы неразумно так делать: указатель p будет содержать неопределенное и совешенно случайное значение. Часто операция присваивания полагется на то, что ее аргументы инициализированы. Для такой инциализации, как здесь, это не так по определению. Следовательно, нужно определить похожую, но другую, функцию, чтобы обрабатывать инициализацию:
      struct string (* char* p; int size; // размер вектора, на который указывает p
      string(int sz) (* p = new char[size=sz]; *) ~string() (* delete p; *) void operator=(string amp;); string(string amp;); *);
      void string::string(string amp; a) (* p=new char[size=a.size]; strcpy(p,a.p); *)
      Для типа X инициализацию тем же типом X обрабатывает конструктор X(X amp;). Нельзя не подчеркнуть еще раз, что присвивание и инициализация – разные действия. Это особенно сщественно при описании деструктора. Если класс X имеет контруктор X(X amp;), выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комлект функций, чтобы полностью избежать побитового копирования объектов:
      class X (* // ... X(something); // конструктор: создает объект X( amp;X); // конструктор: копирует в инициализации operator=(X amp;); // присваивание: чистит и копирует ~X(); // деструктор: чистит *);
      Есть еще два случая, когда объект копируется: как парметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого пременная – формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из фунции, хотя это менее очевидно. В обоих случаях будет применен X(X amp;), если он определен:
      string g(string arg) (* return arg; *)
      main() (* string s = «asdf»; s = g(s);
      *) Ясно, что после вызова g() значение s обязано быть «asdf». Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string amp;). Для взятия копии этого значения из g() требуется еще один вызов string(string amp;); на этот раз инициализируемой является врменная переменная, которая затем присваивается s. Такие перменные, естественно, уничтожаются как положено с помощью string::~string() при первой возможности.

6.7 Индексирование

      Чтобы задать смысл индексов для объектов класса, исползуется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п. В качестве примера давайте перпишем пример из #2.3.10, где при написании небольшой програмы для подсчета числа вхождений слов в файле применялся ассциативный массив. Там использовалась функция. Здесь определяется надлежащий тип ассоциативного массива:
      struct pair (* char* name; int val; *);
      class assoc (* pair* vec; int max; int free; public: assoc(int); int amp; operator[](char*); void print_all(); *);
      В assoc хранится вектор пар pair длины max. Индекс певого неиспользованного элемента вектора находится в free. Конструктор выглядит так:
      assoc::assoc(int s) (* max = (s«16) ? s : 16; free = 0; vec = new pair[max]; *)
      При реализации применяется все тот же простой и неэффетивный метод поиска, что использовался в #2.3.10. Однако при переполнении assoc увеличивается:
      #include «string.h»
      int assoc::operator[](char* p) /* работа с множеством пар «pair»: поиск p, возврат ссылки на целую часть его «pair» делает новую «pair», если p не встречалось */ (* register pair* pp;
      for (pp= amp;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 = amp;vec[free++]; pp-»name = new char[strlen(p)+1]; strcpy(pp-»name,p); pp-»val = 0; // начальное значение: 0 return pp-»val; *)
      Поскольку представление assoc скрыто, нам нужен способ его печати. В следующем разделе будет показано, как опредлить подходящий итератор, а здесь мы используем простую фунцию печати:
      vouid 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(); *)

6.8 Вызов Функции

      Вызов функции, то есть запись выражение(список_выражний), можно проинтерпретировать как бинарную операцию, и операцию вызова можно перегружать так же, как и другие оперции. Список параметров функции operator() вычисляется и прверяется в соответствие с обычными правилами передачи парметров. Перегружающая функция может оказаться полезной главным образом для определения типов с единственной операцей и для типов, у которых одна операция настолько преобладет, что другие в большинстве ситуаций можно не принимать во внимание.
      Для типа ассоциативного массива assoc мы не определили итератор. Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к даным, которые хранятся в assoc, поэтому он сделан другом:
      class assoc (* friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int amp; operator[](char*); *);
      Итератор определяется как
 
      class assoc_iterator(* assoc* cs; // текущий массив assoc int i; // текущий индекс public: assoc_iterator(assoc amp; s) (* cs = amp;s; i = 0; *) pair* operator()() (* return (i«cs-»free)? amp;cs-»vec[i++] : 0; *) *);
      Надо инициализировать assoc_iterator для массива assoc, после чего он будет возвращать указатель на новую 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() ) cout «„ p-“name „„ ": " «« p-“val «« «\n“; *)
      0 Итераторный тип вроде этого имеет преимущество перед нбором функций, которые выполняют ту же работу: у него есть собственные закрытые данные для хранения хода итерации. К тму же обычно существенно, чтобы одновременно могли работать много итераторов этого типа.
      Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).

6.9 Класс String

      Вот довольно реалистичный пример класса строк string. В нем производится учет ссылок на строку с целью минимизировать копирование и в качестве констант применяются стандартные символьные строки С++.
      #include «stream.h» #include «string.h»
      class string (* struct srep (* char* s; // указатель на данные int n; // счетчик ссылок *); srep *p;
      public: string(char *); // string x = «abc» string(); // string x; string(string amp;); // string x = string ... string amp; operator=(char *); string amp; operator=(string amp;); ~string(); char amp; operator[](int i);
      friend ostream amp; operator«„(ostream amp;, string amp;); friend istream amp; operator“»(istream amp;, string amp;);
 
      friend int operator==(string amp; x, char* s) (*return strcmp(x.p-»s, s) == 0; *)
      friend int operator==(string amp; x, string amp; y) (*return strcmp(x.p-»s, y.p-»s) == 0; *)
      friend int operator!=(string amp; x, char* s) (*return strcmp(x.p-»s, s) != 0; *)
      friend int operator!=(string amp; x, string amp; y) (*return strcmp(x.p-»s, y.p-»s) != 0; *)
      *);
      Конструкторы и деструкторы просты (как обычно):
      string::string() (* p = new srep; p-»s = 0; p-»n = 1; *)
      string::string(char* s) (* p = new srep; p-»s = new char[ strlen(s)+1 ]; strcpy(p-»s, s); p-»n = 1; *)
      string::string(string amp; x) (* x.p-»n++; p = x.p; *)
      string::~string() (* if (–p-»n == 0) (* delete p-»s; delete p; *) *)
      Как обычно, операции присваивания очень похожи на контрукторы. Они должны обрабатывать очистку своего первого (лвого) операнда:
      string amp; string::operator=(char* s) (* if (p-»n » 1) (* // разъединить себя p-»n–; p = new srep; *) else if (p-»n == 1) delete p-»s;
      p-»s = new char[ strlen(s)+1 ]; strcpy(p-»s, s); p-»n = 1; return *this; *)
      Благоразумно обеспечить, чтобы присваивание объекта смому себе работало правильно:
 
      string amp; string::operator=(string amp; x) (* x.p-»n++; if (–p-»n == 0) (* delete p-»s; delete p; *) p = x.p; return *this; *)
      Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью операции ««, которая определяется позднее):
      ostream amp; operator«„(ostream amp; s, string amp; x) (* return s „„ x.p-“s „« « [“ «« x.p-“n «« «]\n“; *)
      Операция ввода использует стандартную функцию ввода сивольной строки (#8.4.1).
      istream amp; operator»»(istream amp; s, string amp; x) (* char buf[256]; s »» buf; x = buf; cout «„ "echo: " «« x «« «\n“; return s; *)
      Для доступа к отдельным символам предоставлена операция индексирования. Осуществляется проверка индекса:
      void error(char* p) (* cerr «„ p «« «\n“; exit(1); *)
      char amp; string::operator[](int i) (* if (i«0 !! strlen(p-»s)«i) error(„индекс за границами“); return p-»s[i]; *)
      Головная программа просто немного опробует действия над строками. Она читает слова со ввода в строки, а потом эти строки печатает. Она продолжает это делать до тех пор, пока не распознает строку done, которая завершает сохранение слов в строках, или пока не встретит конец файла. После этого она печатает строки в обратном порядке и завершается.
      main() (* string x[100]; int n;
      cout «„ „отсюда начнем\n“; for (n = 0; cin“»x[n]; n++) (* string y; if (n==100) error(«слишком много строк»); cout «„ (y = x[n]); if (y=="done") break; *) cout «« «отсюда мы пройдем обратно\n“;
      for (int i=n-1; 0«=i; i–) cout «« x[i]; *)

6.10 Друзья и Члены

      Теперь, наконец, можно обсудить, в каких случаях для доступа к закрытой части определяемого пользователем типа ипользовать члены, а в каких – друзей. Некоторые операции должны быть членами: конструкторы, деструкторы и виртуальные функции (см. следующую главу), но обычно это зависит от выбра.
      Рассмотрим простой класс X:
      class X (* // ... X(int); int m(); friend int f(X amp;); *);
      Внешне не видно никаких причин делать f(X amp;) другом дполнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для «настоящего объекта», в то время как друг f() мжет вызываться для объекта, созданного с помощью неявного преобразования типа. Например:
      void g() (* 1.m(); // ошибка f(1); // f(x(1)); *)
      Поэтому операция, изменяющая состояние объекта, должна быть членом, а не другом. Для определяемых пользователем тпов операции, требующие в случае фундаментальных типов опранд lvalue (=, *=, ++, *= и т.д.), наиболее естественно оределяются как члены.
      И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, !! и т.д.).
      Если никакие преобразования типа не определены, то окзывается, что нет никаких существенных оснований в пользу члена, если есть друг, который получает ссылочный параметр, и наоборот. В некоторых случаях программист может предпочитать один синтаксис вызова другому. Например, оказывается, что большинство предпочитает для обращения матрицы m запись m.inv (). Конечно, если inv() действительно обращает матрицу m, а не просто возвращает новую матрицу, обратную m, ей следует быть членом.
      При прочих равных условиях выбирайте, чтобы функция была членом: никто не знает, вдруг когда-нибудь кто-то определит операцию преобразования. Невозможно предсказать, потребуют ли будущие изменения изменять состояние объекта. Синтаксис вызва функции члена ясно указывает пользователю, что объект моно изменить; ссылочный параметр является далеко не столь очвидным. Кроме того, выражения в члене могут быть заметно короче выражений в друге. В функции друге надо использовать явный параметр, тогда как в члене можно использовать неявный this. Если только не применяется перегрузка, имена членов
      обычно короче имен друзей.

6.11 Предостережение

      Как и большую часть возможностей в языках программировния, перегрузку операций можно использовать как правильно, так и неправильно. В частности, можно так воспользоваться возможностью определять новые значения старых операций, что они станут почти совсем непостижимы. Представьте, например, с какими сложностями столкнется человек, читающий программу, в которой операция + была переопределена для обозначения вычтания.
      Изложенный аппарат должен уберечь программиста/читателя от худших крайностей применения перегрузки, потому что прораммист предохранен от изменения значения операций для осноных типов данных вроде int, а также потому, что синтаксис вражений и приоритеты операций сохраняются.
      Может быть, разумно применять перегрузку операций главным образом так, чтобы подражать общепринятому применению операций. В тех случаях, когда нет общепринятой операции или имеющееся в С++ множество операций не подходит для имитации общепринятого применения, можно использовать запись вызова функции.

6.12 Упражнения

      1. (*2) Определите итератор для класса string. Определите операцию конкатенации + и операцию «добавить в конец» +=. Какие еще операции над string вы хотели бы иметь возможность осуществлять?
      2. (*1.5) Задайте с помощью перегрузки () операцию выделния подстроки для класса строк.
      3. (*3) Постройте класс string так, чтобы операция выделния подстроки могла использоваться в левой части присвивания. Напишите сначала версию, в которой строка может присваиваться подстроке той же длины, а потом версию, где эти длины могут быть разными.
      4. (*2) Постройте класс string так, чтобы для присваивания, передачи параметров и т.п. он имел семантику по значнию, то есть, когда копируется строковое представление, а не просто управляющая структура данных класса sring.
      5. (*3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строка копировалась только когда это необходимо. То есть, храните совместно используемое представление двух строк, пока одна из этих строк не бдет изменена. Не пытайтесь одновременно с этим иметь операцию выделения подстроки, которая может использваться в левой части.
      6. (*4) Разработайте класс string с семантикой по значению, копированием с задержкой и операцией подстроки, которая может стоять в левой части.
      7. (*2) Какие преобразования используются в каждом выражнии следующей программы:
      struct X (* int i; X(int); operator+(int); *);
      struct Y (* int i; Y(X); operator+(X); operator int(); *);
      X operator* (X,Y); int f(X);
      X x = 1; Y y = x; int i = 2;
      main() (* i + 10; y + 10; y + 10 * y; x + y + i; x * x + i; f(7); f(y); y + y; 106 + y; *)
      Определите X и Y так, чтобы они оба были целыми типами. Измените программу так, чтобы она работала и печатала значения всех допустимых выражений.
      8. (*2) Определите класс INT, который ведет себя в точности как int. Подсказка: определите INT::operator int().
      9. (*1) Определите класс RINT, который ведет себя в точноти как int за исключением того, что единственные возмоные операции – это + (унарный и бинарный), – (унарный и бинарный), *, /, %. Подсказка: не определяйте INT::operator int().
      10. (*3) Определите класс LINT, ведущий себя как RINT, за исключением того, что имеет точность не менее 64 бит.
      11. (*4) Определите класс, который реализует арифметику с произвольной точностью. Подсказка: вам надо управлять памятью аналогично тому, как это делалось для класса string.
      12. (*2) Напишите программу, доведенную до нечитаемого сотояния с помощью макросов и перегрузки операций. Вот идея: определите для INT + так, чтобы он означал -, и наоборот, а потом с помощью макроопределения определите int как INT. Переопределение часто употребляемых фунций, использование параметров ссылочного типа и несколко вводящих в заблуждение комментариев помогут устроить полную неразбериху.
      13. (*3) Поменяйтесь со своим другом программами, которые у вас получились в предыдущем упражнении. Не запуская ее попытайтесь понять, что делает программа вашего друга. После выполнения этого упражнения вы будете знать, чего следует избегать.
      14. (*2) Перепишите примеры с comlpex (#6.3.1), tiny (#6.3.2) и string (#6.9) не используя friend функций. Используйте только функции члены. Протестируйте каждую из новых версий. Сравните их с версиями, в которых используются функции друзья. Еще раз посмотрите Упражнение 5.3.
      15. (*2) Определите тип vec4 как вектор их четырех float. Определите operator[] для vec4. Определите операции +, -, *, /, =, +=, -=, *=, /= для сочетаний векторов и чсел с плавающей точкой.
      16. (*3) Определите класс mat4 как вектор из четырех vec4. Определите для mat4 operator[], возвращающий vec4. Опрделите для этого типа обычные операции над матрицами. Определите функцию, выполняющие для mat4 исключение Гусса.
      17. (*2) Определите класс vector, аналогичный vec4, но с длиной, которая задается как параметр конструктора vector::vector(int).
      18. (*3) Определите класс matrix, аналогичный mat4, но с размерностью, задаваемой параметрами конструктора matrix::matrix(int,int).

Глава 7 Производные Классы

      Не надо размножать объекты без необходимости
У. Оккам

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

7.1 Введение

      Представим себе процесс написания некоторого универсалного средства (например, тип связанный список, таблица имен или планировщик для системы моделирования), которое преднаначается для использования многими разными людьми в различных обстоятельствах. Очевидно, что в кандидатах на роль таких средств недостатка нет, и выгоды от их стандартизации огроны. Кажется, любой опытный программист написал (и отладил) дюжину вариантов типов множества, таблицы имен, сортирующей функции и т.п., но оказывается, что каждый программист и кадая программа используют свою версию этих понятий, из-за чего программы слишком трудно читать, тяжело отлаживать и сложно модифицировать. Более того, в большой программе вполне может быть несколько копий идентичных (почти) частей кода для рабты с такими фундаментальными понятиями.
      Причина этого хаоса частично состоит в том, что предствить такие общие понятия в языке программирования сложно с концептуальной точки зрения, а частично в том, что средства, обладающие достаточной общностью, налагают дополнительные расходы по памяти и/или по времени, что делает их неудобными для самых простых и наиболее напряженно используемых средств (связанные списки, вектора и т.п.), где они были бы наиболее полезны. Понятие производного класса в С++, описываемое в #7.2, не обеспечивают общего решения всех этих проблем, но оно дает способ справляться с довольно небольшим числом ваных случаев. Будет, например, показано, как определить эффетивный класс обобщенного связанного списка таким образом, чтобы все его версии разделяли код.
      Написание общецелевых средств – задача непростая, и чато основной акцент в их разработке другой, чем при разработке программ специального назначения. Конечно, нет четкой границы между средствами общего и специального назначения, и к метдам и языковым средствам, которые описываются в этой главе, можно относиться так, что они становятся все более полезны с ростом объема и сложности создаваемых программ.

7.2 Производные Классы

      Чтобы разделить задачи понимания аппарата языка и метдов его применения, знакомство с понятием производных классов делается в три этапа. Вначале с помощью небольших примеров, которые не надо воспринимать как реалистичные, будут описаны
      сами средства языка (запись и семантика). После этого демонтрируются некоторые неочевидные применения производных класов, и, наконец, приводится законченная программа.

7.2.1 Построение Производного Класса

      Рассмотрим построение программы, которая имеет дело с людьми, служащими в некоторой фирме. Структура данных в этой программе может быть например такой:
      struct employee (* // служащий char* name; // имя short age; // возраст short department; // подразделение int salary; // жалование employee* next; // ... *);
      Список аналогичных служащих будет связываться через поле next. Теперь давайте определим менеджера:
      struct manager (* // менеджер employee emp; // запись о менеджере как о служащем employee* group; // подчиненные люди // ... *);

  • Страницы:
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20