Указатели, ссылки и массивы в C и C++: точки над i

  • Tutorial
В этом посте я постараюсь окончательно разобрать такие тонкие понятия в C и C++, как указатели, ссылки и массивы. В частности, я отвечу на вопрос, так являются массивы C указателями или нет.

Обозначения и предположения


  • Я буду предполагать, что читатель понимает, что, например, в C++ есть ссылки, а в C — нет, поэтому я не буду постоянно напоминать, о каком именно языке (C/C++ или именно C++) я сейчас говорю, читатель поймёт это из контекста;
  • Также, я предполагаю, что читатель уже знает C и C++ на базовом уровне и знает, к примеру, синтаксис объявления ссылки. В этом посте я буду заниматься именно дотошным разбором мелочей;
  • Буду обозначать типы так, как выглядело бы объявление переменной TYPE соответствующего типа. Например, тип «массив длины 2 int'ов» я буду обозначать как int TYPE[2];
  • Я буду предполагать, что мы в основном имеем дело с обычными типами данных, такими как int TYPE, int *TYPE и т. д., для которых операции =, &, * и другие не переопределены и обозначают обычные вещи;
  • «Объект» всегда будет означать «всё, что не ссылка», а не «экземпляр класса»;
  • Везде, за исключением специально оговоренных случаев, подразумеваются C89 и C++98.


Указатели и ссылки


Указатели. Что такое указатели, я рассказывать не буду. :) Будем считать, что вы это знаете. Напомню лишь следующие вещи (все примеры кода предполагаются находящимися внутри какой-нибудь функции, например, main):

int x;
int *y = &x; // От любой переменной можно взять адрес при помощи операции взятия адреса "&". Эта операция возвращает указатель
int z = *y; // Указатель можно разыменовать при помощи операции разыменовывания "*". Это операция возвращает тот объект, на который указывает указатель


Также напомню следующее: char — это всегда ровно один байт и во всех стандартах C и C++ sizeof (char) == 1 (но при этом стандарты не гарантируют, что в байте содержится именно 8 бит :)). Далее, если прибавить к указателю на какой-нибудь тип T число, то реальное численное значение этого указателя увеличится на это число, умноженное на sizeof (T). Т. е. если p имеет тип T *TYPE, то p + 3 эквивалентно (T *)((char *)p + 3 * sizeof (T)). Аналогичные соображения относятся и к вычитанию.

Ссылки. Теперь по поводу ссылок. Ссылки — это то же самое, что и указатели, но с другим синтаксисом и некоторыми другими важными отличиями, о которых речь пойдёт дальше. Следующий код ничем не отличается от предыдущего, за исключением того, что в нём фигурируют ссылки вместо указателей:
int x;
int &y = x;
int z = y;


Если слева от знака присваивания стоит ссылка, то нет никакого способа понять, хотим мы присвоить самой ссылке или объекту, на который она ссылается. Поэтому такое присваивание всегда присваивает объекту, а не ссылке. Но это не относится к инициализации ссылки: инициализируется, разумеется, сама ссылка. Поэтому после инициализации ссылки нет никакого способа изменить её саму, т. е. ссылка всегда постоянна (но не её объект).

Lvalue. Те выражения, которым можно присваивать, называются lvalue в C, C++ и многих других языках (это сокращение от «left value», т. е. слева от знака равенства). Остальные выражения называются rvalue. Имена переменных очевидным образом являются lvalue, но не только они. Выражения a[i + 2], some_struct.some_field, *ptr, *(ptr + 3) — тоже lvalue.

Удивительный факт состоит в том, что ссылки и lvalue — это в каком-то смысле одно и то же. Давайте порассуждаем. Что такое lvalue? Это нечто, чему можно присвоить. Т. е. это некое фиксированное место в памяти, куда можно что-то положить. Т. е. адрес. Т. е. указатель или ссылка (как мы уже знаем, указатели и ссылки — это два синтаксически разных способа в C++ выразить понятие адреса). Причём скорее ссылка, чем указатель, т. к. ссылку можно поместить слева от знака равенства и это будет означать присваивание объекту, на который указывает ссылка. Значит, lvalue — это ссылка.

А что такое ссылка? Это один из синтаксисов для адреса, т. е., опять-таки, чего-то, куда можно класть. И ссылку можно ставить слева от знака равенства. Значит, ссылка — это lvalue.

Окей, но ведь (почти любая) переменная тоже может быть слева от знака равенства. Значит, (такая) переменная — ссылка? Почти. Выражение, представляющее собой переменную — ссылка.

Иными словами, допустим, мы объявили int x. Теперь x — это переменная типа int TYPE и никакого другого. Это int и всё тут. Но если я теперь пишу x + 2 или x = 3, то в этих выражениях подвыражение x имеет тип int &TYPE. Потому что иначе этот x ничем не отличался бы от, скажем, 10, и ему (как и десятке) нельзя было бы ничего присвоить.

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

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

Принцип «любое lvalue — ссылка» — тоже моя выдумка. А вот принцип «любая ссылка — lvalue» — вполне законный, общепризнанный принцип (разумеется, ссылка должна быть ссылкой на изменяемый объект, и этот объект должен допускать присваивание).

Теперь, с учётом наших соглашений, сформулируем строго правила работы со ссылками: если объявлено, скажем, int x, то теперь выражение x имеет тип int &TYPE. Если теперь это выражение (или любое другое выражение типа ссылка) стоит слева от знака равенства, то оно используется именно как ссылка, практически во всех остальных случаях (например, в ситуации x + 2) x автоматически конвертируется в тип int TYPE (ещё одной операцией, рядом с которой ссылка не конвертируется в свой объект, является &, как мы увидим далее). Слева от знака равенства может стоять только ссылка. Инициализировать (неконстантную) ссылку может только ссылка.

Операции * и &. Наши соглашения позволяют по-новому взглянуть на операции * и &. Теперь становится понятно следующее: операция * может применяться только к указателю (конкретно это было всегда известно) и она возвращает ссылку на тот же тип. & применяется всегда к ссылке и возвращает указатель того же типа. Таким образом, * и & превращают указатели и ссылки друг в друга. Т. е. по сути они вообще ничего не делают и лишь заменяют сущности одного синтаксиса на сущности другого! Таким образом, & вообще-то не совсем правильно называть операцией взятия адреса: она может быть применена лишь к уже существующему адресу, просто она меняет синтаксическое воплощение этого адреса.

Замечу, что указатели и ссылки объявляются как int *x и int &x. Таким образом, принцип «объявление подсказывает использование» лишний раз подтверждается: объявление указателя напоминает, как превратить его в ссылку, а объявление ссылки — наоборот.

Также замечу, что &*EXPR (здесь EXPR — это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR — указатель), а *&EXPR тоже эквивалентно EXPR всегда, когда имеет смысл (т. е. когда EXPR — ссылка).

Массивы


Итак, есть такой тип данных — массив. Определяются массивы, например, так:
int x[5];

Выражение в квадратных скобках должно быть непременно константой времени компиляции в C89 и C++98. При этом в квадратных скобках должно стоять число, пустые квадратные скобки не допускаются.

Подобно тому, как все локальные переменные (напомню, мы предполагаем, что все примеры кода находятся внутри функций) находятся на стеке, массивы тоже находятся на стеке. Т. е. приведённый код привёл к выделению прямо на стеке огромного блока памяти размером 5 * sizeof (int), в котором целиком размещается наш массив. Не нужно думать, что этот код объявил некий указатель, который указывает на память, размещённую где-то там далеко, в куче. Нет, мы объявили массив, самый настоящий. Здесь, на стеке.

Чему будет равно sizeof (x)? Разумеется, оно будет равно размеру нашего массива, т. е. 5 * sizeof (int). Если мы пишем
struct foo
{
  int a[5];
  int b;
};

то, опять-таки, место для массива будет целиком выделяться прямо внутри структуры, и sizeof от этой структуры будет это подтверждать.

От массива можно взять адрес (&x), и это будет самый настоящий указатель на то место, где этот массив расположен. Тип у выражения &x, как легко понять, будет int (*TYPE)[5]. В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &x и &(x[0]) численно равны (тут я лихо написал выражение &(x[0]), на самом деле в нём не всё так просто, к этому мы ещё вернёмся). Но эти выражения имеют разный тип — int (*TYPE)[5] и int *TYPE, поэтому сравнить их при помощи == не получится. Но можно применить трюк с void *: следующее выражение будет истинным: (void *)&x == (void *)&(x[0]).

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

Итак, мы объявили int x[5]. Если мы теперь пишем x + 0, то это преобразует наш x (который имел тип int TYPE[5], или, более точно, int (&TYPE)[5]) в &(x[0]), т. е. в указатель на нулевой элемент массива x. Теперь наш x имеет тип int *TYPE.

Конвертирование имени массива в void * или применение к нему == тоже приводит к предварительному преобразованию этого имени в указатель на первый элемент, поэтому:
&x == x // ошибка компиляции, разные типы: int (*TYPE)[5] и int *TYPE
(void *)&x == (void *)x // истина
x == x + 0 // истина
x == &(x[0]) // истина


Операция []. Запись a[b] всегда эквивалентна *(a + b) (напомню, что мы не рассматриваем переопределения operator[] и других операций). Таким образом, запись x[2] означает следующее:
  • x[2] эквивалентно *(x + 2)
  • x + 2 относится к тем операциям, при которых имя массива преобразуется в указатель на его первый элемент, поэтому это происходит
  • Далее, в соответствии с моими объяснениями выше, x + 2 эквивалентно (int *)((char *)x + 2 * sizeof (int)), т. е. x + 2 означает «сдвинуть указатель x на два int'а»
  • Наконец, от результата берётся операция разыменования и мы извлекаем тот объект, который размещён по этому сдвинутому указателю


Типы у участвовавших выражений следующие:
x // int (&TYPE)[5], после преобразования типа: int *TYPE
x + 2 // int *TYPE
*(x + 2) // int &TYPE
x[2] // int &TYPE


Также замечу, что слева от квадратных скобок необязательно должен стоять именно массив, там может быть любой указатель. Например, можно написать (x + 2)[3], и это будет эквивалентно x[5]. Ещё замечу, что *a и a[0] всегда эквивалентны, как в случае, когда a — массив, так и когда a — указатель.

Теперь, как я и обещал, я возвращаюсь к &(x[0]). Теперь ясно, что в этом выражении сперва x преобразуется в указатель, затем к этому указателю в соответствии с вышеприведённым алгоритмом применяется [0] и в результате получается значение типа int &TYPE, и наконец, при помощи & оно преобразуется к типу int *TYPE. Поэтому, объяснять при помощи этого сложного выражения (внутри которого уже выполняется преобразование массива к указателю) немного более простое понятие преобразования массива к указателю — это был немного мухлёж.

А теперь вопрос на засыпку: что такое &x + 1? Что ж, &x — это указатель на весь массив целиком, + 1 приводит к шагу на весь этот массив. Т. е. &x + 1 — это (int (*)[5])((char *)&x + sizeof (int [5])), т. е. (int (*)[5])((char *)&x + 5 * sizeof (int)) (здесь int (*)[5] — это int (*TYPE)[5]). Итак, &x + 1 численно равно x + 5, а не x + 1, как можно было бы подумать. Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива. Также, заметим, что выражение *(&x + 1) == x + 5 истинно. Ещё его можно записать вот так: (&x)[1] == x + 5. Также будет истинным *((&x)[1]) == x[5], или, что тоже самое, (&x)[1][0] == x[5] (если мы, конечно, не схватим segmentation fault за попытку обращения за пределы нашей памяти :)).

Массив нельзя передать как аргумент в функцию. Если вы напишите int x[2] или int x[] в заголовке функции, то это будет эквивалентно int *x и в функцию всегда будет передаваться указатель (sizeof от переданной переменной будет таким, как у указателя). При этом размер массива, указанный в заголовке будет игнорироваться. Вы запросто можете указать в заголовке int x[2] и передать туда массив длины 3.

Однако, в C++ существует способ передать в функцию ссылку на массив:
void f (int (&x)[5])
{
  // sizeof (x) здесь равен 5 * sizeof (int)
}

int main (void)
{
  int x[5];
  f (x); // OK
  f (x + 0); // Нельзя
  int y[7];
  f (y); // Нельзя, не тот размер
}

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

И что самое интересное, эту передачу можно использовать так:
// Вычисляет длину массива
template <typename t, size_t n> size_t len (t (&a)[n])
{
  return n;
}

Похожим образом реализована функция std::end в C++11 для массивов.

«Указатель на массив». Строго говоря, «указатель на массив» — это именно указатель на массив и ничто другое. Иными словами:
int (*a)[2]; // Это указатель на массив. Самый настоящий. Он имеет тип int (*TYPE)[2]
int b[2];
int *c = b; // Это не указатель на массив. Это просто указатель. Указатель на первый элемент некоего массива
int *d = new int[4]; // И это не указатель на массив. Это указатель

Однако, иногда под фразой «указатель на массив» неформально понимают указатель на область памяти, в которой размещён массив, даже если тип у этого указателя неподходящий. В соответствии с таким неформальным пониманием c и d (и b + 0) — это указатели на массивы.

Многомерные массивы. Если объявлено int x[5][7], то x — это не массив длины 5 неких указателей, указывающих куда-то далеко. Нет, x теперь — это единый монолитный блок размером 5 x 7, размещённый на стеке. sizeof (x) равен 5 * 7 * sizeof (int). Элементы располагаются в памяти так: x[0][0], x[0][1], x[0][2], x[0][3], x[0][4], x[0][5], x[0][6], x[1][0] и так далее. Когда мы пишем x[0][0], события развиваются так:
x // int (&TYPE)[5][7], после преобразования: int (*TYPE)[7]
x[0] // int (&TYPE)[7], после преобразования: int *TYPE
x[0][0] // int &TYPE

То же самое относится к **x. Замечу, что в выражениях, скажем, x[0][0] + 3 и **x + 3 в реальности извлечение из памяти происходит только один раз (несмотря на наличие двух звёздочек), в момент преобразования окончательной ссылки типа int &TYPE просто в int TYPE. Т. е. если бы мы взглянули на ассемблерный код, который генерируется из выражения **x + 3, мы бы в нём увидели, что операция извлечения данных из памяти выполняется там только один раз. **x + 3 можно ещё по-другому записать как *(int *)x + 3.

А теперь посмотрим на такую ситуацию:
int **y = new int *[5];

for (int i = 0; i != 5; ++i)
  {
    y[i] = new int[7];
  }


Что теперь есть y? y — это указатель на массив (в неформальном смысле!) указателей на массивы (опять-таки, в неформальном смысле). Нигде здесь не появляется единый блок размера 5 x 7, есть 5 блоков размера 7 * sizeof (int), которые могут находиться далеко друг от друга. Что есть y[0][0]?
y // int **&TYPE
y[0] // int *&TYPE
y[0][0] // int &TYPE

Теперь, когда мы пишем y[0][0] + 3, извлечение из памяти происходит два раза: извлечение из массива y и последующее извлечение из массива y[0], который может находиться далеко от массива y. Причина этого в том, что здесь не происходит преобразования имени массива в указатель на его первый элемент, в отличие от примера с многомерным массивом x. Поэтому **y + 3 здесь не эквивалентен *(int *)y + 3.

Объясню ещё разок. x[2][3] эквивалентно *(*(x + 2) + 3). И y[2][3] эквивалентно *(*(y + 2) + 3). Но в первом случае наша задача найти «третий элемент во втором ряду» в едином блоке размера 5 x 7 (разумеется, элементы нумеруются с нуля, поэтому этот третий элемент будет в некотором смысле четвёртым :)). Компилятор вычисляет, что на самом деле нужный элемент находится на 2 * 7 + 3-м месте в этом блоке и извлекает его. Т. е. x[2][3] здесь эквивалентно ((int *)x)[2 * 7 + 3], или, что то же самое, *((int *)x + 2 * 7 + 3). Во втором случае сперва извлекает 2-й элемент в массиве y, а затем 3-й элемент в полученном массиве.

В первом случае, когда мы делаем x + 2, мы сдвигаемся сразу на 2 * sizeof (int [7]), т. е. на 2 * 7 * sizeof (int). Во втором случае, y + 2 — это сдвиг на 2 * sizeof (int *).

В первом случае (void *)x и (void *)*x(void *)&x!) — это один и тот же указатель, во втором — это не так.
Share post

Comments 62

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

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

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

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

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

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

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

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

                      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), что выход адреса за элемент следующий за последним элементом массива может привести к неопределённому поведению, если случится переполнение.
                        –5
                        Объявлять массивы можно и с пустыми скобками

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

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

                        Не могу, я это понимаю на интуитивном уровне ^_^
                          +3
                          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.
                            0
                            поэтому адрес самого массива и адрес его нулевого элемента численно совпадают.

                            Вот именно это место меня интересовало.
                              0
                              Я имею в виду вот что: ни в одном стандарте я не вижу утверждения, что массив (как объект) не содержит ничего, кроме своих элементов. И я представляю себе странную реализацию, в которой массив содержит в начале какой-то мусор, потом свои элементы, потом ещё какой-то мусор. При конверсии имени массива в указатель мы по-прежнему легко возвращаем адрес первого элемента, но он не будет численно равен адресу массива.
                                +2
                                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 ]
                                  0
                                  Ок, С++98 5.3.3 говорит, что на мусор в массиве нет места. В стандарте С (я смотрю в С99, стандарта 89 года у меня нет под рукой) в определении sizeof нет такой оговорки.
                                    0
                                    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.
                              +5
                              Поскольку имею дела с разными компиляторами, то настоятельно не рекомендовал бы использовать практику «спорных» интерпретаций стандартов.
                              Плохая эта практика.
                              Пример: на одном из типов процов, попытка обратится к элементу массива char «обманув» компилятор путем манипуляции с указателем приведет к segmentation fault. Поскольку char там (C/C++) один байт (8-бит). Но вот минимальный размер адресации 32 бита.
                              Вообще полезно заглядывать в ASM логи, порожденные компилятором.

                              Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива.

                              Еще более плохая практика. Вероятность нарваться на границу защищенного сегмента памяти — просто.

                              Не надо переносить опыт работы в VisualStudio на весь мир.

                                +3
                                Еще более плохая практика. Вероятность нарваться на границу защищенного сегмента памяти — просто.

                                Здесь у автора все в порядке. Ссылаться за пределы массива можно и даже нужно. Итераторы конца последовательности (end) этим и занимаются. Разыменовывать нельзя, да, а указывать — на здоровье.
                                  +3
                                  Ссылаться можно только на «элемент, следующий за последним». Инкремент указателя за этот предел — U.B.
                                    0
                                    Спасибо. Для меня это оказалось откровением. Действительно, стандарт позволяет при адресной арифметике выходить за пределы массива только на 1 элемент (но не разыменовывать!). Далее одного элемента — UB.
                                    Век живи — век учись.
                                    А как насчет такой конструкции, будет ли это считаться UB?
                                    int x;
                                    std::accumulate(&x, &x + 1);
                                    

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

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

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

                                  Дело не в том, где расположен массив. Вот смотрите:
                                  int (*x)[7] = new int[5][7];
                                  

                                  Мы только что создали двумерный массив в куче. Но он ведёт себя похожим образом на двумерный массив на стеке, про который я говорил в посте. Т. е. он представляет собой единый блок размера 5 x 7, пусть даже в куче.
                                  потому лучше избегать использования двумерных статических массивов

                                  Чтоа? Совершенно неправильный вывод. Динамические массивы динамических массивов — вот чего нужно избегать, потому что они медленные. Использовать их нужно, когда ничего другого использовать не получается. А вот статические двумерные массивы — это то, что нужно использовать в первую очередь (если места на стеке хватает, конечно)
                                  +1
                                  // Вычисляет длину массива
                                  template <typename t, size_t n> size_t len (t (&a)[n])
                                  {
                                    // return len; 
                                    // опечатка, должно быть
                                    return n; 
                                  }
                                  

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

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

                                      Мы создали семь элементов типа int*, находящихся «непрерывно» друг за другом в стеке. Каждый такой элемент указывает на начало «непрерывного» массива из пяти элементов в куче. Поправьте меня, если не прав.
                                        +1
                                        Не правы. Мы создали 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])
                                          –1
                                          Я, видимо, плохо сформулировал мысль.
                                          Объявление типа:
                                          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];
                                          

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

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

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

                                                    В случае ссылки передается не значение, а сам объект. Что принципиально отличается от передачи простого int.

                                                    Не обязательно. Если я пишу int &ref = x, то, скорее всего, даже с выключенной оптимизацией, доступ к ref и x будет одинаково быстрым

                                                    В реальном коде не имеет смысла создавать второе название для переменной x. Весь смысл использования ссылок в том, что они могут инициироваться в рантайме — например как результат вызова функции или от указателя.

                                                    Суть моей реплики: int x — это не int& x! Хотя внешне во многих случаях ведут себя одинаково.
                                                      0
                                                      >> Я лишь указываю на то, что у x и ref как у выражений один и тот же тип — int &TYPE

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

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

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


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


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

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

                                                        Надо просто посмотреть, что такое array-to-pointer conversion.
                                                        0
                                                        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.

                                                        Именно свойство коммутативности позволяют производить такую запись.
                                                        0
                                                        Статья вызвала противоречивые чувства.

                                                        С одной стороны, приятно, что один из самых мощных языков (и мною любимых) изучают и популяризируют.

                                                        С другой стороны у меня какое-то ощущение, что Хабр не то, чтобы деградирует — вот комментарии все о подводных камнях и практиках действительно любопытные, сколько занимается перекопированием книг, скажем, Страуструпа или того же Тома Свана — уровня Turbo C++ родом из 90х. Вот уж правда, может лучше аггрегировать ссылки на хорошие, читаемые источники с указанием подводных камней и практики? Нежели пересказ базовых понятий своими словами?
                                                          0
                                                          Сколько автору лет и как долго он изучает C/C++?
                                                          Статья — набор капитанства. Всё это можно найти в книгах, без особого усилия. Просто эта тема тяжела для понимания новичкам, и такое впечатление, что один новичек наконец познал Дзен и решил об этом написать.

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

                                                            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?
                                                              0

                                                              Какой язык? 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)).

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

                                                          Only users with full accounts can post comments. Log in, please.