Слайд 7Далее идут сообщения об адресах переменных f и q и
c помощью последнего вызова функции printf выводится значение указателя со
снятой ссылкой *t, равное 5.
Однако в данной программе имеется серьезная ошибка, которая может нарушить нормальную работу компьютера. Укажем каким образом она проявляется. Данная ошибка связана с тем, что в программе описан указатель s на переменную типа int. В таком случае компилятор отводит память в два байта для хранения адреса. Однако сама переменная, значение которой должно храниться по этому адресу, не описана и, следовательно, компилятор не отведет соответствующего объема памяти для хранения ее значения. Так как значением является целое число, то для его хранения необходимо два байта оперативной памяти. В подобной ситуации, когда программист хочет по адресу, описываемому ссылкой, поместить целое значение, как в нашем примере, то он сам он сам должен позаботиться о выделении необходимого объема памяти для этой цели. Таким образом, ответственность за инициализацию указателей ложится на самого программиста.
Представленный пример демонстрирует то, что существует проблема инициализации указателей, которую в любой конкретной программе, решающей некоторую прикладную задачу, обязан решать сам программист.
И, наконец, основной вывод, вытекающий из рассмотренного примера, состоит в том, чтобы пользователь (программист) усвоил и надежно зафиксировал в своей памяти то, что любое описание переменной-указателя в программе не приводит к его инициализации разумным значением и также к резервированию памяти для значения переменной, хранящейся по адресу, указанному им.
Слайд 8 Сама проблема задания начального значения переменной-указателю, не
противоречащему смысловому содержанию решаемой задачи, может быть решена несколькими способами.
Первый способ состоит в том, что переменную указатель можно описать как статическую переменную (static). Тогда ей автоматически присваивается нулевой адрес памяти. Кроме того, программист, перед тем как начнет пользоваться указателем, обязан выделить память под значение, которое будет храниться по этому адресу.
Второй способ основан на том, чтобы указателю присвоить адрес уже описанной переменной. Поскольку компилятор отводит память в момент описания, то описанная выше процедура гарантирует, что необходимый объем памяти будет автоматически отведен. Естественно, что после проведения операции снятия ссылки над таким указателем его значение будет совпадать со значением переменной, адрес которой присваивался указателю.
Третий способ таков: указателю присваивается значение другого указателя, который к этому моменту уже инициализирован. При этом одному адресу памяти как бы даются разные имена (идентификаторы указателей). Если значение, на которое будут ссылаться оба указателя, изменится с помощью оператора присваивания, где используется один из указателей, то изменится и значение, получаемое при помощи другого указателя.
Рассмотренная в последнем случае ситуация называется двойным указанием (один объект имеет два различных имени) и может приводить к определенным проблемам. Рассмотрим до некоторой степени следующий искусственный пример, иллюстрирующий возможные неприятности, которые обусловлены двойным указанием.
Слайд 9 Программа 21:
#include
#include
void main (void)
{
extern void printval (int , int , int);
int *aa = (int *) malloc (sizeof (int)),
*bb = (int *) malloc (sizeof (int)),
*cc = (int *) malloc (sizeof (int)),
*aa = 144;
*bb = 155;
*cc = 166;
printval (*aa, *bb, *cc);
*cc = *aa;
printval (*aa, *bb, *cc);
bb = aa; /* Двойное указание */
printval (*aa, *bb, *cc);
*aa = -144;
*bb = -155;
*cc = -166;
printval (*aa, *bb, *cc);
printf ("\n");
}
Слайд 10void printval (int a, int b, int c)
{
printf ("\n a = %d b = %d c =
%d", a, b, c);
}
Оператор описания типа int *aa = (int *) malloc (sizeof (int)) приводит к тому, что переменная aa описана, как указатель на целое, и начальное значение адреса этого указателя равно адресу, которое возвращается функцией malloc, содержащейся в библиотеке
.
Результаты работы программы таковы. При первом вызове функции printval будет выведена следующая информация:
a =144 b = 155 c = 166
в соответствии с тремя операторами присваивания, выполненными до этого момента.
Выполнение оператора *cc = *aa приведет к тому, что значения *cc и *aa будут одинаковы, и поэтому в результате второго обращения к функции printval получим
a =144 b = 155 c = 144
как следует из комментария программы, стоящего после оператора bb = aa, выполнение которого будет обозначать, что переменные-указатели bb и aa имеют одно и то же значение, а именно адрес одной и той же ячейки оперативной памяти. Следовательно, операции снятия ссылки, проведенные над bb и aa, дадут один и тот же результат, о чем свидетельствует информация, полученная при третьем обращении к функции printval
a =144 b = 144 c = 144
Далее в программе проводится обновление значений по адресам соответствующих переменным-указателям aa, bb и cc операторами присваивания, которые меняют
Слайд 11начальные значения на отрицательные. Однако получается неожиданный результат
a = -155 b
= -155 c = -166
такой результат является следствием работы оператора bb = aa потому, что операторы присваивания *aa = -144 и *bb = -155, выполняемые последовательно, обновляют значение по одному и тому же адресу и, следовательно, значение, равное -155, будет выведено с помощью операции снятия ссылки, применяемой к переменным-указателям aa и bb. Такого рода неожиданные результаты являются следствием двойного указания, что, несомненно, демонстрирует важность процедуры инициализации переменных-указателей, которую программист должен проводить аккуратно и с особой тщательностью.
Четвертый способ задания начального значения указателю основан на использовании функции автоматического распределения памяти malloc из библиотеки
. она имеет следующее описание:
void *malloc (int size)
Она отводит size байт памяти. При этом если для запрашиваемой переменной отводимой памяти не достаточно, то она возвращает нулевое значение, в противном случае возвращает указатель на блок выделяемой памяти. После использования выделенной памяти ее необходимо освободить, что осуществляется вызовом функции free, описанной следующим образом
void free (void *block);
Эта функция применяется в паре с функцией malloc.
Слайд 12 Следующая распространенная ошибка, связанная с указателями непосредственно
связана с присвоением переменной-указателю адресного значения (конкретный адрес какой-либо ячейки
памяти). Компилятор не запрещает такие операции, но, тем не менее, подобными вещами пользоваться нужно аккуратно, поскольку таким образом можно вторгнуться в ту часть оперативной памяти, где храниться информация, обеспечивающая работу операционной системы и тем самым нарушить нормальную работу самого компьютера.
Еще один вид ошибок связан с возвратом адресов автоматических переменных функциями. Это так же может привести к сбою в работе программного обеспечения, установленного на Вашем компьютере.
Отметим, что применение переменных-указателей требует жесткой дисциплины при выполнении правил их использования, а также особой тщательности и внимания при анализе различного рода программных ситуаций.
Ссылки и массивы. Достаточно часто при решении конкретных задач возникает ситуация, когда набор значений одной и той же величины должен выступать в роли одного и того же объекта программы. Известно, что для скалярных величин одному значению в программе соответствует какой-либо идентификатор. Ну, а как же быть выше упомянутом случае? Для этого во всех языках программирования вводится сложный тип данных – массив. Под массивом понимается некоторый однотипный набор значений, который в программе описывается одним именем, а для того, чтобы указать на один из элементов данного набора, как правило, используется индексная переменная. Ее значение определяет порядковый номер элемента в рассматриваемом наборе.
Слайд 13 Подобным же образом вводится понятие массива в
языке СИ, однако благодаря наличию указателей возможным стало организовать новую
технологию работы с его элементами.
Согласно основному правилу языка СИ, все переменные должны быть описаны, в том числе и массивы, для которых оператор описания имеет вид
<тип> имя массива [размер массива];
В данной конструкции под словом <тип> понимается одно из ключевых слов, указывающий на тип принимаемых значений элементами массива. Под именем массива следует понимать идентификатор, который как бы выделяет его среди других объектов программы. В квадратных скобках, следующих за идентификатором, указывается размер массива – количество элементов, входящих в него. Приведем примеры операторов описания массивов:
double s[375];
int i[131], k[15];
char text [38], text1 [12];
В первом операторе массив с именем s содержит в себе 375 элементов, которые принимают значения типа double. Компилятор обрабатывая такой оператор, отведет для хранения всех элементов массива память объемом в 3000 байт, так как для хранения одного элемента требуется 8 байт. При обработке второго оператора будут введены в программе два массива, состоящих из 131 и 15 элементов и принимающих целочисленные значения, с именами i и k соответственно. Для хранения всех элементов массива i будет отведена память объемом 262, а для массива k – 30 байт. Как уже говорилось массивы индексируются для того, чтобы обеспечить возможность доступа к его отдельному элементу.
Слайд 14 В языке СИ по умолчанию массивы индексируются
с нуля. Так, например, для 375 элементов массива s имеем:
s[0], s[1], …, s[374]. Для того чтобы сослаться на какой-либо j-ый элемент массива, необходимо указать имя массива и следом за ним в квадратных скобках его номер (т. е. j-1). Учитывая что индексация начинается с нуля, имеем s[j-1].
Примеры:
i [35] – тридцать шестой элемент массива i.
text [8] – девятый элемент массива text.
Таким образом, с помощью индексной переменной, принимающей целочисленные значения, можно обращаться в программе к отдельному элементу любого массива. Такая технология работы с массивами и его элементами используется во всех языках программирования высокого уровня: fortran, pascal, basic и другие. Приведем примеры программ, иллюстрирующие такую технологию работы с массивами.
Задача. Требуется найти сумму всех элементов массива, принимающих значения типа float. Количество элементов массива задано.
Программа 22:
#include
#define NSIZE 20 /* Максимальный размер массива */
viod main (void)
{
int i, n;
float s [NSIZE], sum;
printf ("\nВведите количество элементов массива n = ");
l1:scanf )"%d", &n);
Слайд 15
if ((n NSIZE))
{
printf
("\n0 < n
= ", NSIZE);
goto l1;
}
sum = 0;
for (i=0; i<=n; i++)
{
printf ("Введите элемент массива s[%d] = ", i);
scanf ("%f", &s[i]);
sum = sum + s[i];
}
printf ("\nСумма элементов массива sum = %f", sum);
}
Основная идея алгоритма заключается в том, чтобы организовать цикл, с помощью которого, используя индексную переменную i, программа вводила бы очередной элемент массива и одновременно накапливала бы сумму его элементов в переменной sum.
Другой способ работы с элементами массива основан на использовании указателей. Такая возможность обусловлена тем, что имя массива является константой-указателем на адрес ячейки памяти, занимаемой первым элементом рассматриваемого массива.
Слайд 16
Здесь важно усвоить, что имя массива – константа-указатель и ему
нельзя присваивать значение какой-либо другой переменной-указателя. Ее значение (начальный адрес
первого элемента массива) определяется самим компилятором в момент обработки оператора описания массива, и оно уже никогда в процессе работы программы не может изменить свое значение. Данную особенность (связь имени массива со ссылками) нужно четко понимать для того, чтобы грамотно и эффективно использовать ссылки при работе с элементами массива.
В памяти ПЭВМ элементы массива хранятся последовательно, а поскольку начальный адрес массива соответствует первому его элементу, то в соответствии с типом принимаемых значений можно указать (найти) адреса ячеек, где хранятся других элементов массива, кроме первого. Разница в адресах будет кратна количеству байт, отводимых под хранение значения данного типа. Ранее шла речь об одномерных массивах, элементы которых можно описать одной индексной переменной. По аналогии можно ввести многомерные массивы, требующие для своего описания одно имя и несколько индексных переменных. Широкое применение нашли двумерные массивы, математическим аналогом которых являются таблицы значений, или матрицы. Многомерные массивы объявляются следующим образом
<тип> имя массива [размер по 1 инд. пер.][размер по 2 инд. пер.] …[размер по k инд. пер.];
В отличие от описания одномерного массива здесь в квадратных скобках должны указываться размерности по каждой индексной переменной. Рассмотрим такие примеры:
int l [15][10];
float b [35][18][41];
Слайд 17 В первом операторе объявляется целочисленный массив l, содержащий 150
элементов. Второй оператор вводит трехмерный массив, состоящий из 25830 вещественных
элементов. Укажем порядок следования элементов в памяти ПЭВМ для массивов
float c [3][4];
char tx [2][3][4];
c[0]0[], c[0][1], c[0][2], c[0][3],
c[1][0], c[1][1], c[1][2], c[1][3],
c[2][0], c[2][1], c[2][2], c[2][3]
и
tx[0][0][0], tx[0][0][1], tx[0][0][2], tx[0][0][3],
tx[0][1][0], tx[0][1][1], tx[0][1][2], tx[0][1][3],
tx[0][2][0], tx[0][2][1], tx[0][2][2], tx[0][2][3],
tx[1][0][0], tx[1][0][1], tx[1][0][2], tx[1][0][3],
tx[1][1][0], tx[1][1][1], tx[1][1][2], tx[1][1][3],
tx[1][2][0], tx[1][2][1], tx[1][2][2], tx[1][2][3]
В частности, имя двумерного массива является указателем на первую строку этого массива.
Представим пример программы, иллюстрирующий работу с двумерными массивами посредством технологии, основанной на индексных переменных. Программа решает задачу о вычислении произведения двух прямоугольных матриц по правилу
Слайд 18 Программа 23:
#include
#define N 5
#define L 4
#define M
3
void main (void)
{
int i, j, k;
float a[N][L], b[L][M], c[N][M], p;
printf ("\n");
for (i=0; i for (j=0; j {
printf ("Введите элемент a[%d][%d] = ", i, j);
scanf ("%f", &p);
a[i][j] = p;
}
printf ("\n");
for (i=0; i for (j=0; j {
printf ("Введите элемент b[%d][%d] = ", i, j);
scanf ("%f", &p);
b[i][j] = p;
Слайд 19
}
printf ("\n");
/* Вычисляем элементы матрицы c
*/
for (i=0; i
j++)
{
c[i][j] = 0;
for (k=0; k {
c[i][j] = c[i][j] + a[i][k]*b[k][j];
printf ("Элемент матрицы c[%d][%d} = %f\n",I, j, c[i][j]);
}
}
}
В данной программе в теле первых двух вложенных циклов вводятся элементы матрицы a, вторая группа циклов осуществляет ввод элементов матрицы b, а в последней группе трех вложенных циклов насчитываются элементы матрицы c и выводятся на экран монитора.
Операции над ссылками. Обсудим основные операции над указателями, которые определены в языке СИ. Для этого введем переменные s и ps, описанные оператором
int s, *ps;
Слайд 20
Здесь s – переменная целого типа, а p – переменная-указатель
на целый тип. Тогда для снятой ссылки *ps допустимы все
операции, что и для переменной типа int, в частности
s = *ps + 3;
*ps = s;
*ps -= 2;
(*ps)++;
Таким образом, там, где по смыслу должна применяться переменная целого типа, пользователь имеет полное право употреблять вместо нее *ps. Данное правило справедливо для указателей с точностью до типа значений, применяемых переменной, на которую он (указатель) ссылается.
Над указателями также могут выполняться операции целочисленной арифметики. Данная особенность, как правило, связана с использованием массивов в программе. Мы уже знаем, что имя массива – указатель первого его элемента (хранит адрес ячейки, занимаемой им). Допустим, что программа работает с массивом
float d[28], *pd, e;
Оператор
pd = &d[0];
заносит на хранение в переменную с именем pd адрес нулевого элемента массива d, а оператор
e = *pd;
переменной e присваивает значение нулевого элемента массива, используя операцию снятия ссылки. Однако подобные операции можно проделать иначе:
Слайд 21pd = d;
e = *pd;
поскольку переменная pd указывает на нулевой
элемент массива d, то по определению pd+1 будет указывать на
следующий элемент, а pd+j на j-ый после d [0]. Таким образом, pd+j будет являться адресом j-го элемента массива d, а *(pd+j) соответствовать d[j]. Например, оператор
e = *(pd+j);
эквивалентен следующему:
e = d[j];
аналогичным образом можно использовать имя массива, так как d+j является адресом j-го элемента массива d, и для получения его содержимого применить операцию снятия ссылки *(d+j). В силу данного правила операторы
e = *(d+j);
и
e = d[j];
эквивалентны по своему действию. Адреса различных элементов массива будут отличаться на целое число, кратное количеству байт, которые требуются для хранения одного элемента. Вообще для указателей допустимы операции, представленные в следующей таблице:
Слайд 22Таблица 1.2.1
Операции над указателями
Таким образом, описанная группа
операций позволяет манипулировать элементами массивов, используя указатели. Для иллюстрации работы
операций над указателями представим программу, вычисляющую сумму элементов заданного массива.
Слайд 23 Программа 24:
#include
#define NSIZE 20 /*
Максимальный размер массива */
void main (void)
{
int
i, n;
float s[NSIZE], sum;
printf ("\nвведите число элементов массива n = ");
l1:scanf ("%d:, &n);
if ((n <= 0) || (n > NSIZE))
{
printf ("\n0 <= n <= %d.", NSIZE);
goto l1;
}
sum = 0;
for (i=0; i {
printf ("Введите элемент массива s[%d] = ", i);
scanf ("%f", s+i);
sum += *(s+i);
}
printf ("\nСумма элементов массива sum = %f\n", sum);
}
Слайд 24
Для двумерного массива, объявленного оператором
int date0 [10][15], I, j;
имя массива
date0 является указателем на его первую строку. Выражение вида
*(date0+j)
задает указатель
на строку, отстоящую от первой на j строк. Следовательно, выражение
*(*(date0+i)+j)
эквивалентно более привычному для нас способу обращения к элементу, стоящему в (i+1)-ой строке (j+1)-ом месте, а именно
date0[i][j]
С учетом этого представим новую версию программы 23 о вычислении произведения двух прямоугольных матриц.
Программа 25:
#include
#define N 5
#define L 4
#define M 3
void main (void)
{
int i, j, k;
float a[N][L], b[L][M], c[N][M], p;
printf ("\n");
for (i=0; i for (j=0; j
Слайд 25 {
printf ("Введите элемент
a[%d][%d] = ", i, j);
scanf ("%f",
&p);
*(*(a+i)+j) = p;
}
printf ("\n");
for (i=0; i for (j=0; j {
printf ("Введите элемент b[%d][%d] = ", i, j);
scanf ("%f", &p);
*(*(b+i)+j) = p;
}
printf ("\n");
/* Вычисляем элементы матрицы c */
for (i=0; i for (j=0; j {
*(*(c+i)+j) = 0;
for (k=0; k {
*(*(c+i])+j) += *(*(a+i)+k]*(*(*b+k)+j));
Слайд 26
printf ("Элемент матрицы c[%d][%d} =
%f\n",i, j, *(*(c+i)+j));
}
}
}
Ссылки в качестве параметров функций. В роли передаваемых параметров функций могут выступать обычные переменные и указатели (ссылки). В языке СИ имеется два способа передачи параметров: первый известен как способ передачи по значению, а второй – по ссылке. Рассмотрим их на примере задачи: найти значение функции