Параметры-ссылки и параметры-указатели

Лабораторна робота № 4

З дисципліни «Основи програмування»

Тема: Підпрограми. Методи передачі параметрів.

1. Зміст роботи

 

3.1. Проаналізувати завдання, скласти алгоритм рішення задачі.

3.2. У середовищі програмування набрати текст програми.

3.3. Здійснити компіляцію програми, виявити й усунути помилки компіляції.

3.4. Виконати програму в покроковому режимі й у режимі пуску. Переконатися в правильності реалізації завдання. У разі потреби - виправити помилки і повторити п. 3.3.

 

2. Завдання

Написати програму обробки числового масиву відповідно до індивідуального завдання.

Програма повинна мати текстове меню, складатися з підпрограм. Передбачити можливість вводу масиву з клавіатури і генерації випадкових значень та вивід результату обробки на екран.

 

3. Зміст звіту

1) Тема лабораторної роботи.

2) Короткі теоретичні відомості.

3) Завдання.

4) Структура програми та специфікації підпрограм.

5) Схема алгоритму (блок-схема та схема Нассі).

6) Текст програми.

7) Тестові приклади.

8) Результати виконання програми.

9) Аналіз результатів та висновки.

 

4. Контрольні питання

  1. Як бувають типи данних?
  2. Що таке масив?
  3. Що може бути компонентами масиву?
  4. Як отримати доступ до елементу масиву?
  5. Як динамічний масив?
  6. Як виконується операція new?
  7. Як виконується операція delete?
  8. Чи правильний запис?

а) int mas= new int[7];

б) int* mas = new int*[9];

в) int R=9; int * mas = &R; delete mas;

  1. Що таке вказівник?
  2. Що таке посилання?
  3. Напишіть функцію, яка знаходить суму, середнє арифметичне і середнє геометричне двох чисел, і повертає знайдені значення через вказівник і посилання.

12. Створіть масив дійсних чисел з кількістю елементів N=5, та присвойте першим трьом елементам значення 1,5; 23.87, 8.

  1. Які існують способи передачі параметрів в мові С++?
  2. Що таке список фактичних параметрів функції? Приклад.
  3. Що таке список формальних параметрів функції? Приклад.
  4. Напишіть прототип функції MyFunc, яка повертає дійсне число і приймає на вхід число типу int і два числа типу float.
  5. Таке опис функції?
  6. Як викликати функцію?
  7. Що таке тіло функції?

 

Завдання

Дано массив чисел розмірності N. N та елементи массиву ввести з клавіатури. Розробити функції для вводу масиву, виводу на екран, та обробки масиву відповідно до індивідуального завдання.

 

Теоретичні відомості

Підпрограма - частина програми, яка реалізує певний алгоритм і дозволяє звернення до неї з різних частин загальної (головної) програми. Підпрограма часто використовується для скорочення розмірів програм в тих задачах, в процесі розв'язання яких необхідно виконати декілька разів однаковий алгоритм при різних значеннях параметрів. Інструкції (оператори, команди), які реалізують відповідну підпрограму, записують один раз, а в необхідних місцях розміщують інструкцію виклику підпрограми.

Набір найвживаніших підпрограм утворює бібліотеку стандартних підпрограм.

Функції можуть приймати аргументи за значенням, вказівником або посиланням. Наприклад, функція void f(int& x) {x=3;} присвоює своєму аргументу значення 3. Функції також можуть повертати результат за значенням або посиланням. Наприклад, {double&b=a[3]; b=sin(b);} еквівалентно а[3]=sin(а[3]);. Посилання певною мірою схожі з вказівниками, з такими особливостями: при описі посилання ініціалізувалися вказівкою на існуюче значення даного типу; посилання довічно указує на одну і ту ж адресу; при зверненні до посилання операція читання пам'яті за адресою посилання проводиться автоматично. На відміну від вказівників, посилання не може бути константним саме по собі, однак може посилатися на константний об'єкт. Наприклад, int const & const ref = a[3]; на відміну від int const * const ref = &a[3]; — є некоректним, з точки зору С++, виразом; в свою чергу, і int const & ref = a[3];, і int const * ref = &a[3]; — є цілком прийнятними.

Можуть бути декілька функцій з одним і тим же ім'ям, але різними типами або кількістю аргументів (перевантаження функцій; при цьому тип значення, що повертається, на перевантаження не впливає). Наприклад, цілком можна писати:

void Print(int x);

void Print(double x);

void Print(int x, int y);

Один або декілька останніх аргументів функції можуть задаватися за умовчанням. Наприклад, якщо функція описана як void f(int x, int y=5, int z=10), виклики f(1), f(1,5) і f(1,5,10) еквівалентні.

 

 

Передача аргументов

Функции используют память из стека программы. Некоторая область стека отводится функции и остается связанной с ней до окончания ее работы, по завершении которой отведенная ей память освобождается и может быть занята другой функцией. Иногда эту часть стека называют областью активации.
Каждому параметру функции отводится место в данной области, причем его размер определяется типом параметра. При вызове функции память инициализируется значениями фактических аргументов.
Стандартным способом передачи аргументов является копирование их значений, т.е. передача по значению. При этом способе функция не получает доступа к реальным объектам, являющихся ее аргументами. Вместо этого она получает в стеке локальные копии этих объектов. Изменение значений копий никак не отражается на значениях самих объектов. Локальные копии теряются при выходе из функции.
Значения аргументов при передаче по значению не меняются. Следовательно, программист не должен заботиться о сохранении и восстановлении их значений при вызове функции. Без этого механизма любой вызов мог бы привести к нежелательному изменению аргументов, не объявленных константными явно. Передача по значению освобождает человека от лишних забот в наиболее типичной ситуации.
Однако такой способ передачи аргументов может не устраивать нас в следующих случаях:

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

· // swap() не меняет значений своих аргументов!

· void swap( int vl, int v2 ) {

· int tmp = v2;

· v2 = vl;

· vl = tmp;

}

swap() обменивает значения локальных копий своих аргументов. Те же переменные, что были использованы в качестве аргументов при вызове, остаются неизменными. Это можно проиллюстрировать, написав небольшую программу:

#include <iostream>

void swap( int, int );

int main() {
int i = 10;
int j = 20;

cout << "Перед swap():\ti: "
<< i << "\tj: " << j << endl;

 

swap( i, j );

 

cout << "После swap():\ti: "
<< i << "\tj: " << j << endl;

 

return 0;
}

Результат выполнения программы:

Перед swap(): i: 10 j: 20

После swap(): i: 10 j: 20

Достичь желаемого можно двумя способами. Первый – объявление параметров указателями. Вот как будет выглядеть реализация swap() в этом случае:

// pswap() обменивает значения объектов,

// адресуемых указателями vl и v2

void pswap( int *vl, int *v2 ) {

int tmp = *v2;

*v2 = *vl;

*vl = tmp;

}

Функция main() тоже нуждается в модификации. Вместо передачи самих объектов необходимо передавать их адреса:
pswap( &i, &j );
Теперь программа работает правильно:

Перед swap(): i: 10 j: 20

После swap(): i: 20 j: 10

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

// rswap() обменивает значения объектов,

// на которые ссылаются vl и v2

void rswap( int &vl, int &v2 ) {

int tmp = v2;

v2 = vl;

vl = tmp;

}

Вызов этой функции из main() аналогичен вызову первоначальной функции swap():

rswap( i, j );

Выполнив программу main(), мы снова получим верный результат.

Параметры-ссылки

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

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

Может возникнуть желание использовать параметр-ссылку, чтобы избежать создания копии большого объекта, но в то же время не дать вызываемой функции возможности изменять значение аргумента. Если параметр-ссылка не должен модифицироваться внутри функции, то стоит объявить его как ссылку на константу. В такой ситуации компилятор способен распознать и пресечь попытку непреднамеренного изменения значения аргумента.

 

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

void ptrswap( int *&vl, int *&v2 ) {

int *trnp = v2;

v2 = vl;

vl = tmp;

}


Объявление

int *&v1;

должно читаться справа налево: v1 является ссылкой на указатель на объект типа int. Модифицируем функцию main(), которая вызывала rswap(), для проверки работы ptrswap():

#include <iostream>

void ptrswap( int *&vl, int *&v2 );

int main() {
int i = 10;
int j = 20;

int *pi = &i;
int *pj = &j;

 

cout << "Перед ptrswap():\tpi: "
<< *pi << "\tpj: " << *pj << endl;

ptrswap( pi, pj );
cout << "После ptrswap():\tpi: "
<< *pi << "\tpj: " << pj << endl;

 

return 0;

 

}

Вот результат работы программы:

Перед ptrswap(): pi: 10 pj: 20

После ptrswap(): pi: 20 pj: 10

Параметры-ссылки и параметры-указатели

Когда же лучше использовать параметры-ссылки, а когда – параметры-указатели? В конце концов, и те и другие позволяют функции модифицировать объекты, эффективно передавать в функцию большие объекты типа класса. Что выбрать: объявить параметр ссылкой или указателем?

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

Параметр-ссылка не нуждается в этой проверке, так как всегда существует именуемый ею объект.

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

 

Параметры-массивы

Массив в С++ никогда не передается по значению, а только как указатель на его первый, точнее нулевой, элемент. Например, объявление

void putValues( int[ 10 ] );

рассматривается компилятором так, как будто оно имеет вид

void putValues( int* );

Размер массива неважен при объявлении параметра. Все три приведенные записи эквивалентны:

// три эквивалентных объявления putValues()

void putValues( int* );

void putValues( int[] );

void putValues( int[ 10 ] );

Передача массивов как указателей имеет следующие особенности:

  • • изменение значения аргумента внутри функции затрагивает сам переданный объект, а не его локальную копию. Если такое поведение нежелательно, программист должен позаботиться о сохранении исходного значения. Можно также при объявлении функции указать, что она не должна изменять значение параметра, объявив этот параметр константой:

void putValues( const int[ 10 ] );

  • размер массива не является частью типа параметра. Поэтому функция не знает реального размера передаваемого массива. Компилятор тоже не может это проверить. Рассмотрим пример:

· void putValues( int[ 10 ] ); // рассматривается как int*

· int main() {

· int i, j [ 2 ];

· putValues( &i ); // правильно: &i is int*;

· // однако при выполнении возможна ошибка

· putValues( j ); // правильно: j - адрес 0-го элемента - int*;

// однако при выполнении возможна ошибка

При проверке типов параметров компилятор способен распознать, что в обоих случаях тип аргумента int* соответствует объявлению функции. Однако контроль за тем, не является ли аргумент массивом, не производится.
По принятому соглашению C-строка является массивом символов, последний элемент которого равен нулю. Во всех остальных случаях при передаче массива в качестве параметра необходимо указывать его размер. Это относится и к массивам символов, внутри которых встречается 0. Обычно для такого указания используют дополнительный параметр функции. Например:

void putValues( int[], int size );

int main() {

int i, j[ 2 ];

putValues( &i, 1 );

putValues( j, 2 );

return 0;

}

putValues() печатает элементы массива в следующем формате:

( 10 )< 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >

где 10 – это размер массива. Вот как выглядит реализация putValues(), в которой используется дополнительный параметр:

#include <iostream>

const lineLength =12; // количество элементов в строке
void putValues( int *ia, int sz )
{
cout << "( " << sz << " )< ";
for (int i=0;i<sz; ++i )
{
if ( i % lineLength == 0 && i )
cout << "\n\t"; // строка заполнена

cout << ia[ i ];

// разделитель, печатаемый после каждого элемента,
// кроме последнего
if ( i % lineLength != lineLength-1 &&
i != sz-1 )
cout << ", ";
}
cout << " >\n";
}

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

// параметр - ссылка на массив из 10 целых

void putValues( int (&arr)[10] );

int main() {

int i, j [ 2 ];

putValues(i); // ошибка:

// аргумент не является массивом из 10 целых

putValues(j); // ошибка:

// аргумент не является массивом из 10 целых

return 0;

}

Поскольку размер массива теперь является частью типа параметра, новая версия putValues() способна работать только с массивами из 10 элементов. Конечно, это ограничивает ее область применения, зато реализация значительно проще:

#include <iostream>

void putValues( int (&ia)[10] )
{
cout << "( 10 )< ";
for ( int 1 =0; i < 10; ++i ) { cout << ia[ i ];

// разделитель, печатаемый после каждого элемента,
// кроме последнего
if ( i != 9 )
cout << ", ";
}
cout << " >\n";
}

Здесь matrix объявляется как двумерный массив, который содержит десять столбцов и неизвестное число строк. Эквивалентным объявлением для matrix будет:

int (*matrix)[10]

Многомерный массив передается как указатель на его нулевой элемент. В нашем случае тип matrix – указатель на массив из десяти элементов типа int. Как и для одномерного массива, граница первого измерения не учитывается при проверке типов. Если параметры являются многомерными массивами, то контролируются все измерения, кроме первого.
Заметим, что скобки вокруг *matrix необходимы из-за более высокого приоритета операции взятия индекса. Инструкция

int *matrix[10];объявляет matrix как массив из десяти указателей на int.