Функции в С++

Язык C++ / Функции в С++

 

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

Говорят, что язык C++ обеспечивает строгий контроль типов.

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

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

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

тип имя (СпецификацияФормальныхАгрументов);

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

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

тип имя

Значения по умолчанию

Для аргумента может быть задано (а может отсутствовать) значение по умолчанию. Это значение используется в том случае, если при обращении к функции соответствующий аргумент не передан. В этом случае спецификация аргумента имеет вид
тип имя=ЗначениеПоУмолчанию
Если аргумент имеет значение по умолчанию, то все аргументы, специфицированные после него, также должны иметь значения по умолчанию.

Пример Вычислить n в степени k, где чаще всего k=2. Рекурсивная функция со значением k=2 по умолчанию:

1
2
3
4
5
int pow(int n, int k = 2)  // по умолчанию k=2
{
  if (k == 2) return(n*n);
  else return(pow(n, k - 1)*n);
}


Вызывать эту функции можно двумя способами:
 
 
t = pow(i);  // t = i*i;
q = pow(i, 5); // q = i*i*i*i*i;


Значение по умолчанию может быть задано либо при объявлении функции, либо при определении функции, но только один раз.

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

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

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

 
 
 
int max(int *a, int n);
float max(float *a, int n);
double max(double *a, int n);

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

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

 
 
int sum(int a, int b=1)  { return(a+b); }
int sum(int a)           { return(a+a); }

то вызов
 
int r = sum(2);  // ошибка

выдаст ошибку из-за неоднозначности толкования sum().

Встраиваемые функции

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

 
 
#define SUMMA(a, b) a + b
rez = SUMMA(x, y)*10;


После работы препроцессора получим:
 
rez = x + y*10;


В С++ для определения функции, которая должна встраиваться как макроопределение используется ключевое слово inline. Вызов такой функции приводит к встраиванию кода inline-функции в вызывающую программу. Определение такой функции может выглядеть следующим образом:
1
2
3
4
inline double SUMMA(double a, double b)
{
  return(a + b);
}


При вызове этой функции
 
rez = SUMMA(x,y)*10;

будет получен следующий результат:
 
rez=(x+y)*10;


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

  • Определение и объявление функций должны быть совмещены и располагаться перед первым вызовом встраиваемой функции.
  • Имеет смысл определять inline только очень небольшие функции, поскольку любая inline-функция увеличивает программный код.
  • Различные компиляторы накладывают ограничения на сложность встраиваемых функций. Компилятор сам решает, может ли функция быть встраиваемой. Если функция не может быть встраиваемой, компилятор рассматривает ее как обычную функцию.

Таким образом, использование ключевого слова inline для встраиваемых функций и ключевого слова const для определения констант позволяют практически исключить директиву препроцессора #define из употребления.


Назад: Язык C++

Комментариев к записи: 10

  • Владимир
    Здравствуйте! Пытаюсь сделать универсальную функцию, которая работает с переменным числом аргументов. Дело в том что библиотека с этой функцией должна работать как в ARM-коде на Си, так и в Windows-приложении на С++. Ввиду того что эта библиотека компилируется разными компиляторами, то для доступа к аргументам va_start() va_arg() и va_end() не везде работают. Стал делать указателями. Знаю что аргументы передаются через стек, и компиляторы делают различные выравнивания адресов в стеке (32 или 64 бита). Делать копии библиотек под разные компиляторы нельзя - её ещё редактировать и развивать надо - запутаюсь в этих версиях.
    Но вот под Windows столкнулся с такой проблемой:
    1
    2
    3
    4
    5
    6
    uint8_t x[100]={0,};
    void my_func(uint8_t a, ... )
    {
      for(uint8_t i=1; i<100; i++)
        x[i] = *( ( &a ) + i );
    }
    Выполняю my_func( 222, 10, 20, 30, 40 ) и получаю такой массив x:
    1
    2
    3
    4
    5
    6
    7
    0   0   0  44 254  40   0
    167 137 193   0  44 254  40   0
    84 206  40  19  88 206  40   0
    50  27  64   0   0   0   0
    0   0   0 20 0   0   0
    0   0   0 40 0   0   0
    ...
    То есть по-сути данные начинаются с 28 байта через 4 (выравнивание адреса в стеке)
    а вот если сделать функцию таким образом:
    1
    2
    3
    4
    5
    6
    uint8_t x[100]={0,};
    void my_func(uint8_t b, uint8_t a, ... )
    {
      for(uint8_t i=1; i<100; i++)
        x[i] = *( ( &a ) + i );
    }
    То после my_func( 222, 10, 20, 30, 40 ) получаю уже вот такой массив x:
    1
    2
    3
    4
    5
    6
    7
    8
    206  40   0   0   0   0
    44 254  40   0 167 137 185   0
    44 254  40   0  84 206  40  23
    88 206  40   0  56  27  64   0
    0   0   0  10   0   0   0    0
    20  0   0   0  30   0   0    0
    0   0   0   0   0   0   0
    ...
    и здесь данные уже идут с 32-го байта после &a.
    Вот что это за чудеса?
    QtCreator+MinGW32

    • Елена Вставская
      Вопрос - что должна делать функция и какой тип у ее аргументов? Пожалуйста, не вставляйте в комментарии HTML код, потому что он будет показан полностью. Начало и конец кода: code и /code заключаются в квадратные скобки. Перенос строки - br/ в квадратных скобках.

      • Владимир
        Извините за "HTML". Просто не знал как вам отправить форматированный текст, вчера почитал про микроразметку, подсмотрел код вашей страницы и слепил вручную. Не получилось. И у вас сильно изменились массивы x. Были в первом случае {222 0 0 0 44 254 40 0 167 137 193 0 44 254 40 0 84 206 40 19 88 206 40 0 50 27 64 0 222 0 0 0 10 0 0 0 20 0 0 0 30 0 0 0 40 0 0 0 ... }, во втором случае {10 206 40 0 222 0 0 0 44 254 40 0 167 137 185 0 44 254 40 0 84 206 40 23 88 206 40 0 56 27 64 0 222 0 0 0 10 0 0 0 20 0 0 0 30 0 0 0 40 0 0 0 0 0 0 0 ... } Прошу поправить, а то другие посетители посмотрят и запутаются.
        тип аргументов функции uint8_t (unsigned char) 1 байт без знака. Данную функцию я делал только чтобы разобраться каким образом мне получить значения аргументов внутри функции. Раньше когда-то я делал такое, там использовались указатели таким образом:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18

        void my_func(uint8_t a, ...) {
          uint8_t * pp = &amp;a;
          pp += step_stack;
          uint8_t p1;
          p1 = *pp;
          pp += step_stack;
          uint8_t p2;
          p2 = *pp;
          pp += step_stack;
          uint8_t p3;
          p3 = *pp;
          pp += step_stack;
          uint8_t p4;
          p4 = *pp;
        ...
        }

        как определялась переменная step_stack уже не помню, вроде как я делал какую-то хитрую функцию с переменным числом аргументов и она в процессе инициализации рассчитывала этот step_stack независимо от компилятора. Сейчас восстановить исходники той программы не могу.
        В данный момент мне нужно сделать библиотеку с несколькими подобными функциями, и вот эту портянку хотел бы заменить макросом чтобы более лаконичный текст был. Хотел сделать указатель на структуру, присвоить ему адрес переменной (&a) и далее в теле функции обращаться к значениям параметров как к полям структуры - когда текст скомпилируется в ассемблерный, то там не должно быть никаких дополнительных команд, только смещение адреса от вершины стека. Но компилятор MinGW создает какой-то непонятный кадр в стеке при вызове такой функции.

        • Елена Вставская
          Если все аргументы имеют одинаковый тип, то почему не создать массив? И передать в функцию указатель на массив и размер массива - все элементы будут доступны в функции

          • Владимир
            Это будет совсем не лаконичный вызов функции. В каждом модуле, вызывающем функцию, надо будет городить свои массивы и отслеживать чтобы не наложились друг на друга при вызове как из основного цикла, так и из прерываний с различными приоритетами. То есть делать ещё какую-то буферную inline функцию (потеря процессорного времени) или макрос. Вызов должен быть максимально лаконичным и эффективным как strlen(). А так как в чистом си нет перегрузки функций как в С++, то приходится идти таким путём.
            Сделать функцию с максимальным количеством аргументов и вызывать, заполняя ненужные нулями - тоже не лучший выход. Дело в том что в одной программе требуется максимум 2 дополнительных аргумента, в другой 6. Операция вызова функции помещает в стек 2 байта или +4 ненужных нуля.
            Если передавать как составные байты 32-битного параметра - это будет лапша-код при вызове функции и дополнительные операции на упаковку/распаковку.
            Написать 12 функций my_func2(a,p1,p2); my_func3(a,p1,p2,p3); my_func4(a,p1,p2,p3,p4) и т.д. как-то корявенько это.
            Важно чтобы при написании и отладке более высокоуровневых модулей программы не отвлекаться на это. Вызов функции в тексте программы должен быть максимально лаконичным, удобочитаемым, интуитивно-понятным.
            Я могу конечно расписать задачу более подробно - почему и так не так, и эдак не пойдёт. Думаю это никому не интересно, так как у каждого свои спектры приоритетов.
            Вопрос мой в том - как максимально эффективно ловить аргументы в функции с переменным количеством аргументов? Пока вижу только step_stack в сборке проекта прописывать каждому компилятору опцию -Dstep_stack=4 (как предопределённый define). А в теле функции макрос типа
            1
            2
            3
            4
            5
            6

            #define BASE_FUNC_PEREM_ARGS(VALUE) uint8_t p1= (*(&amp;VALUE + step_stack));\
            uint8_t p2= (*(&amp;VALUE + step_stack*2));\
            uint8_t p3= (*(&amp;VALUE + step_stack*3));\
            uint8_t p4= (*(&amp;VALUE + step_stack*4));\

            либо
            1
            2
            3

            #define p(VALUE,IND) (*(&amp;VALUE + step_stack*IND))

            Перфекционизм в этом месте нужен максимальный. И на ассемблере универсально не могу сделать - у ARM и у PC разные наборы инструкций, если ARM ещё хоть как-то понимаю, то с PC-ассемблером у меня дружбы пока нет.
            C MinGW32 мне наверно надо самостоятельно разобраться почему так криво стек наполняется. На MinGW64 работало штатно через 8 байт без всякого "мусора".

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

  • Поправьте меня, если ошибаюсь, при inline функция\процедура загружается в память с запуском программы, без inline - в момент вызова. Т.е. если функция вызывается откуда-нибудь из цикла, то для избежания постоянных выделений\освобождений памяти для нее ее желательно инлайнить. НО, inline может быть проигнорирован компилятором, т.к. он по своему принципу определяет, что нужно инлайнить, а что нет(сугубо мое субъективное мнение).

    • Елена Вставская
      Как раз наоборот. Если функция вызывается из цикла, инлайнить ее желательно. inline - это встраивание функции в код, то есть обращение к такой функции не происходит, а ее тело встраивается в место вызова. Это подобно макрокомандам. Такой метод увеличивает объем кода, но сокращает время его выполнения.

  • Дмитрий
    Например, если в одной программе перегружены функции
    1
    2
    3
    4
    int sum(int a, int b=1) { return(a+b); }
    int sum(int a) { return(a+a); }
    то вызов
    int r = sum(2); // ошибка
    выдаст ошибку из-за неоднозначности толкования sum(). *** Елена, не могли бы вы пояснить в чем заключается неоднозначность толкования..?

    • Елена Вставская
      Если мы вызываем функцию sum(5), компилятор не сможет принять решение, вызывать функцию
      1
      sum(5, b=1) {return (5+1);}
      или
      1
      sum(5) {return (5+5);}
      В этом и неоднозначность. Поэтому при такой перегрузке возникнет ошибка компиляции.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *