Dlaczego kompilatory C i C ++ zezwalają na długości tablic w sygnaturach funkcji, skoro nie są one nigdy wymuszane?

133

Oto, co odkryłem podczas mojej nauki:

#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;
}  

Więc w zmiennej int dis(char a[1])The [1]zdaje się nic nie robić i nie działa w
ogóle, bo mogę korzystać a[2]. Tak jak int a[]lub char *a. Wiem, że nazwa tablicy jest wskaźnikiem i jak przekazać tablicę, więc moja zagadka nie dotyczy tej części.

Chcę wiedzieć, dlaczego kompilatory zezwalają na to zachowanie ( int a[1]). A może ma inne znaczenia, o których nie wiem?

Fanl
źródło
6
Dzieje się tak, ponieważ nie można w rzeczywistości przekazywać tablic do funkcji.
Ed S.
37
Myślę, że tutaj chodziło o to, dlaczego C pozwala zadeklarować parametr jako typu tablicowego, skoro i tak będzie zachowywał się dokładnie jak wskaźnik.
Brian,
8
@Brian: Nie jestem pewien, czy jest to argument za czy przeciw zachowaniu, ale ma to również zastosowanie, jeśli typ argumentu to typedefz typem tablicy. Więc „zaniku do wskaźnika” w rodzaju argument nie jest po prostu cukier syntaktyczny zastępując []ze *to się naprawdę dzieje za pośrednictwem systemu typu. Ma to rzeczywiste konsekwencje dla niektórych standardowych typów, takich jak ten, va_listktóry może być zdefiniowany za pomocą typu tablicowego lub nie-tablicowego.
R .. GitHub PRZESTAŃ POMÓC W LODZIE
4
@songyuanyao można osiągnąć coś nie całkowicie różniących się między C i C ++ () za pomocą wskaźnika: int dis(char (*a)[1]). Następnie należy przekazać wskaźnik do tablicy: dis(&b). Jeśli chcesz użyć funkcji C, które nie istnieją w C ++, możesz również powiedzieć takie rzeczy jak void foo(int data[static 256])i int bar(double matrix[*][*]), ale to zupełnie inna puszka robaków.
Stuart Olsen
1
@StuartOlsen Nie chodzi o to, który standard definiuje co. Chodzi o to, dlaczego ktokolwiek zdefiniował to w ten sposób.
user253751

Odpowiedzi:

157

Jest to dziwactwo składni przekazywania tablic do funkcji.

W rzeczywistości nie jest możliwe przekazanie tablicy w C. Jeśli piszesz składnię, która wygląda tak, jakby powinna przekazywać tablicę, w rzeczywistości jest przekazywany wskaźnik do pierwszego elementu tablicy.

Ponieważ wskaźnik nie zawiera żadnych informacji o długości, zawartość your []na liście parametrów formalnych funkcji jest w rzeczywistości ignorowana.

Decyzja o dopuszczeniu tej składni została podjęta w latach siedemdziesiątych XX wieku i od tego czasu wywołała wiele zamieszania ...

MM
źródło
22
Jako programista spoza C uważam tę odpowiedź za bardzo przystępną. +1
asteri
22
+1 dla „Decyzja o dopuszczeniu tej składni została podjęta w latach siedemdziesiątych i od tego czasu spowodowała wiele zamieszania”
NoSenseEtAl
8
to prawda, ale możliwe jest również przekazanie tablicy o takim samym rozmiarze przy użyciu void foo(int (*somearray)[20])składni. w tym przypadku 20 jest egzekwowane w witrynach wywołujących.
v.oddou,
14
-1 Jako programista C uważam, że ta odpowiedź jest niepoprawna. []nie są ignorowane w tablicach wielowymiarowych, jak pokazano w odpowiedzi pat. Dlatego dołączenie składni tablicy było konieczne. Ponadto nic nie stoi na przeszkodzie, aby kompilator wydawał ostrzeżenia nawet w przypadku tablic jednowymiarowych.
user694733
7
Przez „zawartość twojego []” mam na myśli konkretnie kod w pytaniu. To dziwactwo składniowe wcale nie było konieczne, to samo można osiągnąć za pomocą składni wskaźnika, tj. Jeśli przekazywany jest wskaźnik, należy wymagać, aby parametr był deklaratorem wskaźnika. Np. W przykładzie Pat. void foo(int (*args)[20]);Ponadto, ściśle mówiąc, C nie ma wielowymiarowych tablic; ale ma tablice, których elementami mogą być inne tablice. To niczego nie zmienia.
MM
144

Długość pierwszego wymiaru jest ignorowana, ale długość dodatkowych wymiarów jest konieczna, aby umożliwić kompilatorowi prawidłowe obliczenie przesunięć. W poniższym przykładzie do foofunkcji przekazywany jest wskaźnik do dwuwymiarowej tablicy.

#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;
}

Rozmiar pierwszego wymiaru [10]jest ignorowany; kompilator nie przeszkodzi w indeksowaniu z końca (zauważ, że formal potrzebuje 10 elementów, ale faktyczny dostarcza tylko 2). Jednak rozmiar drugiego wymiaru [20]jest używany do określenia kroku każdego wiersza, a tutaj formalny musi być zgodny z rzeczywistym. Ponownie, kompilator również nie uniemożliwi indeksowania końca drugiego wymiaru.

Przesunięcie bajtów od podstawy tablicy do elementu args[row][col]jest określane przez:

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

Zauważ, że jeśli col >= 20, to faktycznie indeksujesz do następnego wiersza (lub poza końcem całej tablicy).

sizeof(args[0]), wraca 80na moim komputerze, gdzie sizeof(int) == 4. Jeśli jednak spróbuję to zrobić sizeof(args), otrzymuję następujące ostrzeżenie kompilatora:

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.

W tym przypadku kompilator ostrzega, że ​​zamiast rozmiaru samej tablicy poda tylko rozmiar wskaźnika, na który rozpadła się tablica.

poklepać
źródło
Bardzo przydatne - spójność z tym jest również prawdopodobna jako przyczyna dziwactwa w przypadku 1-d.
jwg
1
To ten sam pomysł, co w przypadku 1-D. To, co wygląda jak tablica 2-w C i C ++, jest w rzeczywistości tablicą 1-W, której każdy element jest kolejną tablicą 1-W. W tym przypadku mamy tablicę z 10 elementami, z których każdy jest „tablicą 20 int”. Jak opisano w moim poście, to, co faktycznie jest przekazywane do funkcji, to wskaźnik do pierwszego elementu args. W tym przypadku pierwszym elementem args jest „tablica 20 int”. Wskaźniki zawierają informacje o typie; przekazywany jest „wskaźnik do tablicy 20 int”.
MM
9
Tak, to jest ten int (*)[20]typ; „wskaźnik do tablicy zawierającej 20 int”.
patrzą
@pat Powiedziałeś, że możemy pominąć tylko pierwszy wymiar, ale nie inne wymiary, to dlaczego ten kod działa bez żadnego błędu lub ostrzeżenia. Link CODE: ide.geeksforgeeks.org/WMoKbsYhB8 Proszę wyjaśnić. Czy coś mi brakuje?
Vinay Yadav
Typ int (*p)[]jest wskaźnikiem do 1-wymiarowej tablicy o nieokreślonej długości. Rozmiar *pjest niezdefiniowany, więc nie możesz indeksować pbezpośrednio (nawet z indeksem 0!). Jedyne, co możesz zrobić, pto wyodrębnić go jako *p, a następnie zindeksować jako (*p)[i]. Nie zachowuje to dwuwymiarowej struktury oryginalnej tablicy.
pat
33

Problem i jak go rozwiązać w C ++

Problem został szczegółowo wyjaśniony przez Pat i Matta . Kompilator zasadniczo ignoruje pierwszy wymiar rozmiaru tablicy, skutecznie ignorując rozmiar przekazanego argumentu.

Z drugiej strony w C ++ można łatwo pokonać to ograniczenie na dwa sposoby:

  • za pomocą odniesień
  • using std::array(od C ++ 11)

Bibliografia

Jeśli twoja funkcja próbuje tylko odczytać lub zmodyfikować istniejącą tablicę (nie kopiować jej), możesz łatwo użyć referencji.

Na przykład załóżmy, że chcesz mieć funkcję, która resetuje tablicę dziesięciu ints, ustawiając każdy element na 0. Możesz to łatwo zrobić, używając następującego podpisu funkcji:

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

Nie tylko będzie to działać dobrze , ale także wymusi wymiar tablicy .

Możesz także skorzystać z szablonów, aby powyższy kod był ogólny :

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

I wreszcie możesz skorzystać z constpoprawności. Rozważmy funkcję, która wyświetla tablicę 10 elementów:

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

Stosując constkwalifikator zapobiegamy ewentualnym modyfikacjom .


Standardowa klasa biblioteczna dla tablic

Jeśli uznasz powyższą składnię za brzydką i niepotrzebną, tak jak ja, możemy wrzucić ją do puszki i użyć std::arrayzamiast niej (od C ++ 11).

Oto refaktoryzowany kod:

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

Czy to nie wspaniałe? Nie wspominając o tym, że ogólna sztuczka z kodem, której nauczyłem Cię wcześniej, nadal działa:

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) { ... }

Nie tylko to, ale otrzymujesz za darmo kopiowanie i przenoszenie semantyczne. :)

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
}

Więc na co czekasz? Idź, użyj std::array.

But
źródło
2
@kietz, przykro mi, że twoja sugerowana edycja została odrzucona, ale automatycznie zakładamy, że używany jest C ++ 11 , chyba że określono inaczej.
But z
to prawda, ale powinniśmy również określić, czy jakiekolwiek rozwiązanie jest dostępne tylko w C ++ 11, na podstawie podanego linku.
trlkly
@trlkly, zgadzam się. Odpowiednio zredagowałem odpowiedź. Dzięki za wskazanie tego.
But
9

To zabawna funkcja C, która pozwala skutecznie strzelić sobie w stopę, jeśli masz taką skłonność.

Myślę, że powodem jest to, że C jest tylko krokiem powyżej języka asemblera. Sprawdzanie rozmiaru i podobne funkcje bezpieczeństwa zostały usunięte, aby umożliwić najwyższą wydajność, co nie jest złe, jeśli programista jest bardzo sumienny.

Ponadto przypisanie rozmiaru do argumentu funkcji ma tę zaletę, że gdy funkcja jest używana przez innego programistę, istnieje szansa, że ​​zauważy on ograniczenie rozmiaru. Samo użycie wskaźnika nie przekazuje tej informacji następnemu programiście.

rachunek
źródło
3
Tak. C został zaprojektowany tak, aby ufać programiście kompilatorowi. Jeśli tak rażąco indeksujesz koniec tablicy, musisz robić coś specjalnego i zamierzonego.
John
7
Obcięłam sobie zęby w programowaniu na C 14 lat temu. Ze wszystkiego, co powiedział mój profesor, jedyne zdanie, które utkwiło mi w pamięci bardziej niż wszystkie inne, „C zostało napisane przez programistów dla programistów”. Język jest niezwykle potężny. (Przygotuj się na frazes) Jak nauczył nas wujek Ben: „Z wielką mocą wiąże się wielka odpowiedzialność”.
Andrew Falanga
7

Po pierwsze, C nigdy nie sprawdza granic tablicy. Nie ma znaczenia, czy są lokalne, globalne, statyczne, parametry, cokolwiek. Sprawdzanie granic tablicy oznacza więcej przetwarzania, a C ma być bardzo wydajne, więc sprawdzanie granic tablic jest wykonywane przez programistę w razie potrzeby.

Po drugie, istnieje sztuczka, która umożliwia przekazanie wartości tablicy do funkcji. Możliwe jest również zwrócenie tablicy z funkcji według wartości. Wystarczy utworzyć nowy typ danych za pomocą struct. Na przykład:

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

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Musisz uzyskać dostęp do takich elementów: foo.a [1]. Dodatkowe „.a” może wyglądać dziwnie, ale ta sztuczka dodaje wielką funkcjonalność do języka C.

user34814
źródło
7
Mylisz sprawdzanie granic środowiska uruchomieniowego ze sprawdzaniem typów w czasie kompilacji.
Ben Voigt,
@Ben Voigt: Mówię tylko o sprawdzaniu granic, tak jak pierwotne pytanie.
user34814
2
Sprawdzanie granic czasu kompilacji @ user34814 wchodzi w zakres sprawdzania typu. Funkcję tę oferuje kilka języków wysokiego poziomu.
Leushenko
5

Aby przekazać kompilatorowi, że myArray wskazuje na tablicę składającą się z co najmniej 10 int:

void bar(int myArray[static 10])

Dobry kompilator powinien dać ci ostrzeżenie, jeśli uzyskasz dostęp do myArray [10]. Bez słowa kluczowego „static” liczba 10 nie miałaby żadnego znaczenia.

gnasher729
źródło
1
Dlaczego kompilator miałby ostrzegać, jeśli uzyskujesz dostęp do 11. elementu, a tablica zawiera co najmniej 10 elementów?
nwellnhof
Prawdopodobnie dzieje się tak dlatego, że kompilator może tylko wymusić, że masz co najmniej 10 elementów. Jeśli spróbujesz uzyskać dostęp do 11. elementu, nie możesz być pewien , czy istnieje (nawet jeśli może).
Dylan Watson
2
Nie sądzę, żeby to poprawna interpretacja normy. [static]pozwala kompilatorowi ostrzec, jeśli wywołasz bar plik int[5]. To nie dyktuje, co można uzyskać dostęp do wewnątrz bar . Obowiązek spoczywa całkowicie po stronie dzwoniącego.
zakładka
3
error: expected primary-expression before 'static'nigdy nie widziałem tej składni. raczej nie będzie to standardowe C lub C ++.
przeciwko oddou,
3
@ v.oddou, jest określony w C99, w 6.7.5.2 i 6.7.5.3.
Samuel Edwin Ward
5

Jest to dobrze znana „funkcja” języka C, przekazana do C ++, ponieważ C ++ ma poprawnie kompilować kod w C.

Problem wynika z kilku aspektów:

  1. Nazwa tablicy ma być całkowicie równoważna ze wskaźnikiem.
  2. C ma być szybki, pierwotnie opracowany jako rodzaj "asemblera wysokiego poziomu" (specjalnie zaprojektowany do napisania pierwszego "przenośnego systemu operacyjnego": Unix), więc nie powinien wstawiać "ukrytego" kodu; sprawdzanie zakresu czasu działania jest zatem „zabronione”.
  3. Kod maszynowy generowany w celu uzyskania dostępu do tablicy statycznej lub dynamicznej (w stosie lub przydzielony) jest w rzeczywistości inny.
  4. Ponieważ wywoływana funkcja nie może znać „rodzaju” tablicy przekazanej jako argument, wszystko powinno być wskaźnikiem i tak traktowane.

Można powiedzieć, że tablice nie są tak naprawdę obsługiwane w C (nie jest to do końca prawdą, jak mówiłem wcześniej, ale jest to dobre przybliżenie); tablica jest tak naprawdę traktowana jako wskaźnik do bloku danych i dostępna za pomocą arytmetyki wskaźników. Ponieważ C NIE ma żadnej formy RTTI, musisz zadeklarować rozmiar elementu tablicy w prototypie funkcji (aby obsługiwać arytmetykę wskaźników). Jest to nawet „bardziej prawdziwe” w przypadku tablic wielowymiarowych.

Zresztą wszystko powyżej nie jest już prawdą: s

Większość nowoczesnych C / C ++ kompilatory zrobić granice wsparcia kontroli, ale standardy wymagają, że jest domyślnie wyłączona (dla wstecznej kompatybilności). Na przykład najnowsze wersje gcc sprawdzają zakres czasu kompilacji za pomocą „-O3 -Wall -Wextra”, a pełne sprawdzanie granic czasu wykonywania za pomocą „-fbounds -Check”.

ZioByte
źródło
Może C ++ został powinien skompilować kod C 20 lat temu, ale na pewno to nie jest, i nie ma od dłuższego czasu (C ++ 98? C99 przynajmniej, która nie została „Fixed” przez każdą nowszą C ++ standard).
hyde
@hyde To brzmi dla mnie trochę zbyt surowo. Cytując Stroustrupa: „Z drobnymi wyjątkami C jest podzbiorem C ++”. (The C ++ PL 4. wydanie, sekcja 1.2.1). Chociaż zarówno C ++, jak i C ewoluują dalej, a istnieją funkcje z najnowszej wersji C, których nie ma w najnowszej wersji C ++, ogólnie myślę, że cytat Stroustrupa jest nadal aktualny.
mvw
@mvw Większość kodu C napisanego w tym tysiącleciu, który nie jest celowo utrzymywany w zgodności z C ++ przez unikanie niekompatybilnych funkcji, użyje wyznaczonej przez C99 inicjalizatora składni ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) do inicjowania struktur, ponieważ jest to o wiele jaśniejszy sposób inicjowania struktury. W rezultacie większość aktualnego kodu C zostanie odrzucona przez standardowe kompilatory C ++, ponieważ większość kodu C będzie inicjalizować struktury.
hyde
@mvw Można by chyba powiedzieć, że C ++ ma być kompatybilny z C, tak że możliwe jest napisanie kodu, który będzie się kompilował zarówno z kompilatorami C, jak i C ++, jeśli dojdzie do pewnych kompromisów. Ale to wymaga użycia podzbioru zarówno C, jak i C ++, a nie tylko podzbioru C ++.
hyde
@hyde Zdziwiłbyś się, ile kodu w C można skompilować w C ++. Kilka lat temu całe jądro Linuksa było kompilowalne w C ++ (nie wiem, czy nadal jest prawdą). Rutynowo kompiluję kod C w kompilatorze C ++, aby uzyskać lepsze sprawdzanie ostrzeżeń, tylko „produkcja” jest kompilowana w trybie C, aby wycisnąć jak najwięcej optymalizacji.
ZioByte
3

C nie tylko przekształci parametr typu int[5]w *int; biorąc pod uwagę deklarację typedef int intArray5[5];, przekształci również parametr typu intArray5na *int. Istnieją sytuacje, w których to zachowanie, choć dziwne, jest przydatne (szczególnie w przypadku rzeczy takich jak va_listzdefiniowane w stdargs.h, które niektóre implementacje definiują jako tablicę). Byłoby nielogiczne dopuszczenie jako parametru typu zdefiniowanego jako int[5](z pominięciem wymiaru), ale niedopuszczenie int[5]do bezpośredniego określenia.

Uważam, że obsługa parametrów typu tablicowego przez C jest absurdalna, ale jest to konsekwencja wysiłków, aby wziąć język ad-hoc, którego duże części nie były szczególnie dobrze zdefiniowane lub przemyślane, i spróbować wymyślić behawioralny specyfikacje, które są spójne z tym, co zrobiły istniejące implementacje dla istniejących programów. Wiele dziwactw języka C ma sens, gdy patrzy się na nie w tym świetle, zwłaszcza jeśli weźmie się pod uwagę, że kiedy wiele z nich zostało wynalezionych, duże części języka, który znamy dzisiaj, jeszcze nie istniały. Z tego, co rozumiem, w poprzedniku C, zwanym BCPL, kompilatory nie bardzo dobrze śledziły typy zmiennych. Deklaracja int arr[5];była równoważna int anonymousAllocation[5],*arr = anonymousAllocation;; po zarezerwowaniu przydziału. kompilator nie wiedział ani nie przejmował się tymarrbył wskaźnikiem lub tablicą. Po uzyskaniu dostępu jako albo arr[x]lub *arr, będzie traktowany jako wskaźnik, niezależnie od tego, jak został zadeklarowany.

supercat
źródło
1

Jedyną rzeczą, na którą jeszcze nie ma odpowiedzi, jest faktyczne pytanie.

Odpowiedzi już podane wyjaśniają, że tablice nie mogą być przekazywane przez wartość do funkcji ani w C, ani w C ++. Wyjaśniają również, że parametr zadeklarowany jako int[]jest traktowany tak, jakby miał typint * i że zmienną typu int[]można przekazać do takiej funkcji.

Ale nie wyjaśniają, dlaczego jawne podanie długości tablicy nigdy nie zostało popełnione.

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

Dlaczego ostatnia z nich nie jest błędem?

Powodem tego jest to, że powoduje problemy z typami czcionek.

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

Gdyby określenie długości tablicy w parametrach funkcji było błędem, nie byłoby możliwe użycie myarraynazwy w parametrze funkcji. A ponieważ niektóre implementacje używają typów tablic dla standardowych typów bibliotek, takich jak va_listi wszystkie implementacje są wymagane do utworzenia jmp_buftypu tablicowego, byłoby bardzo problematyczne, gdyby nie było standardowego sposobu deklarowania parametrów funkcji przy użyciu tych nazw: bez tej możliwości nie może być przenośną implementacją funkcji, takich jak vprintf.


źródło
0

Kompilatory mogą sprawdzić, czy rozmiar przekazanej tablicy jest taki sam, jak oczekiwano. Kompilatory mogą ostrzegać o problemie, jeśli tak nie jest.

hamidi
źródło