учебники, программирование, основы, введение в,

 

Массивы и указатели

Указатели и массивы
Допустимо бесконечно большое число различных типов указателей и массивов. Далее следуют типовые примеры.
Указатель на основной тип:
char *p;
Переменная р является указателем на символ, т.е. этой переменной должен присваиваться адрес символа.
Указатель на указатель:
char **t;
Переменная t - указатель на указатель символа.
Одномерный массив:
int a[50];
Переменная а - массив из 50 целых чисел.
Двухмерный массив:
char m[7][50];
Переменная m - массив из семи массивов, каждый из которых состоит из 50 символов.
Массив из семи указателей:
char *r[7];
Массив r состоит из указателей на символы.
Указатель на функцию:
int (*f)();
f - указатель на функцию, возвращающую целое значение.
Массивы
Массив является сложным объектом, состоящим из объектов-компонентов, называемых элементами одного и того же типа. Простые определения массива имеют вид
Тип данных x[n1][n2]_[nk]
Где x - идентификатор, определяемый в качестве имени массива, а ni - размерности массива. Массив x называется k-мерным массивом с элементами типа тип данных. Элементы i-го измерения имеют индексы от 0 до ni-1. Тип элемента массива может быть одним из основных типов, типом другого массива, типом указателя (pointer), типом структуры (struct) или типом объединения (union). Хотя элементы массива не могут быть функциями, они могут быть указателями на функции. Ниже приведены некоторые примеры определений массива:
int page[10]; /* одномерный массив из 10
элементов, перенумерованный с 0 до 9 */
char line[81];
float big[10][10],sales[10][5][8]; /*двумерный
массив и трехмерный массив*/
Ссылки на элемент k-мерного массива x делаются с помощью следующего обозначения:
x[i1][i2]...[ik]
где ij - целое выражение, при этом 0<=ij<=nj-1, а nj - максимальное значение j-го индекса массива x. Например:
page[5]
line[i+j-1]
big[i][j]
Указывая только первые p индексов, можно ссылаться на p-мерный подмассив k-мерного массива (p<=k), например,
sales[i] /* ссылка на двумерный подмассив массива
sales */
sales[i][j] /* ссылка на одномерный подмассив */
sales[i][j][k] /* ссылка на элемент массива*/
Указатели
Указателем называется компонент заданного типа, являющийся ссылкой на некоторую область памяти. Определение указателя имеет следующий вид:
тип-данных *id1, *id2,_, *idn
Тип переменных id1, id2,_,idn определяется как тип указателей на тип-данных. Эти переменные служат ссылками на объекты типа тип-данных. Этот тип называется базовым типом переменных-указателей. Ниже приведены несколько примеров определений указателей:
int *pi, *qi;/* указатели на целые объекты */
char *c; /* указатель на символьный объект */
Динамические объекты
Указатели используются при создании и обработке динамических объектов. Заранее определяемые объекты создаются с помощью определений. Динамические объекты, в отличие от заранее определяемых, создаются динамически и явно в процессе выполнения программы. Для создания динамических объектов служат функции malloc и calloc. В отличие от заранее определенных объектов, число динамических объектов не фиксировано тем, что записано в тексте программы, - по желанию динамические объекты могут создаваться и уничтожаться в процессе ее выполнения. Динамические объекты, в отличие от заранее определенных, не имеют имен, и ссылка на них выполняется с помощью указателей.
Значение 0 может быть присвоено указателям любого типа. Это значение показывает, что данный указатель не содержит ссылки на какой-либо объект. Попытка использовать это значение для обращения к объекту может привести к ошибке, но только в операционных системах с защитой памяти. По соглашению, для обозначения константы с нулевым значением используется идентификатор NULL, описание которого находится в библиотеке stddef.h и является системозависимым.

Создание динамических объектов
По стандарту аргументы функций malloc, calloc имеют тип возвращаемого объекта void*.
char s = (char*)malloc(size);
unsigned size; /* объем памяти, который необходимо выделить */
char *s = (char *)calloc(nelem,elsize);
unsigned nelem; /* число элементов, для которых нужно выделить память */
unsigned elsize; /* объем памяти, который необходимо выделить для каждого элемента */
/* либо просто заменив char* на void*
void* calloc(nelem, elsize);
unsigned nelem;
unsigned elsize;
*/
Обе функции возвращают знаковый указатель, указывающий на выделенную память. Для определения необходимого объема памяти можно использовать оператор sizeof:
sizeof (выражение)
Объем памяти, необходимый для хранения выражения:
sizeof(T)
Объем памяти, необходимый для хранения значений типа T.
Функции malloc и calloc возвращают указатель на созданный динамический объект. Фактически функции возвращают знаковые указатели, которые могут быть явно преобразованы к подходящему типу указателя. Значения, возвращенные функциями распределения памяти, используются для ссылок на динамические объекты. Например, с помощью оператора
pi = (int *) malloc(sizeof(int));
выделяется память для одного целого значения. Адрес этой области памяти присваивается переменной pi после его преобразования из типа char * (указатель на знак), с которым он возвращается функцией maloc, к типу int * (указатель на целое), т.е. типу переменной pi.
Доступ к динамическим объектам
Присваивание значения объекту, ссылка на который задана указателем pi, выполняется с помощью имени указателя *pi, например:
*pi = 55;
Одно и то же значение может быть присвоено более чем одной переменной-указателю. Таким образом, можно ссылаться на динамический объект с помощью более одного указателя. Про объект, к которому можно обращаться с использованием более чем одного указателя, говорят, что он имеет псевдоимена (alias). Например, в результате присваивания
qi = pi;
и qi, и pi указывают на один и тот же объект, т.е. они являются псевдоименами. Неуправляемое использование псевдоимен может нанести ущерб пониманию текста программы, так как возможность доступа к одному и тому же объекту и его модификация с помощью различных псевдоимен не всегда очевидны при анализе части программы.
Время жизни динамического объекта
Память, занимаемая динамическими объектами, если она необходима для других целей, должна быть освобождена явным указанием. В противном случае эта память может быть потеряна, т.е. станет невозможным ее повторное использование. Явное освобождение выполняется использованием фунукции free, которая имеет следующую спецификацию:
free(ptr)
char *ptr;
Необходимо предпринимать меры предосторожности для избежания ошибок, связанных со ссылками на объект, память для которого уже освобождена - проблема висящей ссылки (Horowwitz, E. 1983. Fundamentals of Programming Languages. Computer Science Press).
Если реализация языка обеспечивает сборку мусора, то память, занимаемая объектами, к которым невозможен доступ, может быть автоматически подготовлена для повторного использования. Однако в языке Си, в отличие от языков Лисп и Снобол, такая возможность отсутствует.
Указание на заранее определенные объекты. Указатели могут обеспечивать ссылку на заранее определенные объекты. Адрес такого объекта может быть определен использованием оператора адресации & (address of operator). Например, рассмотрим переменные i и pi, определенные как
int i, *pi;
Присваивание
pi = &i;
p позволяет ссылаться на объект с именем i также с помощью указателя pi, используя обозначение *pi. Имена i и *pi - псевдоимена. Оператор & является также стандартным средством моделирования передачи параметров по ссылке. Однако его употребление может привести к проблеме висящей ссылки.
Указание на произвольную ячейку памяти. С помощью явных преобразований можно получить указатель на произвольную ячейку памяти. Например, предположим, что pt является указателем типа T*. Тогда указатель на ячейку памяти 0777000 можно получить с помощью следующей записи:
pt = (T*)0777000;
Обращение к конкретным ячейкам памяти часто бывает необходимо в программах, взаимодействующих с оборудованием, например в драйверах устройств, когда для управления устройствами нужно иметь доступ к таким ячейкам памяти, как регистры состояния или ячейки буфера устройства. Хотя такие возможности полезны и даже необходимы для некоторых приложений, пользоваться ими следует с осторожностью.
Связь между указателями и массивами
В языке Си массивы и указатели тесно связаны. Имя каждого массива может рассматриваться как указатель на первый элемент массива. Элемент массива a[i] есть элемент массива, на который указывает значение a+i, т.е. *(a+i), где значение а является адресом первого элемента массива а, а именно a[0]. Выражение a+i является примером арифметических действий с указателями - целое значение i складывается со значением указателя, адресом первого элемента массива а. Значение этого выражения есть а плюс объем памяти, занимаемый i элементами массива a. Предположим, что x - двумерный массив. Тогда ссылка на подмассив x[i] является ссылкой на i-ю строку массива x. x[i] дает адрес первого элемента этой строки, т.е. *(x+i). Элементы каждой строки занимают непрерывную область памяти, так как массивы хранятся з аписанными по строкам, т.е. при записи элементов массива в память быстрее всех изменяется последний индекс.
Аналогично, ссылка на y[i], где y - n-мерный (n>1) массив, является ссылкой на (n-1)-мерный подмассив с элементами y[i, j2,j3,_jn], где значения jk соответствуют определению массива y. y[i] дает адрес первого элемента этого подмассива, т.е. *(y+i). Все элементы этого (n-1)-мерного подмассива занимают непрерывную область памяти.

Строки - дополнительные сведения о тесной связи между указателями и массивами
Строки - это массивы знаков. По соглашению, последним знаком строки должен быть нулевой знак \0. Поскольку имя массива фактически является указателем на первый элемент массива, переменные типа string могут также рассматриваться, как имеющие тип char *. Например, вторая переменная string_array в определении
char *string_pointer, string_array[81];
может рассматриваться также как знаковый указатель. Для строки, представленной первой переменной string_pointer, память должна быть выделена явно. С другой стороны, для массива string_array память является указателем на нее. Заметим, что память должна быть также выделена или зарезервирована для признака конца строки \0.


!

Нет ничего необычного не только в интерпретации переменных типа string, т.е. массивов знаков как указателей, но и в интерпретации строк, которые также могут рассматриваться двояко - как массивы и как указатели - и все в одной программе! Это особенно важно, когда строки передаются как аргументы функции. Вызывающая программа может рассматривать строку как массив знаков, а вызываемая функция может рассматривать ее как знаковый указатель. Если длина строки непостоянна, то использование знаковых указателей для строк имеет определенные преимущества. Хотя строки переменной длины могут быть также реализованы с использованием массивов, такая реализация оказывается слишком неэкономной с точки зрения использования памяти и налагает ограничения на максимальную длину строки. Например, для размещения строк разной длины может быть создан массив знаковых указателей. Альтернативное решение с использованием двумерного массива знаков в общем случае будет использовать память неэффективно, так как в этом случае потребовалось бы сделать число столбцов равным числу знаков в строке наибольшей возможной длины.

Инициализация массивов и классы памяти
Мы знаем, что скалярные переменные можно инициализировать в описании типа при помощи таких выражений, как например:
int fix = 1;
float flax = PI*2;
при этом предполагается, что PI - ранее введенное макроопределение. Можно ли инициализировать массивы?
Внешние, статические и автоматические массивы можно инициализировать!
Регистровые массивы инициализировать нельзя!
Если ничего не засылать в массив перед началом работы с ним, то внешние, статические и автоматические массивы инициализируются для числовых типов нулем и '\0' (null) для символьных типов, а регистровые массивы содержат какой-то мусор, оставшийся в этой части памяти. Если в статическом, внешнем или автоматическом массиве нам нужны первоначальные значения, отличные от нуля, в этом случае мы можем делать так:
/* дни месяца */
int days[12]={31,28,31,30,31,30,31,31,30,31,30,31};
main( )
{
int index;
extern int days[];/*необязательное описание */
for(index = 0; index<12; index++)
printf("Месяц %d имеет %d дней.\n", index+1,
days[index]);
}
Результат:
Месяц 1 имеет 31 дней.
Месяц 2 имеет 28 дней.
Месяц 3 имеет 31 дней.
Месяц 4 имеет 30 дней.
Месяц 5 имеет 31 дней.
Месяц 6 имеет 30 дней.
Месяц 7 имеет 31 дней.
Месяц 8 имеет 31 дней.
Месяц 9 имеет 30 дней.
Месяц 10 имеет 31 дней.
Месяц 11 имеет 30 дней.
Месяц 11 имеет 30 дней.
Месяц 12 имеет 31 дней.
Количество элементов в списке инициализации должно соответствовать размеру массива. Если список меньше размера массива, то элементы массива, на которых не хватило списка, будут забиты нулями. Если же список больше массива, то компилятор выдаст синтаксическую ошибку. Надо просто выделить массив, размер которого будет достаточен для размещения списка.
Предыдущую программу лучше переписать так:
int days[ ] = {31,28,31,30,31,30,31,31,30,31,30,31};
main( )
{
int index;
extern int days[ ];/* необязательное описание */
for(index=0;index<sizeof(days)/(sizeof(int));
index++)
printf("Месяц %d имеет %d дней.\n",index +1,
days[index]);
}
К этой программе следует сделать два существенных замечания.
Первое: если мы используем пустые скобки для инициализации массива, то компилятор сам определит количество элементов в списке и выделит для него массив нужного размера.
Второе: оно касается добавления, сделанного в управляющем операторе for. Не полагаясь на свои вычислительные способности, мы возложили задачу подсчета размера массива на компилятор. Оператор sizeof определяет размер в байтах объекта или типа, следующего за ним. Предположим в нашей вычислительной системе размер каждого элемента типа int равен двум байтам, поэтому для получения количества элементов массива мы делим общее число байтов, занимаемое массивом, на 2. Однако в других системах элемент типа int может иметь иной размер. Поэтому в общем случае выполняется деление на значение переменной sizeof для элемента типа int.
В результате работы этой программы мы получаем точно 12 значений. Наш метод, позволяющий программе самой находить размер массива, не позволил нам напечатать конец массива.


Функции, массивы и указатели
Массивы можно использовать в программе двояко. Во-первых, их можно описать в теле функции. Во-вторых, они могут быть аргументами функции. Все, что было сказано о массивах, относится к первому их применению. Теперь рассмотрим массивы в качестве аргументов. Проанализируем скелет программы, обращая внимание на описания.
/* массив-аргумент */
main( )
{
int ages[50]; /* массив из 50 элементов */
convert(ages);
_
}
convert(years);
int years[ ];/* каков размер массива? */
{
_
}
Очевидно, что массив ages состоит из 50 элементов. А что можно сказать о массиве years? Оказывается в программе нет такого массива. Описатель
int years[ ];
создает не массив, а указатель на него! Посмотрим, почему это так. Вот вызов нашей функции:
convert(ages);
ages - аргумент функции convert. Имя ages является указателем на первый элемент массива, состоящего из 50 элементов. Таким образом, оператор вызова функции передает ей указатель, т. е. адрес функции covert( ). Это значит, что аргумент функции является указателем, и мы можем написать функцию convert( ) следующим образом:
convert(years);
int *years;
{
_
}
Действительно, операторы
int years[ ];
int *years;
- синонимы. Оба они объявляют переменную years указателем массива целых чисел. Однако главное их отличие состоит в том, что первый из них напоминает нам, что указатель years ссылается на массив.
Как теперь связать его с массивом ages? При использовании указателя в качестве аргумента, функция взаимодействует с соответствующей переменной в вызывающей программе, т.е. операторы, использующие указатель years в функции convert( ), фактически работают с массивом ages, находящимся в теле функции main( ). Короче говоря, когда имя массива применяется в качестве аргумента, функции передается указатель. Затем функция использует этот указатель для выполнения изменений в исходном массиве, принадлежащем программе, вызывающей функцию.
Операции с указателями
  1. Присваивание. Указателю можно присвоить адрес. Обычно мы выполняем это действие, используя имя массива или операцию получения адреса &.
  2. Определение значения. Операция * выдает значение, хранящееся в указанной ячейке.
  3. Получение адреса указателя. Подобно любым переменным, переменная типа указатель имеет адрес и значение. Операция & сообщает нам, где находится сам указатель.
  4. Увеличение указателя. Мы можем выполнять это действие с помощью обычной операции сложения либо с помощью операции увеличения. Увеличивая указатель, мы перемещаем его на следующий элемент массива.
  5. Разность. Можно находить разность двух указателей. Обычно это делается для указателей, ссылающихся на элементы одного и того же массива, чтобы определить, на каком расстоянии друг от друга находятся элементы. Помните, что результат имеет тот же тип, что и переменная, содержащая размер массива!

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика