Информация о курсе в систематизированном виде излагаются основные понятия и описываются возможности языка C++. При этом основное внимание уделяется объяснению того, как теми или иными возможностями пользоваться



бет6/15
Дата16.07.2016
өлшемі1.19 Mb.
#204086
түріИнформация
1   2   3   4   5   6   7   8   9   ...   15

Рис. 8.2.  Адресная арифметика.

Размер памяти, выделяемой для числа типа long и для char, различен. Поэтому адрес при увеличении xPtr и cp тоже изменяется по-разному. Однако и в том, и в другом случае увеличение указателя на единицу означает переход к следующей в памяти величине того же типа. Прибавление или вычитание любого целого числа работает по тому же принципу, что и увеличение на единицу. Указатель сдвигается вперед (при прибавлении положительного числа) или назад (при вычитании положительного числа) на соответствующее количество объектов того типа, на который показывает указатель. Вообще говоря, неважно, объекты какого типа на самом деле находятся в памяти — адрес просто увеличивается или уменьшается на необходимую величину. На самом деле значение указателя   ptr всегда изменяется на число, равное sizeof(*ptr).

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

8.7 Связь между массивами и указателями

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

long array[100];

long sum = 0;

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

sum += array[i];

То же самое можно сделать с помощью указателей:



long array[100];

long sum = 0;

for (long* ptr = &array[0];

ptr < &array[99] + 1; ptr++)

sum += *ptr;

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



for (long* ptr = array;

ptr <

sum += *ptr;

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

При использовании многомерных массивов указатели позволяют обращаться к срезам или подмассивам. Если мы объявим трехмерный массив exmpl:

long exmpl[5][6][7]

то выражение вида exmpl[1][1][2] – это целое число, exmpl[1][1] – вектор целых чисел (адрес первого элемента вектора, т.е. имеет тип *long), exmpl[1] – двухмерная матрица или указатель на вектор (тип (*long)[7]). Таким образом, задавая не все индексы массива, мы получаем указатели на массивы меньшей размерности.

8.8 Бестиповый указатель

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



void* ptr;

Для указателя на тип void не определена операция ->, не определена операция обращения по адресу   *, не определена адресная арифметика. Использование бестиповых указателей ограничено работой с памятью при использовании ряда системных функций, передачей адресов в функции, написанные на языках программирования более низкого уровня, например на ассемблере.

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

void

printbytes(void* ptr, int nbytes)

{

if (nbytes == 1) {

char* cptr = (char*)ptr;

cout << *cptr;

} else if (nbytes == 2) {

short* sptr = (short*)ptr;

cout << *sptr;

} else if (nbytes == 4) {

long* lptr = (long*)ptr;

cout << *lptr;

} else {

cout << "Неверное значение аргумента";

}

}

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

8.9 Нулевой указатель

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



long* foo(void);

. . .

long* resPtr;

if ((resPtr = foo()) != 0) {

// использовать результат

} else {

// ошибка

}

В языке Си++ определена символическая константа   NULL для обозначения нулевого значения указателя.

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

8.10 Строки и литералы

Для того чтобы работать с текстом, в языке Си++ не существует особого встроенного типа данных. Текст представляется в виде последовательности знаков (байтов), заканчивающихся нулевым байтом. Иногда такое представление называют Си-строки, поскольку оно появилось в языке Си. Кроме того, в Си++ можно создать классы для более удобной работы с текстами (готовые классы для представления строк имеются в стандартной библиотеке шаблонов).

Строки представляются в виде массива байтов:



char string[20];

string[0] = 'H';

string[1] = 'e';

string[2] = 'l';

string[3] = 'l';

string[4] = 'o';

string[5] = 0;

В массиве   string записана строка "Hello". При этом мы использовали только 6 из 20 элементов массива.

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

"Это строка"

"0123456789"

"*"

Заметим, что символ, заключенный в двойные кавычки, отличается от символа, заключенного в апострофы. Литерал "*" обозначает два байта: первый байт содержит символ звездочки, второй байт содержит ноль. Константа '*' обозначает один байт, содержащий знак звездочки.

С помощью литералов можно инициализировать массивы:

char alldigits[] = "0123456789";

Размер массива явно не задан, он определяется исходя из размера инициализирующего его литерала, в данном случае 11 (10 символов плюс нулевой байт).

При работе со строками особенно часто используется связь между массивами и указателями. Значение литерала – это массив неизменяемых байтов нужного размера. Строковый литерал может быть присвоен указателю на char:

const char* message = "Сообщение программы";

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



void

CopyString(char* src, char* dst)

{

while (*dst++ = *src++)

;

*dst = 0;

}

int

main()

{

char first[] = "Первая строка";

char second[100];

CopyString(first, second);

return 1;

}

Указатель на байт (тип char*) указывает на начало строки. Предположим, нам нужно подсчитать количество цифр в строке, на которую показывает указатель str:



#include

int count = 0;

while (*str != 0) {

// признак конца строки – ноль

if (isdigit(*str++))

// проверить байт, на который

count++;

// указывает str, и сдвинуть

// указатель на следующий байт

}

При выходе из цикла while переменная count содержит количество цифр в строке str, а сам указатель str указывает на конец строки – нулевой байт. Чтобы проверить, является ли текущий символ цифрой, используется функция isdigit. Это одна из многих стандартных функций языка, предназначенных для работы с символами и строками.

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

char* strcpy(char* target,

const char* source);

Копировать строку   source по адресу   target, включая завершающий нулевой байт. Функция предполагает, что памяти, выделенной по адресу target, достаточно для копируемой строки. В качестве результата функция возвращает адрес первой строки.



char* strcat(char* target,

const char* source);

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



int strcmp(const char* string1,

const char* string2);

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



size_t strlen(const char* string);

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

В следующем примере, использующем приведенные функции, в массиве   result будет образована строка "1 января 1998 года, 12 часов":

char result[100];

char* date = "1 января 1998 года";

char* time = "12 часов";

strcpy(result, date);

strcat(result, ", ");

strcat(result, time);

Как видно из этого примера, литералы можно непосредственно использовать в выражениях.

Определить массив строк можно с помощью следующего объявления:

char* StrArray[5] =

{ "one", "two", "three", "four", "five" };
9 Распределение памяти

9.1 Автоматические переменные

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

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



int

funct()

{

double f; // значение f не определено

f = 1.2;

// теперь значение f определено

// явная инициализация автоматической

// переменной

bool result = true;

. . .

}

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



Замечание. Распространенной ошибкой является использование адреса   автоматической переменной после выхода из функции. Конструкция типа:

int*

func()

{

int x;

. . .

return &х;

}

дает непредсказуемый результат.

9.2 Статические переменные

Другой способ выделения памяти – статический

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

double globalMax;

// переменная определена вне функции

void

func(int x)

{

static bool visited = false;

if (!visited) {

. . . // инициализация

visited = true;

}

. . .

}

В данном примере переменная visited создается в начале выполнения программы. Ее начальное значение – false. При первом вызове функции func условие в операторе if будет истинным, выполнится инициализация, и переменной visited будет присвоено значение true. Поскольку статическая переменная создается только один раз, ее значения между вызовами функции сохраняются. При втором и последующих вызовах функции func инициализация производиться не будет.

Если бы переменная visited не была объявлена static, то инициализация происходила бы при каждом вызове функции.

9.3 Динамическое выделение памяти

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

long* lp;

lp = new long;

Complex* cp;

cp = new Complex;

// создать новое целое число

// создать новый объект типа Complex

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



delete lp;

delete cp;

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

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

new int[100];

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

Освобождение памяти, выделенной под массив, должно быть выполнено с помощью следующей операции delete

delete [] address;

9.4 Выделение памяти под строки

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

// стандартная функция strlen подсчитывает

// количество символов в строке

int length = strlen(src_str);

// выделить память и добавить один байт

// для завершающего нулевого байта

char* buffer = new char[length + 1];

strcpy(buffer, src_str);

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

Операция new возвращает адрес выделенной памяти. Однако нет никаких гарантий, что new обязательно завершится успешно. Объем оперативной памяти ограничен, и может случиться так, что найти еще один участок свободной памяти будет невозможно. В таком случае new возвращает нулевой указатель (адрес 0). Результат new необходимо проверять:



char* newstr;

newstr = new char[length];

if (newstr == NULL) { // проверить результат

// обработка ошибок

}

// память выделена успешно

9.5 Рекомендации по использованию указателей и динамического распределения памяти

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

Приведем несколько примеров.

Использование неверного адреса в операции delete. Результат такой операции непредсказуем. Вполне возможно, что сама операция пройдет успешно, однако внутренняя структура памяти будет испорчена, что приведет либо к ошибке в следующей операции new, либо к порче какой-нибудь информации.

Пропущенное освобождение памяти, т.е. программа многократно выделяет память под данные, но "забывает" ее освобождать. Такие ошибки называют утечками памяти. Во-первых, программа использует ненужную ей память, тем самым понижая производительность. Кроме того, вполне возможно, что в 99 случаях из 100 программа будет успешно выполнена. Однако если потеря памяти окажется слишком большой, программе не хватит памяти под какие-нибудь данные и, соответственно, произойдет сбой.

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

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

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


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

  2. Старайтесь локализовать распределение памяти. Если какой-либо метод выделяет память (в особенности под временные данные), он же и должен ее освободить.

  3. Там, где это возможно, вместо указателей используйте ссылки.

  4. Проверяйте программы с помощью специальных средств контроля памяти (Purify компании Rational, Bounce Checker компании Nu-Mega и т.д.)

9.6 Ссылки

Ссылка – это еще одно имя переменной. Если имеется какая-либо переменная, например



Complex x;

то можно определить ссылку на переменную x как



Complex& y = x;

и тогда x и y обозначают одну и ту же величину. Если выполнены операторы



x.real = 1;

x.imaginary = 2;

то y.real равно 1 и y.imaginary равно 2. Фактически, ссылка – это адрес переменной (поэтому при определении ссылки используется символ & -- знак операции взятия адреса), и в этом смысле она сходна с указателем, однако у ссылок есть свои особенности.

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

int& xref;

можно только



int& xref = x;

Во-вторых, нельзя переопределить ссылку, т.е. изменить на какой объект она ссылается. Если после определения ссылки xref мы выполним присваивание



xref = y;

то выполнится присваивание значения переменной y той переменной, на которую ссылается xref. Ссылка   xref по-прежнему будет ссылаться на x. В результате выполнения следующего фрагмента программы:



int x = 10;

int y = 20;

int& xref = x;

xref = y;

x += 2;

cout << "x = " << x << endl;

cout << "y = " << y << endl;

cout << "xref = " << xref << endl;

будет выведено:

x = 22


y = 20

xref = 22

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

Complex a;

Complex* aptr = &a;

Complex& aref = a;

aptr->real = 1;

aref.imaginary = 2;

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

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

Рассказывая о функциях, мы отметили, что у функций (как и у методов классов) есть аргументы, фактические значения которых передаются при вызове функции.

Рассмотрим более подробно метод Add класса Complex. Изменим его немного, так, чтобы он вместо изменения состояния объекта возвращал результат операции сложения:

Complex

Complex::Add(Complex x)

{

Complex result;

result.real = real + x.real;

result.imaginary = imaginary + x.imaginary;

return result;

}

При вызове этого метода



Complex n1;


Достарыңызбен бөлісу:
1   2   3   4   5   6   7   8   9   ...   15




©dereksiz.org 2024
әкімшілігінің қараңыз

    Басты бет