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

131

Вот что я обнаружил в период обучения:

#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]). Или у него есть другие значения, о которых я не знаю?

Fanl
источник
6
Это потому, что вы не можете передавать массивы функциям.
Эд С.
37
Я думаю, что вопрос здесь заключался в том, почему C позволяет вам объявлять параметр как имеющий тип массива, когда он в любом случае будет вести себя точно так же, как указатель.
Брайан
8
@Brian: Я не уверен, является ли это аргументом за или против поведения, но это также применимо, если тип аргумента - typedefс типом массива. Так что «распад на указатель» в типах аргументов не просто синтаксический сахар заменить []с *, это действительно происходит через систему типа. Это имеет реальные последствия для некоторых стандартных типов, подобных тому, va_listкоторые могут быть определены с типом массива или не-массивом.
R .. GitHub ПРЕКРАТИТЕ ПОМОЩЬ ICE
4
@songyuanyao Вы можете сделать что - то не совсем разнородный в C (и C ++) , используя указатель: int dis(char (*a)[1]). Затем передать указатель на массив: dis(&b). Если вы хотите использовать функции C, которых нет в C ++, вы также можете говорить такие вещи, как void foo(int data[static 256])и int bar(double matrix[*][*]), но это совсем другая баня червей.
Стюарт Олсен
1
@StuartOlsen Дело не в том, какой стандарт что определил. Дело в том, почему тот, кто это дал, определил именно так.
user253751

Ответы:

156

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

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

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

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

М.М.
источник
21
Как программист, не использующий C, я считаю этот ответ очень доступным. +1
asteri
21
+1 за «Решение разрешить этот синтаксис было принято в 1970-х и с тех пор вызвало много путаницы ...»
NoSenseEtAl
8
это правда, но также можно передать массив такого размера, используя void foo(int (*somearray)[20])синтаксис. в этом случае на вызывающих сайтах принудительно устанавливается 20.
v.oddou
14
-1 Как программист на C, я считаю этот ответ неверным. []не игнорируются в многомерных массивах, как показано в ответе pat. Поэтому необходимо было включить синтаксис массива. Кроме того, ничто не мешает компилятору выдавать предупреждения даже для одномерных массивов.
user694733
7
Под «содержимым вашего []» я говорю конкретно о коде в Вопросе. В этой синтаксической особенности вообще не было необходимости, того же можно добиться, используя синтаксис указателя, то есть, если указатель передается, тогда требуется, чтобы параметр был декларатором указателя. Например, в примере pat: void foo(int (*args)[20]);Кроме того, строго говоря, C не имеет многомерных массивов; но у него есть массивы, элементами которых могут быть другие массивы. Это ничего не меняет.
MM
143

Длина первого измерения игнорируется, но длина дополнительных измерений необходима, чтобы компилятор мог правильно вычислить смещения. В следующем примере 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.

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

похлопывание
источник
Очень полезно - согласованность с этим также правдоподобна как причина странности в 1-м случае.
jwg
1
Это та же идея, что и в одномерном случае. То, что выглядит как двумерный массив в C и C ++, на самом деле является одномерным массивом, каждый элемент которого является еще одним одномерным массивом. В этом случае у нас есть массив из 10 элементов, каждый элемент которого представляет собой «массив из 20 целых чисел». Как описано в моем сообщении, на самом деле функции передается указатель на первый элемент args. В этом случае первый элемент args - это «массив из 20 целых чисел». Указатели включают информацию о типе; передается «указатель на массив из 20 целых чисел».
MM
9
Ага, вот какой int (*)[20]тип; "указатель на массив из 20 int".
Pat
33

Проблема и как ее решить в C ++

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

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

  • используя ссылки
  • используя std::array(начиная с C ++ 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вместо него (начиная с 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.

башмак
источник
2
@kietz, мне очень жаль, что предложенное вами изменение было отклонено, но мы автоматически предполагаем, что используется C ++ 11 , если не указано иное.
Shoe
это правда, но мы также должны указать, является ли какое-либо решение только на C ++ 11, на основе предоставленной вами ссылки.
trlkly
@trlkly, согласен. Я соответствующим образом отредактировал ответ. Спасибо за указание на это.
Shoe
9

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

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

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

законопроект
источник
3
Да. C предназначен для того, чтобы доверять программисту компилятору. Если вы так явно индексируете конец массива, значит, вы делаете что-то особенное и преднамеренное.
Джон
7
Я начал программировать на C 14 лет назад. Из всего, что сказал мой профессор, одна фраза запомнилась мне больше, чем все остальные: «C был написан программистами для программистов». Язык очень мощный. (Приготовьтесь к клише) Как дядя Бен учил нас: «С большой силой приходит большая ответственность».
Эндрю Фаланга
6

Во-первых, 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.

user34814
источник
7
Вы путаете проверку границ времени выполнения с проверкой типов во время компиляции.
Ben Voigt
@Ben Voigt: Я говорю только о проверке границ, как и исходный вопрос.
user34814
2
@ user34814 Проверка границ времени компиляции входит в сферу проверки типов. Эту функцию предлагают несколько языков высокого уровня.
Леушенко
5

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

void bar(int myArray[static 10])

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

gnasher729
источник
1
Почему компилятор должен предупреждать, если вы обращаетесь к 11-му элементу, а массив содержит не менее 10 элементов?
nwellnhof
Предположительно это связано с тем, что компилятор может обеспечить только то, чтобы у вас было не менее 10 элементов. Если вы попытаетесь получить доступ к 11-му элементу, нельзя быть уверенным, что он существует (даже если это возможно).
Дилан Уотсон
2
Я не думаю, что это правильное прочтение стандарта. [static]позволяет компилятору предупреждать, если вы вызываете bar с расширением int[5]. Она не диктует , что вы можете получить доступ в bar . Бремя ответственности полностью ложится на вызывающего абонента.
tab
3
error: expected primary-expression before 'static'никогда не видел такого синтаксиса. это вряд ли будет стандартный C или C ++.
v.oddou
3
@ v.oddou, он указан в C99, в 6.7.5.2 и 6.7.5.3.
Сэмюэл Эдвин Уорд
5

Это хорошо известная «особенность» C, переданная в C ++, потому что C ++ должен правильно компилировать код C.

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

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

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

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

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

ZioByte
источник
Может быть , C ++ был должен компилировать код С 20 лет назад, но это , конечно , это не так , и не имеет в течение длительного времени (C ++ 98? C99 , по крайней мере, которая не была «фиксированной» любой новой C ++ стандарт).
hyde
@hyde Для меня это звучит слишком резко. Процитирую Страуструпа: «За небольшими исключениями, C является подмножеством C ++». (C ++ PL 4-е изд., Раздел 1.2.1). Хотя и C ++, и C развиваются дальше, и из последней версии C существуют функции, которых нет в последней версии C ++, в целом я думаю, что цитата Страуструпа все еще в силе.
mvw 02
@mvw Большая часть кода C, написанного в этом тысячелетии, который намеренно не поддерживает совместимость с C ++, избегая несовместимых функций, будет использовать синтаксис назначенных инициализаторов C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) для инициализации структур, потому что это гораздо более ясный способ инициализации структуры. В результате самый последний код C будет отклонен стандартными компиляторами C ++, потому что большая часть кода C будет инициализировать структуры.
hyde
@mvw Можно было бы сказать, что C ++ должен быть совместим с C, поэтому можно написать код, который будет компилироваться как с компиляторами C, так и с C ++, если будут сделаны определенные компромиссы. Но для этого необходимо использовать подмножество как C, так и C ++, а не только подмножество C ++.
hyde
@hyde Вы были бы удивлены, какая часть кода C компилируется на C ++. Несколько лет назад все ядро ​​Linux было компилируемым на C ++ (я не знаю, верно ли это до сих пор). Я обычно компилирую код C в компиляторе C ++, чтобы получить превосходную проверку предупреждений, только "производственный" компилируется в режиме C, чтобы добиться максимальной оптимизации.
ZioByte
3

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

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

Supercat
источник
1

Единственный вопрос, на который еще нет ответа, - это актуальный вопрос.

В уже приведенных ответах объясняется, что массивы не могут быть переданы по значению функции ни в 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

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

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

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

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


источник
0

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

Hamidi
источник