СИ. Программирование на языке CC Часть Структурное программирование
Скачать 1.65 Mb.
|
i n Как освободить выделенную область динамической памяти. В программе создан динамический массив n t * a = ( i n t *) malloc ( m * s i z e o f ( i n Как освободить выделенную область динамической памяти. Что происходит, если при попытке выделения динамической области памяти выясняется, что свободная память отсутствует. Какой тип данных описан в данной строке — указатель на массив или массив указателей n t * a [10]; 23. Как элементы многомерного массива хранятся в памяти компьютера — 104 — 24. Укажите три варианта обращения к элементу й строки го столбца двумерного массива a 25. В программе имеются следующие строки n t n = 2, m = 3; i n Какую ошибку допустил программист. Могут ли строки или столбцы двумерного массива содержать разное количество элементов — 105 — 4. Модульное программирование. Функции Обычно при проектировании алгоритма сложную задачу делят на более простые. Каждую простую задачу решают отдельно Функция — это именованная последовательность описаний и операторов, выполняющая какое-либо законченное действие. Функция может принимать параметры и возвращать значение. Объявление и определение функции. Программа на языке C/C++ состоит из функций, одна из которых должна иметь имя main — именно сне начинается выполнение программы. Остальные функции начинают выполняться в момент вызова. Любая функция должна быть объявлена и определена. Объявлений функции в различных местах программы может быть несколько, но определение должно быть только одно. Определение функции должно находиться раньше её вызова (чтобы однопроходный компилятор смог проверить правильность вызова и сгенерировать нужный код). Объявление функции называемое также прототипом или заголовком) зада те имя, параметры и тип возвращаемого значения. Определение функции содержит, кроме объявления, тело функции — последовательность операторов и описаний, заключенные в фигурные скобки: [ класс ] тип имя [ список_параметров ] ) исключения) ] { тело_функции } Необязательные класс и исключения будут рассмотрены ниже, без них определение функции выглядит проще: тип имя [ список_параметров ] ) { тело_функции } Тип возвращаемого значения может быть любым, кроме массива и функции (но может являться указателем на массив или функцию. Если функция не возвращает никакого значения, то пишется void — 106 Список параметров определяет величины, которые передаются функции при её вызове. Элементы списка пишутся через запятую. Для каждого параметра указывается его тип и имя (в объявлении имя можно опустить). В объявлении, определении и при вызове одной и той же функции типы и порядок следования параметров должны совпадать. В определении функции указываются так называемые формальные параметры, а при вызове — фактические параметры. Имена формальных и фактических параметров могут не совпадать соотношение между ними устанавливается порядком следова- ния. В программе можно определить несколько функций с одинаковыми именами, если они имеют разный набор типов входных параметров. Пример 4.1. Рассмотрим программу (листинг, в которой требуется найти модуль комплексного числа. Напомним, что комплексное число это упорядоченная пара вещественных чисел (действительная и мнимая часть, а модуль (длина вектора на комплексной плоскости) вычисляется по теореме Пифагора как корень из суммы квадратов действительной и мнимой частей комплексного числа. На рис. 4.1 показан результат работы программы. Листинг 4.1: Вычисление модуля комплексного числа с помощью функции i n c l u d e // для cin и cout 2 # i n c l u d e // для sqrt() 3 # i n c l u d e // для system() 4 5 using namespace std ; 6 7 // Функция для вычисления модуля комплексного числа Вход - действительная часть - мнимая часть числа Выход модуль комплексного числа) { 12 double z ; — 107 — 13 z = sqrt ( x1 * x1 + x2 * x2 ); 14 return z ; 15 } 16 17 i n t main () { 18 double a1 , a2 , x ; 19 SetConsoleOutputCP (1251); 20 cout << "Введите␣действительную␣и␣мнимую␣часть\n" 21 "комплексного␣числа:\n"; 22 cin >> a1 >> a2 ; 23 x = complAbs ( a1 , a2 ); // Вызов функции "Модуль" Рис. 4.1. Экран программы из листинга 4.1 Здесь сначала определена функция строка, имеющая два вещественных параметра и возвращающая вещественное значение. Хорошим стилем программирования считается подробное документирование всех функций, те. указание комментариев, поясняющих смысл каждой функции, её входных параметров и возвращаемого результата. В строке 12 описана локальная переменная область её действия — от момента описания до конца функции В строке 14 используется оператор возврата return , в нём указывается значение, которая функция complAbs должна возвратить вместо её вызова — 108 В строке 18 описаны локальные переменные, используемые в функции В строке 23 происходит вызов функции complAbs , при этом имена фактических параметров и никак не связаны с именами формальных параметров и x2 , важен только их тип и порядок. Пример 4.2. В предыдущем примере функция сначала определена, а затем используется (вызывается. В некоторых случаях сохранить данный порядок невозможно например, одна функция может вызывать другую, а та, в свою очередь, — первую. Тогда приходится использовать предварительные объявления функций (листинг 4.2 ). Листинг 4.2: Предварительное объявление функции i n c l u d e 2 # i n c l u d e 3 # i n c l u d e 4 5 using namespace std ; 6 7 double complAbs ( double , double ); 8 9 i n t main () { 10 double a1 , a2 , x ; 11 SetConsoleOutputCP (1251); 12 cout << "Введите␣действительную␣и␣мнимую␣часть\n" 13 "комплексного␣числа:\n"; 14 cin >> a1 >> a2 ; 15 x = complAbs ( a1 , a2 ); 16 cout << "Модуль" << x << endl ; 17 system ("pause"); 18 return 0; 19 } 20 — 109 — 21 double complAbs ( double x1 , double x2 ) В сроке 7 объявлена функция при этом имена её формальных параметров писать необязательно важны только их типы. В строке 15 она вызывается с указанием фактических параметров. И только в строке 21 начинается определение этой функции (здесь имена формальных параметров обязательны). Рекурсивные функции. Функция называется рекурсивной, если она вызывает сама себя (прямо или косвенно. Пример рекурсивной функции при- ведён в листинге 4.3 Листинг 4.3: Рекурсивная функция для вычисления факториала i n c l u d e using namespace std ; // Рекурсивная функция - вычисление факториала числа n: long fact ( i n t n ){ i f ( n < 0) // Если отрицательное число - return 0; // возвращаем 0. i f ( n == 0) return 1; // 0! = 1 e l s e // В остальных случаях - return n * fact ( n - 1); // рекурсия n t main (){ i n t n ; setlocale (0, ""); // Включаем кириллицу — 110 — cout << "Введите␣число␣для␣вычисления␣факториала␣=␣"; cin >> n ; cout << n << "!␣=␣" << fact ( n ) Программист должен следить затем, чтобы рекурсивная функция обязательна имела терминальную ветвь рано или поздно процесс рекурсивных вызовов должен закончиться. Выполнение рекурсивной функции связано с дополнительным расходом времени и памяти для манипуляций со стеком и может вызвать переполнение стека, поэтому обычно стараются изменить алгоритм так, чтобы заменить рекурсию обычным циклом, например, как показано в листинге 4.4 Листинг 4.4: Нерекурсивная функция для вычисления факториала Нерекурсивная функция - вычисление факториала n: long fact ( i n t n ){ i f ( n < 0) // Если отрицательное число - return 0; // возвращаем 0. long k = 1; f o r ( i n t i =1; i < n ; ) k *= +Ханойские башни. В листинге 4.5 приведён пример использования рекурсивной функции для решения задачи о Ханойских башнях. Даны три стержня, на один из которых нанизаны колец, причём кольца отличаются размером и лежат в порядке уменьшения диаметра (меньшее на большем. Задача состоит в том, чтобы перенести пирамиду из колец на — 111 третий стержень. Вторым стержнем можно пользоваться для временного размещения колец. Надо решить задачу за наименьшее число ходов. За один ход разрешается переносить только одно кольцо, причём нельзя класть большее кольцо на меньшее. Листинг 4.5: Ханойские башни i n c l u d e 2 using namespace std ; 3 4 s t a t i c long count ; // счётчик ходов (глобальная переменная 6 void hanoi_towers ( i n t n , // число колец n t from , // номер начального стержня n t to , // номер конечного стержня n t buf // номер промежуточного стержня) { 11 i f ( n != 0) { 12 hanoi_towers ( n -1, from , buf , to ); 13 :: count ++; 14 cout << from << "␣->␣" << to << endl ; 15 hanoi_towers ( n -1, buf , to , from ); 16 } 17 } 18 19 i n t main () { 20 i n t n , start , dest , buf ; 21 setlocale ( LC_ALL , "rus"); // кодировка для кириллицы 23 cout << "Номер␣первого␣стержня␣(от␣1␣до␣3)␣=␣"; 24 cin >> start ; 25 cout << "Номер␣конечного␣стержня␣(от␣1␣до␣3)␣=␣"; 26 cin >> dest ; 27 cout << "Номер␣промежуточного␣стержня␣(от␣1␣до␣3)␣=␣"; — 112 — 28 cin >> buf ; 29 cout << "Количество␣дисков␣=␣"; 30 cin >> n ; 31 32 :: count = 0; 33 hanoi_towers ( n , start , dest , buf ); 34 cout << "Сделано" << :: count << "␣ходов\n"; 35 system ("pause"); 36 return 0; 37 } Доказано, что колец можно переложить заходов. Согласно буддийской легенде, конец мира наступит, когда будет окончена игра с 64 стержнями. К счастью, для этого потребуется 18 446 744 073 709 551 615 ходов, что займёт 580 миллиардов лет, если мы делаем один ход за секунду. Встраиваемые функции. При определении функции компилятор созда- ёт соответствующий машинный код в области памяти с некоторым адресом. При каждом вызове функции в машинный код вставляются команды для копирования в стек параметров (а также адреса возврата) и команда перехода на тот адрес, по которому расположен машинный код функции. После выполнения функции компилятор генерирует машинные команды, которые очищают стеки возвращают управление на команду, следующую заточкой вызова. Все эти операции связаны с расходом времени и памяти. Если функция небольшая (соответствует малому числу команд процессора, то её можно оформить как встраиваемую. Тогда компилятор по возможности не будет размещать машинный код функции в отдельной области памяти, а будет генерировать для каждого вызова функции соответствующие ей машинные команды стек при этом не используется. Чтобы сделать функцию встраиваемой, в её заголовке надо написать ключевое слово inline , например n l i n e double sum ( double a , double b ){ — 113 Объявление функции встраиваемой выражает лишь пожелание программиста компилятор оценивает размеры функции и сам решает, выполнять это пожелание или игнорировать. Спецификатор inline будет проигнорирован, если функция является ре- курсивной. Перегрузка функций. Часто бывает необходимо реализовать один и тот же алгоритм для разного набора типов данных. В таких случаях удобно, чтобы соответствующие функции имели одинаковое имя. Использование нескольких функций с одними тем же именем, но разным набором типов параметров, называется перегрузкой. Компилятор определяет, какую именно функцию следует вызвать, по типу фактических параметров (разрешение перегрузки. Тип возвращаемого значения при этом не используется. Например, в программе имеются перегруженные функции max для нахождения максимального из двух чисел n t max ( i n t , i n t ); long max ( long , long ); f l o a t max ( f l o a t , f l o a Тогда при вызове double a =1.3, b =2.5, c ; c = max ( a , b ); будет использована последняя функция max . Если компилятор не найдёт точного соответствия между типами фактических и формальных параметров перегруженных функций, то будет сделана попытка преобразования типов по стандартным правилам. Если с помощью различных вариантов преобразований будет обнаружена возможность использования нескольких разных функций (неоднозначность разрешения перегрузки, то компилятор выдаст сообщение об ошибке. Тогда программист должен будет использовать явное приведение типов параметров. В листинге 4.6 показан пример неоднозначности разрешения перегрузки. Листинг 4.6: Неоднозначность разрешения перегрузки i n c l u d e 2 using namespace std ; 3 4 i n t max ( i n t a , i n t b ){ 5 return a > b ? a : b ; 6 } 7 long max ( long a , long b ){ 8 return a > b ? a : b ; 9 } 10 f l o a t max ( f l o a t a , f l o a t b ){ 11 return a > b ? a : b ; 12 } 13 double max ( double a , double b ){ 14 return a > b ? a : b ; 15 } 16 17 i n t main (){ 18 double x1 =3, x2 =4.5, y ; 19 y = max ( x1 , x2 ); 20 i n t i =1, j =-3, k ; 21 k = max ( i , j ); 22 double a =1.0, z ; 23 i n t b = 7; 24 z = max ( a , b ); // Здесь ошибка компиляции ..... 26 return 0; 27 } — 115 Здесь для вызова функции max в строке 24 имеются три кандидата l o a t max ( f l o a t , f l o a поэтому компилятор выдаст сообщение об ошибке. Однако, если написать, (то программа будет скомпилирована без ошибок. Параметры функций Стек и параметры функции. Вызовы функций в сложных программах могут быть вложены вдруг друга одна функция вызывает другую, та — третью и т.д. При каждом вызове необходимо передать значения параметров и запомнить адрес возврата (куда вернуться после того, как выполнение функции завершится. Для управления вызовами функций используется специальным образом организованная область памяти, называемая стеком. Стек — это буфер, который функционирует по правилу последним во- шёл — первым вышел (LIFO — Last In, First Out). Именно в стеке размещает компилятор адрес возврата и значения фактических параметров функции при её вызове. Когда выполнение функции завершается (после выполнения её последнего оператора или при выполнении команды return ), параметры убираются из стека вместе с адресом возврата и управление передаётся поэтому адресу, тек оператору, находящемуся в тексте программы после вызова только что отработавшей функции. Параметры функции могут передаваться вне по значению, адресу (указателю) или ссылке. Способ передачи задаётся типом формального параметра в заголовке функции при её объявлении и определении. Передача параметров по значению. При передаче по значению в стек заносятся копии значений параметров. Операторы вызванной функции работают с этими копиями, а нес переменными вызвавшей программы, поэтому любые изменения значений этих копий, размещённых в стеке, никак не отражается назначениях исходных переменных в вызвавшей программе. Пример 4.3. Передача параметров по значению. В листинге 4.7 опре делена функция fun1 , имеющая три вещественных параметра и не возвращающая результата ( void ). Все три параметра переданы по значению (такой режим действует в C/C++ по умолчанию, но внутри функции значение третьего параметра изменяется. В конце данной функции оператор return можно опустить (или указать его без параметра). Листинг 4.7: Передача параметров по значению) { 2 z = x + y ; 3 return ; // можно опустить 6 i n t main () { 7 double a , b , c =0; 8 cin >> a >> b ; 9 fun1 ( a , b , c ); // вызов функции будет выведен 0 Далее функция вызывается с фактическими параметрами a , b и. Третий параметр при вызове имеет нулевое значение. Оно не изменится и после вызова (на экран будет выведено число, хотя соответствующий формальный параметр изменил своё значение внутри функции Если мы хотим, чтобы изменения значений параметров функции переходили в вызвавшую программу, надо использовать передачу параметров по адресу или по ссылке — 117 Передача параметров по адресу (указателю). Чтобы передать какой- либо параметр функции по адресу, используются указатели. При этом в стек копируется не значение переменной, а её адрес. |