Тема: Указатели. Динамические массивы
(4 часа)
В языке С, кроме базовых типов, разрешено вводить и использовать производные типы, каждый из которых получен на основе более простых типов. Стандарт языка определяет три способа получения производных типов:
· Массив элементов заданного типа;
· Функция, возвращающая значение заданного типа;
· Указатель на объект заданного типа.
Указатели
В языке С указатели введены как объекты, значениями которых служат адреса других объектов либо функций.
Указатель – это адрес памяти. Значение указателя сообщает о том, где размещен объект, но не говорит о самом объекте. Как и всякие переменные, указатели нужно определять и описывать. Символ операции ‘*’ используется для задания “указателя на” объект. Кроме разделителя ‘*’, в определения и описания указателя входят спецификации типов, задающих типы объектов, на которые ссылаются указатели.
Например: int *ptr;
Данное определение следует понимать как “ptr является указателем на целое”. Указатель на тип void совместим с любым указателем. Например, если задано
void *x;
int *y;
то допустимо следующее присваивание y=x;
В общем случае переменная типа указатель описывается так:
тип *переменная_указатель
Двумя наиболее важными операциями, связанными с указателями, является операция обращения по адресу * (иногда называется операцией снятия ссылки или разыменования) и операция определения адреса &.
Операция обращения по адресу * служит для присваивания или считывания значения переменной, размещенной по адресу переменная_указатель, при помощи лево-определенного выражения *переменная_указатель. Например,
*ptr=value;
что обозначает следующее: значение переменной value помещается в участок памяти, адрес которого определяет указатель ptr. Операндом операции разыменования всегда является указатель. Указатель может ссылаться на объекты того типа, который присутствует в определении указателя. Исключением являются указатели, в определении которых использован тип void – отсутствие значения. Такие указатели могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования, т. е. операцию ‘*’.
Операция value=*ptr; обозначает следующее: переменной value присваивается значение, хранимое в ячейке памяти, адресуемой указателем ptr.
Операция определения адреса & возвращает адрес памяти своего операнда. Операндом должна быть переменная. Операция определения адреса выполняется следующим образом:
адрес=&переменная;
где адрес – это соответствующее выражение, куда помещается адрес, а переменная – имя переменной, определенной выше в программе.
В языке С размер возвращаемого адреса зависит от применяемой модели памяти.
Всем указателям можно присвоить безопасный адрес памяти – нуль целого типа NULL. Гарантируется, что этот адрес не совпадает ни с одним адресом, уже использованным в системе. Такой адрес, нередко называемый нулевым адресом, часто применяют как ограничитель в динамических структурах.
Пример:
#include<stdio.h>
void main(){
int *x, *w;
int y, z;
*x=16;
y=-15;
w=&y;
printf(“\nРазмер x=%d”, sizeof(x));
printf(“\nЗначение указателя x=%u”, x);
printf(“\nЗначение по такому адресу=%d”, *x);
printf(“\nАдрес y=%u”, &y);
printf(“\nАдрес z=%u”, &z);
printf(“\nЗначение *w=%d”, *w);
}
Динамические массивы
В соответствии со стандартом языка массив представляет собой совокупность элементов, каждый из которых имеет одни и те же атрибуты (характеристики). Все элементы размещаются в смежных участках памяти подряд, начиная с адреса, соответствующего началу массива, т.е. значению & имя_массива [0].
При традиционном определении массива:
тип имя_массива [количество_элементов];
имя_массива становится указателем на область памяти, выделяемой для размещения элементов массива. Количество_элементов в соответствии с синтаксисом языка должно быть константным выражением. Тип явно определяет размеры памяти, выделяемой для каждого элемента массива.
Таким образом, общее количество элементов массива и размеры памяти, выделяемой для него, полностью и однозначно заданы определением. Это не всегда удобно. Иногда нужно, чтобы память для массива выделялась в таких размерах, какие нужны для решения конкретной задачи, причем потребности в памяти заранее не известны и не могут быть фиксированы.
Формирование массивов с переменными размерами можно организовать с помощью указателей и средств для динамического выделения памяти. Начнем рассмотрение указанных средств с библиотечных функций, описанных в заголовочных файлах alloc.h и stdlib.h стандартной библиотеки (файл alloc.h не является стандартным). В таблице приведены сведения об этих библиотечных функциях. Функции malloc(), calloc() и realloc() динамически выделяют память в соответствии со значениями параметров и возвращают адрес начала выделенного участка памяти. Для универсальности тип возвращаемого значения каждой из этих функций есть void*. Этот указатель (указатель такого типа) можно преобразовать к указателю любого типа с помощью операции явного приведения типа (тип *). Функция free() решает обратную задачу – освобождает память, выделенную перед этим с помощью одной из трех функций calloc(), mailoc() или realloc(). Сведения об этом участке памяти передаются в функцию free() с помощью указателя – параметра типа void*. Преобразование указателя любого типа к типу void * выполняется автоматически, поэтому вместо формального параметра void * можно подставить в качестве фактического параметра указатель любого типа без операции явного приведения типов.
Таблица. Функции для выделения и освобождения памяти
Функция | Прототип и краткое описание |
malloc | void * malloc (unsigned s); Возвращает указатель на начало области (блока) динамической памяти длиной в s байт. При неудачном завершении возвращает значение NULL. |
calloc | void * calloc (unsigned n, unsigned m); Возвращает указатель на начало области (блока) обнуленной динамической памяти, выделенной для размещения n элементов по m байт каждый. При неудачном завершении возвращает значение NULL. |
realloc | void * realloc (void * bl, unsigned ns); Изменяет размер блока ранее выделенной динамической памяти до размера ns байт. bl – адрес начала изменяемого блока. Если bl равен NULL (память не выделялась), то функция выполняется как malloc. |
free | void * free (void * bl); Освобождает ранее выделенный участок (блок) динамической памяти, адрес первого байта которого равен значению bl |
Следующая программа иллюстрирует на несложной задаче особенности применения функций выделения (malloc) и освобождения (free) динамической памяти. Решается следующая задача: ввести и напечатать в обратном порядке набор вещественных чисел, количество которых заранее не фиксировано, а вводится до начала ввода самих числовых значений. Текст программы может быть таким:
#include <stdio.h>
#include <stdlib.h>
void main ( ){
/* Указатель для выделяемого блока памяти */
float *t;
int i , n ;
printf ("\nn="); /* n - число элементов */
scanf ("%d",&n);
t= (float *)malloc(n*sizeof (float));
for{i=0; i<n; i++) /* Цикл ввода чисел */
{ printf ("x[%d]=",i);
scanf ("%f", fit [i]); }
/* Цикл печати результатов */
for(i=n-l; i>=0; i -- ){
if(i%2== 0)printf ("\n"); printf ("\tx[%d]=%f",i,t[i];
}free (t); /* Освобождение памяти */}
В программе int n – количество вводимых чисел типа float, t – указатель на начало области, выделяемой для размещения n вводимых чисел. Указатель t принимает значение области, выделяемой для n значений типа float. Обратите внимание на приведение типа (float*) значения, возвращаемого функцией malloc( ). Доступ к участкам выделенной области памяти выполняется с помощью операции индексирования: t[i] и t[i-l]. Остальное очевидно из текста программы. Оператор free(t); содержит вызов функции, освобождающей выделенную ранее динамическую память и связанной с указателем t.