Правила программирования на Си и Си++

       

Никогда не допускайте открытого доступа к закрытым данным


Все данные в определении класса должны быть закрытыми. Точка. Никаких исключений. Проблема здесь заключается в тесном сцеплении между классом и его пользователями, если они имеют прямой доступ к полям данных. Я приведу вам несколько примеров. Скажем, у вас есть класс string, который использует массив типа char для хранения своих данных. Спустя год к вам обращается заказчик из Пакистана, поэтому вам нужно перевести все свои строки на урду, что вынуждает перейти на Unicode. Если ваш строковый класс позволяет какой-либо доступ к локальному буферу char*, или сделав это поле открытым (public), или определив функцию, возвращающую char*, то вы в большой беде.

Взглянем на код. Вот действительно плохой проект:

class string

{

public:

   char *buf;

   // ...

};

f()

{

   string s;

   // ...

printf("%s/n", s.buf );



}

Если вы попробуете изменить определение buf на wchar_t* для работы с Unicode (что предписывается ANSI Си), то все функции, которые имели прямой доступ к полю buf, перестают работать. И вы будете должны их все переписывать.

Другие родственные проблемы проявляются во внутренней согласованности. Если строковый объект содержит поле length, то вы могли бы модифицировать буфер без модификации length, тем самым разрушив эту строку. Аналогично, деструктор строки мог бы предположить, что, так как конструктор разместил этот буфер посредством new, то будет безопаснее передать указатель на buf оператору delete. Однако если у вас прямой доступ, то вы могли бы сделать что-нибудь типа:

string s;

char  array[128];

s.buf = array;

и организация памяти разрушается, когда эта строка покидает область действия.

Простое закрытие при помощи модификатора private

поля buf не помогает, если вы продолжаете обеспечивать доступ посредством функции. Листинг 7 показывает фрагмент простого определения строки, которое будет использоваться мной несколько раз в оставшейся части этой главы. (Упрощение, сделанное мной, свелось к помещению всего в один листинг; обычно определение класса и встроенные функции будут в заголовочном файле, а остальной код — в файле .cpp).


{
    return strcmp(buf, r.buf) 0;
}
//------------------------------------------------–––––––––––-----
/* виртуальный */ int string::operator==( const
string r ) const
{
    return strcmp(buf, r.buf) == 0;
}
//--------------------------------------------------–––––––––––---
/* виртуальный */ void string::print( ostream output ) const
{
    cout buf;
}
//–------------------------------------------------–––––––––––----
inline ostream operator( ostream output, const string s )
{
// Эта функция не является функцией-членом класса string,
// но не должна быть дружественной, потому что мной тут
// реализован метод вывода строкой своего значения.

    s.print(output);
    return output;
}
Вы заметите, что я умышленно не реализовал следующую функцию в листинге 7:
string::operator const
char*() { return buf; }
Если бы реализовал, то мог бы сделать следующее:
void f( void
)
{
   string s;
   // ...
   printf("%s\n", (const char*)s );
}
но я не cмогу реализовать функцию operator
char*(), которая бы работала со строкой Unicode, использующей для символа 16-бит. Я должен бы был написать функцию operator
wchar_t*(), тем самым модифицировав код в функции f():
printf("%s/n", (const wchar_t*)s );
Тем не менее, одним из главных случаев, которых я стараюсь избежать при помощи объектно-ориентированного подхода, является необходимость модификации пользователя объекта при изменении внутреннего определения этого объекта, поэтому преобразование в char*
неприемлемо.
Также есть проблемы со стороны внутренней согласованности. Имея указатель на buf,
возвращенный функцией operator
const char*(), вы все же можете модифицировать строку при помощи указателя и испортить поле length, хотя для этого вам придется немного постараться:
string s;
// ...
char *p = (char *)(const char *)s;
gets( p );
В равной степени серьезная, но труднее обнаруживаемая проблема возникает в следующем коде:


const char
*g( void )
{
 string s;
// ...
return (const
char *)s;
}
Операция приведения вызывает функцию operator const char*(), возвращающую buf. Тем не менее, деструктор класса string
передает этот буфер оператору delete, когда строка покидает область действия. Следовательно, функция g()
возвращает указатель на освобожденную память. В отличие от предыдущего примера, при этой второй проблеме нет закрученного оператора приведения в два этапа, намекающего нам, что что-то не так.
Реализация в листинге 7 исправляет это, заменив преобразование char* на обработчиков сообщений типа метода самовывода (print()). Я бы вывел строку при помощи:
string s;
s.print( cout )
или:
cout s;
а не используя printf(). При этом совсем нет открытого доступа к внутреннему буферу. Функции окружения могут меньше беспокоиться о том, как хранятся символы, до тех пор, пока строковый объект правильно отвечает на сообщение о самовыводе. Вы можете менять свойства представления строки как хотите, не влияя на отправителя сообщения print(). Например, строковый объект мог бы содержать два буфера —
один для строк Unicode и другой для строк char* — и обеспечивать перевод одной строки в другую. Вы могли бы даже добавить для перевода на французский язык сообщение translate_to_French() и получить многоязыкую строку. Такая степень изоляции и является целью объектно-ориентированного программирования, но вы ее не добьетесь, если не будете непреклонно следовать этим правилам. Здесь нет места ковбоям от программирования.

Содержание раздела