Открыть список
Как стать автором
Обновить

Комментарии 72

Инициализировать ссылку может только ссылка.
Рассмотрим такой код:

#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++ такой сложный язык, всё-таки!). В данном случае считаете ли вы этот временным объект ссылкой или считаете этот случай исключением из тезиса «Инициализировать ссылку может только ссылка»?
Точно так же, кстати, rvalue reference может быть привязана у rvalue object or subobject thereof, тоже продлевая его время жизни до своей области объявления.
Спасибо, уточнил: «Инициализировать (неконстантную) ссылку может только ссылка». Я считаю этот объект не-ссылкой
Опа, я даже не сразу въехал. Нельзя ведь брать ссылки на временные объекты, но здесь ссылка const, а const-ссылки, вроде как, можно. Но точно ли она продлевает время жизни объекта? Стандарт это гарантирует?
вроде как, можно

Да, можно. Точно также, как и передавать временные объекты в функции по ссылке-на-const можно.

Стандарт это гарантирует

Да, я читал про это тут: alenacpp.blogspot.ru/2008/01/const.html
Стандарт это гарантирует (§8.5.3/5).

Меня это поведение ссылок тоже шокировало. Думаю, это хорошая ловушка для собеседований, можно сравнить с ловушками для JavaScript.

Но эта странность имеет практическую пользу. Зная о ней, можно писать
const std::string& aaa = ...
и не задумываться о том, возвращают ли нам ссылку или временную переменную. Это позволяет писать код, который не придётся менять, если используемые в нём геттеры станут возвращать не ссылки, а временные переменные.
Всегда считал, что синтаксическая эквивалентность имени массива и адреса первого элемента массива это баг языка. Да, может быть в простейших случаях и меньше на 1 символ писать, но последствия для стройности языка в целом неприятные.
Это не баг, а legacy из С. Массив в С и С++ не объект первого класса, точно так же как и функция.
Этот принцип («выражение, являющееся переменной — ссылка») — моя выдумка.

Не совсем Ваша, посмотрите что такое value category.
Насколько я понимаю, с точки зрения стандарта, если я объявил int x, то x — это lvalue типа int, а не int &TYPE. Так что таки моя выдумка
Я бы не советовал так упрощать lvalues и сводить их к ссылкам (а равно и к присваиванию, это уже чисто сишная заморочка). Это все-таки не одно и тоже, и в стандарте эта разница имеет значение. Иначе потом будет путаница с временными объектами, operator=, и rvalue references.
У меня вот на этом месте в глазах зарябило: «Что ж, &x — это указатель на весь массив целиком, + 1 приводит к шагу на весь этот массив. Т. е. &x + 1 — это (int (*)[5])((char *)&x + sizeof (int [5])), т. е. (int (*)[5])((char *)&x + 5 * sizeof (int)) (int (*)[5] — это int (*TYPE)[5]).»
Далее, если прибавить к указателю на какой-нибудь тип 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), что выход адреса за элемент следующий за последним элементом массива может привести к неопределённому поведению, если случится переполнение.
Объявлять массивы можно и с пустыми скобками

Спс, исправил

Не могли бы вы показать место в стандарте, которое гарантирует это?

Не могу, я это понимаю на интуитивном уровне ^_^
jcmvbkbc
В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &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 ]
Ок, С++98 5.3.3 говорит, что на мусор в массиве нет места. В стандарте С (я смотрю в С99, стандарта 89 года у меня нет под рукой) в определении sizeof нет такой оговорки.
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) этим и занимаются. Разыменовывать нельзя, да, а указывать — на здоровье.
Ссылаться можно только на «элемент, следующий за последним». Инкремент указателя за этот предел — U.B.
Спасибо. Для меня это оказалось откровением. Действительно, стандарт позволяет при адресной арифметике выходить за пределы массива только на 1 элемент (но не разыменовывать!). Далее одного элемента — UB.
Век живи — век учись.
А как насчет такой конструкции, будет ли это считаться UB?
int x;
std::accumulate(&x, &x + 1);

Скорее всего, да. Про адресную арифметику с переполнением я в стандарте нашел сноску только насчет массивов. Но можно ли указатель на единичную переменную трактовать как массив из одного элемента?
Да, можно — в [expr.add]:
«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++ был длинный тред несколько лет назад, но в итоге консенсуса не было.
Действительно, в разделе 5.7 (4). Замечательно. В таком случае, у автора все в порядке с выходом за пределы. В выражении &x + 1 нет UB.
Много текста и страшных преобразований, которые сводятся к одной простой сути — массивы в стеке и на куче работают по разному. Если с одномерными массивами особо проблем нету и они легко преобразуются к указателю, то с многомерными надо помнить что «Массив массивов» и «указатель на указатель» — это разные сущности. Можно легко наступить на грабли если не знать этих особенностей и потому лучше избегать использования двумерных статических массивов.
Много текста

Я написал много текста, чтобы было понятнее.

массивы в стеке и на куче работают по разному

Дело не в том, где расположен массив. Вот смотрите:
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; 
}

Более корректный вариант:
template<typename T, size_t N> constexpr size_t len(T (&)[N]) noexcept {
    return N;
}
Везде, за исключением специально оговоренных случаев, подразумеваются C89 и C++98

В C++98 нет constexpr.
Мы только что создали двумерный массив в куче

Мы создали семь элементов типа int*, находящихся «непрерывно» друг за другом в стеке. Каждый такой элемент указывает на начало «непрерывного» массива из пяти элементов в куче. Поправьте меня, если не прав.
Не правы. Мы создали 5 x 7 элементов типа 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];

Всем, чем отличается данная реализация от выше описанной, это то, что в стеке для массива хранится только одна переменная (адрес начала массива) и адресация выполняется на пару машинных инструкций дольше. Зато «не улетим» в переполнение стека.
int (*x)[7] объявляет указатель. Его размер на стеке равен размеру указателя для данной платформы и не зависит от того, на что он указывает.
sizeof(int (*)[7] ) == sizeof(int (*)[1000] )== sizeof(int*)
Да, int(*x)[7] — это один указатель. И адресация в последнем вашем примере выполняется не на пару инструкций дольше, а столько же времени (при условии, что N_COLS известно на этапе компиляции)
Также замечу, что &*EXPR (здесь EXPR — это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR — указатель
Не верно если тип *EXPR переопределяет оператор & странным образом.
Правильней так std::addressof(*EXPR);
Я написал в начале, что будем считать, что странных переопределений у нас нет
«окончательно разобрать» и «окончательно разобраться» — это разные задачи.
Я против того, чтобы на хабре появлялись статьи такого дилетантского качества с претензией «окончательно» что-то кому-то объяснить.

Да, стандарты С++ написаны очень трудным языком. Но они, вообще-то, написаны потом, кровью и слезами, и ничего лишнего там нет.
Поэтому такие упрощения, как «ссылка — это тот же указатель, только в профиль» — это способ запутать.
>> Я против того, чтобы на хабре появлялись статьи такого дилетантского качества с претензией «окончательно» что-то кому-то объяснить.
А я за то, чтобы появлялись.
И чтобы в комментариях к ним появлялись люди, готовые указать на неточности и серьезные нестыковки, а также рассказать как на самом деле, не ссылаясь на RTFM.
Имхо — это вполне легитимный и эффективный способ появления на свет популярного контента для тех, кому не настолько нужно, чтобы разбирать стандарты.
Последствие такой публикации состоит в том, что в комментариях разворачивается дискуссия с грозным постукиванием Стандартом по столу. Хорошо, если по итогам обсуждения выпускается еще одна статья с подробной систематизацией обсуждения вопроса и выводами, по типу этой.

А извлекать из комментариев информацию — дело тяжелое и неблагодарное. Особенно когда один и тот же вопрос поднимается «нечитателями» более одного раза, как уже устроили тут:
первый раз jcmvbkbc поднял тему мусора в массиве
второй раз уже mt_ интересуется фактически тем же
А вот такой тонкий момент, как эквивалентность указателя на массив и указателя на первый элемент массива. Встречал такое предостережение, что в некоторых компиляторах при сборке с проверками и отладочной информацией, в массиве сначала идёт несколько байт отладочной информации, а потом уже данные. В этом случае указатель на массив не совпадает с указателем на первый элемент.
Кто может уточнить, насколько это противоречит или не противоречит стандарту (С, С++)? И если стандарту не противоречит, то имеет смысл отучать себя вообще использовать указатель на массив в коде — только указатель на первый элемент.
Выше есть ветка, где это разобрано. Как минимум в C++, стандарт гарантирует, что в массиве элементы идут последовательно, и нет дырок и padding в начале и конце.
мы объявили 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! Хотя внешне во многих случаях ведут себя одинаково.
>> Я лишь указываю на то, что у x и ref как у выражений один и тот же тип — int &TYPE

typeid() с вами не согласен, кстати.

(в смысле, тип-то у них действительно один, но это не int&)
Хорошо, будем считать, я вас убедил, что массив — это именно массив, а не что-нибудь ещё. Откуда тогда берётся вся эта путаница между указателями и массивами? Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент.

#include <stdio.h>
int main() {
        char s[] = "Hello, world";
        printf("%c\n", 5[s]);
};


Если честно, я вообще не понял смысла Вашей статьи :-) Базовые (а не «тонкие» понятия), но зачем-то настолько усложненные…
именно этот пример я ожидал в статье после предложения
Запись a[b] всегда эквивалентна *(a + b)


При этом фраза Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент всё равно верна:
        char x[3] = { 'a','b','c' };
        assert(x[2] == 2[x]); // 'c'
        char* z = x;
        assert(z[2] == 2[z]);

имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент

Надо просто посмотреть, что такое array-to-pointer conversion.
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х. Вот уж правда, может лучше аггрегировать ссылки на хорошие, читаемые источники с указанием подводных камней и практики? Нежели пересказ базовых понятий своими словами?
Сколько автору лет и как долго он изучает C/C++?
Статья — набор капитанства. Всё это можно найти в книгах, без особого усилия. Просто эта тема тяжела для понимания новичкам, и такое впечатление, что один новичек наконец познал Дзен и решил об этом написать.

Никого не хотел обидеть, Просто жалко впустую потраченых нескольких минут для прочтения стьи, в которой ничего нового не нашел.
Просто мне показалось, что нигде нет нормального объяснения про то, что же такое массивы в C. Я сам до определённого момента думал, что имя массива вообще всегда ведёт себя как указатель, пока не заметил, что, оказывается, от массива можно взять адрес. (Мне 22, C и C++ изучаю 6,5 лет.)
Господа! Читайте книги внимательно. Там всё это есть. ))
Добрый день.
Есть такие две структуры

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)).

Язык С++.
Мне нужно именно так, как я показал в примере. Структура TS является заголовком некоторого буфера данных, а её элемент с[0] — это первый элемент собственно данных (a — тип, b — количество).
Спасибо за развёрнутый ответ.

Лучше поздно, чем никогда :) Осмелюсь вам возразить:


«Указатель на массив». Строго говоря, «указатель на массив» — это именно указатель на массив и ничто другое. Иными словами:
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 );
}

Но полагаю, что все уже успели привыкнуть делать иначе.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.