8.3  Указатели на массивы

Указатели представляют эффективный способ обработки массивов. В языке С/С++ между указателями и массивами существует тесная связь. Например, когда объявляется массив в виде int array[5], то компилятор не только выделяет память для пяти элементов массива, но и создает указатель с именем array, инициализирует его адресом первого по счету (нулевого) элемента массива. Таким образом, сам массив остается безымянным, а доступ к его элементам осуществляется через указатель с именем array.  С точки зрения синтаксиса языка указатель array является константой, значение которой можно использовать в выражениях, но изменить это значение нельзя. Например:

array = = &array[0];

Для доступа к элементам массива существует два различных способа. Первый способ связан с использованием обычных индексных выражений в квадратных скобках, например:

array[2].

Второй способ доступа к элементам массива связан с использованием адресных выражений в форме *(array+2). При реализации на компьютере первый способ приводится ко второму, т.е. индексное выражение преобразуется к адресному. Для приведенного примера array[2] преобразуется в *(array+2).

Для доступа к начальному элементу массива (т.е. к элементу с нулевым индексом) можно использовать просто значение указателя array. Любое из присваиваний

*array = 2;

array[0] = 2;

*(array+0) = 2;

присваивает начальному элементу массива значение 2, но быстрее всего выполнятся присваивание *array=2 так как в нем не требуется выполнять операции сложения.

Отметим главные различия между инициализацией символьного массива, хранящего строку, и инициализацией указателя, указывающего на строку, на следующем примере:

#include<stdio.h>  // подключение заголовочных файлов

#include<math.h>

void main(void)

{

char mas[ ]="Одномерный массив"; // константа

char *m = "Строка символов";        // переменная

// Вывод посимвольно константы как элементов массива:

for(int i=0;i<17;i++) putchar(mas[i]);

putchar(‘n‘);

// Вывод посимвольно константы с помощью инкремента указателя:

for (i=0;i<17;i++) putchar (*(mas+i));

putchar(‘n‘);

// Вывод посимвольно переменной с помощью цикла for:

for (i=0;i<15;i++) putchar (*(m+i));

putchar(‘n‘);

// Вывод посимвольно переменной с помощью цикла while:

while (*(m)!=’’) putchar(*(m++));

}

Главное различие между массивом mas[ ] и строкой m в том, что имя массива mas является константой, а имя строки m – переменной. Для доступа к элементам константы mas и переменной m можно использовать запись в виде массива [ ] и операции инкремента указателя ( ), однако вывод посимвольно циклом while  можно выполнить только переменной m. Чтобы воспользоваться конструкцией while для константы mas, сначала нужно переопределить указатель m:

m=mas; // теперь указатель m будет указывать на первый элемент массива mas;

однако будет потерян адрес строки m (конструкцию mas=m; записать нельзя).

Пример 1

int strlen(char *s)       /* Возвращает длину строки s */

{

int n;

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

return (n);

}

Операция увеличения s совершенно законна, поскольку эта переменная является указателем; s++ никак не влияет на символьную строку в функции strlen(), а только увеличивает локальную для функции strlen() копию адреса. Описания формальных параметров в определении функции в виде:

char s[ ]; char *s;

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

Пример 2

strcpy(char *s, char *t) 

{

while ((*s = *t) != ‘’)

{

s++;

t++;

}

}

Так как аргументы передаются по значению, функция strcpy() копирует по одному символу за шаг, пока не будет скопирован в s завершающий в t символ .

Другой вариант программы strcpy():

strcpy(char *s, char *t)

{

while((*s++ = *t++) != ‘’);

}

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

 strcpy(char *s, char *t)

{

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

 }

При динамическом распределении памяти для массивов следует создать соответствующий указатель и инициализировать его при помощи функции calloc. Например, одномерный массив a[10] из элементов типа float можно создать следующим образом:

float *a;

a=(float*)(calloc(10,sizeof(float));

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

Рис. 8.1. Инструменты, используемые в картах памяти:

а – указатель; б – поле оперативной памяти, занимаемой объектом; в – стрелка, направленная на объект на который указывает указатель

Указатели на многомерные массивы в языке С/С++ – это массивы массивов, т.е. такие массивы, элементами которых являются массивы. При объявлении таких массивов в памяти компьютера создается несколько различных объектов. Объявление mas[4][3] порождает в программе три разных объекта: указатель с идентификатором mas, безымянный массив из четырех указателей (4) и безымянный массив из двенадцати чисел типа int (1).

Для доступа к элементам двумерного массива чисел типа int должны быть использованы два индексных выражения, например, в форме mas[1][2] или эквивалентной ей  форме *(*(mas+1)+2).

При размещении элементов многомерных массивов они располагаются в памяти подряд по строкам, т.е. быстрее всего изменяется последний индекс, а медленнее – первый. Такой порядок дает возможность обращаться к любому элементу многомерного массива, используя адрес его начального элемента и только одно индексное выражение. Например, обращение к элементу mas[1][2] можно осуществить с помощью указателя ptr, объявленного в форме int *ptr=mas[0][0] как обращение ptr[1*3+2] (здесь 1 и 2 это индексы используемого элемента, а 3 – это число элементов в строке), т.е. как ptr[5]. Заметим, что внешне похожее обращение mas[5] выполнить невозможно, так как указателя с индексом 5 не существует.

Рисунок 8.1 – Карта памяти для массива mas[4][3]:

1 – безымянный массив из двенадцати (4 х 3) полей типа int;  2 – столбцы; 3 – строки; 4 – безымянный массив из четырёх указателей; 5 – указатель с идентификатором mas; 6 – указатель

Для создания двумерного динамического массива сначала нужно распределить память для безымянного массива указателей на одномерные массивы, а затем распределять память для одномерных массивов. Пусть, например, требуется создать массив a[n][m] с плавающей точкой двойной точности, тогда это можно сделать при помощи следующего фрагмента программы:

#include <stdlib.h>

#include<stdio.h>

 void main (void)

 {

double **a;

int n,m,i;

scanf("nУкажите значения n=%d m= %d",&n,&m);

a=(double **)calloc(m,sizeof(double *)); // безымянный массив указателей

for (i=0; i<=m; i++)

a[i]=(double *)calloc(n,sizeof(double)); // двумерный массив

. . . . . . . . . . . .

free(a);               

}

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

Вместо функций типа calloc()  и free() в язык C++ добавлены новые операторы new и delete, которые позволяют выделять и удалять в программе динамические области памяти.

Пусть Type *x; где x –  указатель на тип Type. Тогда можно выделить динамическую память размером sizeof(Type) следующим образом:

x=new Type; // Первая форма оператора new без инициализатора

или

x=new Type (4); // Первая форма оператора new с инициализатором

Здесь оператор new выделяет память под безымянный объект типа Type и инициализирует ее значением 4 и возвращает адрес созданного объекта. Это место в памяти теперь можно использовать, обращаясь по указателю x:

Type y= *x;

Чтобы удалить выделенную операцией new память, нужно использовать оператор delete:

delete x;

Нельзя дважды освобождать занятый участок памяти или использовать delete с указателем на память, не выделенную операцией new.

Чтобы выделить динамическую память под одномерный массив, например из четырёх элементов, с помощью операции new, нужно использовать квадратные скобки. Например:

Type * x=new Type[4];  // Вторая форма оператора new.

К сожалению, данная форма оператора new не позволяет инициализировать элементы массива. Здесь оператор new Type [4] возвращает указатель x на выделенную память размером 4*sizeof(Type), являющуюся массивом типа Type.

Для удаления памяти, выделенной операцией new[ ], необходимо использовать операцию delete[ ]. Например:

Type b;

b= x[3];

delete[ ] x;

Приведем пример создания динамических одномерного и двумерного массивов:

// Exis.cpp : Defines the entry point for the console application.

//

#include <iostream.h>

#include <stdlib.h>

int main(int argc, char* argv[ ])

{

int *mass1;   // Указатель для одномерного массива

nt **mass2;  // Указатель для двумерного массива

int i,j;

int size=10;             // Размер одномерного массива

int row=5,col=8;     // Размер двумерного массива

//—————————————————

//  Выделение динамической памяти под одномерный массив:

mass1=new int[size];      // Выделение памяти под элементы одномерного массива

for (i=0; i<size; i++)          // Инициализация одномерного массива

*(mass1+i)=i;

//—————————————————

// Выделение динамической памяти под двумерный массив:

mass2=new int*[row]; // Выделение памяти под массив указателей

for (i=0; i<row; i++)

*(mass2+i)=new int[col]; // Выделение памяти под элементы двумерного массива

for (i=0; i<row; i++)

for (j=0; j<col; j++) *(*(mass2+i)+j)=i+j; // Инициализация двумерного массива

//—————————————————  

// Вывод элементов одномерного и двумерного массивов:      

cout<<"Output massiv1"<<endl;

for (i=0; i<size; i++)

cout<<*(mass1+i)<<" ";

cout<<endl;

cout<<"Output massiv2"<<endl;

for (i=0; i<row; i++)

{

for (int j=0; j<col; j++) cout<<*(*(mass2+i)+j)<<" ";

cout<<endl;

}

// Удаление одномерного массива:

delete[ ] mass1;

// Удаление двумерного массива:

for (i=0; i<row; i++)     

delete[ ] mass2[i];   // Сначала удаляются элементы двумерного массива

delete[ ] mass2;         //  Затем удаляется массив указателей

return 0;

}