Каковы последствия передачи и назначения массивов в качестве указателей на С++?

В качестве фона я недавно дал ответ на этот пост:

Возвращать массив в функции

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

Этот вопрос не предназначен для приманки троллей, я понимаю, что указатель и массив - это не одно и то же, но некоторые из доступных синтаксисов на языке С++, безусловно, заставляют их вести себя очень одинаково во многих случаях. (FYI, мой компилятор i686-apple-darwin9-g++-4.0.1 on OS X 10.5.8)

Например, этот код компилируется и работает отлично для меня (я понимаю, что x[8] является потенциальной ошибкой сегментации):

  //this is just a simple pointer                                                                                                                                                            
  int *x = new int;
  cout << x << " " << (*x) << " " << x[8] << endl; //might segfault                                                                                                                          

  //this is a dynamic array                                                                                                                                                                  
  int* y = new int[10];
  cout << y << " " << (*y) << " " << y[8] << endl;

  //this is a static array                                                                                                                                                                   
  int z[10];
  cout << z << " " << (*z) << " " << z[8] << endl;

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

  x = y;
  x = z;
  y = x;
  y = z;
  //z = x; //won't compile
  //z = y; //won't compile

Таким образом, компилятор, по крайней мере, понимает, что z и x - это разные вещи, но я могу отлично обменивать x и y.

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

void foo(int in[])
{
  cout << in[8] << endl;                                                                                                                                                                                      
}

void bar(int* in)
{
  cout << in[8] << endl;                                                                                                                                                                     
}

int main()
{
  //this is just a simple pointer                                                                                                                                                            
  int *x = new int;
  foo(x);
  bar(x);

  //this is a dynamic array                                                                                                                                                                  
  int* y = new int[10];
  foo(y);
  bar(y);

  //this is a static array                                                                                                                                                                   
  int z[10];
  foo(z);
  bar(z);
}

Весь этот код правильно компилируется и запускается на моей машине.

Я чувствую, что у меня есть достойное внутреннее понимание того, что происходит здесь, но если вы попросили меня четко сформулировать, что происходит, я не чувствую, что могу удовлетворительно объяснить. Итак, вот что я получаю:

  • Когда я передаю массив функции int* in вместо int in[], что я получаю или теряю? То же самое верно при возврате массива как int*? Есть ли когда-либо плохие побочные эффекты от этого?

  • Если бы я спросил вас, что такое тип данных y, вы бы указали на int, массив ints или что-то еще?

  • Аналогично, что происходит, когда я говорю x = y vs. x = z? Я все еще могу использовать x[] и получить доступ к вещам, которые изначально были в y или z, но действительно ли это потому, что арифметика указателя приводит меня в пространство памяти, которое остается в силе?

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

Ответ 1

С++ статически типизирован, поэтому, конечно, компилятор понимает, что x и z - это не то же самое. Они имеют разные типы - z - массив, x и y - указатели.

Причина z = x не компилируется, не является (просто), что типы несовместимы, хотя это и не может быть присвоено переменной массива вообще. Когда-либо. x = z присваивает x, указатель на первый элемент z. x = y присваивает значение y x. [*]

Когда я передаю массив функции int * in вместо int в [], что я получаю или теряю?

Они делают то же самое, поэтому у вас нет выбора. Возможно, вы были введены в заблуждение из-за того, что синтаксис С++ разрешает int in[] как параметр функции. Тип параметра in не является массивом, это int*.

Если бы я спросил вас, какой тип данных y является

Это int*. Это то, что оно объявляло, так, чтобы оно было.

Значение, которое оно содержит, является указателем на (первый элемент) массива. Я часто использую эту формулу: "указатель на (первый элемент)" в тех случаях, когда я хотел бы сказать "указатель на массив", но не может, потому что существует вероятность двусмысленности относительно того, to-array, или нет.

Однако указатели на массивы редко используются в С++, потому что размер массива является частью типа. Там нет такого типа, как "указатель на массив int" в С++, просто "указатель на массив из 1 int", "указатель на массив из 2 int" и т.д. Это обычно не очень удобно, поэтому использование указатель на первый элемент массива, размер которого не может быть известен во время компиляции.

- это действительно просто потому, что арифметика указателя выгружает меня в пространство памяти, которое все еще действует

В значительной степени, да. Размер массива является частью типа z, но не является частью типа x или y, а также не является частью типа результата z, разлагающегося на указатель на его первый элемент. Таким образом, y может быть указателем на первый из 10 элементов или всего на один элемент. Вы знаете только разницу по контексту и требуя от ваших абонентов, чтобы значение, которое вы указали, указывало на то, что оно должно указывать на.

"Happens" оставляет слишком много шансов, однако - часть вашей работы при использовании массивов состоит в том, чтобы убедиться, что вы не отклоняетесь от своих границ.

[*] z = x не разрешается даже после того, как вы сделали x = z, потому что z является (и всегда будет) конкретным массивом из 10 ints в памяти. Назад, когда был разработан C, возник вопрос о том, могут ли переменные массива в принципе быть "reseatable", что означает, что вы могли бы сделать:

int z[10];
int y[10];
z = y; // z is now an alias for y
y[0] = 3;
// z[0] now has the value 3

Деннис Ритчи решил не допускать этого, потому что это помешало ему отличить массивы от указателей так, как ему нужно было делать. Поэтому z никогда не может ссылаться на другой массив из того, который был объявлен. Подробнее читайте здесь: http://cm.bell-labs.com/cm/cs/who/dmr/chist.html под "Embryonic C".

Другим правдоподобным значением для z = y может быть memcpy(z,y,sizeof(z)). Это также не было дано.

Ответ 2

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

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

Вот почему вы можете присвоить его значение другому указателю, но не наоборот. Нет места памяти указателя для обработки как l-значение.

Ответ 3

Массивы не являются указателями, но массивы легко распадаются на указатели на их первый элемент. Кроме того, C (и, следовательно, С++) позволяют использовать синтаксис доступа к сильному массиву, который будет использоваться для указателей.

Когда я передаю массив функции как int * in вместо int в [], что я набираю или теряю? То же самое верно при возврате массива как int *? Есть ли когда-либо плохие побочные эффекты от этого?

Вы ничего не получаете, потому что int[] - это еще один способ написать int*. Если вы хотите передать массив, вы должны передать его за ссылку, точно соответствующую ее размеру. Нестандартные аргументы шаблона могут облегчить проблему с точным размером:

template< std:::size_t N >
void f(int (&arr)[N])
{
   ...
}

Если бы я спросил, что такое тип данных y, вы бы указали на int, массив ints или что-то еще?

Это указатель на первый элемент динамически распределенного массива.

Аналогично, что происходит, когда я говорю x = y vs. x = z?

Вы назначаете адреса разных объектов разных типов одному и тому же указателю. (И вы пропустите int в куче. :))

Я все еще могу использовать x [] и получить доступ к вещам, которые были изначально в y или z, но действительно ли это потому, что арифметика указателя приводит меня в пространство памяти, которое остается в силе?

Угу. Как я уже сказал, указатели удобно и смутно позволяют применять синтаксис массива к ним. Тем не менее, это все еще не делает указатель массивом.

Ответ 4

Здесь приведен фрагмент из этой книги (и семантика С++ вытекает из ее обратной совместимости с C). Массив "являются" указателями в следующих случаях:

  • Имя массива в выражении (в отличие от объявления) обрабатывается компилятором как указатель на первый элемент массива (это не относится к sizeof) (ANSI C, 6.2.2.1)
  • Подстрочный индекс всегда эквивалентен смещению от указателя (6.3.2.1)
  • Имя массива в объявлении параметра функции обрабатывается компилятором как указатель на первый элемент массива (6.7.1)

В основном это означает, что:

int arr[20]; int* p = arr;

эквивалентно:

int arr[20]; int* p = &arr[0];

Тогда

int arr[20]; int x = arr[10];

эквивалентно:

int arr[20]; int x = *( arr + 10 );

и

void func( int arr[] );

эквивалентно:

void func( int* arr );

С другой стороны, указатели никогда не преобразуются обратно в массивы - поэтому ваши последние две строки не компилируются.

Ответ 5

Когда я передаю массив функции как int * in вместо int в [], что я набирать или проигрывать? То же самое верно при возврате массива как int *? Находятся когда-либо плохие побочные эффекты от это?

AFAIK, один - только синтаксический сахар для другого, и они означают точно то же самое.

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

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

Если бы я спросил вас, какой тип данных y есть, вы бы указали указатель на int, массив ints или что-то еще?

Тип y является указателем на int. На самом деле, в случае динамически выделенного массива, вы никогда не увидите массив вообще! То есть, нет способа определить размер выделения с sizeof, в отличие от реальных массивов.

Аналогично, что происходит, когда я говорю x = y против x = z? Я все еще могу использовать x [] и доступ к вещам, которые были первоначально в y или z, но это действительно только потому, что арифметика указателя происходит, чтобы посадить меня в пространстве памяти это все еще актуально?

Это потому, что x является указателем. Вы не сможете сделать z = x;, потому что вы не можете назначить массивы.

Ответ 6

  • Никакой разницы (вообще) между параметром функции, например int *in и int in[]. Для параметра функции это просто разные способы написания pointer to T. Единственный способ, которым они могут вообще различаться, - это (возможно) что-то вроде удобочитаемости (например, если вы намерены всегда передавать базовый адрес массива, вы можете найти нотацию массива более подходящую, тогда как если вы намерены передать адрес одного объект, вы можете найти нотацию указателя более со вкусом).
  • В приведенном выше коде y явно имеет тип pointer to int.
  • x и y - указатели, которые могут быть назначены. z - это массив, который нельзя назначить.

Ответ 7

Как в стороне (которая имеет некоторое отношение к теме - добавила бы это как комментарий, но не имела достаточного количества rep.) - вы можете оценить количество элементов в массиве или использовать указатель. Или указано иначе sizeof возвращает sizeof (array_type) * num_elements_in_array и возвращает размер указателя. Glib предоставляет этот макрос для этой цели.

Ответ 8

Когда я передаю массив функции как int * in вместо int в [], что я набираю или теряю? То же самое верно при возврате массива как int *? Есть ли когда-либо плохие побочные эффекты от этого?

Ваше отсутствие или потерять что-либо

Если бы я спросил, что такое тип данных y, вы бы указали на int, массив ints или что-то еще?

Я бы назвал y указателем на массив ints

Аналогично, что происходит, когда я говорю x = y vs. x = z? Я все еще могу использовать x [] и получить доступ к вещам, которые были изначально в y или z, но действительно ли это потому, что арифметика указателя позволяет мне помещать меня в пространство памяти, которое остается в силе?

x = y делает не сделать копию массива, на которую указывает y, только копия указателя в y.

x = z делает not копию массива z, только указатель на значение первого элемента.

Кроме того, свободная выделенная память.