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”?
„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:
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ć).
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.
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.
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.
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 []”
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).
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.
+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.
char* argv[]
lubchar**
. 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.char* argv[]
stawia miejsce w niewłaściwym miejscu. Powiedzchar *argv[]
, a teraz jest jasne, że oznacza to „wyrażenie*argv[n]
jest zmienną typuchar
”. 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.char * argv[]
z podobnym konstruktem C ++std::string argv[]
i może być łatwiejsze do przeanalizowania. ... Po prostu nie zaczynaj tak pisać !char &func(int);
który nie sprawia, że&func(5)
ma typchar
.Odpowiedzi:
Argv jest w zasadzie taki:
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ć).
źródło
argv[i]
bez skanowania wszystkich poprzednich.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.
źródło
argv
tablicę - środowisko wykonawcze zajmuje się tokenizacją wiersza poleceń i budowaniemargv
tablicy podczas uruchamiania._start
które wywołuje,main
musi tylko przekazaćmain
wskaźnik do istniejącejargv
tablicy w pamięci; jest już w odpowiednim formacie. Jądro kopiuje go z argumentu argv doexecve(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.)argc=2
i 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.Po pierwsze, jako deklaracja parametru
char **argv
jest taka sama jakchar *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
Możliwe jest, że w systemie operacyjnym zapewniono tablicę znaków:
gdyby argument był tylko „wskaźnikiem char”, można by zobaczyć
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.
źródło
argv --> "hello\0world\0"
maszargv --> index 0 of the array
(cześć), tak jak normalna tablica. dlaczego nie jest to wykonalne? następnie odczytujeszargc
czasy tablic . następnie przekazujesz sam argv, a nie wskaźnik do argv.argv[4]
jestNULL
argv[argc] == NULL
. W tym przypadku takargv[3]
nie jestargv[4]
.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ę
main
na takiint 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 wykonaniemfork
(która jest wywołaniem systemowym w celu utworzenia procesu) iexecve
(który jest wywołaniem systemowym w celu wykonania programu) i któryexecve
przesyła tablicę argumentów programu łańcuchowego i jest powiązanymain
z 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
main
jest 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ę (icrt0
dostosowuje ABI oraz określony przez niego układ stosu wywołań do konwencji wywoływania twojej implementacji języka C).źródło
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 domain
:argc
jest liczbą argumentów iargv
umożliwia dostęp do poszczególnych argumentów.źródło
char **
lubchar *[]
, co jest takie samo.W wielu przypadkach odpowiedź brzmi „ponieważ jest to standard”. Aby zacytować standard C99 :
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/bash
lub/bin/sh
nie). Aby zacytować pierwsze wydanie „The C Programming Language” K&R (str. 110) :źródło