Правила программирования на С и С++. Главы 7-8
Страница 23. Никогда не допускайте открытого доступа к закрытым данным


 

109. Все данные в определении класса должны быть закрытыми.

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

Все данные в определении класса должны быть закрытыми. Точка. Никаких исключений. Проблема здесь заключается в тесном сцеплении между классом и его пользователями, если они имеют прямой доступ к полям данных. Я приведу вам несколько примеров. Скажем, у вас есть класс 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).

Вы заметите, что я умышленно не реализовал следующую функцию в листинге 7:

string::operator const char*() { return buf; }

  Если бы реализовал, то мог бы сделать следующее:

Листинг 7. Простой строковый класс.

  1. class string
  2. {
  3. char *buf;
  4. int length; // длина буфера (не строки);
  5. public:
  6. virtual
  7. ~string( void );
  8. string( const char *input_str = "" );
  9. string( const string ?r );
  10. virtual const string ?operator=( const string ?r );
  11. virtual int operator? ( const string ?r ) const;
  12. virtual int operator> ( const string ?r ) const;
  13. virtual int operator==( const string ?r ) const;
  14. virtual void print( ostream ?output ) const;
  15. // ...
  16. };
  17. //--------------------------------------------------------------------------------------------------------------------
  18. inline string::string( const char *input_str /*= ""*/ )
  19. {
  20. length = strlen(input_str) + 1;
  21. buf = new char[ length ];
  22. strcpy( buf, input_str );
  23. }
  24. //--------------------------------------------------------------------------------------------------------------------
  25. inline string::string( const string ?r )
  26. {
  27. length = r.length;
  28. buf = new char[ length ];
  29. strcpy( buf, r.buf );
  30. }
  31. //--------------------------------------------------------------------------------------------------------------------
  32. /* виртуальный */ ~string::string( void )
  33. {
  34. delete buf;
  35. }
  36. //--------------------------------------------------------------------------------------------------------------------
  37. /* виртуальный */ const string ?string::operator=( const string ?r )
  38. {
  39. if( this != ?r )
  40. {
  41. if( length != r.length )
  42. {
  43. free( buf );
  44. length = r.length;
  45. buf = new char[ length ];
  46. }
  47. strcpy( buf, r.buf );
  48. }
  49. return *this;
  50. }
  51. //--------------------------------------------------------------------------------------------------------------------
  52. /* виртуальный */ int string::operator? ( const string ?r ) const
  53. {
  54. return strcmp(buf, r.buf) ? 0;
  55. }
  56. //--------------------------------------------------------------------------------------------------------------------
  57. /* виртуальный */ int string::operator> ( const string ?r ) const
  58. {
  59. return strcmp(buf, r.buf) > 0;
  60. }
  61. //--------------------------------------------------------------------------------------------------------------------
  62. /* виртуальный */ int string::operator==( const string ?r ) const
  63. {
  64. return strcmp(buf, r.buf) == 0;
  65. }
  66. //--------------------------------------------------------------------------------------------------------------------
  67. /* виртуальный */ void string::print( ostream ?output ) const
  68. {
  69. cout ?? buf;
  70. }
  71. //--------------------------------------------------------------------------------------------------------------------
  72. inline ostream ?operator??( ostream ?output, const string ?s )
  73. {
  74. // Эта функция не является функцией-членом класса string,
  75. // но не должна быть дружественной, потому что мной тут
  76. // реализован метод вывода строкой своего значения.
  77. s.print(output);
  78. return output;
  79. }
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() и получить многоязыкую строку. Такая степень изоляции и является целью объектно-ориентированного программирования, но вы ее не добьетесь, если не будете непреклонно следовать этим правилам. Здесь нет места ковбоям от программирования.

 
« Предыдущая статья   Следующая статья »