русс | укр

Языки программирования

ПаскальСиАссемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование

Все о программировании


Linux Unix Алгоритмические языки Аналоговые и гибридные вычислительные устройства Архитектура микроконтроллеров Введение в разработку распределенных информационных систем Введение в численные методы Дискретная математика Информационное обслуживание пользователей Информация и моделирование в управлении производством Компьютерная графика Математическое и компьютерное моделирование Моделирование Нейрокомпьютеры Проектирование программ диагностики компьютерных систем и сетей Проектирование системных программ Системы счисления Теория статистики Теория оптимизации Уроки AutoCAD 3D Уроки базы данных Access Уроки Orcad Цифровые автоматы Шпаргалки по компьютеру Шпаргалки по программированию Экспертные системы Элементы теории информации

Работа с динамической памятью.. 59


Дата добавления: 2014-04-05; просмотров: 972; Нарушение авторских прав


Список литературы.................................................................................... .. 79

 

Указатели. 4

Указатели. 4

Понятие указателя. 4

Работа с указателями. 5

Арифметика указателей. 7

Ошибки при работе с указателями. 8

Указатели и массивы.. 10

Функции и структура программы.. 12

Создание и использование функций. 13

Процедурный подход к разработке программ.. 13

Определение функций в программе. 19

Завершение работы функции (инструкция return) 20

Список параметров функций. 21

Обращение к функциям в программе. 21

Передача данных по значению.. 22

Передача данных с помощью указателей. 23

Передача данных по ссылке. 25

Перегружаемые функции. 26

Параметры по умолчанию.. 27

Функции с переменным числом параметров. 27

Рекурсивное использование функций. 29

Передача функций в качестве параметров. 32

Встраиваемые функции (inline - функции) 33

Прототипы функций. 34

Структура программы. Глобальные и локальные данные (области видимости и время жизни) 35

Структура программы.. 35

Глобальные и локальные данные. 35

Классы памяти. 36

Многофайловые проекты.. 39

Структуры, объединения, перечисления. 39

Структуры.. 39

Определение структур. 39

Доступ к полям структур. 40

Указатели на структуры.. 42

Структурные параметры функций. 42

Битовые поля структур. 44

Объединения. 45

Обычные объединения. 45

Анонимные объединения. 46

Перечисления. 47

11. Организация ввода/вывода и работа с файлами. 48

Потоки для работы с файлами. 48

Общие сведения. 48

Пример работы с файлом.. 48

Создание потока, открытие и закрытие файла. 49

Запись и чтение данных в текстовых файлах. 51

Запись и чтение данных в двоичном режиме. 51



Как обнаружить конец файла?. 54

Прямой доступ при работе с файлами. 54

Статус потоков ввода-вывода. 55

Некоторые другие функции управления потоками ввода-вывода. 56

Примеры по работе с файлами. 56

Работа с динамической памятью.. 59

Распределение памяти при работе программы.. 59

Динамическое выделение и освобождение памяти в стиле C++.. 59

Динамическое выделение и освобождение памяти в стиле C.. 62

Возможные ошибки при работе с динамической памятью.. 63

Динамические массивы.. 64

Одномерные однонаправленные списки. 70

Одномерные двунаправленные списки. 70

Многомерные списки. 70

Стек. 70


8. Указатели

 

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

8.1. Указатели

Понятие указателя

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

 

                                       
                                       
 

 

При определении, например, некоторой переменной, она располагается в памяти по определенному адресу и занимает столько ячеек, сколько требует тип этой переменной. Пусть, например, имеется переменные int A = 2351 и double B = 3.1 и пусть они располагаются в памяти так:

 

      А = 2351 B = 3.14          
                                       
 
                                                                             

 

Говорят, что переменная А располагается по адресу 101 и занимает 4 байта, а переменная B имеет адрес 105 и занимает 8 байт памяти.

Для получения адреса какого-либо программного объекта используется оператор &. Например, если выполнить фрагмент следующей программы (в предположении, что переменные A и B располагаются в памяти, как это показано на предыдущем рисунке):

 

int A = 2351;

double B = 3.14;

cout << “Значение переменной А: ” << A << endl;

cout << “Адрес переменной А: ” << &A << endl;

cout << “Значение переменной В: ” << В << endl;

cout << “Адрес переменной В: ” << &В << endl;

 

получим следующий результат:

 

Значение переменной А: 2351

Адрес переменной А: 101

Значение переменной В: 3.14

Адрес переменной В: 105

 

Правда, значения адресов переменных будут выведены в шестнадцатеричном формате.

 

Указатели – это тоже обычные переменные, но они служат для хранения адресов памяти.

 

Указатели определяются в программе следующим образом:

 

<тип данных> *<имя переменной>

 

Здесь <тип данных> определяет так называемый базовый тип указателя.

<Имя переменной> является идентификатором переменной-указателя.

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

Например:

 

int *p1;

double *p2;

 

Здесь определены две переменные-указатели (или просто – два указателя). Указатель p1 является переменной-указателем на базовый тип int (или, как говорят, переменная p1 указывает на int - значение), а указатель p2 указывает на double – значение.

Иными словами, переменная p1 предназначена для хранения адресов участков памяти, размер которых соответствует типу int (4 байта), а переменная p2 - для хранения адресов участков памяти, размер которых соответствует типу double (8 байт).

Формально указатели представляют собой обычные целые значения типа int и занимают в памяти 4 байта не зависимо от базового типа указателя. Значения указателей при их выводе на экран представляются как целые значения в шестнадцатеричном формате.

Работа с указателями

Присвоить указателю адрес некоторой переменной можно инструкцией присваивания и операцией &, например, так (возьмем предыдущий пример):

 

int A = 2351, *p1;

double B = 3.14, *p2;

p1 = &A; // Указателю p1 присваивается адрес переменной А

p2 = &B; // Указателю p2 присваивается адрес переменной В

cout << “Значение переменной А: ” << A << endl;

cout << “Адрес переменной А: ” << p1 << endl;

cout << “Значение переменной В: ” << В << endl;

cout << “Адрес переменной В: ” << p2 << endl;

 

Результат выполнения этого фрагмента программы будет таким же, как и раньше.

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

Получить значение объекта, на который ссылается некоторый указатель можно с помощью операции * (эту операцию обычно называют разыменованием указателя):

 

int A = 2351, *p1;

double B = 3.14, *p2;

p1 = &A; // Указателю p1 присваивается адрес переменной А

p2 = &B; // Указателю p2 присваивается адрес переменной В

cout << “Значение переменной А: ” << *p1 << endl;

cout << “Адрес переменной А: ” << p1 << endl;

cout << “Значение переменной В: ” << *p2 << endl;

cout << “Адрес переменной В: ” << p2 << endl;

 

Результат выполнения этого фрагмента программы будет таким же, как и в предыдущем примере.

Обращение к указателю с помощью оператора * (например, *p1) означает следующее: взять из памяти по адресу, хранящемуся в указателе (p1 равно 101), столько байт памяти, сколько требуется базовому типу указателя (в данном случае базовый тип указателя int, следовательно – взять 4 байта) и работать с этими байтами, как со значением базового типа указателя (в нашем примере это значение 2351 типа int). Таким образом, *p1 – это (в нашем примере) обычное значение типа int и с ним можно работать как с обычным целым числом.

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

 

*p1 = 4211;

 

приведет к тому, что переменная A, на которую ссылается указатель p1, станет равна 4211, а не 2351.

Указатели могут использоваться в различных выражениях наравне с обычными переменными и константами:

 

B = (*p1 – 1000) * 2; // Переменная В станет равна значению (4211 - 1000) * 2 = 6422.0

 

Или так:

 

*p2 = (*p1 – 1000) * 2; // Переменная B также будет равна значению 6422.0

 

Внимание. При использовании указателей в выражениях важно помнить, что операция * имеет наивысший приоритет по отношению к другим операциям (за исключением операции унарный – (минус)).

Значения переменных указателей можно инициализировать при их определении, как обычные переменные:

 

int A = 5, B = 10, *p1 = &A, *p2 = &B;

double D = 3.14, *d = &D;

 

Здесь указатели p1 и p2 указывают на переменные A и B соответственно. Указатель d – на переменную D.

Указателю можно присвоить значение другого указателя, если совпадают их базовые типы:

 

p1 = p2; // Теперь оба указателя p1 и p2 ссылаются на переменную B

d = p1; // Ошибка – базовые типы не совпадают

 

Последней ошибки можно было бы избежать с помощью явного преобразования типов, например, так:

 

d = (double *) p1;

 

Но теперь указатель d будет содержать адрес переменной B типа int, а попытка взять значение по этому адресу через указатель d приведет к тому, что с этого адреса будут взяты не 4, а 8 байт памяти и полученное значение *d типа double будет непредсказуемым. Еще более неприятная ситуация может возникнуть, если попытаться записать данные по указателю d. Такая попытка может завершиться тем, что будут изменены данные, расположенные за переменной B, а это приведет к неправильной работе программы. Обнаружить такие ошибки бывает очень сложно.

Явное преобразование указателей нежелательно. Но если это необходимо, то делать это надо очень аккуратно.

Хотя формально указатели представляют собой целые значения, присваивать им произвольные целые значения нельзя. Например, попытка присвоить указателю p1 значение 10000 (p1 = 10000;) приведет к возникновению ошибки на этапе компиляции программы. Единственным исключением является присвоение указателю нулевого значения:

 

p1 = 10000; // Ошибка

p1 = 0; // Все правильно

 

Принято соглашение о том, что нулевое значение указателя означает то, что указатель ни на что не ссылается (пустой указатель, не содержащий никакого адреса).

Указатели можно сравнивать с помощью операций отношения ==, !=, >, <. С помощью таких сравнений можно характеризовать взаимное расположение объектов, на которые ссылаются сравниваемые указатели, в памяти. Но сравнение указателей с помощью операций >, < имеет смысл только в том случае, если сравниваемые указатели содержат адреса связанных между собой переменных (например, элементов массива).

Арифметика указателей

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

Рассмотрим следующий пример:

 

int A = 20, B = 30;

int *p1 = &A;

 

Пусть переменные A и B расположены в памяти, например, так, как это показано на следующем рисунке:

 

    A = 20 B = 30 p1 = 100            
                                       
 
                                                                             

 

Указатель p1 содержит адрес переменной A, который равен 100 и *p1 будет равно значению переменной A, то есть 20. Выполним следующую операцию:

 

p1 = p1 + 1;

 

или, что то же самое:

 

p1++;

 

Значение указателя изменится и станет равным 104, а не 101, как, наверное, ожидалось. То есть теперь указатель ссылается уже на переменную B и значение *p1 будет равно 30.

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

 

p1 = p1 + N; // N – некоторое целое значение

 

значение указателя увеличится на sizeof(<базовый тип указателя>) * N и в нашем случае это приращение будет равно sizeof(int) * N = 4 * N. Так, если N = 4, а p1 = 100 , то значение указателя p1 увеличится на 16 и станет равно 116, и указатель будет ссылаться на данные, расположенные по адресу 116.

Внимание. Добавлять к указателям или вычитать из указателей можно только целые значения.

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

*(p1 + 1) и *p1 + 1

 

имеют совершенно разный смысл. Первое выражение даст значение 30, а второе выражение будет равно 21 (в первом выражении сначала изменяется адрес, а затем осуществляется обращение в память по этому измененному адресу; во втором выражении мы обращаемся по старому адресу и к значению, хранящемуся по этому адресу добавляем 1).

Ошибки при работе с указателями

Указатели – это очень мощное, полезное, но и очень опасное средство. Ошибки, которые возникают при неправильном использовании указателей, кроме того, что могут приводить к серьезным и непредсказуемым ошибкам в работе программы, еще и очень трудно диагностировать (обнаруживать).

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

Рассмотрим следующий пример:

 

int *p1;

*p1 = 1001;

cout << *p1;

 

Хотя с точки зрения синтаксиса этот фрагмент программы корректен, попытка его выполнения закончится, скорее всего, плачевно. Когда мы определяем указатель (int *p1;), в некотором участке памяти создается обычная переменная – указатель, но поскольку значения этой переменной никакого не присвоено (она не инициализирована), то ее значение будет соответствовать тем случайным данным (“мусору”), которые содержались в этом участке памяти. Таким образом, неинициализированный указатель будет содержать некоторый случайный адрес. Дальнейшие попытки обратиться по этому адресу в память могут привести к одному из двух неприятным последствиям. Если это случайное значение адреса будет указывать на недопустимую область памяти (например, за пределами памяти, выделенной для нашей программы), то возникнет ошибка времени выполнения, и программа аварийно завершит свою работу. Но может быть и хуже. Если случайно значение указателя будет содержать адрес, принадлежащей области памяти нашей программы, то произойдет непредсказуемое изменение данных программы. Она (программа) может продолжить свою работу, а последствия такого несанкционированного изменения в программе могут сказаться значительно позднее и вызвать некорректное поведение программы. Обнаружить причину возникновения подобных ошибок чрезвычайно трудно.

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

 

int *p1 = 0;

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

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

 

if (p1) // Если указатель не равен 0, то все в порядке

{

*p1 = 1001;

cout << *p1;

}

else

// Реакция на ошибочную ситуацию

;

 

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

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

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

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

 

8.2. Указатели и массивы

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

Кода мы определяем в программе некоторый массив, например,

 

int Arr[10]

 

переменная Arr без индексов представляет собой указатель на первый элемент массива в данном случае из 10 целых чисел (содержит адрес первого элемента массива). Если вывести на экран значение переменной Arr

 

cout << Arr:

 

мы увидим некоторое целое значение в шестнадцатеричном формате, соответствующее адресу первого элемента этого массива.

Замечание. Именно по этой причине в языке C++ отсутствует операция присвоения сразу всех значений одного массива другому (в некоторых других языках, например, в Pascal такая возможность имеется). Действительно, если имеются два массива

 

int A1[10], A2[10]

 

то попытка выполнить присвоение A1 = A2 привела бы к тому, что переменная A1 стала бы указывать на ту же область памяти, что и переменная A2 (мы скопировали адрес из A2 в A1, а не содержимое одного массива в другой). Адрес, который хранился ранее в переменной A1, был бы утерян, что привело бы к утечке памяти (для десяти элементов массива A1 в памяти было выделено место, но теперь мы “забыли”, где оно находится, то есть потеряли память). По этой причине подобные операции с массивами в языке C++ запрещены. Более того, запрещены любые изменения значения переменной массива.

Указателю, имеющему такой же базовый тип, как и элементы массива, можно присвоить массив следующим образом:

 

int Arr[10];

int *p;

p = Arr;

 

Но обратное присвоение выполнить невозможно:

 

Arr = p; // Ошибка

 

Такое присвоение невозможно, поскольку переменная массива – это константа, изменение которой запрещено.

Так как переменная массива является указателем на первый элемент массива, появляются дополнительные возможности по работе с массивами на основе использования арифметики указателей. Например, чтобы получить 5–й элемент массива Arr можно воспользоваться одним из следующих выражений:

 

Arr[4] или *(Arr + 4) или *( p + 4)

 

Первое выражение – это пример обычной индексации элементов массива. Во втором и третьем выражениях мы использовали арифметику указателей и с помощью операции + получили адрес пятого элемента массива. Затем с помощью операции * взяли значение по этому адресу и получили значение 5-го элемента массива. Обратите внимание на скобки в этих выражениях, если их не поставить и написать *Arr + 4 или *p + 4, то эти выражения будут равны значению первого элемента массива увеличенного на 4, так как операция * имеет больший приоритет, чем операция +.

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

 

int A[10];

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

{

cin >> A[i];

A[i] = A[i] * A[i];

}

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

cout << A[i] << “ “;

cout << endl;

…..

 

А вот тот же фрагмент, но с использованием арифметики указателей:

 

int A[10];

for (int *Next = A, *End = Next + 9; Next <= End; ++ Next)

{

cin >> *Next;

*Next = *Next * *Next; // *Next = (*Next) * (*Next);

}

for (int *Next = A, *End = Next + 9; Next <= End; ++ Next)

cout << *Next << “ “;

cout << endl;

…..

 

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

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

 

int A[10], *P = A;

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

cout << P[i] << “ “;

 

Можно создавать и массивы указателей. Например:

 

int a = 1, b = 2, c = 3, *M[3];

M[0] = & a; // Элементам массива М присваиваются адреса переменных a, b и c

M[1] = & b;

M[2] = & c;

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

cout << *M[i] << “ ”;

cout << endl;

 

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

 

С помощью массивов указателей можно моделировать различные интересные конструкции данных. Например, пусть имеется квадратная матрица размерности 5 х 5 симметричная относительно главной диагонали. Для ее однозначного представления достаточно хранить в памяти не все 25 элементов этой матрицы, а только 15 (например, элементы под главной диагональю вместе с элементами главной диагонали). Для этого можно предложить следующую конструкцию:

 

int A1[1], A2[2], A3[3], A4[4], A5[5], *A[5] = { A1, A2, A3, A4, A5 };

 

// Вводим 15 целых значений - элементы под главной диагональю и диагональные

// элементы матрицы

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

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

cin >> A[i][j];

cout << endl;

 

// Выводим симметричную матрицу 5 на 5 на экран

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

{

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

cout << A[i][j] << " ";

for (int j = i + 1; j < 5; ++ j)

cout << A[j][i] << " ";

cout << endl;

}

cout << endl;

 

А это пятиэлементный массив указателей на символы, инициализированный некоторыми текстовыми строками:

 

char * Words[5] = { "Слово1", "Слово2", "Слово3", "Слово4", "Слово5" }

 

Как это работает: когда компилятор встречает в программе некоторый текст, заключенный в кавычки, в памяти создается символьный массив соответствующей этому тексту длины и адрес этого символьного массива присваивается соответствующему элементу – указателю массива Words.

9. Функции и структура программы

 

Создание и использование функций. Вызов функции (аргументы функции) и возврат значения. Передача параметров по значению, по ссылке. Глобальные и локальные переменные. Классы памяти и область действия. Автоматические переменные. Внешние переменные. Статические переменные. Внешние статические переменные. Регистровые переменные. Функции с переменным количеством аргументов. Использование функции как параметра другой функции. Рекурсия. Представление программы в виде набора функций. Многофайловая структура программы.

Использование функций позволяет:

Значительно упростить разработку сложных программ;

Сократить объем текста программы и генерируемого результирующего кода программы;

Значительно упростить отладку и модификацию программ;



<== предыдущая лекция | следующая лекция ==>
Систематические ЦК. | Распределить работу над одной программой между различными исполнителями программистами.


Карта сайта Карта сайта укр


Уроки php mysql Программирование

Онлайн система счисления Калькулятор онлайн обычный Инженерный калькулятор онлайн Замена русских букв на английские для вебмастеров Замена русских букв на английские

Аппаратное и программное обеспечение Графика и компьютерная сфера Интегрированная геоинформационная система Интернет Компьютер Комплектующие компьютера Лекции Методы и средства измерений неэлектрических величин Обслуживание компьютерных и периферийных устройств Операционные системы Параллельное программирование Проектирование электронных средств Периферийные устройства Полезные ресурсы для программистов Программы для программистов Статьи для программистов Cтруктура и организация данных


 


Не нашли то, что искали? Google вам в помощь!

 
 

© life-prog.ru При использовании материалов прямая ссылка на сайт обязательна.

Генерация страницы за: 0.218 сек.