Тема: Указатели. Динамические массивы

(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.