Почему синтаксис C для массивов, указателей и функций был разработан таким образом?

16

Увидев (и задав!) Столько вопросов, похожих на

Что int (*f)(int (*a)[5])значит в С?

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

Почему синтаксис C был разработан таким образом?

Например, если бы я проектировал указатели, я бы перевел «указатель на массив из 10 элементов указателей» в

int*[10]* p;

и не

int* (*p)[10];

Я думаю, что большинство людей с этим согласятся.

Так что мне интересно, почему неинтуитивный синтаксис? Была ли конкретная проблема, которую решает синтаксис (возможно двусмысленность?), О которой я не знаю?

user541686
источник
2
Вы знаете, что нет реального ответа на этот и другие вопросы. Правильно? То, что вы получите, это только догадки.
BЈовић
7
@ VJo - вполне может быть «реальный» (т. Е. Объективный) ответ - авторы языка и комитеты по стандартизации однозначно обосновали (или хотя бы объяснили) многие из этих решений.
детально
Я не думаю, что предложенный вами синтаксис обязательно более или менее «интуитивно понятен», чем синтаксис Си. С есть то, что есть; как только вы это узнаете, у вас больше не возникнет этих вопросов. Если вы этого не узнали ... ну, может, в этом и заключается настоящая проблема.
Калеб
1
@Caleb: Забавно, как ты так легко пришел к выводу, потому что я узнал об этом, и у меня все еще был этот вопрос ...
user541686 31.10.11
1
Команда cdeclочень удобна для декодирования сложных объявлений языка Си. Есть также веб-интерфейс на cdecl.org .
Кит Томпсон

Ответы:

16

Я понимаю историю этого так, что он основан на двух основных моментах ...

Во-первых, авторы языка предпочли сделать синтаксис ориентированным на переменные, а не на типовые. То есть они хотели, чтобы программист посмотрел на объявление и подумал: «если я напишу выражение *func(arg), это приведет к int; если я напишу, у *arg[N]меня будет число с плавающей точкой», а не « funcдолжен быть указатель на функцию, принимающую это и вернуть это ".

Запись C в Википедии утверждает, что:

Идея Ричи состояла в том, чтобы объявить идентификаторы в контекстах, напоминающих их использование: «объявление отражает использование».

... ссылаясь на p122 из K & R2, который, увы, мне не нужно искать расширенную цитату для вас.

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

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

Сказав это, я не эксперт по грамматике языка или написанию компилятора. Но я знаю достаточно, чтобы знать, что есть, что знать;)

detly
источник
2
«облегчение написания компиляторов» ... за исключением того, что C известен тем, что его трудно анализировать (только в C ++).
Ян Худек
1
@JanHudec - Ну ... да. Это не водонепроницаемое утверждение. Но в то время как C невозможно разобрать как грамматику без контекста, как только один человек придумал способ ее проанализировать, это перестает быть трудным шагом. И дело в том, что в первые годы он был плодовитым из-за того, что люди могли легко создавать компиляторы, поэтому K & R, должно быть, достигла определенного баланса. (В печально известном Ричарде Гэбриэле « Повышение« хуже - лучше »» он воспринимает как должное - и оплакивает - тот факт, что легко написать компилятор C для новой платформы.)
ловко
Кстати, я рад, что меня поправили - я мало что знаю о разборе и грамматике. Я собираюсь больше на вывод из исторического факта.
детально
12

Многие странности языка C можно объяснить тем, как компьютеры работали при его разработке. Объем памяти хранилища был очень ограничен, поэтому было очень важно минимизировать размер самих файлов исходного кода . Практика программирования еще в 70-х и 80-х годах заключалась в том, чтобы исходный код содержал как можно меньше символов и, желательно, не содержал чрезмерных комментариев к исходному коду.

Сегодня это, конечно, нелепо, ведь на жестких дисках практически неограниченное пространство для хранения. Но это одна из причин, почему C имеет такой странный синтаксис в целом.


Что касается конкретно указателей массива, ваш второй пример должен быть int (*p)[10];(да, синтаксис очень запутанный). Возможно, я бы прочитал это как "int указатель на массив из десяти" ... что имеет смысл несколько. Если бы не скобки, компилятор интерпретировал бы его как массив из десяти указателей, что придало бы объявлению совершенно другое значение.

Так как указатели на массивы и указатели на функции имеют довольно непонятный синтаксис в C, разумная вещь, которую нужно сделать, - это убрать странности. Возможно так:

Неясный пример:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Непонятный, эквивалентный пример:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

Вещи могут стать еще более неясными, если вы имеете дело с массивами указателей на функции. Или самые неясные из них: функции, возвращающие указатели на функции (слегка полезные). Если вы не используете typedef для таких вещей, вы быстро сошли с ума.


источник
Ах, наконец, разумный ответ. :-) Мне любопытно, как конкретный синтаксис фактически уменьшит размер исходного кода, но в любом случае это правдоподобная идея и имеет смысл. Благодарю. +1
user541686
Я бы сказал, что речь шла не о размере исходного кода, а о написании компилятора, но определенно +1 за «typedef out the странность». Мое психическое здоровье резко улучшилось в тот день, когда я поняла, что могу это сделать.
детально
2
[Требуется цитата] на предмет размера исходного кода. Я никогда не слышал о таком ограничении (хотя, может быть, это что-то "все знают").
Шон Макмиллан
1
Я хорошо кодировал программы в 70-х годах на COBOL, Assembler, CORAL и PL / 1 для IBM, DEC и XEROX, и я НИКОГДА не сталкивался с ограничением размера исходного кода. Ограничения на размер массива, размер исполняемого файла, размер имени программы - но не размер исходного кода.
Джеймс Андерсон
1
@Sean McMillan: я не думаю, что размер исходного кода был ограничением (учтите, что в то время многословные языки, такие как Pascal, были довольно популярны). И даже если бы это было так, я думаю, что было бы очень легко предварительно проанализировать исходный код и заменить длинные ключевые слова короткими однобайтовыми кодами (как, например, некоторые базовые интерпретаторы раньше). Поэтому я нахожу аргумент «С кратким, потому что он был изобретен в период, когда было доступно меньше памяти», немного слабым.
Джорджио
7

Это довольно просто: int *pозначает, что *pэто int; int a[5]означает, что a[i]это инт.

int (*f)(int (*a)[5])

Значит это *f это функция, *aэто массив из пяти целых чисел, так fже как и функция, которая берет указатель на массив из пяти целых чисел и возвращает int. Однако в Си бесполезно передавать указатель на массив.

С заявлениями очень редко это усложняется.

Также вы можете уточнить, используя typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);
Кевин Клайн
источник
4
Извиняюсь, если это звучит грубо (я не хочу, чтобы это было так), но я думаю, что вы пропустили весь смысл вопроса ...: \
user541686
2
@Mehrdad: я не могу сказать вам, что было в уме Кернигана и Ричи; Я рассказал вам логику синтаксиса. Я не знаю о большинстве людей, но я не думаю, что предложенный вами синтаксис более понятен.
Кевин Клайн
Я согласен - необычно видеть такую ​​сложную декларацию.
Калеб
Конструкция C декларации предшествует typedef, const, volatile, а также возможность инициализировать вещи внутри объявлений. Многие из досадных двусмысленностей синтаксиса объявления (например, int const *p, *q;должны ли они связываться constс типом или декларантом) не могли возникнуть в языке, который изначально разрабатывался. Хотелось бы, чтобы язык добавил двоеточие между типом и декларантом, но допускал его пропуск при использовании встроенных типов «зарезервированных слов» без квалификаторов. Значение int: const *p,*q;и int const *: p,*q;было бы ясно.
суперкат
3

Я думаю, вы должны рассматривать * [] как операторы, которые связаны с переменной. * записывается перед переменной, [] после.

Давайте читать тип выражения

int* (*p)[10];

Самым внутренним элементом является переменная p, поэтому

p

означает: р является переменной.

Перед переменной стоит *, оператор * всегда ставится перед выражением, на которое он ссылается, поэтому

(*p)

означает: переменная p является указателем. Без () оператор [] справа имел бы более высокий приоритет, т.е.

**p[]

будет проанализирован как

*(*(p[]))

Следующим шагом является []: поскольку дальнейшее () не существует, [] имеет более высокий приоритет, чем внешний *, поэтому

(*p)[]

означает: (переменная p является указателем) на массив. Тогда у нас есть второе *:

* (*p)[]

означает: ((переменная p является указателем) на массив) указателей

Наконец, у вас есть оператор int (имя типа), который имеет самый низкий приоритет:

int* (*p)[]

означает: (((переменная p - указатель) на массив) указателей) на целое число.

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

Джорджио
источник
0

Это не так сложно, когда вы начинаете думать, и C никогда не был очень простым языком. И на int*[10]* pсамом деле не проще, чем int* (*p)[10] И какой тип К будет вint*[10]* p, k;

Дайниус
источник
2
k будет неудачным обзором кода, я могу понять, что будет делать компилятор, я даже могу быть обеспокоен, но я не могу понять, что задумал программист - не
удалось
и почему к не удалось проверить код?
Дайний
1
потому что код не читается и не поддерживается. Код не является правильным для исправления, очевидно корректным и, вероятно, останется верным при обслуживании. Тот факт, что вы должны спросить, каким будет тип k, является признаком того, что код не соответствует этим основным требованиям.
Mattnz
1
По сути, в одной строке есть 3 (в данном случае) объявления переменных разных типов, например int * p, int i [10] и int k. Это недопустимо. Допускается несколько объявлений одного и того же типа, при условии, что переменные имеют некоторую форму отношения, например, int width, height, глубина; Имейте в виду, что многие люди программируют с использованием int * p, так что же я в «int * p, i;».
Mattnz
1
@Mattnz пытается сказать, что вы можете быть настолько умным, насколько захотите, но все это бессмысленно, когда ваши намерения не очевидны и / или ваш код плохо написан / нечитаем. Такие вещи часто приводят к поломке кода и потере времени. Плюс, pointer to intи intдаже не одного типа, поэтому они должны быть объявлены отдельно. Период. Послушай мужчину. У него есть 18 тыс. Повторений по причине.
Брэден Бест