Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
84
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

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

30

2. Краткий обзор С++

Эту главу мы начнем с рассмотрения встроенного в язык С++ типа данных массив”. Массив это набор данных одного типа, например массив целых чисел или массив строк. Мы рассмотрим недостатки, присущие встроенному массиву, и напишем для его представления свой класс Array, где попытаемся избавиться от этих недостатков. Затем мы построим целую иерархию подклассов, основываясь на нашем базовом классе Array.

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

2.1. Встроенный тип данных массив

Как было показано в главе 1, С++ предоставляет встроенную поддержку для основных

//объявление целого объекта ival

//ival инициализируется значением 1024 int ival = 1024;

//объявление вещественного объекта двойной точности dval

//dval инициализируется значением 3.14159

double dval = 3.14159;

//объявление вещественного объекта одинарной точности fval

//fval инициализируется значением 3.14159

типов данных целых и вещественных чисел, логических значений и символов: float fval = 3.14159;

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

int ival2 = ival1 + 4096; // сложение

операции: объекты числового типа можно складывать, вычитать, умножать, делить и т.д.

int ival3 = ival2 - ival;

// вычитание

dval = fval * ival;

// умножение

ival = ival3 / 2;

// деление

bool result = ival2 == ival3;

// сравнение на равенство

result = ival2 + ival != ival3;

// сравнение на неравенство

result = fval + ival2 < dval;

// сравнение на меньше

result = ival > ival2;

 

// сравнение на больше

В дополнение к встроенным типам стандартная библиотека С++ предоставляет поддержку для расширенного набора типов, таких, как строка и комплексное число. (Мы отложим рассмотрение класса vector из стандартной библиотеки до раздела 2.7.)

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

31

Промежуточное положение между встроенными типами данных и типами данных из стандартной библиотеки занимают составные типы массивы и указатели. (Указатели рассмотрены в разделе 2.2.)

Массив это упорядоченный набор элементов одного типа. Например,

последовательность

0 1 1 2 3 5 8 13 21

представляет собой первые 9 элементов последовательности Фибоначчи. (Выбрав начальные два числа, вычисляем каждый из следующих элементов как сумму двух предыдущих.)

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

int fibon[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };

Здесь fibon это имя массива. Элементы массива имеют тип int, размер (длина) массива равна 9. Значение первого элемента 0, последнего 21. Для работы с массивом мы индексируем (нумеруем) его элементы, а доступ к ним осуществляется с помощью операции взятия индекса. Казалось бы, для обращения к первому элементу массива естественно написать:

int first_elem = fibon[1];

Однако это не совсем правильно: в С++ (как и в С) индексация массивов начинается с 0, поэтому элемент с индексом 1 на самом деле является вторым элементом массива, а индекс первого равен 0.Таким образом, чтобы обратиться к последнему элементу

fibon[0]; // первый элемент fibon[1]; // второй элемент

...

fibon[8]; // последний элемент

массива, мы должны вычесть единицу из размера массива: fibon[9]; // ... ошибка

Девять элементов массива fibon имеют индексы от 0 до 8. Употребление вместо этого индексов 1-9 является одной из самых распространенных ошибок начинающих программистов на С++.

Для перебора элементов массива обычно употребляют инструкцию цикла. Вот пример программы, которая инициализирует массив из десяти элементов числами от 0 до 9 и

int main()

затем печатает их в обратном порядке:

{

int ia[10]; int index;

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

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