- •Пример 4.1 - Создание (определение) массивов
- •Пример 4.2 - Выделение памяти под массив
- •Пример 4.4 - Указатели-константы
- •Функция free() с прототипом
- •Вызов функции calloc() с прототипом
- •Пример 4.11 - Операции new и delete в задаче сложения векторов
- •Пример 4.12 – Второй вариант функции сложения векторов
- •Функция с прототипом
4 ПРОИЗВОДНЫЕ ТИПЫ ДАННЫХ
В главе рассматриваются встроенные типы данных, которые за тесную связь с основными типами, рассмотренными в Главе 1, получили название производных типов. Приводятся примеры их использования.
4.1 Массивы
Определение массива. Примеры. Выделение памяти.
Именованная последовательность однотипных объектов, расположенных в памяти один за другим, называется в языке Си массивом, а сами объекты – элементами массива. Использование массива начинается с его определения (создания) в тексте приложения. Оно включает тип объектов, образующих массив, выбранный программистом идентификатор, называемый имя массива, квадратные скобки, знак “=” и фигурные скобки, с его элементами, разделенными запятыми. В конце определения ставится точка с запятой. Наглядно определение можно представить в виде следующей схемы:
Тип Имя[константа типа int]=
{Список элементов, разделенных запятыми};
Тип элементов массива может быть любым из известных на момент создания массива типов. Имя массива выбирается программистом. Программист может указать количество элементов (размер) создаваемого массива внутри квадратных скобок. Для этого должна использоваться положительная константа типа int. Если размер массива пропущен (не указан), то компилятор определяет его сам путем подсчета числа элементов, указанных в фигурных скобках.
Пример 4.1 - Создание (определение) массивов
float fA[4]={0.1f, 0.2f, 0.3f, 0.5f};
const int iDim=3;
int iMassiv[iDim]={1, 2, 3};
char cArray[]={‘a‘, ‘b‘, ‘c‘, ‘d‘};
Встретив
в тексте программы указание о создании
массива из
элементов типа
,
компилятор выделяет блок памяти объема
равного
*
и размещает в нем элементы массива в
порядке, указанном в его определении.
Множество
всех массивов из
элементов типа
является типом данных, для обозначения
которого используется конструкция вида
,
которая не является идентификатором.
Например, множество всех массивов из
10 объектов типа double
образует тип double[10].
Встроенные операции над массивами в
языке Си отсутствуют.
Правила языка позволяют программисту на этапе разработки приложения выделять память для массивов, элементы которых будут вычисляться позднее. Для этого следует воспользоваться специальной командой. В ней нужно указать тип элементов массива, его имя и размер, который должен быть положительной константой типа int
Type Name[константа типа int];
Пример 4.2 - Выделение памяти под массив
// Для массива из 10 элементов типа float
float fArray[10];
// Для массива из 13 указателей на int
const int Dim=13;
int* pInt[Dim];
4.2 Указатели и массивы. Доступ к элементам массивов
Указатель на константу. Указатель-константа. Адрес массива. Адреса элементов массива. Операции с указателями. Операция индексирования.
Указатели были введены в 3.3. Так называются переменные, которые используются для хранения адресов. Там же было сказано о том, что получить адрес объекта можно по его имени при помощи операции, обозначаемой символом &. Получить объект по указателю с его адресом можно при помощи операции, обозначаемой символом *. Обе операции являются унарными и имеют второй приоритет.
Если объект с именем a в ходе выполнения программы не должен изменяться, то его определение должно начинаться со слова const. Все попытки изменения таких объектов обнаруживаются на этапе компиляции с отображением соответствующих сообщений на экране дисплея. В соответствии с правилами, определение указателя на константу должно также начинаться со слова const
сonst Тип* Имя=АдресКонстанты;
В этом случае контроль за использованием такого указателя возлагается на компилятор.
Пример 4.3 - Указатели на константу
// Создание константы типа int
const int constInt=3;
// Создание указателя на константу
const int* pconstInt=&constInt;
// Попытка изменения константы является ошибкой
*pconstInt=1; // ошибка
// Попытка создания указателя на константу является ошибкой
int* p=&constInt; // ошибка
Для того, чтобы запретить изменение значения указателя в ходе выполнения программы, необходимо включить в его определение служебное слово const.
Тип* const Имя_Указателя=АдресОбъекта;
Такие указатели называются указателями-константами.
Пример 4.4 - Указатели-константы
// Создание объектов типа int
int i1=1, i2=2;
// Создание указателя-константы
int* const pInt1=&i1;
// Попытка изменения указателя-константы является ошибкой
pInt1=&i2; // ошибка
// Изменение объекта с адресом pInt1 разрешается
i1=12;
По определению, имя массива является указателем-константой, содержащим адрес его элемента с номером нуль. Этот адрес также называется адресом массива и используется для получения доступа к его элементам. По определению, адресом элемента с номером 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* или, другими словами, нулевой указатель.
