Почему компиляторы C и С++ допускают длину массива в сигнатурах функций, когда они никогда не применяются?

Это то, что я нашел в течение моего учебного периода:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

Итак, в переменной int dis(char a[1]) [1], кажется, ничего не делает и не работает на
все, потому что я могу использовать a[2]. Также как int a[] или char *a. Я знаю, что имя массива - это указатель и как передать массив, поэтому моя головоломка не касается этой части.

Я хочу знать, почему компиляторы допускают такое поведение (int a[1]). Или у него есть другие значения, о которых я не знаю?

Ответ 1

Это причуда синтаксиса для передачи массивов в функции.

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

Поскольку указатель не содержит никакой информации о длине, содержимое вашего [] в списке формальных параметров функции фактически игнорируется.

Решение разрешить этот синтаксис было сделано в 1970-х годах и вызвало много путаницы с тех пор...

Ответ 2

Длина первого измерения игнорируется, но длина дополнительных измерений необходима, чтобы компилятор мог правильно вычислить смещения. В следующем примере функции foo передается указатель на двумерный массив.

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

Размер первого измерения [10] игнорируется; компилятор не помешает вам индексировать с конца (обратите внимание, что формальный хочет 10 элементов, но фактический дает только 2). Однако размер второго измерения [20] используется для определения шага каждой строки, и здесь формальная форма должна соответствовать фактическому. Опять же, компилятор не помешает вам индексировать и конец второго измерения.

Байт, смещенный от основания массива к элементу args[row][col], определяется:

sizeof(int)*(col + 20*row)

Обратите внимание, что если col >= 20, вы фактически индексируете следующую строку (или вне конца всего массива).

sizeof(args[0]), возвращает 80 на моей машине, где sizeof(int) == 4. Однако, если я попытаюсь принять sizeof(args), я получаю следующее предупреждение компилятора:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

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

Ответ 3

Проблема и как ее преодолеть в С++

Проблема была подробно объяснена pat и Matt. Компилятор в основном игнорирует первое измерение размера массива, эффективно игнорируя размер переданного аргумента.

В С++, с другой стороны, вы можете легко преодолеть это ограничение двумя способами:

  • используя ссылки
  • с помощью std::array (начиная с С++ 11)

Ссылки

Если ваша функция пытается только прочитать или изменить существующий массив (не копируя его), вы можете легко использовать ссылки.

Например, предположим, что вы хотите иметь функцию, которая сбрасывает массив из десяти int, устанавливая каждый элемент в 0. Вы можете легко сделать это, используя следующую сигнатуру функции:

void reset(int (&array)[10]) { ... }

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

Вы также можете использовать шаблоны, чтобы сделать приведенный выше код общий:

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

И, наконец, вы можете использовать правильность const. Рассмотрим функцию, которая печатает массив из 10 элементов:

void show(const int (&array)[10]) { ... }

Применяя квалификатор const, мы предотвращаем возможные модификации.


Стандартный класс библиотеки для массивов

Если вы считаете приведенный выше синтаксис как уродливым, так и ненужным, как я, мы можем бросить его в банку и использовать std::array вместо (с С++ 11).

Здесь реорганизованный код:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

Разве это не чудесно? Не говоря уже о том, что общий трюк кода, который я вам преподавал раньше, все еще работает:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Не только это, но вы получаете копию и перемещаете семантику бесплатно.:)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

Итак, чего же вы ждете? Используйте std::array.

Ответ 4

Это интересная функция C, которая позволяет эффективно стрелять в ногу, если вы так склонны.

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

Также, назначение размера аргументу функции имеет то преимущество, что, когда функция используется другим программистом, есть шанс, что они заметят ограничение размера. Просто использование указателя не передает эту информацию следующему программисту.

Ответ 5

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

Во-вторых, есть трюк, который позволяет передавать значение по значению массива функции. Также возможно возвратить по значению массив из функции. Вам просто нужно создать новый тип данных, используя struct. Например:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Вам нужно получить доступ к следующим элементам: foo.a [1]. Дополнительный ".a" может показаться странным, но этот трюк добавляет большую функциональность для языка C.

Ответ 6

Чтобы сообщить компилятору, что myArray указывает на массив не менее 10 ints:

void bar(int myArray[static 10])

Хороший компилятор должен дать вам предупреждение, если вы обращаетесь к myArray [10]. Без "статического" ключевого слова значение 10 не будет означать ничего.

Ответ 7

Это хорошо известная "функция" C, переданная в С++, потому что С++ должен правильно компилировать C-код.

Проблема возникает из нескольких аспектов:

  • Имя массива должно быть полностью эквивалентно указателю.
  • C должен быть быстрым, изначально разработчиком быть своего рода "высокоуровневый ассемблер" (специально разработанный для записи первой "портативной операционной системы": Unix), поэтому он не должен вставлять "скрытый" код; Таким образом, проверка диапазона времени выполнения "запрещена".
  • Машинный код, созданный для доступа к статическому массиву или динамическому (либо в стеке, либо выделенном), фактически отличается.
  • Так как вызываемая функция не может знать "вид" массива, переданного как аргумент, все должно быть указателем и рассматриваться как таковое.

Вы можете сказать, что массивы не поддерживаются в C (это не так, как я говорил раньше, но это хорошее приближение); массив действительно рассматривается как указатель на блок данных и доступен с использованием арифметики указателя. Поскольку C не имеет какой-либо формы RTTI, вы должны объявить размер элемента массива в прототипе функции (для поддержки арифметики указателя). Это даже "более верно" для многомерных массивов.

В любом случае все вышеизложенное больше не так: p

Большинство современных компиляторов C/С++ поддерживают проверку границ, но стандарты требуют, чтобы она была отключена по умолчанию (для обратной совместимости). Разумно последние версии gcc, например, проводят проверку диапазона времени компиляции с помощью "-O3 -Wall -Wextra" и проверки полных временных рядов с помощью "-fbounds-check".

Ответ 8

C не только преобразует параметр типа int[5] в *int; с учетом объявления typedef int intArray5[5]; он преобразует параметр типа intArray5 в *int. Есть ситуации, когда это поведение, хотя и нечетное, полезно (особенно с такими вещами, как va_list, определенными в stdargs.h, которые некоторые реализации определяют как массив). Было бы нелогичным разрешать в качестве параметра тип, определенный как int[5] (игнорируя размерность), но не позволяющий напрямую указывать int[5].

Я нахожу, что C обработка параметров типа массива абсурдна, но это следствие усилий по использованию специального языка, значительная часть которого не была особенно четко определенной или продуманной и пыталась прийти с поведенческими спецификациями, которые согласуются с существующими реализациями для существующих программ. Многие из причуд С имеют смысл, если смотреть на этот свет, особенно если учесть, что, когда многие из них были изобретены, большая часть языка, который мы знаем сегодня, еще не существует. Из того, что я понимаю, в предшественнике C, называемом BCPL, компиляторы действительно не очень хорошо отслеживали типы переменных. Объявление int arr[5]; эквивалентно int anonymousAllocation[5],*arr = anonymousAllocation;; как только распределение было отменено. компилятор не знал и не заботился о том, был ли arr указателем или массивом. При доступе как arr[x] или *arr он будет считаться указателем независимо от того, как он был объявлен.

Ответ 9

Одна вещь, на которую еще не ответили, является актуальным вопросом.

В приведенных ответах объясняется, что массивы не могут передаваться по значению функции в C или С++. Они также объясняют, что параметр, объявленный как int[], обрабатывается так, как если бы он имел тип int *, и что переменная типа int[] может быть передана такой функции.

Но они не объясняют, почему никогда не была сделана ошибка, чтобы явно предоставить длину массива.

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

Почему не последняя из этих ошибок?

Причина в том, что он вызывает проблемы с typedefs.

typedef int myarray[10];
void f(myarray array);

Если бы ошибка указала длину массива в параметрах функции, вы бы не смогли использовать имя myarray в параметре функции. И поскольку в некоторых реализациях используются типы массивов для стандартных типов библиотек, таких как va_list, и для всех типов реализаций требуется тип jmp_buf типа массива, было бы очень проблематично, если бы не было стандартного способа объявления параметров функции с использованием этих имен: без этой способности не может быть переносимой реализации таких функций, как vprintf.