Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

32

 

for (index=0; index<10; ++index)

 

 

 

 

// ia[0] = 0, ia[1] = 1 и т.д.

 

 

ia[index] = index;

 

 

for (index=9; index>=0; --index)

 

 

cout << ia[index] << " ";

 

 

cout << endl;

 

 

}

 

 

 

 

Оба цикла выполняются по 10 раз. Все управление циклом for осуществляется

 

инструкциями в круглых скобках за ключевым словом for. Первая присваивает

 

начальное значение переменной index. Это производится один раз перед началом цикла:

 

 

index = 0;

 

 

 

 

Вторая инструкция:

 

 

index < 10;

 

 

 

 

 

 

представляет собой условие окончания цикла. Оно проверяется в самом начале каждой

 

итерации цикла. Если результатом этой инструкции является true, то выполнение цикла

 

продолжается; если же результатом является false, цикл заканчивается. В нашем

 

примере цикл продолжается до тех пор, пока значение переменной index меньше 10. На

 

каждой итерации цикла выполняется некоторая инструкция или группа инструкций,

 

составляющих тело цикла. В нашем случае это инструкция

 

 

ia[index] = index;

 

 

 

 

 

 

Третья управляющая инструкция цикла

 

 

++index

 

 

 

 

 

 

выполняется в конце каждой итерации, по завершении тела цикла. В нашем примере это

 

увеличение переменной index на единицу. Мы могли бы записать то же действие как

 

 

index = index + 1

 

 

 

 

но С++ дает возможность использовать более короткую (и более наглядную) форму

 

записи. Этой инструкцией завершается итерация цикла. Описанные действия

 

повторяются до тех пор, пока условие цикла не станет ложным.

 

Вторая инструкция for в нашем примере печатает элементы массива. Она отличается от

 

первой только тем, что в ней переменная index уменьшается от 9 до 0. (Подробнее

 

инструкция for рассматривается в главе 5.)

 

Несмотря на то, что в С++ встроена поддержка для типа данных массив”, она весьма

 

ограничена. Фактически мы имеем лишь возможность доступа к отдельным элементам

 

массива. С++ не поддерживает абстракцию массива, не существует операций над

 

массивами в целом, таких, например, как присвоение одного массива другому или

 

сравнение двух массивов на равенство, и даже такой простой, на первый взгляд,

 

операции, как получение размера массива. Мы не можем скопировать один массив в

 

другой, используя простой оператор присваивания:

 

С++ для начинающих

33

 

int array0[10]; array1[10];

 

 

 

 

...

 

 

 

array0 = array1; // ошибка

 

 

Вместо этого мы должны программировать такую операцию с помощью цикла:

 

 

for (int index=0; index<10; ++index)

 

 

array0[index] = array1[index];

 

 

 

 

 

 

 

Массив не знаетсобственный размер. Поэтому мы должны сами следить за тем, чтобы

 

случайно не обратиться к несуществующему элементу массива. Это становится особенно

 

утомительным в таких ситуациях, как передача массива функции в качестве параметра.

 

Можно сказать, что этот встроенный тип достался языку С++ в наследство от С и

 

процедурно-ориентированной парадигмы программирования. В оставшейся части главы

 

мы исследуем разные возможности улучшитьмассив.

 

Упражнение 2.1

 

Как вы думаете, почему для встроенных массивов не поддерживается операция

 

присваивания? Какая информация нужна для того, чтобы поддержать эту операцию?

 

Упражнение 2.2

 

 

 

 

Какие операции должен поддерживать полноценныймассив?

 

2.2. Динамическое выделение памяти и указатели

Прежде чем углубиться в объектно-ориентированную разработку, нам придется сделать небольшое отступление о работе с памятью в программе на С++. Мы не сможем написать сколько-нибудь сложную программу, не умея выделять память во время выполнения и обращаться к ней.

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

До сих пор во всех наших примерах использовалось статическое выделение памяти. Скажем, определение переменной ival

int ival = 1024;

заставляет компилятор выделить в памяти область, достаточную для хранения переменной типа int, связать с этой областью имя ival и поместить туда значение 1024. Все это делается на этапе компиляции, до выполнения программы.

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

int ival2 = ival + 1;

С++ для начинающих

34

то обращаемся к значению, содержащемуся в переменной ival: прибавляем к нему 1 и инициализируем переменную ival2 этим новым значением, 1025. Каким же образом обратиться к адресу, по которому размещена переменная?

С++ имеет встроенный тип указатель”, который используется для хранения адресов объектов. Чтобы объявить указатель, содержащий адрес переменной ival, мы должны написать:

int *pint; // указатель на объект типа int

Существует также специальная операция взятия адреса, обозначаемая символом &. Ее результатом является адрес объекта. Следующий оператор присваивает указателю pint

int *pint;

адрес переменной ival:

pint = &ival; // pint получает значение адреса ival

Мы можем обратиться к тому объекту, адрес которого содержит pint (ival в нашем случае), используя операцию разыменования, называемую также косвенной адресацией. Эта операция обозначается символом *. Вот как можно косвенно прибавить единицу к ival, используя ее адрес:

*pint = *pint + 1; // неявно увеличивает ival

Это выражение производит в точности те же действия, что и

ival = ival + 1; // явно увеличивает ival

В этом примере нет никакого реального смысла: использование указателя для косвенной манипуляции переменной ival менее эффективно и менее наглядно. Мы привели этот пример только для того, чтобы дать самое начальное представление об указателях. В

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

Основные отличия между статическим и динамическим выделением памяти таковы:

статические объекты обозначаются именованными переменными, и действия над этими объектами производятся напрямую, с использованием их имен. Динамические объекты не имеют собственных имен, и действия над ними производятся косвенно, с помощью указателей;

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

Выделение и освобождение памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы new и delete.

Оператор new имеет две формы. Первая форма выделяет память под единичный объект определенного типа:

int *pint = new int(1024);

С++ для начинающих

35

Здесь оператор new выделяет память под безымянный объект типа int, инициализирует его значением 1024 и возвращает адрес созданного объекта. Этот адрес используется для инициализации указателя pint. Все действия над таким безымянным объектом производятся путем разыменовывания данного указателя, т.к. явно манипулировать динамическим объектом невозможно.

Вторая форма оператора new выделяет память под массив заданного размера, состоящий из элементов определенного типа:

int *pia = new int[4];

В этом примере память выделяется под массив из четырех элементов типа int. К сожалению, данная форма оператора new не позволяет инициализировать элементы массива.

Некоторую путаницу вносит то, что обе формы оператора new возвращают одинаковый указатель, в нашем примере это указатель на целое. И pint, и pia объявлены совершенно одинаково, однако pint указывает на единственный объект типа int, а pia на первый элемент массива из четырех объектов типа int.

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

//освобождение единичного объекта delete pint;

//освобождение массива

иnew, две формы для единичного объекта и для массива: delete[] pia;

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

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

Упражнение 2.3

(a)int ival = 1024;

(b)int *pi = &ival;

(c)int *pi2 = new int(1024);

Объясните разницу между четырьмя объектами:

(d) int *pi3 = new int[1024];

Упражнение 2.4

С++ для начинающих

36

Что делает следующий фрагмент кода? В чем состоит логическая ошибка? (Отметим, что операция взятия индекса ([]) правильно применена к указателю pia. Объяснение этому

int *pi = new int(10); int *pia = new int[10];

while ( *pi < 10 ) { pia[*pi] = *pi; *pi = *pi + 1;

}

delete pi;

факту можно найти в разделе 3.9.2.) delete[] pia;

2.3. Объектный подход

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

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

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

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

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

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

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

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

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

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

который был бы удобен в использовании даже самым неискушенным программистам на С++.

С++ для начинающих

37

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

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:

С++ для начинающих

38

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

должны написать:

int min_val = myArray.min();

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

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

int min_val = pArray->min();

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

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

С++ для начинающих

39

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;

};

С++ для начинающих

40

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

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

IntArray array;

мы изменили спецификатор доступа члена _size на public):

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, позволяющее объявить встроенной любую функцию3.

Мы до сих пор ничего не сказали о том, как будем инициализировать наш массив.

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

3 Объявление функции inline это всего лишь подсказка компилятору. Однако компилятор не всегда может сделать функцию встроенной, существуют некоторые ограничения. Подробнее об этом сказано в разделе 7.6.

С++ для начинающих

41

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

Функция-член класса, носящее то же имя, что и сам класс, считается конструктором. (Нет никаких специальных ключевых слов, позволяющих определить конструктор как-то по- другому.) Мы уже сказали, что конструкторов может быть несколько. Как же так: разные функции с одинаковыми именами?

В С++ это возможно. Разные функции могут иметь одно и то же имя, если у этих функций различны количество и/или типы параметров. Это называется перегрузкой функции. Обрабатывая вызов перегруженной функции, компилятор смотрит не только на ее имя, но и на список параметров. По количеству и типам передаваемых параметров компилятор может определить, какую же из одноименных функций нужно вызывать в данном случае. Рассмотрим пример. Мы можем определить следующий набор перегруженных функций min(). (Перегружаться могут как обычные функции, так и

//список перегруженных функций min()

//каждая функция отличается от других списком параметров

#include <string>

int min (const int *pia,int size); int min (int, int);

int min (const char *str); char min (string);

функции-члены.)

string min (string,string);

Поведение перегруженных функций во время выполнения ничем не отличается от поведения обычных. Компилятор определяет нужную функцию и помещает в объектный код именно ее вызов. (В главе 9 подробно обсуждается механизм перегрузки.)

Итак, вернемся к нашему классу IntArray. Давайте определим для него три

class IntArray { public:

explicit IntArray (int sz = DefaultArraySize); IntArray (int *array, int array_size); IntArray (const IntArray &rhs);

// ...

private:

static const int DefaultArraySize = 12;

конструктора:

}

Первый из перечисленных конструкторов

IntArray (int sz = DefaultArraySize);

называется конструктором по умолчанию, потому что он может быть вызван без параметров. (Пока не будем объяснять ключевое слово explicit.) Если при создании объекта ему задается параметр типа int, например