Семестр 2
Практическое занятие 3 . Ссылки и указатели. Функции.
Что нужно предварительно знать для выполнения задания:
1. Ссылки (reference) и указатели.
В С++ используются две конструкции для косвенного обращения к переменной: указатели и ссылки. Указатели и ссылки подчиняются разным синтаксическим правилам! Переменная-указатель просто содержит адрес объекта, с помощью которого можно получить косвенный доступ к данному объекту (занятие 2). Переменная-ссылка создает альтернативное имя для объекта. Такая переменная тоже (как и указатель) содержит адрес объекта, но синтаксически ею можно (и нужно) пользоваться как самим объектом.
1) Ссылки необходимо инициализировать при объявлении:
int nX = 0;
int& refX = nX; //объявление переменной-ссылки refX на переменную nX (то есть refX становится альтернативным именем для nX)
int& refY; //ошибка компилятора – нет инициализирующего значения
extern int& refZ; //правильно – refZ проинициализирована в другом месте
2) Несмотря на то, что ссылка является указателем, синтаксически ее используют, как будто она является объектом:
int nI = 3;
int& refJ = nI; //refJ – имя для ссылки на nI (то есть альтернативное имя для nI)
if(refJ == 3) //на самом деле сравнивается содержимое nI с 3
{
refJ--; //тот же результат, что и nI--
}
3) Нельзя объявить ссылку типа void.
void& ref = 3; //это неверно!
4) Использование ссылок так, как это показано в 1) и 2) на мой взгляд весьма экзотично, а в основном ссылки используются в качестве параметров функции и возвращаемых значений – смотри Передача параметров «по ссылке». .
5) Ссылка на указатель обозначается следующим образом:
int n = 1;
int* pn = &n;
int*& refN = pn; //и можете использовать N вместо указателя pn
*refN = 2; // переменной n будет присвоено значение 2
// int*& refM = &n; //а это неверно, так как не существует реального //объекта, на который ссылалась бы refM
6) Ссылку можно интерпретировать как константный указатель, при каждом использовании которого происходит разыменование. Исходя из такой интерпретации необходимо помнить, что ссылка не является объектом, над которым можно выполнять операции (то есть значение ссылки после инициализации изменить нельзя!). Например:
int nN = 1;
int* const nPtr = &nN; //константный указатель
nPtr++; //ошибка компилятора
int& refN = nN; //ссылка
refN++; //увеличивается значение nN, а не refN
Замечание: указатель может быть равен NULL (это значение говорит о том, что указатель "никуда не указывает"). Ссылка всегда "ссылается" на объект, которым она была инициализирована!
7) Чтобы получить указатель на объект, именем которого является ссылка refX, можно применить к ссылке операцию получения адреса объекта – «&»:
int iX;
int& refX = iX;
int* pI = &refX; //pI указывает на iX
8) Ссылка на константу.
double& dRef1 = 1.1; //ошибка компилятора
const double& dRef2 = 1.1; //корректно, но особого смысла, на мой взгляд, не имеет
2. Понятие функции
В языке C функции эквивалентны подпрограммам в Бейсике, подпрограммам или функциям в Фортране или процедурам в Паскале, PL1и т. д. Они принимают параметры, выполняют инструкции (называемые телом функции), а затем возвращают управление вызвавшей программе.
Язык C разрабатывался таким образом, чтобы сделать использование функций легким, удобным и эффективным (чему Вы и должны научиться в ходе данного занятия). В C++ каждая функция должна иметь объявление (прототип, шаблон функции) и определение.
Замечание: только в единственном случае, когда определение функции и вызов этой функции находятся в одном файле и определение предшествует вызову, объявление функции можно опустить.
Хорошей практикой является помещение прототипов функций в заголовочный файл (*.h), подключаемый директивой препроцессора #include к тексту программы.
3. Прототип функции.
- это «предварительное» объявление, которое извещает компилятор о типе возвращаемого значения, количестве и типах передаваемых аргументов. Используя прототип, компилятор может выполнить контроль числа аргументов и проверить соответствие их типов в вызовах функции и ее определении (а при необходимости сделать неявное преобразование типа).
[тип] имя_функции([список_аргументов] | [void]); ,где
тип – задает тип возвращаемого функцией значения. Если поле отсутствует, по умолчанию функция должна возвращать тип int (Замечание: не экономьте на “умолчании” - хорошим стилем программирования считается указание типа возвращаемого значения – всегда!). Если поле содержит ключевое слово void, функция не возвращает никакого значения. Так как на организацию возврата тратится несколько машинных инструкций, использование void позволяет получать более короткий и производительный код.
Пример:
char MyFunc(); //функция возвращает переменную типа char
char* MyFunc(); //функция возвращает переменную типа указатель на char
void MyFunc(); //функция не возвращает никакого значения
MyFunc(); //возвращаемый тип – int по умолчанию.
int MyFunc(); //то же самое, что и в предыдущем объявлении. Явное указание возвращаемого типа является предпочтительным!
имя_функции – это особый тип указателя, называемый - указатель на функцию. Его значением является адрес точки входа в функцию (адрес первой инструкции). Необязательным правилом является написание всех значащих составляющих имени с заглавной буквы, например: OnMouseMove.
список_аргументов – определяет количество и тип аргументов (параметров), передаваемых в функцию.
список_аргументов == тип_аргумента1 [имя_аргумента1] , тип_аргумента2 [имя_аргумента2]...
Если функция имеет несколько аргументов, каждая пара –тип_аргумента [имя_аргумента] – разделяются запятыми. Поле имя_аргумента является необязательным и может быть опущено, но хорошим тоном считается присутствие формальных имен, так как это упрощает использование и документацию функций (см. замечание ниже).
например:
int MyFunc( const char* szTarget, char chSearchChar, int nStartAt );//функция принимает параметры: 1)указатель на строку для поиска, 2)символ, который следует найти, 3)позиция, с которой следует начать поиск. Возвращает найденную позицию
int MyFunc( const char*, char, int ); //то же самое, но ничего не понятно
Если в функцию не передаются никакие аргументы, это поле пустое или содержит ключевое слово void.
например: double* MyFunc(void);
double* MyFunc();
Замечание: в прототипе функции важны типы параметров, а не их имена (компилятор просто игнорирует имена аргументов). Имена параметров используются в основном для «подсказки» программисту – какую смысловую нагрузку несет данный параметр.
4. Определение функции.
Определение функции включает те же поля, что и прототип функции плюс тело функции – код, выполняемый при вызове функции (заключенный в фигурные скобки).
Например:
int MyFunc(int nN1, int nN2)
{
int nN=nN1+nN2;
return nN;
}
Список аргументов, приводимый в определении функции, называется «списком формальных аргументов». Задание конкретных значений для аргументов функции делается в вызывающей Си-программе при вызове функции.
Для каждого вызова функции с аргументами компилятор добавляет в точке вызова машинные инструкции, которые записывают копии аргументов в стек. Функция в процессе исполнения пользуется копиями аргументов в стеке как локальными переменными, обращаясь к ним по именам, приведенным в списке формальных аргументов. Копии аргументов в стеке, с которыми функция будет оперировать, называют фактическими аргументами.
5. Вызов функции
5.1. Вызов функции, объявленной без спецификатора inline
Встречая в исходном тексте программы вызов функции, например:
int nN1 = 2;
int nN2 = 3;
int nResult = Sum(nN1, nN2); //вызов функции
а) компилятор использует прототип функции ( int Sum(int nX, int nY); )для сравнения типов аргументов и при необходимости делает преобразование типов. (Если преобразование с точки зрения компилятора некорректно, будет выдана соответствующая ошибка компилятора).
б) компилятор вставляет в код вызывающей программы последовательность машинных команд, которые помещают в стек копии переменных (nN1, nN2) перечисленных в списке аргументов. Замечание: вместо Sum(nN1, nN2) Вы можете указать непосредственно константы – Sum(2,3).
в) компилятор вставляет в код программы команду вызова процедуры с возвратом. Выполняя данную команду, процессор передает управление из вызывающей процедуры в вызываемую.
г) если функция должна возвращать значение, в вызываемой функции должен быть оператор
return выражение; //в нашем примере выражение должно преобразовываться к типу int
Транслируя этот оператор, компилятор сформирует возвращаемое значение (при необходимости приведя в нашем случае к типу int) и вставит машинную команду возврата из функции, по которой управление возвращается вызвавшей процедуре.
д) если функция ничего не возвращает, Вы можете использовать оператор Си return; без параметра, а можете и вовсе его опустить (машинную команду возврата за Вас сгенерирует компилятор, транслируя закрывающую фигурную скобку функции).
Замечание: оператор return в соответствующей форме (return; или return выражение) может встречаться внутри функции много раз – как только возникает необходимость возврата.
5.2. Вызов функции, объявленной со спецификатором inline
Функцию можно определить со спецификатором inline. Такие функции называются встроенными. Например:
inline int Sum(int x, int y) //встроенная функция
{
return (x+y);
}
Спецификатор inline указывает компилятору, что он должен пытаться каждый раз при вызове данной функции сгенерировать код функции прямо в месте вызова вместо того, чтобы единожды создать код функции в некоторой области памяти, а в месте вызова передавать туда управление (смотри 5.1). Следует отметить, что компилятор далеко не всегда удовлетворяет Ваш запрос о “встроенности” функции.
Замечание 1: Не злоупотребляйте inline-функциями! Встроенными рекомендуется делать только очень "маленькие" функции. При этом текст программы остается структурированным, а "накладных" расходов на вызов функции удается избежать.
Замечание 2: используйте inline-функции вместо макросов. Пример:
Вместо #define SQUARE(x) (x)*(x)
пользуйтесь: inline double Square(double x) { return x*x;}
6. Передача параметров функции
В общем случае существуют два стиля передачи параметров функции:
6.1. передача параметров по значению (Call-By-Value)
– это простая передача копий переменных в функцию, не оставляющая никаких возможностей для изменения самих переменных в вызывающей процедуре. Например:
int ScaleSum(int nX, int nY); //сложение с домножением аргументов на масштабные коэффициенты
void main(void)
{
int nA=1, nB=5, nC;
nC = ScaleSum(nA, nB); //в функцию передаются копии значений переменных nA и nB, помещенные в стек!=> После возвращения из функции значения переменных nA и nB не изменяются
}
int ScaleSum(int nX, int nY)
{ //в нашем случае nX=1, nY=5
nX = nX*3; //изменяются копии переданных значений!
nY = nY*2; //изменяются копии переданных значений!
return (nX + nY);
}
Передача параметров по значению применяется в тех случаях, когда общий объем передаваемых аргументов невелик и функция не возвращает большой объем данных.
6.2. передача адресов переменных (Call-By-Reference)
-предполагает, что в качестве аргументов функции передаются не копии переменных, а копии адресов переменных. Получив адрес объекта, функция получает доступ к тому объекту, который хранится по этому адресу, то есть получает возможность изменить значение объекта! Вызовы функций с передачей адреса объекта подразделяются на вызовы с передачей указателя на параметр («по указателю») и с передачей параметра «по ссылке». Замечание: такое деление является условным, но имеет смысл, так как указатели и ссылки подчиняются разным синтаксическим правилам.
6.2.1. Передача параметров «по адресу» (с помощью указателя).
Модифицируем программу, приведенную в пункте передача параметров по значению (Call-By-Value):
int ScaleSum(int* pnX, int* pnY); //сложение с домножением аргументов на масштабные коэффициенты
void main(void)
{
int nA=1, nB=5, nC;
nC = ScaleSum(&nA, &nB); //в процессе выполнения функции значения переменных nA и nB изменятся!
}
int ScaleSum(int* pnX, int* pnY)
{ //содержимое по адресу pnX равно 1
//содержимое по адресу pnY равно 5
*pnX = *pnX*3; //изменяются сами значения!
*pnY = *pnY*2; //изменяются сами значения!
return (*pnX + *pnY);
}
Вызов функции с передачей параметров по-адресу позволяет разрабатывать функции, имеющие доступ к массивам и другим протяженным объектам данных. (Замечание: если в список переменных включено имя массива, то в функцию передается только адрес начала массива). Например, так выглядит функция, выполняющая копирование строки:
int MyStrcpy(char* pcDest, const char* pcSource)
{
…
}
6.2.2. Передача параметров «по ссылке».
Передается ссылка (специальный вид указателя), что позволяет использовать синтаксически эту ссылку как объект. Синтаксис передачи по ссылке на первый взгляд кажется странным, поэтому для начала предлагаю запомнить формальное правило: «Если в прототипе и определении функции в списке аргументов фигурирует параметр - ссылка на тип, то в вызывающей процедуре в качестве параметра следует передавать значение объекта»
В следующем примере переменная nX передается по ссылке и модифицируется функцией IncIt():
void IncIt(int& x) //объявление функции – обратите внимание на то, что функция принимает аргумент-ссылку
{
x++; //изменится значение переменной в вызывающей программе
}
void main()
{
int nX=5; //инициализация переменной nX
IncIt(nX); //передача ссылки на переменную nX в качестве параметра функции IncIt(), где ее значение будет модифицировано!
}
7. Переменное число аргументов
Язык программирования С++ допускает использование переменного числа аргументов. Классическим примером такой функции является printf() – «старая» стандартная библиотечная функция. Признаком функции с переменным числом аргументов является многоточие в списке аргументов прототипа функции. Встретив (...), компилятор прекращает контроль соответствия типов параметров для такой функции.
Например:
int Func(int i, ...); //функция может иметь один и более параметров
int Func2(int i, char byte, ...); //функция может иметь не менее двух параметров
Естественно, что функция с переменным числом аргументов должна иметь способ определения точного их числа при каждом вызове (в случае printf() – таким способом является подсчет символов «%» в первом аргументе-строке). Язык С++ использует несколько макрокоманд для доступа к параметрам (макросы определены в stdarg. h). Следующий пример иллюстрирует вызов функции с переменным числом параметров. Использованы макросы: va_start, va_arg, va_end и структура типа va_list (используется для хранения информации, необходимой для макрокоманд va_arg и va_end)
#include <cstdio>
#include <cstdarg>
int Average( int nFirst, ... ); //прототип функции с переменным числом параметров
void main( void )
{
//Вызов функции с тремя целыми параметрами (-1 используется как признак конца).
printf( "Average is: %d\n", Average( 2, 3, 4, -1 ) );
//Вызов функции с четырьмя целыми параметрами
printf( "Average is: %d\n", Average( 5, 7, 9, 11, -1 ) );
// Вызов функции только с признаком конца
printf( "Average is: %d\n", Average( -1 ) );
}
//Функция возвращает среднее значение переменного числа целых параметров
int Average( int first, ... )
{
int count = 0, sum = 0, i = first;
va_list marker;
va_start( marker, first ); /* Initialize variable arguments. */
while( i!= -1 )
{
sum += i;
count++;
i = va_arg( marker, int);
}
va_end( marker ); /* Reset variable arguments. */
return( sum? (sum / count) : 0 );
}
В файле stdarg. h тип va_list и va_...-макросы определены следующим образом:
typedef char * va_list; //используется для описания указателя на начало списка переданных функции фактических аргументов
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )//, где v-это имя параметра, предшествующего первому необязательному параметру. Устанавливает указатель на первый необязательный параметр
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //”сдвигает” указатель на следующий аргумент, а вычисляет значение текущего параметра
#define va_end(ap) ( ap = (va_list)0 )//обнуляет указатель
8. Значения аргументов по умолчанию.
В С++ можно задавать значения аргументов по умолчанию (значения таких аргументов задаются в прототипе функции). Параметры со значением по умолчанию должны являться последними в списке параметров. При вызове данной функции программист может не указывать параметр по умолчанию – компилятор автоматически добавит значение, заданное в прототипе.
Например:
int SaveName(char* pName, char* pLastName = “” );
По умолчанию можно использовать больше одного аргумента, но все они должны располагаться в конце списка аргументов.
Например:
int SaveName(char* pFirstName,
char* pSecondName = “”,
char* pThirdName = “”,
char* pFourthName = “”);
Данный код показывает, что при реальном вызове функции SaveName() программист может опустить параметры pSecondName, pThirdName, pFourthName.
SaveName(“Первый”);
Если какой-либо из аргументов по умолчанию определяется пользователем, то все предшествующие аргументы по умолчанию тоже должны быть определены явно. В нашем случае, для того, чтобы определить значение для pThirdName, необходимо также явно задать значение для pSecondName:
SaveName(“Первый”,”Второй”,”Третий”); //или хотя бы
SaveName(“Первый”,””,”Третий”);
, а значение для pFourthName можно опустить.
9. Возвращаемое значение.
Функция может возвращать а)объект одного из базовых типов, б) объект агрегатного типа, а также в) указатель или ссылку на объект. Значения а) и в) объектов возвращаются в машинных регистрах. Массивы могут быть возвращены только по адресу. Механизм возвращения объектов структур или классов более сложен и будет рассмотрен позже.
10. Ключевое слово const и функции.
10.1. Передача функции константных параметров.
Язык С++ позволяет объявлять параметры функции с ключевым словом const:
void SomeFunc(const int* pn);
Такое объявление параметра pn обеспечивает «неизменяемость» объекта, на который указывает pn, функцией SomeFunc(). То есть объявление функций с константными параметрами позволяет компилятору блокировать нежелательные побочные эффекты вызова функций.
Например:
void Func(const char* pc);
void main()
{
Func(“1234”);
}
void Func(const char* pc)
{
*pc = ‘A’; //ошибка компилятора
}
10.2. Возвращение константных значений
Вполне допустимо объявить функцию, которая возвращает константу:
const char* GetName();
void main()
{
const char* cp = GetName(); //переменная cp указывает на константную строку=> любая попытка изменить значение строки вызовет ошибку компилятора.
cp[2] = 'A'; //ошибка компилятора
}
const char* GetName()
{
return “Name”;
}
11. Проблемы при возвращении ссылки или указателя
При возвращении ссылки или указателя из функции объект, на который указывает ссылка или указатель, должен существовать после возврата из функции. При написании функций, возвращающих указатель или ссылку, начинающий программист часто допускает следующие ошибки:
int* GetValue()
{
int nN;
...
return &nN; //никогда так не делайте! Ваша функция возвратит указатель на область памяти в стеке, которая после возвращения из функции может быть задействована для других целей
}
int& GetValue()
{
int nN;
...
return nN; //и так тоже никогда не делайте! Ваша функция по-прежнему возвращает указатель на временную переменную!
}
Возвращать можно указатели и ссылки на статические объекты или объекты, созданные по new.
12. Рекурсивные функции
то есть функции, вызывающие сами себя. Рекурсивные вычисления выполняются повторным вхождением в одну и ту же функцию; каждое вхождение имеет свою область стека для копий аргументов и других локальных переменных. Поэтому каждое вхождение в функцию можно рассматривать как абсолютно не зависящее от других вхождений. Достоинством рекурсивных функций является возможность создания компактного кода, особенно при использовании сложных типов данных. Недостатками рекурсивных вычислений являются: затраты времени на вызов функции и передачу ей копий параметров и «затраты» памяти (стека) для организации каждого «вложенного» вызова.
13. Указатель на функцию.
Сочетая основные типы данных в языке С++, можно образовать неограниченное число более сложных производных типов. В частности, одним из таких производных типов является указатель на функцию. Объекты этого типа можно использовать как аргументы при вызове функций, хранить в массиве и использовать другим способом.
13.1. Объявление такого объекта выглядит следующим образом:

Скобки вокруг *Func необходимы, поскольку иначе объявление int *Func(); означало бы функцию, возвращающую указатель на int.
Если функция принимает параметры, то их типы указываются в скобках также, как при объявлении функции:
char* (*Func2)(char*, int); //указатель на функцию, которая принимает два параметра (указатель на char и целое значение) и возвращает указатель на char
13.2. Присвоить переменной Func значение указателя на функцию можно следующим образом:
int RealFunc(); //объявление функции
void main()
{
...
int (*Func)(); //объявление указателя на функцию, не принимающую параметров и возвращающую тип int
Func = RealFunc;
}
int RealFunc() //определение функции
{
int nN;
............
return nN;
}
13.3. Вызов функции с помощью указателя:

13.4. Использование указателей на функции в качестве аргументов.
13.4.1. Объявление функции, принимающей в качестве аргумента указатель:
void Func( void (*Func1)(int, int*), int (*Func2)(char, char*) );
-объявлена функция, принимающая:
1)указатель на функцию, принимающую два аргумента – int и указатель на int и ничего не возращающую
2) указатель на функцию, принимающую два аргумента (char и указатель на char) и возращающую int
13.4.2. Вызов функции:
void F1(int n, int* pn)
{
}
int F2(char c, char* pc)
{
int nResult;
return nResult;
}
void main()
{
Func(F1, F2);
}
14. Массив указателей на функции
Как и обычные переменные, указатели на функции можно объединить в массив.
14.1. Объявление массива указателей на функции:

Например, объявить и инициализировать массив указателей на функции можно следующим образом:
int cmp_year(const void* p1, const void* p2)
{
}
int cmp_month(const void* p1, const void* p2)
{
}
int cmp_day(const void* p1, const void* p2)
{
}
void main()
{
int (*fcmp[3])(const void*, const void*) = {cmp_year, cmp_month, cmp_day };
}
14.2. Доступ к элементам массива указателей на функции выполняется как к обычным элементам массива:
int* ptr1;
int* ptr2;
(*fcmp[0])(ptr1, ptr2); //вызывает функцию cmp_year
15. .”Перегрузка” функций.
Одной из особенностей С++ является возможность «перегрузки» функций, то есть использования одного и того же имени для разных функций. Техника «перегрузки» давно используется для базовых операций С++. Например, существует только одно имя для операции сложения – ”+”, но Вы используете это имя для сложения целых чисел, чисел с плавающей точкой, прибавляете константу к указателю..., может быть даже не задумываясь о том, что компилятор при этом генерит совершенно разные машинные инструкции. Идея перегрузки операций легко распространяется на функции, определяемые пользователем. Иногда такой подход бывает очень удобен, когда функции выполняют одинаковые (по смыслу) действия над объектами разных типов. Компилятор различает такие функции по числу и типу параметров. При каждом конкретном вызове перегруженной функции компилятор должен определить, какую из функций с данным именем вызвать. Идея состоит в том, чтобы использовать функцию с наиболее подходящими аргументами или выдать сообщение об ошибке, если подходящей функции не найдено. При этом действуют следующие ограничения:
а) не могут перегружаться функции, имеющие совпадающие тип и число аргументов, но разные типы возвращаемых значений;
б) не могут перегружаться функции, имеющие неявно совпадающие типы аргументов, например int и int&.
Пример:
1) без перегрузки функций:
int MaxInt(int x, int y) { return (x>y) ? x : y; }
double MaxDouble(double x, double y) { return (x>y) ? x : y; }
void main()
{
int i = MaxInt( 12, 8 ); //в зависимости от типа аргументов Вы вызываете
double d = MaxDouble( 32.9, 17.4 ); //разные функции При этом Вы должны помнить имена функций, аргументы...
}
2) с перегрузкой функций. В зависимости от типа аргументов компилятор сам генерирует вызов разных функций:
int Max(int x, int y) { return (x>y) ? x : y; }
double Max(double x, double y) { return (x>y) ? x : y; }
void main()
{
int i = Max( 12, 8 ); //вызов Max(int, int)
double d = Max( 32.9, 17.4 ); //вызов Max(double, double)
double dd = Max( 1.1 , 2 ); //ошибка компилятора. Компилятор считает, что не может найти однозначного соответствия =>выдает ошибку
}
Замечание: механизм перегрузки основывается на относительно сложном наборе правил для неявного преобразования типов аргументов => в некоторых случаях даже если компилятор не выдает ошибки, на первый взгляд неочевидно, какая функция вызовется. Поэтому для того, чтобы избежать ошибок при компиляции или неприятностей при неявном преобразовании типов аргументов, пользуйтесь явным преобразованием для точного соответствия типов параметров:
double dd = Max( 1.1 , static_cast<double>(2) ); //вызов
Max(double, double)


