Главная » Статьи » Мои статьи » С++ для начинающих

2.3. Объектный подход (Часть1)

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

1.       обладать некоторыми знаниями о самом себе. Пусть для начала это будет знание собственного размера;

2.       поддерживать операцию присваивания и операцию сравнения на равенство;

3.       отвечать на некоторые вопросы, например: какова величина минимального и максимального элемента; содержит ли массив элемент с определенным значением; если да, то каков индекс первого встречающегося элемента, имеющего это значение;

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

5.       Конечно, мы должны реализовать и базовые операции работы с массивом, а именно:Возможность задать размер массива при его создании. (Речь не идет о том, чтобы знать эту величину на этапе компиляции.)

6.       Возможность проинициализировать массив некоторым набором значений.

7.       Возможность обращаться к элементу массива по индексу. Пусть эта возможность реализуется с помощью стандартной операции взятия индекса.

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

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

class classname {

public:

  // набор открытых операций

private:

  // закрытые функции, обеспечивающие реализацию

};

class, public и private – это ключевые слова С++, а classname – имя, которое программист дал своему классу. Назовем наш проектируемый класс IntArray: на первом этапе этот массив будет содержать только целые числа. Когда мы научим его обращаться с данными любого типа, можно будет переименовать его в Array.

Определяя класс, мы создаем новый тип данных. На имя класса можно ссылаться точно так же, как на любой встроенный описатель типа. Можно создавать объекты этого нового типа аналогично тому, как мы создаем объекты встроенных типов:

// статический объект типа IntArray

IntArray myArray;

 

// указатель на динамический объект типа IntArray

IntArray *pArray = new IntArray;

Определение класса состоит из двух частей: заголовка (имя, предваренное ключевым словом class) и тела, заключенного в фигурные скобки. Заголовок без тела может служить объявлением класса.

// объявление класса IntArray

// без определения его

class IntArray;

Тело класса состоит из определений членов и спецификаторов доступа – ключевых слов public, private и protected. (Пока мы ничего не будем говорить об уровне доступа protected.) Членами класса могут являться функции, которые определяют набор действий, выполняемых классом, и переменные, содержащие некие внутренние данные, необходимые для реализации класса. Функции, принадлежащие классу, называют функциями-членами или, по-другому, методами класса. Вот набор методов класса IntArray:

class IntArray {

public:

  // операции сравнения: #2b

  bool operator== (const IntArray&) const;

  bool operator!= (const IntArray&) const;

 

  // операция присваивания: #2a

  IntArray& operator= (const IntArray&);

 

  int size() const; // #1

  void sort();      // #4

 

  int min() const;  // #3a

  int max() const;  // #3b

 

  // функция find возвращает индекс первого

  // найденного элемента массива

  // или -1, если элементов не найдено

  int find (int value) const; // #3c

 

private:

  // дальше идут закрытые члены,

  // обеспечивающие реализацию класса

  ...

}

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

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

// инициализация переменной min_val

// минимальным элементом myArray

int min_val = myArray.min();

Чтобы найти минимальный элемент в динамически созданном объекте типа IntArray, мы должны написать:

int min_val = pArray->min();

(Да, мы еще ничего не сказали о том, как же проинициализировать наш объект – задать его размер и наполнить элементами. Для этого служит специальная функция-член, называемая конструктором. Мы поговорим об этом чуть ниже.)

Операции применяются к объектам класса точно так же, как и к встроенным типам данных. Пусть мы имеем два объекта типа IntArray:

IntArray myАrray0, myArray1;

Инструкции присваивания и сравнения с этими объектами выглядят совершенно обычным образом:

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

// вызывает функцию-член myArray0.operator=(myArray1)

myArray0 = myArray1;

 

// инструкция сравнения -

// вызывает функцию-член myArray0.operator==(myArray1)

if (myArray0 == myArray1)

  cout << "Ура! Оператор присваивания сработал!\n";

Спецификаторы доступа public и private определяют уровень доступа к членам класса. К тем членам, которые перечислены после public, можно обращаться из любого места программы, а к тем, которые объявлены после private, могут обращаться только функции-члены данного класса. (Помимо функций-членов, существуют еще функции-друзья класса, но мы не будем говорить о них вплоть до раздела 15.2.)

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

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

·                   если мы меняем или расширяем реализацию класса, то изменения можно выполнить так, что большинство пользовательских программ, использующих наш класс, их "не заметят”: модификации коснутся лишь скрытых членов (мы поговорим об этом в разделе 6.18);

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

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

class IntArray {

public:

  // ...

  int size() const { return _size; }

private:

  // внутренние данные-члены

  int _size;

  int *ia;

};

Поскольку мы поместили член _size в закрытую секцию, пользователь класса не имеет возможности обратиться к нему напрямую. Чтобы позволить внешней программе узнать размер массива, мы написали функцию-член size(), которая возвращает значение члена _size. Нам пришлось добавить символ подчеркивания к имени нашего скрытого члена _size, поскольку функция-член с именем size() уже определена. Члены класса – функции и данные – не могут иметь одинаковые имена.

Может показаться, что реализуя подобным образом доступ к скрытым данным класса, мы очень сильно проигрываем в эффективности. Сравним два выражения (предположим, что мы изменили спецификатор доступа члена _size на public):

IntArray array;

int array_size = array.size();

array_size = array._size;

Действительно, вызов функции гораздо менее эффективен, чем прямой доступ к памяти, как во втором операторе. Так что же, принцип сокрытия информации заставляет нас жертвовать эффективностью?

На самом деле, нет. С++ имеет механизм встроенных (inline) функций. Текст встроенной функции подставляется компилятором в то место, где записано обращение к ней. (Это напоминает механизм макросов, реализованный во многих языках, в том числе и в С++. Однако есть определенные отличия, о которых мы сейчас говорить не будем.) Вот пример. Если у нас есть следующий фрагмент кода:

for (int index=0; index<array.size(); ++index)

  // ...

то функция size() не будет вызываться _size раз во время исполнения. Вместо вызова компилятор подставит ее текст, и результат компиляции предыдущего кода будет в точности таким же, как если бы мы написали:

for (int index=0; index<array._size; ++index)

  // ...

Если функция определена внутри тела класса (как в нашем случае), она автоматически считается встроенной. Существует также ключевое слово inline, позволяющее объявить встроенной любую функцию[1].


Категория: С++ для начинающих | Добавил: Vayolet (28.05.2010)
Просмотров: 1116 | Теги: учебник по С++, Объектный подход в С++ | Рейтинг: 0.0/0
Всего комментариев: 0
Имя *:
Email *:
Код *: