Почему компиляторы C и 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]
). Или у него есть другие значения, о которых я не знаю?
9 ответов
Это особенность синтаксиса для передачи массивов в функции.
На самом деле невозможно передать массив в C. Если вы пишете синтаксис, который выглядит так, как будто он должен проходить через массив, то на самом деле происходит то, что вместо этого передается указатель на первый элемент массива.
Поскольку указатель не содержит никакой информации о длине, содержимое вашего []
в списке формальных параметров функции фактически игнорируются.
Решение разрешить этот синтаксис было принято в 1970-х годах и с тех пор вызывает много путаницы...
Длина первого измерения игнорируется, но длина дополнительных измерений необходима, чтобы компилятор мог правильно вычислять смещения. В следующем примере 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.
Здесь компилятор предупреждает, что он собирается дать только размер указателя, в который распался массив, а не размер самого массива.
Проблема и как ее преодолеть в C++
Проблема была подробно объяснена Пэт и Мэттом. Компилятор в основном игнорирует первое измерение размера массива, фактически игнорируя размер переданного аргумента.
В C++, с другой стороны, вы можете легко преодолеть это ограничение двумя способами:
- используя ссылки
- с помощью
std::array
(начиная с C++11)
Рекомендации
Если ваша функция только пытается прочитать или изменить существующий массив (не копируя его), вы можете легко использовать ссылки.
Например, давайте предположим, что вы хотите иметь функцию, которая сбрасывает массив из десяти int
s устанавливает каждый элемент в 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
вместо этого (начиная с C++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
,
Это забавная функция C, которая позволяет вам эффективно стрелять себе в ногу, если вы так склонны.
Я думаю, что причина в том, что C - просто шаг выше языка ассемблера. Проверка размера и аналогичные функции безопасности были удалены, чтобы обеспечить максимальную производительность, что неплохо, если программист очень старателен.
Кроме того, назначение размера аргументу функции имеет то преимущество, что когда функция используется другим программистом, есть вероятность, что они заметят ограничение размера. Простое использование указателя не передает эту информацию следующему программисту.
Во-первых, 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, переданная C++, потому что C++ должен корректно компилировать код C.
Проблема возникает из нескольких аспектов:
- Предполагается, что имя массива полностью эквивалентно указателю.
- Предполагается, что C быстрый, изначально developerd был своего рода "высокоуровневым ассемблером" (специально предназначенным для написания первой "портативной операционной системы": Unix), поэтому он не должен вставлять "скрытый" код; проверка диапазона времени выполнения, таким образом, "запрещена".
- Машинный код, сгенерированный для доступа к статическому или динамическому массиву (либо в стеке, либо в выделенном), на самом деле отличается.
- Поскольку вызываемая функция не может знать "вид" массива, переданного в качестве аргумента, все должны быть указателями и рассматриваться как таковые.
Можно сказать, что массивы в C не поддерживаются (это не совсем так, как я говорил ранее, но это хорошее приближение); массив действительно рассматривается как указатель на блок данных и доступ к нему осуществляется с использованием арифметики указателей. Поскольку C НЕ имеет никакой формы RTTI, вы должны объявить размер элемента массива в прототипе функции (для поддержки арифметики указателей). Это даже "более верно" для многомерных массивов.
В любом случае, все вышесказанное уже не так:p
Большинство современных компиляторов C/C++ поддерживают проверку границ, но стандарты требуют, чтобы она была отключена по умолчанию (для обратной совместимости). Например, в последних версиях gcc проверка диапазона во время компиляции выполняется с помощью "-O3 -Wall -Wextra", а полная проверка границ во время выполнения - с помощью "-fbounds-check".
Чтобы сообщить компилятору, что myArray указывает на массив не менее 10 дюймов:
void bar(int myArray[static 10])
Хороший компилятор должен выдавать вам предупреждение, если вы обращаетесь к myArray [10]. Без ключевого слова "static" число 10 ничего бы не значило.
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
, он будет рассматриваться как указатель независимо от того, как он был объявлен.
Одна вещь, на которую еще не ответили, является фактическим вопросом.
Ответы, которые уже даны, объясняют, что массивы не могут быть переданы по значению функции в C или 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
,
Компиляторам разрешено проверять, соответствует ли размер переданного массива ожидаемому. Компиляторы могут предупредить о проблеме, если это не так.