Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / Лекции по C++ ОККТ "Сервер" [12].pdf
Скачиваний:
124
Добавлен:
02.05.2014
Размер:
1.04 Mб
Скачать

Одесский колледж компьютерных технологий "СЕРВЕР"

Вкладывать можно как for, так и while и do.. while. Можно их как угодно комбинировать.

Массивы

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

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

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

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

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

Число, определяющее, сколько элементов может быть в массиве, на-

зывается размерностью.

Массивы, как и переменные, должны быть объявлены перед использованием.

Элементы массива нумеруются с нуля. При описании массива используются те же модификаторы (класс памяти, const и инициализатор), что и для простых переменных. Инициализирующие значения для массивов записываются в фигурных скобках. Значения элементам присваиваются по порядку. Если элементов в массиве больше, чем инициализаторов, элементы, для которых значения не указаны, обнуляются.

Размерность массива вместе с типом его элементов определяет объём памяти, необходимый для размещения массива, которое выполняется на этапе компиляции, поэтому размерность может быть задана только целой положительной константой или константным выражением. Если при описании массива не указана размерность, должен присутствовать инициализатор, в

26

Одесский колледж компьютерных технологий "СЕРВЕР"

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

Примеры объявления и инициализации массивов: int a[10];

Вэтом примере объявляется массив a из 10 элементов, для него выделяется достаточно свободной памяти, чтобы он туда поместился. Содержимое самих ячеек памяти при этом остается нетронутым.

int a[10]={1,2,3,4,5,345,7,8,9,10};

Вэтом примере объявляется тот же массив а из 10 элементов. Но теперь он инициализируется, как переменная, значениями. Эти значения всегда берутся в фигурные скобки и идут через запятую. Перечисленные значения последовательно заносятся в элементы массива. То есть, значение '1' попадет в а[0] (нумерация идет с нуля), '2'- в a[1], а '345'-в a[5](к этому нужно привыкнуть; элемент шестой, но под номером 5). При таком присвоении элементов в ряду может быть меньше, но не больше, размерности массива. Если их будет меньше, то элементы массива, на которых их не хватит, просто останутся нетронутыми. Зато если их будет больше, компилятор сообщит об ошибке: оставшиеся элементы ряда (которые не влезли в массив) будет просто некуда деть.

int a[]={1,2,3,4,5,345,7,8,9,10};

Эта запись отличается от предыдущей тем, что не указана размерность массива. Компилятор посчитает, сколько элементов в фигурных скобках, и выделит, сколько нужно памяти; при объявлении такого массива в фигурных скобках может быть сколько угодно элементов. Но неверно считать, что такой массив будет «резиновым», то есть что в него всегда можно будет добавлять новые элементы. Будучи раз объявлен, он уже будет иметь определенную размерность, соответствующую количеству элементов при объявлении. А объявлять два раза одно и то же C++ не позволит никогда.

Для того чтобы обратиться к элементу массива, нужно записать имя массива, затем в квадратных скобках индекс нужного элемента. Например: a[5]. Такое действие иначе называется адресацией.

При обращении к элементам массива автоматический контроль выхода индекса за границу массива не производится, что может привести к ошибкам. Можно написать и a[-3]; и это не будет ошибкой с позиции синтаксиса, но непонятно, чему будет равно это значение.

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

Вследующем примере подсчитывается сумма элементов массива. #include <iostream.h>

int main(){

const int n = 10;

//константа n=10 для указания размерности массива

int i, sum;

 

27

Одесский колледж компьютерных технологий "СЕРВЕР"

//инициализация целочисленного массива из 10 элементов int marks[n] = {3, 4, 5, 4, 4, 3, 4, 5, 4, 4};

//цикл по элементам массива, в результате в sum «накопится» сумма for (i = 0, sum = 0; i<n; i++) sum += marks[i];

cout <<"Сумма элементов: " << sum; return 0;

}

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

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

char ca1[] = {'C', '+', '+'}; char ca2[] = "C++";

Размерность массива ca1 равна 3, массива ca2 - 4, так как в строках учитывается завершающий нулевой символ. Следующее определение вызовет ошибку компиляции, так как строка "Daniel" состоит из 7 элементов:

char ca3[6] = "Daniel";

Пример работы с массивом. Сортировка целочисленного массива методом выбора. Алгоритм состоит в том, что выбирается наименьший элемент и меняется местами с первым элементом массива, затем рассматриваются элементы, начиная со второго, и наименьший из них меняется местами со вторым элементом, и так далее n-1 раз (при последнем проходе цикла при необходимости меняются местами предпоследний и последний элементы массива).

#include <iostream.h>

 

 

int main(){

 

 

const int n = 20;

// количество элементов массива будет равно 20

int b[n];

// описание массива

int i;

 

 

for (i = 0; i<n; 1++) cin >> b[i];

// ввод массива с клавиатуры

for (i=0; i<n-l; i++){

 

// n-1 раз ищем наименьший элемент

int imin = i;

 

// принимаем за наименьший первый из

 

 

//рассматриваемых элементов

// поиск номера минимального элемента из неупорядоченных: for (int j = i + 1; j<n; j++)

// если нашли меньший элемент, запоминаем его номер:

28

Одесский колледж компьютерных технологий "СЕРВЕР"

if (b[j] < b[imin]) imin = j;

 

int a = b[i];

// обмен элементов

b[i] = b[imin];

// с номерами

b[imin] = a;

//i и imin

}

// вывод упорядоченного массива: for (i = 0; i<n; i++) cout<< b[i] << ' ';

return 0;

}

В С++ не существует операций над массивом в целом, таких, например, как присвоение одного массива другому или сравнение двух массивов на равенство.

int array0[10]; array1[10];

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

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

for (int i=0; i<10; ++i) array0[i] = array1[i];

Указатели.

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

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

Когда компилятор обрабатывает оператор определения переменной, например, int alfa=10, то он выделяет память в соответствии с типом (int), связывает эту область памяти с именем alfa и помещает в неё указанное значение (10). Всё это делается на этапе компиляции, до выполнения программы, даже если переменная в программе использоваться не будет.

Все обращения в программе к переменной по её имени (alfa) компилятор заменяет на адрес области памяти, в которой хранится значение пере-

29

Одесский колледж компьютерных технологий "СЕРВЕР"

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

int beta = alfa+1;

то обращаемся к значению, содержащемуся в переменной alfa: прибавляем к нему 1 и инициализируем переменную beta этим новым значением, 11.

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

Если переменные для хранения данных имею разную длину, то указатели сами по себе занимают одинаковое количество байт: 2 или более, в зависимости от того, с какой областью памяти работает программа. В два байта больше чем 65535, как известно, записать нельзя (см. Переменная). То есть, в двухбайтных указателях можно хранить только адреса не более этого значения. То есть, все данные, с которыми работает программа, должны помещаться в эту область. Если эта область больше, указатели будут занимать 4, 8 или 16 байт.

Указатель не является самостоятельным типом, он всегда связан с каким-либо другим конкретным типом.

Указатели объявляются так <тип>*<идентификатор>, …;

При объявлении указателя звездочка(*) дает компилятору понять, что это именно указатель. Например:

int * a;

//a - указатель на целую переменную

double * things;

//указатель на вещественную переменную двойной точности

const int * pci;

//pci - указатель на целую константу

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

int *a, b, *c;

(a и c - указатели на целую переменную, b - целочисленная переменная).

С помощью амперсанта (&) можно получить адрес любой переменной. Например:

int number=88;

int *a=&number; //в указателе a сейчас находится адрес переменной number. Можно посмотреть, что находится в переменной, на которую указывает указатель. Такая операция называется разыменованием. Когда указа-

тель уже объявлен, звездочка означает разыменование. Например: int number=88;

int * a = &number;

 

cout <<number;

//выводит содержимое переменной number (88)

cout << *a;

//значение переменной, на которую указывает a - тоже 88

int cool=*a*number;

//содержимое cool=88 в квадрате

30

Одесский колледж компьютерных технологий "СЕРВЕР"

Указатели чаще всего используют при работе с динамической памятью, называемой кучей (в переводе с английского heap - куча). Это свободная память, в которой можно во время выполнения программы выделять место в соответствии с потребностями. Выделенные участки динамической памяти называются динамическими переменными. Доступ к ним производится только через указатели. Время жизни динамических переменных - от точки создания переменной до конца программы или до явного освобождения памяти.

При определении указателя надо выполнить его инициализацию, то есть присвоить начальное значение. Непреднамеренное использование неинициализированных указателей - распространённый источник ошибок в программах. Инициализатор записывается после имени указателя либо в круглых скобках, либо после знака равенства.

int a = 5;

//a - целая переменная

int* p = &a;

//в указатель p записывается адрес a

int* p (&a);

//то же самое, только другим способом

int* r = p;

//указателю r присваивается значение указателя p

Указателю можно присвоить адрес области памяти в явном виде: char* vp = (char *)0xB8000000;

Здесь 0xB8000000 - шестнадцатеричная константа, (char *) - операция приведения типа: константа преобразуется к типу «указатель на char». Синтаксис операции явного приведения типа такой: перед именем переменной в скобках указывается тип, к которому её требуется преобразовать. При этом не гарантируется сохранение информации, поэтому в общем случае явных преобразований типа следует избегать.

Указателю можно присвоить нулевое значение:

int* suxx = NULL;

// NULL - указатель, равный нулю

int* rulez = 0;

//вместо NULL лучше использовать просто 0

Выделение динамической памяти осуществляется операцией new. С помощью оператора new выделяется память (сколько нужно), и адрес, с которого она начинается, заносится в указатель на нужный тип. Синтаксис new:

<указатель_на_тип>=new <этот_тип>;

Например:

 

int* n = new int;

//1

int* m = new int (10)

//2

В операторе 1 операция new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес начала этого участка в переменную n. Память под саму переменную n (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

31

Одесский колледж компьютерных технологий "СЕРВЕР"

В операторе 2, кроме описанных выше действий, производится инициализация выделенной динамической памяти значением 10.

Освобождение памяти, выделенной с помощью операции new, должно выполняться с помощью delete. При этом переменная-указатель сохраняется и может инициализироваться повторно. Приведённые выше динамические переменные n и m уничтожаются следующим образом:

delete n; delete m;

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

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

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

Указателю не может быть присвоена величина, не являющаяся адресом. int alfa = 10;

int *pi = &alfa;

pi = alfa; //ошибка: pi не может принимать значение int Нельзя присвоить указателю одного типа значение, являющееся ад-

ресом переменной другого типа. Если определены следующие переменные: double beta;

double *pd = β

то следующие выражения присваивания вызовут ошибку при компиляции: pi = pd;

pi = β

Дело не в том, что переменная pi не может содержать адреса переменной beta, - адреса объектов разных типов имеют одну и ту же длину. Такие операции смешивания запрещены потому, что интерпретация переменных компилятором зависит от типа указателя на них.

Бывают случаи, когда нас интересует само значение адреса, а не объект, на который он указывает (допустим, мы хотим сравнить этот адрес ка- ким-то другим). Для разрешения таких ситуаций введён специальный указатель - void, который может указывать на любой тип данных, и следующие выражения будут правильны:

void *pv = pi; pv = pd;

32

Одесский колледж компьютерных технологий "СЕРВЕР"

Тип переменной, на которую указывает void*, неизвестен, и мы не можем манипулировать этой переменной. Всё, что мы можем сделать с таким указателем - присвоить его значение другому указателю или сравнить с ка- кой-либо адресной величиной.

Операции с указателями

Если к указателю прибавить 1 (инкремент), то изменится адрес, на который он указывает. Но это не значит, что он начнет указывать на следующий байт. Адрес увеличится на размер типа указателя. То есть:

char ch;

 

char* chPtr;

 

chPtr++;

//Увеличился на размер char(1 байт)

double db; double *dbPtr;

dbPtr++; //Увеличился на размер double(8 байт)

Таким же образом к указателю можно прибавить любую целую константу. Разность двух указателей - это разность их значений, делённая на

размер типа в байтах (в применении к массивам разность указателей, например, на третий и шестой элементы равна 3).

Суммирование двух указателей не допускается.

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

*p++ = 10;

Операции разыменования и инкремента имеют одинаковый приоритет и выполняются справа налево, но поскольку инкремент постфиксный, он выполняется после выполнения операции присваивания. То же самое можно записать подробнее:

*p = 10; p++;

Выражение (*p)++ инкрементирует значение, на которое ссылается указатель.

Можно использовать указатели на константы - это указатели, объявленные с квалификатором const:

const double *cptr;

где cptr - указатель на переменную типа cost double. Сам указатель - не константа, значит, его значение можно изменять. Адрес константного объекта присваивается только указателю на константу.

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

Ссылка представляет собой синоним имени, указанного при инициализации ссылки. Она служит для задания объекту дополнительного име-

33

Одесский колледж компьютерных технологий "СЕРВЕР"

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

тип & имя; где тип - это тип величины, на которую указывает ссылка, & - оператор

ссылки, означающий, что следующее за ним имя является именем перемен-

ной ссылочного типа, например:

 

int kol=10;

 

int& pal = kol;

//ссылка pal - альтернативное имя для kol

const char& CR = '\n';

//ссылка на константу

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

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

pal += 2;

прибавляет 2 к kol - переменной, на которую ссылается ссылка pal.

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

При работе с массивами чисто используются указатели. Идентификатор массива является константным указателем на его нулевой элемент. Например, для целочисленного массива b[n] имя b - это то же самое, что указатель на нулевой элемент: &b[0]. Можно описать указатель, присвоить ему адрес начала массива и работать с массивом через указатель.

Если написать: b++, то начало массива сдвинется вперед на один элемент(на 2 байта(длина int); компилятор сам за этим следит). Теперь, чтобы получить доступ к элементу массива, который раньше назывался b[0], нужно написать b[-1].

Запись: int a=b+3 равнозначна int a=b[3];

Запись: a=*b+3 равнозначна a=b[0]+3;

Фрагмент программы копирует все элементы массива a в массив b:

int a[100], b[100];

 

int *pa = a;

// или int *p = &a[0];

int *pb = b;

 

for (int i=0; i<100; i++)

 

*pb++=*pa++;

// или pb[i] = pa[i];

Проход по массиву можно осуществлять с помощью индекса или с помощью указателей:

#include <iostream.h> int main() {

34