Double temp что это
Double temp что это
Прототипы функций
В языке C++ все функции должны иметь прототипы, а в языке C прототипы формально необязательны, но весьма желательны. Общая форма определения прототипа имеет следующий вид. Например.
Возврат значений (оператор return )
Перегрузка функций
В языке C++ функции могут перегружаться. Когда говорят, что функция перегружена, это означает, что две или более функций имеют одинаковые имена, однако все версии перегруженных функций имеют различное количество или тип параметров. Рассмотрим пример следующие три перегруженные функции.
В каждом случае для определения того, какая версия функции func() будет вызвана, анализируется тип и количество аргументов.
Передача аргументов функции по умолчанию
В языке C++ параметру функции можно присвоить значение по умолчанию, которое автоматически будет использовано в том случае, если при вызове функции соответствующий аргумент не будет задан. Например.
Области видимости и время жизни переменных.
В языках C и C++ определенны правила видимости, которые устанавливают такие понятия как область видимости и время жизни объектов. Различают глобальную область видимости и локальную.
Глобальная область видимости существует вне всех других областей. Имя объявленное в глобальной области, известно всей программе. Например глобальная переменная доступна для использования всеми функциями программы. Глобальные переменные существуют на протяжении всего жизненного цикла программы.
Рекурсия
В языках C и C++ функции могут вызывать сами себя. Этот процесс называют рекурсией, а функцию, которая сама себя вызывает — рекурсивной. В качестве примера приведём функцию fact(), вычисляющую факториал целого числа.
Функция main()
Передача указателей
Несмотря на то что в языках C и С++ по умолчанию используется передача параметров по значению, можно вручную построить вызов ф. с передачей параметров по ссылке. Для этого нужно аргументу передать указатель. Поскольку в таком случае функции передаётся адрес аргумента, оказывается возможным изменять значение аргумента вне функции. Например.
В языке C++ адрес переменной можно передать функции автоматически. Это реализуется с помощью параметра-ссылки. При использовании параметра-ссылки функции передаётся адрес аргумента, и функция работает с аргументом, а не с копией. Чтобы создать параметр-ссылку необходимо перед его именем поставить символ «амперсанда» (&). Внутри ф. этот параметр можно использовать обычным образом, не прибегая к оператору «звёздочка» (*), например.
Спецификаторы функций
Спецификатор inline представляет собой обращённое к компилятору требование: вместо создания вызова функции раскрыть её код прямо в строке. Если компилятор не может вставить функцию в строку он имеет право проигнорировать это требование. Спецификатором inline могут определяться как функции-члены, так и не функции-члены.
В качестве виртуальной функции (с помощью спецификатора virual ) ф. определяется в базовом классе и переопределяется в производным классом. На примере виртуальных функций можно понять, как язык C++ поддерживает полиморфизм.
Шаблоны функций
Указатели на функцию
Следующий пример демонстрирует создание указателя и и два способа вызова функции через указатель, а также передачу функции как параметра другим функциям.
Создавать массивы указателей на функции так же можно.
В разделе «Функция main()», во втором блоке кода, должно быть: #include using namespace std; int main (int argc, char argv[])
Калькулятор на основе Обратной Польской записи
Здравствуйте друзья! Хочу вам рассказать об использовании алгоритма Обратной польской записи для создания калькулятора на языке C#.
О самом алгоритме рассказывать не буду, так как такая тема уже поднималась. Заострю внимание на конкретной программе.
Об алгоритме
Алгоритм Обратной польской нотации (ОПН) — форма записи математических выражений, в которой операнды расположены перед знаками операций. Также именуется как обратная польская запись, обратная бесскобочная запись (ОБЗ). © Wikipedia
Наша программа будет состоять из двух частей:
1) Класс RPN (Reverse Polish Notation) — тут будет реализована работа алгоритма
2) Класс Program — тут, собственно, будет реализовано использование класса RPN — писать будем в консоли, что бы было понятнее.
Часть первая: класс RPN
Класс будет содержать ряд методов:
Это 3 основных метода класса, опишем их подробнее:
Calculate — метод общедоступный, ему будет передаваться выражение в виде строки, и он будет возвращать результат вычисления. Результат он будет получать используя другие методы.
GetExpression — метод, которому основной метод Calculate будет передавать выражение, и получать его уже в постфиксной записи.
Counting — метод, который получая постфиксную запись выражения будет вычислять его результат.
Так же, помимо 3 основных методов, в классе будет еще 3 метода «обеспечения», это:
IsDelimeter — возвращает true, если проверяемый символ — разделитель
IsOperator — возвращает true, если проверяемый символ — оператор
GetPriority — метод возвращает приоритет проверяемого оператора, нужно для сортировки операторов
Сначала рассмотрим неосновные методы, они простые, так что проблем с пониманием быть не должно:
А теперь основные методы:
Calculate:
Часть 2: класс Program
Тут все просто, думаю объяснять большого смысла нет, просто прокомментирую:
Внимание! Ошибки!
Хочу обратить ваше внимание на одну вещь! В нашем классе я не реализовывал проверку входных данных, поэтому при некорректном вводе будут вылетать исключения!
Шаблоны и шаблонные функции в C++. Введение
Шаблонные функции
Давайте рассмотрим простой пример. Допустим, у нас есть функция, которая меняет местами значения двух переменных типа int:
Теперь, допустим, у нас в функции main так же есть две переменные типа double, значения которых тоже нужно обменять. Функция для обмена значений двух переменных типа int нам не подойдет. Напишем функцию для double:
И теперь перепишем main:
Как видите, у нас алгоритм абсолютно одинаковый, отличаются лишь типы параметров и тип переменной temp. А теперь представьте, что нам еще нужны функции для short, long double, char, string и еще множества других типов. Конечно, можно просто скопировать первую функцию, и исправить типы на нужные, тогда получим новую функцию с необходимыми типами. А если функция будет не такая простая? А вдруг потом еще обнаружится, что в первой функции была ошибка? Избежать всего этого можно, например, «шаманством» с препроцессором, но это нам ни к чему, нам помогут шаблоны.
Для начала, заглянем в википедию и посмотрим, что же такое шаблоны:
Шабло́ны (англ. template) — средство языка C++, предназначенное для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию).
https://ru.wikipedia.org/wiki/Шаблоны_C++
Итак, описание шаблона начинается с ключевого слова template за которым в угловых скобках (« ») следует список параметров шаблона. Далее, собственно идет объявление шаблонной сущности (например функция или класс), т. е. имеет вид:
Теперь давайте напишем шаблонную функцию my_swap. Исходя из упомянутой выше структуры объявления шаблона следует, что наша функция будет выглядеть так:
typename в угловых скобках означает, что параметром шаблона будет тип данных. T — имя параметра шаблона. Вместо typename здесь можно использовать слово class: template В данном контексте ключевые слова typename и class эквивалентны (лично мне больше нравится typename, а кому-то class). Далее, в тексте шаблона везде, где мы используем тип T, вместо T будет проставляться необходимый нам тип.
теперь давайте напишем функцию main:
Таким образом, если мы инстанцируем этот шаблон три раза с разными типами, то компилятор создаст три разные функции
Вывод типа шаблона исходя из параметров функции
На самом деле, мы можем вызвать функцию my_swap не указывая тип в угловых скобках. В ряде случаев компилятор может это сделать за вас.
рассмотрим вызов функции без указания типа:
Наша шаблонная функция принимает параметры типа T&, основываясь на шаблоне, компилятор видит, что Вы передаете в функцию аргументы типа int, поэтому может самостоятельно определить, что в данном месте имеется ввиду функция my_swap с типом int. Это deducing template arguments. Теперь давайте напишем пример посложнее. Например, программу сортировки массива(будем использовать сортировку «пузырьком»). Естественно, что алгоритм сортировки один и тот же, а вот типы элементов в массиве будут отличаться. Для обменивания значений будем использовать нашу шаблонную функцию my_swap. Приступим:
Source arrays: 10 5 7 3 4 7.62 5.56 38 56 9 Sorted arrays: 3 4 5 7 10 5.56 7.62 9 38 56
Как видите, компилятор сам генерирует out_array для необходимого типа. Так же он сам генерирует функцию bubbleSort. А в bubbleSort у нас применяется шаблонная функция my_swap, компилятор сгенерирует и её код автоматически. Удобно, не правда ли?
Введение в шаблонные классы
Шаблонными могут быть не только функции. Рассмотрим шаблонные классы. Начнем с простого примера. Мы добавим в наш предыдущий код функцию, которая будет искать максимум и минимум в массиве. При создании функции «упираемся» в проблему — как вернуть два указателя? Можно передать их в функцию в качестве параметров, а можно вернуть объект, который будет содержать в себе два указателя. Первый вариант при большом кол-ве возвращаемых значений приведет к заваливанию функции параметрами, поэтому я предлагаю сделать структуру:
А какого же типа будут указатели? Можно сделать их void*, но тогда придется постоянно кастовать их к нужному типу, и код станет похож на «Доширак». А что, если сделать эту структуру шаблонной? Попробуем:
Теперь компилятор при виде кода my_pointer_pair сам сгенерирует нам код структуры с соответствующими типами. В данном примере указатели у нас будут одинакового типа, но структуру мы сделаем такой, чтобы типы указателей могли быть разными. Это может быть полезно в других примерах (в данном случае я просто хотел показать, что у шаблона может быть не только один параметр).
Компилятор не будет автоматически определять типы для шаблона класса, поэтому необходимо их указывать самостоятельно.
Теперь давайте напишем код шаблонной функции для поиска максимума и минимума:
Теперь мы можем вызывать данную функцию:
Для классов мы должны явно указывать параметры шаблона. В стандарте C++11, устаревшее ключевое слово auto поменяло свое значение и теперь служит для автоматического вывода типа в зависимости от типа инициализатора, поэтому мы можем написать так:
Предлагаю написать еще одну функцию, которая будет выводить объект my_pointer_pair в стандартный поток вывода:
Теперь соберем всё воедино:
Arrays: 10 5 7 3 4 7.62 5.56 38 56 9 min = 3 max = 10 min = 5.56 max = 56
Шаблоны и STL
В комплекте с компилятором Вам предоставляется стандартная библиотека шаблонов (Standart Template Library). Она содержит множество шаблонных функций и классов. Например, класс двусвязного списка(list), класс «пара» (pair), функция обмена двух переменных(swap), функции сортировок, динамически расширяемый массив(vector) и т.д. Всё это — шаблоны и Вы можете их использовать. Для небольшого примера возьмем std::vector:
Заметьте, когда писали std::vector, авторы понятия не имели, элементы какого типа Вы будете хранить.
Шаблоны это слишком большой и мощный инструмент и описать всё в одной статье не представляется возможным. Это было лишь небольшое введение в мир шаблонов. Углубляясь в шаблоны, Вы поразитесь тому, какой мощный это инструмент и какие возможности он предоставляет.
9.4 – Структуры
В программировании есть много случаев, когда нам для представления объекта необходимо более одной переменной. Например, чтобы представить себя, вы можете сохранить свое имя, дату рождения, рост, вес или любое другое количество характеристик о себе. Сделать это можно так:
Однако теперь у вас есть 6 независимых переменных, которые никак не сгруппированы. Если вы хотите передать информацию о себе в функцию, вам придется передавать каждую переменную отдельно. Кроме того, если вы хотите сохранить информацию о ком-то еще, то для каждого дополнительного человека вам нужно будет объявить еще 6 переменных! Как видите, это может быстро выйти из-под контроля.
К счастью, C++ позволяет нам создавать собственные пользовательские агрегированные типы данных. Агрегированный тип данных – это тип данных, который группирует вместе несколько отдельных переменных. Одним из простейших агрегированных типов данных является структура. Структура (сокращенно struct ) позволяет нам группировать переменные смешанных типов данных вместе в единое целое.
Объявление и определение структур
Предупреждение
Одна из самых простых ошибок C++ – это забыть о точке с запятой в конце объявления структуры. Это вызовет ошибку компилятора в следующей строке кода. Современные компиляторы, такие как Visual Studio 2010, укажут вам, что вы, возможно, забыли точку с запятой, но более старые или менее сложные компиляторы могут этого не делать, что может затруднить обнаружение реальной ошибки.
Можно определить несколько переменных одного и того же типа структуры:
Доступ к членам структуры
Как и обычные переменные, переменные-члены структуры не инициализируются и обычно содержат мусор. Мы должны инициализировать их вручную.
В приведенном выше примере очень легко определить, какие переменные-члены принадлежат Джо, а какие – Фрэнку. Это обеспечивает гораздо более высокий уровень организации, чем отдельные переменные. Более того, поскольку переменные-члены Джо и Фрэнка имеют одинаковые имена, это обеспечивает согласованность для нескольких переменных одного и того же типа структуры.
Переменные-члены структуры действуют как обычные переменные, поэтому с ними можно выполнять обычные операции:
Инициализация структур
Инициализация структур путем присвоения значений каждому члену поочередно может быть немного утомительной, поэтому C++ поддерживает более быстрый способ инициализации структур с использованием списка инициализаторов. Это позволяет вам во время объявления инициализировать некоторые или все члены структуры.
Инициализация нестатического члена
Нестатическим (обычным) членам структуры можно дать значение по умолчанию:
Если представлены и инициализатор нестатического члена, и инициализация списком, то инициализация списком имеет больший приоритет.
В приведенном выше примере Rectangle x будет инициализирован с длиной и шириной 2,0.
О том, что такое статические члены, мы поговорим в следующей главе. А пока не беспокойтесь о них.
Присваивание структурам
Если мы хотим присвоить значения членам структур, мы можем сделать это по отдельности:
Если вы хотите присвоить новые значения всем членам, это будет утомительно, особенно для структур с большим количеством членов. Присваивать значения членам структур вы можете так же, используя список инициализаторов:
Структуры и функции
Большим преимуществом использования структур вместо отдельных переменных является то, что мы можем передать всю структуру в функцию, которая должна работать с ее членами:
Приведенная выше программа выводит:
Функция также может возвращать структуру, что является одним из немногих способов вернуть функцией несколько переменных.
Эта программа печатает:
Вложенные структуры
Структуры могут содержать другие структуры. Например:
В этом случае, если мы хотим узнать, какова зарплата генерального директора (CEO), мы просто дважды используем оператор выбора члена: myCompany.CEO.wage;
Для вложенных структур вы можете использовать вложенные списки инициализаторов:
Размер структуры и выравнивание структуры данных
Обычно размер структуры – это сумма размеров всех ее членов, но не всегда!
На машине автора эта программа печатает:
Оказывается, мы можем сказать только то, что размер структуры будет не меньше размера всех содержащихся в ней переменных. Но может быть и больше! По соображениям производительности компилятор иногда добавляет разрывы в структуры (это называется заполнением).
Доступ к структурам в нескольких файлах
Поскольку объявления структуры не занимают никакой памяти, если вы хотите использовать объявление структуры в нескольких файлах (чтобы можно было создавать экземпляры переменных этого типа структуры в нескольких файлах), поместите объявление структуры в заголовочный файл и включите с помощью #include этот файл везде, где вам это нужно.
Переменные структур подчиняются тем же правилам, что и обычные переменные. Следовательно, чтобы сделать переменную структуры доступной для нескольких файлов, вы можете использовать ключевое слово extern в объявлении в заголовке и определить переменную в исходном файле.
Заключительные примечания по структурам
Структуры в C++ очень важны, поскольку понимание структур – первый важный шаг к объектно-ориентированному программированию! Позже в этой серии обучающих статей вы узнаете о другом агрегированном типе данных, называемом классом, который построен на основе структур. Хорошее понимание структур поможет значительно упростить переход к классам.
Структуры, представленные в этом уроке, иногда называют простыми старыми структурами данных (или структурами POD, plain old data struct), поскольку все члены являются членами данных (переменными). В будущем (когда мы будем обсуждать классы) мы поговорим о других типах членов.
Небольшой тест
Вопрос 1
У вас есть веб-сайт, и вы пытаетесь отслеживать, сколько денег вы зарабатываете в день на рекламе. Объявите структуру advertising (реклама), которая отслеживает, сколько объявлений вы показали читателям, на какой процент объявлений нажимали пользователи, и сколько вы в среднем зарабатывали на каждом клике по объявлению. Значения для каждого из этих полей заполните данными, вводимыми пользователем. Передайте структуру advertising функции, которая печатает каждое из значений, а затем вычисляет, сколько вы заработали за этот день (умножьте все 3 поля вместе).
Вопрос 2
Указатели и ссылки в C и C++
Немного о памяти
Память можно представить по-разному.
Объяснение для военных на примере взвода. Есть взвод солдат. Численность — 30 человек. Построены в одну шеренгу. Если отдать им команду рассчитаться, у кажого в этой шеренге будет свой уникальный номер. Обязательно у каждого будет и обязательно уникальный. Этот взвод — доступная нам память. Всего нам здесь выделено для работы 30 ячеек. Можно использовать меньше. Больше — нельзя. К каждой ячейке можно обратиться и быть уверенным, что обратился именно к ней. Любому солдату можно дать что-то в руки. Например, цветы. То есть поместить по адресу данные.
Объяснение для Маленького Принца. Здравствуй, Маленький Принц. Представим, что твоему барашку стало одиноко. И ты попросил нарисовать ему друзей. Ты выделил для барашков целую планету (точнее, астероид) по соседству. Эта планета — доступная память. Вся она уставлена коробочками, в которых будут жить барашки. Чтобы не запутаться, все коробочки пронумерованы. Коробочки — это ячейки памяти. Барашек в коробочке — это данные. Допустим, что попался какой-то особо упитанный барашек. Ему понадобится две коробочки. Или даже больше. Барашек — неделимая структура (для нас с тобой, Маленький Принц, это точно так), а коробочки идут подряд. Нет ничего проще. Мы вынимает стенки между двумя рядом стоящими коробочками и кладем туда барашка. Места в коробочке не очень много. И барашек не может свободно развернуться. Поэтому мы всегда знаем, где его голова, а где хвост. И если нам что-то нужно будет сказать барашку, мы обратимся к той коробочке, где у него голова.
Объяснение для хулиганов. Есть забор. Забор из досок. Забор — доступная память. Доска — ячейка памяти. Забор длинный. И чтобы потом похвастаться друзьям, где ты сделал надпись, надо как-то обозначить место. Я знаю, о уважаемый хулиган, что ты нашел бы что-то поинтереснее, чем нумеровать каждую доску. Но в программировании не такие выдумщики. Поэтому доски просто пронумерованы. Возможно, твоя надпись поместится на одну доску. Например, %знак футбольного клуба%. Тогда ты просто скажешь номер и друзья увидят серьезность твоего отношения к футболу. А возможно, что одной доски не хватит. Ничего, главное, чтобы хватило забора. Пиши подряд. Просто потом скажи, с какой доски читать. А что если не подряд? Бывает и не подряд. Например, ты хочешь признаться Маше в любви. Ты назначаешь ей встречу под доской номер 40. Если все пройдет хорошо, ты возьмешь Машу и поведешь ее к доске 10, где заранее написал «Хулиган + Маша = любовь». Если что-то пошло не так, ты поведешь Машу к доске 60, на которой написано все нехорошее, что ты думаешь о Маше. Примерно так выглядит условный переход. То есть оба его исхода помещаются в память заранее. На каком-то этапе вычисляется условие. Если условие выполнилось — переходим к одному месту памяти и начинаем идти дальше подряд. Если условие не выполнилось — переходим к другому месту, с другими инструкциями. И тоже продолжаем выполнять их подряд. Инструкции всегда выполняются одна за другой, если только не встретился переход (с условием или без условия). Ну, или что-то поломалось.
Модель взаимодействия программы с памятью компьютера может быть разной. Будем считать, что для каждой программы выделяется своя обособленная область памяти. Даже если запущены два экземпляра одной программы — память у них будет разная.
В памяти хранятся числа. Ни с чем кроме чисел компьютер работать не умеет. Если вы поместили в память какую-то комплексную структуру, она все равно будет представлена числами. Даже если вы работаете с ней как со структурой. Примером комплексной структуры в терминах языков C и C++ может быть, например, экземпляр структуры или объект класса.
Наименьшей адресуемой величиной в памяти типового компьютера является байт. Это означает, что каждый байт имеет собственный адрес. Для того, чтобы обратиться к полубайту, придется обратиться сначала к байту, а затем выделить из него половину.
Возможно, подобные объяснения кажутся очевидными и даже смешными. Но в действительности имеет значение только формализация. И то, что кажется привычным, в определенных случаях может быть совсем иным. Например, запросто можно задать условие, при котором байт не будет равен 8 битам. И такие системы существуют.
Раз уж мы договорились, что минимальная адресуемая величина — байт, то всю доступную программе память можно представить в виде последовательности байтов.
Система в компьютере двоичная (хотя есть и тернарные машины). В 1 байте 8 бит. Английское bit означает binary digit, то есть двоичный разряд. Получается, что байт может принимать числовые значения от 0 до 2 в 8 степени без единицы. То есть от 0 до 255. Если представлять числа в шестнадцатеричной системе, то от 0x00 до 0xFF.
Представим область памяти.
0x01 | 0x02 | 0x03 | 0x04 |
0x05 | 0x06 | 0x07 | 0x08 |
0x09 | 0x0A | 0x0B | 0x0C |
0x0D | 0x0E | 0x0F | 0x10 |
В ней лежат числа от 1 до 16. Направление обхода обычно задается слева направо и сверху вниз. Помните, что никакой таблицы на самом деле нет (почти как ложки в Матрице). Она нужна человеку для удобства восприятия. Каждая такая ячейка описывается двумя величинами: значением и адресом. В приведенной таблице значение и адрес совпадают.
Понятие указателя
Указатель — это переменная. Такая же, как и любая другая. Со своими «можно» и со своими «нельзя». У нее есть свое значение и свой адрес в памяти.
Значение переменной-указателя — адрес другой переменной. Адрес переменной-указателя свой и независимый.
Переменная-указатель (далее будем говорить просто — указатель) объявляется также, как и любые другие переменные, но после имени типа ставится звездочка.
Здесь объявляется переменная pointerToInteger. Ее тип — указатель на переменную типа int.
Как следует писать звездочку относительно типа и имени переменной? Встречаются, например, такие формы записи, и все они имеют право на существование:
Аргументы за первую форму. Чтобы объявить переменную следует указать ее тип, а затем имя. Звездочка является частью типа, а не частью имени. Это также подтверждается тем, что при привидении типов пишется тип со звездочкой, а не тип отдельно. Следовательно, должна писаться слитно с типом. Минус в том, что при объявлении нескольких переменных после объявления int*, только первая из них будет указателем, а вторая будет просто переменной типа int. Не объявляйте несколько указателей в одной строчке. Это не очень хороший стиль.
Аргументы за вторую форму. Есть люди, которым нравится «когда код дышит» Они ставят пробел до скобок и после скобок. И здесь тоже ставят. Возможно, это просто такой компромисс.
Аргументы за третью форму. Если писать так, то с объявлением нескольких указателей в одной строчке проблем быть не должно (хотя это все равно плохой тон). Некоторая идеология нарушается. Но этот стиль — самый распространенный, так как точно видно, что переменная — указатель.
И помните, что компилятору все это безразлично.
Адрес переменной и значение переменной по адресу
Рассмотрим две переменные: целочисленную переменную x и указатель на целочисленную переменную.
Чтобы получить адрес переменной, нужно перед ее именем написать амперсанд.
Данная конструкция будет выполняться справа налево. Сначала с помощью оператора &, примененного к переменной x, будет получен адрес x. Затем адрес x будет сохранен в указателе p.
Есть и обратная операция. Чтобы получить значение переменной по ее адресу, следует написать звездочку перед именем указателя.
Такая операция в русском языке называется не слишком благозвучным словом «разыменование». В английском — dereference.
В данном примере с помощью оператора * мы получим то значение, которое находится в памяти по адресу p. Затем мы сохраним его в переменную y. В итоге получится, что значения x и y совпадают.
Все это несложно увидеть на экране.
В указанном примере значение x и y будут одинаковы. А также адрес x и значение p.
Адресная арифметика
К указателям можно прибавлять числа. Из указателей можно вычитать числа. На основе этого сделана адресация в массиве. Этот код показывает несколько важных вещей.
Первая строка простая и понятая. Объявлен массив и заполнен числами от 1 до 5.
Во второй строке объявляется указатель на int и ему присваивается адрес нулевого элемента массива. Некоторые компиляторы разрешают писать такое присвоение так, считая, что имя массива означает адрес его нулевого элемента.
Но если вы хотите избежать неоднозначности, пишите явно. Таким образом в p лежит адрес начала массива. А конструкция *p даст 1.
Третья строчка увеличивает значение p. Но не просто на 1, а на 1 * sizeof(int). Пусть в данной системе int занимает 4 байта. После увеличения p на 1, p указывает не на следующий байт, а на первый байт из следующей четверки байтов. Программисту не нужно думать в данном случае о размере типа.
С вычитанием ситуация такая же.
Последний важный момент этого кода в том, как преобразуется обращение к элементу массива. Имя массива — это указатель на его начало. Точка отсчета. Индекс, переданный в квадратных скобках, — смещение относительно начала массива.
Конструкция array[i] будет преобразована компилятором к *(array + i). К начальному адресу массива будет прибавлено число с учетом размерности типа данных. А затем будет взято значение по вычисленному адресу. Обратите внимание, что никто не запрещает написать и так i[array]. Ведь конструкция будет преобразована к виду.
С указателем можно складывать число, представленное переменной или целочисленной константой. Вычесть можно не только число, но и указатель из указателя. Это бывает полезно. А вот сложить два указателя, умножить или разделить указатель на число или на другой указатель — нельзя.
С указателями разных типов нельзя обходиться легкомысленно.
С точки зрения языка C все корректно. А вот в C++ будет ошибка, потому что типы указателей не совпадают.
Вот такая конструкция будет принята C++.
Применение указателей
Обычно функция возвращает одно значение. А как вернуть больше одного? Рассмотрим код функции, которая меняет местами две переменные.
Пусть есть переменные x и y с некоторыми значениями. Если выполнить функцию, передав в нее x и y, окажется, что никакого обмена не произошло. И это правильно.
При вызове этой функции в стеке будут сохранены значения x и y. Далее a и b получат значения x и y. Будет выполнена перестановка. Затем функция завершится и значения x и y будут восстановлены из стека. Все по-честному.
Чтобы заставить функцию работать так, как нужно, следует передавать в нее не значения переменных x и y, а их адреса. Но и саму функцию тогда нужно адаптировать для работы с адресами.
Не стоить забывать о том, что и вызов функции теперь должен выглядеть иначе.
Теперь в функцию передаются адреса. И работа ведется относительно переданных адресов.
Если функция должна вернуть несколько значений, необходимо передавать в нее адреса.
Если функция должна менять значение переменной, нужно передавать ей адрес этой переменной.
У тех, кто только начинает программировать на C, есть одна распространенная ошибка. При вводе с клавиатуры с помощью функции scanf() они передают значение переменной, а не ее адрес. А ведь scanf() должна менять значение переменной.
Еще один важный случай, когда указатели крайне полезны — это передача большого объема данных.
Пусть нам нужно передать в функцию целое число типа int. Таким образом мы передаем в функцию sizeof(int) байт. Обычно это 4 байта (размер будет зависеть от архитектуры компьютера и компилятора). 4 байта — не так много. 4 байта уйдут в стек. Потому что имеет место передача по значению.
Теперь нам нужно передать 10 таких переменных. Это уже 40 байт. Тоже невелика задача.
Вообразим себя проектировщиками Большого Адронного Коллайдера. Вы отвечаете за безопасность системы. Именно вас окружают люди с недобрыми взглядами и факелами. Нужно показать им на модели, что конца света не будет. Для этого нужно передать в функцию collaiderModel(), скажем, 1 Гб данных. Представляете, сколько информации будет сохранено в стек? А скорее всего программа не даст вам стек такого объема без специальных манипуляций.
Когда нужно передать большой объем данных, его передают не копированием, а по адресу. Все массивы, даже из одного элемента, передаются по адресу.
Указатели — это мощный инструмент. Указатели эффективны и быстры, но не слишком безопасны. Потому как вся ответственность за их использования ложится на разработчика. Разработчик — человек. А человеку свойственно ошибаться.
В большинстве компиляторов C и С++ неинициализированные локальные переменные имеют случайное значение. Глобальные обнуляются.
Если мы захотим разыменовать указатель и присвоить ему значение, скорее всего, будет ошибка.
Неинициализированный указатель p хранит случайный адрес. Мы честно можем попытаться получить значение по этому адресу и что-то туда записать. Но совсем не факт, что нам можно что-то делать с памятью по этому адресу.
Указатели можно и нужно обнулять. Для этого есть специальное значение NULL.
Это запись больше соответствует стилю C. В C++ обычно можно инициализировать указатель нулем.
Ловкость рук и никакого мошенничества. На самом деле, если изучить библиотечные файлы языка, можно найти определение для NULL.
Для C NULL — это нуль, приведенный к указателю на void. Для C++ все немного не так. Стандарт говорит: «The macro NULL is an implementation-defined C++ null pointer constant in this International Standard. Possible definitions include 0 and 0L, but not (void*)0». То есть это просто 0 или 0, приведенный к long.
Предлагаю вам такую задачку. Папа Карло дал Буратино 5 яблок. Злой Карабас Барабас отобрал 3 яблока. Сколько яблок осталось у Буратино?
Ответ: неизвестно. Так как нигде не сказано, сколько яблок у Буратино было изначально.
Мораль: обнуляйте переменные.
Ссылки
В языке C++ появился новый механизм работы с переменными — ссылки. Функция swap() была хороша, только не слишком удобно применять разыменование. С помощью ссылок функция swap() может выглядеть аккуратнее.
А вызов функции тогда будет уже без взятия адреса переменных.
Конструкция double& объявляет ссылку на переменную типа double. При таком объявлении функции в стек будут положены не значения переменных, а их адреса.
Ссылка — это указатель, с которым можно работать, как с обычной переменной.
Ссылка не может быть равна NULL. Указатель может. Ссылка не может быть непроинициализирована. Указатель может.
Для взятия адреса переменной и для объявления ссылки используется одинаковый символ — амперсанд. Но в случае взятия адреса & стоит в выражении, перед именем переменной. А в случае объявления ссылки — в объявлении, после объявления типа.
Использование ссылок и указателей — это очень широкая тема. Описание основ на этом закончим.
За мысли и замечания спасибо Юрию Борисову, @vkirkizh, @vitpetrov.