Dlaczego główny argv C / C ++ deklarowany jest jako „char * argv []”, a nie tylko „char * argv”?

21

Dlaczego jest argvzadeklarowany jako „wskaźnik do wskaźnika do pierwszego indeksu tablicy”, a nie po prostu „wskaźnik do pierwszego indeksu tablicy” ( char* argv)?

Dlaczego tutaj wymagane jest pojęcie „wskaźnik do wskaźnika”?

użytkownik
źródło
4
„wskaźnik do wskaźnika do pierwszego indeksu tablicy” - to nie jest poprawny opis char* argv[]lub char**. To wskaźnik do wskaźnika do znaku; konkretnie zewnętrzny wskaźnik wskazuje na pierwszy wskaźnik w tablicy, a wewnętrzne wskaźniki wskazują na pierwsze znaki łańcuchów zakończonych znakiem nul. Nie ma tu żadnych wskaźników.
Sebastian Redl
12
Jak uzyskałbyś drugi argument, gdyby był to tylko char * argv?
gnasher729
15
Twoje życie stanie się łatwiejsze, gdy umieścisz przestrzeń we właściwym miejscu. char* argv[]stawia miejsce w niewłaściwym miejscu. Powiedz char *argv[], a teraz jest jasne, że oznacza to „wyrażenie *argv[n]jest zmienną typu char”. Nie daj się wciągnąć w próbę ustalenia, co to jest wskaźnik, a co wskaźnik do wskaźnika i tak dalej. Deklaracja mówi ci, jakie operacje możesz wykonać na tej rzeczy.
Eric Lippert,
1
Mentalnie porównaj char * argv[]z podobnym konstruktem C ++ std::string argv[]i może być łatwiejsze do przeanalizowania. ... Po prostu nie zaczynaj tak pisać !
Justin Time - Przywróć Monikę
2
@EricLippert pamiętaj, że pytanie obejmuje również C ++ i tam możesz np. Mieć, char &func(int);który nie sprawia, że &func(5)ma typ char.
Ruslan

Odpowiedzi:

59

Argv jest w zasadzie taki:

wprowadź opis zdjęcia tutaj

Po lewej stronie jest sam argument - co tak naprawdę przekazano jako argument do main. Który zawiera adres tablicy wskaźników. Każdy z tych punktów wskazuje na miejsce w pamięci zawierające tekst odpowiedniego argumentu przekazanego w wierszu poleceń. Następnie na końcu tej tablicy jest gwarantowany wskaźnik zerowy.

Zauważ, że faktyczne miejsce przechowywania poszczególnych argumentów jest przynajmniej potencjalnie przydzielane osobno, więc ich adresy w pamięci mogą być rozmieszczone dość losowo (ale w zależności od tego, jak rzeczy się zapisują, mogą być również w jednym ciągłym bloku pamięć - po prostu nie wiesz i nie powinno cię to obchodzić).

Jerry Coffin
źródło
52
Jakikolwiek silnik układu narysował dla ciebie ten schemat, ma błąd w algorytmie minimalizacji skrzyżowań!
Eric Lippert,
43
@EricLippert Może być celowe podkreślenie, że osoby wskazujące mogą nie być ciągłe ani uporządkowane.
jamesdlin
3
Powiedziałbym, że to celowe
Michael
24
Z pewnością było to celowe - i przypuszczam, że Eric prawdopodobnie to zorientował, ale (słusznie, IMO) uznał ten komentarz za zabawny.
Jerry Coffin
2
@JerryCoffin, można również zauważyć, że nawet jeśli rzeczywiste argumenty były ciągłe w pamięci, mogą mieć dowolne długości, więc nadal potrzebne byłyby odrębne wskaźniki dla każdego z nich, aby uzyskać dostęp argv[i]bez skanowania wszystkich poprzednich.
ilkkachu
22

Ponieważ to zapewnia system operacyjny :-)

Twoje pytanie dotyczy trochę problemu inwersji kurczaka / jajka. Problemem nie jest wybranie tego, co chcesz w C ++, problemem jest to, jak mówisz w C ++, co daje ci system operacyjny.

Unix przekazuje tablicę „ciągów”, przy czym każdy ciąg jest argumentem polecenia. W C / C ++ ciąg jest „char *”, więc tablica ciągów to char * argv [] lub char ** argv, zgodnie z gustem.

przechodzień
źródło
13
Nie, to dokładnie „problem z wyborem tego, co chcesz w C ++”. Na przykład Windows udostępnia wiersz poleceń jako pojedynczy ciąg, a mimo to programy C / C ++ wciąż otrzymują swoją argvtablicę - środowisko wykonawcze zajmuje się tokenizacją wiersza poleceń i budowaniem argvtablicy podczas uruchamiania.
Joker_vD
14
@Joker_vD Myślę, że w sposób pokręcony dotyczy on tego, co daje system operacyjny. W szczególności: Myślę, że C ++ zrobił to w ten sposób, ponieważ C zrobił to w ten sposób, a C zrobił to w ten sposób, ponieważ w tym czasie C i Unix były tak nierozerwalnie połączone i Unix zrobił to w ten sposób.
Daniel Wagner
1
@DanielWagner: Tak, pochodzi z uniksowego dziedzictwa C. W systemach Unix / Linux minimum, _startktóre wywołuje, mainmusi tylko przekazać mainwskaźnik do istniejącej argvtablicy w pamięci; jest już w odpowiednim formacie. Jądro kopiuje go z argumentu argv do execve(const char *filename, char *const argv[], char *const envp[])wywołania systemowego, które zostało utworzone w celu uruchomienia nowego pliku wykonywalnego. (W systemie Linux argv [] (sama tablica) i argc znajdują się na stosie podczas wprowadzania procesu. Zakładam, że większość uniksów jest taka sama, ponieważ jest to dobre miejsce.)
Peter Cordes
8
Ale Joker wskazuje tutaj, że standardy C / C ++ pozostawiają to implementacji, z której pochodzą argumenty; nie muszą być prosto z systemu operacyjnego. W systemie operacyjnym, który przechodzi przez płaski ciąg, dobra implementacja C ++ powinna obejmować tokenizację, zamiast ustawiania argc=2i przekazywania całego płaskiego ciągu. (Przestrzeganie litery standardu nie jest wystarczające, aby było użyteczne ; celowo pozostawia wiele miejsca na opcje implementacji.) Chociaż niektóre programy Windows będą chciały specjalnie traktować cytaty, więc rzeczywiste implementacje zapewniają sposób na uzyskanie płaskiego ciągu, zbyt.
Peter Cordes
1
Odpowiedź Basile to w zasadzie korekta + @ Joker i moje komentarze, z dodatkowymi szczegółami.
Peter Cordes
15

Po pierwsze, jako deklaracja parametru char **argvjest taka sama jak char *argv[]; oba sugerują wskaźnik do (tablicy lub zestawu co najmniej jednego możliwego) wskaźnika do ciągów.

Następnie, jeśli masz tylko „wskaźnik do char” - np. Po prostu char *- to aby uzyskać dostęp do n-tego elementu, musisz zeskanować pierwsze n-1 elementy, aby znaleźć początek n-tego elementu. (I nałożyłoby to również wymóg, aby każdy ciąg był przechowywany w sposób ciągły.)

Za pomocą tablicy wskaźników możesz bezpośrednio zindeksować n-ty element - więc (choć nie jest to absolutnie konieczne - zakładając, że ciągi są ciągłe), jest to na ogół znacznie wygodniejsze.

Ilustrować:

./program witaj świecie

argc = 3
argv[0] --> "./program\0"
argv[1] --> "hello\0"
argv[2] --> "world\0"

Możliwe jest, że w systemie operacyjnym zapewniono tablicę znaków:

            "./program\0hello\0world\0"
argv[0]      ^
argv[1]                 ^
argv[2]                        ^

gdyby argument był tylko „wskaźnikiem char”, można by zobaczyć

       "./program\0hello\0world\0"
argv    ^

Jednak (choć prawdopodobnie z projektu systemu operacyjnego) nie ma prawdziwej gwarancji, że trzy ciągi „./program”, „hello” i „world” są ciągłe. Ponadto ten rodzaj „pojedynczego wskaźnika do wielu ciągłych ciągów znaków” jest bardziej nietypową konstrukcją typu danych (dla C), szczególnie w porównaniu z tablicą wskaźników do łańcucha.

Erik Eidt
źródło
co jeśli zamiast tego argv --> "hello\0world\0"masz argv --> index 0 of the array(cześć), tak jak normalna tablica. dlaczego nie jest to wykonalne? następnie odczytujesz argcczasy tablic . następnie przekazujesz sam argv, a nie wskaźnik do argv.
użytkownik
@auser, właśnie to argv -> "./program\0hello\0\world\0" to: wskaźnik do pierwszego znaku (tj. „.”). Jeśli weźmiesz ten wskaźnik za pierwszym \ 0, wtedy mieć wskaźnik do „hello \ 0”, a następnie do „world \ 0”. Po razach argc (uderzenie \ 0 ") skończyłeś. Jasne, że można go uruchomić i, jak powiedziałem, niezwykły konstrukt.
Erik Eidt
Zapomniałeś powiedzieć, że w twoim przykładzie argv[4]jestNULL
Basile Starynkevitch
3
Istnieje gwarancja, że ​​(przynajmniej początkowo) argv[argc] == NULL. W tym przypadku tak argv[3]nie jest argv[4].
Miral
1
@Hill, tak, dziękuję, ponieważ starałem się wyraźnie powiedzieć o terminatorach znaków zerowych (i tego nie zauważyłem).
Erik Eidt
13

Dlaczego główny argv C / C ++ jest zadeklarowany jako „char * argv []”

Możliwą odpowiedzią jest to, że standard C11 n1570 (w §5.1.2.2.1 Uruchomienie programu ) i standard C ++ 11 n3337 (w głównej funkcji §3.6.1 ) wymagają tego w środowiskach hostowanych (ale zauważ, że standard C wspomina także §5.1.2.1 środowiska wolnostojące ) Zobacz także to .

Kolejne pytanie brzmi: dlaczego standardy C i C ++ zdecydowały się mainna taki int main(int argc, char**argv)podpis? Wyjaśnienie jest w dużej mierze historyczne: C zostało wynalezione z Unixem , który ma powłokę, która wykonuje globbing przed wykonaniem fork(która jest wywołaniem systemowym w celu utworzenia procesu) i execve(który jest wywołaniem systemowym w celu wykonania programu) i który execveprzesyła tablicę argumentów programu łańcuchowego i jest powiązany mainz wykonanym programem. Przeczytaj więcej o filozofii Uniksa i ABI .

A C ++ starał się postępować zgodnie z konwencjami języka C i być z nim kompatybilny. Nie mógł zdefiniować, mainże jest niezgodny z tradycjami C.

Jeśli zaprojektowałeś system operacyjny od zera (wciąż mając interfejs wiersza poleceń) i język programowania dla niego od zera, będziesz mógł wymyślić różne konwencje uruchamiania programu. Inne języki programowania (np. Common Lisp, Ocaml lub Go) mają różne konwencje uruchamiania programów.

W praktyce mainjest wywoływany przez jakiś kod crt0 . Zauważ, że w systemie Windows globowanie może być wykonywane przez każdy program jako odpowiednik crt0, a niektóre programy Windows mogą uruchamiać się w niestandardowym punkcie wejścia WinMain . W Unixie globowanie odbywa się przez powłokę (i crt0dostosowuje ABI oraz określony przez niego układ stosu wywołań do konwencji wywoływania twojej implementacji języka C).

Basile Starynkevitch
źródło
12

Zamiast myśleć o nim jako o „wskaźniku do wskaźnika”, pomaga myśleć o nim jako o „tablicy ciągów” z []tablicą char*oznaczającą i łańcuchem oznaczającym. Po uruchomieniu programu możesz przekazać mu jeden lub więcej argumentów wiersza poleceń, które są odzwierciedlone w argumentach do main: argcjest liczbą argumentów i argvumożliwia dostęp do poszczególnych argumentów.

Casablanka
źródło
2
+1 to! W wielu językach - bash, PHP, C, C ++ - argv to tablica ciągów znaków. O tym musisz pomyśleć, kiedy widzisz char **lub char *[], co jest takie samo.
rexkogitans
1

W wielu przypadkach odpowiedź brzmi „ponieważ jest to standard”. Aby zacytować standard C99 :

- Jeśli wartość argc jest większa od zera, elementy tablicy argv [0] do argv [argc-1] włącznie zawierają wskaźniki do łańcuchów , którym środowisko hosta otrzymuje wartości zdefiniowane przed implementacją programu.

Oczywiście, zanim został znormalizowany, był już używany przez K&R C we wczesnych implementacjach Uniksa, w celu przechowywania parametrów wiersza poleceń (coś, co trzeba dbać w powłoce uniksowej, takiej jak systemy wbudowane /bin/bashlub /bin/shnie). Aby zacytować pierwsze wydanie „The C Programming Language” K&R (str. 110) :

Pierwszy (konwencjonalnie nazywany argc ) to liczba argumentów wiersza poleceń, z którymi program został wywołany; drugi ( argv ) jest wskaźnikiem do tablicy ciągów znaków zawierających argumenty, po jednym na ciąg.

Sergiy Kolodyazhnyy
źródło