Передача имен функций в качестве параметров

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

Пример:

void f (int а) { / * . . . * / } // определение функции

void (*pf)(int); // указатель на функцию

pf = &f; // указателю присваивается адрес функции

// (можно написать pf = f;)

pf(10); // функция f вызывается через указатель pf

// (можно написать (*pf)(10))

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

// Описание типа PF как указателя на функцию с одним параметром типа int;

typedef void (*PF)(int);

// Описание и инициализация массива указателей:

PF menu[] = {&new, &open, &save};

menu[l](10); // Вызов функции open

Здесь new, openи save— имена функций, которые должны быть объявлены ранее.

Указатели на функции передаются в подпрограмму так же, как и параметры других типов.

Пример:

#include <iostream.h>

typedef void (*PF)(int);

void f1(PF pf){ // функция f l получает в качестве параметра указатель типа PF

pf(5); // вызов функции, переданной через указатель

}

void f(int i){cout << i;}

int main( ){

f1(f);

return 0;

}

Тип указателя и тип функции, которая вызывается посредством этого указателя, должны совпадать.

Параметры со значениями по умолчанию

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

В качестве значений параметров по умолчанию могут использоваться константы, глобальные переменные и выражения.

Пример:

int f (int а, int b = 0);

void f1 (int, int = 100, char* = 0); // пробел между * и = обязателен, без него

// получилась бы операция

// сложного присваивания *=

void err (int errValue = errno); // errno - глобальная переменная

f (100); f(a, 1); // варианты вызова функции f

fl (a); fl (a, 10); fl (a, 10, "Vasia"); // варианты вызова функции fl

fl (a,"Vasia") // неверно

Функции с переменным числом параметров

Если список параметров функции заканчивается многоточием, при ее вызове на этом месте можно указать еще несколько параметров.

Примером может быть функция стандартной библиотеки printf, прототип которой имеет вид:

int printf (const char*, ...);

Это означает, что вызов функции должен содержать, по крайней мере, один параметр типа char* и может либо содержать, либо не содержать другие параметры:

printf("Введите исходные данные"); // один параметр

printf("Сумма: %5.2f рублей", sum); // два параметра

printf("%d %d %d %d, a, b, с, d); // пять параметров

Проверка соответствия типов для функций с переменным числом параметров не выполняется (компилятор не имеет для этого информации), char и short передаются как int, а float – как double. Поэтому предпочтительнее пользоваться параметрами по умолчанию или перегруженными функциями, хотя в некоторых случаях, переменное число параметров является лучшим решением.

 

Тема 1.17

Перегрузка функций

 

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

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

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

int max (int, int); // Возвращает наибольшее из двух целых

char* max (char*, char*); // Возвращает подстроку наибольшей длины

int max (int, char*); // Возвращает наибольшее из 1-ого параметра и

// длины второго

int max (char*, int); // Возвращает наибольшее из 2-ого параметра

// и длины первого

void f(int a, int b, char* c, char* d){

cout << max (a, b) << max(c, d) << max(a, c) << max(c, b);

}

Какую функцию вызвать компилятор определяет по типу аргументов. Этот процесс называется разрешением (т.е. решением) перегрузки. Механизм разрешения основан на том, чтобы использовать функцию с наиболее подходящими аргументами. Тип возвращаемого функцией значения в разрешении не участвует. В примере будут последовательно вызваны все четыре варианта функции.

Если точного соответствия типов аргументов и типам параметров не найдено выполняются:

· продвижение порядковых типов в соответствии с общими правилами (например, bool и char в int, float в double),

· стандартные преобразования типов (например, int в double или указателей в void*),

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

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

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

Пример: неоднозначность при преобразовании типа.

#inclucle <iostream.h>

float f(float i){

cout << “function float f(float i)” << endl;

return i;

}

double f(double i){

cout << “function double f(double i )” << endl;

return i*2;

}

int main( ){

float x = 10.09;

double у = 10.09;

cout << f(x) << endl; // Вызывается f(float)

cout << f(y) << endl; // Вызывается f(double)

cout << f(10) << endl; // Ошибка – неоднозначность

// как преобразовать 10 – во float или double?

return 0;

}

Для устранения неоднозначности требуется явное приведение типа для константы 10.

Пример:неоднозначность при использовании параметров-ссылок.

#include <iostream.h>

int f(int а, int b) {…};

int f(int а, int &b) {…};

int main( ){

int i = 1, j = 2;

cout << f(i, j); // Ошибка - неоднозначность

}

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

Пример: неоднозначность при использовании аргументов по умолчанию.

#include <iostream.h>

int f(int a){return a;}

int f (int a, int b = 1){return a * b;}

int main( ){

cout << f(10, 2); // Вызывается f(int, int)

cout << f(10); // Ошибка – неоднозначность.

// Вызывается f(int, int) или f(int)?

return 0;

}

Правила описания перегруженных функций:

· Функции должны находиться в одной области видимости, иначе произойдет сокрытие одинаковым именам переменных во вложенных блоках.

· Функции могут иметь параметры по умолчанию, их количество в различных вариантах функций может быть различным.

· Значения одного и того же параметра в разных функциях должны совпадать.

· Функции не могут быть перегружены, если описание их параметров отличается только модификатором const или использованием ссылки (например, int и const int или int и int&).

 

Тема 1.18

Шаблоны функций

Многие алгоритмы (например, сортировка) не зависят от типов данных, с которыми они работают. Такие алгоритмы удобно параметризовать для различных типов данных.

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

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

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

Формат простейшей функции-шаблона:

template <class Туре> заголовок{ /* тело функции */ }

Вместо слова Туре может использоваться произвольное имя.

В общем случае шаблон может содержать несколько параметров, каждый из которых может быть не только типом, но и просто переменной, например:

template <class А, class В, int i> void f( ){ ... }

На месте параметра шаблона, являющегося переменной, должно указываться константное выражение.

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

Пример: сортировка массива методом выбора (тип для инстацирования определяется по типу аргументов).

include <iostream.h>

template <class Туре>

void sort_vybor(Type *b, int n){

Type a; //буферная переменная для обмена элементов

for (int i = 0; i < n - l; i++){

int imin = i;

for (int j = i + 1; j<n; j++)

if (b[j] < b[imin]) imin = j;

а = b[i]; b[i] = b[imin]; b[imin] = а;

}

}

 

int main( ){

const int n = 20;

int i, b[n];

for (i = 0; i < n; i++) cin >> b [ i ];

sort_vybor(b, n); // Сортировка целочисленного массива

for (i = 0; i < n; i++) cout << b[i] << ' ';

cout << endl;

double a[ ] = {0.22, 117, -0.08, 0.21, 42.5};

sort_vybor(a, 5); // Сортировка массива вещественных чисел

for (i = 0; i < 5; i++) cout << а[i] << ' ';

return 0;

}

Пример: явное задание аргументов шаблона при вызове.

template<class X, class Y, class Z> void f(Y, Z) {…};

void g( ){

f <int, char*, double> ("Vasia", 3.0);

f<int, char*> ("Vasia", 3.0); // Z определяется как double

f<int>("Vasia", 3.0); // Y определяется как char*, a Z - как double

f (“Vasia”, 3.0) // ошибка - X определить невозможно

}

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

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

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

void sort_vibor<int>(int *b, int n){

//*Тело специализированного варианта функции *// }

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

 

Тема 1.19

Работа со строками

 

Строковый тип в С++ не входит в число базовых типов, а является производным. В С++ поддерживаются два типа строк – С-строки и строки класса string стандартной библиотеки С++.

С-строка представляет собой массив символов (типа char), заканчивающийся нуль-символом. Нуль-символ - это символ с кодом, равным 0, который записывается в виде управляющей последовательности ‘\0’. По положению нуль-символа определяется фактическая длина строки.

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

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

Стандартная библиотека предоставляет набор функций для манипулирования строками. Для их использования необхолимо подключить заголовочный файл string.h.

С-строки

Память С-строки, как и под другие массивы, может выделяться либо на этапе компиляции (обычные строки), либо непосредственно в программе (динамические сроки).

Длина обычной строки может быть только константным выражением. Чаще всего она задается частным случаем константного выражения – константой:

const int len_str = 80;

char str[len_str];

При задании длины необходимо учитывать завершающий нуль-символ (в приведенной выше строке можно хранить только 79 символов).

Строку можно инициализировать строковым литералом (нуль-символ формируется автоматически):

char str[10] = "Vasia"; // выделено 10 элементов с номерами от 0 до 9

// первые элементы – ‘V’, ‘а’, ‘s’, ‘i’, ‘а’, ‘\0’

В данном случае под строку выделяется 10 байт, 5 из которых занято под символы строки, а шестой — под нуль-символ.

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

char str[ ] = "Vasia"; // выделено и заполнено 6 байт

Для размещения строки в динамической памяти надо описать указатель на char, а затем выделить необходимый объем памяти:

char *str = new char [m] или char *str = (char *)malloc(m*sizeof(char));

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

Динамические строки, как и другие динамические массивы, нельзя инициализировать при создании.

После выполнения оператора

char *str = "Цена бутылки вина"

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

Изменить строковую константу невозможно, поэтому оператор str[1]='0' вызовет ошибку (при этом указатель объявлен как char*, а не const char*)

Для работы со строковыми константами необходимо объявлять указатель на константу типа char и проинициализировать его адресом начала строки:

const char *st = "Цена бутылки вина";

Для доступа к отдельным символами строковой константы должны использоваться арифметические операции с указателем. Например, для перебора всех символов строки можно увеличивать указатель на 1, пока очередным символом не станет нуль:

while (*st++ ) { ... }

Указатель st разыменовывается, и получившееся значение проверяется на истинность. Любое отличное от нуля значение считается истинным, поэтому цикл закончится, когда будет достигнут символ с кодом 0. Операция инкремента ++ прибавляет 1 к указателю st, т.е. сдвигает его к следующему символу.

Пример: функция для вычисление длины строки.

int string_length( const char *st )

{

int len = 0;

if (st) while ( *st++ ) ++len;

return len;

}

Поскольку указатель может содержать нулевое значение (ни на что не указывать), перед операцией разыменования его следует проверять.

С-строка может считаться пустой в двух случаях: если указатель на строку имеет нулевое значение (строка отсутствует) или указывает на массив, состоящий из одного нулевого символа (строку, не содержащую ни одного значимого символа):

char *pc1 = 0; // pc1 не адресует массива символов

const char *pc2 = ""; // pc2 адресует нулевой символ

Доступ к символам строки

С-строка хранится в памяти как массив символов типа char, отдельные ее символы являются элементами данного массива. Доступ к символам статической строки осуществляется путем указания индекса массива (s[i]), к элементам динамической строки - через указатель ( *s).

Пример: копирование строки src в строку dest.

Очевидный алгоритм решения задачи имеет вид:

char src[10], dest[10];

for (int i = 0; i<=strlen(src); i++) dest[i] = src[i];

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

Увеличение индекса можно заменить инкрементом указателей (для этого память под строку src должна выделяться динамически, а также требуется определить дополнительный указатель и инициализировать его адресом начала строки dest):

#include <iostream.h>

int main( ){

cin >> n;

char *src = new char [n];

char *dest = new char [n],

*d=dest;

cin >> src;

while ( *src!= 0) *d++ = *src++;

*d = 0; // завершающий нуль

cout << dest;

return 0;

}

В цикле производится посимвольное присваивание элементов строк с одновременной инкрементацией указателей.

Результат операции присваивания — передаваемое значение, которое, проверяется в условии цикла, поэтому можно поставить присваивание на место условия, а проверку на неравенство нулю опустить (при этом завершающий нуль копируется в цикле, и отдельного оператора для его присваивания не требуется). В результате цикл копирования строки принимает вид:

while ( *d++ = *src++);

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