Файл: А. Б. Шнейвайс основы программирования.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 06.12.2023

Просмотров: 333

Скачиваний: 6

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

3. k=4; do j=1,m-1; tek=>tek%next; enddo.
1 5
2
tek
4 3
После исключения третьего звена в кольцевом списке оста- лось четыре участника 1,2,4,5,
причём очередная проговорка считалки начинается с чет- вёртого. Двукратный повтор оператора tek=>tek%next поместит в переменную tek указатель на пятое звено. Так что очередным кандидатом на исключение окажется первое звено.
4. tek%next=>tek%next%next
1 5
2
tek
4 3
Первый участник исключён из кольцевого списка, но его зве- но продолжает занимать место в оперативной памяти. Теперь в кольцевом списке только три участника 2,4,5.
143

5. k=3; do j=1,m-1; tek=>tek%next; enddo
1 5
2
tek
4 3
После очередного двукрат- ного повторения оператора tek=>tek%next в перемен- ную tek будет помещён ука- затель на четвёртое звено, т.е.
очередным кандидатом на вы- лет окажется участник под но- мером 5.
6. tek%next=>tek%next%next
1 5
2
tek
4 3
Пятое звено исключено, но продолжает занимать место в оперативной памяти.
Теперь в кольцевом списке только два звена 2,4.
144

7. k=2; do j=1,m-1; tek=>tek%next; enddo
1 5
2
tek
4 3
До работы оператора цикла переменная tek хранит указа- тель на четвёртого участника.
Однако теперь, когда пятый участник исключён, после чет- вёртого следует второй. Так что после отработки упомяну- того цикла в переменной tek опять окажется указатель на четвёртого участника, т.е. по- следнее слово считалки при- ходится на второго участника,
который должен будет выле- теть.
8. tek%next=>tek%next%next
1 5
2
tek
4 3
В итоге в кольцевом списке ока- зывается один участник под но- мером 4, который и должен во- дить.
На самом деле, утверждая ранее, что для поиска водящего потребуется лишь одна переменная типа указатель на звено списка, мы не планировали экономое использование оперативной памяти. В результате место, отведённое под звенья всех вылетевших участников удерживается программой до завершения её ра- боты, причём доступ к этим звеньям нашей программой утерян навсегда. Для экономного использования памяти можно вылетающие звенья возвращать из под юрисдикции программы под юрисдикцию операционной системы, используя оператор deallocate, который удаляет размещённые объекты.
145

3.4.7
Операторы nullify, deallocate и функция associated nullify разрывает связь указателя с объектом. (т.е. nullify(first, nov)
поместит в переменные first и nov признак отсутствия связи их со зве- ньями, на которые указывали они раньше). Восстановить прежнее зна- чение указателя на оторванное звено можно лишь, если предварительно позаботиться о сохранении значений, уничтожаемых nullify в перемен- ных first или nov, например, в других переменных-указателях.
program tpoint6; implicit none type candidat integer number type (candidat), pointer :: next end type candidat type (candidat), pointer :: q, first, tek, nov
! здесь значения указателей
! не определены, т.е. нам
! не известно, связан ли каждый из описанных указателей хоть с каким-нибудь
! адресом. Узнать об этом можно посредством встроенной функции булева типа
! associated(имя_указателя).
integer ier write(*,*) associated(q),associated(first),associated(tek),associated(nov)
nullify(q,first,tek,nov)
write(*,*) associated(q),associated(first),associated(tek),associated(nov)
allocate(q,stat=ier); if (ier/=0) stop 1
write(*,*) associated(q),associated(first),associated(tek),associated(nov)
q%number=77; write(*,*) ’q%number=’,q%number nullify(q);
write(*,*) ’q%number=’,q%number allocate(q,stat=ier); if (ier/=0) stop 2
q%number=55; write(*,*) ’q%number=’,q%number tek=>q nullify(q);
write(*,*) ’q%number=’,q%number write(*,*) ’tek%number=’,tek%number allocate(q,stat=ier); if (ier/=0) stop 3
q%number=33; write(*,*) ’q%number=’,q%number tek=>q deallocate(q);
write(*,*) ’q%number=’,q%number write(*,*) ’tek%number=’,tek%number write(*,*) associated(q),associated(tek)
end
146


Результаты работы программы tpoint6:
T F T T
! Как видим, q, tek и nov связаны, а first --- нет.
! Более того, даже можем вывести содержимое их q%number=
-843398271 ! number-полей, правда, лишь для того, чтобы first%number=
0 ! убедиться, что в них действительно есть tek%number=
604801882 ! что-то, воспринимаемое компьютером как данные nov%number= -1077198184 ! типа integer.
! Однако после nullify(q, first, tek, nov) все
F F F F
! все указатели не связаны ни с каким адресом.
q%number=
0
first%number=
0
tek%number=
0
nov%number=
0
!
После allocate(q) указатель q успешно адресует звено
T F F F
!
типа type(candidat). Пока поля этого звена не заданы.
q%number=
77
! Теперь полю number присвоено значение.
q%number=
0
! Поскольку связь разорвана, то выводится
! значение, определяемое системой по умолчанию.
! Однако, на самом деле звено, с которым
! разорвана связь, хотя и недоступно программе
! тем не менее существует в оперативной памяти.
q%number=
55
! После tek=>q разрыв nullify(q) не помешает q%number=
0
! достать значение, занесённое в поле number,
tek%number=
55 ! через указатель tek посредством tek%number.
q%number=
33
! Однако, если бы использовали не nullify(q), а q%number=
0
! deallocate(q), то не только разорвали связь, но tek%number=
0 ! и уничтожили адресуемое звено, так что достать
! из него содержимое number-поля уже невозможно
! даже через посредство указателя tek, хранящего
! адрес места расположения бывшего звена, о чём
F T
! свидетельствует associated(tek).
147

3.4.8
Атрибуты target и pointer
В программах testlist и testring указатели использовались для созда- ния связного списка, когда связь одного звена с другим осуществлялась исключительно через поле с атрибутом pointer. Однако, указатели мо- гут адресовать и обычные переменные (скаляры, массивы), имена кото- рых явно указаны в разделе описаний.
Пусть нужно обменять значениями целые переменные a и b. В про- грамме tpoint7 приведены два способа такого обмена и третий способ —
обмен значениями адресующих a и b указателей.
program tpoint7; implicit none integer, target ::
a,
b,
c ! Объявили, что доступ к этим переменным
! возможен не только посредством имён
! a, b и c, НО и посредством указателей.
integer, pointer :: pa, pb, pc ! Место для указателей pa, pb, pc
! отведено, но их значения пока неизвестны.
a=1; b=8
! Задание значений a и b
! ПЕРВЫЙ СПОСОБ ОБМЕНА
a и b значениями
! --------------------
(для него ни
! target, ни pa, ни pb, ни pc НЕ НУЖНЫ)
write(*,*) ’
a=’,a,’
b=’,b
! Вывод заданных значений a и b c=a; a=b; b=c
! Обычный обмен переменных значениями write(*,*) ’
a=’,a,’
b=’,b,’ после ПЕРВОГО СПОСОБА ОБМЕНА.’
write(*,’(40("-"))’)
! ВТОРОЙ СПОСОБ ОБМЕНА a и b значениями
! -------------------- (через посредство
! указателей pa, pb и pc)
pa=>a
! Указатель pa настроен на адресацию a pb=>b
! Указатель pb настроен на адресацию b pc=>c
! Указатель pc настроен на адресацию с
!----------------------------------
! Другими словами, пока pa, pb и pc не переопределены, то ФОРТРАН трактует
!---------------------------------- их просто ссылочными синонимами имён
! a, b и c, что и видно из вывода значений write(*,*) ’
a=’, a,’
b=’, b ! переменных a и b и их ссылочных write(*,*) ’ pa=’,pa,’
pb=’,pb ! синонимов pa и pb. Они пока отражают
! результат обмена ПЕРВЫМ СПОСОБОМ.
pc=pa
! И здесь имена pc, pa и pb (слева и справа)
pa=pb
! от обычного оператора присваивания (=)
pb=pc
! ФОРТРАН трактует просто как ссылочные
! синонимы имён c, a и b соответственно.
write(*,*) ’
a=’,a, ’
b=’, b,’
Результат ВТОРОГО СПОСОБА ОБМЕНА’
write(*,*) ’ pa=’,pa,’
pb=’,pb,’
Результат ВТОРОГО СПОСОБА ОБМЕНА’
write(*,’(40("-"))’)
148


! ТРЕТИЙ СПОСОБ - имитация обмена на
! -------------
! основе переприсваивания значений.
write(*,*) ’
a=’, a,’
b=’, b ! указателей. Таков результат обмена write(*,*) ’ pa=’,pa,’
pb=’,pb !
a и b
предыдущим способом.
pc=>pa
! Здесь ситуация принципиально отлична от предыдущей.
pa=>pb
! Здесь указателям присваиваются НОВЫЕ ЗНАЧЕНИЯ, т.е.
pb=>pc
! pa, pb и pc перестают быть соответствующими синонимами
! первоначальных имён переменных a, b и c, т.е. происходит.
! обмен содержимым именно указателей, а не тех переменных, синонимами
! которых они были во ВТОРОМ СПОСОБе. В результате ТРЕТЬЕГО СПОСОБА
!
pa становится синонимом уже переменной b (но не a) и
!
pb становится синонимом переменной a (но не b).
! Поэтому, содержимое переменных a и b остаётся таким же каким и было,
! НО, если забыть про новое толкование pa и pb, то может возникнуть
! иллюзия, что обменялись содержимым переменные a и b.
write(*,*) ’
a=’, a,’
b=’, b, ’ после ТРЕТЬЕГО СПОСОБА ОБМЕНА’
write(*,*) ’ pa=’,pa,’
pb=’,pb, ’ после ТРЕТЬЕГО СПОСОБА ОБМЕНА’
end
Результаты работы программы tpoint7:
a=
1
b=
8
a=
8
b=
1
после ПЕРВОГО СПОСОБА ОБМЕНА.
---------------------------------------- a=
8
b=
1
pa=
8
pb=
1
a=
1
b=
8
после ВТОРОГО СПОСОБА ОБМЕНА
pa=
1
pb=
8
после ВТОРОГО СПОСОБА ОБМЕНА
---------------------------------------- a=
1
b=
8
pa=
1
pb=
8
a=
1
b=
8
после ТРЕТЬЕГО СПОСОБА ОБМЕНА
pa=
8
pb=
1
после ТРЕТЬЕГО СПОСОБА ОБМЕНА
1. Атрибут target в описании переменных a и b означает, что на эти переменные программа может ссылаться не только посредством имён a и b, но и через посредство указателей. Объекты, снабжённые атрибутом target часто называют адресатами.
2. Указатель всегда определяется типом объекта, который предпола- гается адресовать, и атрибутом pointer. Естественно, содержимое указателя (cсылка на адресат) должно быть настроено соответству- ющим образом (сразу после описания указателя статус его связ- ности неопределён; поэтому, результат работы, например, функции associated, выясняющей статус связности, непредсказуем).
149

3. Для помещения в указатель той или иной ссылки (прикрепления ссылки к адресату) в ФОРТРАНе используется оператор => (не путать с оператором обычного присваивания = — в противном случае компиляция может завершиться успешно, а на этапе выпол- нения получим аварийный останов). Во ВТОРОМ СПОСОБЕ перед обменом в указатель pa посредством pa=>a записывается ссылка на адресат a, а в pb — ссылка на адресат b. Тем самым pa и pb становяться ссылочными синонимами переменных a и b.
4. В отличие от testlist, testring и tpoint6, где слева и справа от опе- ратора => стояли указатели (т.е. объекты, наделённые атрибутом pointer), правые части операторов pa=>a и pb=>b из tpoint7
корректны исключительно из-за наличия в описании a и b атрибута target. Если слово target убрать из описания integer, target :: a,
b, c, то при компиляции программы получим:
$ gfortran tpoint7.f95
tpoint7.f95:11.4:
pa=>a
1
ошибка: Pointer assignment target is neither TARGET nor POINTER at (1)
tpoint7.f95:12.4:
pb=>b
1
ошибка: Pointer assignment target is neither TARGET nor POINTER at (1)
5. Наличие слова target нисколько не мешает использовать имена a и b обычным образом, не обращаясь к указателям, если это предпо- чтительнее. Например, весь ПЕРВЫЙ СПОСОБ обмена осуществ- лён посредством обычного оператора присваивания (естественно,
для такого способа можно обойтись и без атрибута target).
6. В языке СИ есть операция разыменования, обозначаемая в кон- тексте выполняемых СИ-операторов звёздочкой перед именем СИ- указателя. Так что, образно говоря, ФОРТРАН-имена pa, pb можно понимать как СИ-имена *pa и *pb.
Когда-то резко критиковали ФОРТРАН за используемый им способ доступа процедуры к фактическому аргументу — по адресу (по ссыл- ке). Критика (приводилась в первом семестре — см.5.3.7) была настоль- ко объективно обоснованной, что в языках С и C++ был выбран способ
150

доступа — по значению. Правда, при этом возникла проблема возвра- та результата, которая удачно решалась посредством указателей. Указа- тель, передаваемый по значению, нельзя изменить внутри функции, но через него можно было изменить содержимое адресуемой им перемен- ной. И вот в С++ появляется новая возможность по сравнению с СИ
— скрытый указатель или параметр-ссылка (см. опять-таки первый семестр 5.3.6).
В книге [9], в частности, отмечается (и с этим трудно не согласиться),
что «по мнению многих программистов, параметры-ссылки предлагают более понятный и элегантный интерфейс, чем неуклюжий механизм ука- зателей». Для уяснения сути дела приведём аналоги двух программ из
[9] на СИ и С++, демонстрирующие удобство параметра-ссылки:
#include
1   2   3   4   5   6   7   8   9   10   ...   23


using namespace std;
void swap_latent(int&,int&);
void swap_pointr(int*, int*);
int main()
{ int a, b;
a=1; b=8;
cout<<" a="<b="<swap_latent(a,b);
cout<<" a="<
b="<swap_pointr(&a,&b);
cout<<" a="<
b="<return 0;
}
void swap_latent(int &a, int &b)
{ int c;
c=a;
a=b;
b=c;
}
void swap_pointr(int *a, int *b)
{ int c;
c=*a;
*a=*b;
*b=c;
}
151

Результаты работы этой программы после запуска исполнимого фай- ла, полученного посредством g++ swap.cpp -lm:
a=1
b=8
a=8
b=1
a=1
b=8
Мало кто предпочтёт функцию swap_pointr варианту swap_latent.
Для вызова последней и адреса переменных a и b определять не на- до, и внутри не нужно явно заботиться об операции разыменования, и значок & амперсанда встречается только в заголовке функции. Имен- но он означает, что нужно не копировать значения переменных a и b главной программы в формальные аргументы, а просто работать по ад- ресам фактических, в неявной форме (скрытно) аппелируя к механизму указателей, но формально в тексте, используя только имена перемен- ных. Другими словами, ... — поступать так, как это испокон веков де- лал ФОРТРАН (см. ниже функцию swap_var, не пугая прикладников страшилками-адресов и указателей):
program tpoint8
implicit none interface subroutine swap_poi1(pa,pb); integer, pointer :: pa, pb end subroutine swap_poi1
subroutine swap_poi2(pa,pb); integer, pointer :: pa, pb end subroutine swap_poi2
end interface integer, target :: a, b integer, pointer :: pa, pb a=1; b=9
pa=>a pb=>b write(*,*) ’
a=’,a,’
b=’,b call swap_var(a,b)
write(*,*) ’var:
a=’,a,’
b=’,b write(*,’(40("-"))’)
call swap_poi1(pa,pb)
write(*,*) ’poi1:
a=’, a,’
b=’,b write(*,*) ’poi1: pa=’,pa,’
pb=’,pb write(*,’(40("-"))’)
call swap_poi2(pa,pb)
write(*,*) ’poi2:
a=’, a,’
b=’,b write(*,*) ’poi2: pa=’,pa,’
pb=’,pb end
152
subroutine swap_var(a,b)
implicit none integer a,b,c c=a; a=b; b=c end subroutine swap_poi1(pa, pb)
implicit none integer, pointer :: pa, pb integer c c=pa;
pa=pb; pb=c end subroutine swap_poi2(pa, pb)
implicit none integer, pointer :: pa, pb, pc pc=>pa;
pa=>pb; pb=>pc end
Результаты работы tpoint8:
a=
1
b=
9
var:
a=
9
b=
1
---------------------------------------- poi1:
a=
1
b=
9
poi1: pa=
1
pb=
9
---------------------------------------- poi2:
a=
1
b=
9
poi2: pa=
9
pb=
1
В tpoint8 вызываются три подпрограммы swap_var, swap_poi1 и swap_poi2, реализующие алгоритмы обмена, ранее продемонстриро- ванные программой tpoint7.
1. swap_var(a,b) реализует обычный алгоритм обмена содержимым аргументов.
2. swap_poi1(pa,pb) — реализует (по сути) тот же алгоритм, но в ка- честве аргументов использует указатели на переменные a и b глав- ной программы.
3. swap_poi2(pa,pb) — обменивает ссылками аргументы-указатели.
4. Конечно, программируя на ФОРТРАНе, вряд ли кто-то предпочтёт вместо процедуры swap_var процедуру swap_poi1 (разве что в качестве простого упражнения по вызову процедур с аргументами- указателями).
153

5. Более того, при использовании swap_poi1 и swap_poi2 (в каче- стве внешних процедур) архиважно явно описать в главной про- грамме их интерфейс:
interface subroutine swap_poi1(pa,pb); integer, pointer :: pa, pb end subroutine swap_poi1
subroutine swap_poi2(pa,pb); integer, pointer :: pa, pb end subroutine swap_poi2
end interface
При исключении его из текста на этапе комиляции получим сооб- щения вида:
t1.f95:18.14:
call swap_poi1(pa,pb)
1
ошибка: Dummy argument ’pa’ of procedure ’swap_poi1’ at (1) has an attribute that requires an explicit interface for this procedure ...
6. В случае программы на С++ при вызове swap_latent в качестве аргументов явно указывались имена переменных главной програм- мы, и только из описания прототипа функции swap_latent (её ин- терфейса) главная программа узнавала, что имена аргументов внут- ри функции следует толковать как параметры-ссылки. В случае
ФОРТРАНа, когда формальными аргументами swap_poi1 явля- ются именно указатели, нельзя при её вызове поступить как в С++
— указать в качестве фактических аргументов имена переменных a и b, хотя значения указателей и являются их синонимами. Так в программе tpoint8 вызов call swap_poi1(pa,pb) нельзя заменить на вызов call swap_poi1(a,b) — при компиляции получим:
tpoint8.f95:18.15:
call swap_poi1(a,b)
1
ошибка: Actual argument for ’pa’ must be a pointer at (1)
ФОРТРАН, в данном случае, требует полного соответствия атрибу- тов фактического и формального аргументов.
154

7. Формальные аргументы swap_var(a,b) были описаны без атрибу- та target. Его можно включить в их описание. Однако, тогда по- требовалось бы включить в интерфейсный блок и явное описание интерфейса swap_var(a,b). Вообще говоря, при переходе на вер- сии современного ФОРТРАНа всегда полезно явно указывать ин- терфейс всех вызываемых процедур (как — решать нам: либо по- средством оператора interface, либо модульно).
8. Вызов swap_poi2 тоже можно рассматривать как соответствую- щее упражнение по освоению работы с указателями в ФОРТРАНе.
Однако, именно обмен указателей ссылками при решении соответ- ствующих задач способен реально сократить затраты процессорного времени. Так, если бы a и b были массивами с большим количеством элементов, то время обмена данными между ними в случае работы аналогов swap_var и swap_poi1 было бы пропорционально этому количеству. Осуществляя же один обмен ссылками двух указателей,
адресующих эти массивы, можем использовать вместо одного мас- сива другой практически без каких-либо временн ´
ых затрат, касаю- щихся пересылки их элементов (см. следующий пункт).
155

3.4.9
Достоинство работы с указателями
Использование указателей требует неослабного внимания, что, конечно,
вряд ли можно отнести к достоинству. Однако, временн ´
ые затраты при работе с указателями могут оказаться значительно меньше временных затрат программы, работающей без указателей. Рассмотрим задачу об- мена содержимым двух одномерных массивов.
В старых версиях ФОРТРАНа не было типа данного указатель. По- этому обмен массивов своим содержимым свелся бы к полномасштабной перекачке содержимого одного массива в другой. Программа tpoint9
(приведена ниже) вызывает процедуры swap1 и swap2, которые оцени- вают временн ´
ые затраты двух вариантов kmax=100001-кратной пере- качки массивов из n=100000 элементов целого типа.
Оценка временн ´
ых затрат осуществлется и процедурой swap3, кото- рая, как бы, имитирует обмен содержимым двух массивов посредством обмена значениями соответствующих указателей на эти массивы.
program tpoint9; use swap; implicit none integer i write(*,’(7x,a,i10)’) ’ kmax=’,kmax write(*,’(7x,a,i10)’) ’
n=’,n a=(/(i,i=1,n)/)
! Задание значений b=(/(i,i=n,1,-1)/)
! векторов a и b.
call swap1
! с=a; a=b; b=c call swap2
! цикл по обмену каждого из n элементов call swap3
! обмен обычными ФОРТРАН-ссылками на вектора end
Получение исполнимого файла достигается командой gfortran swap.f95 tpoint9.f95
Оптимизация -O0
Оптимизация -O1
! Результаты
! программы kmax=
100001
kmax=
100001
!
tpoint9
n=
100000
n=
100000
swap1: time= 0.923E+01
swap1: time= 0.927E+01
swap2: time= 0.503E+02
swap2: time= 0.111E+02
swap3: time= 0.717E-03
swap3: time= 0.751E-03
Если требование о копировании данных можно заменить копированием ссылок на данные, то преимущество указателей очевидно.
156

Исходный текст модуля swap module swap; implicit none integer, parameter ::
n=100000
! количество элементов в массивах integer, parameter :: kmax=100001
! количество повторов обмена integer, target :: a(n), b(n)
integer, pointer :: pa(:), pb(:), pc(:)
contains subroutine swap1
! пересылка массивов без integer c(n), k
! цикла по всем их элементов real t1, t2
call cpu_time(t1);
do k=1,kmax; c=a; a=b; b=c; enddo call cpu_time(t2);
write(*,’(a,e10.3)’) ’ swap1: time=’,t2-t1
end subroutine swap1
subroutine swap2
! пересылка массивов с integer k, i, m
! циклом по всем их элементам real t1, t2
call cpu_time(t1);
do k=1,kmax do i=1,n m=a(i); a(i)=b(i); b(i)=m enddo enddo call cpu_time(t2); write(*,’(a,e10.3)’) ’ swap2: time=’,t2-t1
end subroutine swap2
subroutine swap3
integer k, i, m real t1, t2
pa=>a
! Настройка pa и pb.
pb=>b
!
call cpu_time(t1);
do k=1,kmax
! kmax-кратный обмен pc=>pa; pa=>pb; pb=>pc
!
pa и pb ссылками.
enddo call cpu_time(t2); write(*,’(a,e10.3)’) ’ swap3: time=’,t2-t1 ! Его время end subroutine swap3
end module swap
Ещё м´
еньших затрат времени можно достичь, используя, так называе- мые, целочисленные ФОРТРАН-указатели.
157

О целочисленных показателях.
Целочисленные указатели обеспечива- ют переход к СИ-указателям при совмещённом программировании, хотя могут использоваться и сами по себе в ФОРТРАН-программах.
В ФОРТРАНе-90 их нет, но в ФОРТРАН-95 и gfortran они включе- ны. В gfortran использование целочисленных указателей требует при компиляции включения опции -fcray-pointer. Целочисленные указате- ли всегда имеют тип integer(4).
1. Значение целочисленного указателя (т.е. именно адреса) можно вы- вести в виде некоторого целого числа.
2. При объявлении целочисленного указателя имя типа (на значение которого ссылается указатель) не конкретизируется — только слово pointer за которым в круглых скобках следует пара pointer(имя_указателя, имя_адресной_переменной).
Адресная переменная (см. [4, 5]) связана с областью памяти, адрес которой установлен в указатель, так что через посредство указателя
Значения из адресной переменной передаются адресату.
Значения из адресата передаются адресной переменной.
program tpoint10; implicit none; integer, parameter :: n=5
! Пример integer i real(8) ::
a(n)=(/(dble(i),i=1,n)/)
real(8) :: var(n), x
! var и х - адресные переменные, на которые pointer (p,var), (q,x)
!
нацеливаются указатели p и q.
p=loc(a)
! loc(a) находит адрес (a) в виде целого числа write(*,*) ’1)’
p=loc(a)
write(*,*) p do i=1,n
! Тут просто выводим значение write(*,*)
p, loc(a(i)), a(i)
! указателя p, вычисляемое по p=p+8
p=p+8
! и через loc(a(i)), а затем и enddo
! значение a(i), адресуемое p write(*,*) ’2)’
! Однако, если хотим осуществить p=loc(a)
! аналогичный вывод адресной write(*,*) p
! переменной (т.е. через приращение,
do i=1,n
! указателя), то помним, что её
write(*,*)
p, loc(var(1)), var(1) ! элементы адресуются относительно p=p+8
! адреса её начала, каковым является enddo
! в данном случае элемент var(1).
158
write(*,*) ’3)’
! При формальной замене во фрагменте p=loc(a)
! 1) обозначения a(i) на var(i)
write(*,*) p
! результат (кроме var(1))
do i=1,n
! не совпадёт с результатом 1),
write(*,*)
p, loc(var(i)), var(i) ! так как в качестве начального p=p+8
! адреса var будет использован не enddo
! loc(a(1)), а loc(a(i)+8) !!!
write(*,*) ’4)’
! Здесь целочисленный указатель параметр - цикла.
do q=loc(a),loc(a)+32,8
! Поскольку с q связана адресная переменная x, и,
write(*,*) q, loc(x), x ! в q послан "адрес" массива A, то теперь x --- enddo
! просто другое имя текущего элемента a(i).
end program tpoint10
Получение исполнимого файла и его активация gfortran -fcray-pointer tpoint10.f95 -o tpoint10
./tpoint10 > tpoint10.res
1)
! Результат работы tpoint10 6299776 6299776 6299776 1.0000000000000000 6299784 6299784 2.0000000000000000 6299792 6299792 3.0000000000000000 6299800 6299800 4.0000000000000000 6299808 6299808 5.0000000000000000 2)
6299776 6299776 6299776 1.0000000000000000 6299784 6299784 2.0000000000000000 6299792 6299792 3.0000000000000000 6299800 6299800 4.0000000000000000 6299808 6299808 5.0000000000000000 3)
6299776 6299776 6299776 1.0000000000000000 6299784 6299792 3.0000000000000000 6299792 6299808 5.0000000000000000 6299800 6299824 0.0000000000000000 6299808 6299840 0.0000000000000000 4)
6299776 6299776 1.0000000000000000 6299784 6299784 2.0000000000000000 6299792 6299792 3.0000000000000000 6299800 6299800 4.0000000000000000 6299808 6299808 5.0000000000000000 159

Ещё один пример. Программа tpoint11.
program tpoint11; implicit none; integer, parameter :: n=5
real(8)
a(n) /n*0d0/, b(n) /n*8.0_8/
real(8) var (n), x integer i pointer (p,var)
pointer (q,x)
p=loc(a)
write(*,’(a,10f5.1)’) ’
a(1:5)=’,
a(1:5)
write(*,’(a,10f5.1)’) ’
var(1:5)=’,var(1:5)
var(2)=0.3_8
write(*,’(a,10f5.1)’) ’ var( 2 )=0.3 -->
a(1:5)=’,
a(1:5)
write(*,’(a,10f5.1)’) ’
var(1:5)=’,var(1:5)
a(5)=5.7_8
write(*,’(a,10f5.1)’) ’
a( 5 )=5.7 -->
a(1:5)=’,
a(1:5)
write(*,’(a,10f5.1)’) ’
var(1:5)=’,var(1:5)
p=loc(b)
write(*,’(a,10f5.1)’) ’
b(1:5)=’,
b(1:5)
write(*,’(a,10f5.1)’) ’
var(1:5)=’,var(1:5)
var(1:5:2)=(/(i,i=1,5,2)/)
write(*,’(a,10f5.1)’) ’
var(1:5:2)=(/(i,i=1,5,2)/) -->
b(1:5)=’,
b(1:5)
write(*,’(a,10f5.1)’) ’
var(1:5)=’,var(1:5)
q=loc(var)
write(*,*) p,q end
1. В tpoint11 описаны три массива a(5), b(5) и var(5) типа real(8)).
2. На этапе компиляции массивы a и b заполняются константами 0.0
и 8d0 соответственно.
3. Массив var(5) — адресная переменная указателя p.
4. Оператор pointer (p,var) должен располагаться в разделе описа- ний программной единицы
5. Адресную переменную var нельзя инициализировать при описании её типа или в операторе data.
6. p=loc(a): в p помещается указатель на начало массива a.
7. Вывод элементов массивов a и непосредственно, и через адресную переменную var. Имя var(i) элемента адресной переменной — си- ноним имени a(i) до тех пор, пока не переопределим указатель p.
160

8. Изменение var(2) эквивалентно изменению a(2), что подтвержда- ется последующим выводом.
9. Аналогично, и изменение элемента a(5) эквивалентно изменению того, на что ссылается var(5).
10. p=loc(b): переключение указателя p на адресацию массива b.
11. Теперь адресная переменная var адресует элементы массива b, что и подтверждается соответствующим выводом.
Результат работы tpoint11
$ gfortran tpoint11.f95 -fcray-pointer -o tpoint11
$ ./tpoint11
a(1:5)=
0.0 0.0 0.0 0.0 0.0
var(1:5)=
0.0 0.0 0.0 0.0 0.0
var( 2 )=0.3 -->
a(1:5)=
0.0 0.3 0.0 0.0 0.0
var(1:5)=
0.0 0.3 0.0 0.0 0.0
a( 5 )=5.7 -->
a(1:5)=
0.0 0.3 0.0 0.0 5.7
var(1:5)=
0.0 0.3 0.0 0.0 5.7
b(1:5)=
8.0 8.0 8.0 8.0 8.0
var(1:5)=
8.0 8.0 8.0 8.0 8.0
var(1:5:2)=(/(i,i=1,5,2)/) -->
b(1:5)=
1.0 8.0 3.0 8.0 5.0
var(1:5)=
1.0 8.0 3.0 8.0 5.0 161

3.5
О чем узнали из третьей главы? (2-ой семестр)
1. При разработке и написании программ использовать переменные только простых элементарных типов (integer, int, real(8), double,
и т.д) не всегда удобно.
2. И ФОРТРАН, и СИ предоставляют программисту возможность опе- рировать более сложными структурами данных (массивы, струк- туры, указатели).
3. Массив — именованный набор данных, состоящий из конечного ко- личества элементов одинакового типа. Доступ к конкретному эле- менту осуществляется по номеру (индексу) элемента.
4. Производный тип (структура) — именованный набор данных из конечного количества элементов (полей) возможно разных типов.
Доступ к нужному полю осуществляется по имени поля.
5. Имя поля придумываем сами так, чтобы оно мнемонически ясно отражало смысловую нагрузку данного, хранимого в поле.
6. Указатель — тип данных, предназначенный для хранения адреса.
7. Массив называется одномерным (или вектором), если для указа- ния его элемента достаточно одного индекса.
8. Индексация СИ-массива всегда начинается с нуля.
9. Индексация ФОРТРАН-массива (если она не изменена при его описании) всегда начинается с единицы.
10. Размер одномерного массива в СИ обычно указывается в квадрат- ных скобках (называемых конструктором массива) после имени опи- сываемого массива.
11. Размер одномерного массива в ФОРТРАНе можно указать в круг- лых скобках, которые пишутся после имени массива при описании типа его элементов, либо в операторе dimension.
12. Для отделения имени структуры от имени её поля используется: в
СИ точка, а в стандарте ФОРТРАНа-95 значок процента, хотя некоторые компиляторы допускают и точку.
162

13. Доступ к содержимому поля СИ-структуры при использовании ука- зателя на структуру возможен только через операцию разыменова- ния указателя (например, либо ((*a).x), либо a->x, где x — имя поля, a - указатель на структуру).
14. В ФОРТРАНе операция присваивания значения указателю обозна- чается парой символов => (это вовсе не ссылка на поле структуры).
15. Указатели и производные типы данных позволяют создавать в опе- ративной памяти динамические структуры данных.
16. В СИ полезен оператор typedef позволяющий описать синоним су- ществующего типа данных.
163

3.6
Третье домашнее 3a-задание (2-ой семестр)
1. Написать СИ-программу, которая: моделирует координаты точки пространства вектором из трёх элементов, вводит координаты двух точек и вызывает функцию расчёта расстояния между ними.
Описания главной программы и функции должны быть расположе- ны в разных файлах.
2. Написать СИ-программу, которая, используя производный тип struct coord
{
float x, y, z; };
вызывает процедуры:
• rdr_coord ввода данного типа struct coord для чтения коор- динат точки A,
• wrt_coord вывода данного типа struct coord для печати ко- ординат точки A,
• rdr_coord ввода данного типа struct coord для чтения коор- динат точки B,
• wrt_coord вывода данного типа struct coord для печати ко- ординат точки B,
• расчёта расстояния между точками A и B,
• выводит результат расчёта.
Тип struct coord должен быть определён в одном файле;
Описание процедур — в другом файле;
Описание главной программы — в третьем.
Обеспечить ввод исходных данных из файла input и вывод ре- зультатов в файл result.
Обеспечить работу с программой посредством Make-файла.
3. Задача та же (что и 2), ОДНАКО главная программа и процедуры должна использовать вместо типа struct coord тип struct coord*.
164

3.7
Третье домашнее 3b-задание (2-ой семестр)
1. Написать ФОРТРАН-программу, которая, используя тип type coord4
real(4) x, y, z end type coord4
• вызывает процедуру rdr4 ввода данного типа coord4 для чте- ния координат точки A,
• вызывает процедуру wrt4 вывода данного типа coord4 для пе- чати координат точки A,
• вызывает процедуру rdr4 ввода данного типа coord4 для чте- ния координат точки B,
• вызывает процедуру wrt4 вывода данного типа coord4 для пе- чати координат точки B,
• вызывает процедуру dist4 расчёта расстояния между точками
A и B,
• выводит результат расчёта.
Тип type coord4 должен быть определён в одном файле;
Описание процедур — в другом файле;
Описание главной программы — в третьем.
Обеспечить ввод исходных данных из файла input и вывод ре- зультатов в файл result.
Обеспечить работу с программой посредством Make-файла.
2. Задача та же, но поместить описания всех процедур в единицу ком- пиляции типа module.
3. Задача та же, но дополнить module описаниями соответствующих процедур, работающих с координатами типа real(8). Добавить в главную программу вызов новых функций и вывод их результата.
4. Задача та же, но ОБЕСПЕЧИТЬ возможность вызова процедур rdr4 и rdr8 по одному имени rdr; процедур wrt4 и wrt8 по од- ному имени wrt и процедур dist4 и dist8 по одному имени dist.
165

5. Задача. Модифицировать СИ- и ФОРТРАН-решения задачи о счи- талке так, чтобы оперативная память, отведённая изначально для кандидатов на водящего, передавалась по мере их выбывания опе- рационной системе.
6. Написать две СИ-функции расчёта значения полинома n-ой степени по заданным значениям его коэффициентов, хранящихся в массиве,
и аргументу. В первой описание формального параметра-массива оформить в терминах конструкции []; во второй — через указатель.
Сформировать своё мнение о достоинствах и недостатках обоих спо- собов. Продемонстрировать работу соответствующей тестирующей программы.
166

4
Mассивы (введение).
4.1
Уяснение ситуации.
В программировании использовать только простые переменные не всегда удобно. Например, вряд ли удобно описывать 2550 различных перемен- ных для хранения коэффициентов системы линейных уравнений с 50-ю неизвестными. Структура данных массив предоставляет возможность доступа к любому коэффициенту по одному имени.
1. Массив – набор конечного количества нумерованных элементов одинакового типа, расположенных в оперативной памяти непо- средственно друг за другом в порядке возрастания номера.
2. Массив называют одномерным (или вектором), если в его описа- нии указано, что все элементы распределены вдоль одного измере- ния. Поэтому для выборки элемента достаточно указать его поряд- ковый номер (одно целое число; например, a(3) или a(k)).
3. Массив называют двумерным (или матрицей), если в его описании указано, что элементы распределены по двум измерениям (т.е. для выборки элемента массива нужно два целых числа (первое — номер строки матрицы, второе – номер столбца).
4. Массивы могут быть и многомерными. Книгу yдобно моделиро- вать трехмерным массивом. Положение буквы в ней определяется тремя числами — номерами страницы, строки и знака в строке.
5. Массив называют индексированной переменной. Иногда нумера- цию элементов выгодно с нуля или с отрицательного числа. Поэтому вместо слова номер часто используется термин индекс.
6. По адресу начального элемента массива, индексу и типу элемента просто вычислить адрес элемента, указанного индексом. Чем боль- ше индексов имеет элемент массива, тем большее время требуется для расчета его адреса.
7. Число измерений массива называют рангом (или размерностью)
массива, а число элементов массива – его размером (не путаем размер с размерностью). Массив ранга 1 – вектор; ранга 2 – матрица
(допускаются массивы до 7-го ранга включительно).
167

8. Форма(или конфигурация) массива определяется его рангом и количеством элементов по каждому измерению. Это количество на- зывают также протяженностью или экстентом измерения. Напри- мер, описание real b(2,7,10) или же real, dimension (2,7,10) :: b определяют массив ранга 3 и размером 140 элементов (2 × 7 × 10).
9. Конфигурация массива – вектор целого типа, размер которого ра- вен рангу исходного массива, а элементы равны размерам соответ- ствующих измерений. Находит форму встроенная функции shape:
program testshape; implicit none
!
Результат пропуска testshape:
integer conf(2)
!
$ gfortran testshape.f95
real(8) a(-5:5,0:20), b(2,7,10)
!
$ ./a.out conf=shape(a)
!
write(*,’(a,2i3)’)
shape(a)=’, conf
!
shape(a)= 11 21
write(*,’(a,3i3)’) shape(b)=’, shape(b) !
shape(b)=
2 7 10
end
10. Массивы одинаковой конфигурации называют согласованными
(так массивы real(8) d(-3:-2, -3:3,-4:5), b(2,7,10) согласованы).
11. В современном ФОРТРАНе можно задать массив без имени в виде последовательности скаляров посредством конструктора массива:
integer a(3)
! Здесь набор чисел 111, 222, 333, заключённый a=(/111, 222, 333/) ! в символы конструктора массива (/ и /), есть write(*,*) a
! массив без имени. Результат работы:
end
!
111 222 333 12. Секция массива (начиная с ФОРТРАНа -90) – часть массива, за- даваемая посредством имени массива и списка индексов секции:
program testsect; implicit none
! Результаты пропуска testsect:
integer a(3,5)
! $ gfortran a2.f95
integer b(5), c(5), e (3), f(3)
! $ ./a.out do i=1,3
!
11 21 31
do j=1,5
!
12 22 32
a(i,j)=10*i+j
!
13 23 33
enddo
!
14 24 34
enddo
!
15 25 35
write(*,’(3i4)’) a
!
b=a(1,:); write(*,’(a,5i5)’) ’ b=’,b
! b=
11 12 13 14 15
c=a(3,:); write(*,’(a,5i5)’) ’ c=’,c
! c=
31 32 33 34 35
e=a(:,2); write(*,’(a,5i5)’) ’ e=’,e
! e=
12 22 32
f=a(:,4); write(*,’(a,5i5)’) ’ f=’,f
! f=
14 24 34
end
168

13. При описании матрицы первым указывается количество её строк,
а затем количество столбцов. Например, описание integer a(3,5)
означает, что матрица состоит из трёз строк и пяти столбцов. При использовании в программе её элемента a(i,j) первым индексом i указывается индекс строки, а вторым j – индекс столбца. В СИ с точностью до синтаксиса имеем такую же очередность записи раз- мерностей матрицы и индексов её элементов.
Однако СИ-матрица упорядочена в оперативной памяти по по строкам, в то время ФОРТРАН-матрица упорядочена по столб- цам. Именно поэтому в первой строке вывода матрицы a оказались числа 11, 21, 31, которые расположены в её первом столбце.
14. В общем случае размещение в оперативной памяти многомерного
ФОРТРАН-массива происходит по правилу:
чем левее индекс, тем быстрее он меняется.
program test3d; implicit none ! Результаты работы test3d:
integer a(4,3,2)
! $ gfortran test3d.f95 ! Наряду с кратными do i=1,4
! $ ./a.out
! циклами DO
do j=1,3
!
! в ФОРТРАНе-95
do k=1,2
!
111 211 311 411
! можно использовать a(i,j,k)=100*i+10*j+k
!
121 221 321 421
! оператор forall enddo
!
131 231 331 431
enddo
!
112 212 312 412 forall (i=1:4,j=1:3,k=1:2)
enddo
!
122 222 322 422
a(i,j,k)=100*i+10*j+k write(*,’(4i4)’) a
!
132 232 332 432 endforall end
Размещение же СИ-массивов следует правилу:
чем правее индекс, тем быстрее он меняется.
#include
//
Результат работы:
int main()
//
111 112 121 122 131 132
{ int i, j, k, a[4] [3] [2];
//
211 212 221 222 231 232
int *pa;
//
311 312 321 322 331 332
for(i=0;i<=3;i++)
//
411 412 421 422 431 432
for(j=0;j<=2;j++)
for(k=0;k<=1;k++)
a[i][j][k]=100*(i+1)+10*(j+1)+(k+1); pa=&a [0] [0] [0];
for (i=1;i<=4;i++)
{printf("%4d %4d
%4d %4d
%4d %4d\n", *pa,
*(pa+1),
*(pa+2), *(pa+3),
*(pa+4), *(pa+5));
pa=pa+6;
}
return 0;
}
169

15. В СИ многомерный массив описывается последовательным указа- нием его конструкторов (квадратных скобок) по очередному изме- рению int a[4] [3] [2]. В ФОРТРАНе integer a(4,3,2).
16. Совпадение размеров СИ- и ФОРТРАН-массивов (когда размер по- следнего указан просто числом элементов) не означает совпадения значений соответствующих индексов. Индексация СИ-массива все- гда начинается с нуля, а ФОРТРАН-массива (если не оговорено иное) — с единицы. Так последний элемент СИ-массива int a[4]
[3] [2] доступен по имени a[3][2][1], а Фортран-массива integer a(4,3,2) — по имени a(4,3,2).
17. Доступ к элементу СИ-массива можно выразить, как заключая со- ответствующие индексы в квадратные скобки, так и используя эти индексы в адресных выражениях языка указателей. Например,:
#include
int main()
{ double a[2][3];
int i, j; a[0][0]=
0;
a[0][1]=
1;
a[0][2]=
2;
a[1][0]= 10;
a[1][1]= 11;
a[1][2]= 12;
for (i=0; i<=1;i++)
{ printf("%5.1lf %5.1lf %5.1lf\n",
a[i][0],
a[i][1],
a[i][2]);
printf("%5.1lf %5.1lf %5.1lf\n", *(
a[i]+0),*(
a[i]+1),
*( a[i]+2));
printf("%5.1lf %5.1lf %5.1lf\n\n",*(*(a+i)+0),*(*(a+i)+1),*(*(a+i)+2));
}
for (i=0; i<=1;i++)
{ printf("%p %p %p\n",
&(a[i][0]),
&(a[i][1]), &(a[i][2]));
printf("%p %p %p\n",
a[i]+0 ,
a[i]+1,
a[i]+2);
printf("%p %p %p\n\n", *(a+i)+0 , *(a+i)+1, *(a+i)+2);
}
return 0; }
В случае двумерного массива обозначение a[i] трактуется как адрес начального элемента i-ой строки. Имя матрицы (a) можно тракто- вать как указатель на начальный элемент одномерного массива с элементами a[0], a[1] и т.д., которые хранят адреса нулевых эле- ментов каждой из строк.
Запись *(a[i]+2) означает:
1) a[i] — адрес начального байта i-ой строки;
170

2) a[i]+2 — увеличить его на количество байт 2*sizeof(double)
необходимое для размещения двух ее элементов (0-го и 1-го), находя,
тем самым, адрес элемента a[i][2];
3) *(a[i]+2) — через операцию разыменования адреса a[i]+2 полу- чить доступ к его содержимому.
Запись *(*(a+i)+2) означает:
1) a — получить адрес начального байта нулевой строки (адрес,
хранящийся в a[0]);
2) a+i — увеличить его на i*sizeof(double) байт, находя адрес эле- мента a[i], который хранит адрес начального элемента i-ой строки матрицы;
3) *(a+i) — извлекаем из a[i] через операцию разыменования (внут- ренняя *) адрес начального элемента i-ой строки;
4) *(a+i)+2 — вычисляем адрес элемента a[i][2] по формуле
&a[0]+i*sizeof(double) +2*sizeof(double);
5) *(*(a+i)+2) через операцию разыменования найденного адреса получаем доступ к содержимому элемента a[i][2].
Результат работы последней программы:
0.0 1.0 2.0 0.0 1.0 2.0 0.0 1.0 2.0 10.0 11.0 12.0 10.0 11.0 12.0 10.0 11.0 12.0
// Легко заметить, что
0x7fff07a6ed10 0x7fff07a6ed18 0x7fff07a6ed20 // адрес элемента a[0][1]
0x7fff07a6ed10 0x7fff07a6ed18 0x7fff07a6ed20 // на 8 байт больше адреса
0x7fff07a6ed10 0x7fff07a6ed18 0x7fff07a6ed20 // элемента a[0][0].
// П О Ч Е М У (на первый
0x7fff07a6ed28 0x7fff07a6ed30 0x7fff07a6ed38 // взгляд) адрес a[0][2]
0x7fff07a6ed28 0x7fff07a6ed30 0x7fff07a6ed38 // больше адреса a[0][1]

1   ...   4   5   6   7   8   9   10   11   ...   23