В этой лабораторной работе одномерные и двумерные массивы используются для работы с векторами и матрицами. В примерах 5-8 приведены программы перемножающие два вектора, матрицу на вектор и матрицу на матрицу. Демонстрируется использование массивов указателей для передачи массивов переменных размеров как параметров в функции.
В качестве первого примера рассмотрим программу, скалярно перемножающую два вектора x и y произвольного размера N. Перемножение осуществляется по обычному правилу:
Для хранения элементов векторов используются одномерные массивы.
Пример 1.
/*Program 10.1*/
#include
#include
void main()
{
float x[100], y[100], scal; int i,n; char* str; // (1)
printf(“\n Введите размерность векторов N “); // (2)
printf(“\n Введите координаты вектора y \n “); // (7)
for(i=0;i
for (i=0;i
printf ( “\n\n Скалярное произведение (x*y) = %f”,scal); // (10)
}
Требуется ввести с консоли размерность векторов N и их координаты (строки 2-8). Затем в цикле эти вектора перемножаюгся (строка 9), результат записывается в переменную scal и выводится на экран (строка10). Элементы массивов x и y можно было бы задавать не с консоли, а непосредственно инициализируя массивы. В этом случае вместо строки (1) следует написать, например,
float x[100] = {1., 2., 3., 4., 5.};
float y[100] = {-.1, -.2, -.3, -.4,-.5, -.6};
Элементы массива x с 0-го по 4-ый получают значения 1.,2.,3.,4.,5. . Остальные 95 элементов не инициализируются, и компилятор автоматически присваивает им всем нулевые значения. Аналогично, первые 6 элементов массива y будут инициализированы значениями -.1, -.2, -.3, -.4, -.5, -.6 , а оставшиеся с 6-го по 99-ый - нулевыми. Однако при таком способе задания координат векторов каждое их изменение потребует перекомпиляции программы.
Используем полученную программу перемножения векторов для нахождения угла между ними. Косинус угла вычисляется по формуле
Размерность векторов N и их координаты вводятся с консоли. Функция scal() вызывается из главной программы трижды с разными фактическими параметрами. Вычисляется угол и результат выводится на экран. Обратите внимание на способ передачи параметров в функцию scal() из основной программы. По правилам языка C при передаче простой переменной в функцию передается ее значение, а при передаче массива - адрес начала размещения этого массива в памяти. Изменяя локальную переменную, являющуюся формальным параметром в вызываемой функции, мы не затрагиваем соответствующую ей простую переменную - фактический параметр в вызывающей функции. Иная ситуация при работе с массивами. И вызывающая , и вызываемая функции ссылаются на один и тот же участок памяти и, следовательно, изменение значений элементов массива - формального параметра в вызываемой функции приводят к точно таким же изменениям фактического параметра в вызывающей функции.
В качестве фактических параметров при вызове функции scal() из главной функции используются имена массивов x и y. (Имена массивов являются указателями на начало размещения массивов в памяти, а значениями этих указателей служат адреса.) В определении самой функции scal() этим именам соответствуют формальные параметры - указатели float*a и float*b того же типа, что и элементы массивов x и y. С помощью этих указателей, используя адресную арифметику, функция scal() получает доступ к соответствующим элементам массивов x и y. Для иллюстрации в строке 2 доступ осуществляется и с помощью индексирования, и с помощью операции разыменования.
Для создания программы, которая будет перемножать матрицу на вектор-столбец используем ту же функцию scal() перемножения векторов. Операцию умножения матрицы на вектор определим обычным правилом: i-тая строка матрицы скалярно умножается на вектор-столбец и даёт i-тый элемент результирующего вектора. При таком определении, очевидно, число столбцов матрицы должно совпадать с числом элементов вектор-столбца.
Пример 3.
/*Program 10.3*/
#include
#include
#include
float scal(float*,float*,int);
void main()
{
static float a[100][100], b[100], c[100];
int i,j,n,m; char* str;
system(“cls”); //Выполняет команду DOS очистки экрана.
printf(“\nВведите размеры матрицы :”);
printf(\n число столбцов N “); gets(str); n = atoi(str);
printf(“\nчисло строк M “); gets(str); m = atoi(str);
При вызове функции scal() в неё передаются в качестве параметров i-тая строка матрицы A и вектор b. a[i] является указателем на нулевой элемент i-той строки двумерного массива a, а имя одномерного массива b являтся указателем на начало его расположения в памяти. В определении функции scal() им соответствуют формальные параметры - указатели float *a, float*b. С помощью индексирования в функции scal() становятся доступными элементы i-той строки массива a и массива b. Главная программа содержит некоторые отличия от описанной в предыдущем примере.
Во-первых массивы a,b,c описаны с классом static. Связано это с особенностями использования памяти компилятором TС++. По умолчанию в C все данные имеют класс auto и размещаются в стеке. Предусмотрена возможность размещать данные в других сегментах. Для этого они могут быть описаны с классом static, и(или) программы должны компилироваться для большой (large) модели памяти. Для двумерных массивов N на N типа float это ограничивает их размер N = sqrt(64000/sizeof(float)) = 128. Если потребуется работать с массивами большего размера,нужно установить модель Huge при компиляции программы и использовать модификатор huge при объявлении массива. (На ТС этот режим не реализован.)
Второе отличие - объявление прототипа функции scal(). Синтаксис объявления прототипа похож на синтаксис объявления функции, но вместо параметров функции могут стоять только их типы и в конце объявления прототипа должна стоять точка с запятой. Появление прототипов связано с особенностями работы компиляторов языка С. К моменту, когда компилятор, последовательно обрабатывающий строки исходного текста, доходит до вызова функции, он уже должен “знать” какое количество памяти нужно зарезервировать в стеке для передачи параметров и возвращаемого функцией значения. В связи с этим, функция либо должна быть определена до вызова, как это сделано в примере 2, либо должен быть описан её прототип как в примере 3. В прототипе обязательно должен указываться тип возвращаемого значения и типы формальных параметров. При вызове функции формальные параметры заменяются фактическими, при этом язык С предусматривает автоматическое преобразование типов в тех случаях, когда тип фактических параметров не совпадает с типом формальных.
Вообще говоря, если вызывающая и вызываемая функции размещаются в одном файле, использование прототипа в языке С в тех случаях, когда вызов функции размещён до её определения, не является обязательным. (В отличие от С++). Например, если в примере 3 и главная функция, вычисляющая угол, и функция scal, перемножающая вектора, будут находится в одном файле, то строку с описанием прототипа можно закоментировать - результат не изменится. Если же вызываемая функция размещается в другом файле использование прототипа обязательно. Это относится и к стандартным библиотечным функциям, прототипы которых описаны в заголовочных файлах. (Они имеют расширение .h и находятся в подкаталоге INCLUDE).
В следующем примере приводится программа перемножения матрицы на матрицу. Матрицы перемножаются по следующему правилу. i-тая строка первой матрицы скалярно умножается на j-тый столбец второй и получается элемент стоящий на пересечении i-той строки и j-того столбца результирующей матрицы. Иначе, перемножая первую матрицу на j-тый столбец второй, получим j-тый столбец результирующей матрицы. Так как программа перемножения матрицы на вектор у нас уже есть используем её, оформив в виде отдельной функции.
Пример 4.
/*Program 10.4*/
/*Функция перемножения двух квадратных матриц A и B размером N на N с вызовом функции mult() перемножения матрицы на вектор. Матрицы A и B формируются в главной функции. В функцию mult() передается указатель на массив указателей на строки матрицы A - ptl_A (point to line of A) и указатель на столбцы матрицы B (на строки транспониованной матрицы B) - ptl_b. Возвращаемое значение - указатель ptl_c на столбцы результирующей матрицы C.*/
#include
#include
#include
#include
void mult(float**,float*,float*,int); // Пототипы функций mult() и
float scal(float*,float*,int); // scal().
void main()
{
static float a[50][50],b[50][50],c[50][50];
float *ptl_A[50], *ptl_b, ptl_c[50], btmp;
int i,j,n; char* str;
system(“cls”); // Функция выполняет команду DOS очистки экрана.
printf(“\nВведите N “); gets(str); n=atoi(str);
// Настройка массива указателей ptl_A на строки массива A.
for(i=0;i
// Формирование матриц A и B.
for(i=0;i
for(j=0;j
// Подготовка (транспонирование) матрицы B к перемножению.
for(i=0;i
for(j=0;j
// Перемножение матрицы на матрицу с вызовом функции умножения // матрицы на вектор.
for(i=0;i
ptl_b = (float*)&b[i]; //Настройка указателя на i-тую строку
//матрицыB
mult(ptl_A, ptl_b, ptl_c, n); // Вызов функции mult.
for(j=0;j
c[j][i] = ptl_c[j]; }} // матрицы.
// Восстановление исходной матрицы B.
for(i=0;i
for(j=0;j
// Вывод результирующей матрицы на экран.
printf(“\n\n Результирующая матрица”);
for(i=0;i
for(j=0;j
printf(“%8.3f”,c[i][j]);
}
}
// Функция перемножения матрицы на вектор.
void mult(float**a,float*b,float*c,int n)
{
float *ptl_A; int i;
for(i=0;i
ptl_A = (float*)&a[i][0];
c[i] = scal(ptl_A,b,n); }
}
Функция scal() описана выше.
Для передачи массива а переменного размера n в качестве параметра в функцию mult() перемножения матрицы на вектор используется промежуточный одномерный массив указателей ptl_A, в котором сохраняются адреса строк масива a. Эта процедура требует некоторых пояснений. В функцию mult() перемножения матрицы на вектор требуется передавать двумерный массив - матрицу A и одномерный массив - очередной вектор-столбец матрицы B. С передачей одномерного массива проблем не возникает. Учитывая, что в памяти машины матрицы хранятся по строкам, транспонируем матрицу B и тогда указатель на j-тую строку транспонированной матрицы будет одновременно адресовать j-тый столбец исходной матрицы. Этот указатель и передается в функцию mult() в качестве второго параметра.
С передачей двумерных массивов переменных размеров возникают некоторые сложности связанные с тем, что компилятор не может распознать по имени массива его размерность и размеры. Чтобы обойти эту трудность можно, например, подменить двумерный массив одномерным и иммитировать доступ внутри функции к двумерному массиву. В нашей программе использован другой способ. Заводится вспомогательный массив указателей *point_A[100], который настраивается на строки исходной матрицы A, т.е. элементам массива *point_A присваиваются адреса, с которых начинаются строки массива a. И именно этот одномерный массив указателей передается в функцию mult() в качестве первого фактического параметра. Так как имя любого массива есть указатель на его нулевой элемент, то в функцию mult() передается указатель на массив указателей.
Задания.
1. Изучите приведенные в примерах 1-4 программы. Скопируйте эти программы к себе в каталог. Убедитесь в правильности их работы.
2*. Напишите пограмму перемножающую по обычному правилу матрицу размера N на N, на вектор-столбец размера N. Матрицу и вектор сформируйте в программе, и выведите их вместе с результирующим вектором на экран.
3*. По аналогии с программойиз примера 4 напишите программу перемножения прямоугольных матриц A размера N на M и B размера M на N, оформив транспонирование матрицы B в виде отдельной функции.
4**. Напишите программу непосредственно перемножающую две матрицы без обращения к функциям перемножения матрицы на вектор и вектора на вектор. Оформите ее в виде отдельной функции.
5**. Напишите программу, вычисляющую коммутатор двух квадратных матриц A и B размера N на N, т.е. матрицу равную разности произведения A на B и B на A. Оформите для этого программу перемножения матрицы на матрицу в виде отдельной функции.
К ф. м н. А. О. Беляков. Список публикаций Замечания к статье Л. Д. Акуленко и С. В. Нестерова "Устойчивость равновесия маятника переменной длины". Пмм. 2009. Т. 73. Вып. С....
Лабораторная работа «Одномерные массивы» Цели: формирование практических умений и навыков составления блок-схем и записи на языке программирования Паскаль алгоритмов заполнения,...