Бьерн Страуструп. Язык программирования С++ Второе дополненное издание



Pdf көрінісі
бет233/256
Дата11.07.2022
өлшемі2,87 Mb.
#37591
1   ...   229   230   231   232   233   234   235   236   ...   256
12.2.7 Отношения внутри класса 
До сих пор мы обсуждали только классы, и хотя операции упоминались, если не считать обсуждения 
шагов процесса развития программного обеспечения ($$11.3.3.2), то они были на втором плане, 
объекты же практически вообще не упоминались. Понять это просто: в С++ класс, а не функция или 
объект, является основным понятием организации системы. 
Класс может скрывать в себе всякую специфику реализации, наравне с "грязными" приемами 
программирования, а иногда он вынужден это делать. В то же время объекты большинства классов 
сами образуют регулярную структуру и используются такими способами, что их достаточно просто 
описать. Объект класса может быть совокупностью других вложенных объектов (их часто называют 
членами), многие из которых, в свою очередь, являются указателями или ссылками на другие объекты. 
Поэтому отдельный объект можно рассматривать как корень дерева объектов, а все входящие в него 
объекты как "иерархию объектов", которая дополняет иерархию классов, рассмотренную в $$12.2.4. 
Рассмотрим в качестве примера класс строк из $$7.6: 
class String { 
int 
sz; 
char* 
p; 
public: 


Бьерн Страуструп.
Язык программирования С++ 
 
326 
String(const 
char* 
q); 
~String(); 
//... 
}; 
Объект типа String можно изобразить так: 
12.2.7.1 Инварианты 
Значение членов или объектов, доступных с помощью членов класса, называется состоянием объекта 
(или просто значением объекта). Главное при построении класса - это: привести объект в полностью 
определенное состояние (инициализация), сохранять полностью определенное состояние обЪекта в 
процессе выполнения над ним различных операций, и в конце работы уничтожить объект без всяких 
последствий. Свойство, которое делает состояние объекта полностью определенным, называется 
инвариантом. 
Поэтому назначение инициализации - задать конкретные значения, при которых выполняется инвариант 
объекта. Для каждой операции класса предполагается, что инвариант должен иметь место перед 
выполнением операции и должен сохраниться после операции. В конце работы деструктор нарушает 
инвариант, уничтожая объект. Например, конструктор String::String(const char*) гарантирует, что p 
указывает на массив из, по крайней мере, sz элементов, причем sz имеет осмысленное значение и v[sz-
1]==0. Любая строковая операция не должна нарушать это утверждение. 
При проектировании класса требуется большое искусство, чтобы сделать реализацию класса 
достаточно простой и допускающей наличие полезных инвариантов, которые несложно задать. Легко 
требовать, чтобы класс имел инвариант, труднее предложить полезный инвариант, который понятен и 
не накладывает жестких ограничений на действия разработчика класса или на эффективность 
реализации. Здесь "инвариант" понимается как программный фрагмент, выполнив который, можно 
проверить состояние объекта. Вполне возможно дать более строгое и даже математическое 
определение инварианта, и в некоторых ситуациях оно может оказаться более подходящим. Здесь же 
под инвариантом понимается практическая, а значит, обычно экономная, но неполная проверка 
состояния объекта. 
Понятие инварианта появилось в работах Флойда, Наура и Хора, посвященных пред- и пост-условиям, 
оно встречается во всех важных статьях по абстрактным типам данных и верификации программ за 
последние 20 лет. Оно же является основным предметом отладки в C++.
Обычно, в течение работы функции-члена инвариант не сохраняется. Поэтому функции, которые могут 
вызываться в те моменты, когда инвариант не действует, не должны входить в общий интерфейс 
класса. Такие функции должны быть частными или защищенными. 
Как можно выразить инвариант в программе на С++? Простое решение - определить функцию, 
проверяющую инвариант, и вставить вызовы этой функции в общие операции. Например: 
class String { 
int 
sz; 
int* 
p; 
public: 
class 
Range 
{}; 
class 
Invariant 
{}; 
void 
check(); 
String(const 
char* 
q); 
~String(); 
char& 
operator[](int 
i); 
int size() { return sz; } 
//... 
}; 
void String::check() 

if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz-1]) 
throw 
Invariant; 


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

char& String::operator[](int i) 

check();
// проверка на входе 
if (i<0 || i// 
действует 
check();
// проверка на выходе 
return 
v[i]; 

Этот вариант прекрасно работает и не осложняет жизнь программиста. Но для такого простого класса 
как String проверка инварианта будет занимать большую часть времени счета. Поэтому программисты 
обычно выполняют проверку инварианта только при отладке: 
inline void String::check() 

if 
(!NDEBUG) 
if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz]) 
throw 
Invariant; 

Мы выбрали имя NDEBUG, поскольку это макроопределение, которое используется для аналогичных 
целей в стандартном макроопределении С assert(). Традиционно NDEBUG устанавливается с целью 
указать, что отладки нет. Указав, что check() является подстановкой, мы гарантировали, что никакая 
программа не будет создана, пока константа NDEBUG не будет установлена в значение, обозначающее 
отладку. С помощью шаблона типа Assert() можно задать менее регулярные утверждения, например: 
template inline void Assert(T expr,X x) 

if 
(!NDEBUG) 
if (!expr) throw x; 

вызовет особую ситуацию x, если expr ложно, и мы не отключили проверку с помощью NDEBUG. 
Использовать Assert() можно так: 
class Bad_f_arg { }; 
void f(String& s, int i) 

Assert(0<=i && i//... 

Шаблон типа Assert() подражает макрокоманде assert() языка С. Если i не находится в требуемом 
диапазоне, возникает особая ситуация Bad_f_arg. 
С помощью отдельной константы или константы из класса проверить подобные утверждения или 
инварианты - пустяковое дело. Если же необходимо проверить инварианты с помощью объекта, можно 
определить производный класс, в котором проверяются операциями из класса, где нет проверки, см. 
упр.8 в $$13.11. 
Для классов с более сложными операциями расходы на проверки могут быть значительны, поэтому 
проверки можно оставить только для "поимки" трудно обнаруживаемых ошибок. Обычно полезно 
оставлять по крайней мере несколько проверок даже в очень хорошо отлаженной программе. При всех 
условиях сам факт определения инвариантов и использования их при отладке дает неоценимую 
помощь для получения правильной программы и, что более важно, делает понятия, представленные 
классами, более регулярными и строго определенными. Дело в том, что когда вы создаете инварианты, 
то рассматриваете класс с другой точки зрения и вносите определенную избыточность в программу. То 
и другое увеличивает вероятность обнаружения ошибок, противоречий и недосмотров. Мы указали в 
$$11.3.3.5, что две самые общие формы преобразования иерархии классов состоят в разбиении класса 
на два и в выделении общей части двух классов в базовый класс. В обоих случаях хорошо 
продуманный инвариант может подсказать возможность такого преобразования. Если, сравнивая 


Бьерн Страуструп.
Язык программирования С++ 
 
328 
инвариант с программами операций, можно обнаружить, что большинство проверок инварианта 
излишни, то значит класс созрел для разбиения. В этом случае подмножество операций имеет доступ 
только к подмножеству состояний объекта. Обратно, классы созрели для слияния, если у них сходные 
инварианты, даже при некотором различии в их реализации. 


Достарыңызбен бөлісу:
1   ...   229   230   231   232   233   234   235   236   ...   256




©emirsaba.org 2024
әкімшілігінің қараңыз

    Басты бет