Nickolay.info. Обучение. Лекции по Си. Глава 7

7. Указатели

 

Указателем называют переменную, содержащую адрес другой переменной.

Таким образом, именно указатели дают возможность косвенного доступа программы к объектам в памяти. Предположим, что х - переменная, например, типа int, а рх - указатель. Они описываются следующим образом:

int x; int *px;

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

Унарная операция * ("взятие значения") рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если y также имеет тип int, то операция

y = *рх;

присваивает y содержимое того объекта, на который указывает рх. Так последовательность операций

рх = &х;

y = *рх;

присваивает y то же самое значение, что и оператор

y = x;

Указателю можно присваивать адрес объекта и непосредственно при описании:

int x;

int *px=&x;

Унарная операция & ("взятие адреса") уже упоминалась нами в п. 5.5. Здесь указатель px содержит адрес переменной x, ему присвоен ее адрес.

Над указателями определены операции сложения и вычитания с числом:

px++;

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

px-=2;

Здесь операция px+=i увеличивает px так, чтобы он указывал на элемент, отстоящий на i элементов от текущего.

Сравнение указателей в общем случае некорректно! Это связано с тем, что одним и тем же физическим адресам памяти могут соответствовать различные пары значений "сегмент‑смещение".

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

int *py=px;

или

int *py; py=px;

Теперь py указывает на то же, что px.

Указатели px и py адресуют одну и ту же переменную x, но сравнение px==py может быть некорректным в отличие от сравнения значений *px==*py.

Унарная операция & выдает адрес объекта, так что оператор

рх = &х;

присваивает адрес х переменной рх; говорят, что теперь рх указывает на х. Операция & применима только к переменным и элементам массива, конструкции вида &(х-1) и &3 являются незаконными. Нельзя также получить адрес регистровой переменной.

Указатели могут входить в выражения. Например, если px указывает на целое x, то *px может появляться в любом контексте, где может встретиться x. Так, оператор

y = *px + 1;

присваивает y значение, на 1 большее значения x (получаем значение из указателя, затем прибавляем 1). Оператор printf ("\n%d", *px); печатает текущее значение x, а оператор d = sqrt((double)*px); получает в d квадратный корень из x, причем до передачи функции sqrt значение x преобразуется к типу double.

В выражениях вида

y = *px + 1;

унарные операции * и & связаны со своим операндом более крепко, чем арифметические операции (см. табл. 3.3), так что это выражение берет значение, на которое указывает px, прибавляет 1 и присваивает результат переменной y:

y = (*px) + 1;

Выражение

y = *(px + 1);

имеет совершенно иной смысл: записать в y значение, взятое из ячейки памяти, следующей за той, на которую указывает px. Адрес, на который указывает px, при этом не изменится.

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

*px = 0;

записывает в x значение 0, а

*px += 1;

увеличивает значение x на единицу, как и выражение

(*px)++;

Круглые скобки в последнем примере необходимы; если их опустить, то поскольку унарные операции, подобные * и ++, выполняются справа налево, это выражение увеличит px, а не ту переменную, на которую указывает px.

Наконец, операция

*px++;

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

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

 

7.1 Указатели и аргументы функций.

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

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

Пример ниже иллюстрирует применение указателей в качестве аргументов функции - иными словами, передачу параметров по адресу и прием по значению.

void swap (int *a, int *b) {

int c=*a; *a=*b; *b=c;

}

int a,b,c;

swap (&a,&b);

int *p=&c;

swap (&a, p);

Сравните этот подход с ранее применявшимися передачей по значению и приемом по адресу, использующими операцию &:

void swap (int &a, int &b) {

int c=a; a=b; b=c;

}

int a,b;

swap (a,b);

 

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

Как правило, указатель используется для последовательного доступа к элементам статического или динамического массива. Так, конструкция

int a[]={1,2,3};

int *p=a;

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

printf ("\t%d",*p++);

последовательно распечатает элементы массива a, доступ к которым осуществлялся через указатель p.

Присваивание указателю адреса нулевого элемента массива можно было записать и в виде

int *p=&a[0];

или

int *p=&(*a+0);

или

int *p=&*a;

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

int strlen(char *s){

int n;

for (n = 0; *s != '\0'; s++) n++;

return(n);

}

char *s="Test";

int len=strlen (s);

Здесь с указателем на тип char сопоставляется статическая строка символов, подробнее этот прием будет раскрыт в п. 7.3.

Тело функции strlen можно было записать и короче:

int n=0;

while (*s++) n++;

return n;

Существуют важные различия между массивами и указателями.

·       Указатель занимает одну ячейку памяти, предназначенную для хранения машинного адреса (в частности, адреса нулевого элемента массива). Массив занимает столько ячеек памяти, сколько элементов определено в нем при его объявлении. Только в выражении массив представляется своим адресом, который эквивалентен указателю.

·       Адрес массива является постоянной величиной, поэтому, в отличие от идентификатора указателя, идентификатор массива не может составлять левую часть операции присваивания.

Для одномерного массива следующие 2 выражения эквивалентны, если а - массив или указатель, а b - целое:

а[b]    *(а + b)

Аналогично, для матрицы a с целочисленными индексами i и j эквивалентны выражения

a[i][j]   *(*(a+i)+j)

Так, следующий оператор выводит элемент матрицы a[1][2]:

int i=1,j=2;

printf ("%d",*(*(a+i)+j));

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

int i; void *p;

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

p = &i;

но изменить значение указателя без приведения типа нельзя:

р++;  /* недопустимо */

(int *)р++; /* допустимо */

В стандартном включаемом файле stdio.h определена константа с именем NULL. Она предназначена специально для инициализации указателей. Гарантируется, что никакой программный объект никогда не будет иметь адрес NULL.

 

7.3. Указатели и символьные данные.

Если описать указатель message в виде

char *message;

то в результате оператора

message = "Any string of text";

message будет указывать на фактический массив символов. Это не копирование строки, так как в операции участвует только указатель. Также важно то, что в Си не предусмотрены какие-либо операции для обработки всей строки символов как целого. Как и в других контекстах, присваивание значения переменной можно объединить с ее определением:

char *message = "Any string of text";

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

Следующий пример реализует на Си функцию копирования строки с именем strcpy.

#include <stdio.h>

char *strcpy (char *s, char *t) {

char *n=s; //запомнили, куда показывал s

while (*t!='\0') { *s++=*t++; }

return n; //s сдвинулся, вернули

           //его начальное значение

}

void main () {

char *s1="none",*s2="test";

printf ("\n%s",strcpy(s1,s2));

}

Функция получает 2 указателя на строки s (строка назначения) и t (строкаисточник). Значением *t++ является символ, на который указывал t до увеличения; постфиксная операция ++ не изменяет t, пока этот символ не будет извлечен. Точно так же этот символ помещается в старую позицию s, до того как s будет увеличено. Конечный результат заключается в том, что все символы копируются из t в s, исключая завершающий символ нуля.

Более кратко процесс копирования можно было бы описать в виде

while ((*s++ = *t++) != '\0');

Здесь увеличение s и t вынесено в проверочную часть цикла. Также за счет того, что сначала выполняется присваивание, а затем  сравнение с нулевым байтом, код копирует и завершающий символ '\0'. Наконец, сравнение с нулем также можно было опустить:

while (*s++ = *t++);

Напишем функцию сравнения строк с использованием указателей. Она вернет число меньше 0, если строка s лексикографически (по кодам символов) предшествует t, вернет 0, если строки одинаковы и положительное значение, если s "больше" t по кодам символов.

int strcmp(char *s, char *t) {

for ( ; *s == *t; s++, t++)

  if (*s == '\0') return(0);

return(*s-*t);

}

Запись этой функции также можно сократить.

 

7.4. Указатели и динамическая память.

Подробнее тема использования динамической памяти рассматривается в п. 8.5. Здесь мы ограничимся двумя стандартными функциями, имеющимися в библиотеке stdlib.h:

·              функция void *malloc(n) с целочисленным беззнаковым аргументом n возвращает в качестве своего значения нетипизированный указатель p, который указывает на первый из n выделенных байт памяти. Эта память может быть использована программой для хранения данных; перед использованием указатель должен быть типизирован операцией приведения типа. Если выделить память не удалось, функция возвращает NULL. Операция приведения типа имеет вид sizeof(тип) и позволяет узнать размер переменной этого типа в байтах.

·              функция void free(p) освобождает приобретенную таким образом память, так что ее в дальнейшем можно снова использовать. Обращения к free должны производиться в порядке, обратном тому, в котором производились обращения к malloc.

В приведенном далее примере указателю p сопоставляется динамическая память под строку из n символов, значение n вводится пользователем.

#include <stdlib.h>

#include <stdio.h>

//...

unsigned char *p;

unsigned n;

printf ("\nN="); fflush (stdin);

scanf ("%u",&n);

p=(unsigned char *)

malloc(n*sizeof(unsigned char));

if (p==NULL) {

//Здесь производится диагностика ошибки

}

В следующем примере функция strsave копирует свою строку‑аргумент в динамически выделенную область памяти.

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

 /*прототипы строковых функций*/

char *strsave(char *s) {

char *p=NULL;

p=(char *) malloc(strlen(s)+1);

if (p != NULL) strcpy(p, s);

return(p);

}

void main () {

char *s1="hello", *s2;

s2=strsave(s1);

printf ("\n%s",s2);

}

 

7.5. Указатели и функции с переменным числом аргументов.

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

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

В списке типов аргументов в качестве имени типа допускается также конструкция void *, которая специфицирует аргумент типа "указатель на любой тип". Для доступа к переменному списку параметров можно использовать указатели.

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

#define KEY unsigned int

#define MAX_BUF 9

static KEY Buf [MAX_BUF];

KEY Start = 0;

 

void Set_Key( KEY kol, ... ) {

//Параметр kol задает

//количество остальных параметров

 KEY *ptr;   //Указываем на первый символ

 ptr = &kol; //в строке параметров

 for(; kol != 0; kol-- ) {

  Buf [Start++] = *++ptr;  

  //Последовательно записываем символы

  Start %= MAX_BUF;        

  //во внутренний буфер с контролем

  //его переполнения

 }

 return;

}

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

#define ENTER   0x000D

#define END     0x4F00

#define RIGHT   0x4d00

Тогда функция может быть вызвана, например, так:

Set_Key (1,ENTER);

Set_Key (2,END,RIGHT);

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

#define MODE unsigned char

KEY Get_Key (void) {

 KEY sim;

 MODE scan,ascii;

 if (Start != End) {

  sim = Buf [End++];  End %= MAX_BUF;

 }

 else  {

  asm MOV AH,0x00;

  asm INT 16H;

  sim=_AX; //или вызов bioskey (0);

  scan=(MODE)((sim&0xff00)>>8);  //_AH

  ascii=(MODE)(sim&0x00ff);      //_AL

  if (ascii) scan=0;

  sim=(scan<<8)+ascii;

 } 

 return (sim);

}

Здесь инструкция asm позволяет выполнить ассемблерный код непосредственно из программы на Си. Инструкция разрешает выполнять и блок ассемблерных команд: asm { }. Существует также библиотека stdarg.h для работы с переменными списками аргументов.

 

7.6. Указатели и прямой доступ к памяти.

Организацию прямого доступа к памяти с помощью указателей рассмотрим на примере обращения к памяти видеоадаптера в текстовом режиме монитора с разрешением экрана 80*25 позиций. Как известно, видеопамять при этом начинается с адреса B800:0000 и состоит из пар байт "символ‑атрибут", описывающих экранные позиции слева направо и затем сверху вниз.

static unsigned char far *s =

 (unsigned char far *) 0xB8000000UL;

void putc (int x, int y, char c) {

 *(s+y*160+x*2)=c;

}

void main () {

 putc (0,0,'*'); putc (79,0,'*');

 putc (0,24,'*'); putc (79,24,'*');

 //вывели звездочки по краям

 //текстового экрана

}

Модификатор far определяет "длинный" 4-байтовый указатель (см. п. 11).

Поскольку одна строка экрана консоли состоит из 80 символов и требует 160 байт памяти, конструкция *(s+y*160+x*2), где x - экранный столбец, а y - строка, адресует на экране позицию в y‑строке и x‑столбце.

Учитывая, что операции сдвига порождают более быстрый код, чем умножение, а 160=128+32=27+25, в функции putc лучше использовать присваивание вида

*(s+ (y<<7) + (y<<5) + (x<<1)) = c;

 

 

Рейтинг@Mail.ru
вверх гостевая; E-mail