Чому компілятори 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]). Або це має інші значення, про які я не знаю?


6
Це тому, що ви фактично не можете передавати масиви функції.
Ред С.

37
Я думаю, що питання тут полягало в тому, чому C дозволяє оголосити параметр типу масиву, коли він так чи інакше буде вести себе як вказівник.
Брайан

8
@Brian: Я не впевнений, що це аргумент за чи проти поведінки, але це також застосовується, якщо тип аргументу - це typedefтип масиву. Так що «розпад на покажчик» в типах аргументів не просто синтаксичний цукор замінити []з *, це дійсно відбувається через систему типу. Це має наслідки в реальному світі для деяких стандартних типів, таких як, va_listякі можуть бути визначені з масивом або не масивом.
R .. GitHub СТОП ДОПОМОГАТИ МОРО

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 Справа не в тому, який стандарт визначав, що. Річ у тому, чому той, хто це визначив, визначав саме так.
користувач253751

Відповіді:


156

Це химерність синтаксису для передачі масивів до функцій.

Насправді неможливо передати масив у C. Якщо ви пишете синтаксис, який виглядає так, що він повинен передавати масив, насправді відбувається те, що замість цього передається вказівник на перший елемент масиву.

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

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


21
Як програміст, що не належить до Ц, я вважаю цю відповідь дуже доступною. +1
астері

21
+1 за "Рішення дозволити цей синтаксис було прийнято в 1970-х роках і викликало багато плутанини з тих пір ..."
NoSenseEtAl

8
це правда, але також можна передавати масив саме такого розміру, використовуючи void foo(int (*somearray)[20])синтаксис. у цьому випадку 20 застосовується на сайтах, що викликають абонент.
v.oddou

14
-1 Як програміст C, я вважаю цю відповідь неправильною. []не ігноруються в багатовимірних масивах, як показано у відповіді Пата. Отже, включаючи синтаксис масиву було необхідно. Крім того, ніщо не заважає компілятору видавати попередження навіть на одновимірних масивах.
user694733

7
Під "змістом вашого []" я говорю конкретно про код у запитанні. Ця синтаксична вигадка зовсім не була необхідною, те ж саме можна досягти, використовуючи синтаксис вказівника, тобто, якщо вказівник переданий, тоді потрібен параметр бути декларатором покажчика. Наприклад, у прикладі Пата, void foo(int (*args)[20]);Крім того, строго кажучи, C не має багатовимірних масивів; але в ньому є масиви, елементами яких можуть бути інші масиви. Це нічого не змінює.
ММ

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
Це та сама ідея, що і для 1-D справи. Те, що схоже на 2-D масив у C та C ++, насправді є 1-D масивом, кожен елемент якого є іншим 1-D масивом. У цьому випадку у нас є масив з 10 елементами, кожен елемент якого є "масивом 20 ints". Як описано в моєму дописі, те, що насправді передається функції, - це вказівник на перший елемент args. У цьому випадку першим елементом args є "масив 20 ints". Покажчики включають інформацію про тип; що передається - це "вказівник на масив 20 ints".
ММ

9
Так, це int (*)[20]тип; "вказівник на масив 20 ints".
пт

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 використовується , якщо не вказано інше.
взуття

це правда, але ми також повинні вказати, чи якесь рішення є лише C ++ 11, виходячи з посилання, яке ви надали.
trlkly

@trlkly, я згоден. Відповідь я відредагував відповідно. Дякуємо, що вказали на це.
Взуття

9

Це весела особливість C, яка дозволяє ефективно стріляти в ногу, якщо ви так схильні.

Я думаю, що причина полягає в тому, що C - це лише крок над мовою складання. Перевірка розміру та подібні функції безпеки були зняті, щоб забезпечити максимальну продуктивність, що не погано, якщо програміст дуже старанний.

Крім того, призначаючи розмір аргументу функції має таку перевагу, що коли функцію використовує інший програміст, є ймовірність, що вони помітять обмеження розміру. Просто використання вказівника не передає цю інформацію наступному програмісту.


3
Так. C призначений для довіри програміста над компілятором. Якщо ви так нахабно індексуєте кінець масиву, ви повинні робити щось особливе та навмисне.
Іван

7
Я різав зуби в програмуванні на С 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.


7
Ви плутаєте перевірку меж часу виконання з перевіркою типу компіляції.
Ben Voigt

@Ben Voigt: Я говорю лише про перевірку меж, як і оригінальне питання.
користувач34814

2
@ user34814 перевірка меж часу компіляції знаходиться в межах перевірки типу. Кілька мов високого рівня пропонують цю функцію.
Левшенко

5

Щоб сказати компілятору, що myArray вказує на масив щонайменше з 10 ints:

void bar(int myArray[static 10])

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


1
Чому компілятор повинен попереджати, якщо ви отримуєте доступ до 11-го елемента, а масив містить щонайменше 10 елементів?
nwellnhof

Імовірно, це тому, що компілятор може стверджувати, що у вас є щонайменше 10 елементів. Якщо ви спробуєте отримати доступ до 11-го елемента, він не може бути впевнений, що він існує (хоча це може бути).
Ділан Уотсон

2
Я не думаю, що це нормальне читання стандарту. [static]дозволяє компілятору попереджати, якщо ви телефонуєте bar з int[5]. Вона не диктує , що ви можете отримати доступ в bar . Тонус знаходиться повністю на стороні виклику.
вкладка

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. С повинен бути швидким, спочатку розробник був свого роду "високомобільним асемблером" (спеціально розробленим для написання першої "портативної операційної системи": Unix), тому не слід вставляти "прихований" код; Перевірка діапазону виконання тим самим "заборонена".
  3. Машинний код, генерований для доступу до статичного масиву або динамічного (або в стеці, або в виділеному), насправді відрізняється.
  4. Оскільки викликана функція не може знати "вид" масиву, що передається як аргумент, все повинно бути вказівником і трактуватися як таке.

Можна сказати, що масиви насправді не підтримуються в C (це насправді не так, як я вже говорив раніше, але це гарне наближення); масив дійсно трактується як вказівник на блок даних і доступ до нього використовується за допомогою арифметики вказівника. Оскільки C не має жодної форми RTTI, ви повинні оголосити розмір елемента масиву в прототипі функції (для підтримки арифметики вказівника). Це навіть "більш вірно" для багатовимірних масивів.

Так чи інакше все вище насправді вже не так: с

Більшість сучасних компіляторів C / C ++ виконують перевірку меж підтримки, але стандарти вимагають відключення за замовчуванням (для зворотної сумісності). Наприклад, останні версії gcc, наприклад, виконують перевірку діапазону компіляції в часі за допомогою "-O3 -Wall -Wextra" і перевірку меж часу виконання за допомогою "-fbounds-testing".


Можливо, C ++ був повинен компілювати код З 20 років тому, але це , звичайно , це НЕ так , і не має в протягом тривалого часу (C ++ 98? C99 , по крайней мере, яка не була «фіксованою» будь-якої нової C ++ стандарт).
Гайд

@hyde Це здається мені занадто суворим. Цитувати Stroustrup "За незначними винятками, C - це підмножина C ++." (C ++ PL 4-е видання, розділ 1.2.1). Хоча і C ++, і C розвиваються далі, і існують функції останньої версії C, яких немає в останній версії C ++, в цілому я вважаю, що цитата Stroustrup все ще діє.
mvw

@mvw Код більшості C, написаний у цьому тисячолітті, який навмисно не підтримує C ++ сумісним, уникаючи несумісних функцій, використовуватиме призначений C99 синтаксис ініціалізаторів ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) для ініціалізації структур, оскільки це просто набагато зрозуміліший спосіб ініціалізації структури. Як результат, більшість поточних кодів C буде відхилено стандартними компіляторами C ++, оскільки більшість C код буде ініціалізаційними структурами.
Гайд

@mvw Можливо, можна сказати, що C ++ повинен бути сумісним з C, так що можна писати код, який буде компілюватися як із компіляторами C, так і з C ++, якщо будуть зроблені певні компроміси. Але для цього потрібно використовувати підмножину як C, так і C ++, а не лише підмножину C ++.
Гайд

@hyde Ви були б здивовані, скільки С-коду компілюється 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, який називався BCPL, компілятори насправді не дуже добре відслідковували змінні типи. Декларація int arr[5];була еквівалентною int anonymousAllocation[5],*arr = anonymousAllocation;; як тільки виділення було відкладено. компілятор не знав і не хвилювавсяarrбув покажчиком або масивом. При зверненні як до arr[x]або *arr, це вважатиметься покажчиком незалежно від того, як воно було оголошено.


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

Чому остання з цих помилок не є?

Причиною тому є те, що це викликає проблеми з typedefs.

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

Якби було помилкою вказати довжину масиву в параметрах функції, ви не змогли б використовувати myarrayім'я в параметрі функції. Оскільки деякі реалізації використовують типи масивів для стандартних типів бібліотеки, таких як va_list, і всі реалізації потрібні для створення jmp_bufтипу масиву, було б дуже проблематично, якби не було стандартного способу оголошення параметрів функції за допомогою цих імен: без цієї здатності не можна було б не бути портативною реалізацією таких функцій, як vprintf.


0

Компіляторам дозволяється перевіряти, чи розмір переданого масиву такий самий, як очікуваний. Укладачі можуть попередити проблему, якщо це не так.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.