4881

Указатели на функции. Перегрузка функций. Шаблоны функций

Лекция

Исторические личности и представители мировой культуры

Указатели на функции. Перегрузка функций. Шаблоны функций. Предположим, что нужно реализовать функцию сортировки массива строк с примерно таким прототипом: void sort( char beg, char end ) здесь beg и end являются указателями на начало и конец...

Русский

2012-11-28

61 KB

7 чел.

Указатели на функции. Перегрузка функций. Шаблоны функций.

Предположим, что нужно реализовать функцию сортировки массива строк с примерно таким прототипом:

void sort( char ** beg, char ** end );

здесь beg и end являются указателями на начало и конец диапазона элементов массива, который необходимо отсортировать. Для сортировки должно быть определено правило упорядочивания строк (предикат) и различных вариантов выбора этого правила может быть много, при этом сам алгоритм сортировки слабо зависит от выбора предиката. Конечно, можно было бы реализовать несколько разных функций для различных правил сравнения: сортировка строк по длине, в лексикографическом (словарном) порядке, по возрастанию, убыванию и т.п., однако это привело бы к массовому дублированию однотипного кода, и в случае обнаружения какой-либо ошибки в реализации алгоритма, пришлось бы исправлять её во всех вариантах сортировки. В подобных случаях удобно использовать указатель на функцию в качестве одного из параметров функции сортировки.

Будем считать, что любое правило сравнения строк можно реализовать в виде функции с прототипом вида:

int compare( const char * s1, const char * s2 );

в случае равенства строк compare возвращает 0, иначе 1, если строка s1 «больше» s2, и  -1, если s1 «меньше» s2. В этом случае, указатель на любую функцию с таким прототипом будет выглядеть так:

int ( * pf ) ( const char *, const char * );

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

int compareLen( const char * s1, const char * s2 )

{

 int len1 = strlen( s1 );

 int len2 = strlen( s2 );

 

 return ( len1 == len2 ) ? 0 : ( len1 > len2 ? 1 : -1 );

}

bool compareAlpha( const char * s1, const char * s2 )

{

 return s1[0] != s2[0];

}

int ( * pf ) ( const char *, const char * );

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

{

pf = compareLen;   // допустимо

pf = & compareLen; // допустимо, эквивалентно предыдущему

pf = strcmp;       // допустимо

 

pf = compareAlpha; // ошибка, несовпадение типов

}

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

оператор разыменования при этом необязательно. И прямой вызов функции по имени, и косвенный вызов по указателю записываются одинаково:

compareLen( "a", "b" ); // прямой вызов

 pf( "a", "b" );         // косвенный вызов

( * pf )( "a", "b" );   // косвенный вызов с использованием

                        // явного синтаксиса указателя

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

Можно объявить массив указателей на функции. Например:

int ( * funcArray[5] )( double );

Здесь funcArray – это массив из пяти элементов, каждый из которых является указателем на функцию, принимающую один параметр типа double и возвращающую значение типа int. Объявления такого типа достаточно трудно читать, поэтому часто используют директиву typedef для объявления вспомогательного типа, соответствующего указателю на функцию:

typedef int ( * t_funcPtr )( double );

t_funcPtr funcArray[5];

Присваивание значений элементам массива и вызов функций осуществляется с использованием обычного оператора [], например:

int f1( double d )

{

 return static_cast< int >( d );

}

funcArray[0] = f1;

... // инициализация остальных элементов массива funcArray

double values[5] = { 1.1, 2.2, 3.3, 4.4, 5.5 };

double results[5];

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

results[i] = funcArray[i]( values[i] );

В результате, функция сортировки строк с учетом произвольного правила сортировки может быть реализована, например, так (использован алгоритм сортировки «пузырьком»):

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

typedef int ( * t_compareFunc ) ( const char *, const char * );

void sortStrings( char ** beg, char ** end, t_compareFunc compare )

{

 // Временный буфер для обмена строк

 const int BUFSIZE = 128;

 char tmp[BUFSIZE];

   

 bool swapped = true;

 while ( swapped )

{

 swapped = false;

 for ( char ** strPtr = beg; strPtr != end; ++strPtr )

 {

  if ( compare( * strPtr, * ( strPtr + 1 ) ) > 0 )

  {

   strcpy( tmp, * strPtr );

   strcpy( * strPtr, * ( strPtr + 1 ) );

   strcpy( * ( strPtr + 1 ), tmp );

   swapped = true;

  }

 }

}

}

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

Механизм перегрузки функций позволяет иметь несколько одноименных функций, выполняющих схожие операции над аргументами разных типов. Например, именно этот механизм позволяет осуществлять сложение аргументов разных типов одним и тем же оператором сложения +. Для вычисления выражения 1 + 2 вызывается операция целочисленного сложения, в то время как вычисление выражения 1.0 + 2.0 осуществляет сложение с плавающей точкой. Выбор конкретной версии этого оператора зависит от типов аргументов и производится незаметно для программиста. Ответственность за распознавание контекста и применение операции берет на себя компилятор.

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

int max_pair( int a, int b );

int max_array( const int * A, int size );

int max_matrix( const int ** M, int rows, int cols );

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

int max( int a, int b );

int max( const int * A, int size );

int max( const int ** M, int rows, int cols );

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

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

int max( int a, int b );

double max( double a, double b );

unsigned long max( unsigned long a, unsigned long b );

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

template < class T >

T max( T a, T b )

{

  return ( a > b ) ? a : b;

}

Как объявление, так и определение шаблона функции всегда должны начинаться с ключевого слова template, за которым следует список разделенных запятыми идентификаторов, заключенный в угловые скобки < и >, – список параметров шаблона, обязательно непустой. У шаблона могут быть параметры-типы, представляющие некоторый тип, и параметры-константы, представляющие фиксированное константное выражение.

Параметр-тип состоит из ключевого слова class или ключевого слова typename, за которым следует идентификатор. Эти слова всегда обозначают, что последующее имя относится к встроенному или определенному пользователем типу. Имя параметра шаблона выбирает программист. В приведенном примере мы использовали имя T, но могли выбрать и любое другое.

Процесс подстановки типов и значений вместо параметров называется конкретизацией шаблона. При конкретизации (порождении конкретного экземпляра) шаблона вместо параметра-типа подставляется фактический встроенный или определенный пользователем тип. Любой из типов int, double, char* является допустимым аргументом шаблона. Параметр-константа выглядит как обычное объявление. Он говорит о том, что вместо имени параметра должно быть подставлено значение константы из определения шаблона. Выполняется конкретизация неявно, как побочный эффект вызова или взятия адреса шаблона функции. Например, в следующей программе шаблон max конкретизируется дважды  – один раз для массива из пяти элементов типа int, а другой – для четырех элементов типа double:

// Определение шаблона функции max()

// с параметром-типом T и параметром-константой size

template < typename T, int size >

T max( T ( & refArr ) [ size ] )

{

  T maxVal = refArr[ 0 ];

  for ( int i = 1; i < size; ++i )

     if ( refArr[i] > maxVal )

        maxVal = refArr[i];

  return maxVal;

}

// размер определится по числу элементов в списке инициализации

int ia[] = { 10, 7, 14, 3, 25 };

double da[6] = { 10.2, 7.1, 14.5, 3.2, 25.0, 16.8 };

void main()

{

  // конкретизация max() для массива из 5 элементов типа int

  // подставляется T => int, size => 5

  int i = max( ia );

  

  // конкретизация max() для массива из 6 элементов типа double

  // подставляется T => double, size => 6

  double d = max( da );

}

Для определения фактического типа и значения константы, которые надо подставить в шаблон, исследуются фактические аргументы, переданные при вызове функции. В нашем примере для идентификации аргументов шаблона при конкретизации используются тип ia (массив из пяти int) и da (массив из шести double). Процесс определения типов и значений аргументов шаблона по известным фактическим аргументам функции называется выведением (deduction) аргументов шаблона.

Шаблон конкретизируется либо при вызове, либо при взятии адреса функции. В следующем примере указатель pf инициализируется адресом конкретизированного экземпляра шаблона. Его аргументы определяются путем исследования типа параметра функции, на которую указывает pf:

template < typename T, int size >

T max( T ( & refArr ) [ size ] )

{/*…*/}

// pf указывает на int max( int ( & )[10] )

int ( * pf )( int ( & )[10] ) = max;


 

А также другие работы, которые могут Вас заинтересовать

47447. Филогенез систем органов хордовых. Мочеполовая система. Центральная нервная система. Эндокринная система 90.5 KB
  Образование головного мозга называют цефализацией. Совместная эволюция органов чувств и головного мозга приводит к возникновению динамических координации между обонятельными рецепторами и передним мозгом зрительными и средним слуховыми и задним. Внутри головного и спинного мозга расположена общая полость соответствующая невроцелю. В спинном мозге это спинномозговой канал а в головном желудочки мозга.
47448. Антропогенез 83.5 KB
  Место человека в системе животного мира 2. Методы изучения эволюции человека 3. Адаптивные экологические типы человека 4. Место человека в системе животного мира Неограниченный прогресс в эволюции живой материи проявился в возникновении человека как биосоциального существа.
47449. Общая экология. Основные понятия экологии 45 KB
  Факторы среды и адаптации к ним организмов. Среды жизни и адаптации к ним организмов 5. Связи организмов в экосистемах 1. Геккелем для обозначения науки изучающей о взаимоотношения организмов со средой обитания.
47450. Общая экология. Виды биологических ритмов 42 KB
  Динамика и развитие экосистем. Динамика экосистем 2. Динамика и развитие экосистем. Динамика экосистем Любая экосистема приспосабливаясь к изменениям внешней среды находится в состоянии динамики.
47451. Биология как наука. Общая характеристика жизни 44.5 KB
  Общая характеристика жизни. Общая характеристика жизни. Развитие представлений о сущности жизни. Определение жизни.
47452. Клетка – элементарная биологическая система 117 KB
  Вне клетки не существует настоящей жизнедеятельности. Исходя из предположения о схожести гомологичности растительных и животных клеток доказываемой одинаковым механизмом их возникновения Шванн обобщил многочисленные данные в виде теории согласно которой клетки являются структурной и функциональной основой живых существ. Ему принадлежит вывод о том что клетка может возникнуть лишь из предсуществующей клетки. Выдающаяся роль клетки как первоисточника жизни обусловливается тем что именно она является биологической единицей с помощью...
47453. Изменчивость и ее формы 41.5 KB
  Изменчивость и ее формы. Изменчивость как свойство живых систем Модификационная изменчивость. Наследственная генотипическая изменчивость
47454. Генетика человека. Нормальная наследственность человека 31.5 KB
  Генеалогический метод Популяционностатистический метод Близнецовый метод Метод дерматоглифики Цитогенетический метод Биохимические методы Методы рекомбинантной ДНК Методы генетики соматических клеток Карты хромосом 1. Генеалогический метод Генеалогический метод является наиболее старым методом генетики человека. Метод относительно прост и доступен. В методе составляются и ё анализируются семейные родословные что позволяет определить наследственный или ненаследственный характер заболевания отдельного симптома;...
47455. Медицинская генетика. Медико-генетическое консультирование 33.5 KB
  Наследственные болезни человека Генные болезни Хромосомные болезни Болезни с наследственным предрасположением