Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ПОСОБИЕ ЯПиМТ 2010 new.doc
Скачиваний:
28
Добавлен:
14.08.2019
Размер:
2 Мб
Скачать
☆
  • Использование указателей

    • набрать и отладить следующую программу:

    int main(int argc, char* argv[])

    {

    int a=7, b=2;

    int *ptr=NULL;

    cout<<"\na="<<a<<", &a="<<&a;

    cout<<", b="<<b<<", &b="<<&b;

    ptr=&a;

    cout<<"\nДействие: ptr=&a. Результат: ptr="

    <<ptr<<", *ptr="<<*ptr;

    ptr=&b;

    cout<<"\nДействие: ptr=&b. Результат: ptr="

    <<ptr<<", *ptr="<<*ptr;

    *ptr=137;

    cout<<"\nДействие: *ptr=137. Результат: b="<<b;

    ptr++;

    *ptr=-105;

    cout<<"\nДействие: ptr++, *ptr=-105. Результат: ptr="

    <<ptr<<", *ptr="<<*ptr<<endl;

    return 0;

    }

    • объяснить работу программы.

    Занятие 7

    Лабораторная работа № 7 Исследование массивов и указателей

    Цель занятия:

    • Совершенствование навыков разработки программ в среде программирования MS Visual C++

    • Совершенствование навыков в использовании циклов для работы с массивами

    • Совершенствование навыков в программировании с использованием указателей

    • Исследование взаимосвязи между массивами и указателями

    • Исследование способов создания динамических массивов

    Время на выполнение работы: 2 часа

    Программа исследований:

    1. Исследование взаимосвязи между массивами и указателями

    2. Исследование способов создания динамических массивов

    Подготовка к выполнению работы:

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

    2. Практически освоить порядок работы на ПЭВМ в интегрированной среде разработки MS Visual C++.

    Материалы для подготовки к занятию:

    1. Конспект лекций

    2. [1] стр. 51-63

    Содержание отчета:

    1. Цели исследования.

    2. Программу работы.

    3. Листинги программ

    4. Результаты исследований

    5. Выводы по каждому пункту и общий вывод.

    Методические рекомендации

    Массивы и указатели в Си тесно связаны и могут быть использованы почти эквивалентно. Имя массива можно понимать как константный указатель на первый элемент массива. Его отличие от обычного указателя только в том, что его нельзя модифицировать. Например, задан целочисленный массив A на 5 элементов:

    int A[5]={-2,5,4,-17,3};

    Посмотрим, как этот массив разместится в оперативной памяти (рисунок 7.1).

    .

    .

    .

    A[4]

    0x0012FF90

    A[3]

    0x0012FF8C

    A[2]

    0x0012FF88

    A[1]

    0x0012FF84

    A[0]

    0x0012FF80

    .

    .

    .

    const int *A

    0х0012FF70

    .

    .

    .

    Рисунок 7.1 – Представление массива элементов в памяти ЭВМ

    Обратите внимание, что под хранение значения каждого элемента массива выделяется четыре ячейки памяти, что видно из чередования адресов ячеек памяти через четыре байта. Это связано с тем, что тип этого массива – int. Причем, под хранение адреса начала массива выделяется тоже четыре байта, так как все указатели для 32-разрядных процессоров 4-х байтные.

    Указатели можно использовать для выполнения любой операции, включая индексирование массива. Пусть вы сделали следующее объявление:

    int b[5] = {1,2,3,4,5}, *Pt;

    Тем самым вы объявили массив целых чисел b[5] и указатель на целое Pt. Поскольку имя массива является указателем на первый элемент массива, вы можете задать указателю Pt адрес первого элемента массива b с помощью оператора Pt = b;

    Это эквивалентно присваиванию адреса первого элемента массива следующим образом: Pt = &b[0];

    Теперь можно сослаться на элемент массива b[3] с помощью выражения *(Pt + 3).

    Указатели можно индексировать точно так же, как и массивы. Например, выражение Pt[3] ссылается на элемент массива b[3].

    Или, например, определение int a[5]; задает массив из пяти элементов а[0], a[1], a[2], a[3], a[4]. Если объект *у определен как

    int *у;

    то оператор

    у = &a[0];

    присваивает переменной у адрес элемента а[0]. Если переменная у указывает на очередной элемент массива а, то y+1 указывает на следующий элемент, причем здесь выполняется соответствующее масштабирование для приращения адреса с учетом длины объекта (для типа int – 4 байта, long - 4 байта, (double - 8 байт и т.д.).

    Так как само имя массива есть адрес его нулевого элемента, то оператор у = &a[0]; можно записать и в другом виде: у = а. Тогда элемент а[1] можно представить как *(а+1). С другой стороны, если у - указатель на массив a, то следующие две записи: a[i] и *(у+i) эквивалентны. Рассмотрим пример:

    int main(int argc, char* argv[])

    {

    int a[5]={-5,0,34,12,-17};

    cout<<endl<<(a+2); // оператор 1

    cout<<endl<<&a[2]; // оператор 2

    cout<<endl<<*(a+2); // оператор 3

    cout<<endl<<a[2]; // оператор 4

    cout<<endl;

    return 0;

    }

    Так, операторы 1 и 2 выведут одно и то же значение – адрес элемента массива с индексом 2. А операторы 3 и 4 – одно и то же значение 34.

    Между именем массива и соответствующим указателем есть одно важное различие. Указатель - это переменная и у = а или y++ - допустимые операции. Имя же массива - константа, поэтому конструкции вида a = y, a++ использовать нельзя, так как значение константы постоянно и не может быть изменено.

    Указатели могут применяться как операнды в арифметических выражениях, выражениях присваивания и выражениях сравнения. Однако, не все операции, обычно используемые в этих выражениях, разрешены применительно к переменным указателям.

    С указателями может выполняться ограниченное количество арифметических операций. Указатель можно увеличивать (++), уменьшать (--), складывать с указателем целые числа (+ или +=), вычитать из него целые числа (- или -=) или вычитать один указатель из другого.

    Сложение указателей с целыми числами отличается от обычной арифметики. Прибавить к указателю 1 означает сдвинуть его на число байтов, содержащихся в переменной, на которую он указывал. Обычно подобные операции применяются к указателям на массивы. Если продолжить приведенный выше пример, в котором указателю Pt было присвоено значение b - указателя на первый элемент массива, то после выполнения оператора

    Pt += 2;

    Pt будет указывать на третий элемент массива b. Истинное же значение указателя Pt изменится на число байтов, занимаемых одним элементом массива, умноженное на 2. Например, если каждый элемент массива b занимает 2 байта, то значение Pt (т.е. адрес в памяти, на который указывает Pt) увеличится на 4.

    Аналогичные правила действуют и при вычитании из указателя целого значения.

    Переменные указатели можно вычитать один из другого. Например, если Pt указывает на первый элемент массива b, а указатель Pt1 - на третий, то результат выражения Pt1 - Pt будет равен 2 - разности индексов элементов, на которые указывают эти указатели. И так будет, несмотря на то, что адреса, содержащиеся в этих указателях, различаются на 4 (если элемент массива занимает 2 байта).

    Арифметика указателей теряет всякий смысл, если она выполняется не над указателями на массив. Сравнение указателей операциями >, <, >=, <= также имеют смысл только для указателей на один и тот же массив. Однако, операции отношения == и != имеют смысл для любых указателей. При этом указатели равны, если они указывают на один и тот же адрес в памяти.

    Динамические массивы создают с использованием операции new, при этом необходимо указать тип и размерность, например:

    int a=10;

    int *ptr=new int [a];

    В этой строке создается переменная-указатель на int, в динамической памяти отводится непрерывная область, достаточная для размещения 10 элементов целого типа, и адрес ее начала записывается в указатель ptr. Динамические массивы нельзя при создании инициализировать, и они не обнуляются.

    Доступ к элементам динамического массива осуществляется точно так же, как к статическим, например, к элементу номер 5 можно обратиться как ptr[5] или *(ptr+5).

    Альтернативный способ создания динамического массива – использование функции malloc библиотеки Си:

    int a=10;

    int *ptr=(int*)malloc(a*sizeof(int));

    Операция преобразования типа, записанная перед обращением к функции malloc, требуется потому, что функция возвращает значение указателя типа void*.

    Память, зарезервированная под динамический массив с помощью new[], должна освобождаться оператором delete [], а память, выделенная функцией malloc – посредством функции free, например:

    delete [] ptr;

    free(ptr);

    Размерность массива в операции delete не указывается, но квадратные скобки обязательны.

    Задание на исследование

    1. Исследование взаимосвязи между массивами и указателями

    1. Описать два массива и два вспомогательных указателя.

    2. Проинициализировать указатели адресами массивов.

    3. Исследовать возможность обмена адресами начала массивов через указатели.

    4. Исследовать возможность обмена значениями указателей.

    5. Исследовать возможность обращения к элементу массива через индекс и через вспомогательный указатель.

    6. Исследовать возможность обращения к элементу массива через индекс и через имя массива.

    7. Исследовать возможность изменения значения элемента массива через вспомогательный указатель.

    8. Исследовать возможность изменения значения элемента массива через имя массива без использования индекса.

    1. Исследование способов создания динамических массивов

    1. Создать динамический массив с использованием операции new[].

    2. Создать динамический массив с использованием функции malloc.

    3. Исследовать возможность удаления первого массива функцией free().

    4. Исследовать возможность удаления второго массива операцией delete [].

    Занятие 8

    Лабораторная работа № 8 Применение функций работы со строками для решения прикладных задач

    Цель занятия:

    • Совершенствование навыков разработки программ в среде программирования MS Visual C++

    • Совершенствование навыков использования циклов и ветвлений в программах

    • Получение начальных навыков в объявлении, инициализации и использовании символьных массивов

    • Получение начальных навыков в обработке строк в языке Си

    Время на выполнение работы: 2 часа

    Учебные вопросы:

    1. Изучение способов формирования строк в языке Си

    2. Применение функций работы со строками для обработки символьных массивов

    Подготовка к выполнению работы:

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

    2. Изучить материал настоящего руководства.

    Материалы для подготовки к занятию:

    1. Конспект лекций

    2. [1] стр. 63-65.

    Основные теоретические сведения

    В Cи отсутствует специальный тип строк. Строки рассматриваются как массивы символов, оканчивающиеся нулевым символом ('\0'). Строка доступна через указатель на первый символ в строке. Значением строки является адрес ее первого символа. Таким образом, можно сказать, что в Cи строка является указателем - указателем на первый символ строки. В этом смысле строки подобны массивам, потому что массив тоже является указателем на свой первый элемент (рисунок 8.1).

    .

    .

    .

    A[4]

    \0

    0x0012FF84

    A[3]

    я

    0x0012FF83

    A[2]

    с

    0x0012FF82

    A[1]

    а

    0x0012FF81

    A[0]

    В

    0x0012FF80

    .

    .

    .

    const char *A

    0x0012FF80

    0x0012FF70

    .

    .

    .

    Рисунок 8.1 – Представление массива элементов в памяти ЭВМ

    В приведенном примере объявлен символьный массив A на 5 элементов: char A[]=”Вася”;

    Обратите внимание на то, что теперь под хранение каждого символа выделяется один байт памяти, а под указатель будет по-прежнему выделено 4 байта. Необходимо отметить, что в приведенном примере размер массива не указан, он определен автоматически по длине строки-инициализатора, причем, автоматически добавлен символ окончания строки.

    Строка может быть объявлена либо как массив символов, либо как переменная типа char*. Каждое из двух приведенных ниже эквивалентных объявлений

    char S[] = "строка";

    char *Sp = "строка";

    присваивает строковой переменной начальное значение "строка". Первое объявление создает массив из 7 элементов S содержащий символы 'с', 'т', 'р', 'о', 'к', 'а' и '\0'. Второе объявление создает переменную указатель Sp, который указывает на строку с текстом "строка", лежащую где-то в памяти. Но в любом случае число хранимых символов на 1 больше числа значащих символов за счет оконечного нулевого символа.

    Доступ к отдельным символам строки осуществляется по индексам, начинающимся с нуля. Например, S[0] и Sp[0] - первые символы объявленных выше строк, S[1] и Sp[1] - вторые и т.д.

    В приведенных объявлениях длина строк определялась автоматически компилятором. Можно объявлять строковые переменные заданной длины. Например, оператор char buff[100];

    объявляет переменную buff, которая может содержать строку до 99 значащих символов плюс заключительный нулевой символ.

    Теперь приступим к знакомству с наиболее полезным применением символь­ных указателей: запоминанию массивов строк. На самом деле нельзя создать массив строк, но можно запомнить массив символьных ука­зателей, каждый из которых содержит адрес строки в памяти.

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

    Все двумерные таблицы, которые рассматривались ранее, выровнены с обеих сторон. Например, объявим символьную таблицу с 5 строками и 10 колонка­ми, каждая строка которой содержит одинаковое число символов. Ее можно определить следующим образом:

    char names [ 5 ] [ 10 ] = { { "Сергей" },

    { "Михаил" },

    { "Николай" },

    { "Иван" },

    { "Алексей" }};

    Она показана на рисунке 8.2. Обратите внимание, в таблице много про­белов. Каждая строка занимает 10 символов даже в том случае, когда символов в строке намного меньше. Незаполненные элементы содержат символ ограничитель строки, потому что Cи записывает этот символ во все неинициализированные вами элементы массива. Этот тип таблиц занимает слиш­ком много места в памяти.

    Колонки

    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    0

    С

    е

    р

    г

    е

    й

    \0

    1

    М

    и

    х

    а

    и

    л

    \0

    Строки 2

    Н

    и

    к

    о

    л

    а

    й

    \0

    3

    И

    в

    а

    н

    \0

    4

    А

    л

    е

    к

    с

    е

    й

    \0

    Рисунок 8.2 – Пример двумерного символьного массива строк

    Для устранения проблемы нерационального использования памяти объ­явим одномерный массив символьных указателей. Каждый указатель со­держит адрес сроки в памяти, и эти строки могут иметь разную длину. Вот примеры определения таких массивов:

    char *names [ 5 ] =

    {

    { "Сергей" },

    { "Михаил" },

    { "Николай" },

    { "Иван" },

    { "Алексей" }

    };

    или просто

    char *names [ ] =

    {

    { "Сергей" },

    { "Михаил" },

    { "Николай" },

    { "Иван" },

    { "Алексей" }

    };

    Это одномерные массивы. Хотя раньше ничего подобного не рассматривалось, не следует смущаться. Звездочка перед names говорит о том, что это мас­сив указателей. Указатели имеют символьный тип. Строки не присваива­ются элементам массива, но в массиве содержатся адреса этих строк. На ри­сунке 8.3 показан этот массив указателей. Строки могут находиться в любом месте памяти. Их местоположение не существенно, так как каждый указатель содержит адрес первого элемента соответствующей строки. Строки не занимают лишнюю память; каждая строка использует ровно столько пространства, сколько нужно для ее размещения и символа ограничителя строки. Такая структура содержит данные в форме с рваными краями.

    names

    [0]

    С

    е

    р

    г

    е

    й

    \0

    [1]

    М

    и

    х

    а

    и

    л

    \0

    [2]

    Н

    и

    к

    о

    л

    а

    й

    \0

    [3]

    И

    в

    а

    н

    \0

    [4]

    А

    л

    е

    к

    с

    е

    й

    \0

    Рисунок 8.3 – Массив указателей

    Для вывода первой строки можно использовать оператор cout:

    cout « *names; // Печать Сергей

    Для вывода второй строки используется следующий оператор cout:

    cout *(names+1); // Печать Михаил

    При обращении к элементу массива при помощи оператора *, получаем доступ к одной из строк массива. Можно использовать возвращаемое этим оператором значение везде, где можно использовать строковую констан­ту или символьный массив.

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

    • копирования строк (strcpy, strncpy);

    • сравнения строк (strcmp, strncmp);

    • объединения строк (strcat, strncat);

    • поиска подстроки (strstr);

    • поиска вхождения символа (strchr, strrchr, strpbrk);

    • определения длины строки (strlen);

    • преобразования строки в число (atoi, atoll, atof);

    • и так далее.

    Более подробно функции работы со строками рассмотрены в [1] стр. 91, 416.

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

    1. Функции проверки на принадлежность символа множеству:

      • isalmum(c) – букв и цифр (A-Z, a-z, 0-9);

      • isalfa(c) – букв (A-Z, a-z);

      • iscntrl(c) – управляющих символов (с кодами 0…31 и 127);

      • isdigit(c) – цифр (0-9);

      • isgraph(c) – печатаемых символов кроме пробела;

      • islower(c) – букв нижнего регистра (a-z);

      • isprint(c) – печатаемых символов;

      • ispunct(c) – знаков пунктуации;

      • isspace(c) – символов-разделителей;

      • isupper(c) – букв верхнего регистра (A-Z);

      • isxdigit(c)- шестнадцатеричных цифр.

    2. Функции преобразования символов:

    • tolower(c) - к нижнему регистру;

    • toupper(c) – к верхнему регистру.

    Более подробно функции работы с символами рассмотрены в [1] стр. 92, 416.

    Программа работы

    1. Изучение способов формирования строк в языке Си

      • Набрать и отладить программу:

    int main(int argc, char* argv[])

    {

    char Arr1[25];

    char Arr2[]={"Second string!"};

    char Arr3[]={'T','h','i','r','d',' ',

    's','t','r','i','n','g'};

    char *Arr4="Fouth string";

    cout<<"\nEnter string: \"First string\": ";

    cin>>Arr1;

    cout<<"\nString 1: "<<Arr1; // Оператор 1

    cout<<"\nString 2: "<<Arr2; // Оператор 2

    cout<<"\nString 3: "<<Arr3; // Оператор 3

    cout<<"\nString 4: "<<Arr4; // Оператор 4

    return 0;

    }

    • Проанализировать результат работы программы и ответить на вопрос, почему Оператор 2 и Оператор 4 выводят на экран свои фразы полностью, а оператор 1 – только ее часть.

    • Почему Оператор 3 выводит свою фразу своей строки и чужую.

    • Что нужно сделать, чтобы Оператор 3 выводил только свою фразу.

    1. Применение функций работы со строками для обработки символьных массивов

    • Набрать и отладить программу заполнения числового массива из символьной строки:

    int main(int argc, char* argv[])

    {

    char str[]="2, 75.3, 24.45, 55, 0, 0.37";

    char *ptr=str; //Оператор 1

    float Arr[10];

    int i=0;

    printf("\nИсходная строка:\n%s\n",str);

    do

    {

    Arr[i]=atof(ptr); //Оператор 2

    i++;

    if (i>9) break;

    }while(ptr=strchr(ptr,','),ptr++); //Оператор 3

    printf(“\nМассив чисел: \n“)

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

    printf("%5.2f\n", Arr[s]);

    return 0;

    }

    • Проанализировать результат работы программы и ответить на вопрос: что происходит при выполнении Операторов 1-3.

    Занятие 9

    Лабораторная работа № 9 Использование функций для решения прикладных задач

    Цель занятия:

    • Совершенствование навыков разработки программ в среде программирования MS Visual C++

    • Совершенствование навыков использования циклов и ветвлений в программах

    • Получение начальных навыков в объявлении и использовании функций

    Время на выполнение работы: 2 часа

    Учебные вопросы:

    1. Изучение порядка объявления и описания функций

    2. Изучение порядка вызова функций

    Подготовка к выполнению работы:

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

    2. Изучить материал настоящего руководства.

    Материалы для подготовки к занятию:

    1. Конспект лекций

    2. [1] стр. 73-78.

    ОСНОВНЫЕ ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ

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

    С понятием функции в языке C++ связано три следующих компонента:

    • описание функции;

    • прототип;

    • вызов функции.

    Описание функции состоит из двух частей: заголовка и тела. Описание функции имеет следующую форму записи:

    [тип_результата] <имя_функции> [спсок_параметров])

    {

    /* объявления и операторы

    или, другими словами, тело функции*/

    }

    Здесь тип_результата — тип возвращаемого значения. В случае отсутствия спецификатора типа предполагается, что функция возвращает целое значение (int). Если функция не возвращает никакого значения, то на месте типа записывается спецификатор void. В списке параметров для каждого параметра должен быть указан тип. При отсутствии параметров список может быть пустым или иметь спецификатор void.

    Тело функции представляет собой последовательность объявлений и операторов, описывающих определенный алгоритм. Важным оператором тела функции является оператор возврата в точку вызова: return [выражение];. Оператор return имеет двойное назначение. Он обеспечивает немедленный возврат в вызывающую функцию и может использоваться для передачи вычисленного значения функции. В теле функции может быть несколько операторов return, но может не быть и ни одного. В последнем случае возврат в вызывающую программу происходит после выполнения последнего оператора тела функции.

    Прототип функции может указываться до вызова функции вместо описания функции для того, чтобы компилятор мог выполнить проверку соответствия типов аргументов и параметров. Прототип функции по форме такой же, как и заголовок функции, в конце его ставится «;». Параметры функции в прототипе могут иметь имена, но компилятору они не нужны.

    Компилятор использует прототип функции для сравнения типов аргументов с типами параметров. Язык Cи не предусматривает автоматического преобразования типов в случаях, когда аргументы не совпадают по типам с соответствующими им параметрами, т. е. язык Cи обеспечивает строгий контроль типов. При наличии прототипа вызываемые функции не обязаны размещаться в одном файле с вызывающей функцией. То есть, описание тел функций можно разместить в другом файле, который необходимо подключить к исходному с использованием директивы #include.

    Вызов функции может быть оформлен в виде оператора, если у функции отсут­ствует возвращаемое значение, или в виде выражения, если существует возвращаемое значение.

    В первом случае оператор имеет следующий формат:

    [имя_функции] (список_аргументов);

    Например: void Ptint(“Hello World!”);

    Во втором случае выражение записывается следующим образом:

    [Переменная] = [имя_функции] (список_аргументов);

    Например: z=max(x,y);

    Значение вычисленного выражения является возвращаемым значением функции. Возвращаемое значение передается в место вызова функции и является результатом ее работы. Число и типы аргументов должны совпадать с числом и типом параметров функции. При вызове функции параметры подставляются вместо аргументов. Ниже приводится пример функции с возвращаемым значением.

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

    Возможное решение данной задачи имеет вид:

    /* включение файла max.cpp с

    телом функции max */

    #include "max.cpp"

    void main()

    {

    int x,y,z;

    cout << "\n поочередно введите х и у \n";

    cin >> х; cin >> y;

    z=max (х, y); /* вызов функции */

    cout << "z=" << z;

    return;

    }

    Описание функции max находится в отдельном файле max.cpp, находящемся в том каталоге, из которого запускается исходная программа, и имеет следующий вид:

    int max (int a, int b)

    {

    int с; //

    c=(a>b)?a:b; // тело функции

    return с; //

    }

    Вызов функции является выражением в правой части оператора присваивания z=max(x,y);, при выполнении которого значения аргументов х и у подставляются вместо параметров а и b соответственно (передача параметров в функцию по значению). После выполнения тела функции возвращаемое значение передается в место вызова функции и присваивается переменной z. Описание функции находится в файле, поэтому для включения файла в программу необходимо в тексте программы указать директиву препроцессора #include "max.cpp".

    Описание функции может находиться в одном файле с главной программой. При этом директива #include "max.cpp" не указывается, а вместо нее помещается описание функции. В рассмотренной программе функция имеет возвращаемое значение. Существуют за­дачи, которые не требуют передачи возвращаемого значения. Такой пример приведен ниже.

    Пример 2. Использование функции без возвращаемого значения.

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

    Void print(char); // прототип функции

    void main()

    {

    char x;

    cout <<”\nВводите символы.”

    <<” Enter – окончание ввода”;

    do

    {

    x=getch();

    print (x);

    } while (x!= 13);

    return 0;

    }

    void print(char a) { cout <<a << “\t“; }

    В функции рrint( ) отсутствует возвращаемое значение, поэтому обращение к функции осуществляется оператором вызова функции рrint(x);. В результате обращения к функции на экран будет выведен символ, введенный с клавиатуры с помощью оператора x=getch().

    Язык Си позволяет использовать процедуру перегрузки функций. Суть этого заключается в использовании одного и того же имени функции для решения различных, но схожих по назначению задач. Для реализации этого механизма описывается каждый из вариантов такой функции. Например, рассмотрим задачу нахождения среднего значения двух чисел. При реализации этой задачи требуется описывать отдельную функцию для целых чисел, чисел с плавающей точкой и их комбинации. Это связано с тем, что каждый передаваемый параметр должен иметь свой тип (int, float, double). Следовательно, если в функцию для нахождения среднего значения целых чисел передать числа с плавающей точкой, то в функции в соответствии с прототипом эти числа преобразуются путем отбрасывания дробной части в целые, что приведет к неправильному результату. Поэтому, для решения этой задачи пришлось бы описывать несколько функций с разными именами для разных типов данных. Механизм перегрузки функций позволяет для решения этой задачи использовать несколько функций с одним именем, при этом решение о выборе того или иного варианта функции для конкретного типа данных принимает сама программа.

    Рассмотрим листинг такой программы:

    Пример 3. Использование механизма перегрузки функции

    void PrintData(int *s, int N);

    void PrintData(char *w);

    void main()

    {

    int *a,n; char str[50];

    cout<<”\nEnter the array size: “; cin>>n;

    a=new int[n];

    for (int i=0;i<n;i++) a[i]=rand()%101-50;

    PrintData(a,n);

    cout<<”\nEnter a string: “; cin>>str;

    PrintData(str);

    return;

    }

    void PrintData(int *s, int N)

    {

    cout<<endl;

    for (int i=0;i<N;i++) cout<<s[i]<<” “;

    return;

    }

    void PrintData(char *w)

    {

    cout<<endl<<w; return;

    }

    Как видно из примера, функция PrintData() имеет одно имя, но два разных набора параметров. При вызове функции по списку передаваемых параметров автоматически вызывается тот вариант функции, который соответствует этому набору параметров. Если же функции с таким набором параметров нет, то компилятор выдаст сообщение об ошибке при компиляции листинга программы.

    Программа работы

    1. Изучение порядка объявления и описания функций

    • реализовать приведенные в теоретических сведениях примеры 1 - 3 ;

    • отладить и запустить программы;

    • разработать прототип и тело функции вычисления факториала числа.

    1. Изучение порядка вызова функций

    • разработать программу вычисления факториала числа с использованием пользовательской функции;

    • отладить и запустить ее в среде программирования.

    Занятие 10

    Лабораторная работа № 5

    Исследование способов работы с функциями

    Цель занятия:

    • Совершенствование навыков разработки программ в среде программирования MS Visual C++

    • Совершенствование навыков в программировании с использованием указателей

    • Исследование способов передачи параметров в функцию

    • Исследование порядка возвращения параметров из функции

    Время на выполнение работы: 2 часа

    Программа исследований:

      1. Исследование способа передачи параметров в функцию по значению

      2. Исследование способов передачи параметров в функцию по адресу

      3. Исследование порядка возвращения параметров из функции

    Подготовка к выполнению работы:

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

    2. Практически освоить порядок работы на ПЭВМ в среде программирования MS Visual C++.

    Материалы для подготовки к занятию:

    1. Конспект лекций

    2. [1] стр. 73-82

    Содержание отчета:

    1. Цели исследования.

    2. Программу работы.

    3. Листинги программ

    4. Результаты исследований

    5. Выводы по каждому пункту и общий вывод.

    Методические рекомендации

    Все переменные, объявленные в теле функции без указания класса памяти, имеют класс памяти auto, т.е. они являются локальными. При вызове функции локальным переменным отводится память в стеке и производится их инициализация. Управление передается первому оператору тела функции и начинается выполнение функции, которое продолжается до тех пор, пока не встретится оператор return или последний оператор тела функции. Управление при этом возвращается в точку, следующую за точкой вызова, а локальные переменные становятся недоступными. При новом вызове функции для локальных переменных память распределяется вновь, и поэтому старые значения локальных переменных теряются.

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

    Пример:

    /* использование параметров по значению */

    void change (int x, int y);

    void main()

    {

    int a=5,b=15;

    cout<<”\na=”<<a<<”, b=”<<b;

    change(a,b);

    cout<<”\na=”<<a<<”, b=”<<b;

    return;

    }

    void change (int x, int y)

    {

    int k=x;

    x=y;

    y=k;

    }

    В данной функции значения переменных x и y, являющихся формальными параметрами, меняются местами, но поскольку эти переменные существуют только внутри функции change, значения фактических параметров, используемых при вызове функции, останутся неизменными, то есть значения переменных a и b как были 5 и 15 соответственно, так и остались. Для того чтобы менялись местами значения фактических аргументов можно использовать функцию приведенную в следующем примере.

    Пример:

    /* использование параметров по адресу */

    void change (int *x, int *y)

    { int k=*x;

    *x=*y;

    *y=k;}

    При вызове такой функции в качестве фактических параметров должны быть использованы не значения переменных, а их адреса change (&a,&b);. Тогда функция будет оперировать не с локальными значениями, а со значениями по адресам, переданным в функцию в качестве параметров, что приведет к обмену значениями переменных a и b.

    Существует еще один способ передачи параметров в функцию – по ссылке. Механизм использования ссылок очень удобен и прост в использовании. Рассмотрим тот же самый пример, но с использованием механизма ссылок:

    Пример:

    /* использование параметров по ссылке */

    void change (int &x, int &y)

    { int k=x;

    x=y;

    y=k;}

    При вызове такой функции в качестве фактических параметров должны быть использованы имена переменных: change (a,b);. При этом, в функции мы будем работать с самими переменными a и b, но через альтернативные их имена: x и y.

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

    Функция возвращает значение если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и возвращается в точку вызова функции в качестве результата. Если оператор return не содержит выражения или выполнение функции завершается после выполнения последнего ее оператора (без выполнения оператора return), то возвращаемое значение не определено. Для функций, не использующих возвращаемое значение, должен быть использован тип void, указывающий на отсутствие возвращаемого значения. Если функция определена как функция, возвращающая некоторое значение, а в операторе return при выходе из нее отсутствует выражение, то поведение вызывающей функции после передачи ей управления может быть непредсказуемым.

    Задание на исследование

    1. Исследование способа передачи параметров в функцию по значению

      • реализовать в среде программирования следующий листинг:

    void Swap(int a, int b);

    int main(int argc, char* argv[])

    {

    int x,y;

    cout<<"\nEnter two numbers: ";

    cin>>x>>y;

    cout<<"\nx="<<x<<", y="<<y;

    Swap(x,y);

    cout<<"\nx="<<x<<", y="<<y;

    cout<<endl;

    return 0;

    }

    void Swap(int a, int b)

    {

    int c;

    c=a;

    a=b;

    b=c;

    return;

    }

    • с использованием режима отладки исследовать процесс исполнения программы в микропроцессоре. Обратить внимание на способ и место хранения значений локальных переменных a,b и c.

  • Исследование способа передачи параметров в функцию по адресу

    • реализовать в среде программирования следующий листинг:

    void Swap(int *a, int *b);

    int main(int argc, char* argv[])

    {

    int x,y;

    cout<<"\nEnter two numbers: ";

    cin>>x>>y;

    cout<<"\nx="<<x<<", y="<<y;

    Swap(&x,&y);

    cout<<"\nx="<<x<<", y="<<y;

    cout<<endl;

    return 0;

    }

    void Swap(int *a, int *b)

    {

    int c;

    c=*a;

    *a=*b;

    *b=c;

    return;

    }

    • с использованием режима отладки исследовать процесс исполнения программы в микропроцессоре. Обратить внимание на способ и место хранения значений локальных переменных a,b и c;

    • реализовать в среде программирования следующий листинг:

    void Swap(int &a, int &b);

    int main(int argc, char* argv[])

    {

    int x,y;

    cout<<"\nEnter two numbers: ";

    cin>>x>>y;

    cout<<"\nx="<<x<<", y="<<y;

    Swap(x,y);

    cout<<"\nx="<<x<<", y="<<y;

    cout<<endl;

    return 0;

    }

    void Swap(int &a, int &b)

    {

    int c;

    c=a;

    a=b;

    b=c;

    return;

    }

    • с использованием режима отладки исследовать процесс исполнения программы в микропроцессоре. Обратить внимание на способ и место хранения значений локальных переменных a,b и c;

    • сделать выводы по рассмотренным методам.