Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Larkin Лабораторная работа2.docx
Скачиваний:
2
Добавлен:
09.11.2019
Размер:
619.79 Кб
Скачать

Return выражение;

где в качестве выражения может выступать как отдельная переменная, так и выражение (арифметическое или логическое), которое сначала будет вычислено, а затем этот результат будет возвращен в вызывающую функцию.

Адреса и указатели

Вообще говоря, указатель – это некоторое символическое представление адреса переменной. В том случае, если переменная описана, например, как p, то использование в программе p означает использование значения переменной (например, 3.14), а &p означает "указатель на переменную p", т.е. это – "адрес переменной p". Фактический адрес – это число (например, 32550). Тогда выполнение оператора p = 2.5; можно "перевести" как фразу "разместить число 2.5 по адресу 32550".

Адрес ячейки, отводимой переменной, в процессе выполнения программы не меняется, поэтому символическое представление адреса &p является константой типа указатель (вспомните параметры функции scanf()!).

В языке Си имеются и переменные типа указатель. Точно так же, как значением переменной типа float является вещественное число, значением переменной типа указатель служит адрес некоторой величины.

При описании переменной как указателя необходимо задать, на переменную какого типа ссылается данный указатель:

тип *имя;

Символ звездочка (*) определяет саму переменную как указатель (вспомните описание переменной, связанной с файлом).

Например:

float *p;

int *k;

Тогда p – это указатель (его величина – это адрес), а *p – величина типа float., которую можно использовать, например, в арифметических выражениях:

*p = 3.1;

float a = *p + 1;

Так как значение адреса переменной не изменяется, то если в качестве аргумента передать функции адрес переменной, то внутри этой функции можно изменить значение этой переменной, и это новое значение будет известно после выполнения и в вызывающей функции.

Пример:

void obmen(int* a, int* b)

{

int temp;

temp = *a;

*a = *b;

*b = temp;

}

void main()

{

int x, y;

x = 2;

y = 3;

printf("здесь x = %d, а y = %d\n", x, y);

obmen( &x, &y );

printf("теперь x = %d, а y = %d\n", x, y);

}

В приведенном примере функции obmen() передаются в качестве аргументов адреса локальных переменных x и y, а сама функция obmen() занимается обменом значений, расположенных по этим адресам. Указатели позволяют обойти тот факт, что что переменные функции obmen() являются локальными. Они дают возможность этой функции "добраться" до функции main() и изменить величины описанных в ней объектов.

Размещение функций в файле

1) Каждая функция описывается ОТДЕЛЬНО, это отдельные "строительные блоки".

2) В принципе все функции равноправны, т.е. из любой функции можно вызвать любую другую. Но не надо забывать при этом, что функция main() выполняется первой, и обращаться к ней из других функций без веских причин, мягко говоря, не стоит.

3) Описана функция (её тип и типы аргументов) должна быть ДО первого (от начала файла) её использования. Здесь возможно 2 варианта:

a) привести описание самой функции ДО той функции, из которой она вызывается;

b) привести описание прототипа функции ДО вызова – в этом случае описание прототипа может быть как локальным (внутри вызывающей функции), так и глобальным (например, до описаний всех функций). Под прототипом здесь понимается описание типа функции, её имени и типов всех аргументов (имена аргументов можно не приводить). В этом случае сама функция может быть описана в любом месте файла. Не забудьте, что описание прототипа функции – это отдельный оператор, который должен заканчиваться соответствующим символом.

Оба варианта эквивалентны.

Пример:

Вариант а)

Вариант b)

int pr (float a, int* b)

{

... тело функции

}

void main ()

{...

a = pr (bb, &cc);

}

int pr (float, int*);

void main ()

{...

a = pr (bb, &cc);

}

int pr (float a, int* b)

{

... тело функции

}

Массивы как параметры функций

Имя массива является указателем на первый элемент массива. Поэтому при вызове функций readvektor()и poiskmax() в примере п.9.2.1 в качестве первого фактического параметра передавалось на самом деле не имя соответствующего массива, а адрес его первого элемента. Соответственно в описании этих функций использовалась форма тип имя_массива[], что является эквивалентом оператору тип *имя. То есть можно было, например, заголовок функции написать следующим образом:

double poiskmax(double *y, int m);

а не

double poiskmax(double y[], int m);

просто во втором случае описание y[] лишний раз напоминает о том, что указатель y ссылается на массив. Все остальные действия с массивом y в функции poiskmax() остаются без изменений, в частности, обращение к элементам массива.

Более того, вообще говоря, вместо y[i] можно использовать *(y+i).

Двумерный массив как параметр функции

Двумерный массив можно рассматривать как массив указателей или указатель на указатель.

Одним из способов избежать неприятностей при необходимости работать с двумерными массивами как аргументами функции является явное указание, как минимум, величины второй размерности (т.е. количества столбцов) при объявлении функции:

void vv (int x[][10], int m, int n)

{...

.. x[i][j] .. // использование элемента массива x[i][j]

}

void main ()

{

int a[20][10], k1=5, k2=4;

vv (a, k1, k2);

...

}

Таким образом, компилятор "поймет", что массив следует разбить на строки по 10 столбцов. Или, если описать формальный параметр как одномерный массив, необходимо будет обращаться к каждому элементу массива, предварительно вычисляя его адрес, например:

void vv (int x[], int m, int n)

{...

.. x[i*n+j] .. // использование элемента массива x[i][j]

...

}

...

vv (a[0], k, 10); /* обращение к функции; здесь в качестве первого аргумента передаём адрес первой строки; количество столбцов должно соответствовать заданному в описании массива!!! */

...

Это связано с тем, что в памяти массив располагается, занимая последовательные ячейки памяти. Так, массив z[2][3] располагается в памяти следующим образом:

z[0][0] z[0][1] z[0][2] z[1][0]

Тогда z[0]&z[0][0], z[1]&z[1][0] и т.д. Вычислить адрес z[1][0] можно, добавив к адресу самого первого элемента массива количество столбцов из описания: z+3.

Пример:

# include <stdio.h>

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; /* массив, описанный глобально, можно инициализировать таким способом */

void vv (int x[], int m, int k, int n)

{int i, j;

for (i=0; i<m; i++)

{

for (j=0; j<k; j++)

printf ("x[%d][%d]=%d ",i,j,x[i*n+j]); //или *(x+i*n+j)

printf("\n");

}

}

void main ()

{

int nn = 2, kk = 3;

vv (a[0], nn, kk, 4);

}

Нахождение обратной матрицы в С.

Из изученного курса линейной алгебры нам известно как минимум два способа нахождения обратной матрицы. Стандартный подход заключается в следующем:

  1. Найти определитель матрицы.

  2. Если определитель неравен нулю, перейти к следующему шагу, в противном случае нахождение обратной матрицы таким способом невозможно.

  3. Составить матрицу из алгебраических дополнений.

  4. Произвести транспонирование.

  5. Разделить полученную матрицу на определитель исходной.

Использовать данный способ нерационально, так как нахождение определителя потребует N! действий(где N – размерность матрицы). Более того нахождение алгебраических дополнений элементов так же требует нахождение определителей матриц размерностей (N-1). В общем итоге количество действий возрастет до N*N!, то есть для обращения матрицы 10 10 потребуется 36288000 действий.

Исходя из вышесказанного, рациональнее использовать метод Гаусса, который заключается в следующем:

По определению произведение взаимообратных матриц является единичной матрицей, где при i=j =1, при i j =0. Запишем систему уравнений для матрицы 3*3:

Запишем СЛАУ в матрицы:

Легко видеть, что для нахождения матрицы можно воспользоваться методом Гаусса для решения СЛАУ. Запишем расширенную матрицу:

Теперь, проводя операции со строками, преобразовываем левую часть в единичную матрицу, тогда в правой части получим матрицу обратную матрице . Такой алгоритм потребует вспомогательную единичную матрицу, что займет действий, а основная часть . Таким образом, для вычисления обратной матрицы размерности 10 потребуется всего 1100 итераций. Окончательный алгоритм приведен в приложении С.

Описание алгоритма решения задачи

Для операций с матрицами удобнее всего ввести пользовательские функции для каждого действия

  1. Для считывания матрицы из файла объявим подпрограмму, на входе указывается имя файла и массив, в который будут записаны данные(здесь и далее в качестве параметра указывается адрес первого элемента массива, адреса остальных вычисляются по формуле: , где i,j-номера строк и столбцов, N-максимальная размерность массива, -адрес первого элемента.

  2. Для записи массива в файл так же организована подпрограмма, где в качестве параметра указан нужный массив и имя файла.

  3. Процедура транспонирования матрицы требует на входе нужную матрицу и с помощью двух вложенных циклов(внешний с счетчиком i пробегает значения от 0 до n-1, а внутренний от i+1 до n-1) меняет местами элемент c элементом

  4. Функция умножения матриц имеет три массива, из которых первые два – умножаемые матрицы, а третий - результирующий массив. Имеет три цикла, которые производят операции по формуле

  5. Функция сложения матриц так же имеет три массива и складывает элементы с одинаковыми номерами

  6. Функция обращения матрицы требует на входе нужный массив, инициализирует единичную матрицу по правилу , делит строку на диагональный элемент (действия проводятся как над исходной так и над единичной матрицей), а далее зануляет недиагональные элементы. В итоге единичная матрица преобразуется в обратную исходной, нам нужно только перенести значения в нее.

  7. В основном блоке программ сначала запрашивает размерность трех исходных матриц, (в ходе анализа формулы из задания было выяснено, что все шесть размерностей должны быть одинаковы, но так как указано, что необходимо сделать проверку на возможность вычисления матриц с указанной размерностью, был введен цикл, который проверяет равенство введенных шести значений).

  8. Далее программа считывает из трех файлов значения для трех матриц.

  9. Проводится последовательный запуск функций согласно формуле

D=AT *C*(C-1 +BT ) *B:

  1. Транспонирование матрицы А

  2. Умножение полученной на С, результат выводится в D

  3. Обращение матрицы С

  4. Транспонирование В

  5. Сложение матриц В и С, результат выводится в более ненужную матрицу А.

  6. Умножение матриц D и A, результат выводится в больше ненужную матрицу C.

  7. Повторное транспонирование матрицы В, что даст нам исходную матрицу В.

  8. Умножение матриц С и В, результат записывается в D.

  1. Производится запись результатов в файл.

Руководство программиста

Программный код приложения разрабатывался на языке СИ.

В программе использованы следующие пользовательские функции:

  • void scn(имя входного файла, нулевой элемент массива для матрицы)

Считывает файл с матрицей в нужный массив а так же производит проверку на наличие файла, если файл отсутствует, программа выдает ошибку и матрица будет нулевой.

  • void prnt(имя выходного файла, нулевой элемент массива с матрицей)

Выводит матрицу в файл

  • void transp(нулевой элемент массива с матрицей)

Транспонирует матрицу

  • void mult(нулевой элемент массива с матрицей 1, нулевой элемент массива с матрицей 2, нулевой элемент массива с матрицей 3)

Умножает матрицу 1 на матрицу 2, результат записывает в матрицу 3

  • void sum(нулевой элемент массива с матрицей 1, нулевой элемент массива с матрицей 2, нулевой элемент массива с матрицей 3)

Складывает матрицу 1 с матрицей 2, результат записывает в матрицу 3

  • void inv(нулевой элемент массива с матрицей)

Обращает матрицу

Так же в программе использована директива #define, которая заменяет нужную константу на на символ, например #define N 3 позволяет вместо значения 3 использовать символ N. Преимущество такого подхода заключается в том, что если константа используется много раз и нужно изменить её значение во всех местах, достаточно просто изменить значение в директиве.

Чтобы не использовать оператор goto, что является плохим тоном программирования, для многократной проверки правильности ввода был использован цикл do while, который будет выполняться, пока пользователь не введет удовлетворяющие условию значения.

Блок-схема алгоритма программы приведена в приложении А.

Листинг программы приведен в приложении В.

Пример входного и выходного файла в приложении С.

Руководство пользователя

Чтобы программа работала корректно, необходимо в каталоге с приложением иметь три текстовых файла A, B, C, где записаны элементы матриц через пробел. Если не будет хотя бы одного файла, программа занулит соответствующую матрицу. После запуска программы она запросит значения размерностей матриц, в случае неправильного ввода программ выдаст ошибку и запросит значения заново. В файлах значения матриц могут быть записаны в строчку, если число элементов матрицы будет меньше числа элементов в файле, программа считает первые элементов. В обратном случае недостающие элементы заменят нули. Далее программа произведет действия с матрицей по вышеописанной формуле и выдаст результат в файл D.txt.

Вывод

В ходе лабораторной работы были освоены методы разбиения решаемой задачи на подзадачи и реализации вычислительных процедуры в виде программ на языке СИ с использованием функций пользователя.

Приложение В

#include <stdio.h>

#define N 20

int n;

float D[N][N],A[N][N],B[N][N],C[N][N];;

void scn(char fil[5],float *mat)

{

FILE *x;

if((x = fopen(fil,"r"))==NULL) printf("error: WHERE IS FILE '%s'?????? Matrix from it will be null \n",fil);

for(int i=0;i<n;i++)

for(int j=0;j<n;j++)

fscanf(x,"%f",(mat+i*N+j));

fclose(x);

}

void prnt(char fil[10],float *mat)

{

FILE *x;

x = fopen(fil,"w");

for(int i=0;i<n;i++)

{

for(int j=0;j<n;j++)

fprintf(x,"%f ",*(mat+i*N+j));

fprintf(x,"\n");

}

fclose(x);

}

void transp(float *mat)

{

float tmp;

for(int i=0;i<n;i++)

for(int j=i+1;j<n;j++)

{

tmp=*(mat+i*N+j);

*(mat+i*N+j)=*(mat+j*N+i);

*(mat+j*N+i)=tmp;

}

}

void mult(float *mat1,float *mat2,float *m)

{

float a,b,c;

for(int i=0;i<n;i++)

for(int j=0;j<n;j++)

{

c=0;

for(int k=0;k<n;k++)

{

a=*(mat1+i*N+k);

b=*(mat2+k*N+j);

c=c+a*b;

}

*(m+i*N+j)=c;

}

}

void sum(float *mat1,float *mat2,float *m)

{

for(int i=0;i<n;i++)

for(int j=0;j<n;j++)

*(m+i*N+j)= *(mat1+i*N+j)+ *(mat2+i*N+j);

}

void inv(float *mat)

{

float e[N][N];

for (int i=0;i<n;i++)

for (int j=0;j<n;j++)

e[i][j]=(i==j?1:0);

for(int i=0;i<n;i++)

{

float tmp=*(mat+i*N+i);

for(int j=n-1;j>=0;j--)

{

e[i][j]/=tmp;

*(mat+i*N+j)/=tmp;

}

for(int j=0;j<n;j++)

if (j!=i)

{

tmp=*(mat+j*N+i);

for(int k=n-1;k>=0;k--)

{

e[j][k]-=e[i][k]*tmp;

*(mat+j*N+k)-=*(mat+i*N+k)*tmp;

}

}

}

for(int i=0;i<n;i++)

for(int j=0;j<n;j++)

*(mat+i*N+j)=e[i][j];

}

void main()

{

bool f=false;

int n1,i;

printf("Input dimentions of matrix A,B,C \n");

do

{

if(f==true)

{

printf("error: Bad dimmention of matrixes, they must be quadratic,\nequal and dimention must be not more, then 20, input all values again \n");

f=false;

}

scanf("%d",&n);

for(i=0;i<5;i++)

{

scanf("%d",&n1);

if(n1!=n || n1>20) f=true;

}

}

while(f == true);

scn("A.txt",A[0]);

scn("B.txt",B[0]);

scn("C.txt",C[0]);

transp(A[0]);

mult(A[0],C[0],D[0]);

inv(C[0]);

transp(B[0]);

sum(B[0],C[0],A[0]);

mult(D[0],A[0],C[0]);

transp(B[0]);

mult(C[0],B[0],D[0]);

prnt("D.txt",D[0]);

}

Приложение С

Входной файл A.txt:

-1 2 3 4 5 6 7 8 -9 0 1 2 3 4 5 6 7 8 9 0 2 3 4 5 6

Входной файл В.txt:

4 -6 5 8 9 3 4 5 6 7 3 4 5 0 32 -45 5 3 4 5 3 2 4 8 9

Входной файл С.txt:

-4 5 3 8 76 6 4 -5 23 4 1 9 5 6 0 -5 3 2 3 4 5 6 8 4 3

Выходной файл D.txt с размерностью матриц 2*2:

848.000122 2138.000000

864.000122 4284.000000

Выходной файл D.txt с размерностью матриц 5*5:

14297.002930 -150.999756 12503.000977 23166.000000 13230.000977

-22672.000000 40532.000000 92410.000000 86382.000000 337929.000000

-31941.000000 56075.000000 124737.000000 113738.000000 464238.000000

-208286.000000 75776.000000 103334.000000 72478.000000 435027.000000

-26439.000000 68741.000000 137811.000000 108710.000000 561406.000000

21