// Создание объектов типа int
int i1= 1, i2= 2;
// Создание указателя-константы
int* const pInt 1 =&i 1;
// Попытка изменения указателя-константы является ошибкой
pInt 1 =&i 2; // ошибка
// Изменение объекта с адресом pInt 1 разрешается
i 1 =1 2;
По определению, имя массива является указателем-константой, содержащим адрес его элемента с номером нуль. Этот адрес также называется адресом массива и используется для получения доступа к его элементам. По определению, адресом элемента с номером k массива ar является значение выражения ar+k. Чтобы получить элемент массива с номером k требуется разыменовать его адрес. То есть *(ar+k) – элемент массива с номером k.
Изложенный способ вычисления адреса k -ого элемента массива является следствием из правила, в соответствии с которым к указателю можно прибавлять или вычитать из него объект типа int. Если p – адрес объекта типа Type, а k – объект типа int то результатами вычислений выражений p+k и p-k будут адреса
p+k*sizeof (Type)и p-k*sizeof (Type).
Таким образом, конструкция p+k будет адресом элемента типа Type, находящимся в памяти за элементом с адресом p на расстоянии k от него. Конструкция p-k будет адресом элемента типа Type, находящегося перед элементом с адресом p на расстоянии k от него.
Более того, операции ++ и -- можно использовать для получения адресов следующего и предшествующего объектов. То есть ar ++ и ar —означают то же, что и ar+1 и ar-1.
С другой стороны, если ar+k и ar+k+j – адреса элементов массива ar с номерами k и k+j, j>0, то результатом вычисления выражения (ar+k+j) - (ar+k) будет объект j типа int. Именно он является решением уравнения (ar+k) +x= (ar+k+j).
Пусть p1 и p2 – указатели на один и тот же тип. Тогда к ним разрешается применять операции сравнения. Если, например, адрес из p1 меньше адреса из p2, то результат операции p1<p2 равняется единице.
В языке Си имеется бинарная операция c первым приоритетом, которая называется операцией “индексирования” и обозначается знаком “[]”. Первым операндом этой операции должен быть указатель p на некоторый тип а, вторым - объект типа int. Результатом вычисления выражения p [ i ] является объект типа Type, адресом которого является значение выражения p+i. То есть выражения p [ i ] и * (p+i) эквивалентны. В частности, если p – адрес массива, то p [ i ] - его элемент с номером i. Номер элемента может быть задан или явным образом, используя допустимые способы представления целых чисел, или именем переменной типа int, или выражением с целочисленным значением.
Пример 4.5 - Доступ к элементам массива
int A []={ 0, 1, 2, 3, 4 };
int i=1;
int iNum=A [0]+ A [ i ]+ A [(int) 1.2+2 ]+*(A+3);
4.3 Массивы и функции
Передача массива в функцию. Вычисление среднего арифметического. Сортировка выбором минимального элемента. Вычисление суммы двух векторов.
Установленные свойства операций над адресами широко применяются при создании функций, параметрами которых являются массивы. Из них следует, что для обеспечения доступа к любому элементу массива в теле функции достаточно передать ей имя массива и его размер. Для иллюстрации сказанного рассмотрим определения двух функций. Первая вычисляет среднее арифметическое значение чисел, хранящихся в массиве. Вторая сортирует элементы массива по возрастанию выбором минимального элемента.
Пример 4.6 - Вычисление среднего значения
#include <stdio.h>
double Aver (double*, int);
void main (void) {
const int Dim = 4;
double s;
double dfMassiv []={ 0.1, 0.2, 0.3, 0.4 };
s=Aver (dfMassiv, Dim);
printf ("%f\n", s);
}
// Определение функции Aver ()
double Aver (
// Адрес массива
double* pA,
// Размер массива
int n) {
// Подготовка переменной для хранения суммы
double sum= 0.0;
// Вычисление суммы
for (int i=0; i<n; i ++)
sum +=*(pA+i);
// или s+=pA [ i ];
// Определение возвращаемого значения
return sum/n;
}
Пример 4.7 - Сортировка выбором наименьшего элемента
#include <stdio.h>
int IndexMinItem (int*,int);
void SortMin (int*,int);
void PrintAr (int*,int,int);
void main (){
const int Dim=9;
int a []={14,4,9,2,0,5,13,9,1};
SortMin (a,Dim);
PrintAr (a,Dim,Dim);
}
int IndexMinItem (int* Ar,int Dim){
int min=*Ar;
int index=0;
for (int i=1; i<Dim; i++){
if (Ar [ i ] <min){
min=Ar [ i ];
index=i;
}
}
return index;
}
void SortMin (int* Ar, int Dim){
int index;
int temp;
for (int i=0; i<Dim-1; i++){
index=IndexMinItem (Ar+i,Dim-i) +i;
temp=Ar [ index ];
Ar [ index ] =Ar [ i ];
Ar [ i ] =temp;
}
}
void PrintAr (int* Ar, int Dim, int w){
int h=Dim/w;
for (int i=0; i<h; i ++){
for (int j=0; j<w; j ++)
printf ("%d ", Ar [ i*w+j ]);
printf ("\n");
}
w=Dim%w;
for (int j=0; j<w; j ++)
printf ("%d ", Ar [ h*w+j ]);
printf ("\n");
}
4.4 Динамическое управление памятью
Статическое управление памятью. Отличие динамической памяти от статической. Операции new и delete. Два варианта функции сложения векторов.
Каждому приложению для хранения данных требуется память. Ее распределением между выполняемыми приложениями занимается операционная система. Если объем памяти, необходимый для хранения данных известен заранее, то программист определяет нужное количество переменных и массивов. На этапе компиляции, то есть до начала выполнения приложения операционная система выделяет затребованный объем памяти. После завершения выполнения приложения выделенная память возвращается операционной системе так же без непосредственного участия программиста. Такое получение и освобождение памяти называется статическим управлением памятью.
Довольно часто объем памяти, который требуется для выполнения приложения, заранее неизвестен. Например, подпрограмма вычисления суммы двух векторов должна уметь складывать векторы, длина которых становится известна только на этапе выполнения приложения (например, после ввода векторов с клавиатуры). В подобных случаях программисту приходится обращаться к операционной системе в ходе выполнения приложения для получения памяти необходимой для хранения трех векторов. Как только необходимость в полученной памяти проходит, программист должен вернуть ее операционной системе (освободить). Для этого существуют специальные команды. Если такие команды в приложении отсутствуют, то выделенная приложению память возвращается операционной системе только после его завершения. Предоставление и освобождение памяти по командам программиста в ходе выполнения приложения называется динамическим управлением памятью.
Для управления динамической памятью современные редакции языка Си предоставляют в распоряжение программиста три стандартных функции и три операции. Прототипы функций содержатся одновременно в двух файлах с именами malloc.h и stdlib.h. Функция malloc () спрототипом
void* malloc (
// Объем запрашиваемой памяти в байтах
unsigned int uSize);
запрашивает у операционной системы блок памяти объемом uSize байт. При наличии такого блока функция возвращает его адрес. Конструкция void* перед именем функции означает, что функция malloc () может использоваться для выделения памяти для хранения данных любого из определенных к этому моменту типов данных. Поэтому перед использованием полученный от операционной системы адрес блока памяти необходимо преобразовать явным образом к соответствующему типу. При отсутствии блока памяти нужного объема функция возвращает объект NULL типа void* или, другими словами, нулевой указатель.