Перейти к содержимому

Pointer to c что это

  • автор:

Указатели в C/C++ для начинающих

Andrew Peace, “A Beginner’s Guide to Pointers”, публичный перевод на русский с английского Подробнее об этом переводе .

An article showing the use of pointers in C and C++

Что такое указатели?

Указатели — это те же переменные. Разница в том, что вместо того, чтобы хранить определенные данные, они хранят адрес (указатель), где данные могут быть найдены. Концептуально это очень важно. Многие программы и идеи зависят от указателей, как от основы их архитектуры, например, связанные списки (linked lists).

Как объявить указатель? Собственно, так же, как и любую другую переменную, но с добавлением звездочки перед именем переменной. Так, например, следующий код создает два указателя, которые указывают на целое число.

Обратили внимание на префикс «p» в обоих именах переменных? Это принятый способ обозначить, что переменная является указателем. Так называемая венгерская нотация. Теперь давайте сделаем так, чтобы указатели на что-нибудь указывали:

Знак & (амперсанд) следует читать как «адрес переменной . » и означает адрес переменной в памяти, который будет возвращен вместо значения самой переменной. Итак, в этом примере pNumberOne установлен и содержит адрес переменной some_number, также pNumberOne указывает на some_number.

Таким образом, если мы хотим получить адрес переменной some_number, мы можем использовать pNumberOne. Если мы хотим получить значение переменной some_number через pNumberOne, нужно добавить звездочку (*) перед pNumberOne (*pNumberOne). Звездочка (*) разыменовывает (превращает в саму переменную) указатель и должна читаться как «место в памяти, которое указывается через . «, кроме объявлений, как в строке int *pNumber.

Чему мы научились: Пример

Фух! Многовато для начала, не правда ли? Я бы рекомендовал, если вы не понимаете то, что описано выше, перечитать ещё раз. Указатели — дело непростое и понимание их концепции может занять определенное время. Вот пример, который показывает идеи, изложенные выше. Пример написан на C, без дополнений C++.

// инициализируем объявленные переменные:

// выводим значение переменной nNumber:

printf(«nNumber is equal to : %d\n», nNumber);

// теперь изменяем nNumber через pPointer:

// убедимся что nNumber изменил свое значение в результате предыдущего действия,

// выведя значение переменной ещё раз

printf(«nNumber is equal to : %d\n», nNumber);

Внимательно просмотрите код выше и скомпилируйте. Убедитесь в том, что вы понимаете, почему он работает. Потом, если вы готовы, читайте дальше!

Попробуйте найти ошибку в этой программе:

// делаем так, чтобы pPointer указывал на nNumber

SomeFunction(); // сделайте чтобы pPointer указывал на что нибудь

// почему это не работает?

printf(«Value of *pPointer: %d\n», *pPointer);

Программа сначала вызывает функцию SomeFunction, которая создает переменную nNumber, а потом инициализирует pPointer, чтобы он указывал на nNumber. Далее начинаются проблемы. Когда функция завершается, nNumber удаляется, поскольку это локальная переменная. Локальные переменные всегда удаляются, когда выполнение программы выходит из блока, где эти переменные были объявлены. Это означает, что когда функция SomeFunction завершается и выполнение возвращается в main(), переменная исчезает. Таким образом pPointer указывает на ту область памяти, где была переменная, но которая уже не принадлежит программе. Если вы не совсем поняли, о чем идет речь, ещё раз прочтите о локальных и глобальных переменных, а также об областях определения (scope). Эта концепция также очень важна.

Итак, как проблема может быть решена? Ответ — использованием техники известной как динамическое выделение памяти. Обратите внимание, что в языке C такой техники нет, а пример ниже относится к C++.

Динамическое выделение памяти

Динамическое выделение памяти, возможно, является ключом к указателям. Она используется для того, чтобы выделить память без необходимости объявлять переменные, а затем создавать указатели, указывающие на эти переменные. Все же концепция может показаться сначала запутанной, но она очень проста. Код ниже демонстрирует, как выделить память для целого числа:

pNumber = new int;

Первая строчка объявляет указатель pNumber. Вторая строчка выделяет память для целого числа (int) и указывает pNumber на эту область памяти. Вот ещё один пример, на этот раз с числом с двойной точностью (double).

pDouble = new double;

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

// pPointer указывает на целое число

pPointer = new int;

SomeFunction(); //инициализируем указатель чтобы он на что то указывал

printf(«Value of *pPointer: %d\n», *pPointer);

Прочитайте и скомпилируйте приведеный пример. Убедитесь что вы понимаете как и почему он работает. Когда вызывается SomeFunction, она выделяет память и инициализирует pPointer чтобы он указывал на неё. В этот раз когда функция завершается выделенная память остается нетронутой и pPointer всё ещё указывает на неё. На этом всё про динамическое выделение памяти!

Память приходит, память уходит

С памятью всегда существуют сложности и в данном случае довольно серьезные, но эти сложности можно с легкостью обойти. Проблема заключается в том что не смотря на то что память которая была динамически выделена остается нетронутой она никогда не освобождается автоматически. Память будет оставаться выделенной до тех пор пока вы не скажете компьютеру что вам она больше не нужна. Проблема в том что если вы не скажете системе что память вам больше не нужна, она будет занимать место которое возможно необходимо другим приложениям либо частям вашего приложения. В частности это может привести к сбою системы по причине использования всей доступной памяти, поэтому это очень важно. Освобождение памяти когда она вам больше не нужна делается очень просто:

С этим всё. Вы должны быть осторожны поскольку нужно передавать правильный указатель, указатель который который указывает именно на ту область памяти которую вы выделяли а не на непонятный мусор. Попытка освободить память (при помощи delete) которая уже была освобождена опасно и может привести к сбою программы. По-этому следующий обновленный пример показывает как это делать правильно без траты памяти:

// делаем так чтобы pPointer указывал на целое число

pPointer = new int;

SomeFunction(); // указываем pPointer на что нибудь

printf(«Value of *pPointer: %d\n», *pPointer);

Разница в одну строку — это все, что сделано, но это важная строка. Если вы не удалите память, то получите так называемую «утечку памяти», когда память постепенно утекает и не может быть возвращена, пока приложение не закроется.

Передача указателей в функции

Возможность передавать указатели в функции очень полезна, и ее легко освоить. Если нам нужна программа, которая получает число и прибавляет к нему пять, мы можем написать что-то похожее на этот код:

void AddFive(int Number)

Number = Number + 5;

int nMyNumber = 18;

printf(«My original number is %d\n», nMyNumber);

printf(«My new number is %d\n», nMyNumber);

Однако здесь проблема в том, что переменная Number, к которой мы обращаемся внутри функции — это копия переменной nMyNumber, передаваемой в функцию. Таким образом, строка Number = Number + 5 добавляет пять к копии переменной, оставляя оригинальную переменную в main() неизменной. Попробуйте запустить программу, чтобы убедиться в этом.

Чтобы избавиться от этой проблемы, мы можем передавать в функцию указатель на место в памяти, где хранится число, но тогда мы должны поправить функцию, чтобы она принимала указатель вместо числа. Для этого изменим void AddFive(int Number) на AddFive(int* Number) добавлением звездочки. Здесь снова текст программы, с внесенными изменениями. Обратите внимание, чт мы должны убедиться, что передаем в функцию адрес nMyNumber вместо самого числа. Это сделано при помощи оператора &, который (как вы помните), читается как «получить адрес».

void AddFive(int *Number)

*Number = *Number + 5;

int nMyNumber = 18;

printf(«My original number is %d\n», nMyNumber);

printf(«My new number is %d\n», nMyNumber);

Попробуйте придумать свой собственный пример для демонстрации этих возможностей указателей. Обратили внимание на важность звездочки (*) перед Number в функции AddFive? Это необходимо для того чтобы указать компилятору что мы хотим добавить 5 к числу на которое указывает переменная Number, а не добавить 5 к самому указателю. Последнее замечание по поводу функций это то что вы можете возвращать из них указатели, это делается так:

В этом примере, MyFunction возвращает указатель на целое число (int).

Указатели на классы

Есть ещё парочка предостережений, одно из них это структуры и классы. Вы можете задать класс как показано ниже:

Далее вы можете объявить переменную типа MyClass следующим образом:

Вы уже должны это знать. Если нет, перечитайте снова текст выше. Для того чтобы объявить указатель на MyClass, вы сделаете следующее:

. как вы и ожидали. Далее вы выделяете память и инициализируете указатель чтобы он указывал на неё:

thing = new MyClass;

Тут то и начинаются проблемы. Как вы будете дальше использовать этот указатель? Да, в обычной ситуации вы бы написали thing.m_Number, но вы не можете, потому что это не MyClass, а только указатель на него. Так, thing сам посебе не содержит переменной m_Number, это структура на которую он указывает которая содержит m_Number. Соответственно мы должны использовать другую систему доступа. Для этого точка (.) заменяется на черту со знаком больше (->). Следующий пример показывает как:

pPointer = new MyClass;

Указатели на массивы

Вы также можете создавать указатели которые указывают на массивы. Это делается так:

pArray = new int[6];

Этот пример создает указатель pArray и направляется его на масив из шести элементов. Другой способ, без использования динамического выделения, выглядит так:

Обратите внимание что вместо написания &MyArray[0], вы можете просто написать MyArray. Это, конечно же, применимо только к массивам по причине способа их реализации в языках C/C++. По общим правилам необходимо было бы написать pArray = &MyArray, но это неправильно. Если вы так напишите то получите указатель на указатель на массив (не опечатка), что определенно не то что вам нужно.

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

Если у вас есть указатель на масив, как его использовать? Например у вас есть указатель на массив целых чисел (int[]). Указатель изначально будет указывать на первое значение в массиве как показывает следующий пример:

printf(«pArray points to the value %d\n», *pArray);

Для того чтобы переместить указатель к следующему элементу массива, мы можем сделать pArray++. Мы также можем, как некоторые из вас могли уже догадатьтся, сделать pArray+2, что передвинет указатель сразу на 2 элемента. С чем нужно быть осторожным так это с верхней границей массива (в данном случае это 3 элемента), потому что компилятор не может проверить вышли ли вы за предел массива используя указатели. Вы легко можете получить полный сбой системы если будете не аккуратны. Вот ещё один пример, на этот раз показывающий три значения которые мы установили:

printf(«pArray points to the value %d\n», *pArray);

printf(«pArray points to the value %d\n», *pArray);

printf(«pArray points to the value %d\n», *pArray);

Мы также можем двигать указатель в любую сторону, так pArray — 2 это 2 элемента от того места куда указывает указатель. Убедитесь что вы добавляете и вычитает значение указателя а не у значения на которое он указывает. Данный метод использования указателей и массивов более всего полезен при использовании циклов, таких как for или while.

Заметим так же , что если мы имеем указатель на значение, например int* pNumberSet, мы можем обращаться к нему как к массиву. Вот пример, pNumberSet[0] равно *pNumberSet; а pNumberSet[1] равно *(pNumberSet + 1).

Еще одно важное замечание по массивам состоит в том, что если мы выделяем память при помощи new, как в таком примере:

pArray = new int[6];

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

Обратим внимание на [] после delete. Это указание компилятору о том, что удаляется целый массив, а не отдельный элемент. Мы должны использовать этот метод всякий раз при удалении массивов, иначе у нас возникнет утечка памяти.

Последнее замечание: мы не должны удалять память, которую не выделили ранее с использованием new, как в следующем примере:

int *pNumber = number;

delete pNumber;// ошибка — память для *pNumber не была выделена при помощи new.

Общие вопросы и ЧаВо

Вопрос: Почему я получаю ошибки «symbol undefined» при использовании new и delete?

Ответ: вероятнее всего, это происходит потому, что ваш исходный код интерпретируется компилятором как написанный на языке C. Операторы new и delete являются новыми возможностями языка C++. Ситуацию обычно можно исправить, указав для исходных файлов расширение *.cpp вместо *.c

Вопрос: в чем разница между new и malloc?

Ответ: Оператор new сушествует только в языке C++ и является стандартным (за исключением особых функций в Windows) способом выделения памяти. Вы не должны использовать malloc в C++, разве что в случае крайней необходимости. Поскольку malloc не был разработан для объектно ориентированных возможностей языка C++, его использование для выделения памяти для классов приведет к тому что конструктор класса вызван не будет, как пример того какие проблемы могут возникнуть. В результате проблем возникших при использовании malloc и free, а также потому что они устарели для любого использования (в языке C++) в этой статье они не обсуждаются детально. Я не одобряю использование этих функций где бы то нибыло.

Вопрос: можно ли использовать free и delete вместе?

Ответ: Вы должны освобождать память тем же механизмом которым вы её выделяли. Например используйте free только в случае если память выделялась функцией malloc, delete используется только если память выделялась при помощи new, и так далее.

Ссылки определенно находятся вне темы этой статьи. Но поскольку меня очень часто спрашивали о ссылках люди которые читали эту статью я опишу их кратко. Ссылки очень схожи с указателями и в большенстве случаем они могут быть использованы как альтернатива указателям. Как вы помните я уже писал что амперсанд (&) читается при объявлении как «адрес переменной . «. В случае присутствия амперсанда в объявлении в том виде который показан ниже — его следует читать как «ссылка на переменную . «.

Ссылки это те же что и указатель на myOtherNumber, за исключение того что ссылка автоматически разименовывается. Таким образом ссылка ведет себя как переменная со значением а не как переменная с адресом (как указатель). Идентичный по смыслу код, используя указатели показан ниже:

Другое отличие между указателями и ссылками это то что вы не можете «сбросить» ссылку. Это означает что нельзя изменять значение адреса ссылки после того как она была инициализирована. Например, код ниже выведет «20»:

int myFirstNumber = 25;

int mySecondNumber = 20;

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

CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)

Тему указателей сложно освоить с первого раза, так что стоит просмотреть всё заново. Большинство людей не понимает всё с первого прочтения. Вот основные моменты снова:

Указатели это переменные которые указывают на область памяти. Вы объявляете указатель добавляя звездочку (*) перед именем переменной (int * number).

Вы можете получить адрес переменной добавив амперсанд (&) перед её именем, например pNumber = &my_number.

Звездочка при объявлении переменной (например int * number), читается как «адрес памяти который указывается переменной . «.

Амперсанд при объявлении переменной (например int &number), читается как «адрес переменной . «

Вы можете выделять память используя оператор new.

Указатели ДОЛЖНЫ быть того же типа что и переменные на которые вы хотите чтобы они указывали. Например int *number не будет указывать на MyClass.

Вы можете передавать указатели в функции.

Вы должны удалит память которая была выделена при помощи оператора delete.

Вы можете получить указатель на массив который уже существует следующим образом: &array[0];.

Вы должны удалить массив память для которого была динамически выделена следующим образом: delete[], а не просто delete.

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

Вот и все! Попробуйте запустить код представленый в этой статье и придумать ещё какие нибудь свои вариации и примеры.

Страницы: ← предыдущая Ctrl следующая →

Лицензия: The Code Project Open License (CPOL)

Понравился перевод? Добавьте в свои закладки и поделитесь ссылкой с другими.

Похожие переводы

Marijn Haverbeke, Eloquent JavaScript: A Modern Introduction to Programming. Перевод на испанский с английского. Для перевода предложил xlator 4 года, 1 месяц назад.
Метки: Eloquent JavaScript: в введение программирование Современное
3% переведено начерно. Объем: 18.0 тыс. знаков Перевод «What Is RAD Studio»

Техническая документация к среде разработки RAD Studio. О Среде Разработки.

embarcadero co, What Is RAD Studio. Перевод на русский с английского. Для перевода предложил JAnti 6 лет, 11 месяцев назад.
Метки: программирование среда разработки технологии
Перевод еще не начат. Объем: 2.4 тыс. знаков Основы протокола Buffer: C++

Вводный урок по протоколу Protocol Buffer для языка программирования C++

google, Protocol Buffer Basics: C++. Перевод на русский с английского. Для перевода предложил neon1ks 8 лет назад.
Метки: c++ Google protocol-buffers
65% переведено начерно. Объем: 18.5 тыс. знаков 11 нестандартных советов для развития навыков программирования

There are plenty of good resources out there that teach the technical skills that are necessary for becoming a good .

Livio De La Cruz, 11 Unconventional Tips for Improving your Programming Skills. Перевод на русский с английского. Для перевода предложила EvilCat 8 лет, 7 месяцев назад. Опубликовано 8 лет, 7 месяцев назад.
Метки: программирование
Перевод закончен. Объем: 23.7 тыс. знаков Руководство Rust

The Rust Project Developers, The Rust Guide. Перевод на русский с английского. Для перевода предложил wi34rd 9 лет назад.
Метки: rust программирование руководство
2% переведено начерно. Объем: 145.0 тыс. знаков Смотрите также ещё 82 похожих перевода

Disclaimer

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

  • оплачиваем хостинг
  • поднимаем сервис когда он падает и когда у наших сотрудников есть на это время. Пожалуйста, учитывайте это, когда принимаете решение пользоваться этим сервисом.

βετα. Проект делается силами компании JetStyle.

Лесной проект

  • О проекте
  • Наше сообщество
  • Пишите нам письма

Pointer Checker: проверим наши указатели

Мы все сталкивались с проблемами, возникающими при неправильной работе с указателями: выход за пределы массива и переполнение буфера, случайная запись в неизвестный кусок памяти, с последующим чтением этого «мусора» в другом месте, а в некоторых отдельных случаях и просто падение всей системы. Иногда это просто «дичь», господа! И нужно уметь обходится с этой «дичью» правильно – вовремя находить и исправлять подобные ошибки и проблемы. Именно этим занялись в «плюсовом» компиляторе Intel ещё несколько релизов тому назад. Кроме того, многие идеи пошли дальше и будут реализованы в «железе» через технологию Intel Memory Protection Extensions. Давайте-ка посмотрим, как всё это работает в компиляторе.

«Как бы было хорошо, если бы была такая опция компилятора, которая позволяла бы находить ошибки с указателями сразу в коде, изменяла его и давала на выходе отлаженное рабочее приложение», – мечталось одному разработчику. На самом деле такого нет, и, вроде как, не планируется. Компилятор Intel дает только средство динамической проверки кода. Это означает, что нам, как обычно, нужно подключить одну из магических опций, собрать с ней код и запустить приложение на исполнение, получив при этом ошибку, в причинах которой будет легко разобраться. Вот и весь путь. В деталях же это выглядит так.

С помощью фичи Pointer Checker мы можем отлавливать работу с памятью через указатели во всем приложении. Для этого у каждого указателя определяется нижняя и верхняя допустимые границы, которые проверяются при работе с памятью и гарантируют корректную работу. Естественно, эта информация хранится в специальной табличке по какому-то адресу. В ней мы можем для любого указателя найти значения нижней (lower bound) и верхней (upper_bound) границы.

В простейшем случае, если мы выделили память для p через malloc(size), то в lower_bound(p) будет адрес (char *)p, а в upper_bound(p) — адрес (lower_bound(p) + size – 1). И в самом тривиальном примере это позволит обнаружить проблему:

 char * buffer = (char*)malloc(5); for (int i = 0; i  

В массиве только 5 элементов, и при попытке записи в адрес, превышающий допустимую верхнюю границу, мы получим ошибку времени выполнения, говорящую о выходе за границы.
Выглядеть будет примерно так:

CHKP: Bounds check error ptr=0X012062ED sz=1 lb=0X012062E8 ub=0X012062EC loc=0X0 0131149 Traceback: wmain [0x131149] in file C:\ConsoleApplication1.cpp at line 12 __tmainCRTStartup [0x13F959] in file f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c at line 623 wmainCRTStartup [0x13FA9D] in file f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c at line 466 BaseThreadInitThunk [0x76D3919F] RtlInitializeExceptionChain [0x77550BBB] RtlInitializeExceptionChain [0x77550B91] CHKP Total number of bounds violations: 1 
  • Check bounds for reads and writes (/Qcheck-pointers:rw)
  • Check bounds for writes only (/Qcheck-pointers:write)
  • Check bounds for reads and writes, using Intel MPX (/Qcheck-pointers-mpx:rw)
  • Check bounds for writes only, using Intel MPX (/Qcheck-pointers-mpx:write)
 for (int i = 0; i  

Скомпилировав же с ключом Qcheck-pointers:rw, будем отлавливать все случаи, включая чтение. Кстати, при передачи указателя в функцию, информация о границах так же сохраняется.
Есть ещё одна интересная особенность: нужно уметь различать понятия работы с памятью и простой арифметики с указателями. Пример:

 char *p = (char *)malloc(100); p += 200; p[-101] = 0; p[0] = 0; 

В первом выражении мы только сдвигаем указатель, а затем обращаемся к памяти, находящейся в валидной области – p[-101] в данном случае является p[99]. Поэтому всё проходит гладко. Ошибка выхода за границы случится только на последней строчке, потому что мы, фактически, пытаемся записать в p[200].
Существует специальный алгоритм для нахождения висящих (dangling) указателей для чего используется опция Qcheck-pointers-dangling (её нужно указывать вместе с Qcheck-pointers). Это те случаи, когда память уже очищена, а мы упорно пытаемся что-то сделать через указатель. Если продолжать наш пример с буфером, что-то из этого разряда:

 free(buffer); printf("%c", buffer[2]); 

При этом без дополнительной установки Qcheck-pointers-dangling данный случай не будет рассматриваться как ошибка. Если же прописать Qcheck-pointers-dangling, то компилятор будет использовать специальную обертку для функций free и оператора delete. Она находит все указатели с очищенной памятью и выставляет значения нижней границы в 2, а верхней в 0. Таким образом, любая попытка работать с памятью через этот указатель приведет к ошибке. В рассмотренном примере ошибка будет выглядеть так (traceback информацию убрал для компактности):

CHKP: Bounds check error ptr=0X007F62EA sz=1 lb=0X00000002 ub=00000000 loc=0X00DB11D5 

Кстати, если у нас есть своя реализация функции для работы с памятью, мы можем включить в неё функцию проверки висящих указателей с помощью вызова функции __chkp_invalidate_dangling, объявленной в chkp.h.
Пример функции, выполняющей очищение памяти, будет выглядеть так:

 #include void my_free(void *ptr) < size_t size = my_get_size(ptr); // do the free __chkp_invalidate_dangling(ptr, size); >

В заключение скажу, что возможностей у Pointer Checker’a очень много и всё это нужно пробовать ручками. Например, есть возможность выборочно компилировать один или несколько модулей с проверкой указателей, а другие — без. Кроме того, доступны многие API функции для более гибкой работы. Стоит отметить, что Pointer Checker поддерживается только под Windows и Linux, на Mac его нет.
Существует и обратная сторона медали – выполнение приложения существенно замедляется (минимум, в два раза, но, не более чем в 5). Естественно, размер кода так же увеличивается. Тем не менее, для целей отладки приложения функциональность очень интересная, а с её реализацией в «железе» все будет гораздо эффективнее.

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

  • Блог компании Intel
  • Программирование
  • C++
  • Компиляторы

C - Pointers

Pointers in C are easy and fun to learn. Some C programming tasks are performed more easily with pointers, and other tasks, such as dynamic memory allocation, cannot be performed without using pointers. So it becomes necessary to learn pointers to become a perfect C programmer. Let's start learning them in simple and easy steps.

As you know, every variable is a memory location and every memory location has its address defined which can be accessed using ampersand (&) operator, which denotes an address in memory. Consider the following example, which prints the address of the variables defined −

#include int main ()

When the above code is compiled and executed, it produces the following result −

Address of var1 variable: bff5a400 Address of var2 variable: bff5a3f6

What are Pointers?

A pointer is a variable whose value is the address of another variable, i.e., direct address of the memory location. Like any variable or constant, you must declare a pointer before using it to store any variable address. The general form of a pointer variable declaration is −

type *var-name;

Here, type is the pointer's base type; it must be a valid C data type and var-name is the name of the pointer variable. The asterisk * used to declare a pointer is the same asterisk used for multiplication. However, in this statement the asterisk is being used to designate a variable as a pointer. Take a look at some of the valid pointer declarations −

int *ip; /* pointer to an integer */ double *dp; /* pointer to a double */ float *fp; /* pointer to a float */ char *ch /* pointer to a character */

The actual data type of the value of all pointers, whether integer, float, character, or otherwise, is the same, a long hexadecimal number that represents a memory address. The only difference between pointers of different data types is the data type of the variable or constant that the pointer points to.

How to Use Pointers?

There are a few important operations, which we will do with the help of pointers very frequently. (a) We define a pointer variable, (b) assign the address of a variable to a pointer and (c) finally access the value at the address available in the pointer variable. This is done by using unary operator * that returns the value of the variable located at the address specified by its operand. The following example makes use of these operations −

#include int main () < int var = 20; /* actual variable declaration */ int *ip; /* pointer variable declaration */ ip = &var; /* store address of var in pointer variable*/ printf("Address of var variable: %x\n", &var ); /* address stored in pointer variable */ printf("Address stored in ip variable: %x\n", ip ); /* access the value using the pointer */ printf("Value of *ip variable: %d\n", *ip ); return 0; >

When the above code is compiled and executed, it produces the following result −

Address of var variable: bffd8b3c Address stored in ip variable: bffd8b3c Value of *ip variable: 20

NULL Pointers

It is always a good practice to assign a NULL value to a pointer variable in case you do not have an exact address to be assigned. This is done at the time of variable declaration. A pointer that is assigned NULL is called a null pointer.

The NULL pointer is a constant with a value of zero defined in several standard libraries. Consider the following program −

#include int main ()

When the above code is compiled and executed, it produces the following result −

The value of ptr is 0

In most of the operating systems, programs are not permitted to access memory at address 0 because that memory is reserved by the operating system. However, the memory address 0 has special significance; it signals that the pointer is not intended to point to an accessible memory location. But by convention, if a pointer contains the null (zero) value, it is assumed to point to nothing.

To check for a null pointer, you can use an 'if' statement as follows −

if(ptr) /* succeeds if p is not null */ if(!ptr) /* succeeds if p is null */

Pointers in Detail

Pointers have many but easy concepts and they are very important to C programming. The following important pointer concepts should be clear to any C programmer −

Sr.No. Concept & Description
1 Pointer arithmetic

There are four arithmetic operators that can be used in pointers: ++, --, +, -

You can define arrays to hold a number of pointers.

C allows you to have pointer on a pointer and so on.

Passing an argument by reference or by address enable the passed argument to be changed in the calling function by the called function.

C allows a function to return a pointer to the local variable, static variable, and dynamically allocated memory as well.

Kickstart Your Career

Get certified by completing the course

Ссылки

Ссылка reference — механизм языка программирования (C++), позволяющий привязать имя к значению. В частности, ссылка позволяет дать дополнительное имя переменной и передавать в функции сами переменные, а не значения переменных.

Синтаксически ссылка оформляется добавлением знака & (амперсанд) после имени типа. Ссылка на ссылку невозможна.

Ссылка требует инициализации. В момент инициализации происходит привязка ссылки к тому, что указано справа от = . После инициализации ссылку нельзя “отвязать” или “перепривязать”.

Любые действия со ссылкой трактуются компилятором как действия, которые будут выполняться над объектом, к которому эта ссылка привязана. Следующий пример демонстрирует ссылку в качестве дополнительного имени переменной.

int n = 0; int &r = n; /* теперь r -- ссылка на n или второе имя переменной n */ n = 10; cout '\n'; // выведет 10 r = 20; cout '\n'; // выведет 20 cout '\n'; // выведет 1, т.е. истина

Казалось бы, зачем нам второе имя переменной? Ответа может быть, по крайней мере, два.

  1. Что-то имеет слишком длинное, неудобное название. Привязав к нему ссылку, мы получим более удобное, короткое локальное название. При этом мы можем не указывать тип этого “чего-то”, можно использовать вместо типа ключевое слово auto :
auto &short_name = some_namespace::some_long_long_name;
  1. Выбор объекта привязки ссылки может происходить во время исполнения программы и зависеть от некоего условия. Пример:
int a = 0, b = 0; cin >> a >> b; int &max = a < b? b: a; // привязать к b, если a < b, иначе -- к amax = 42; cout "a = " << a "; b = " << b '\n';

Впрочем, основным применением ссылок является передача параметров в функции “по ссылке” и возвращение функциями ссылок на некие внешние объекты.

Передача по ссылке by reference напоминает передачу “по имени”. Таким образом, можно сказать, что, используя ссылки, мы передаём не значения, а сами переменные, содержащие эти значения. В реальности “за ширмой” происходит передача адресов этих переменных. Передача ссылки на переменную, время жизни которой заканчивается, например, возврат из функции ссылки на локальную переменную, приводит к неопределённому поведению.

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

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

int& max_byref(int &a, int &b) < return a < b? b: a; >int main() < int x = 0, y = 0; // собственно имена переменных не обязаны совпадать cin >> x >> y; max_byref(x, y) = 42; cout "x = " << x "; y = " << y '\n'; return 0; >

Так как при передаче ссылки реально копируется лишь адрес значения, а не само значение, то передав ссылку можно избежать копирования значения. Поэтому ссылки широко используются для передачи в функцию аргументов, которые или запрещено копировать или вычислительно дорого копировать. Типичный пример — объекты string. При копировании строки происходит выделение динамической памяти, копирование всех символов, затем — при удалении этой копии — освобождение памяти. Часто нет никакой необходимости в копировании. Например, следующей функции, считающей количество повторений заданного символа в строке нет нужды копировать строку — можно обойтись ссылкой:

size_t char_freq(const string &s, char c) < size_t freq = 0; for (size_t i = 0, sz = s.size(); i != sz; ++i) freq += s[i] == c; return freq; >

Обратите внимание на ключевое слово const . Данное ключевое слово позволяет нам указать, что мы хотим ссылку на константу, т.е. функция char_freq использует s как константу и не пытается её изменять, а ссылка нужна для того, чтобы избежать копирования. Рекомендуется использовать const везде, где достаточно константы. Компилятор проверит, действительно ли мы соблюдаем константность.

Ставить слово const можно перед именем типа и после имени типа, это эквивалентные записи.

int x; const int &r1 = x; // ссылка на x "только для чтения" int const &r2 = x; // тоже ссылка на x "только для чтения" int & const r3 = x; // ошибка компиляции, нельзя ставить const после &

Указатели

Общие сведения

Что такое указатель pointer уже рассказывалось во введении.

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

Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.

Указатели — простые переменные. Указатели не “делают вид”, что они — те значения в памяти, к которым они привязаны. Чтобы получить указатель на переменную, нужно явно взять её адрес с помощью оператора & . Чтобы обратиться к переменной, на которую указывает указатель, требуется явно разыменовать его с помощью оператора * .

int n = 0; int *r = &n; // теперь r -- указатель на n n = 10; cout '\n'; // выведет 10 *r = 20; cout '\n'; // выведет 20 cout '\n'; // выведет 1

Так же, как и в случае ссылок, можно использовать ключевое слово const , чтобы создать указатель на константу.

int x = 0, y = 1; const int *p1 = &x; // указатель на x "только для чтения" y = *p1; // можно *p1 = 10; // ошибка компиляции: нельзя изменить константу *p1 p1 = &y; // можно: сам указатель p1 не является константой int const *p2 = &x; // тоже указатель на x "только для чтения", всё аналогично p1 int * const p3 = &x; // теперь константа -- сам указатель y = *p3; // можно *p3 = 10; // тоже можно! p3 = &y; // ошибка компиляции: нельзя изменить константу p3 const int * const p4 = &x; /* комбо: теперь у нас константный указатель на x "только для чтения" */ y = *p4; // можно *p4 = 10; // ошибка компиляции: нельзя изменить константу *p4 p4 = &y; // ошибка компиляции: нельзя изменить константу p4

Указатели можно сравнивать друг с другом. Указатели равны, если указывают на один и тот же объект, и не равны в противном случае.

Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:

int* max_byptr(int *a, int *b) < return *a < *b? b: a; >int main() < int x = 0, y = 0; // собственно имена переменных не обязаны совпадать cin >> x >> y; *max_byref(&x, &y) = 42; cout "x = " << x "; y = " << y '\n'; return 0; >

Для обращения к полю структуры по указателю на объект структуры предусмотрен специальный оператор -> (“стрелка”).

struct Point < float x, y; >; Point a = < 20, 30 >; cout ' ' '\n'; // > 20 30 Point *p = &a; p->x = 42; (*p).y = 23; // то же самое, что p->y = 23; cout ' ' '\n'; // > 42 23

В отличие от ссылок, указатели не обязательно инициализировать. Указатели можно инициализировать специальным значением нулевой указатель nullptr , которое сигнализирует об отсутствии привязки указателя к чему-либо. Присваивание указателю другого адреса меняет его привязку. Это позволяет использовать указатели там, где семантика ссылок слишком сильно ограничивает наши возможности.

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

Наличие нулевого указателя позволяет, например, возвращать указатель на искомый объект и в том случае, когда ничего не было найдено. Просто в этой ситуации возвращаем нулевой указатель, а принимающая сторона должна быть готова к такому развитию событий. Указатель автоматически преобразуется к булевскому значению: нулевой указатель даёт false , прочие указатели дают true , поэтому, если p — указатель, то

if (p) . 

есть то же самое, что

if (p != nullptr) . 
if (!p) . 

есть то же самое, что

if (p == nullptr) . 

Например, поиск самого левого нуля в массиве чисел с плавающей точкой может быть записан так:

// Ищет нулевой элемент в диапазоне [from, to). // Возвращает нулевой указатель, если нуль не был найден. float* find_next_zero(float *from, float *to) < for (; from != to; ++from) if (*from == 0.f) return from; // нашли return nullptr; // ничего не нашли > int main() < float num[] < 1, 2, 3, 0, 3, 4 >; if (auto zero_pos = find_next_zero(num, num + sizeof(num)/sizeof(num[0]))) cout '\n'; else cout "zero not found\n"; // невозможно! return 0; >

Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе массивы и ссылки.

Бестиповый указатель

Вместо типа данных при объявлении указателя можно поставить ключевое слово void . Данное ключевое слово означает, что мы описываем указатель “на что угодно”, т. е. просто адрес в памяти. Любой указатель автоматически приводится к типу void* — бестиповому указателю typeless pointer . Прочие указатели, соответственно, называются типизированными или типизованными typed . Приведение от void* к типизованному указателю возможно с помощью оператора явного приведения типа.

В C бестиповые указатели широко применяются для оперирования кусками памяти или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).

#include #include // setw -- ширина поля вывода, hex -- вывод в 16-ричной системе #include using namespace std; // Ещё один способ получить битовое представление числа с плавающей точкой. int main() < unsigned char buffer[sizeof(float)]; // Настройка потока вывода. cout.fill('0'); // Заполнять нулями. cout.setf(ios::right); // Выравнивать по правому краю. for (float x; cin >> x; ) < // Скопировать побайтово память x в память buffer. memcpy(buffer, &x, sizeof(float)); // Вывести каждый байт buffer в 16-ричной форме. for (int byte: buffer) cout 2) ' '; cout '\n'; > >

О цикле for (int byte: buffer) см. здесь.

Указатель на указатель

Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.

int n = 4; int *p = &n; // уровень косвенности 1 *p = 5; cout // выведет 5 int **pp = &p; // уровень косвенности 2 **p = 6; cout // выведет 6 int ***ppp = &pp; // уровень косвенности 3 ***p = 7; cout // выведет 7

“Система ранжирования C-программистов.

Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.

Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.

Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент."

Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.

В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:

DWORD WINAPI GetFullPathName( _In_ LPCTSTR lpFileName, _In_ DWORD nBufferLength, _Out_ LPTSTR lpBuffer, _Out_ LPTSTR *lpFilePart );

Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.

В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.

Неограниченный уровень косвенности

Несмотря на ограниченность применения уровня косвенности выше двух, довольно часто встречается то, что можно назвать неограниченным уровнем косвенности или рекурсивным типом данных. Типичный (и простейший) пример — структура данных, называемая “связанный список” linked list .

Следующий пример демонстрирует использование связанного списка для чтения последовательности строк и вывода этой последовательности в обратном порядке:

struct Line < Line *prev; string line; >; int main() < Line *last = nullptr; // Чтение строк. for (string line; getline(cin, line);) < Line *new_line = new Line; new_line->prev = last; new_line->line = line; last = new_line; > // Вывод строк в обратном порядке. while (last) < cout line '\n'; Line *old_line = last; last = last->prev; delete old_line; > return EXIT_SUCCESS; >

Упражнение. Попробуйте изменить этот пример так, чтобы введённые строки выводились в том же порядке, в котором были введены.

Указатели на функции

Язык C позволяет определять указатели на функции (в указателе хранится адрес точки входа в функцию) и вызывать функции по указателю. Таким образом, можно во время исполнения программы выбирать какая именно функция будет вызвана в конкретной точке, выбирая значение указателя. Язык C++ позволяет создавать также и ссылки на функции, но ввиду того, что ссылка после инициализации не может быть изменена, область применения ссылок на функции весьма узка.

Функцией высшего порядка higher order function называют функцию, принимающую в качестве параметров другие функции. Функции высшего порядка — одно из базовых понятий функционального программирования. Единственная форма функций высшего порядка в C — функции, принимающие указатели на функции. Язык C++ расширяет круг доступных форм функций высшего порядка, но в примерах ниже мы ограничимся возможностями C.

Простой пример использования указателя на функцию — функция, решающая уравнение вида f(x) = 0, где f(x) — произвольная функция. Конкретные функции f можно передавать по указателю. Приведение функций к указателю на функцию и наоборот производится неявно автоматически, поэтому при присваивании указателю адреса конкретной функции можно не использовать оператор взятия адреса & , а при вызове функции по указателю — не использовать оператор разыменования * (поведение, аналогичное поведению с массивами).

/// Тип "правая часть уравнения" -- функция одного действительного параметра. typedef double (*Unary_real_function)(double); /// Точность приближённого решения, используемая по умолчанию. const double Tolerance = 1e-8; /// Алгоритм численного решения уравнения f(x) = 0 на отрезке [a, b] делением отрезка пополам. /// Данный алгоритм является вариантом двоичного поиска. double nsolve(Unary_real_function f, double a, double b, double tol = Tolerance) < using namespace std; assert(f != nullptr); assert(a < b); assert(0. for (auto fa = f(a), fb = f(b);;) < // Проверим значения функции на концах отрезка. if (fa == 0.) return a; if (fb == 0.) return b; // Делим отрезок пополам. const auto mid = 0.5 * (a + b); // середина отрезка if (mid return abs(fa) < abs(fb)? a: b; if (b - a return mid; // Выберем одну из половин в качестве уточнённого отрезка. const auto fmid = f(mid); if (signbit(fa) != signbit(fmid)) < // Корень на левой половине. b = mid; fb = fmid; > else < assert(signbit(fb) != signbit(fmid)); // Корень на правой половине. a = mid; fa = fmid; > > >

Довольно типичной областью применения указателей на функции является связывание источников (регистраторов) некоторых событий, обычно определяемых в составе некоторой библиотеки, и обработчиков событий, предоставляемых пользователем этой библиотеки. Обработчики событий (функции) вызываются автоматически по переданным указателям. Такие функции также называются функциями обратного вызова callback functions или колбеками callbacks . Например, при щелчке мышью по элементу графического интерфейса вызывается функция-обработчик этого события, “зарегистрированная”, путём передачи её адреса библиотеке графического интерфейса.

В качестве простого примера применения функции обратного вызова рассмотрим функцию, занимающуюся поиском набора корней уравнения f(x) = 0 на заданном отрезке. Сама функция будет работать по достаточно простому алгоритму (который, естественно, не гарантирует, что будут найдены все или даже какие-то из существующих на отрезке корней): предполагаем, что есть некая функция, способная найти один корень на отрезке, если он там есть (например, функция nsolve из примера выше). Теперь берём исходный отрезок поиска [a, b] и некоторое значение “шага” step и проходим по этому отрезку с этим шагом, проверяя участки [a + i step, min(b, a + (i + 1)step], i = 0, … пока не пересечём правую границу отрезка. На каждом участке проверяем, являются ли его границы корнями, и есть ли на нём корень (принимает ли функция f разнознаковые значения на границах). В последнем случае используем “решатель” вроде nsolve (переданный по указателю), чтобы найти корень. Каждый найденный корень — это событие, вызываем для него “обработчик” — функцию обратного вызова по указателю report.

/// Тип "решатель уравнения на отрезке" -- функция вроде nsolve, определённой выше. typedef double (*Equation_solver)(Unary_real_function, double a, double b, double tol); /// Тип функции, вызываемой для каждого корня. /// Процесс поиска останавливается, если эта функция возвращает ложь. typedef bool (*Root_reporter)(double); /// Применяет заданный алгоритм поиска корня на отрезке, /// разбивая заданный отрезок [a, b] на отрезки одинаковой длины step (кроме, возможно, последнего). /// Для каждого найденного корня вызывает функцию report (callback-функция). /// Возвращает правую границу пройденного участка (идёт слева направо по заданному отрезку). double repeated_nsolve ( Unary_real_function f, double a, double b, double step, // шаг на отрезке Root_reporter report, double x_tol = TOLERANCE, // чувствительность по аргументу double f_tol = TOLERANCE, // чувствительность по значению функции Equation_solver solver = nsolve ) < assert(x_tol >= 0. && f_tol >= 0.); assert(a 0.); assert(f && report && solver); using namespace std; double left = a, f_left = f(left); bool f_left_zero = abs(f_left) // Корень на левой границе исходного отрезка? if (f_left_zero && !report(left)) return left; while (left != b) < // Правая граница очередного участка. const double right = fmin(b, left + step), f_right = f(right); const bool f_right_zero = abs(f_right) // Корень на правой границе участка? if (f_right_zero && !report(right)) return right; // Есть корень внутри участка? if (!(f_left_zero || f_right_zero) && signbit(f_left) != signbit(f_right)) < const double root = solver(f, left, right, x_tol); if (!report(root)) return root; > // Передвинуть левую границу. left = right; f_left = f_right; f_left_zero = f_right_zero; > return b; >

Следующий пример демонстрирует “двухзвёздное программирование” и использование указателя на функцию для определения порядка сортировки массива строк с помощью стандартной функции qsort .

#include // qsort #include // strcmp #include using namespace std; // Функция сравнения строк. int line_compare(const void *left, const void *right) < // Обращаем словарный порядок, поменяв местами left и right. return strcmp(*(const char**)right, *(const char**)left); > int main() < const char *lines[] < "may the force be with you", "this is it", "so be it", "it is a good day to die", "through the time and space", "the light shines in the darkness" >; // Сортировать: массив, количество элементов qsort(lines, sizeof(lines) / sizeof(lines[0]), // размер элемента, функция сравнения. sizeof(lines[0]), line_compare); // Распечатаем результат сортировки. for (auto line : lines) cout '\n'; return EXIT_SUCCESS; >

Функция qsort является частью Стандартной библиотеки C. Стандартная библиотека C++ предлагает более удобную и эффективную функцию sort (определённую в заголовочном файле ), однако её рассмотрение выходит за пределы темы данного раздела.

Следующий пример является развитием примера со списком из предыдущего подраздела и использует бестиповые указатели, указатели на указатели и указатели на функции для управления “обобщённым” связанным списком в стиле C. Звенья такого списка могут содержать произвольные данные. Основное требование к звеньям списка — наличие в начале звена указателя на следующее звено, фактически каждый предыдущий указатель указывает на следующий.

/// Возвращает ссылку на указатель на следующее звено звена link. void*& next(void *link) < return *(void**)link; > /// Вставляет link перед head и возвращает link (теперь это -- новая голова списка). void* insert_head(void *head, void *link) < next(link) = head; return link; > /// Вычисляет длину списка. size_t size(void *head) < size_t sz = 0; for (; head; head = next(head)) ++sz; return sz; > /// Указатель на функцию, выполняющую удаление звена. using Link_delete = void(*)(void*); /// Удаляет список, используя пользовательскую функцию удаления. void delete_list(void *head, Link_delete link_delete) < while (head) < auto next_head = next(head); link_delete(head); head = next_head; > >

Теперь сама программа, выводящая строки в обратном порядке, упрощается:

/// Звено списка -- одна строка. struct Line < void *prev; string line; >; /// Вывести строку и удалить объект Line. void print_and_delete(void *ptr) < auto line = (Line*)ptr; cout line '\n'; delete line; > int main() < Line *head = nullptr; // Чтение строк. for (string line; getline(cin, line);) < Line *new_line = new Line; new_line->line = line; head = (Line*)insert_head(head, new_line); > // Вывод количества строк -- элементов списка. cout "\nLines: " << size(head) "\n\n"; // Вывод строк в обратном порядке. delete_list(head, print_and_delete); cin.clear(); cin.ignore(); return EXIT_SUCCESS; >

Впрочем, необходимо отметить, что сочетая такие приёмы со средствами C++, выходящими за пределы “чистого” C, вы рискуете нарваться на неопределённое поведение. Низкоуровневые средства требуют особой внимательности, так как компилятор в таких случаях не страхует программиста. В частности, в общем случае нельзя интерпретировать произвольный указатель как void* и наоборот без выполнения приведения типа. А это может произойти неявно, например, в примере выше мы полагаем, что указатель prev, указывающий на объект структуры Line совпадает с указателем на поле prev этого объекта.

Синтаксическая справка

Правило чтения сложных описаний типов

Конструкции, определяющие переменные или вводящие новые типы в языках C и C++, могут порой иметь довольно запутанный вид. Ниже дано правило, помогающее разобраться в смысле сложных конструкций.

  1. Начиная с имени (в случае typedef , в случае using имя находится вне — см. ниже), читать вправо, пока это возможно (до закрывающей круглой скобки или точки с запятой).
  2. Пока невозможно читать вправо, читать влево (убирая скобки).

Некоторые примеры “расшифровки” типов переменных:

// c (влево) константа char (const и char можно поменять местами) const char c; // str (влево) указатель на (влево) константу char (или константный массив из char) const char* str; // str (влево) константный (влево) указатель на константу char const char* const str; // n (вправо) массив (вправо) из 10 (влево) int int n[10]; // n (вправо) массив (вправо) из 10 (влево) указателей на (влево) int int* n[10]; // n (влево) указатель на (вправо) массив из 10 (влево) указателей на int int* (*n)[10]; // n указатель на массив из 10 (влево) указателей на (вправо) функции, не принимающие аргументов, // (влево) возвращающие указатели (влево) на константы типа int const int* (*(*n)[10])();

Разница между typedef и using

Директива typedef объявляет синоним типа. Используется синтаксис определения переменной, к которой добавили ключевое слово typedef , только вместо собственно переменной вводится синоним типа этой как-бы переменной с её именем.

int * p; // переменная: указатель на int typedef int * pt; // имя pt -- синоним типа "указатель на int" pt px; // тоже переменная типа "указатель на int"

В С++11 появилась возможность объявлять синонимы типов с помощью using-директивы в стиле инициализации переменных:

using pointer = type*;

Объявление typedef можно превратить в using-директиву, заменив typedef на using , вставив после using имя типа и знак равно и убрав это имя типа из объявления справа.

// то же, что typedef double (*Binary_op)(double, double); using Binary_op = double (*)(double, double);

Типы, ассоциируемые с массивами

Пусть N — константа времени компиляции и дано определение

float a[N];
  • float — тип элемента;
  • float& — ссылка на элемент, тип результата операции обращения по индексу, например a[0] ;
  • float* — указатель на элемент, например &a[0] ; a и &a автоматически неявно приводятся к float* ;
  • float[N] — формальный тип переменной a ;
  • float(*)[N] — формальный тип указателя на массив a , результат операции взятия адреса &a ;
  • float(&)[N] — тип ссылки на массив a ; a автоматически неявно приводятся к этому типу; так же как сам массив, ссылка на него автоматически приводится к указателю на массив и на его первый элемент.

Типы, ассоциируемые с функциями

Пусть дано объявление

float foo(int, int);
  • float — тип результата, получаемый при вызове функции, например foo(1, 2) ;
  • float(int, int) — формальный тип символа foo — foo не является переменной, так как переменные функционального типа невозможны, и тем не менее, имеет тип;
  • float(*)(int, int) — указатель на функцию, результат &foo ; foo автоматически неявно приводится к этому указателю;
  • float(&)(int, int) — ссылка на функцию; foo автоматически неявно приводится к этому типу; так же как сама функция, ссылка на неё автоматически приводится к указателю на неё же.

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

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