Comments 73
Инициализировать ссылку может только ссылка.Рассмотрим такой код:
#include <iostream>
#include <string>
std::string f() {
return "test";
}
int main() {
const std::string& ttt = f();
std::cout << ttt << std::endl;
// object returned by f is deleted here
}
В данном примере ссылка инициализируется временным объектом и тем самым продлевает его жизнь (C++ такой сложный язык, всё-таки!). В данном случае считаете ли вы этот временным объект ссылкой или считаете этот случай исключением из тезиса «Инициализировать ссылку может только ссылка»?
вроде как, можно
Да, можно. Точно также, как и передавать временные объекты в функции по ссылке-на-const можно.
Стандарт это гарантирует
Да, я читал про это тут: alenacpp.blogspot.ru/2008/01/const.html
Меня это поведение ссылок тоже шокировало. Думаю, это хорошая ловушка для собеседований, можно сравнить с ловушками для JavaScript.
Но эта странность имеет практическую пользу. Зная о ней, можно писать
const std::string& aaa = ...
и не задумываться о том, возвращают ли нам ссылку или временную переменную. Это позволяет писать код, который не придётся менять, если используемые в нём геттеры станут возвращать не ссылки, а временные переменные.Этот принцип («выражение, являющееся переменной — ссылка») — моя выдумка.
Не совсем Ваша, посмотрите что такое value category.
Далее, если прибавить к указателю на какой-нибудь тип T число, то реальное численное значение этого указателя увеличится на это число, умноженное на sizeof (T).
Сейчас автор наверно тоже удивится. Стандарты обоих языков говорят, что «The value representation of pointer types is implementation-defined» и «A pointer can be explicitly converted to any integral type large enough to hold it. The mapping function is implementation-defined». Так что ваше заявление описывает какой-то частный случай и в общем не совсем корректно.
Объявляются массивы, например, так:
int x[5];
Выражение в квадратных скобках должно быть непременно константой времени компиляции в C89 и C++98. При этом в квадратных скобках должно стоять число, пустые квадратные скобки не допускаются.
Объявлять массивы можно и с пустыми скобками (С++98: 8.3.4:1). Вот определять нужно так. Пожалуйста, не путайте объявление и определение.
В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &x и &(x[0]) численно равны (тут я лихо написал выражение &(x[0]), на самом деле в нём не всё так просто, к этому мы ещё вернёмся). Но эти выражения имеют разный тип — int (*TYPE)[5] и int *TYPE, поэтому сравнить их при помощи == не получится. Но можно применить трюк с void *: следующее выражение будет истинным: (void *)&x == (void *)&(x[0]).
Не могли бы вы показать место в стандарте, которое гарантирует это?
Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива.
Да, не проверяется. Но стандарт явно говорит (С++98 5.7:5), что выход адреса за элемент следующий за последним элементом массива может привести к неопределённому поведению, если случится переполнение.
Объявлять массивы можно и с пустыми скобками
Спс, исправил
Не могли бы вы показать место в стандарте, которое гарантирует это?
Не могу, я это понимаю на интуитивном уровне ^_^
В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &x и &(x[0]) численно равны (тут я лихо написал выражение &(x[0]), на самом деле в нём не всё так просто, к этому мы ещё вернёмся). Но эти выражения имеют разный тип — int (*TYPE)[5] и int *TYPE, поэтому сравнить их при помощи == не получится. Но можно применить трюк с void *: следующее выражение будет истинным: (void *)&x == (void *)&(x[0]).
Не могли бы вы показать место в стандарте, которое гарантирует это?
4.2 Array-to-pointer conversion [conv.array]
1 An lvalue or rvalue of type “array of N T” or “array of unknown bound of T” can be converted to a prvalue
of type “pointer to T”. The result is a pointer to the first element of the array.
поэтому адрес самого массива и адрес его нулевого элемента численно совпадают.
Вот именно это место меня интересовало.
8.3.4.1 Arrays [dcl.array]
An object of array type contains a contiguously allocated non-empty set of N subobjects of type T.
8.3.4.9
[ Note: It follows from all this that arrays in C++ are stored row-wise (last subscript varies fastest) and that the first subscript in the declaration helps determine the amount of storage consumed by an array but plays no other part in subscript calculations. —end note ]
5.3.3 Sizeof [expr.sizeof]
When applied to an array, the result is the total number of bytes in the array. This implies that the size of an array of n elements is n times the size of an element.
1.8.6
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies.
Из вышеперечисленного можно понять, что ни на какой мусор просто напросто нет места.
Просьба не путать с
5.3.4.12
— new T[5] results in a call of operator new[](sizeof(T)*5+x), and
— new(2,f) T[5] results in a call of operator new[](sizeof(T)*5+y,2,f).
Here, x and y are non-negative unspecified values representing array allocation overhead; the result of the
new-expression will be offset by this amount from the value returned by operator new[]. This overhead
may be applied in all array new-expressions, including those referencing the library function operator
new[](std::size_t, void*) and other placement allocation functions. The amount of overhead may vary
from one invocation of new to another. —end example ]
1.8.6
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies.
В моём стандарте таких слов нет. В нём вообще пункта 1.8.6 нет, есть 1.8:6, но это Note.
Плохая эта практика.
Пример: на одном из типов процов, попытка обратится к элементу массива char «обманув» компилятор путем манипуляции с указателем приведет к segmentation fault. Поскольку char там (C/C++) один байт (8-бит). Но вот минимальный размер адресации 32 бита.
Вообще полезно заглядывать в ASM логи, порожденные компилятором.
Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива.
Еще более плохая практика. Вероятность нарваться на границу защищенного сегмента памяти — просто.
Не надо переносить опыт работы в VisualStudio на весь мир.
Еще более плохая практика. Вероятность нарваться на границу защищенного сегмента памяти — просто.
Здесь у автора все в порядке. Ссылаться за пределы массива можно и даже нужно. Итераторы конца последовательности (end) этим и занимаются. Разыменовывать нельзя, да, а указывать — на здоровье.
Век живи — век учись.
А как насчет такой конструкции, будет ли это считаться UB?
int x;
std::accumulate(&x, &x + 1);
Скорее всего, да. Про адресную арифметику с переполнением я в стандарте нашел сноску только насчет массивов. Но можно ли указатель на единичную переменную трактовать как массив из одного элемента?
«For the purposes of these operators, a pointer to a nonarray object behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.»
Но вообще это все на самом деле очень мутная тема. Например, непонятно даже, валиден ли указатель, если он указывает на что-то валидное внутри массива, но был получен арифметикой из другого массива, даже если стандарт гарантирует непрерывное размещение в памяти. Например, если есть:
int a[2][3];
И мы взяли указатель на &a[0][2], а потом сдвинули его на два элемента вперед. По логике вещей, мы должны попасть в &a[1][1], и стандарт гарантирует именно такое размещение в пемяти. Но ведь исходный указатель был взят от массива a[0], и в стандарте есть этот параграф, который явно запрещает сдвигать его за пределы (плюс один элемент в конце), т.е. вроде как это UB. На эту тему в comp.std.c++ был длинный тред несколько лет назад, но в итоге консенсуса не было.
Много текста
Я написал много текста, чтобы было понятнее.
массивы в стеке и на куче работают по разному
Дело не в том, где расположен массив. Вот смотрите:
int (*x)[7] = new int[5][7];
Мы только что создали двумерный массив в куче. Но он ведёт себя похожим образом на двумерный массив на стеке, про который я говорил в посте. Т. е. он представляет собой единый блок размера 5 x 7, пусть даже в куче.
потому лучше избегать использования двумерных статических массивов
Чтоа? Совершенно неправильный вывод. Динамические массивы динамических массивов — вот чего нужно избегать, потому что они медленные. Использовать их нужно, когда ничего другого использовать не получается. А вот статические двумерные массивы — это то, что нужно использовать в первую очередь (если места на стеке хватает, конечно)
// Вычисляет длину массива
template <typename t, size_t n> size_t len (t (&a)[n])
{
// return len;
// опечатка, должно быть
return n;
}
Мы только что создали двумерный массив в куче
Мы создали семь элементов типа int*, находящихся «непрерывно» друг за другом в стеке. Каждый такой элемент указывает на начало «непрерывного» массива из пяти элементов в куче. Поправьте меня, если не прав.
#include <iostream>
using namespace std;
int main() {
int (*x)[7] = new int[5][7];
cout << x << " " << &(x[0][0]) << " " << &(x[0][1]) << " " << &(x[0][6]) << " " << &(x[1][0]) << "\n";
}
На моей машине (GNU/Linux x86_64 gcc, sizeof (int) == 4, sizeof (void *) == 8) это выдаёт «0x1c78010 0x1c78010 0x1c78014 0x1c78028 0x1c7802c». Обратите внимание, что первые два указателя численно равны, а
&(x[1][0]) - &(x[0][6]) == &(x[0][1]) - &(x[0][0])
Объявление типа:
int (*x)[7];
уменьшает стек на 7 * sizeof(int*) байт. То есть, выделяется память для 7-ми указателей на int в стеке. Далее, инициализируя следующим образом:
int (*x)[7] = new int[5][7];
мы выделяем, действительно, непрерывную память в куче, размер которой равен 7 * 5 * sizeof(int) = 140 байт. И получается, что в стеке хранятся 7 чисел, значения которых равны адресам этой кучи со смещениями = (StartAddress + 5 * sizeof(int) * n)байт (n изменяется от 0 до 6). Когда мы обращаемся по адресу x[0][4], то идет обращение к памяти, адрес которой записан в стеке и численно равен *x[0], и к этому адресу добавляется смещение 4 * sizeof(int).
Но это все, простой способ запутать новичка, и не более чем финт ушами. Гораздо проще и понятнее писать следующим образом:
const size_t N_COLS = 7;
const size_t N_ROWS = 5;
int* p = new int[N_ROWS * N_COLS];
// аналог p[col_i][row_i]
int tmp = p[col_i * N_COLS + row_i];
Всем, чем отличается данная реализация от выше описанной, это то, что в стеке для массива хранится только одна переменная (адрес начала массива) и адресация выполняется на пару машинных инструкций дольше. Зато «не улетим» в переполнение стека.
sizeof(int (*)[7] ) == sizeof(int (*)[1000] )== sizeof(int*)
Также замечу, что &*EXPR (здесь EXPR — это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR — указательНе верно если тип *EXPR переопределяет оператор & странным образом.
Правильней так std::addressof(*EXPR);
Я против того, чтобы на хабре появлялись статьи такого дилетантского качества с претензией «окончательно» что-то кому-то объяснить.
Да, стандарты С++ написаны очень трудным языком. Но они, вообще-то, написаны потом, кровью и слезами, и ничего лишнего там нет.
Поэтому такие упрощения, как «ссылка — это тот же указатель, только в профиль» — это способ запутать.
А я за то, чтобы появлялись.
И чтобы в комментариях к ним появлялись люди, готовые указать на неточности и серьезные нестыковки, а также рассказать как на самом деле, не ссылаясь на RTFM.
Имхо — это вполне легитимный и эффективный способ появления на свет популярного контента для тех, кому не настолько нужно, чтобы разбирать стандарты.
А извлекать из комментариев информацию — дело тяжелое и неблагодарное. Особенно когда один и тот же вопрос поднимается «нечитателями» более одного раза, как уже устроили тут:
первый раз jcmvbkbc поднял тему мусора в массиве
второй раз уже mt_ интересуется фактически тем же
Кто может уточнить, насколько это противоречит или не противоречит стандарту (С, С++)? И если стандарту не противоречит, то имеет смысл отучать себя вообще использовать указатель на массив в коде — только указатель на первый элемент.
мы объявили int x. Теперь x — это переменная типа int TYPE и никакого другого. Это int и всё тут. Но если я теперь пишу x + 2 или x = 3, то в этих выражениях подвыражение x имеет тип int &TYPE. Потому что иначе этот x ничем не отличался бы от, скажем, 10, и ему (как и десятке) нельзя было бы ничего присвоить.
— не верно. В этих выражениях х имеет именно значение int. Потому что если нам дано:
int x = ...;
int& ref = ....;
int y1 = x + 10;
int y2 = ref + 10;
… то для вычисления x + 10 и ref + 10 генерируется, в общем случае, разный код. У ссылки всегда (без оптимизации) присутствует лишний уровень индирекции, как и у разыменовывания указателя. У простой переменной — нет. Более того, переменную нельзя считать ссылкой в таком случае:
void f1(int x) {...}
void f2(int& x) {...}
Если следовать вашей логике, то эти функции эквиваленты — ведь x «это по сути int&». © Что не верно.
Этот принцип («выражение, являющееся переменной — ссылка») — моя выдумка. Т. е. ни в каком учебнике, стандарте и т. д. я этот принцип не видел. Тем не менее, он многое упрощает и его удобно считать верным. Если бы я реализовывал компилятор, я бы просто считал там переменные в выражениях ссылками, и, вполне возможно, именно так и предполагается в реальных компиляторах.
Еще раз — ничего это не упрощает, а лишь наводит тень на плетень. У ссылки и у указателя лишний уровеь индирекции — сначала в регистр загружается значение адреса (который находится по какому-то другому адрему), а потому уже а этому адресу загружается финальное значение.
P.S. Когда-то правил огромную математическую модель радиополя на С, там там физик везде так и писал: *(pX + i) вместо рХ[i]. Он думал, что так будет выполняеться быстрее :)
int x = ...
int &ref = ...
Так вот, я нигде не говорил, что x и ref — это теперь одно и то же. Я лишь указываю на то, что у x и ref как у выражений один и тот же тип —
int &TYPE
. Далее, я нигде не говорил, что из совпадения типа следует совпадение ассемблерного кода для работы с этими данными. Действительно, в ассемблерном коде запросто код для работы с ref может включать ещё один уровень индерекции по сравнению с кодом для работы с x.void f1(int x) {...}
void f2(int& x) {...}
В обоих случаях выражение x внутри функции будет иметь тип
int &TYPE
. Но, разумеется, это два разных способа передачи значения в функцию.У ссылки и у указателя лишний уровеь индирекции
Не обязательно. Если я пишу
int &ref = x
, то, скорее всего, даже с выключенной оптимизацией, доступ к ref и x будет одинаково быстрымНо, разумеется, это два разных способа передачи значения в функцию.
В случае ссылки передается не значение, а сам объект. Что принципиально отличается от передачи простого int.
Не обязательно. Если я пишу int &ref = x, то, скорее всего, даже с выключенной оптимизацией, доступ к ref и x будет одинаково быстрым
В реальном коде не имеет смысла создавать второе название для переменной x. Весь смысл использования ссылок в том, что они могут инициироваться в рантайме — например как результат вызова функции или от указателя.
Суть моей реплики: int x — это не int& x! Хотя внешне во многих случаях ведут себя одинаково.
typeid() с вами не согласен, кстати.
(в смысле, тип-то у них действительно один, но это не int&)
Запись a[b] всегда эквивалентна *(a + b)
При этом фраза Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент всё равно верна:
char x[3] = { 'a','b','c' };
assert(x[2] == 2[x]); // 'c'
char* z = x;
assert(z[2] == 2[z]);
8.3.4 Arrays [dcl.array]
[ Note: Except where it has been declared for a class (13.5.5), the subscript operator [] is interpreted in such
a way that E1[E2] is identical to *((E1)+(E2)). Because of the conversion rules that apply to +, if E1 is an
array and E2 an integer, then E1[E2] refers to the E2-th member of E1. Therefore, despite its asymmetric
appearance, subscripting is a commutative operation.
Именно свойство коммутативности позволяют производить такую запись.
С одной стороны, приятно, что один из самых мощных языков (и мною любимых) изучают и популяризируют.
С другой стороны у меня какое-то ощущение, что Хабр не то, чтобы деградирует — вот комментарии все о подводных камнях и практиках действительно любопытные, сколько занимается перекопированием книг, скажем, Страуструпа или того же Тома Свана — уровня Turbo C++ родом из 90х. Вот уж правда, может лучше аггрегировать ссылки на хорошие, читаемые источники с указанием подводных камней и практики? Нежели пересказ базовых понятий своими словами?
Статья — набор капитанства. Всё это можно найти в книгах, без особого усилия. Просто эта тема тяжела для понимания новичкам, и такое впечатление, что один новичек наконец познал Дзен и решил об этом написать.
Никого не хотел обидеть, Просто жалко впустую потраченых нескольких минут для прочтения стьи, в которой ничего нового не нашел.
Есть такие две структуры
struct TS {
int a;
int b;
int c[0]; // указатель на 0 элемент массива int
};
union TR
{
char * cptr;
int * iptr;
TS * sptr;
};
{
TR tr;
int p[] = { 111, 222, 333, 444, 555, 0, 0 };
tr.iptr = p;
tr.sptr->a = 900;
tr.sptr->b = 800;
tr.sptr->c[0] = 1000;
tr.sptr->c[1] = 2000;
}
Есть ли какой другой способ описать int c[0]; в качестве указателя на массив целых в структуре TS?
Какой язык? C или C++? Какая версия стандарта? Используем ли нестандартные расширения компилятора?
Когда мы пишем struct foo { int a[4]; }
, то это не указатель на массив, это сам массив находится прямо внутри структуры. Т. е. внутри структуры будет выделено место для 4-х int'ов. Если вместо 4 написано 0, то ничего от этого принципиально не меняется. Вы таким образом выделяете в структуре место для нуля интов. И у вас будет всё же не указатель на массив, а просто массив. Но когда вы пишите tr.sptr->c[0]
, то перед применением оператора индексации, т. е. перед применением этих квадратных скобок, tr.sptr->c
конвертится из массива в указатель на этот массив, т. е. в сущность типа int *
, ну или int *TYPE
, если использовать обозначения из этой статьи.
Теперь смотрите. При обращении к элементу массива проверок выхода за границы не производится. Значит, можно смело писать tr.sptr->c[0]
, хоть там и нет 0-го элемента. Разберём такой пример:
struct TS {
int a;
int b;
int c[0];
};
struct TS *d = (struct TS *)malloc (sizeof (struct TS));
d->c[0] = 0;
В этом случае вы обращаетесь к нулевому элементу, которого нет, т. к. элементов ноль. И вы можете схватить сегфолт.
Но бывает, что после структуры в памяти лежат некие инты, и нам как раз нужно уметь к ним обращаться (как в вашем примере). Вот тогда такой хак с полем в виде массива длины ноль действительно используют.
Есть ли какой другой способ описать int c[0]; в качестве указателя на массив целых в структуре TS?
c — это не указатель на массив целых в структуре TS. Это массив нулевой длины в конце TS. Который неявно конвертится в указатель на массив целых, идущих после TS. Если вам нужно именно это, то это действительно единственный способ, если не считать некоторых специальных хаков компилятора. Если же вам просто нужно иметь указатель на массив, то способов полно. Просто делайте int a[4]; int *b = a
, ну или int *a = (int *)malloc (sizeof (int))
.
Лучше поздно, чем никогда :) Осмелюсь вам возразить:
«Указатель на массив». Строго говоря, «указатель на массив» — это именно указатель на массив и ничто другое. Иными словами:
int (*a)[2]; // Это указатель на массив. Самый настоящий. Он имеет тип int (*TYPE)[2]
Интересно, почему же "указатель на массив" не может указывать на массив?
int (*a)[2];
int array[2] = {0};
a = array;
jdoodle.c:7:7: warning: assignment to ‘int (*)[2]’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]
7 | a = array;
| ^
Окей, проигнорируем варнинг, попробуем обратиться по "указателю на массив":
a[0] = 1;
error: assignment to expression with array type
Почему же? Потому что int (*a)[2]
— это не "указатель на массив", а указатель на двумерный массив, у которого вторая размерность равна 2. Поэтому по такому указателю можно будет обратиться через двойные квадратные скобки и компилятор проведет вычисление смещения до элемента, так же, как сделал бы это для обычного двумерного массива.
А int (*a)[2][3]
был бы указателем на трехмерный массив соответствующих размерностей.
Но никак не просто "указателем на массив"!
int b[2]; int *c = b; // Это не указатель на массив. Это просто указатель. Указатель на первый элемент некоего массива int *d = new int[4]; // И это не указатель на массив. Это указатель
Безусловно, int * a — это просто указатель. Но он может указывать и на массив (на первый элемент массива), именно поэтому к указателю можно применять оператор квадратные скобки. Как будто это одномерный массив.
Соответственно "неформально" говорят "указатель на массив" всего лишь имея в виду, что это указатель на одномерный массив.
Разумеется, int *
может указывать и не на массив вовсе, а на отдельную переменную, но это на уровне языка неразличимо. Соответственно фраза "указатель на массив" дополнительно подчеркивает, что по указателю лежит именно массив.
Интересно, почему же «указатель на массив» не может указывать на массив?
a = array;
Потому что указатель на тип иницициализируется не объектом соответствующего типа, а адресом этого объекта.
Пишите правильно:
a = &array;
Окей, проигнорируем варнинг, попробуем обратиться по «указателю на массив»:
a[0] = 1;
Чтобы обратиться к массиву array, сначала надо разыменовать указатель, для этого используется синтаксис *a:
(*a)[0] = 1;
Хмм. Резонно.
Тем не менее, array "распадается" в обычный указатель int *
— и таким указателем можно пользоваться так же, как самим массивом, без разыменования.
Двумерный массив распадется в указатель int (*a)[2]
— и таким указателем, опять же, можно пользоваться как самим массивом.
У меня не такой уж большой опыт программирования, но я еще никогда не видел, чтобы чей-то код ожидал одномерный массив по указателю типа int (*a)[2]
; все массивы везде передаются как пара int *
и размер.
Соответственно моя основная претензия все-таки к называнию int (*a)[2]
"указателем на массив" (хотя пример в вашем комментарии логичен); поскольку это противоречит сложившейся практике и только сильнее запутывает.
К тому же непонятно как тогда называть int (*a)[2][2]
?
К тому же непонятно как тогда называть int (*a)[2][2]?Указатель на 2-мерный массив int[2][2]
И вас не смущает, что двумерный массив в таком случае будет распадаться в "указатель на массив", а трехмерный массив — в "указатель на двумерный"?
Такс, на самом деле прежде чем спорить, мне стоило бы почитать стандарт.
ISO/IEC 9899:201x, пункт 6.5.2.1 Array subscripting:
Successive subscript operators designate an element of a multidimensional array object.
If E is an n-dimensional array (n ≥ 2) with dimensions i × j ×... × k, then E (used as
other than an lvalue) is converted to a pointer to an (n − 1)-dimensional array with
dimensions j ×... × k.
Так что да, признаю, был не прав.
Но пока что не могу изменить своего мнения насчет практического использования указателей как массивов, о чем писал в предыдущем комментарии. Так что по-прежнему нахожу противопоставление указателя и "указателя на массив" запутывающим.
Если же предметная область предполагает фиксированные размеры массивов (такие, как матрицы 4x4 в 3-мерной графике), такой матрице заводится имя типа, например matrix_t, и там нет путаницы с индексами массивов таких матриц и элементов внутри матрицы. matrix_t* — указатель на матрицу, или на начало массива матриц, нет никаких лишних скобочек.
Справедливо, но я скорее имел в виду что одномерные массивы не передают по "указателю на массив", а просто парой "обычный указатель + размер". Поэтому-то и возникает путаница из-за такого названия — что "указатель на массив" не используют… чтобы указывать на одномерный массив.
Насколько я знаю, в С99 уже можно задавать размеры в рантайме, в т.ч. для многомерных (формально это вроде как считается VLA и нужно соблюдать порядок аргументов):
void foo( size_t n, int (*a)[n])
{
a[1][1] = 1;
}
int main(void) {
void * array = malloc( 8*9*sizeof(int) );
foo( 9, array );
}
Но полагаю, что все уже успели привыкнуть делать иначе.
void foo( size_t n, int (*a)[n])Это что за синтаксис? У меня такое не компилируется
https://godbolt.org/z/M9shEz — потому что это валидно только в С99, не в С++
Буду обозначать типы так, как выглядело бы объявление переменной TYPE соответствующего типа. Например, тип «массив длины 2 int'ов» я буду обозначать как int TYPE[2];
Я буду предполагать, что мы в основном имеем дело с обычными типами данных, такими как int TYPE, int *TYPE
Вот это специально так делать чтоб людей запутать? Давать переменной имя TYPE - какую еще дичь можно было придумать?
Указатели, ссылки и массивы в C и C++: точки над i