Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Практикум по программированию на языке Си

..pdf
Скачиваний:
24
Добавлен:
12.11.2023
Размер:
3.53 Mб
Скачать

ЗАДАНИЕ. Напишите программы генерации, формирующие значения элементов матриц в соответствии с алгоритмами (правилами), описанными в [3, §10.4.1].

ЗАДАНИЕ. Напишите программы обработки матриц в соответствии с условиями задач, приведенными в [3, §10.4.2]. Предусмотрите в этих программах формирование матриц по исходным данным, подготовленным с помощью программ генерации из предыдущего задания.

Коротко о важном

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

!Индексация элементов массивов начинается с нуля (06_1.с и

др.).

!Размерность – количество индексов, необходимое для обращения к элементу массива.

!Размер одномерного массива – число элементов в массиве и количество значений, принимаемых индексом при переборе элементов массива.

!Нельзя определить массив, не указав тип его элементов и его размерность (не путайте размерность с размером).

!Список инициализации может определять размер массива, т.е. количество его элементов (06_05.с, 06_06.с).

!Длина списка инициализации не может превышать явно заданный размер массива (06_01_1.с).

!Традиционный Си допускает только константные выражения при определении размеров массива автоматической памяти.

!Конкретные компиляторы, соответствующие Стандарту 99, допускают определение во внутренних блоках массивов автоматической памяти "с переменными размерами" (06_03.с, 06_07.с –

06_11.с, 06_12B.с).

!Определение массивов автоматической памяти с переменными размерами противоречит традициям языка Си, но допускается

211

Стандартом 99 и некоторыми реализациями (06_03.с, 06_07.с

– 06_11.с).

!Массив переменной длины (Стандарт 99) получает память при каждом входе в блок, где он определен, и уничтожается при выходе из блока.

!Массив переменой длины за время "своей жизни" не меняет своих размеров, которые не известны до входа в блок, где определен массив.

!Обращения к элементам переменной длины корректны только в пределах его длины.

!Нельзя инициализировать массивы переменной длины

(06_03_1.с).

!В программах для обработки массивов с переменными размерами традиционный язык Си требует явного задания (константами) предельных значений этих размеров (06_13_1.с, 06_18А.с, 06_18В.с).

!Алгоритмы сортировки (упорядочения) элементов массива проще при использовании вспомогательных массивов.

!Сортировка массива перестановкой значений его элементов обычно использует алгоритмы с вложенными циклами

(06_09.с, 06_10.с, 06_11.с).

!Для многократных экспериментов с программами, требующими ввода большого количества исходных данных, удобно переназначать входной поток и читать данные из заранее подготовленного текстового файла (06_12B.с, 06_13B.с).

!Наиболее простой путь передачи данных между двумя последовательно исполняемыми программами – переназначение стандартных потоков ввода-вывода (06_12A.с, 06_12B.с, 06_13A.с, 06_13B.с и др.).

!Переназначение стандартных потоков ввода-вывода выполняется на этапе исполнения готовой программы (06_12A.с, 06_12B.с, 06_13A.с и др.).

!Неудобство переназначения стандартных потоков ввода-вывода – нарушение запланированного режима диалога пользователя с исполняемой программой (06_12A.с).

!Передача данных "переназначением" стандартных потоков вво- да-вывода требует согласования формата данных, выводимых

212

первой программой, с требованиями к представлению данных, читаемых второй программой (06_12B.с, 06_13B.с).

!Многомерные массивы размещаются в памяти "по строкам" (быстрее изменяется правый индекс), что определяет правила расположения значений в списке инициализации (06_14.с, 06_15.с).

!Для многомерных массивов применимо вложение списков инициализации, позволяющее сделать более наглядным инициализатор массива (06_15_1.с).

!При необходимости представлять матрицы переменных размеров

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

размеров массивов (06_16_3.с).

!Стандарт 99 допускает неконстантные выражения для задания размеров матриц (06_16.с, 06_18В.с).

Тема 7

Указатели и адреса объектов

Опасно использовать языковые возможности и приемы, которые вы понимаете лишь частично.

Б. Страуструп. Язык программирования С++

Основные вопросы темы

!Указатели как объекты разных классов памяти.

!Препроцессорная константа NULL.

!Возможности нетипизированных указателей.

!Разыменование указателей разных типов.

!Способы доступа к участку памяти, выделенному для переменной.

!Имя массива и указатель, адресующий элементы массива.

!Индексация и разыменование при обращении к элементу массива.

!Индексация и разыменование при обращении к элементам многомерных массивов.

!Массивы указателей как средство регулярного доступа к разноименным объектам.

!Косвенное упорядочение объектов с помощью массива их адресов.

!Средства динамического распределения памяти.

!Представление массивов с изменяемыми размерами с помощью динамической памяти.

!Моделирование многомерных динамических массивов.

!Непрямоугольные двумерные массивы.

7.1. Указатели, адреса, разыменование, адресация

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

214

ЗАДАЧА 07-01. Определите в программе следующие указатели одного (выбранного вами) типа разных классов памяти: глобальный, внутренний статический и два указателя автоматической памяти. Напечатайте их значения, используя в форматной строке функции printf() спецификацию %p. Выведите также значение препроцессорной константы NULL, представляющей отсутствие значения (адреса).

/* 07_01.c - указатели как объекты разных классов памяти */

#include <stdio.h>

#define PRINTP(EXPRESSION) \ printf(#EXPRESSION"=%p\n",EXPRESSION)

long * pGlobal; int main ()

{

long * pAuto1; long * pAuto2;

static long * pInternal; PRINTP(pGlobal); PRINTP(pAuto1); PRINTP(pAuto2); PRINTP(pInternal); PRINTP(NULL);

return 0;

}

Результаты выполнения программы (DJGPP):

pGlobal=0

pAuto1=366c

pAuto2=4c4ac

pInternal=0

NULL=0

При использовании 16-разрядного компилятора TC:

pGlobal=0000:0000

pAuto1=00DE:0727

pAuto2=09BD:0000

pInternal=0000:0000

NULL=0000:0000

215

Посмотрим на результаты, приведенные для 32- и 16-разрядных компиляторов. Обратите внимание, что числовые значения указателей являются шестнадцатеричными числами, как это принято для адресов. Константа NULL имеет нулевое значение. Именно этим значением инициализированы по умолчанию статические указатели (глобальный pGlobal и внутренний pInternal). Два указателя автоматической памяти pAuto1, pAuto2 получили произвольные значения.

Известно, что указателю типа void * можно присвоить значение адреса объекта любого типа или (что то же самое) значение указателя на объект любого типа. Чтобы уяснить возможности указателей типа void *, рассмотрите следующую задачу.

ЗАДАЧА 07-02. В программе определить и инициализировать переменную типа double, указатель double * и указатель типа void *. Присвойте указателям адрес переменной. Напечатайте адрес переменной, значения указателей и значения, получаемые при разыменовании указателей. Чтобы продемонстрировать роли и последовательность выполнения унарных операций получения адреса & и разыменования *, выведите на печать значение выражения *&имя_переменной.

/* 07_02.c - адреса, указатели, операция разыменования*/

#include <stdio.h>

#define PRINTD(EXPRESSION) \ printf(#EXPRESSION"=%f\n",EXPRESSION)

#define PRINTP(EXPRESSION) \ printf(#EXPRESSION"=%p\n",EXPRESSION)

int main ()

{

double d=0.1234; double * pd; void * pv;

pd = &d; pv = &d;

PRINTP(&d);

PRINTP(pd);

PRINTP(pv);

216

PRINTD(*pd); PRINTD(*(double *)pv); PRINTD(*&d);

return 0;

}

Результаты выполнения программы (DJGPP):

&d=4c48c

pd=4c48c

pv=4c48c

*pd=0.123400

*(double *)pv=0.123400 *&d=0.123400

Тот факт, что значения выражений *pd, *&d и переменной d одинаковы, очевиден. Равенство адреса &d и значений указателей pd и pv также не вызывает удивления. Основная цель приведенной программы – демонстрация необходимости приведения типа при разыменовании указателя void *.

ЭКСПЕРИМЕНТ. Удалите приведение типа в обращении к макросу PRINTD(*(double*)pv), т.е. запишите его в виде

PRINTD(*pv).

Результат трансляции будет таким (см. программу 07_02_1.с):

07_02_1.c: In function `main':

07_02_1.c:18: warning: dereferencing `void *' pointer

07_02_1.c:18: invalid use of void expression

Попытка прямого разыменования указателя void * не удалась!

Если в определении указателя отсутствует инициализация, то статический указатель принимает значение NULL, а указатель автоматической памяти случайным образом адресует произвольный участок памяти. Результаты разыменования такого указателя непредсказуемы, а присваивание значений объекту, адресованному неинициа-

217

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

ЗАДАНИЕ. Определите без инициализации статические глобальный и внутренний указатели и указатель – объект автоматической памяти. Напечатайте значения разыменований этих явно не инициализированных указателей. Занесите значения в участки памяти, адресованные этими неинициализированными указателями, и вновь напечатайте их разыменования.

Заданию соответствует программа 07_02_2.c. Текст программы в книге не приведен, чтобы она случайно не послужила опасным примером для начинающего программиста. Ведь компилятор DJGPP не выдает даже предупреждающих сообщений.

ЗАДАЧА 07-03. Определите и инициализируйте переменные разных типов (char, int, double) и один указатель типа void *. Последовательно присваивая указателю адреса переменных разных типов, выведите значения переменных с помощью разыменования указателя.

/* 07_03.c - "настройка" указателей типа void */ #include <stdio.h>

int main ()

{

char symbol ='#'; int nom = 33; double d=0.1234; void * pv;

pv = &symbol;

printf("*(char *)pv=%c\n",*(char *)pv); pv = &nom;

printf("*(int *)pv=%d\n",*(int *)pv); pv = &d;

printf("*(double *)pv=%f\n",*(double *)pv); return 0;

}

Результаты выполнения программы (DJGPP):

*(char *)pv=#

218

*(int *)pv=33

*(double *)pv=0.123400

В программе обратите еще раз внимание на необходимость преобразования типов при разыменовании указателя типа void*.

ЗАДАЧА 07-04. Определите и инициализируйте переменную типа double. Определите указатели char *, int *, double *, void *,

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

/* 07_04.c - указатели разных типов, адресующие один объект */

#include <stdio.h>

#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)

#define PRINTP(EXPRESSION) \ printf(#EXPRESSION"=%p\n",EXPRESSION)

int main ()

{

double d=0.1234;

char * pc = (char *)&d; int * pi = (int *)&d; double * pd = &d;

void * pv = &d; PRINTP(pc); PRINTP(pi); PRINTP(pd); PRINTP(pv); PRINTI(sizeof(pc)); PRINTI(sizeof(pi)); PRINTI(sizeof(pd)); PRINTI(sizeof(pv)); PRINTI(sizeof(*pc)); PRINTI(sizeof(*pi)); PRINTI(sizeof(*pd));

PRINTI(sizeof(*(char *)pv)); PRINTI(sizeof(*(int *)pv)); PRINTI(sizeof(*(double *)pv)); return 0;

}

219

Результаты выполнения программы (DJGPP):

pc=4c68c

pi=4c68c

pd=4c68c

pv=4c68c

sizeof(pc)=4

sizeof(pi)=4

sizeof(pd)=4

sizeof(pv)=4

sizeof(*pc)=1

sizeof(*pi)=4

sizeof(*pd)=8 sizeof(*(char *)pv)=1 sizeof(*(int *)pv)=4 sizeof(*(double *)pv)=8

Обратите внимание на равенство числовых значений указателей разных типов, адресующих один объект (одну и ту же переменную). Размеры участков памяти, выделенных указателям разных типов, одинаковы.

Как и следовало ожидать, размеры участков памяти, "доступных" с помощью разыменования указателей разных типов, различны и равны длинам переменных соответствующих типов. Особыми свойствами обладает указатель типа void *. Длина участка памяти, доступного при его разыменовании, определяется приведением типа.

ЗАДАНИЕ. Выведите на печать значения из участков памяти, адресуемых указателями в предыдущей программе.

/* 07_04_1.c – разыменование разных указателей, адресующих один объект */

#include <stdio.h>

#define PRINTC(EXPRESSION) \ printf(#EXPRESSION"=%c\n",EXPRESSION)

#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)

#define PRINTD(EXPRESSION) \ printf(#EXPRESSION"=%f\n",EXPRESSION)

220