Программирование
для начинающих
С++ как первый язык обучения
(Базовый курс)
Фрагмент главы 8. Указатели
8.1 Что такое указатель.
Итак, темой сегодняшнего урока будут указатели. У большинства студентов эта тема часто ассоциируется с чем - то сложным и непонятным. Но так происходит всегда, когда человек трудно представляет себе, для чего нужен тот или иной инструмент языка. Начнем по порядку.
Есть программа, в которой объявлены две переменные и указатель.
Листинг 8.1.
#include "stdafx.h"
#include <iostream>
#include <iomanip>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
1) int a = 5, b = 7;
2) int * pA;
3)
cout<<&a<<' '<<&b<<endl;
return 0;
}
К этому времени Вы уже знаете, что любые
используемые переменные где то лежат в
оперативной памяти, а схематически их удобно представлять на адресном
пространстве.

Рис.8.1
Например, на рисунке показано, что у
Вас есть два гигабайта оперативной памяти
а Ваши переменные где то лежат и в них хранятся значения 5 и 7 (В
указателе пока что мусор, и он ни на кого еще
не указывает). Вот только теперь
мы будем уточнять, где именно они лежат.
Если мы говорим, что был выделен кусок памяти размером в 4 байта, 100 мегабайт, 2 гигабайта, то это
означает, что каждый байт этого блока имеет свой адрес.
Так вот, номер первого байта любой области памяти (в данном случае любой
переменной) и есть ее адрес. На рисунке 8.1 адреса показаны в шестнадцатеричном формате. Если говорить более точно, то реальные адреса
выглядят немного длиннее – каждый адрес
в шестнадцатеричном формате состоит из 8-и значного числа. Например, при выполнении третьей строчки кода из
Листинга 8.1, Вы увидите приблизительно такие значения
0012FF64 0012FF58. Я просто
для краткости пишу не все 8 цифр, а только
2-3 и добавляю приставку 0х, чтоб было понятно, что это адрес. Кстати, если Вы переведете калькулятор в инженерный вид, нажмете на кнопку Hex, введете этот адрес 0012FF64, а потом
нажмете на кнопку Dec, Вы увидите число 1 245 028. Это и есть номер первого байта Вашей переменной а. Скорее всего на Вашей машине будут
совсем другие адреса, но это уже не
принципиально. Кстати, по закону стека, переменная, объявленная позже, лежит в памяти
раньше – рисунок правильный.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Шестнадцатеричный формат, это всего
лишь еще одна форма записи какого нибудь числового значения. Вы прекрасно
знаете, что такое десятичный формат. Но давайте распишем его для понимания
шестнадцатеричного. Итак, допустим у нас
есть число 6783. Мы с первого взгляда
уже можем сказать, что это шесть тысяч семьсот восемьдесят три (мы к этому привыкли еще со школы). А теперь давайте распишем это для
машины. Итак 6783 получается в
результате сложения
6000 + 700 +80 + 3 или, если это же записать более развернуто, то
6783 = 6 * 103 + 7 * 102 + 8
* 101 + 3 * 100.
Почему так, да потому что позиции
цифр числа считаются справа налево и начинаются
с нуля –
63 72 81 30. Мы просто к этому уже привыкли и поэтому даже
не задумываемся, как мы воспринимаем
число. Но будем разбирать тему дальше. В каждой позиции числа можно записать
цифру в диапазоне от 0 до 9 – всего 10 разных цифр. То есть каждая позиция может хранить значение
от 0 до 9. В шестнадцатеричном формате каждая позиция може хранить значение от
0 до 15. Но вот только невозможно число 10 или 15 записать в одну позицию
используя только цифры - это уже будут
две позиции – символ 1 и символ 5. Для
этого в шестнадцатеричном формате добавили к цифрам буквы A B C D E F, которые эквивалентны значениям A = 10, B = 11, C = 12, D = 13, E = 14 и F = 15. В итоге число 6783 в
шестнадцатеричном формате будет записано как 1A7F. Возникает вопрос, а почему для записи адресов используется именно этот
формат? Ответ: Для более сложной
работы с адресами очень удобно знать какая часть адреса лежит в каждом из
байтов указателя. Например, размер указателя в 32-х разрядной программе (то
есть написанной для 32-х разрядной платформы)
равен 4 байтам. Значит адрес записывается в 4-х байтовую область памяти.
Если Вы выполните такой код:
int a;
cout<<&a<<endl;
Вы
увидите приблизительно такой адрес 0012FF64. Его
удобно рассматривать попарно 00 12 FF 64, потому что каждая такая пара
представляет собой ту часть значения адреса, которая лежит в соответствующем
байте. То есть, в первом байте лежит
число 64, во втором FF, а в третьем 12.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Те, кто уже более глубоко знаком с програмированием и принципами работы программ, могут сказать, что эти адреса на самом деле вовсе не адреса в оперативной памяти, а фиксированные адреса в адресном пространстве, созданном операционной системой для нашей программы. И что менеджер памяти постоянно при использовании вытесняющей многозадачности будет размещать наши переменные в оперативной памяти в только ему известных местах, и что постоянно будет выполняться соотношение (проецирование) между реальными адресами в оперативке и адресами в адресном пространстве. Да, они будут абсолютно правы. Все именно так и есть. Но моя книга рассчитана на начинающих программистов, и я в ней хочу исправить ошибку многих авторов, которые нагружают студента сразу очень большим объемом информации. Информация точная, ценная, но ее бывает слишком много для понимания конкретной темы. Поэтому я пока что буду объяснять так, чтобы это было понятно начинающим – адрес переменной в оперативной памяти. Тем же, кто хочет сразу охватить все глубины этой темы – однозначно рекомендую любую книгу Джеффри Рихтера по написанию приложений для Windows (Programming Applications for Microsoft Windows 4-th edition, или Windows via C/C++, Fifth Edition) . Но ее лучше читать немного позже, или по крайней мере параллельно с книжками для начинающих и минимум два раза :-).
Но вернемся к рассмотрению указателей.
Итак, на рисунке 1
Вы видите переменные и указатель, который еще ни на кого не указывает. В нем
мусор, как и в любой другой переменной, объявленной на стеке. Итак, указатель, это переменная, предназначенная
для хранения адреса какой либо области памяти. Память нужно рассматривать
как обыкновенный сырьевой материал, с которым мы работаем, например, как кузнец
работает с железом или что еще более точнее, как экскаваторщик работает с
кузовом грузовика – он видит кузов и насыпает туда соответствующий груз. Он
видит, куда сыпать, а мы не видим физически память с которой мы работаем, мы
должны поэтому знать основные две ее характеристики - адрес первого байта, и размер этой памяти в
байтах. Тип указателя, это уже второстепенная характеристика (при всем
уважении к жесткой типизации языка С++) и мы это рассмотрим позже. Итак, если
мы допишем к листингу 8.1такой
фрагмент кода:
4) pA = &a;
Мы получим вот такую картинку:

Рис. 8.2
Теперь указатель pA хранит в себе адрес переменной а и, как мы говорим, указывает на нее.
(Кстати приставка для имени переменной pA дана от английского слова pointer, что и переводится указатель).
Теперь мы можем работать с переменной а
используя указатель. Например, поменять
ее значение.
5) *pA += 10;
в итоге к значению 5 в переменной а прибавится 10. Мы
могли бы сделать то же самое непосредственно используя имя переменной
a += 10;
И вот теперь пора объяснить одно из
предназначений указателей – возможность
обработать одним и тем же алгоритмом разные области памяти (привыкайте к
тому, что переменные – это не более чем именованные области памяти, с которыми
можно делать все, что нам будет нужно). Например, есть переменные int a = 5, b = 7,
c(0); к значению каждой из них нужно
добавить 10.
a += 10;
b += 10;
c += 10;
Алгоритм вроде бы
одинаковый, но буквы меняются, и в цикл его уже не запишешь, чтобы 2 строчками
кода обработать 3 или 100 переменных типа int.
Придется писать 3 или 100 строчек кода. Это долго, скучно и неправильно. А если
вот так:
4) pA =
&a;
5) *pA += 10;
6) pA =
&b;
7) *pA += 10;
Вот теперь 5-я и 7-я строчки кода
абсолютно одинаковые.
Кстати после 6-й строки кода наш рисунок выглядит уже так:

Рис. 8.3
А после 7-й строки в переменной b уже будет лежать
значение 17.
Рассмотрим, как воспринимать символ * возле указателя. Если инструкция написана
так
2) int * pA;
То в данном
случае звезда используется для
объявлении указателя, кстати, если записать так:
int * pA, pB, *pC;
то в этой строке
были объявлены два указателя pA и pC,а вот pB несмотря на имя, просто переменная типа int, поскольку при ее объявлении перед ней не было символа *. Теперь
дадим определение тому, что происходит в 5-й и 7-й строках кода:
5) *pA += 10;
Здесь звезда стоит
перед уже объявленным указателем и данный оператор называется
разименованием (по английски это пишется как indirection. Запомните, это Вам пригодится при рассмотрении текста ошибок,
который будет выводить компилятор.). Своим студентам я даю такое определение
разименования: Разименование, это
обращение из-под указателя к содержимому области памяти, на которую он
указывает, то есть по адресу, который в нем хранится. Понятие «из-под
указателя» нужно понимать как «используя имя указателя». Потому что у
переменной уже есть имя, но мы к ее памяти обращаемся используя имя указателя.
Такой прием работы дает возможность еще и защищать память, на которую указывает
указатель (указатель на константу - const int * pA = &a;), но об
этом позже.
Итак, в 7-й строке мы разименовываем
указатель pA, в котором хранится адрес 0х50, то есть, обращаемся к содержимому области
памяти, которая и находится по этому адресу (числу 7) и добавляем число 10. Но
прежде чем мы сможем применить разименование к указателю, он должен быть
проинициализирован – указывать на кого то. Если мы попробуем разименовать непроинициализированный
указатель или записать в указатель какое
либо число не используя оператор взятия адреса (например вместо pA = &a; напишем pA = 100;) и при разименовании записать что-либо в незнакомую область
памяти, это приведет к смерти программы.
Поэтому при работе с указателями к правилу нельзя
использовать непроинициализированные переменные, добавляется еще одно: нельзя работать с указателями указывающими
непонятно куда.
// **
advanced
С помощью указателей
можно легко обратиться к любой области памяти, даже к той, которая еще не помечена менеджером памяти, как выделенная для работы
(например пространство между
переменными). Например, если мы выведем
адреса переменных a и b ( а переменная, это и есть именованная область памяти,
которая помечена менеджером памяти, как доступная для работы) :
cout<<&a<<' '<<&b<<endl;
мы увидим следующие значения: 0012FF64 и 0012FF58. Если к 0012FF58 прибавить 4
байта (размер интовой переменной), мы
получим значение 0012FF5С, что на 8 байт отстоит от начала следующей переменной 0012FF64. Что и
доказывает, что между переменными есть адреса, которые мы не можем использовать
– там программа сама хранит какие то свои данные. И попытка обратиться к ним
приведет к смерти программы. Поэтому указатели, будучи очень мощным
инструментом для работы, являются также
источниками серьезных и труднонаходимых багов. Но бояться или избегать работы с
ними не стоит, поскольку даже в повседневной жизни любой, даже самый мирный и
полезный инструмент, в неумелых руках может легко превратиться в оружие
разрушения. Просто указатели, как и любой другой инструмент нужно вначале
хорошо изучить и затем внимательно с ними работать.
А вообще, я рекомендую взять какую либо
написанную без ошибок программу и сделать то, что я написал выше, что этого
делать нельзя. Ваша програма завалится (компьютер нет :-)). Либо сразу после
ошибочной инструкции, либо через некоторое время после нее, либо будет выдавать
неадекватные результаты работы. Посмотрите внимательно на текст в диалоговом
окне, которое выводится на экран перед смертью программы (иногда для этого нужно
перейти в режим отладки) запомните этот текст, и в следующий раз, когда у Вас
оно появится уже в результате реальной, незамеченной Вами ошибки – Вы уже
будете знать, что искать.
//**
Вопросы:
1)
Что такое указатель?
2) Есть ли какое нибудь отличие между указателями и обычными
переменными?
3) Какими характеристиками описывается любой блок памяти?
4) Когда указатель начинает на кого то указывать?
5) Могут ли два указателя указывать на одну и ту же переменную?
8.2 Указатели и массивы. Арифметика
указателей
Добавьте фрагмент кода перед рисунком 3 в программу, а также вот эту
строку:
8) cout<<a<<' '<<&a<<'
'<<b<<' '<<&b<<' '<<pA<<endl;
Вы увидите число 15,
адрес переменной а, число 17, адрес переменной b и опять адрес переменной b, поскольку в указателе pA
хранится адрес этой переменной.
Глядя на фрагмент добавленного кода Вы скажете: «А в чем же выгода от абсолютно
одинакового алгоритма, если все равно приходится переставлять указатель на
другие области памяти (пока что переменные)?». Ответ: Да. Никакого ускорения при такой работе с несколькими
переменными нет. Для того, чтобы одним алгоритмом обработать несколько областей
памяти используются не переменные, а массивы. Давайте вспомним, что такое имя
массива. Имя массива, это константный
указатель на первый элемент (байт) непрерывной области памяти выделенной под
массив. Пока что не будем обращать внимание на слово константный, но займемся
вплотную словом указатель.
Добавьте в программу вот этот код:
Листинг 8.2
8) cout<<a<<' '<<&a<<'
'<<b<<' '<<&b<<' '<<pA<<endl;
9)
10) const int SIZE = 10;
11) int iAr[SIZE];
12) cout<<iAr<<endl;
13) for(int i = 0; i < SIZE; ++i)
14) {
15) iAr[i] =
rand()%21;
16) cout<<setw(3)<<i<<setw(3)<<iAr[i]<<setw(3)<<*(iAr
+ i)<<' '<<&iAr[i]<<' '<<iAr + i<<endl;
17) }
18) cout<<endl;
Вы
должны увидеть приблизительно вот это:
Листинг 8.3
1) 15
0012FF64
17 0012FF58
0012FF58
2) 0012FF10
3) 0 20
20 0012FF10
0012FF10
4) 1 8 8
0012FF14
0012FF14
5) 2 13
13 0012FF18
0012FF18
6) 3 19
19 0012FF1C 0012FF1C
7) 4 17
17 0012FF20
0012FF20
8) 5 16
16 0012FF24
0012FF24
9) 6 12
12 0012FF28
0012FF28
10) 7
0 0 0012FF2C 0012FF2C
11) 8 19 19 0012FF30 0012FF30
12)
9 20 20 0012FF34
0012FF34
Первая
строка, это переменные a и b и их адреса. Вторая строка –
адрес массива iAr. Причем, обратите внимание, что
значения адреса в 2-й и 3-й строках листинга
8.3 одинаковое. То есть, вот доказательство, что имя
массива, это указатель на первый элемент массива. Также обратите внимание,
на то, что каждый следующий элемент массива отстоит от предыдущего на 4 байта,
поскольку массив типа int, а размер инт-а 4 байта.
(Конечно, более правильно говорить, что размер любого типа в байтах равен sizeof(type) – это более универсальное
определение и годится для работы на любой платформе).
Давайте
рассмотрим 16-ю строку кода, а именно выражение *(iAr + i). Это арифметика указателей. На самом деле, вот такой привычный кусок кода iAr[i] раскладывается компилятором именно на вот
такое выражение *(iAr + i). (Точнее
раскладывается operator [].)
Как это все работает? В скобках
вычисляется новый адрес, который потом разименовывается и выполняется работа с
содержимым памяти по этому адресу. Правило
вычисления адреса: При прибавлении к указателю числа, к значению адреса
указателя прибавляется произведение этого числа на размер типа указателя.
Сложно, согласен. Значит нужен рисунок.
Вот первые пять элементов массива iAr, которые показаны в листинге 8.3.

Рис. 8.4
Для сокращения я взял только
последние три цифры адреса, а сверху ячеек массива написаны их индексы. Как видно
из строки 2 листинга, имя массива хранит адрес
первого байта (первого элемента). Итак, выполняем стандартную операцию
iAr[2] += 10;
Компилятор разложит это на *(iAr + 2) +=
10;
подставляем значение указателя *(0xF10 + 2) +=
10;
Согласно правилу, к указателю прибавится
не 2, а 2 * 4 поскольку указатель (имя массива) был объявлен как int iAr[SIZE]; а размер инта – 4 байта. Зачит, получаем *(0xF10 + 2 * 4) += 10; что в результате
дает *(0xF18) += 10; То есть,
полученный адрес соответствует адресу второй (по индексу) ячейки массива, где
уже лежит число 13. При разименовании, а
оно имеет более высокий приоритет чем + или =,
идет обращение к содержимому этой
ячейки, и в результате там будет лежать уже число 23. Вот так происходит работа
с массивами.
А вот теперь скажите пожалуйста, если у
нас есть массив типа double. Например double dArr[10]; При выполнении такой операции dArr[5] =
12.456; Выражение dArr[5] , а это
тоже выражение, поскольку в нем используется опеоратор [], раскладывается на *(dArr + 5). Вопрос: 1) Сколько байт прибавится к значению адреса,
записанному в указателе dArr? 2) Какой размер указателя?
Ответ: 1) прибавится 5 * 8, т.е. 40
байт; 2) те же 4 байта, что и у указателя типа int.
А если это будет массив типа char? Тогда 1) 5 байт; 2) 4 байта. На вопросы №1 ответ был дан в
правиле: прибавляется произведение
размера типа на число. А вот ответы
вопросы №2 нуждаются в более точной фоормулировке.
//** advanced
Размер указателя зависит от разрядности программы. Посметрите пож. вверх на
панель инструментов Visual Studio. Там рядом с окном, в котором написано Debug есть окно,
в котором Вы увидите Win32. Вспомните кокой тип проекта Вы заказывали – консольное Win32
приложение (Win32 Console Application). Указав Win32, Вы говорите Visual Studio, что
собираетесь писать приложение, которое должно работать на 32-х разрядных
пратформах Windows. Разрядность платформ зависит от возможностей железа, на
котором эта платформа будет работать. Например, 32-х разрядная означает, что:
за один такт процессора по шине передается 32 бита информации, размер регистра
процессора тоже 32 бита, а также то, что для хранения адреса участка памяти
выделяется тоже 4-е байта. В 4-е байта, как Вы знаете можно записать значения
от 0 до 4294967295. А это означает, что программа может максимум оперировать
одновременно с 4 гигабайтами памяти. (В реальности Windows XP максимум может использовать 3 гигабайта оперативки).
Поэтому на 32-х разрядной платформе любой указатель будет иметь размер 4-е
байта, а на 64-х разрядной, соответственно, 8 байт.
**//
8.3 Динамически выделяемая память
Итак, указатель способен хранить
адрес как отдельной переменной, так и целого блока памяти – массива, ему все
равно. Из этого следует еще одна функциональность, связанная с указателями –
динамическое выделение памяти. Вспомните правило:
при объявлении массива на стеке, его размер должен быть указан с помощью
константы, а значит должен быть известен еще на этапе компиляции. Но в
реальной жизни нам часто неизвестно, какой размер хранилища данных (памяти)
может понадобиться . А заказывать сразу очень большой массив – это означает
нерационально расходовать ресурсы системы.
Поэтому, для того, чтобы можно
было создать хранилище (массив) такого размера, который будет известен при
выполнении программы (runtime), в С++ существуют следующие инструменты: операторы new и new []. С их
помощью память выделяется в куче (по-английски НЕАР). Куча – это отдельная
область памяти (не стэк) получив память в которой уже не программа, а мы отвечаем за ее возвращение.
Для выделения массива интов выполним
следующую инструкцию: int * pAr = new int[iSize]; где iSize – полученный с клавиатуры размер массива, который нам будет нужен. Обратите
внимание, что размер массива указан в квадратных скобках.
Если же вы напишите вот так: int * pA = new int(5); (после new используются круглые скобки) то выделится
всего лищь 4 байта памяти (размер инта) в которой будет лежать значение 5. Эту
инструкцию можно написать и без круглых скобок - int * pA = new int; Но тогда,
так же как и в мнструкции new int[iSize]; будет
выделен блок памяти с мусором, только размер блока будет всего 4 байта. К
сожалению не существует синтаксиса для инициализации динамически создаваемого массива
(в отличие от массива на стеке int iAr[3] = {2,3,4}; ) После выполнения инструкции int * pAr
= new int[iSize]; указатель pAr будет содержать адрес блока памяти, которая была выделена во
время выполнения программы. Но, как я уже не раз говорил, в программировании
существует много физических и логических скобок. Здесь имеют место логические
скобки – выделивши память (ресурс), мы теперь за нее отвечаем – то есть, должны
ее вернуть на место. Это правило верно для всех видов ресурсов – описатели
файлов, кистей для рисования, сокетов и так далее. Закрывающая логическая скобка
для new – оператор delete. Поэтому совет: написавши new, сразу же напишите в нужном месте оператор delete. А работа с динамической памятью
выглядит так:
int * pAr = new int[iSize];
// работа
delete [] pAr;
//|========
int * pA = new int;
// работа
delete pA;
То есть, если мы выделяем массив используя
оператор new со скобками [], мы и вернуть его должны как масссив – используя delete со скобками [], иначе программа неправильно вернет блок памяти и будет дальше
работать неадекватно (причем, это может проявиться далеко от строки выполнения
оператора delete) Нужно всегда использовать соответствующий
оператор. new и new[] – это совершенно разные и не
взаимозаменяемые операторы, точно так же как и deletе и delete[]. В программировании даже самое
маленькое отличие в синтаксисе оператора
может означать абсолютно разные действия.
Пора перейти к практике.
Давайте напишем программу, в которой мы спросим пользователя, сколько чисел он
хочет сохранить, создадим массив этих чисел, заполним его, покажем и в конце
вернем память.
Итак, добавьте перед main-ом прототипы функций Init и Show. (При объяснении
масивов на предыдущих лекциях я говорил, что массивы нужно передавать в функции
по адресу, то есть функция должна принимать указатель соответствующего типа.
Далее этой главе мы детально рассмотрим, как же передаются параметры по адресу,
значению и по ссылке.)
void Init(int * pAr, int iSize);
void Show(int *
pAr, int iSize);
после main-а тела этих функции.
void Init(int * pAr, int iSize)
{
for(int i = 0; i <
iSize; ++i)
pAr[i]
= rand() % 21;
}
void Show(int * pAr, int iSize)
{
for(int i = 0; i <
iSize; ++i)
cout<<setw(3)<<pAr[i];
cout<<endl;
}
Закомментарьте
в программе все, что мы делали раньше и вставьте вот эти строчки кода: (нумерация
продолжается с листинга 8.2)
Листинг 8.4
19) int iCount;
20) cout<<"Enter how many numbers do you want to store"<<endl;
21) cin>>iCount;
22)
23) int * pAr = new int[iCount];
24) Init(pAr, iCount);
25) Show(pAr, iCount);
26)
27) delete [] pAr;
Вы увидите
массив чисел, причем их будет столько, сколько заказал пользователь. (Мы пока
не проверяем правильность ввода – чтоб число было больше нуля и меньше
миллиарда – поскольку такое кол-во памяти нам не дадут.) А в конце работы мы в
27-й строке возвращаем память.
Вопрос: Что произойдет, если мы не
вернем память?
Ответ: При завершении программы все
ресурсы, которые она брала у системы, освобождаются. Но если программа будет
работать долго и не заботиться о возвращении памяти (ресурсов), то
невозвращенная память будет накапливаться и, в конце концов, и наша программа, и другие программы, и сама
операционка начнут ощущать нехватку оперативной памяти – станут работать
медленнее и даже могут остановиться и потребовать выключить что-нибудь для
продолжения своей работы. Эта ошибка работы програмы называется утечкой памяти.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Проделайте такой эксперимент: Подключите <windows.h> и <conio.h>, закомментируйте все внутри main-а, и
напишите вот это:
Листинг 8.5
1) int *
pAr;
2)
3)
//to stop the program before memory allocation
4) _getch();
5) for(int i = 0; i <
1000; ++i)
6) {
7) pAr = new int[200000];
8) // to see memory consumption in the task manager
9) // 0.2 sec pause. Without it all will be done instantly.
10) Sleep(200);
11) cout<<"Iteration # "<<i<<' '<<200 * (i + 1)/ 1000.0<<" sec"<<endl;
12) }
Скомпилируйте, запустите. Програма
остановится на 4-й строке – будет ждать Вашего нажатия на клавишу. А Вы в это
время запустите диспетчер задач, и перейдите на вкладку «Процессы». Найдите
Вашу программу, (у меня она называется Pointers) и посмотрите на колонку «Память»

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

Вы увидите, как количество памяти,
которую потребляет наша программа, начало быстро расти. И уже на 80-й итерации
цикла через 16.2 секунды программа уже
забрала 65 мегабайт памяти. Это тестовая программа и мы специально
инициировали утечку памяти – в 7-й
строке кода мы ее выделяем и нигде не возвращаем. Последствия такого
неграмотного кода я описал выше.
У нас с есть инструмент, позволяющий проконтролировать утечки
памяти в нашей программе: подключаем заголовочник <crtdbg.h> (аббревиатура от c runtime debug) и возвращаемся к
листингу 8.4. (То есть раскомментируйте его, а все, что связано с листингом 8.5 спрячьте от компилятора) Закомментарьте 27-ю строку кода а перед return-ом вставьте вот эти строчки:
28)
29) if(_CrtDumpMemoryLeaks())
30) cout<<"Memory Leaks!!!"<<endl;
31) else
32) cout<<"All heep memory was returned"<<endl;
Функция _CrtDumpMemoryLeaks() возвращает true если обнаружена динамически
выделенная память, которая к этому моменту не была возвращена. (Кстати Leaks так и переводится – утечки.) Остается рассмотреть некоторые
нюансы, связанные с тем, что указатель лежит на стэке, а область памяти,
полученная с помощью опреатора new в куче. Посмотрим вот такой пример:
Мы знаем, что переменная на стэке сама
умрет по достижению завершающей скобки своей области видимости:
1) {
2) int a,
b;
3) }
на 3-й строчке кода a и b умрут. То есть, выделенная под них
память вернется в систему. А что произойдет, если написать так:
1) {
2) int a = 5,
b;
3) int * pC = new int(12);
4)
5) }
на 5-й строке кода умрут a, b и указатель pC, но память, которая была выделена и чей адрес хранился в
указателе останется невозвращенной, потому что она выделене не в стеке, а в
куче. И если мы до пятой строки не вернем память, указатель умрет, и вместе
сним мы потеряем адрес памяти, которую мы должны были вернуть. То есть, будет
все та же утечка памяти.
Теперь еще один инструмент. Для
определения размера в байтах массива выделенного на стеке, мы использовали
оператор sizeof().
Вопрос: Что вернет оператор sizeof(pAr) в третьей строке кода?
1) int iCount = 5;
2) int *
pAr = new int[iCount];
3) cout<<sizeof(pAr)<<endl;
Ответ:
4. Потому что pAr находится на стеке. Для определения
размера динамической памяти используется функция _msize(), которая
объявлена в заголовочнике <malloc.h>
Пора задавать домашнее задание.
Задача 8.1: Запросите у пользователя два числа в диапазоне от 5-и до 20-и
и создайте два массива. Проинициализируйте их, покажите.
(Инициализация и показ в функциях) Напишите функцию, которая принимает эти
массивы и склеивает их – то есть создает третий массив, который по размеру
равен сумме первых двух. Скопируйте в
него все данные этих массивов верните в точку вызова (в main), покажите
его там и верните память в систему.
Задача 8.2: Запросите у пользователя размер массива, создайте его,
проинициализируйте и покажите. (Инициализация и показ в функциях) Напишите
функцию, которая принимает массив,
находит в нем все четные числа, выделяет память под них, копирует их туда и
возвращает в точку вызова. Покажите этот массив и верните память в систему.
В книге должно быть приложение с решениями заданных задач. Но поскольку это
только фрагмент книги, код решения придется показать сразу. Хотя я рекоммендую
всем попытаться вначале решить задачу самостоятельно (иначе Вы ничему не
научитесь), и только потом проверить, все ли Вы правильно сделали.
Решение
8.2: Создайте новый проект. Можете
перенести в него все, что касается листинга 8.4. Здесь уже есть получение массива,
его инициализация и показ . Напишем требуемую функцию. Вначале нужно подумать,
что она будет собой представлять, а именно, какие параметры она должна
принимать и возвращать. Итак, принимает массив, значит передаем ей указатель на
массив и его размер.
GetEvens(int * pAr, int iSize);
Функция должна вернуть массив четных чисел, который там
должен быть создан. Значит память будет выделяться динамически, функция должна
вернуть адрес этого массива и его размер. Что то можно вернуть с помощью
инструкции return. Но нужно вернуть два значения – адрес и размер. Поэтому,
адрес вернем return-ом, а для передачи размера получившегося массива передадим в
функцию адрес переменной, куда и запишем размер. В итоге прототип функции будет выглядеть так:
int * GetEvens(int * pAr, int iSize, int * pEvArSize);
Распишем тело
этой функции:
Листинг 8.6.
1) int * GetEvens(int * pAr, int iSize, int *
pEvArSize)
2) {
3) int
iEvCount = 0;
4) for(int i = 0; i <
iSize; ++i)
5) if(pAr[i] % 2 == 0)
6) ++iEvCount;
7)
8) int * pEvAr = new int[iEvCount], iInd = 0;
9) for(int i = 0; i <
iSize; ++i)
10) if(pAr[i] % 2 == 0)
11) pEvAr[iInd++] = pAr[i];
12)
13) *pEvArSize =
iEvCount;
14) return pEvAr;
15) }
Итак, вначале нужно подсчитать,
сколько в массиве четных. В 3-й строке объявляем счетчик iEvCount и в цикле (4-5-6-я строчки) считаем их. Зная теперь,
какого размера нам нужен массив, создаем его -
pEvAr – строка 8.
Здесь же объявляем переменную iInd, с помощью которой запишем в новый массив все четные числа
так, чтобы они лежали вплотную, без просветов. В исходном массиве между четными
есть еще и нечетные. В 9-й строчке кода снова бежим по массиву, в 10-й
спрашивам у каждого элемента исходного массива, четный он или нет, и если да, в
11-й строке записываем его в новый массив, одновременно смещая
(увеличивая) индекс iInd, по которому в новый массив и ведется запись. Конечно можно
было обнулить переменную iEvCount
и использовать ее еще раз в качестве индекса, но так более понятно назначение каждой
переменной. Все, массив готов. В 13-й строке мы разименовываем адрес
переменной, в которой должен лежать размер нового массива, и записываем в нее
это число. (Более подробно эта тема «Передача в функцию параметров по адресу и
по значению» будет рассмотрена далее в этой главе.) И, наконец, в 14-й
строке возвращаем из функции адрес полученного массива. Это означает, что в main-е
мы не только должны вызвать эту функцию, но и принять
адрес массива а также предоставить переменную, чей адрес полетит в функцию
(чтоб в нее там записали размер полученного массива).
Внизу приведен весь код программы, в main-е пронумерованы только те строчки кода, которые
еще не рассматривались.Нумерация строк будет соответствовать листингу 8.4.
/////////////////////////////////////
// Pointers.cpp : Defines the entry
point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <iomanip>
using namespace std;
void Init(int * pAr, int
iSize);
void Show(int * pAr, int
iSize);
int * GetEvens(int * pAr, int iSize,
int * pEvArSize);
int * GetEvens2(int * pAr, int iSize,
int * pEvArSize);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 7;
int * pA;
cout<<&a<<' '<<&b<<endl;
pA = &a;
*pA += 10;
pA = &b;
*pA += 10;
cout<<a<<' '<<&a<<'
'<<b<<' '<<&b<<' '<<pA<<endl;
const int SIZE = 10;
int iAr[SIZE];
cout<<iAr<<endl;
for(int i = 0; i <
SIZE; ++i)
{
iAr[i] =
rand()%21;
cout<<setw(3)<<i<<setw(3)<<iAr[i]<<setw(3)<<*(iAr
+ i)
<<' '<<&iAr[i]<<' '<<iAr + i<<endl;
}
cout<<endl;
int iCount;
cout<<"Enter how many numbers do you want to store"<<endl;
cin>>iCount;
int * pAr = new int[iCount];
Init(pAr,
iCount);
Show(pAr,
iCount);
28)
29) int iEvenArSize;
30) //int * pEvens = GetEvens2(pAr, iCount, &iEvenArSize);
31) int * pEvens = GetEvens(pAr, iCount,
&iEvenArSize);
32) cout<<"There are "<<iEvenArSize<<" even numbers"<<endl;
33) Show(pEvens,
iEvenArSize);
34)
35) delete [] pAr;
36) delete [] pEvens;
if(_CrtDumpMemoryLeaks())
cout<<"Memory Leaks!!!"<<endl;
else
cout<<"All heep memory was returned"<<endl;
return 0;
}
////////////////////////////////////////////////////////////////////////////////
void Init(int * pAr, int iSize)
{
for(int i = 0; i <
iSize; ++i)
pAr[i] =
rand() % 31;
}
void Show(int * pAr, int iSize)
{
for(int i = 0; i <
iSize; ++i)
cout<<setw(3)<<pAr[i];
cout<<endl;
}
int * GetEvens(int * pAr, int iSize,
int * pEvArSize)
{
int iEvCount = 0;
for(int i = 0; i <
iSize; ++i)
if(pAr[i] % 2 == 0)
++iEvCount;
int * pEvAr = new int[iEvCount], iInd = 0;
for(int i = 0; i <
iSize; ++i)
if(pAr[i] % 2 == 0)
pEvAr[iInd++]
= pAr[i];
*pEvArSize =
iEvCount;
return pEvAr;
}
int * GetEvens2(int * pAr, int iSize,
int * pEvArSize)
{
int * pTemp = new int[iSize];
int iEvCount = 0;
for(int i = 0; i <
iSize; ++i)
if(pAr[i] % 2 == 0)
pTemp[iEvCount++]
= pAr[i];
int * pEvAr = new int[iEvCount];
memcpy(pEvAr,
pTemp, sizeof(int)
* iEvCount);
delete [] pTemp;
*pEvArSize =
iEvCount;
return pEvAr;
}/////////////////////////////////////
Итак, вставьте пож. в main-е перед удалением
массива pAr строчки 29 – 33. В 29-й создаем (объявляем) переменную iEvenArSize, в которую запишется размер массива с четными. В 31-й
объявляем указатель pEvens, в который запишется адрес массива, созданного
внутри функции и вызываем функцию GetEvens, которой передаем по адресу переменную iEvenArSize. Итак, посторю еще раз: В 31-й строке вызывается функция,
которая возвращает адрес нового массива. Для того, чтобы этот адрес сохранить,
мы и подставляем емкость для хранения - указатель pEvens, с которым дальше можно будет работать как с обыкновенным
массивом. Поставьте точку останова на эту строку, и посмотрите в debug-e, что перед
ее выполнением и в указателе, и в переменной iEvenArSize мусор. Но срезу после ее выполнения они оба получают
значения. В 33-й строке показываем
массив, ведь для показа массивов у нас есть функция Show. Ну и наконец,
отработавши с массивами мы в 35-й и 36-й строках возвращаем системе взятую у
нее память.
Обратите внимание на функцию GetEvens2. Она работает быстрее, чем GetEvens, поскольку в ней мы не бежим два раза по исходному массиву,
а делаем все за один заход. Для этого во
временный массив записываем четные числа, а затем копируем его в новый массив,
чей размер уже идеально подогнан под кол-во четных чисел.
//** advanced
Конечно, прототипы функций Show GetEvens должны были быть написаны так:
void Show(const int * pAr, int iSize);
int * GetEvens(const int * pAr, int iSize, int * pEvArSize);
int * GetEvens2(const int * pAr, int iSize, int * pEvArSize);
Хороший стиль написания кода требует, чтоб, если в функцию передается указатель на память, которая не будет там
меняться, то этот указатель нужно передавать с модификатором const. Тем самым мы не только визуально показываем программисту,
который будет читать наш код, что передаваемые данные не будут изменены, но и гарантируем это средствами языка. Но эта тема
будет рассматриваться позже.
**//
Дальше в этой главе будут рассмотрены
такие темы:
Ссылки int & rA = a;
Отличия указателей от ссылок.
Передача в функцию параметров по адресу, ссылке и по значению void Func(int * pA, int & rC, int b);
Как передать в функцию указатель как параметр, чтобы ему там выделить память;
Константные указатели и указатели на константу const int * pAr = &a;
Работа с динамическими массивами (размер которых можно изменять по ходу
выполнения программы);
Двумерные массивы, созданные в куче int ** ppAr = new int*[iSize];
Что такое void *;
Как в массиве типа void * хранить адреса переменных любого типа;
Как в массив типа char записать число;
Также будет отдельная глава по
указателям на функции. bool (*pFn)(int, int);
Функциям с неопределенным количеством параметров, где все и построено на
использовании указателей.
Но над книгой еще много работы,
поэтому, если Вы хотите это все узнать раньше, приходите на мои курсы (В
заголовке указан сайт и телефон). Там я
смогу не только объяснить Вам сложные темы, но и ответить на Ваши
вопросы. Эту часть главы я выложил на
нескольких сайтах для свободного пользования. Но хотелось бы узнать,
пригодилась ли Вам эта информация, понравился ли стиль изложения, есть ли у Вас
какие то пожелания для улучшения книги и вообще Ваша оценка этого фрагмента
главы. Если есть, что сказать, пишите по
адресу bhva_nd@ukr.net с
названием темы book.
#include "stdafx.h"
#include <iostream>
#include <iomanip>
using namespace std;
void Init(int * pAr, int iSize);
void Show(int * pAr, int iSize);
int * GetEvens(int * pAr, int iSize, int * pEvArSize);
int * GetEvens2(int * pAr, int iSize, int * pEvArSize);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 7;
int * pA;
cout<<&a<<' '<<&b<<endl;
pA = &a;
*pA += 10;
pA = &b;
*pA += 10;
cout<<a<<' '<<&a<<' '<<b<<' '<<&b<<' '<<pA<<endl;
const int SIZE = 10;
int iAr[SIZE];
cout<<iAr<<endl;
for(int i = 0; i < SIZE; ++i)
{
iAr[i] = rand()%21;
cout<<setw(3)<<i<<setw(3)<<iAr[i]<<setw(3)<<*(iAr + i)
<<' '<<&iAr[i]<<' '<<iAr + i<<endl;
}
cout<<endl;
int iCount;
cout<<"Enter how many numbers do you want to store"<<endl;
cin>>iCount;
int * pAr = new int[iCount];
Init(pAr, iCount);
Show(pAr, iCount);
28)
29) int iEvenArSize;
30) //int * pEvens = GetEvens2(pAr, iCount, &iEvenArSize);
31) int * pEvens = GetEvens(pAr, iCount, &iEvenArSize);
32) cout<<"There are "<<iEvenArSize<<" even numbers"<<endl;
33) Show(pEvens, iEvenArSize);
34)
35) delete [] pAr;
36) delete [] pEvens;
if(_CrtDumpMemoryLeaks())
cout<<"Memory Leaks!!!"<<endl;
else
cout<<"All heep memory was returned"<<endl;
return 0;
}
////////////////////////////////////////////////////////////////////////////////
void Init(int * pAr, int iSize)
{
for(int i = 0; i < iSize; ++i)
pAr[i] = rand() % 31;
}
void Show(int * pAr, int iSize)
{
for(int i = 0; i < iSize; ++i)
cout<<setw(3)<<pAr[i];
cout<<endl;
}
int * GetEvens(int * pAr, int iSize, int * pEvArSize)
{
int iEvCount = 0;
for(int i = 0; i < iSize; ++i)
if(pAr[i] % 2 == 0)
++iEvCount;
int * pEvAr = new int[iEvCount], iInd = 0;
for(int i = 0; i < iSize; ++i)
if(pAr[i] % 2 == 0)
pEvAr[iInd++] = pAr[i];
*pEvArSize = iEvCount;
return pEvAr;
}
int * GetEvens2(int * pAr, int iSize, int * pEvArSize)
{
int * pTemp = new int[iSize];
int iEvCount = 0;
for(int i = 0; i < iSize; ++i)
if(pAr[i] % 2 == 0)
pTemp[iEvCount++] = pAr[i];
int * pEvAr = new int[iEvCount];
memcpy(pEvAr, pTemp, sizeof(int) * iEvCount);
delete [] pTemp;
*pEvArSize = iEvCount;
return pEvAr;
}
/////////////////////////////////////




