Подписка на блог

РСС — лучше всего

Ещё есть автоматические трансляции в Тумблере и Же-же. Если что-то не работает, напишите мне: ilyabirman@ilyabirman.ru.

Почему указатели трудны и что с этим делать

Указатели сложны не потому, что это объективно что-то сложное, а потому, что авторы языка Си — козлы, которые позаботились об экономии числа нажатий при печатании, а о том, удобство чтения кода куда важнее, чем удобство его написания, они не подумали. Чего только стоят все эти название функций типа strcspn () и feof ().

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

int n[10]; // здесь n хранит адрес, но нет ни звёздочки, ни амперсанда
n[5] = 77; // незаметно поиграли в указатели
*(n + 5) = 77; // здесь звёдочка означает «значение по адресу» (уже заметно)

char *s; // здесь звёздочка уже означает «переменная хранит не значение, а адрес»

unsigned int m = 2131;
*((char *) &m + 1) = ’A’; // теперь, если у меня правильно взорвался мозг, m == 2113

Если бы вместо звёздочки и амперсанда использовались конструкции addressof () и valueat (), для объявления типов был бы модификатор address, а для кастинга использовался бы оператор as указатели бы понимало в 10 раз больше человек. Назовём такой язык Ди (хоть такой уже и есть).

Квадратные скобки в выражениях в Ди пусть означают «значение по адресу с указанным сдвигом», тогда есть для любого x будет справедливо:

x == x[0] == valueat (addressof (x) + 0).

Наличие квадратных скобок в объявлении переменной пусть само по себе не превращает переменную в указатель, то есть, если мы захотим указатель, нам придётся дописать слово address. Тогда запишем первые три строчки нашего кода на Ди:

int address n[10];
valueat (n)[5] = 77; // пока получается некрасиво
valueat (n + 5) = 77; // а тут — нормально

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

int n[10]; // так само n будет хранить значение нулевого инта (n == n[0], напомню)
n[5] = 77; // значение со сдвигом — как раз то, что нам нужно
valueat (addressof (n) + 5) = 77; // длинная запись того же самого

Теперь простая вещь выглядит просто, а игры с указателями выглядят как игры с указателями, но вдобавок не теряют понятности. Синонимичность последних двух строк тоже очевидна. Это прекрасно. А вот другие наши сишные строчки в переводе на Ди:

char address s;

unsigned int m = 2131;
valueat ((addressof (m) as char address) + 1) = ’A’

Теперь, если мы что-нибудь перепутаем, это сразу будет видно:

valueat ((m as char address) + 1) = ’A’ // пытаемся представить значение в качестве адреса

valueat (m) = ’A’ // пытаемся взять значение по адресу m, в то время как m не является адресом

Такой язык не требует ни больших вычислительных ресурсов, чем Си, ни каких-либо ещё достижений современности, зато читать его легче. Чтобы его придумать, нужно было просто отнестись к задаче чуть внимательнее, чем к ней отнёсся тот, кто нажимал Шифт+цифры в поисках ещё незадействованных символов.

Не исключено, что я где-нибудь наошибался, потому что я вхожу в число тех людей, у кого с указателями дружба складывается весьма посредственно. Тогда подскажите, пожалуйста.
Подписаться на блог
Поделиться
Отправить
56 комментариев
Роман Добровенский
Во-первых, тебе _не нужно_ знать о том, что int n[10] — это указатель. Это массив. Например, объявление функций
void f(int n[10]);
это не тоже самое, что и 
void f(int n*);

Так же массив нельзя вернуть из функции, а указатель можно. То есть
int[10] f(); — запись недопустимая, в отличие от
int* f();

Во-вторых, голых указателей вообще следует избегать, пользуясь auto_ptr с typedef-ами:

typedef std::auto_ptr<int> int_ptr;
int_ptr p = new int;

В-третьих, си-касты типа (char*) — так же оставлены только в целях совместимости, и пользоваться ими не следует (то есть вообще за них надо отрывать руки).

int *p = static_cast<int*>(q);

В-четвертых, следует избегать указателей там, где можно использовать ссылку (90% случаев). В том числе это здорово упрощает запись.

В-пятых, «m as char address» резко сокращает гибкость языка, так как такую конструкцию уже не запихнешь, например, в темплейт, а это автоматически снижает ценность языка процентов на 50.

Вообще 99% случаев критики C++ — это следствие того, что люди имеют лишь отдаленное о нем представление на уровне «С++ за 10 дней».
Илья Бирман
Во-первых, у тебя в typedef’е синтаксическая ошибка на символе „<“ (мы тут говорим не про Си-плюс-плюс). А во-вторых, если за 10 дней о некоем языке можно получить лишь «отдалённое представление», то по-моему ему уже и никакой критики не надо, просто сразу ясно, что это говно собачье.

int *p = static_cast<int*>(q); // никто не знает, что написано в этой строчке и как это работает
Sergey Azarkevich
Понимали бы в 10 раз больше, а пользовались в 10 раз меньше.
В результате был бы популярен другой, более лаконичный, язык. И все бы жаловались уже на него.
Илья Бирман
Я думаю, даже самые старые среды разработки на 80286 компьютерах могли себе позволить вставку слов valueat и addressof по горячим клавишам (например, Шифт+8 и Шифт+7 :-)
Дмитрий Желнин
примерно те же мысли возникают у меня при работе с командами bash в линуксе — там создатели сократили почти все команды до 2-3 символов
вконец добивают слова, завершающие блок: if — fi, case — esac
Ярослав
If—fi сделали просто потому, что поленились сделать нормально.

Сергей, самый лаконичный язык — это Brainfuck :-)
bes island
Сложны, трудны… Указатели не нужны. В прикладном программировании. Почти никогда.
Илья Бирман
Конечно, но понимать их нужно просто для того, чтобы мозги работали. Даже если на ПХП пишешь.
Юрий Хан
Роман Добровенский:
> int[10] f(); — запись недопустимая, в отличие от

Если бы в C можно было вернуть массив из функции, согласно правилу «направо—налево», это бы выглядело так:

int f()[10];

А указатели не нужны. Рулят контейнеры и итераторы.
Роман Добровенский
Да, если говорить о Си, то это безусловно хлам. Так а зачем вообще о нем говорить? Его сечас используют только старперы, которые не желают учить ничего нового (ну и программисты под специализированное железо, да, тоже, но это отдельная история).

Насчет выучить язык за 10 дней — насколько я помню ты тут недавно рассуждал о профессинализме. То есть в дизайне быть профессионалом необходимо, и если кто-то где-то допустил малейшую ошибку, то его надо расстрелять. Почему ты считаешь, что язык программирования можно выучить за десять дней? Почему такая ассиметричность?
Илья Бирман
Ты с лёгкостью меняешь местами понятия «выучить» и «получить отделённое представление».
Денис
Мне кажется, что этот язык называется сейчас Дельфи. В нём указатели и работа с ними — сказка.
> Если бы вместо звёздочки и амперсанда использовались конструкции addressof () и valueat (), для объявления
> типов был бы модификатор address, а для кастинга использовался бы оператор as указатели бы понимало в 
> 10 раз больше человек. Назовём такой язык Ди (хоть такой уже и есть).
Владимир
>>«авторы языка Си — козлы, которые позаботились об экономии числа нажатий при печатании, >> а о том, удобство чтения кода куда важнее, чем удобство его написания, они не подумали»
Аминь! 
Иван Коростелёв
Объявление «int n[10];» в первых версиях C для простоты означали выделение _11_ машинных слов. 10 безымянных, которые содержат цыелые и 11-го, которое содержит адрес первого элемента массива и имеет имя n. Указатели тогда объявлялись так: «int[] n» (массив без элементов). Так было проще реализовать массивы (достаточно было уметь работать с указателями, а массивы получались, как небольшой хак). Отсюда и пошло то, что массив может быть неявно преобразован к указателю на первый элемент.

Затем такая семантика массивов привела к проблемам — сложно реализовать структуры содержащие массивы, в которых всегда неявно инициализируется одно поле (адресом начала массива в той же структуре), к тому же так их размер получался на одно машинное слово больше, чем кажется. Поэтому массивы были введены в язык по нормальному и понятия массива и указателя были разъединены.

Длина идентификаторов в символьной таблице первых компиляторов C была то ли 7, то ли 8 символов, что накладывало ограничения на имена функций.

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

P.S. «n == n[0]» это бред, который помимо отсутствия любого вменяемого физического смысла может так же приводить к идиотским ошибкам. (Что если я забуду указать один из индексов в цикле?)
Иван Коростелёв
Мы не ожидаем, что человек изучающий высшую математику сразу (или даже всего за несколько дней) научится правильно записывать и читать частные производные высших порядков (не говоря уж о том, что начать их понимать). Почему-то в математике, не используют слова для записи формул, а используют сложные не использующиеся в обычном письме значки. Любая нотация, пока к ней не привыкнешь является сложной и неопонятной. То же самое касается указателей. Укзатели в C одна из основных и наиболее часто использующихся концепций, поэтому для её записи надо использовать достаточно выразительную _и краткую_ нотацию.

P.S. http://nsl.com/ — это не про Си но в тему. Языки программирования с очень сложной нотацией, которые тем не менее реально используются во многих областях. Многие программы занимающие на обычных языках целую страницу на них можно записать в строчку. И многим это нравится. Ссылка на тему: http://dr-klm.livejournal.com/42312.html
Илья Бирман
В качестве краткой записи значения по адресу можно использовать знак @, который означает «at». Адрес можно сократить до addr (скобки сами по себе часто читают как «of»). Тогда:

int n[10];
n[5] = 77;
@ (addr (n) + 5) = 77;

char addr s;

unsigned int m = 2131;
@ ((addr (m) as char addr) + 1) = ’A’

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

Создатели ПХП, кстати, еще хуже себе представляли, что такое указатели (сравните работу оператора «=» для объектов в 4 и 5 версии), так что не комплексуйте, Илья! :)
Илья Бирман
На мой взгляд, опечатка в названии strcspn () куда более вероятна, чем, допустим, в названии stringWithFormat: или mysql_connect_db ().
Степан Столяров
unsigned int m = 2131;
*((char *) &m + 1) = ’A’;

Результат тут будет зависеть от того, на каком компьютере вы его запускаете. Little-endian, big-endian, каков размер int в байтах, все дела. На то он и низкоуровневый язык.

На интеле вроде получается m = 16723, я на калькуляторе посчитал.
Илья Бирман
Я исхожу из того, что чем левее бит, тем он старше (даже если если он оказывается в другом байте). Вроде бы это называется big-endian? Размер int в байтах я взял за 2 (от балды :-) А чтобы16723 получилось вам нужно 65 (’A’) записать в байт, который умножается на 256, т. е. во второй справа, и при этом его адрес должен быть на один байт правее начала m... стало быть, в ваших расчётах int имеет длину 3 байта. Разве так бывает? Или я где-то ошибся?
Антон Вернигор
Мне кажется, что в той области, для которой создавался язык C (системное программирование, ЯП «среднего уровня»), такой синтаксис вполне допустим. Он краток и понятен тем, кто этим занимается.
А для прикладного программирования просто стоило бы использовать другой язык. Тот же паскаль выглядит в этом плане более привлекательно, хоть тоже и не лишен недостатков. Но, конечно, это не должен быть паскаль, а просто другой язык, с синтаксисом, более понятным человеку, чем компилятору.
Степан Столяров
А, да, еще результат зависит от того, какая у вас кодировка — ASCII или EBCDIC. То есть, чему равно ’A’.
Илья Бирман
Я исхожу из того, что ’A’ — это 65 (константа такая, типа :-)
Роман Добровенский
Илья, я не в коем случае не путаю «выучить» и «получить представление». Я просто полагаю, что если человек берется что-то критиковать, то он не просто «имеет представление», а хотя бы знает о совеременном положении вещей в критикуемой области.

Твоя критика — это то же самое что прочитать кригу «HTML for Dummies», а потом ругать HTML за то, что оформление не отделено от содержания, а CSS имеет лишь примитивнейшие свойства типа color и font-size. CSS есть за что критиковать, но мы с тобой понимаем, что недостатки CSS кроются совершенно в другом, и они не являются следствием тупорылости W3C — на то есть другие причины, которые ненасильственными мерами просто не поборешь. Ты же рассуждаешь о C++ именно на уровне «представление за 10 дней».

И заметь, что о современном веб-программирование со всеми AJAX-ами тоже за 10 дней нормального представления тоже не получишь. Это не значит, что JavaScript говно.
Степан Столяров
В 40 килобайт памяти такие длинные имена функции не влезают. 1972-й год на дворе.

#define stringWithFormat strcspn
Илья Бирман
А с дифайном уже влезают?.. (Я плохо понимаю, как работает компилятор, но если такой простой способ научить его нормальным словам, то почему он не использовался с первого дня?)
Иван Коростелёв
Потому что препроцессор Си появился намного позже компилятора.
Степан Столяров
2131 = 0x0853;
’A’ = 65 = 0x41;

Предположим, что int действительно два байта В зависимости от архитектуры исходное значение 2131 будет идти в памяти как байты 08 53 (у байта 08 адрес младше), либо как 53 08 (у байта 53 адрес младше). При этом адрес m всего числа совпадает с адресом того байта, который «левее» :). Соответственно, меняя байт m + 1 на 0x41, мы получим либо 0x0841 = 2113, либо 0x4153 = 16723. Я могу ошибаться, но мне помнится, что в интеле у байт, представляющих старшие разряды числа, адрес старше. То есть получим мы 16723.

Кстати, очень интересный вопрос, почему интеловцы решили записывать числа в память «задом наперед». И ведь они правы.
Илья Бирман
Да, я считал в прямом порядке :-)
Степан Столяров
’A’ в ASCII равно 65. ’A’ в EBCDIC = 193, если верить википедии. Непросто жилось древним программистам. :)
Антон
С# (Java, VisualBasic, ObjectiveC... что подвернётся). Или, если уж припёрло C++ — то все указатели в контейнеры, чтобы «*» наружу и не торчала.
А С/C++ остаётся для тех задач, когда надо «очень быстро, на четвереньках и задом наперёд» (из старого анекдота). Для встраиваемых систем, низкоуровневых библиотек, слабого железа, и некторых идиотских мобилок, на которых нет ни Java, ни ObjectiveC))) Т. е. оказывается в той сравнительно узкой нише, где раньше был ассемблер.
З.Ы. а упомянутый «strcspn» — это уже не просто С, это POSIX (http://www.space.unibe.ch/comp_doc/c_manual/C/FUNCTIONS/funcref.htm). Уже успели даже стандарт принять (ISO/IEC 9945). 
Anatoliy Knyazev
char* s; же! Как можно писать иначе? :)
Илья Бирман
Кстати, да, вроде такая запись имеет больше смысла. Если читать * как value at, то char value at (s) явно предпочтительнее, чем char (value at s). Спасибо :-)
Степан Столяров
Ну чтобы уже закрыть тему про злоключения числа 2131, расскажу про логику интеловских инженеров.

Арабы пишут справа налево, европейцы пишут слева направо. Числа при этом и те, и другие записывают одинаково. При заимствовании арабских цифр эта особенность не была учтена. Мы начинаем запись числа с его старшего разряда, арабы — с младшего. С первого взгляда кажется логичным расположить нумеровать байты памяти слева направо («нулевой» байт слева, «первый» правее, и т. д.) и расположить число 2131 = 0x0853 в памяти как 08 53. Так же наглядно и естественно, правда?

А теперь посмотрим на число в двоичной записи. Младший бит, указывающий множитель при слагаемом двойки в степени 0, располагается справа. Множитель двойки в степени 1 находится слева от него. И так далее. Выходит, биты нашего числа в выбраной нами записи на распечатке будут выглядеть перемешанными и идти в порядке:

7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8

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

И да, если мы называем старшие адреса памяти «верхними», то, вероятно, при распечатке больших диапазонов памяти старшие адреса должны быть вверху, что мы и видим в интеловской документации. Вот блок памяти 64 КБ строками по 256 байт, заполненный двухбайтовыми числами-указателями на самих себя:

Старшие адреса
FFFF ... FF00
FEFF ... FE00
...
00FF ... 0000
Младшие адреса

А вот диаграммы, изображающие формат сетевых протоколов и файлов, устроены так, что их читают справа налево, сверху вниз. Это потому что по сети и из файла байты приходят по порядку, а не все сразу, и писать код заполнения шаблона заголовка пакета удобнее по картинке, в которой порядок байтов соответствует привычному способу чтения.
Степан Столяров
Anatoliy Knyazev, прочитайте этот код и скажите, сколько здесь переменных типа «указатель на char»:

char* x, y;

Правильный ответ: один.
Илья Бирман
Чёрт, что за долбанутый язык... :-)
Аза
выдеру из комментариев:
int n[10];
n[5] = 77;
@ (addr (n) + 5) = 77;
не совсем понял зачем заменять «*» на «@». Ну то есть совсем не понял. Если вам очень хочется, это может сделать препроцессор.
Илья Бирман
Так @ понятнее, чем *, потому, что @ — это at. At address of n + 5 assign 77 — это почти предложение на английском языке, в котором делается как раз то, что написано. А звёздочка вообще ничего не значит.
Егор
Странно, как раз с указателями мне все более-менее понятно и логично, возможно тк изучал язык в юном возрасте)) Мне кажется тут предъявлять претензии не оч верно, все-таки Си низкоуровневый язык, вроде ассемблера.

И мне кстати лаконичность нравится, {} в 100 раз лучше begin..end.

Есть куда более ужасные вещи, например необходимость объявлять функцию *до* ее использования, или дурацкая система инклюдов (+ необходимость писать каждый раз по 2 файла, заголовочный и с кодом, + отсутствие директивы вроде include_once). Ну и еще необходимость ручного управления памятью, куча 2-смысленностей, из-за которых программировать на этом языке можно (имхо) только любителям острых извращений.

Кстати, язык D, очень даже ничего, если бы не сборщик мусора, и огромные бинарники, который делает компилятор (ну и еще пару моментов, вроде плохо реализованных строк :), был бы вообще идеальным языком ))
Anatoliy Knyazev
Степан Столяров, всё верно, но можно же:

char* x; char* y;

Согласён с Ильёй в том, что сейчас, в век широкоэкранных мониторов и code autocompletion, можно не экономить символы.
Роман Добровенский
То что @ читается как «at» знает не так много людей. Попытка приблизить язык программирования к естесственному языку — самая дурацкая затея.

Во-первых, естесственный язык намного сложнее любого языка программирования (хотя бы запомнить где когда какой предлог использовать — уже большая проблема), так же естесственный язык часто допускает многие варианты записи одного и того же, что для языка программирования не допустимо.

Во-вторых обычный текст гораздо хуже читается, когда это программа, так как он не структурирован. С ходу найти в куске текста конкретное предложение, где говорится о чем-либо весьма сложно. Уродливый reinterpret_cast<type>(value) сразу бросается в глаза.

В-третьих, человеческие языки не обладают достаточной краткостью для обозначения строгих формальных понятий (вспомним уродливое «тогда и только тогда»). Как ты в двух словах написашь название операции, которая проверяет корректность приведения типа во время выполнения так, чтобы ее однозначно можно было отделить от приведения типа, который выполняет проверку во время компиляции и от приведения типа, который отменяет свойство const? Кажется, const_cast, reinterpret_cast, static_cast и dynamic_cast — лучшие варианты на это.

Можно на самом деле продолжать бесконечно.

Вообще я не понимаю почему такое внимание уделяется указателям и приведениям типов. Это редкие и опасные операции, которых следует избегать, о чем уродливость конструкции reinterpret_cast<int*>(p) нам еще раз напоминает. Если кто-то работает с указателями и приведениями типов — пускай витамины чтоли пьет, для развития мозга.
Илья Бирман
@ никак иначе, чем «эт», не читается, об этом знают все, кто говорит по-английски. Это настолько же очевидно, как то, что & читается «энд».

«Естесственный язык часто допускает многие варианты записи одного и того же, что для языка программирования не допустимо» — чушь какая-то, я могу сотней способов одно и то же написать на куче ЯП.

Ты передёргиваешь вообще, я не предлагаю приближать язык к человеческому, я предлагаю давать конструкциям вменяемые и осмысленные названия или использовать понятные символы. Если бы оператор ?: использовал вместо вопросительного знака обратный слеш, я бы тоже предложил заменить на вопросительный знак. Звёздочка могла бы работать, допустим, как оператор goto (типа, сноска), а как оператор «значение по адресу» она работает херово, а собака — хорошо, вот и всё.

Писать на Си без указателей невозможно, это не Паскаль тебе.
Kalan
«Да... Этот чёртов язык на редкость логичен. Только мозги об его логику сломать можно.» ? один из моих знакомых

:)
Павел Малинников
x == x[0] == valueat (addressof (x) + 0)

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

Указатели сложны для многих людей, многие работают программистами, не понимая их, предпочитая Визуал Бейсик и прочее.

Человеку стремящемуся, все равно придется принять саму идею, заложенную в указателях. Если он не в состоянии понять идею, то слова valueat и addressof ему всё равно не помогут. А когда идея будет понята, он первый попросит заменить valueat на «*», а addressof на «&», потому что краткосрочный период обучения — это одно, а всю жизнь потом писать addressof — это другое.

Насчёт прикладного программирования трудно представить, чтобы человек не понимал значение указателей. По крайней мере, если он понимает, что такое полиморфизм и для чего он нужен.
Антон Вернигор
Егор, да, мне тоже C кажется далеким от языков высокого уровня, поэтому и в голову не приходило предъявлять к нему такие же требования.
А его лаконичность — это красиво. Такие короткие и емкие записи получаются, вроде while (*s++ = *t++); — сказка просто :-)
Павел Малинников
2 #26 Егор:
>... дурацкая система инклюдов ... отсутствие директивы вроде include_once ...

но есть ведь #pragma once ? Не стандарт, но это другое дело.

а вообще

#ifndef __MYFILE_H__
#define __MYFILE_H__

...

#endif //__MYFILE_H__

и вся проблема
Александр
> Например, звёздочка рядом с именем переменной в выражении означает обратное тому, что она означает в описании переменной
Для понимания достаточно знать, что типы переменных в Си именуются не так, как в более новых языках, т. е. не слева-направо. Они называются так, как будут использоваться
int *x; // *x has type int
int n[10]; // n[i] has type int
И всё. Достаточно прочесть учебник. Это другой способ, а не кривой, хотя, конечно, каждый знает единственно верный способ.

> int n[10]; // здесь n хранит адрес, но нет ни звёздочки, ни амперсанда
n не хранит ничего (т. е. нигде не написано, что там лежит указатель), это массив, который неявно преобразуется в указатель на нулевой элемент (что прекрасно имплементируется и при Вашем способе хранения).

> x == x[0]
Зато это считается нормальным и однозначным.

По поводу козлов авторов есть хорошая поговорка «Был бы я такой умный вчера, как моя жена сейчас». Задним числом все умны.
Алик Кириллович
Мне правильно показалось, что название этой статьи «Почему указатели трудны и что с этим делать» — намек на статью Джоэла Спольски: «Why are the Microsoft Office file formats so complicated? (And some workarounds)»: http://www.joelonsoftware.com/items/2008/02/19.html?
Илья Бирман
Нет.
Иван Коростелёв
В дополнение к комментарию #33: http://kalinin.ru/programming/cpp/17_07_00.shtml
Сергей К.
Илья, ты пытаешься выдумать новый язык, а он давно выдуман. Это java. Или C#. Или python. Любой из них выглядит проще и понятнее. В яве можно написать «Just a string».length(), и получится длина строки (надеюсь, эти кавычки не станут ёлочками). Питон вообще задумывался как язык, на котором будет трудно писать неразборчиво (это не главное его свойство, но об этом автор тоже подумал). Есть ещё такой язык модный Vala, который по синтаксу похож на яву и сишарп, но является просто надстройкой над си. То есть он транслирует более-менее читаемый код в код на языке си.

Я давно для себя решил, что в нормальном объектном языке для обращения к методам и атрибутам используется точка. Если используется что-то другое, значит это плохой объектный язык.
Игорь
Илья, а тебе не кажется, что фраза «Указатели сложны не потому, что это объективно что-то сложное, а потому, что авторы языка Си — козлы» это какой-то разрыв мозга? На кой сравнивать трудность понимания указателей и язык? Указатели в С есть потому что они есть на уровне архитектуры большей части ныне существующих процессоров. И их понимание довольно слабо связано с языком. Я в этом скорее встану на позицию Джоэла: есть люди, которые понимают указатели, и есть люди, которые не понимают указатели. Если ты их понимаешь, то синтаксис Си() будет тебя смущать недолго — достаточно взять и написать что-нибудь. Если ты их не понимаешь, то тут и изменения синтаксиса слабо помогут. А если ты хотел показать, что на С() с восхитительной легкостью можно создавать совершенно нечитаемый код, то пример выбран слабоватый: если уж собирался ругать указатели, то лучше приводить код с указателями функций, ну или что-нибудь хрестоматийное с программой в одну строку записанной и т. п., благо примеры нечитабельности в каждой второй книжке по С приводятся.
Егор
2 Павел Малинников:

Я знаю, про использвоание дефайнов для этого, их сейчас некоторые иды даже сами подставляют, но все равно это уродство. А между pragma once и include_once есть разница — одна применяется в включаемом файле, другая  — в том, который включает.
Павел Малинников
2 #38 Егор:
> все равно это уродство

ну, если «уродство» — это свойство явления, вызывающее отвращение, то это субъективное высказывание, т. к. у вас вызывает, а у кого-то не вызывает :-)

> между pragma once и include_once есть разница

а как эта разница мешает вам обеспечить одноразовое включение .заголовочного файла?
Павел Малинников
ну что же, попробуем повысить читаемость кода:

было:
pCar->goForward(); //машина — вперёд!

стало:
valueat (pCar).goForward(); // что-то по адресу «машина»... а что там? оно умеет вперёд?..
Илья Бирман
У вас не получилось :-)
Павел Малинников
да, не получилось при помощи такого подхода. Может быть, не такой уж Страуструп и козёл? :-)
Илья Бирман
Про Страуструпа вообще никто ничего не говорил. Си-плюс-плюс и Си — это разные языки.
Павел Малинников
Илья, из вашего ответа на 1-й комментарий я понял, что примеры из C++ не против правил: вы писали «(мы тут говорим про Си-плюс-плюс)».

Ну ладно, хоть Страуструпа отмазали :-) Кериниган и Ричи по-прежнему опасносте! :-)

Попробуем применить методику на трудночитаемом фрагменте чистого Си:

было:
void ** (*func) (int &, char **(*)(char *, char **));

стало:
void address address (address func) (int ref, char address address (address) (char address, char address address));

стало лучше?
Илья Бирман
Я имел в виду «не про Си-плюс-плюс», это опечатка (что следует из остального текста моего ответа).

Стало ли лучше я не знаю, потому, что я не понимаю что написано. Расскажите?
Павел Малинников
а, ну func — это указатель на функцию с двумя аргументами:
1. ссылка на int
2. указатель на функцию, которая тоже имеет два параметра:
1. указатель на char
2. указатель на указатель на char
эта функция возвращает указатель на указатель на char
а func возвращает указатель на указатель на void
Илья Бирман
Приведите, пожалуйста, пример такой функции.
Павел Малинников
а как код обрамить? code?
Илья Бирман
Процентпроцент...процентпроцент.
Павел Малинников
Она могла бы использоваться примерно так:

typedef void ** (*func) (int &, char **(*)(char *, char **));

char ** callback (char *, char **)
{
...
}

func pFunc = (func) GetProcAddress (hSomeDll, "funcFromSomeLibrary");
int a = 5;
void **p;

p = pFunc (a, callback);


понятно, что это учебный пример, но у меня есть реальный проект, где я писал объектные обёртки для одной географической библиотеки. Если интересно, могу показать, как в «прикладном ПО» выглядит, только там Си++ и кода надо приводить страницы на полторы-две, иначе контекста не уловить.
Илья Бирман
А в конце строчки про тайпдеф не пропущено слово func случайно? А то иначе я вообще не могу связать это всё :-)
Павел Малинников
неа. func теперь и есть новый тип. Переменные этого типа хранят указатель на функцию, которая ... см. #43
Илья Бирман
А, всё, распарсил про func. А что значит звезда в скобках?

Это всё жесть, конечно.
Павел Малинников
звезда в скобках — это запись того, что аргументом будет ещё один указатель на функцию, но специального имени для неё не нужно придумывать. Будет называться callback или ещё как-нибудь, не важно.

Жесть, но на первый взгляд, а вообще, в работе встречается. Для меня лично жесть — это наследование шаблонов.
Павел Малинников
для того, чтобы придать немножко больше смысла примеру:

typedef void ** (*RENDER_MAP_FUNC) (int, char **(*)(char *, char **));

char ** callback (char *pc, char **ppc)
{
/* pCurRenderFunc могла бы вызывать её
по ходу длительной операции, типа "n% complete..." */
...
}

RENDER_MAP_FUNC pCurRenderFunc =
  (RENDER_MAP_FUNC) GetProcAddress (hSomeDll, "funcFromSomeLibrary");
int zoomLevel = 5;
void **pResult; /* массив массивов с результатами */

pResult = pCurRenderFunc (zoomLevel, callback);
Василий Журавлёв
У «Ай-Би-Эм» есть такой язык программирования, РПГ, выросший из языка создания отчётов и имеющий нечеловеколюбивую природу: команды языка нужно начинать писать со строго определённых отступов (не как в Питоне, а гораздо неудобнее: звёздочка в восьмой колонке обозначает начало комментария, например), все переменные объявляются в самом начале файла, а процедуры оформлены зачастую безусловными переходами.
На самом деле, на РПГ можно писать и в более удобном для программиста стиле (в «Ай-Би-Эм» тоже нашёлся свой Илья Бирман), но большинство профессиональных программистов на РПГ начинают с поддержки существующего кода, который оформлен в старом стиле. Переписать весь код по-новому ?— задача неподъёмная, поэтому проще научить програмистов понимать неудобную запись.
Вова
Мне нравится следующая аналогия матанализа и указателей C. Оператор взятия адреса «&» это как взятие дифференциала от функции, оператор разыменовывания указателя «*» это как взятие интеграла : ). Чтобы получить нормальную функцию, а не дифференциал какой-нибудь бесконечно малый, надо дифференциал n-го порядка проинтегрировать n раз. Так же и с указателями. Например, у нас есть Type** type. Если написать *type то получим «дифференциал», а он нам не нужен и надо четыре раза «проинтегрировать» **type.f(). =)
Вова
Про синтаксис языков давно есть нормальная идея отделить содержимое от представления. Надо исходники хранить в каком-нибудь подходящем формате, хоть, XML, где тегами обрамлять семантические конструкции, а программист будет в IDE выбирать какой ему нравится стиль оформления, фигурные скобки или бегин-энд, а может вообще в прямоугольничке все это зафигачить, картинки добавить, анимацию можно включить : )

P. S. В предыдущем сообщении я набрал четыре звездочки подряд, очевидно тут полужирное начертание делается с помощью обрамления в двойные звездочки. Может сделать тег <code>?
Дмитрий Акимов
Да, указатели действительно сложны для первоначального освоения. Когда я в детстве изучал Паскаль, я долго не мог «въехать» в концепцию указателей, так же, как и объектов, кстати говоря.

Хотя идея с предложенной вами нотацией неплоха, мне кажется, что в указателях трудна не столько нотация, сколько сами указатели.

Трудность состоит в том, что в языке есть переменные (они же объекты), которые хранят некоторые значения, а есть указатели, которые, хоть и выглядят, как остальные переменные, но кардинально отличаются от них тем, что сами как будто бы ничего не хранят, а лишь неким образом ссылаются на другие переменные. Таким образом, в языке создается два типа разнородных сущностей, внешне выглядящих одинаково.

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

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

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

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

Указатель в C и C++ — это лишь частный ограниченный случай ссылки, заставляющий размещать объекты только в оперативной памяти, занимая непрерывную ее область, и давать им строго определенную структуру. А ведь объект, в принципе, может размещаться и в памяти другого процесса или компьютера, на диске или в сети. К тому же, объекта вообще может не существовать физически — его значение может вычисляться в результате работы какого-то алгоритма, он может быть разбит на части в оперативной памяти, он может занимать несколько битов внутри различных байт, не находящихся рядом друг с другом, и так далее.

Интересно, что понятие ссылки является фундаментальным в программировании, и реализация ссылок в языке программирования весьма существенно влияет на стиль, характер и возможности языка. Например, некоторые авторы основной характеристикой языка C называют именно указатели.
Павел Малинников
2 #52 Дмитрий Акимов:

> Указатель в C и C++ — это лишь частный ограниченный случай ссылки ...

Дмитрий, в C++ есть понятие ссылки. Как во-вашему, в чем её отличие от указателя?

И ваше понимание «объектов» очень заинтересовало меня. Это которые не в оперативной памяти, а на диске или в сети.
Дмитрий Акимов
#53 Павел Малинников

Если вы знаете C, вы сами скажете, в чем отличие ссылки от указателя.

Но я говорю не про эту ссылку. Кстати, дискуссия-то шла о C, а не C
.

Я говорю об абстрактном понятии ссылки. Например, текст «#53 Павел Малинников» — это ссылка на ваше сообщение, она локальна для этой страницы, а это ссылка на страницу: «http://ilyabirman.ru/meanwhile/2009/05/26/1/comments/», ISBN книги — это ссылка на книгу, адрес объекта в оперативной памяти — это ссылка на объект, и так далее.

А понятие «объекта» в том смысле, в котором его определяют в теории объектно-ориентированного программирования, вряд ли изменится, если объект будет храниться на диске или в сети, а не в оперативной памяти.

Ведь диск — это тоже память, которая отличается от оперативной памяти энергонезависимостью, скоростью и, возможно, другой моделью адресации (ссылок).

Как вы знаете, еще есть такие типы памяти как ПЗУ и SSD.

Вся разница здесь только в том, как вы ссылаетесь на этот объект (ну и, возможно, как вы получаете доступ к его данным).

Допустим, «0x0F326E70» — это ссылка на объект в памяти текущего процесса (его адрес), «c:\file.bin, 0x32FE0» — это ссылка на объект хранящийся в файле (имя файла и смещение в байтах), «http://site.com/file.bin, 0x32FE0» — это ссылка на объект, хранящийся в сети (url файла и  смещение в байтах).
Павел Малинников
2 #54 Дмитрий Акимов:

Например, ISBN книги — это ссылка на книгу. Как из этого следует, что «оптимальным решением было бы вообще не использовать указатели в таком виде, в каком они используются в C и C++»?

Да, дискуссия о C. Зачем же вы тогда говорите об «абстрактном понятии ссылки»? Или вы просто нас разыгрываете?

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

У вас не возникало вопроса, почему при доступности терабайтных винчестеров люди всё-таки покупают оперативную память? Ставят себе жалкие 16 Гб оперативки, а винчестером 1 Тб не пользуются? Это ведь тоже память? Наверное, потому, что в случае оперативной памяти скорость доступа выше, да? А так бы пользовались за милую душу?

По вашей ссылке «http://site.com/file.bin, 0x32FE0» записано, например, «ED 03 BF 16 A4». Раз уж вы упомянули ООП, скажите, пожалуйста, какого типа это объект? Есть ли у него какие-нибудь методы? Как их вызвать?

Павел Малинников
Это не C и не указатели трудны. Они лишь отражают сложность (не такую уж преодолимую)
самого устройства компьютера. Но они отражают её честно, давая программисту модель,
при помощи которой он будет понимать, что происходит у него в программе.
Вне зависимости от языка, на котором пишет.

А языки, которые пытаются скрыть устройство объектов (например Паскаль),
защищает программиста не от сложности, а от понимания, как работает память.
Из-за чего полно людей, рассуждающих об указателях, основываясь только на своих фантазиях.
Причём все как один, склоняются, что «не надо бы их использовать», а то вдруг «объект»
не в тех битах байта разместится, что тогда будем делать?

Вот они, какие, указатели-то. Опасные.
Популярное