Академический Документы
Профессиональный Документы
Культура Документы
7.1. Указатели
Указатель представляет собой переменную, значением которой является адрес
памяти. Значение указателя "показывает" (отсюда и термин – указатель), по какому
адресу памяти размещено некоторое данное, а из описания указателя известен тип
этого данного.
7.1.1. Описание указателей
Указатели описываются операторами вида:
<тип> *<объект1>,*<объект2>,...,*<объектN>;
где
<тип> – это любой, допустимый в Си тип данных; * звездочка говорит о том,
что следующий за ней <объект> является объектом, указывающим на данное
1
<тип>; <объект> в простейшем случае это <идентификатор> и тогда он
представляет собой скалярную переменную, являющуюся указателем.
Более сложным объектом может быть <идентификатор> [<кол-во>]...[<кол-
во>], и тогда идентификатор представляет собой имя n-мерного массива
указателей, содержащего <кол-во> элементов по n-му измерению. Если в качестве
объекта в оператор описания подставить конструкцию вида *<объект>, то тогда
объект является указателем на указатель данного <тип>. Если вправо от
идентификатора объекта записаны парные пустые круглые скобки, то такой
идентификатор является указателем на функцию. При описании указателей можно
использовать круглые скобки для задания нужной интерпретации. Правила
интерпретации сложных описаний будут рассмотрены чуть ниже, пока же
исследуем простые примеры описаний,
int *p1; p1 – указатель на данные целого типа.
float *p2; p2 – указатель на данные с плавающей точкой.
char *p3; p3 – указатель на символьные данные.
int *p4[3]; p4 – массив указателей на данные целого типа, каждый из
элементов которого p4[0], р4[1], p4[2] является
указателем на целое.
void *p5; p5 – указатель на данные, тип которых заранее не
определен.
char **p6 p6 – указатель на указатель данных типа сhar.
2
1. Начать с идентификатора и посмотреть вправо, есть ли квадратные или
круглые скобки.
2. Если они есть, то проинтерпретировать эту часть описания и затем
посмотреть налево в поиске звездочки.
3. Если на любой стадии справа встретится закрывающая круглая скобка, то
вначале необходимо применить все эти правила внутри круглых скобок, а
затем продолжить интерпретацию.
4. Интерпретировать описатель типа.
Применим это правило для интерпретации последнего из вышеприведенных
примеров:
3
3. Medium (средняя). Под код программы отводится 1Мгб памяти. Это
обозначает, что все обращения к функциям и возвраты из функций,
осуществляемые через указатель, должны использовать указатель длиной в четыре
байта, чтобы в нем мог разместиться далекий адрес памяти (далекий указатель).
Под стек, кучу и статические данные выделяется 64К памяти и, следовательно, для
адресации данных используются близкие указатели.
4. Compact (компактная). Под код программы отводится 64К. Под данные
отводится всего 1Мгб, но объем статических данных ограничивается размером
64К. Под стек выделяется 64К. Адресация внутри программы осуществляется
близкими указателями, адресация – данных далекими указателями.
5. Large (большая). Под код программы отводится 1Мгб, под статические
данные – 64К, куча может занимать до 1Мгб памяти. Как программа, так и данные
адресуются далекими указателями.
6. Huge (огромная). Аналогична большой модели, но объем статических
данных может превышать 64К. Для любой адресации необходимы далекие
указатели.
В зависимости от соотношения объемов код–данные, выбирается модель
памяти для компиляции вашей программы. Нередки случаи, когда при
неправильном выборе модели памяти программа работает некорректно, хотя на
первый взгляд – все в порядке. В некоторых случаях возникает необходимость
задать размер указателя, отличный от размера, устанавливаемого при компиляции
в той или иной модели памяти. Это можно сделать при помощи модификаторов
описания указателей near, far и huge:
модификатор near устанавливает 16-тибитный, т.е. близкий указатель. При
использовании near-указателей данные программы ограничены размером
сегмента 64К;
модификатор far устанавливает 32-ухбитный, т.е. далекий указатель. При
использовании far-указателей можно ссылаться на данные в пределах 1
мегабайта адресного пространства;
модификатор huge также устанавливает 32-ухбитный указатель
аналогичный far-указателю, однако между ними есть различия. Во-первых,
операции отношения ==, !=, <, >, <= и >= работают правильно с huge-
указателями, а с far-указателями они работают неправильно. Во-вторых,
все арифметические операции над указателями типа huge воздействуют как
на адрес сегмента, так и на смещение, а при использовании указателей
типа far воздействуют только на смещение.
Из-за того, что арифметические операции при работе с far-указателями
воздействуют только на смещение, то хотя far-указатель и может адресовать
данные в пределах одного мегабайта, данные, к которым идет обращение, должны
начинаться и заканчиваться в пределах сегмента. Например, все элементы массива,
расположенного в нескольких сегментах, доступны при помощи huge-указателя, а
при помощи far-указателя можно получить доступ только к элементам,
расположенным в пределах данного сегмента, остальные будут недоступны для far-
4
указателя, т.к. арифметические операции изменяют смещение, но оставляют
неизменным адрес сегмента. Попутно заметим, что для near–указателя ограничения
еще более жесткие: все данные должны располагаться в пределах одного сегмента
(для huge u far эти пределы – 1 Мб).
Рассмотрим способы описания указателей с модификаторами near, far и huge.
Когда одно из этих ключевых слов встречается в описании, то оно модифицирует
объект, расположенный справа от ключевого слова, например:
&<переменная>
5
*<адресное_выражение>
6
программе должны быть известны адрес объекта, тип объекта и его значение.
Только в этом случае операция косвенной адресации будет выполнена корректно.
Тип объекта не может быть типом void – пустым типом, но значение объекта
может быть "пустым", т.е. еще не известным, если ему присваивается значение с
использованием левосторонней операции косвенной адресации. Мы уже приводили
пример описания указателя на данные, тип которых заранее не определен: void
*p5; Такой указатель совместим с указателем на любой тип данных, т.е. ему
можно присвоить адрес объекта любого типа, но перед использованием указателя,
объявленного как void, он обязательно должен быть специфицирован типом
объекта, на который он указывает, например:
void *p5;
double r,q;
q=1.85e-12;
p5=&q;
r=*((double *)p5);
7
Первый способ: описать переменные программы. Поскольку компилятор
отводит память под переменную в момент ее описания, то присваивание указателю
адреса переменной гарантирует, что нужная память отведена и там будут
находиться значения переменных, например:
float r, b;
float *ukr=&r, *ukb=&b;
float r, b;
float *ukr, *ukb;
. . . . . . . . .
ukr=&r; ukb=&b:
malloc(<размер>);
Для такого способа инициализации полезно задать себе ряд вопросов, задуматься
над ними и получить аргументированные ответы. Рассуждать будем так: это
8
оператор описания, ибо он начинается с ключевого слова, задающего <тип> . Мы
знаем, что при описании переменных допустима их инициализация только
константными выражениями. Исходя из такой посылки, сформулируем наши
вопросы и дадим на них ответы. Вопрос первый: является ли выражение справа от
знака присваивания константным? Да, является, так как функция возвращает адрес
выделенной памяти и можно считать, что обращение к функции есть именованная
константа-указатель, значение которой устанавливается в момент применения
функции. Вопрос второй: почему в константном выражении использована
операция преобразования типа (int *)? Потому что возвращается константа-
указатель на неопределенный тип void, а мы инициализируем указатель на данные
типа int. Следовательно, нам нужно преобразовать константу-указатель на
неопределенный тип к константе-указателю на целый тип. И, наконец, последний,
третий вопрос: почему <размер> задан операцией sizeof(int)? Ответ: для того,
чтобы обеспечить мобильность программы. В самом деле, если мы запишем этот
оператор в виде int*x=(int*)malloc(2);, то при использовании компьютера
другой архитектуры, где, например, данные типа int представляются
четырехбайтными словам памяти, такая программа может оказаться
неработоспособной, т.к. для четырехбайтного целого резервируется два байта
памяти. С использованием функции malloc, инициализацию описанного
указателя можно осуществлять в нужный момент и в ходе выполнения программы:
int *x;
. . . .
x=(int *)malloc(sizeof(int);
if (x != 0)
{
/* действия программы, если память выделена */
*х=-5244;
/* в выделенную память послали -5244 */
};
else {
/* действия программы, если память не выделена */
};
где <указатель> должен содержать в себе адрес памяти, которая должна быть
освобождена. Чтобы уничтожить в памяти значение -5244, динамически созданное
в области кучи, как показано в предыдущем примере, нужно записать оператор
free(x); .
В языке С++ для освобождения динамически выделенной памяти
используется операция delete:
delete <указатель>;
delete [ ] <указатель>;
Например:
int * p;
int i = 10;
p= new int[i]; //выделить память под массив данных
if(p != NULL) printf("Память выделена");
else printf("Свободной памяти нет");
delete[] p; //освободить память массива данных по адресу р
int *x;
x = new int;
delete x; // освободить память по адресу x
7.1.6. Адресная арифметика
Под адресной арифметикой понимаются действия над указателями, связанные
с использованием адресов памяти. Рассмотрим операции, которые можно
применять к указателям, попутно заметив, что некоторые из них уже
рассматривались, однако здесь мы повторимся, для систематизации изложения
материала.
1. Присваивание. Указателю можно присвоить значение адреса. Любое
число, присвоенное указателю, трактуется как адрес памяти:
int *u,*adr;
int N;
u=&N; // указателю присвоен адрес переменной N */
adr=0х00FD; // указателю присвоен 16-теричный адрес, (не
10
// рекомендуется так делать )
.int *a,b;
a=&b; // указателю a присвоен адрес указателя b
int *uk;
int n;
int m=5;
uk=&m; // uk присвоен адрес переменной m
n=*uk; // переменная n примет значение 5
*uk=-13; // переменная m примет значение -13
int i, *ptr;
i=0x8e41;
ptr=&i;
printf("%d\n", *ptr); //печатается значение int:-29119
printf("%d\n", *((char *)ptr));
// ptr преобразован к типу сhar, извлекается 1 байт и его двоичный код
// печатается в виде десятичного числа65
Преобразование типа указателя чаще всего применяется для приведения
указателя на неопределенный тип данных void к типу объекта, доступ к которому
будет осуществляться через этот указатель.
11
7. Индексация. Указатель может индексироваться применением к нему
операции индексации, обозначаемой в Си квадратными скобками []. Индексация
указателя имеет вид <указатель>[<индекс>], где <индекс> записывается
целочисленным выражением. Операция индексирования является унарной и
применяется справа от операнда, а не слева, как для других унарных операций.
Возвращаемым значением операции индексации является данное, находящееся по
адресу, смещенному в бóльшую или меньшую сторону относительно адреса,
содержащегося в указателе в момент применения операции. Этот адрес
определяется так: (адрес в указателе) + (значение <индекс>) * sizeof(<тип>), где
<тип> – это тип указателя. Из этого адреса извлекается или в этот адрес
посылается, в зависимости от контекста применения операции, данное, тип
которого интерпретируется в соответствии с типом указателя. Рассмотрим
следующий пример:
int *uk1;
int b,k;
uk1=&b; //в uk1 адрес переменной b */
k=3;
b=uk1[k]; // переменной b присваивается значение int, взятое из
// адреса на 6 большего, чем адрес переменной b; в uk1
// адрес не изменился
uk1[k]=-14; // в адрес на 6 больший, чем адрес переменной b,
// посылается -14
Операция индексации не изменяет значение указателя, к которому она
применялась.
13
Ссылку можно рассматривать как постоянный указатель, который всегда
разадресован и для которого не надо выполнять операцию разадресации (*).
Подчеркнем, что ссылка не создает копии объекта, а является именем объекта.
Т.е. выражение &ref_a == &a всегда истинно.
Из этого правила имеется два исключения, когда при объявлении ссылки
создается копия во временной памяти.
1. При объявлении ссылки на константу. При этом создается внутренняя
переменная, ей присваивается константа, а затем ссылка инициализируется этой
переменной.
Например, есть объявление:
int t = (int)ui;
int &r_ui = t;
Когда при объявлении ссылки создается копия объекта во временной памяти,
компилятор выдает соответствующее предупреждение. В случае выдачи
сообщения об ошибке положение можно исправить следующим образом:
14
объектам пользовательских типов (классов). Передача по значению объектов,
возможно больших, в качестве операндов вызовет лишние затраты на их
копирование. Кроме того, передача операндов с использованием указателей,
приводит к необходимости взятия адресов операндов в выражениях.
Домашнее задание. Определите данные:
int a, b, c;
int *pa = &a, *pb = &b, *pc = &c;
int x = 75;
int &refx = x;
printf ("a %p a %d \nb %p b %d \nc %p c %d \n\n",
pa,pa,pb,pb,pc,pc);
printf ("a %p a %d \nb %p b %d \nc %p c %d \n\n",
&a,&a,&b,&b,&c,&c);
7.2. Массивы
Как и в большинстве других языков программирования, в Си под массивом
понимается совокупность однородных данных, рассматриваемых как нечто единое.
Математическими аналогами программистского понятия массив является,
например, матрица, которая в программе задается в виде двухмерного массива, или
вектор, задаваемый одномерным массивом. Массив – это такой объект программы,
который характеризуется именем, размерностью, количеством элементов по
каждому измерению и типом значений его элементов.
7.2.1. Описание массивов
В программе на Си описание массивов осуществляется оператором, имеющим
следующую общую форму записи:
<тип> <имя>[<размер1>][<размер2>]...[<размерN>];
где <тип> определяет тип элементов массива. Все элементы массива однородны и
могут принимать значения одного из любого допустимого в Си типа данных, за
исключением функций и файлов. Чаще всего в качестве <тип> используются
ключевые слова, такие, как int, float, char и другие ключевые слова и
словосочетания, задающие основные и производные типы данных, например,
указатели, структуры и т.д.; <имя> – это <идентификатор>, задающийся по
усмотрению программиста и являющийся именем массива. Имя массива
используется для доступа к элементам массива; <размер> определяет количество
15
элементов массива по каждому измерению и записывается целой беззнаковой
константой. Количество конструкций [<размер>] определяет размерность массива.
При описании одномерного массива указывается только <размер1>, двухмерного
массива <размер1> и <размер2> и т.п. для многомерных массивов. Многомерные
массивы рассматриваются в Си как одномерные, элементами которых могут быть
любые объекты, в том числе и массивы.
Примеры описания массивов:
16
Рассуждаем так: константа 2 в первых квадратных скобках определяет
базовый одномерный массив m из двух элементов m[0] и m[1]. Вот они:
17
Здесь описан массив данных целого типа с именем num. Список
устанавливаемых значений заключается в фигурные скобки. Во время компиляции
программы элементы массива приобретут следующие значения:
num[0]=1; num[1]=3; num[2]=5; num[3]=7;
num[4]=9; num[5]=11; num[6]=13; num[7]=9;
При описании массивов с инициализацией элементов не обязательно
указывать размеры массива. Компилятор сам определит количество элементов и
выделит для них память соответствующего размера, например:
int num[]={1,3,5,7,9,11,13,9};
Приведем еще несколько примеров инициализации массивов:
float vector[4]={1.2,34.57,81.9,100.77}; – здесь все ясно;
int p[3][2][3] = {
{ 1, 2, 3, 4},
{ 7, 8, 9, 10, 11, 12},
{13, 14, 15, 16, 17, 18}
};
//то эквивалентное объявление "в одну строчку" будет следующим:
int p[3][2][3] =
{1,2,3,4,0,0,7,8,9,10,11,12,13,14,15,16,17,18};
20
/* пересечении 2-ой строки и 4-го столбца */
N=m[i-2][2*j-3]; /* N получит значение элемента нулевой строки и */
/*5-го столбца */
N=m[0][0];
m[0][0]=m[2][5];
m[2][5]=N;
/* m[0][0] и m[2][5] - первый и последний элементы матрицы */
/* обменялись значениями */
/* печать матрицы в один столбец: */
for(i=0; i<=2; i++)
for(j=0; j<=5; j++)
printf("%d\n", m[i][j]);
/* печать матрицы по строкам: */
for(i=0; i<=2; i++)
printf("%3d%3d%3d%3d%3d%3d\n", m[i][0],m[i][1],m[i]
[2],m[i][3],m[i][4],m[i][5]);
2. Использование имени одномерного массива. Этот способ доступа к
элементам массива основан на том, что в Си имя одномерного массива трактуется
как константа-указатель на данные, тип которых определен типом элементов
массива, а значением этой константы является адрес нулевого элемента.
Следовательно, применив к имени массива операции адресной арифметики, можно
получить доступ к любому элементу массива. Поэтому, например, для
одномеpного массива, описанного как int mas[15];, выражения Си mas и
&mas[0], mas+1 и &mas[1], mas+2 и &mas[2] являются равносильными.
#include <stdio.h>
main()
{ int m[3][6]={ { 1, 2, 3, 4, 5, 6},
{ 7, 8, 9,10,11,12},
{13,14,15,16,17,18}
};
int i,j;
/* Доступ к элементам через указатели m[0], m[1], m[2] */
for(i=0; i<=2; i++)
for(j=0; j<=5; j++) printf("%d \n",*(m[i]+j));
/* Доступ к элементам массива через указатель m[0] */
for(i=0; i<=17; i++) printf("%d \n",*(m[0]+i));
/* Доступ к элементам через указатель m */
for(i=0; i<=17; i++) printf("%d \n",*(*m+i));
}
4. Доступ через переменную указатель. К элементам массива можно
обратиться при помощи вспомогательного указателя. Для этого необходимо
описать указатель с типом, соответствующим типу массива, и присвоить ему
значение имени массива, которое является константой типа указатель. Теперь с
указателем можно манипулировать как с переменной, ссылающейся на элементы
массива, над ней можно выполнять любые действия, допустимые в адресной
арифметике. При этом необходимо учитывать принятый порядок расположения
элементов массива в памяти компьютера:
int arr[2][3]={1,2,3,4,5,6};
int *uk;
int i;
uk=arr; // uk указывает на начало массива arr
uk++; // теперь uk указывает на arr[0][1]
i=*uk; // i получило значение 2
uk=uk+2; // uk указывает на arr[1][2]
i=*uk; // i получило значение 4
uk=arr[0]; // uk снова указывает на начало массива
uk=uk+1;
i=*uk; // i приняло значение 2
i=uk[1]; // i приняло значение 3
i=uk[3]; // i приняло значение 5
i=uk[0]; // i приняло значение 2
23
При доступе к элементам массива при помощи указателя надо учитывать , что
указатель "передвигается" по данным в той последовательности, в какой они
расположены в памяти. Если, увеличивая указатель, мы выйдем за пределы данных
массива, то он будет указывать на неопределенные данные. Для того, чтобы
обращаться к массиву при помощи указателя точно так же, как и при помощи
имени массива, необходимо соответствующим образом описать указатель.
Например, объявлен массив:
int arr[2][3]={1,2,3,4,5,6}; ,
который можно рассматривать как одномерный массив, элементами которого
являются одномерные массивы, содержащие по 3 элемента, т.е. arr[0] и arr[1]
являются указателями на соответствующие трехэлементные массивы. Теперь, если
описать указатель на трехэлементные массивы:
int (*ukt)[3];
то, используя ukt, можно обращаться к элементам массива arr по индексам:
ukt=arr; // ukt[0] – указывает на 1 строку матрицы
// ukt[1] – указывает на 2 строку матрицы
i=ukt[0][1]; // i приняло значение 2, ибо arr[0][1] равносильно
// ukt[0][1],
// что равносильно *(ukt[0]+1), которое дает значение 2
i=ukt[0][0]; // i приняло значение равное 1
ukt=ukt+1; // для arr присваивание было бы недопустимо
// ukt[0] сейчас указывает на 1 строку
i=ukt[0][0]; // i приняло значение, равное 4
Таким образом, любое действие, которое достигается с помощью индексации
массива, достигается и с помощью указателей на него. Тесная взаимосвязь
указателей, массивов и адресной арифметики представляет собой одно из
достоинств языка Си.
Замечание. Т.к. операция индексации массива является бинарной операцией,
в которой участвуют адрес массива и индекс, указывающий на смещение от начала
массива (первый элемент имеет смещение 0), то возможны такие индексации
массива:
int a[] = { 1, 2, 3, 4, 5, 6 };
std::cout << (1 + 3)[a] - a[0] + (a + 1)[2] << std::endl;
//Ответ: 8
//(1 + 3)[a] == a[1+3] (идентичные выражения)
// а – адрес, (a + 1)[2] a + 1- адрес, [2] - индексация
0 1 2 3 4 5 6 7 8 9 10 11
/* Упорядочивание указателей */
for (p=razmer; p>=2; p--)
for (i=0; i<p-1; i++)
if (*str[i] > *str[i+1])
{
s=str[i+1];
str[i+1]=str[i];
str[i]=s;
};
/* Вывод на печать строк, упорядоченных по алфавиту */
for (i=0; i<=razmer; i++)
printf("%s \n ",str[i]);
}
7.3. Перечисления
Перечисление – это конструируемый тип данных, во время описания которого
задается имя типа данных и значения, которые могут принимать переменные этого
типа.
7.3.1. Описание перечисляемого типа
Описание перечисляемого типа данных осуществляется оператором вида:
29
умолчанию первому имени списка значений соответствует значение 0, второму 1 и
т.д.
Использование в программе именованной константы равносильно
использованию ее значения. Явная инициализация именованной константы
константным выражением переопределяет последовательность значений, заданных
по умолчанию. Имя, следующее за переопределенным именем, принимает
значение, увеличенное на 1, если только его значение не задано явно другой
величиной. Список значений может содержать повторяющиеся значения имен, но
сами имена должны быть различны.
31