4881

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

Лекция

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

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

Русский

2012-11-28

61 KB

8 чел.

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

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

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;


 

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

82514. Правовая семья (правовая система) – основное понятие сравнительного правоведения 27.46 KB
  Правовая система как совокупность социальных институтов их функциональных связей и качественных характеристик посредством которых осуществляется правовое регулирование общественных отношений. Наиболее распространен в современной компаративистике термин правовая семья Категория правовая семья служит для обозначения группы правовых систем имеющих сходные юридические признаки позволяющие говорить об относительном единстве этих систем. Понятие правовая семья отражает те особенности некоторых правовых систем которые являются результатом...
82515. Критерии классификации правовых систем (семей) 25.42 KB
  Например в раннегосударственную эпоху критерий был более чем простым правовые системы делились на свои правильные и чужие неправильные. Так по этому критерию можно разграничить христианские мусульманские и языческие правовые системы. компаративистский Компаративистский подход основан на том что правовые системы отдельных государств можно объединить в правовые семьи на основе сходства по следующим критериям: 1. Сходство структуры системы права и системы законодательства системы применения права.
82516. Соотношение сравнительного правоведения с международным публичным и международным частным правом 25.76 KB
  Взаимодействие сравнительного правоведения и МЧП как особых научных дисциплин широкое понимание Все методы решения коллизии законов предусматривают применение в ряде случаев иностранного закона. Сравнительное правоведение предоставляет в распоряжение МЧП инструментарий позволяющий правильно построить соответствующие институты...
82517. Понятие, формирование и распространение романо-германской правовой семьи 27.53 KB
  К романогерманской правовой семье относятся правовые системы возникшие в континентальной Европе на основе римских канонических и местных правовых традиций. Романогерманская правовая семья это правовые системы созданные с использованием римского правового наследия и объединенные общностью структуры источников права и сходством понятийноюридического аппарата Формирование. Романогерманская правовая семья имеет весьма длинную юридическую историю.
82518. Структура романо-германской правовой семьи 26.68 KB
  Основным источником является нормативный правовой акт. Правоприменительные органы также построены по иерархическому принципу. Правовое регулирование строится на принципе строго разделения публично и частноправовой сфер в зависимости от принадлежности правоотношения к той или иной сфере определяются права и обязанности субъектов права.
82519. Источники романо-германского права 26.8 KB
  Среди нормативно-правовых актов главное место занимает закон. Среди законов наибольшей юридической силой обладают конституции. Затем следуют органические (конституционные) законы, которые, однако, существуют не во всех странах этой семьи.
82520. Задания, направленные на развитие логического мышления младших школьников с ЗПР 61.5 KB
  Примеры такого анализа: Данное упражнение можно использовать на уроках математики. Данное упражнение рекомендуется использовать на уроках математики ознакомления с окружающим миром а также труда 2. Упражнение можно использовать на уроках математики. Это задание рекомендуется использовать на уроках математики ознакомления с окружающим миром и труда.
82521. Развитие мышления у детей с ЗПР младшего школьного возраста 35.5 KB
  Особый интерес представляет метод обучения детей модельному конструированию разработанный А. Важным при таком способе обучения конструированию является то что мыслительные процессы детей приобретают опосредованный характер нежели при конструировании по образцу. Для этого необходим достаточно высокий уровень абстрагирования что дает возможность формированию у детей специфических способов соотнесения определенных свойств условий с соответствующими свойствами постройки.
82522. Развитие мышления у младших школьников с ОНР 4.26 MB
  Отгадывание ребусов: отгадать слово из двух предлогов: найти в слове квас три предлога: Цель: развитие словеснологического мышления Установление простых аналогий навыков словообразования. Цель: развитие словеснологического мышления устойчивости и концентрации слухового внимания. Задание: определить что пропущено в рассказе: названия предметов предлоги или названия признаков Цель: развитие нагляднообразного мышления.