Реализуем эти действия также с помощью соответствующих функций:
void Заполнение_массива()
{
}
void Вывод_массива()
{
}
void Подготовка_данных()
{
Заполнение_массива();
Вывод_массива();
}
Для заполнения данными матрицы воспользуемся датчиком случайных чисел, сама процедура заполнения двумерного массива значениями настолько проста, что дальнейшей детализации не требует:
void Заполнение_массива()
{
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
A[i][j] = rand() % 3; // Генерация сл. чисел в диапазоне [0, 2]
}
Здесь мы заполнили матрицу значениями 0, 1 и 2. При таких значениях элементов матрицы легче получить матрицу, содержащую седловые точки.
Вывод значений двумерного массива на экран также не должно вызывать затруднений:
void Вывод_массива()
{
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
cout << setw(4) << right << A[i][j];
cout << endl;
}
}
Вывод значений матрицы мы оформили в виде таблицы. Для задания ширины каждого столбца этой таблицы использован манипулятор вывода setw. Для использования этого манипулятора необходимо включить заготовочный файл iomanip.
Теперь мы получили следующую программу:
#include "stdafx.h"
#include <iostream>
#include <iomanip>
using namespace std;
const int n = 5, m = 5; // n - количество строк, m - количество столбцов матрицы
int A[n][m]; // А - исходная матрица
void Заполнение_массива()
{
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
A[i][j] = rand() % 3;
}
void Вывод_массива()
{
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
cout << setw(4) << right << A[i][j];
cout << endl;
}
}
void Подготовка_данных()
{
Заполнение_массива();
Вывод_массива();
}
void Решение_и_вывод_результатов()
{
}
int _tmain(int argc, _TCHAR* argv[])
{
Подготовка_данных();
Решение_и_вывод_результатов();
system("Pause");
return 0;
}
Если запустить эту программу на выполнение, на экран будет выведена матрица, содержащая 5 строк и 5 столбцов с элементами, значения которых принадлежат диапазону [0, 2] .
Переходим к реализации функции Решение_и_вывод_результатов(). Общий алгоритм работы этой функции: необходимо выполнить проверку каждого элемента исходной матрицы – является ли этот элемент седловой точкой. Если проверяемый элемент является седловой точкой, то вывести на экран значения индексов этого элемента. Если проверяемый элемент не является седловой точкой, то перейти к проверке следующего элемента.
Перебор элементов матрицы можно выполнить с помощью двух вложенных циклов.
Предположим, что имеется функция bool Это_седловая_точка(int i, int j), которая возвращает значение true, если элемент матрицы А [i] [j] является седловой точкой. Тогда реализация функции Решение_и_вывод_результатов() будет выглядеть так:
Реализуем функцию Это_седловая_точка. Для того чтобы определить является ли элемент матрицы с индексами i и j седловой точкой, необходимо знать минимальное и максимальное значения в строке с индексом i значения, а также минимальное и максимальное значения в столбце с индексом j. Допустим, что у нас имеются четыре функции:
int Максимум_строки (int Строка)
int Минимум_строки (int Строка)
int Максимум_столбца (int Столбец)
int Минимум_столбца (int Столбец)
Каждая из этих функций возвращает соответствующее значение из указанной параметром функции строки или столбца. Если эти функции у нас будут, то функция Это_седловая_точка будет выглядеть так:
Теперь реализуем оставшиеся 4 функции по определению максимальных и минимальных значений элементов:
int Максимум_строки(int Строка)
{
int Максимум = A[Строка][0];
for (int i = 1; i < n; ++i)
if (A[Строка][i] > Максимум)
Максимум = A[Строка][i];
return Максимум;
}
int Минимум_строки(int Строка)
{
int Минимум = A[Строка][0];
for (int i = 1; i < n; ++i)
if (A[Строка][i] < Минимум)
Минимум = A[Строка][i];
return Минимум;
}
int Максимум_столбца(int Столбец)
{
int Максимум = A[0][Столбец];
for (int i = 1; i < n; ++i)
if (A[i][Столбец] > Максимум)
Максимум = A[i][Столбец];
return Максимум;
}
int Минимум_столбца(int Столбец)
{
int Минимум = A[0][Столбец];
for (int i = 1; i < n; ++i)
if (A[i][Столбец] < Минимум)
Минимум = A[i][Столбец];
return Минимум;
}
Теперь подкорректируем главную функцию нашей программы для возможности многократного повторения выполнения программы с разными исходными данными (это необходимо сделать, поскольку получить случайную матрицу с седловыми точками иногда бывает не так просто):
int _tmain(int argc, _TCHAR* argv[])
{
char c;
setlocale(0, "");
do
{
Подготовка_данных();
Решение_и_вывод_результатов();
}
while (cout << "\n\t\t\tПродолжим? (нет - Esc)", c = _getch(),
cout << "\n\n", c != 27);
return 0;
}
Итак, получена следующая рабочая программа:
#include "stdafx.h"
#include <iostream>
#include <iomanip>
#include <conio.h>
using namespace std;
const int n = 5, m = 5; // n - количество строк, m - количество столбцов матрицы
while (cout << "\n\t\t\tПродолжим? (нет - Esc)", c = _getch(),
cout << "\n\n", c != 27);
return 0;
}
А вот фрагмент результатов ее работы:
Здесь мы получили матрицу с 4-мя седловыми точками – выделены красным.
Проанализируем ход проектирования программы.
Использованный метод проектирования называется проектированием “сверху вниз” или методом пошаговой детализации. Этот метод основан на последовательном разбиении решения задачи на более простые функционально независимые подзадачи. Такая детализация продолжается до тех пор, пока реализация очередной подзадачи в виде исполняемой программы не становится очевидной. Естественным способом реализации этого метода является представление каждой подзадачи в виде функций, взаимодействующих между собой на уровне данных. Основная часть разработки программы проводилась на чисто логическом уровне, практически без использования инструкций языка программирования. Постепенное дробление алгоритма на более простые подзадачи привело к тому, что реализация каждой из функции, соответствующих этим подзадачам уже не вызывает никаких затруднений при их реализации на языке программирования. Дальнейшая модификация программы (например, реализация ручного ввода исходных данных) связана с коррекцией отдельных функций и не влечет за собой переделки всей программ. Отладка разработанных по такой методике программ может осуществляться по частям (на уровне отдельных функций). Разработку различных частей программы (отдельных ее функций) можно поручить различным исполнителям. Таким образом, использование функций приводит к очевидным преимуществам.
Определение функций в программе
Любая функция состоит из двух основных элементов: заголовка и тела функции.
Заголовок функции имеет следующий формат:
<Тип возвращаемого значения> <Идентификатор – имя функции> (<Параметры>)
Тело функции представляет собой блок инструкций языка программирования, разделенных символами “точка с запятой”:
{
<Инструкция 1>;
<Инструкция 2>;
………………….
<Инструкция N>;
}
Например:
double Example (double d, int k)
{
double r;
r = d * k;
return r;
}
Внутри тела функции могут использоваться любые инструкции языка программирования. Количество инструкций не ограничено (но лучше разрабатывать небольшие по размеру функции – их легче отлаживать, меньше вероятность допустить ошибку).
Функция может возвращать одно, сформированное внутри функции, значение через свое имя. Тип данных этого значения определяется элементом заголовка <Тип возвращаемого значения>. Тип возвращаемого значения может быть любым, за исключением типа массива (но указателем он может быть).
Не все функции должны возвращать значения. В этом случае <Тип возвращаемого значения> задается ключевым словом void, которое означает – “пусто” – возвращаемое значение отсутствует:
void ErrMessage (int N)
{
cout << “\nВнимание! Ошибка номер ” << N << “\n\n”;
}
Завершение работы функции (инструкция return)
Если функция не возвращает через свое имя никакого значения, то она завершается после выполнения последней инструкции тела функции. При необходимости досрочного завершения работы функции необходимо использовать инструкцию return. Например:
void Proc ()
{
……..
if ( Ошибка )
return;
……..
cout << “\nНормальное завершение функции\n ” ;
}
Если произошла ошибка, то выполняется инструкция return, и выполнение функции немедленно прекращается (осуществляется выход из функции). Если ошибки не было, то функция продолжит свою работу до последней инструкции и нормально завершит свою работу, когда достигнет конца блока тела функции.
Таким образом, инструкция return приводит к немедленному завершению работы функции.
В одной и той же функции могут быть использованы несколько инструкций return.
Если функция возвращает через свое имя некоторое значение, то выход из функции обязательно должен осуществляться с помощью инструкции return. В этом случае эта инструкция не только вызывает окончание работы функции, но и осуществляет передачу возвращаемого функцией значения. Например:
double Calc(double Op1, double Op2, char Oper)
{
switch (Oper)
{
case '+': return Op1 + Op2;
case '-': return Op1 - Op2;
case '/':
if (!Op2)
return Op1 / Op2;
else
{
cout << "Деление на 0!\n";
return 0;
}
case '*': return Op1 * Op2;
default:
{
cout << "Неверная операция!\n";
return 0;
}
}
}
Значение, которое возвращает инструкция return, по типу должно соответствовать типу возвращаемого функцией значения.
Список параметров функций
Параметры функций служат для обеспечения взаимодействия между функцией и вызвавшей ее программой. Другими словами, параметры служат для обмена данными между программой и функцией.
Не у всех процедур должны быть параметры. Если у функции нет параметров, то соответствующий элемент заголовка, либо пропускается, либо обозначается словом void:
void Pause () // Или void Pause ( void )
{
cout << “\n Для продолжения нажмите любую клавишу…\n ”;
_getch();
}
Параметры функций перечисляются в заголовке через символ ‘,’ (запятая). Каждый параметр должен соответствовать определенному типу данных. Параметры могут быть любых типов данных. Тип данных для каждого параметра (даже если все они имеют один и тот же тип данных) задается отдельно в соответствии со следующим форматом:
<Тип данных параметра> <Идентификатор – имя параметра>
Например:
void Example1 (int a, int b)
или
double Example2 (int A, double B)
Обращение к функциям в программе
Если функция не возвращает значение, она вызывается в программе, например, так:
int I = 10, J = 200;
……
Example1 (I, J);
…….
Если функция возвращает значение, она вызывается в программе обычно так:
double d;
……
d = Example2 ( 10, 3.14);
…….
Однако если даже функция возвращает значение, но необходимости в этом значении нет, то вызвать такую функцию можно и так:
double d;
……
Example2 ( 10, 3.14); // Возвращаемое значение не используется
…….
При вызове функции на места параметров подставляются некоторые конкретные значения, которые обычно называют аргументами функции. Иногда вместо понятия параметров и аргументов используют термины формальные и фактические параметры. Формальные параметры соответствуют понятию параметр функции, а фактический параметр – это аргумент функции. Например, в предыдущем примере A и B являются формальными параметрами (или просто параметрами) функции Example2, а значения 10 и 3.14 – фактическими параметрами (или аргументами) функции.
Количество, типы данных и порядок следования аргументов должны соответствовать списку параметров функции.
Функции, возвращающие значения, могут использоваться в качестве элементов различных выражений.
Передача данных по значению
Механизм передачи данных через параметры функции очень прост. При вызове функции в определенной области памяти (в стеке программы) для каждого параметра функции создается переменная соответствующая типу данных параметра. В эти переменные копируются значения аргументов, использовавшихся при вызове функции. При выполнении кода функции эти копии значений аргументов могут использоваться для обработки, могут изменять свои значения, но эти изменения никак не затрагивают значений самих аргументов. Поэтому после завершения работы функции, значения аргументов, которые были использованы при вызове функции, останутся такими же, какими они были до вызова функции. Например:
void F (int I)
{
I = I + 20;
cout << I << endl;
}
int main()
{
int A = 10;
cout << A << endl; // Выведено значение 10
F ( A ); // Выведено значение 30
cout << A << endl; // Выведено значение 10
return 0;
}
В этом примере значение переменной А до вызова функции F и после завершения работы этой функции одинаковы. При выполнении функции значение параметра I изменяется, но это изменение не затрагивает значение переменной A, так как функция работает не с самой переменной A, а с ее копией, которая представляется параметром I.
Такой способ передачи данных обычно носит название передача данных по значению. Можно считать, что такой способ служит для передачи данных только внутрь функции, но не из функции обратно в программу.
Передача данных с помощью указателей
Из рассмотренных выше примеров может создаться впечатление, что функции могут возвращать только одно значение – через свое имя. Однако это было бы очень серьезным ограничением полезности функций. Очень часто (в подавляющем большинстве случаев) требуется, чтобы функции возвращали более одного, сформированного внутри функции, значения.
Решение этой проблемы: необходимо заставить функцию обрабатывать не копию аргумента, а само значение аргумента.
Для этого можно использовать передачу данных с помощью указателей.
Рассмотрим пример: необходимо разработать функцию, возвращающую результат деления и остаток от деления двух целых чисел.
int Div (int N1, int N2, int *Ost) // int *Ost – параметр-указатель
cout << I << “ / ” << J << “ = ” << R << “. Остаток равен “ << O << endl;
return 0;
}
Функция Div находит результат и остаток от деления параметра N1 на N2. Результат деления возвращается через имя функции, а остаток через параметр Ost.
Для того чтобы обеспечить возвращение вычисленного внутри функции остатка, соответствующий параметр функции определен как указатель (int *Ost). При вызове функции в качестве аргумента для этого параметра был использован адрес переменной &O, а не само значение переменной O. При вычислении остатка внутри функции выполняется разыменование параметра Ost (*Ost - обращение по адресу, хранящемуся в параметре Ost), и вычисленный остаток записывается по этому адресу (то есть в переменную O). Таким образом, значение переменной O изменяется.
В принципе здесь также используется передача данных по значению. Но в качестве значения используется адрес аргумента, а не само значение аргумента. И далее в функции осуществляется работа со значением аргумента путем обращения к нему через его адрес.
Таким образом, для использования передачи данных с помощью указателей необходимо обязательно выполнить три следующих пункта: