Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ПЯВУ - С-С++методичка и титул.doc
Скачиваний:
12
Добавлен:
08.11.2019
Размер:
789.5 Кб
Скачать

Многомерные массивы

Рассмотрим более подробно работу с многомерными массивами. Размерность массива – это число индексов, используемых для ссылки на конкретный элемент в массиве. Многомерный массив – это массив, состоящий из массивов, то есть его элементами являются массивы. Например, двухмерный массив, описанный как int m[2][3], состоит из двух элементов-строк (одномерных массивов): m = { m[0], m[1] } по три элемента в каждом:

массив с индексом 0: m[0] с элементами m[0][0], m[0][1], m[0][2],

массив с индексом 1: m[1] с элементами m[1][0], m[1][1], m[1][2].

Имя массива m является указателем-константой на начало двухмерного массива: m=&m[0][0]; в свою очередь элементы m[0], m[1] являются указателями-константами на начала своих одномерных массивов: m[0] = &m[0][0], m[1] = &m[1][0]. Поскольку m = m[0] = &m[0][0], постольку имя массива m является указателем на массив указателей на начала своих одномерных массивов (m = { m[0], m[1] }), что определяет вид адресных выражений для двухмерного массива. Для ссылки на элементы двухмерного массива “точкой отсчета” может быть как самый первый элемент, так и указатели на строки массива.

Адрес начала i-й строки массива (m[i]) можно получить выражением *(m+i), где имя массива m используется как адрес начала массива указателей. Адрес j-го элемента i-й строки определяется эквивалентными выражениями:

m[i] + j = *(m+i) + j = &m[i][j].

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

Пример 30.

Ввести и вывести по строкам двухмерный массив.

Программа:

void main ()

{ int m[2][3], i, j; // описание массива и параметров циклов

puts (“Ввод массива m по строкам”);

for (i=0; i < 2; i++) // цикл по строкам

for (j=0; j < 4; j++) // цикл по столбцам

scanf (“%d”, &m[i][j] ); // ввод элемента массива

// варианты адресных выражений: m[i]+j; *(m+i)+j)

printf (“Вывод массива m по строкам:\n”, );

for (i=0; i < 2; i++) // цикл по строкам

{ for (j=0; j < 4; j++) // цикл по столбцам

printf (“%d”, m[i][j]); // индексное выражение

printf (“\n”); // переход на новую строку

// варианты адресных выражений: *(m[i]+j); *(*(m+i)+j)

}

}

Отметим, что использование указателей для доступа к элементам массива дает более короткие подпрограммы. Из примера 30 видно, что адресное обращение к элементам двухмерного массива выглядит более сложным и громоздким по сравнению с индексным обращением. Недостаток этого примера в том, что он неуниверсален, поскольку размерности массива заданы конкретными числами. Изменив размерности массива, мы должны изменить их в программе и перекомпилировать программу. Использование директивы #define k 10 (примеры 28, 29) позволяет улучшить текст программы, но изменение k опять требует ее перекомпилирования. Таким образом, эти программы не отвечают требованию массовости (универсальности) алгоритма. Универсальной можно считать программу, которая получает значения размерностей массива в процессе выполнения программы, то есть динамически. Использование ограниченной по объему статической памяти, выделяемой для текста и данных программы, не позволяет этого добиться.

Динамическая память.

Но есть динамическая память, имеющая гораздо больший объем (так называемая "куча" на жаргоне программистов), которой можно управлять. В начале программы необходимо добавить директиву препроцессора подключения библиотеки функций для работы с динамической памятью:

#include <alloc.h>.

Функции управления динамической памятью:

  • (type*) malloc (nbytes); – выделяет блок памяти размера nbytes байтов, и возвращает указатель на первый байт блока для данных, определяемых типом type;

  • (type*) calloc (nelem, elsize); – выделяет блок памяти для nelem элементов по elsize байтов каждый (всего nelem*elsize байтов), обнуляет выделенную память и возвращает указатель на нее;

  • (type*) realloc (*bloc, size) – делает попытку установить новый размер size блока, на который ссылается указатель bloc.

  • free (*bloc) – освобождает блок памяти, на начало которого ссылается указатель bloc. Использование этой функции обязательно, после работы с динамической памятью.

Использование указателей является средством разработки универсальных программ. Выше было показано, что для массива int m[2][3] имя массива m является указателем на массив указателей. Так как первый элемент массива – указатель, то имя массива m есть указатель на указатель (константа). В С/С++ можно описать переменную "указатель на указатель". Если простой указатель объявляется *р, то указатель на указатель обозначается: **рр, то есть число звездочек * определяет уровень указателя. Фактически указатель на указатель – это ячейка памяти, хранящая адрес другого указателя. Рассмотрим, как можно воспользоваться этим средством для обработки матриц.

Пример 31.

Программа демонстрирует использование указателя на указатель для работы с двухмерными массивами и выделение динамической памяти. Выполняется ввод прямоугольной матрицы (S[n][k]) и вывод ее на экран по строкам:

#include<stdio.h>

#include<conio.h>

#include<alloc.h> // функции для динамической памяти

void main()

{ int n, k, i, j; // размеры массива и индексы

int **S; // указатель на указатель для массива

clrscr();

printf("Введите количество строк матрицы n = ");

scanf("%d", &n); // ввод числа строк

printf("Введите количество столбцов матрицы k = ");

scanf("%d", &k); // ввод числа столбцов

S=(int**) malloc(n*sizeof(int)); // память для массива указателей строк

for (i=0; I < n; i++) // цикл выделения памяти

S[i]=(int*) malloc(k*sizeof(int)); // для элементов строк матрицы

printf("Введите матрицу S[%i][%i]:\n", n, k);

for (i=0; i <n; i++) // цикл по строкам

for (j=0; j < k; j++) // цикл по столбцам

scanf("%d", &S[ i ][ j ]); // ввод элементов матрицы

printf("Введена матрица S[%i][%i]:\n", n, k);

for (i=0; i <n; i++) // цикл по строкам

{ for (j=0; j < k; j++) // цикл по столбцам

printf("%d ", S[i][j]); // вывод элементов матрицы

printf('\n'); // переход на новую строку

}

// Освобождение динамической памяти:

for (i=0; i<n; i++) // цикл по строкам

free(S[i]); // удаление строк

free(S); // удаление массива указателей

}

Из программы видно, что после динамического выделения памяти для матрицы можно обращаться к ее элементам в привычной для пользователя индексной форме. Освобождение использованной динамической памяти обязательно, так как неиспользуемые блоки не только уменьшают объем памяти, но и фрагментируют ее.

Символьные массивы

Описание массива типа

char string[80]; позволяет описывать и обрабатывать символьные данные, однако тип char[ ] относится к целому типу, поскольку имеет дело с ASCII-кодами символов.

Особым случаем массива является строковый литерал (строка-константа) – последовательность любых символов, заключенных в кавычки: “это строковый литерал”. Строковый литерал представляется в памяти как массив элементов типа char[ ], в конце которого помещен символ ‘\0’ (нуль-байт), поэтому его называют ACSIIZ-строкой (от zero – нуль). Как и с любым массивом, со строковым литералом связан указатель-константа на первый элемент строки.

Можно также описать указатель символьного типа

char *str; который пока не связан ни с какой символьной информацией, но ему выделено два байта для хранения адреса.

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

Совершенно идентичны следующие описания с инициализацией:

char string [10] = {‘с’,’л’,’о’,’в’,’о’,’\0’};

char string [10] = ”слово”;

Последнее описание выглядит проще, а нулевой байт записывается в конце строки автоматически. Адрес первого элемента (‘с’) становится адресом массива string. Строка в массиве ограничена нуль-байтом, а остальные элементы массива не используются. Можно не задавать размер массива, компилятор выделит нужное число элементов плюс нуль-байт при инициализации:

char string[ ]=”слово”;

Описание вида char *str; выделит место в памяти только для указателя str, а место для отсутствующей строки не выделяется. Можно инициировать указатель строковым литератором, например,

char *str = “Иванов А.С.”; или присваиванием определить указатель:

str = “Иванов А.С.”; тогда указатель получит адрес первого символа литерала, который располагается в сегменте данных программы.

Стандартные функции ввода-вывода можно использовать для обработки символьных данных.

Пример 30.

Ввести и вывести строку символов.

Программа:

void main ( )

{ char name [25];

puts (“Введите Фамилию И.О.:”);

gets ( name );

puts (“Вывести Фамилию И.О.:”);

puts ( name ); // или printf(“%s”, name);

}

Функция scanf (“%s”, name) менее удобна, поскольку вводит символы до первого пробела.

При использовании указателя для выделения памяти под любое содержимое строки необходимо использовать функции динамического выделения памяти.

Пусть дано описание указателя

char *str; тогда оператор вида

str = (char*) malloc (30); выделит 30 байтов в динамической памяти и запишет адрес начала этого блока в указатель str.

Пример 31.

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

Программа:

#include <alloc.h>

void main ( )

{ char *name;

pname = (char*) malloc (30); // выделение динамической памяти

printf (“Введите Фамилию И.О.:”);

gets ( name );

printf (“Вывод Фамилии И.О.:”);

free (name ); // освобождение динамической памяти

}

Особый случай – инициализация двухмерного массива строковыми литералами. Многомерные массивы могут инициироваться и без указания одной (самой левой) из размерностей массива в [ ]. Компилятор сам определяет число элементов по числу членов в списке инициализации.

Например,

char str [ ] [80] = { “Первая строка”,

“Вторая строка”,

“Третья строка” };

Как было показано выше, элементы массива str[0], str[1], str[2] являются указателями на строки двухмерного массива, что можно использовать при обработке таких массивов.

Пример 32.

Ввод и вывод строковых данных.

Программа:

void main ( )

{ int i ;

char buffer [3][80];

puts (“Введите строки в буфер:”);

for (i=0; i<3; i++) gets (buffer[i]);

puts (“Вывод строк из буфера:”);

for (i=0; i<3; i++) puts (buffer[i]);

}

Пример 33.

Скопировать символы из массива s1, на который ссылается указатель ps1, в массив s2, на который ссылается указатель ps2. Исходная строка заканчивается символом ‘\0’.

Программа:

void main ( )

{ char s1[80], s2[80], *ps1=s1, *ps2=s2; // инициализация указателей

clrscr ();

puts (“Введите строку и нажмите Enter:”);

gets (s1); // чтение строки и запись нуль-байта

while (*ps1!= ’\0’) // цикл пока не конец строки

{ *ps2 = *ps1; // копирование символа

ps1++; // переход к следующему символу в s1

ps2++; // переход к следующему символу в s2

}

*ps2 = ’\0’; // запись нуль-байта в строку s2

printf (“Скопирована строка :\n%s”,s2); // вывод строки s2

}

С учетом старшинства и действий операций присваивания (=), разадресации (*) и увеличения (++) копирование символов в цикле while можно выполнить в заголовке оператора while() с пустым телом цикла с одновременной проверкой условия:

while ( *ps2++ = *ps1++ ) ;

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

Проще выглядит копирование символов с помощью оператора цикла:

for ( i=0; s2[i]=s1[i]; i++ ) ; // тело цикла – пустой оператор