Наиболее трудным для понимания в 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", что не соответствует истине).
Таким образом, если подходить к изучению вышеуказанных операторов исходя из правильного стиля синтаксиса, и различая разные по значению но похожие по написанию операторы, то их понимание придёт гораздо быстрее.
P.S.:
Через несколько дней после написания этого поста я наткнулся на сайт Алексея Курзенкова, профессионального программиста (кстати, он тоже 1970 года рождения), и его заметку ещё двухлетней давности "Где поставить звёздочку?":
http://www.sofmos.com/lyosha/Articles/CNotes_Asterisk.html. С большим интересом я прочёл его размышления на эту тему, которые, к моей радости полностью совпали с моими! Кроме того, в заметке объясняется даже историческая подоплёка текущего положения дел с синтаксисом указателей, а именно то, что конструкция множественного объявления указателей досталась языку C++ в наследство от C, и в C++ не рекомендуется (указатели следует инициализировать сразу после их объявления, чтобы в дальнейшем исключить возникновение трудновылавливаемых ошибок), что только подтверждает мою точку зрения.
Таким образом, не только я, оказывается, озаботился этим вопросом! :)
P.P.S: Если я ещё кого-то не убедил, предлагаю к ознакомлению книгу от
создателя языка 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. Многое станет понятно.