V Flashcards
Функция, которая ничего не возвращает
- При создании функции, которая не должна возвращать никаких значений, тип возвращаемого ею значения указывается как void (пустота).
- Если функция не объявлена с типом возвращаемого значения void, в теле функции необходимо наличие оператора return .
Параметры функций со значениями по умолчанию
- До сих пор мы использовали в примерах фиксированное значение числа π как константу, не предоставляя пользователю возможности его изменить. Однако пользователя функции может интересовать более точное или менее точное ее значение. Как при создании функции, использующей число π, позволить ее пользователю использовать собственное значение, а при его отсутствии задействовать стандартное?
- Один из способов решения этой проблемы подразумевает создание в функции Area ( ) дополнительного параметра для числа π и присваивание ему значения но умолчанию (default value). Такая адаптация функции Area( ) выглядела бы следующим образом:
- double Area(double radius, double Pi = 3.14);*
- Обратите внимание на второй параметр Pi и присвоенное ему по умолчанию значение 3,14. Этот второй параметр является теперь необязательным параметром (optional parameter) для вызывающей функции. Вызывающая функция все равно может вызвать функцию Area (), используя синтаксис
- Area(radius);*
- В данном случае второй параметр был проигнорирован, поэтому используется его значение по умолчанию 3,14. Но если пользователь захочет задействовать другое значение числа π, то можно сделать это, вызвав функцию Area ( ) следующим образом:
- Area (radius, Pi); // Pi определяется пользователем*
Рекурсивная функция
- В некоторых случаях функция может фактически вызывать сама себя. Такая функция называется рекурсивной (recursive function). Обратите внимание, что у рекурсивной функции должно быть четко определенное условие выхода, когда она завершает работу и больше себя не вызывает.
- При отсутствии условия выхода или при ошибке в нем выполнение программы застрянет в рекурсивном вызове функции, которая непрерывно будет вызывать сама себя, пока в конечном счете не приведет к переполнению стека и аварийному завершению приложения.
- Рекурсивные функции могут пригодиться при вычислении чисел ряда Фибоначчи: значение каждого следующего числа последовательности — это сумма двух предыдущих чисел. Таким образом, n-е значение последовательности (при п> 1) определяется следующей (рекурсивной) формулой:
Fibonacci (n) = Fibonacci (n - 1) + Fibonacci (n - 2)
Функции с несколькими операторами return
- Вы не ограничены наличием только одного оператора return в определении функции. По желанию вы можете осуществлять выход из функции в любом месте, не обязательно только в одном. В зависимости от логики и задачи приложения это может быть и преимуществом, и недостатком.
- Используйте несколько выходов из функции осторожно. Значительно проще исследовать и понять функцию, которая начинается вверху и заканчивается внизу, чем функцию, которая имеет несколько выходов в разных местах.
Перегрузка функций
- Функции с одинаковым именем и одинаковым типом возвращаемого значения, но с разными наборами параметров называют перегруженными функциями (overloaded function).
- Перегруженные функции могут быть весьма полезными, например, в приложениях, в которых имеется функция с определенным именем, которая осуществляет некоторый вывод, но может быть вызвана с различными наборами параметров. Предположим, необходимо написать приложение, которое вычисляет площадь круга и площадь поверхности цилиндра. Функция, которая вычисляет площадь круга, нуждается в одном параметре — радиусе. Вторая функция, которая вычисляет площадь поверхности цилиндра, нуждается, кроме радиуса, во втором параметре — высоте цилиндра. Обе эти функции должны возвратить данные одного типа, содержащие площадь.
Передача в функцию массива значений
Функция, которая выводит на консоль целое число, может быть представлена следующим образом:
void Displaylnteger (int number);
Прототип функции, способной отобразить массив целых чисел, должен быть немного другим:
void Displaylntegers( int[] numbers, int length);
Первый параметр указывает, что передаваемые в функцию данные являются массивом, а второй параметр указывает его длину, чтобы, используя массив, вы не вышли за его границы
Передача аргументов по ссылке
Иногда нужны функции, способные работать с исходной переменной или изменять значение так, чтобы это изменение было доступно вне функции, скажем, в вызывающей функции. В таком случае следует объявить параметр как получающий аргумент по ссылке (by reference).
Используя оператор return , функция может возвратить только одно значение. Но если функция должна возвращать вызывающей функции несколько значений, то передача аргументов по ссылке является единственным способом, обеспечивающим такой возврат информации вызывающей функции.
Как процессор обрабатывает вызовы функций
- Вызов функции, по существу, означает, что процессор переходит к выполнению следующей команды, принадлежащей вызываемой функции и расположенной в некоторой не последовательной области памяти. После выполнения команд функции поток выполнения возвращается туда, откуда был совершен переход в функцию. Для реализации этой логики компилятор преобразует вызов функции в команду процессора CALL. Данная команда включает адрес следующей команды для выполнения (это адрес команды вызываемой функции). Когда процессор встречает команду CALL, он сохраняет в стеке позицию команды, которая будет выполнена после возврата из функции, и переходит к командам в области памяти, указанной в команде CALL.
- Эта область памяти содержит команды, принадлежащие функции. Процессор выполняет их до тех пор, пока не встретит команду RET (машинный код для оператора return в программе C++). Команда RET требует от процессора извлечь из стека адрес, сохраненный во время выполнения команды CALL, и использовать его в качестве адреса команды в вызывающей функции, которой должно продолжиться выполнение программы. Таким образом, процессор возвращает выполнение вызывающей функции, и оно продолжается с того места, где было прервано вызовом функции.
Понятие стека
Встраиваемые функции
- Вызов обычной функции преобразуется в команду CALL, которая приводит к выполнению операций со стеком, переходу процессора к выполнению кода функции и т.д. Эта дополнительная работа, выполняемая невидимо для пользователя, в большинстве случаев невелика. Но что если функция очень проста, как эта?
- double GetPi( )*
- {*
- return 3.14159;*
- }*
- Накладные расходы времени на выполнение фактического вызова функции в этом случае весьма высоки по сравнению со временем, затраченным на выполнение кода функции GetPi( ). Вот почему компиляторы C++ позволяют программисту объявлять такие функции как встраиваемые (inline). Ключевое слово inline — это просьба встроить реализацию функции вместо ее вызова в место ее вызова.
- inline double GetPi( )*
- {*
- return 3.14159;*
- }*
- Большинство современных компиляторов C++ оснащены высококачественными оптимизаторами кода. Многие из них позволяют оптимизировать программу по размеру, создавая приложение минимального размера, или по скорости, обеспечивая максимальную его производительность. Первое весьма важно при разработке программного обеспечения для различных устройств наподобие мобильных, в которых не так уж много памяти. При такой оптимизации компилятор отклоняет большинство просьб о встраивании, поскольку это может увеличить размер кода.При оптимизации по скорости компилятор обычно удовлетворяет просьбы о встраивании (там, где это имеет смысл), причем делает это зачастую даже в тех случаях, когда никто его об этом не просит.
Автоматический вывод возвращаемого типа
- Вместо указания типа возвращаемого значения можно использовать ключевое слово auto и позволить компилятору самому вывести тип возвращаемого значения на основе вашего кода.
- Функции с использованием автоматического вывода типа возвращаемого значения должны быть определены (т.е. реализованы) до того, как вы будете к ним обращаться. Это связано с тем, что компилятор должен знать тип возвращаемого значения функции в точке, где она используется. Если такая функция содержит несколько операторов return, все они должны возвращать один и тот же тип. Рекурсивные вызовы должны предваряться по крайней мере одним оператором return в теле функции.
Лямбда-функции
Лямбда-функции введены стандартом С++11 и очень помогают использовать алгоритмы STL для сортировки и обработки данных. Как правило, функция сортировки требует бинарного предиката, который представляет собой функцию, сравнивающую два аргумента и возвращающую true, если первый меньше второго, и false в противном случае (тем самым определяя порядок, в котором должны находиться отсортированные элементы). Такие предикаты обычно реализуются в виде операторов класса, что требует весьма кропотливого программирования.
Почему бы не встраивать каждую функцию? Ведь это увеличит скорость выполнения, не так ли?
Все зависит от обстоятельств. Результатом встраивания всех функций будет многократное повторение их содержимого во множестве мест вызова, что приведет к увеличению объема кода. Поэтому наиболее современные компиляторы сами судят о том, какие вызовы могут быть встроены, в зависимости от настроек производительности.
У меня есть две функции, обе по имени Area. Одна получает радиус, а другая высоту. Я хочу, чтобы одна возвращала тип float, а другая тип double. Это возможно?
Для перегрузки обе функции нуждаются в одинаковом имени и одинаковом типе возвращаемого значения. В данном случае компилятор сообщит об ошибке, поскольку две функции с разными типами возвращаемых значений не могут иметь одинаковое имя.
Что такое указатель?
- Не усложняя, можно сказать, что указатель (pointer) — это переменная, которая хранит адрес области в памяти. Точно так же, как переменная типа int используется для хранения целочисленного значения, переменная указателя используется для хранения адреса области памяти
- Таким образом, указатель — это переменная, и, как и все переменные, он занимает пространство в памяти (в случае рис. — по адресу 0x101). Но особенными указатели делает то, что содержащиеся в них значения (в данном случае — 0x558) интерпретируются как адреса областей памяти. Следовательно, указатель — это специальная переменная, которая указывает на область в памяти.
- Адреса памяти обычно представлены в шестнадцатеричной записи. Это система счисления с основанием 16, т.е. использующая 16 различных символов - за 0-9 следуют символы A-F. По соглашению перед шестнадцатеричным числом записывается префикс Ох. Таким образом, шестнадцатеричное число ОхА представляет собой 10 в десятичной системе счисления; шестнадцатеричное OxF - 15; а шестнадцатеричное 0x10 - 16.
Объявление указателя
- Поскольку указатель является переменной, его следует объявить, как и любую иную переменную. Обычно вы объявляете, что указатель указывает на значение определенного типа (например, типа int). Это значит, что содержавшийся в указателе адрес указывает на область в памяти, содержащую целое число. Можно определить указатель и на нетипизированный блок памяти (называемый также указателем на void). Указатель должен быть объявлен, как и все остальные переменные:
- Указываемый_тип * Имя_Переменной_Указателя;*
- Как и в случае с большинством переменных, если не инициализировать указатель, он будет содержать случайное значение. Во избежание обращения к случайной области памяти указатель инициализируют значением nullptr (это значение — нулевой указатель — появилось в языке C++ в стандарте С++11.). Значение указателя всегда можно проверить на равенство значению nullptr, которое не может быть адресом реальной области памяти:
- Указываемый_тип * Имя_Переменной_Указателя = nullptr;*
- Таким образом, объявление указателя на целое число может иметь следующий вид:
- int *plnteger = nullptr;*
- Указатель, как и переменная любого другого изученного на настоящий момент типа данных, до инициализации содержит случайное значение. В случае указателя это случайное значение особенно опасно, поскольку означает некоторый адрес области памяти. Неинициализированные указатели способны заставить вашу программу обратиться к недопустимой области памяти, приводя (в лучшем случае) к аварийному завершению.
Определение адреса переменной с использованием оператора получения адреса &
Если varName — переменная, то выражение &varName возвращает адрес места в памяти, где хранится ее значение.
Так, если вы объявили целочисленную переменную, используя хорошо знакомый вам синтаксис
int age = 30;
то выражение &аgе вернет адрес области памяти, в которую помещается указанное значение 30.
Оператор получения адреса & иногда называют также оператором ссылки (referencing operator).
Использование указателей для хранения адресов
- // Объявление переменной*
- Тип Имя_Переменной = Начальное_Значение;*
Чтобы сохранить адрес этой переменной в указателе, следует объявить указатель на тот же Тип и инициализировать его, используя оператор получения адреса &:
- // Объявление указателя на тот же тип и его инициализация*
- Тип* Указатель = &Имя_Переменной;*
Доступ к данным с использованием оператора разыменования *
- Предположим, у вас есть указатель, содержащий вполне допустимый адрес. Как же теперь обратиться к этой области, чтобы записать или прочитать содержащиеся в ней данные? Для этого используется оператор разыменования (dereferencing operator) *. По существу, если есть корректный указатель pData, выражение *pData позволяет получить доступ к значению, хранящемуся по адресу, cодержащемуся в этом указателе.
- Оператор разыменования * называется также оператором косвенного обращения (indirection operator).
Значение sizeof ( ) для указателя
Результат выполнения оператора sizeof ( ) для указателя зависит от компилятора и операционной системы, для которой программа была скомпилирована, и не зависит от характера данных, на которые он указывает.
Динамическое распределение памяти
Когда вы пишете программу, содержащую объявление массива, такое как
int Numbers[100]; // Статический массив для 100 целых чисел
возникают две проблемы.
- Вы фактически ограничиваете возможности своей программы, поскольку она не сможет хранить больше 100 чисел.
- Вы неэффективно используете ресурсы в случае, когда храниться должно, скажем, только 1 число, а память все равно выделяется для 100 чисел.
Причиной этих проблем является статическое, фиксированное выделение памяти для массива компилятором.
Чтобы программа могла оптимально использовать память, в зависимости от конкретных потребностей пользователя, необходимо использовать динамическое распределение памяти. Оно позволяет при необходимости выделять большее количество памяти и освобождать ее, когда необходимости в ней больше нет. Язык C++ предоставляет два оператора, new и delete , позволяющие управлять использованием памяти в вашем приложении. В эффективном динамическом распределении памяти критически важную роль играют указатели, хранящие адреса памяти.
Использование new и delete для выделения и освобождения памяти
- Оператор new используется для выделения новых блоков памяти. Чаще всего используется версия оператора new, возвращающая указатель на затребованную область памяти в случае успеха и генерирующая исключение в противном случае. При использовании оператора new необходимо указать тип данных, для которого выделяется память:
- Тип* Указатель = new Тип; // Запрос памяти для одного элемента*
Вы можете также определить количество элементов, для которых хотите выделить память (если нужно выделять память для массива элементов):
Тип* Указатель = new Тип[Количество]; // Запрос памяти для указан
// ного количества элементов
Таким образом, если необходимо разместить в памяти целые числа, используйте следующий код:
- int* pointToAnlnt = new int; // Указатель на целое число*
- int* pointToNums = new int[10]; // Указатель на массив из 10*
- // целых чисел*
- Обратите внимание на то, что оператор new запрашивает область памяти. Нет никакой гарантии, что запрос всегда будет удовлетворен успешно, поскольку это зависит от состояния системы и доступного количества памяти.
Каждая область памяти, выделенная оператором new, должна быть в конечном счете освобождена соответствующим оператором delete:
- Тип* Указатель = new Тип;*
- delete Указатель; // Освобождение памяти, выделенной*
- // ранее для одного экземпляра Типа*
Это справедливо и при запросе памяти для нескольких элементов:
- Тип* Указатель = new Тип [Количество] ;*
- delete[] Указатель; // освободить выделенный ранее массив*
- Обратите внимание на применение оператора delete [] при выделении блока с использованием оператора new [. . .] и оператора delete при выделении только одного элемента с использованием оператора new.
- Если не освободить выделенную память по окончании ее использования, она останется выделенной и недоступной для последующих выделений вашему или иным приложениям. Такая утечка памяти может привести даже к замедлению работы приложения или компьютера в целом, и ее следует избегать любой ценой.
- Операторы new и delete выделяют область в динамической памяти. Динамическая память (free store) - это абстракция памяти в форме пула памяти, из который диспетчер памяти может выделять блоки памяти для вашего приложения и освобождать их, возвращая в пул свободной памяти.
Использование ключевого слова const с указателями
Указатели — это особый вид переменных, которые содержат адреса областей памяти и позволяют модифицировать данные в памяти. Таким образом, когда дело доходит до указателей и констант, возможны следующие комбинации.
- Содержащийся в указателе адрес является константным и не может быть изменен, однако данные, на которые он указывает, вполне могут быть изменены:
- int daysInMonth = 30;*
- int* const pDaysInMonth = &daysInMonth;*
- *pDaysInMonth = 31; // OK! Значение может бьггь изменено*
- int daysInLunarMonth = 28;*
- pDaysInMonth = &daysInLunarMonth; // Ошибка компиляции: нельзя*
- // изменить адрес!*
- Данные, на которые указывает указатель, являются константными и не могут быть изменены, но сам адрес, содержащийся в указателе, вполне может быть изменен (т.е. указатель может указывать и на другое место):
- int hoursInDay = 24;*
- const int* pointsToInt = &hoursInDay;*
- int monthsInYear = 12;*
- pointsToInt = &monthsInYear; //OK!*
- *pointsToInt = 13; // Ошибка времени компиляции: изменять данные нельзя*
- int* newPointer = pointsToInt; //Ошибка времени компиляции: нельзя присваивать указатель на константу указателю на не константу*
- И содержащийся в указателе адрес, и значение, на которое он указывает, являются константами и не могут быть изменены (самый ограничивающий вариант):
int hoursInDay = 24; const int\* const pHoursInDay = &HoursInDay; \*pHoursInDay = 25; // Ошибка компиляции: нельзя изменять значение, на которое указывает данный указатель int daysInMonth = 30; pHoursInDay = SdaysInMonth; // Ошибка компиляции: нельзя изменять значение данного указателя
- Эти разнообразные формы константности особенно полезны при передаче указателей в функции. Параметры функций следует объявлять, обеспечивая максимально ограничивающий уровень константности, чтобы гарантировать невозможность функции изменить значение, на которое указывает указатель, если таковое изменение не предполагается в данной функции. Это предотвратит возможность допущения программистом ошибочного изменения значения указателя или данных, на которые он указывает.
Передача указателей в функции
Указатели — это эффективное средство передачи функции областей памяти, содержащих значения и способных содержать результат.
При использовании указателей с функциями важно гарантировать, что вызываемой функции разрешено изменять только те параметры, которые вы хотите позволить ей изменять, но не другие. Например, функции, вычисляющей площадь круга по заданному радиусу, передаваемому через указатель, нельзя позволить изменять этот радиус. В этом случае пригодятся константные указатели, позволяющие эффективно управлять тем, что функции разрешено изменять, а что — нет.