Как мы видели, с помощью операций над указателями можно вручную организовать передачу по ссылке, однако такой подход несколько неуклюж. Во-первых, вам приходится выполнять все операции посредством указателей. Во-вторых, вы должны помнить, что при вызове функции ей следует передавать не значения переменных, а их адреса. К счастью, в С++ имеется возможность заставить компилятор автоматически использовать для одного или нескольких параметров конкретной функции передачу по ссылке вместо передачи по значению. Это достигается с помощью параметра-ссылки. Если вы используете параметр-ссылку, функции автоматически передается адрес (не значение) аргумента. Внутри функции при операциях над параметром-ссылкой автоматически выполняется снятие ссылки, в результате чего исчезает необходимость в использовании указателей.
Параметр-ссылка объявляется помещением перед именем параметра в объявлении функции знака &. Операции с использованием параметра-ссылки фактически выполнятся над аргументом, указанным при вызове функции, а не над самим параметром-ссылкой.
Чтобы показать применение параметров-ссылок (и для демонстрации их преимуществ) в приведенную ниже программу включен вариант функции swap(), использующей ссылки. Обратите особое внимание на объявление и вызов функции swap():
// Использование функции swap() с параметрами-ссылками.
#include <iostream>
using namespace std;
// Объявим swap() с использованием параметров-ссылок,
void swap(int &x, int &y);
int main()
{
int i, j;
cout << " Начальные значения i и j : " ;
cout << i << ' ' << j << '\n' ;
swap(i,j);
cout << "Новые значения i и j : " ;
cout << i << ' ' << j << '\n';
return 0;
}
/* Здесь swap() определена как использующая передачу по ссылкe,
а не передачу по значению.
*/
void swap(int &x, int &y)
{
int temp;
temp = x;
x =y;
y = temp;
}
Вывод этой программ будет такой же, как в предыдущем варианте. Еще раз обратите внимание на то, что при обращении к параметрам х и у с целью обмена их значений нет необходимости указывать оператор *. Компилятор автоматически определяет адреса аргументов, используемых при вызове swap(), и автоматически снимает ссылки с хи у.
Подытожим сказанное. Когда вы создаете параметр-ссылку, этот параметр автоматически ссылается на аргумент, используемый при вызове функции (т. е. явным образом указывает на него). Далее, отпадает необходимость указывать при аргументе оператор &. Так же и внутри функции параметр-ссылка используется непосредственно; оператор * не нужен. Все операции с параметром-ссылкой автоматически воздействуют на аргумент, указанный при вызове функции. Наконец, когда вы присваиваете параметру-ссылке некоторое значение, фактически вы присваиваете это значение переменной, на которую указывает ссылка. Применительно к функциям это будет та переменная, которая указана в качестве аргумента при вызове функции.
Последнее замечание. Язык С не поддерживает ссылки. Таким образом, организовать передачу параметра по ссылке в С можно только с помощью указателя, как это было показано в первом варианте функции swap(). Переводя программу с языка С на язык С++, вы можете, где это будет оправдано, преобразовать параметры-указатели в параметры-ссылки.
В этом подразделе вы познакомитесь с одной из самых захватывающих средств С++: перегрузкой функций. В С++ две или даже несколько функций могут иметь одно и то же имя при условии, что различаются объявления их параметров. Такие функции называются перегруженными, а само это средство называют перегрузкой функций. Перегрузка функций является одним из способов реализации полиморфизма в С++.
Для того, чтобы перегрузить функцию, достаточно объявить другой вариант функции с тем же именем. Компилятор возьмет на себя все остальное. Вы только должны соблюсти одно важное условие: типы или число параметров (или и то, и другое) каждой из перегруженных функций должны различаться. Для перегруженных функций недостаточно различия только в типе возвращаемых значений. Они должны различаться типом или числом их параметров. (Типы возврата не во всех случаях предоставляют достаточную информацию для С++, чтобы тот мог решить, какую из функций использовать.) Разумеется, перегруженные функции могут различаться также и типами возвращаемых значений. Когда вызывается перегруженная функция, реально выполняется тот вариант, у которого параметры соответствуют аргументам вызова.
Начнем с простого программного примера:
// Перегрузим функцию трижды.
#include <iostream>
using namespace std;
#include <conio>
void f(int i); //параметр типа int
void f(int i, int j); //два параметра типа int <
void f(double k) ; //один параметр типа double
int main(){
f(10) ; // вызов f(int) I
f(10, 20); // вызов f(int, int)
f(12.23); // вызов f(double)
getch();
return 0;
}
void f (int i)
{
cout << "B f(int) i равно " << i << '\n';
}
void f(int i, int j)
{
cout << "B f(int, int) i равно " << i;
cout << " , j равно " << j << '\n' ;
}
void f (double k)
{
cout << "B f(double) к равно " << k << '\n';
}
Эта программа выведет на экран следующее:
f(int) i равно 10
f(int, int) i равно 10, j равно 20
f(double) k равно 12.23
Итак, f( ) перегружена трижды. Первый вариант требует один целочисленный параметр, второй вариант требует два целочисленных параметра, и третий вариант требует один параметр типа double. Из-за того, что списки параметров каждой функции различаются, компилятор имеет возможность выбрать требуемый вариант функции исходя из типа аргументов, указанных при вызове функции.
Для того, чтобы осознать ценность перегрузки функций, рассмотрим функцию, названную neg(), которая возвращает отрицание своего аргумента. Например, при вызове с числом -10 функция возвращает 10. При вызове с числом 9 функция возвращает -9. Не имея средства перегрузки функций, вы должны были бы для данных с типами int, double и long разработать три отдельные функции с различающимися именами, например, ineg( ), lneg( ) и fneg( ). Благодаря перегрузке вы можете для всех функций, возвращающих отрицание своего аргумента, использовать одно имя, например, neg( ). Таким образом, перегрузка поддерживает концепцию полиморфизма "один интерфейс, много методов". Приведенная ниже программа демонстрирует эту концепцию:
// Создание различных вариантов функции neg ().
#include <iostream>
using namespace std;
int neg(int n); // neg() для int.
double neg(double n) ; // neg () для double,
long neg (long n) ; // neg () для long.
int main () {
cout << "neg(-10): " << neg(-10) << "\n";
cout << "neg(9L): " << neg(9L) << "\n";
cout << "neg(11.23): " << neg(11.23) << "\n";
return 0;
}
// neg()для int.
int neg(int n) {
return -n;
}
// neg()для double,
double neg(double n)
{
return -n;
}
// neg()для long.
long neg(long n) {
return -n;
}
В программе предусмотрены три схожие, но все же разные функции с именем neg, каждая из которых возвращает обратное значение своего аргумента. Компилятор определяет, какую функцию вызывать в каждой ситуации, по типу аргумента вызова.
Ценность перегрузки заключается в том, что она позволяет обращаться к набору функций с помощью одного имени. В результате имя neg описывает обобщенное действие. Обязанность выбрать конкретный вариант для конкретной ситуации возлагается на компилятор. Вам, программисту, нужно только запомнить имя обобщенного действия. Таким образом, благодаря применению полиморфизма число объектов, о которых надо помнить, сокращается с трех до одного. Хотя приведенный пример крайне прост, вы можете, развив эту концепцию, представить, как перегрузка помогает справляться с возрастающей сложностью.
Другое преимущество перегрузки функций заключается в возможности определять слегка различающиеся варианты одной и той же функции, каждый из которых предназначен для определенного типа данных. В качестве примера рассмотрим функцию с именем min( ), которая находит меньшее из двух значений. Нетрудно написать варианты min(), которые будут выполняться по-разному для данных различных типов. Сравнивая два целых числа, min( ) вернет меньшее из них. Сравнивая два символа, min( ) может вернуть букву, стоящую в алфавите ранее другой, независимо от того, прописные эти буквы или строчные. В таблице ASCII прописные буквы имеют значения, на 32 меньшие, чем соответствующие строчные. Таким образом, игнорирование регистра букв может быть полезным при упорядочении по алфавиту. Сравнивая два указателя, можно заставить min( ) сравнивать значения, на которые указывают эти указатели, и возвращать указатель на меньшее из них. Ниже приведена программа, реализующая все эти варианты min( ):
Сравнивает значения и возвращает указатель на меньшее значение.*/
int * min (int *a, int *b)
{
if (*a < *b) return a;
else return b;
}
Вот вывод этой программы:
min('X', 'а'): а
min(9, 3) : 3
*min(&i, &j): 10
Когда вы перегружаете функцию, каждый вариант этой функции может выполнять любые нужные вам действия. Нет никаких правил, устанавливающих, что перегруженные функции должны быть похожими друг на друга. Однако с точки зрения стиля перегрузка функции предполагает их взаимосвязь. Поэтому, хотя вы и можете дать одно и то же имя перегруженным функциям, выполняющим совсем разные действия, этого делать не следует. Например, вы можете выбрать ими sqr()для функций, одна из которых возвращает квадрат int, а другая - квадратный корень из значения double. Однако эти две операции фундаментально различаются, и использование для них понятия перегрузки функций противоречит исходной цели этого средства. (Такой способ программирования считается исключительно дурным стилем!) В действительности перегружать следует только тесно связанные операции.