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; // Ошибка компиляции: нельзя изменять значение данного указателя
- Эти разнообразные формы константности особенно полезны при передаче указателей в функции. Параметры функций следует объявлять, обеспечивая максимально ограничивающий уровень константности, чтобы гарантировать невозможность функции изменить значение, на которое указывает указатель, если таковое изменение не предполагается в данной функции. Это предотвратит возможность допущения программистом ошибочного изменения значения указателя или данных, на которые он указывает.
Передача указателей в функции
Указатели — это эффективное средство передачи функции областей памяти, содержащих значения и способных содержать результат.
При использовании указателей с функциями важно гарантировать, что вызываемой функции разрешено изменять только те параметры, которые вы хотите позволить ей изменять, но не другие. Например, функции, вычисляющей площадь круга по заданному радиусу, передаваемому через указатель, нельзя позволить изменять этот радиус. В этом случае пригодятся константные указатели, позволяющие эффективно управлять тем, что функции разрешено изменять, а что — нет.
Сходство между массивами и указателями
- Массив — это указатель на его первый элемент.
- Поскольку переменные массивов, по существу, являются указателями, при работе с массивами вполне можно использовать оператор разыменования *, как при работе с обычными указателями.
- Объявление массива подобно созданию указателя для работы в пределах фиксированного диапазона памяти.
- Не забывайте, что указатели, созданные динамически с помощью оператора new, следует освободить с помощью оператора delete , даже если вы использовали для обращения к данным тот же синтаксис, что и для статических массивов. Если вы забудете об этом, то произойдет утечка памяти, а это плохо.
Наиболее распространенные ошибки при использовании указателей
1. Утечки памяти
Вероятно, это одна из самых распространенных проблем приложений C++: чем дольше они выполняются, тем больший объем памяти используют и тем самым замедляют систему. Это, как правило, случается, когда программист не обеспечил освобождение памяти, выделенной динамически оператором new, с помощью вызова оператора delete по завершении ее использования.
Обеспечение освобождения всей выделенной памяти — задача программиста, т.е. ваша. Вы должны следить, чтобы никогда не происходили некоторые вещи наподобие показанных ниже:
- int* pointToNums = new int[5]; // Выделение памяти*
- // Использование указателя pointToNums*
- ….*
- // Освобождение памяти с помощью delete[] pointToNums не сделано*
- ….*
- // Очередное выделение и перезапись указателя*
- pointToNums = new int[10]; // Утечка ранее выделенной памяти*
2. Когда указатели указывают на недопустимые области памяти
- Когда вы разыменовываете указатель для доступа к значению, на которое он указывает, необходимо гарантировать, что указатель содержит допустимый адрес области памяти, иначе поведение вашей программы будет непредсказуемым. Некорректные указатели являются практически наиболее частой причиной аварийных отказов приложения. Указатели могут оказаться некорректными по ряду причин, связанных с плохим управлением памятью.
3. Висячие (беспризорные, дикие) указатели
- Любой корректный указатель становится некорректным после того, как он освобождается оператором delete. Чтобы избежать этой проблемы, большинство программистов присваивают указателю при инициализации и после его освобождения значение nullptr, а затем, используя его, проверяют корректность указателя, прежде чем применить к нему оператор разыменования.
4. Проверка успешности запроса с использованием оператора new
- До сих пор в нашем коде мы полагали, что оператор new всегда возвращает корректный указатель на блок памяти. На самом деле оператор new, как правило, выполняется успешно, если только не был запрошен необычно большой объем памяти или если система не находится в столь критическом состоянии, что у нее не хватает памяти. Существуют приложения, которые должны запрашивать большие объемы памяти (например, приложения баз данных), да и вообще, не следует думать, что распределение памяти всегда осуществляется успешно. Язык C++ предоставляет две возможности удостовериться в успешности выделения памяти. Основной способ, действующий по умолчанию, подразумевает генерацию исключенш std::bad_alloc при неудачном выделении памяти. Генерация исключения приводит к прерыванию выполнения приложения с сообщением об ошибке наподобие “unhandled exception” (необработанное исключение), если только вы не создали обработчик исключений (exception handler).
Полезные советы по применению указателей
Что такое ссылка?
Ссылка (reference) — это псевдоним переменной. При объявлении ссылки ее необходимо инициализировать переменной. Таким образом, ссылочная переменная — это только другое средство доступа к данным, хранимым в переменной.
Для объявления ссылки используется символ &, как в следующем примере:
VarType original = Value;
VarType& referenceVariable = original;
Зачем нужны ссылки
- Ссылки позволяют работать с областью памяти, которой они инициализируются. Это делает ссылки особенно полезными при создании функций. Типичная функция объявляется так:
- Возвращаемый_Тип Сделать__Нечто (Тип Параметр);*
Функция Сделать_Нечто() вызывается так:
- Возвращаемый_Тип Результат = Сделать_Нечто (Аргумент);*
- Приведенный выше код ведет к копированию аргумента в параметр, который затем используется функцией Сделать__Нечто (). Этап копирования может быть весьма продолжительным, если рассматриваемый Аргумент занимает много памяти. Аналогично, когда функция Сделать_Нечто ( ) возвращает значение, оно копируется в Результат. Было бы хорошо, если бы можно было избежать этого копирования, позволяя функции работать непосредственно с данными в стеке вызывающей функции. Ссылки обеспечивают эту возможность.
Версия функции без копирования выглядит следующим образом:
Возвращаемый_Тип Сделать_Нечто (Тип& Параметр);
Вызывается функция так же, как и раньше:
- Возвращаемый_Тип Результат = Сделать_Нечто (Аргумент) ;*
- Функция, которая получает параметр как ссылку, может возвращать значение, используя ссылочные же параметры.
Использование ключевого слова const со ссылками
Возможны ситуации, когда ссылка не должна позволять изменять значение исходной переменной. При объявлении таких ссылок используют ключевое слово const:
int original = 30; const int& constRef = original; constRef =40; // Недопустимо: constRef не может // изменить значение в original int& ref2 = constRef; // Недопустимо: ref2 не const const int& constRef2 = constRef; // OK
Передача аргументов в функции по ссылке
- Одно из главных преимуществ ссылок в том, что они позволяют вызываемой функции работать с параметрами без копирования из вызывающей функции, что существенно увеличивает производительность. Но поскольку вызываемая функция работает с параметрами, расположенными непосредственно в стеке вызывающей функции, зачастую важно гарантировать невозможность вызываемой функции изменить значение переменной в вызывающей функции. Ссылки, определенные как const, обеспечивают эту возможность.
- Константные ссылки — мощный инструмент, предоставляемый языком C++ для входных параметров и гарантии того, чтопередаваемое по ссылке значение не может быть изменено вызываемой функцией. На первый взгляд, это может показаться тривиальным, но в коллективе разработчиков, где один программист пишет первую версию функции, второй ее дополняет, а третий исправляет или совершенствует, использование константных ссылок имеет важное значение для качества программы.
У меня есть два указателя:
int* pointToAnlnt = new int;
int* pCopy = pointToAnlnt;
Полагаю, после использования будет лучше освободить их оба, чтобы гаранти
рованно избежать утечки памяти?
Нет, это неправильное решение. Оператор delete можно использовать только один раз для каждого адреса, возвращенного оператором new. Кроме того, желательно избегать наличия двух указателей на один и тот же адрес, поскольку выполнение оператора delete для любого из них сделает некорректным другой указатель. Не нужно допускать в программах никаких неопределенностей в отношении корректности используемых указателей.
Зачем передавать значения в функцию по ссылке?
Это не нужно, пока не возникнет необходимость. Если параметрами функции являются объекты очень большого размера, то передача по значению требует весьма дорогостоящего копирования. Вызов функции будет намного эффективнее при использовании ссылок. Не забудьте использовать ключевое слово const, если только функция не должна изменять значение соответствующей переменной, например, для возврата результата работы.