Создание нового проекта консольного типа, страница 4

Указатели first и last содержат адреса начала и конца массива вещественных чисел длиной в size элементов. Массив заполняется в цикле for. Сумма его элементов, вычисленная функцией sum, возвращается в точку вызова и непосредственно выводится функцией printf. Прототип функции sum дан заранее, так как сама функция следует за главной процедурой. Указатели start и end, действующие в функции sum, получают значения указателей first и last из функции main, с помощью аппарата передачи формальных и фактических параметров. Использование указателей позволяет последовательно выбрать все элементы массива и накопить их сумму в переменной sum. Рассмотрим, как работает конструкция *start++. Сначала выполняется разадресация указателя start, так как использована постфиксная модификация операции приращения При этом содержимое ячейки памяти, на которую в данный момент ссылается start, выбирается и используется в выражении. Затем происходит сдвиг указателя на один элемент вперед. Он теперь ссылается на следующий элемент массива. Выход из цикла накопления суммы произойдет, когда указатель start выйдет за верхнюю границу массива, на которую ссылается указатель end. Переменная sum, объявленная внутри функции sum(), не вступает в конфликт с одноименной функцией, так как синтаксис (наличие скобок у функции) различен. Область действия локальной переменной sum не распространяется за пределы функции, в то время как функция sum() известна глобально. Обратите внимание на то, как работает адресная арифметика при вычислении адреса last. С целью иллюстрации этого положения адреса начала и конца массива выводятся в main на экран. Различные запуски программы в многозадачной операционной среде могут давать разные значения адресов, что объясняется динамическим способом выделением памяти. Windows NT на Pentium 300 справляется с этой задачей, даже если задавать количество членов ряда равным десяти миллионам. Система при этом осуществляет интенсивный swapping и просит выгрузить ненужные приложения, но примерно через минуту выдает ответ.

Распространенные ошибки

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

double *p1, *p2;      *p1=12.56e7;   *p2=8.85e12;

Локальная (внутри функции) декларация double *p1, *p2; заставляет компилятор отвести память для двух указателей p1, p2, но не для двух переменных типа double. Так как сами указатели в этом фрагменте не инициализированы, то присвоения будут осуществлены по каким-то непредусмотренным адресам, на которые случайно показывают р1 и р2 в данный момент, а это приведет к затиранию хранимой там информации. Инициализация указателей производится различными способами. Один из них это динамическое выделение памяти из области heap. Перепишем предыдущий фрагмент:

double *p1,*p2;

p1 = new double;    p2 = new double;

*p1=12.56e7;  *p2=8.85e12;

Здесь на этапе выполнения с помощью операции new будет выделена память для двух переменных типа double и адреса этих новых участков памяти будут присвоены указателям. Числовые значения будут присвоены по законным адресам. Другим распространенным типом ошибок является dangling (подвешивание или потеря адреса памяти) и aliasing (псевдонимизация адреса). В фрагменте:

int *p = new int, *q = new int;

*p = 255;   *q = 128;

p = q;      *p = 64;

присвоение p=q означает, что р, начиная с этого момента, указывает на ту же область памяти, что и q (псевдонимизация). После этого значение *p==255 потеряно навсегда. Не существует способа доступа к этой области памяти. Она остается захваченной (подвешенной) до конца выполнения программы. Кроме того, присвоение *p=64 в качестве побочного эффекта изменяет и содержимое по адресу q, что, возможно, не входило в намерения программиста.

Константные указатели и указатели на константу

Продолжая рассмотрение особенностей использования указателей, отметим, что описатель const в языке C++ может заморозить как значение по адресу, так и сам адрес. В первом случае описание выглядит так:

const char *title = "Diagram";     // Указатель на константу

Изменить константное значение "Diagram" по адресу title нельзя, так как title — указатель на константу. Например, следующая попытка приведет к ошибке на стадии компиляции: title[0]='d';. В то же время значение указателя title может быть изменено, то есть его можно заставить указывать на другую строку символов, например, простым присвоением title="Function W(p)";. Если нужно, чтобы указатель ссылался на один и только один адрес памяти, то следует объявить не указатель на константу, а константный указатель на тип. Описание должно выглядеть следующим образом:

char *const title="Diagram";   // Константный указатель

Теперь присвоение title[0]='d'; законно, а title="Function W(p)"; ошибочно. Если нужно заморозить как адрес, так и значение по адресу, то следует использовать конструкцию:

const char *const title = "Diagram";

Обычному указателю (p) нельзя присвоить значение указателя на константу (pc) и это сначала кажется нелогичным, так как простой указатель по идее может указывать на что угодно. Но это запрещено, так как в противном случае появилась бы возможность изменить значение константы косвенным путем: сначала присвоить адрес константы простому указателю (p=pc), потом изменить содержимое по этому адресу (*p). Необходимо помнить, что с помощью указателя можно косвенно (indirectly) попытаться изменить значение константы. Например, если она объявлена: const size=16535;, то следующее присвоение проходит без сообщения об ошибке: *(int*)&size=1;. Выражение может быть прочитано таким образом. Сначала берется адрес & константной переменной size, затем он преобразуется к типу (int*), потом содержимому по этому адресу производится присвоение единицы. Любопытно просмотреть этот фрагмент в режиме отладки. В окне Variables вы увидите, что содержимое по адресу &size изменилось на единицу, но при попытке обращения к переменной ее значение остается старым. Если записать проще: *(&size)=1;, то компилятор видит попытку изменить переменную с описателем const и выдает сообщение об ошибке.