Куда лепить знак указателя в C++?
Новиков М.Г.
29.10.2009
Содержание
В процессе изучения программирования в Линуксе, мне пришлось вплотную заняться языком C++. И тут практически сразу же передо мной снова возник вопрос синтаксиса знака указателя (*). В многочисленных примерах в различных книгах по языку его лепят то к типу, то к имени переменной, то вообще ставят посередине через пробелы. Такое положение дел меня не устраивало, и я решил чётко определиться с этим вопросом. В итоге родилась статья, которую я привожу ниже.
Тип «указатель на…»
Наиболее трудным для понимания в C++ оказываются указатели, создание ссылок и взятие адреса. Это происходит из-за того, что оператор указателя (*) как части типа и создания ссылки (&) употребляется разными программистами и в разных случаях по разному (то вплотную к имени типа, то вплотную к имени переменной, то вообще с пробелами с обеих сторон — и это синтаксически допустимо); оператор взятия адреса (&) пытаются логически связать с оператором создания ссылки (&); указатель как часть типа смешивают с указателем как командой доступа к значению, на которое указывает указатель (разыменовывание); поэтому всё это не укладывается в голове в общую систему. Однако, если чётко определить стиль использования этих операторов, а также различать разные, но похожие по написанию операторы, в зависимости от места их употребления, то всё встаёт на свои места.
Итак, оператор указателя в объявлениях следует писать вплотную к типам, потому что он является частью этого типа:
int* a; //Это переменная «а» типа «указатель на int»
int* func(int*, float*); //Объявление функции с параметрами //типа «указатель на int» и «указатель на float», //которая возвращает значение типа «указатель на int».
Тут следует заметить, что компилятор, видимо, рассматривает строку «int* a;» не как переменную «a» типа «указатель на int», а как переменную указателя «a» на тип «int», и с этой точки зрения следовало бы писать «int *a». На это указывает результат одновременного объявления двух переменных:
int* a, b; //Это переменная «а» типа «указатель на int»
//и переменная «b» типа «int», //а не две переменные типа «указатель на int», как казалось бы.
Но тогда становятся непонятными типы параметров в объявлении функций, где символ указателя используется без имени переменной. Рождаются какие-то новые сущности, не вписывающиеся в логику типов и только запутывающие программистов. Кроме того, при таком объявлении переменной (int *a), вместо образа некоего типа и образа обычной переменной в голове приходится представлять образ типа и образ указателя на переменную этого типа, что, согласитесь, сложнее. Вдобавок, инициализация указателя «int *a = 0» выглядит, как присвоение нуля значению, на которое указывает указатель, а не самому указателю, тогда как «int* a = 0» отражает фактическое положение дел, а именно присваивание ноля самой переменной «a» (инициализация указателя нулём).
Поэтому я предлагаю не засорять голову, а просто запомнить в виде исключения эту особенность множественного объявления переменных типа «указатель на…», которые следует объявлять так:
int *a, *b; //Это переменные указателей «а» и «b» на тип «int»
Обращение же к значению, на которое указывает указатель, берётся так:
*a; //Значение, на которое указывает указатель «а»
Здесь символ «*» является не частью типа, а командой обращения по адресу, содержащемуся в переменной «a». Поэтому надо различать указатель как часть типа, и указатель, как команду разыменовывания — это две разные вещи.
Тип «ссылка на…»
Оператор создания ссылки также пишется вплотную к типу, поскольку он является как-бы частью типа «ссылка на…»:
int& b = a; //Создание ссылки на переменную «a».
//Объявляется «b» типа «ссылка на int», //которая становится псевдонимом переменной «a».
int*& b = a; //Создание ссылки на указатель «a».
//Объявляется «b» типа «ссылка на указатель на int», //которая становится псевдонимом указателя «a».
func(int&, float&); //Объявление функции с параметрами
//типа «ссылка на int» и «ссылка на float».
Оператор взятия адреса
Существует ещё оператор взятия адреса, который выглядет также (&), но выполняет совсем другую функцию, а именно, функцию, обратную указателю:
&a; //Адрес, по которому располагается значение переменной «a»
Оператор ссылки и оператор взятия адреса хотя и пишутся одинаково, но фактически это разные операторы. Оператор ссылки (int& b = a) всегда является частью типа, тогда как оператор взятия адреса (&a) всегда используется вплотную к переменной, и просто возвращает адрес, по которому её значение расположено в памяти. Несмотря на внешнюю схожесть эти операторы нельзя смешивать логически, и пытаться понять сущность оператора ссылки исходя из знаний об операторе взятия адреса (иначе логичнее было бы создавать ссылку так: «int& b = &a», что не соответствует истине).
Таким образом, если подходить к изучению вышеуказанных операторов исходя из правильного стиля синтаксиса, и различая разные по значению но похожие по написанию операторы, то их понимание придёт гораздо быстрее.
Послесловие
Через несколько дней после написания этой статьи я наткнулся на сайт Алексея Курзенкова, профессионального программиста (кстати, он тоже 1970 года рождения), и его заметку ещё двухлетней давности «Где поставить звёздочку?»:
http://www.kurzenkov.com/Articles/CNotes_Asterisk.html
С большим интересом я прочёл его размышления на эту тему, которые, к моей радости полностью совпали с моими! Кроме того, в заметке объясняется даже историческая подоплёка текущего положения дел с синтаксисом указателей, а именно то, что конструкция множественного объявления указателей досталась языку C++ в наследство от C, и в C++ не рекомендуется (указатели следует инициализировать сразу после их объявления, чтобы в дальнейшем исключить возникновение трудновылавливаемых ошибок), что только подтверждает мою точку зрения.
Если я ещё кого-то не убедил своей статьёй, предлагаю к ознакомлению книгу от создателя языка C++ Бьерна Страуструпа «Язык программирования C++»:
http://www.proklondike.com/books/cpp/straustrup_cpp.html
Думаю, более авторитетного автора по данной теме просто не найти! :))) Скачайте её (она в формате pdf) и откройте на странице 53. Глава 2.3.5 Указатели. Думаю, вы будете удивлены.
Кроме того, для общего развития и понимания причин существования в C++ вышеописонных мной исключений вроде множественного объявления указателей и т.п. можно почитать книгу того же автора «Дизайн и эволюция C++», где он описывает процесс создания языка C++:
http://www.proklondike.com/books/cpp/cpp_cpp_straustrup_desing_evolution_cpp.html.