Por que o argumento principal do C / C ++ é declarado como "char * argv []" em vez de apenas "char * argv"?

21

Por que é argvdeclarado como "um ponteiro para apontar para o primeiro índice da matriz", em vez de apenas ser "um ponteiro para o primeiro índice da matriz" ( char* argv)?

Por que a noção de "ponteiro para ponteiro" é necessária aqui?

um usuário
fonte
4
"um ponteiro para apontar para o primeiro índice da matriz" - Essa não é uma descrição correta de char* argv[]ou char**. Isso é um ponteiro para um ponteiro para um personagem; especificamente, o ponteiro externo aponta para o primeiro ponteiro em uma matriz e os ponteiros internos apontam para os primeiros caracteres de cadeias terminadas em nulo. Não há índices envolvidos aqui.
Sebastian Redl
12
Como você obteria o segundo argumento se fosse apenas char * argv?
gnasher729 20/01
15
Sua vida ficará mais fácil quando você colocar o espaço no lugar certo. char* argv[]coloca o espaço no lugar errado. Digamos char *argv[], e agora está claro que isso significa "a expressão *argv[n]é uma variável do tipo char". Não seja pego tentando descobrir o que é um ponteiro e o que é um ponteiro para um ponteiro, e assim por diante. A declaração está dizendo quais operações você pode executar nessa coisa.
Eric Lippert
1
Compare mentalmente char * argv[]com a construção semelhante em C ++ std::string argv[]e poderá ser mais fácil analisar. ... Apenas não comece a escrever dessa maneira!
Justin Time 2 Restabelecer Monica
2
@ Ericricipper note que a questão também inclui C ++, e aí você pode ter, por exemplo, o char &func(int);que não faz &func(5)ter tipo char.
Ruslan

Respostas:

59

Argv é basicamente assim:

insira a descrição da imagem aqui

À esquerda, está o argumento em si - o que é realmente passado como argumento para main. Que contém o endereço de uma matriz de ponteiros. Cada um desses pontos aponta para algum lugar na memória que contém o texto do argumento correspondente que foi passado na linha de comando. Então, no final dessa matriz, é garantido que seja um ponteiro nulo.

Observe que o armazenamento real dos argumentos individuais é pelo menos potencialmente alocado separadamente um do outro, portanto, seus endereços na memória podem ser organizados de maneira bastante aleatória (mas, dependendo de como as coisas são escritas, eles também podem estar em um único bloco contíguo de memória - você simplesmente não sabe e não deve se importar).

Jerry Coffin
fonte
52
Qualquer que seja o mecanismo de layout que desenhou esse diagrama para você, há um erro no algoritmo de minimização de cruzamentos!
Eric Lippert 20/01
43
@EricLippert Pode ser intencional enfatizar que os apontadores podem não ser contíguos nem em ordem.
jamesdlin 20/01
3
Eu diria que é intencional
Michael
24
Certamente foi intencional - e eu acho que Eric provavelmente imaginou isso, mas (corretamente, a IMO) achou o comentário engraçado de qualquer maneira.
Jerry Coffin
2
@JerryCoffin, pode-se também salientar que, mesmo que os argumentos reais sejam contíguos na memória, eles podem ter comprimentos arbitrários; portanto, ainda é necessário ponteiros distintos para que cada um deles possa acessar argv[i]sem varrer todos os argumentos anteriores.
ilkkachu 21/01
22

Porque é isso que o sistema operacional fornece :-)

Sua pergunta é um pouco de inversão de frango / ovo. O problema não é escolher o que você deseja em C ++, o problema é como você diz em C ++ o que o sistema operacional está fornecendo.

O Unix passa uma matriz de "strings", cada uma sendo um argumento de comando. No C / C ++, uma string é um "char *", portanto, uma matriz de strings é char * argv [] ou char ** argv, de acordo com o gosto.

transeunte
fonte
13
Não, é exatamente "o problema de escolher o que você deseja em C ++". O Windows, por exemplo, fornece a linha de comando como uma única sequência e, ainda assim, os programas C / C ++ ainda recebem sua argvmatriz - o tempo de execução cuida da tokenização da linha de comando e da construção da argvmatriz na inicialização.
Joker_vD 20/01
14
@Joker_vD Eu acho que de uma maneira distorcida é sobre o que o sistema operacional oferece a você. Especificamente: Eu acho que o C ++ fez dessa maneira porque C fez dessa maneira, e C fez dessa maneira porque na época C e Unix estavam tão inextricavelmente vinculados e o Unix fazia dessa maneira.
Daniel Wagner
1
@DanielWagner: Sim, isso é da herança do Unix da C. No Unix / Linux, um mínimo de _startchamadas mainapenas precisa passar mainum ponteiro para a argvmatriz existente na memória; já está no formato certo. O kernel o copia do argumento argv para a execve(const char *filename, char *const argv[], char *const envp[])chamada do sistema que foi feita para iniciar um novo executável. (No Linux, argv [] (o próprio array) e argc estão na pilha na entrada do processo eu assumo a maioria dos Unixes são as mesmas, porque isso é um bom lugar para ele..)
Peter Cordes
8
Mas o argumento de Joker aqui é que os padrões C / C ++ deixam para a implementação a origem dos argumentos; eles não precisam ser diretos do sistema operacional. Em um sistema operacional que passa uma cadeia simples, uma boa implementação de C ++ deve incluir tokenização, em vez de definir argc=2e passar toda a cadeia plana. (Seguir a letra do padrão não é suficiente para ser útil ; deixa intencionalmente muito espaço para opções de implementação.) Embora alguns programas do Windows desejem tratar especialmente aspas, as implementações reais fornecem uma maneira de obter a sequência simples, também.
Peter Cordes
1
A resposta de Basile é basicamente essa correção do @ @ Joker e meus comentários, com mais detalhes.
Peter Cordes
15

Primeiro, como uma declaração de parâmetro, char **argvé o mesmo que char *argv[]; ambos implicam um ponteiro para (uma matriz ou conjunto de um ou mais possíveis) ponteiros para seqüências de caracteres.

Em seguida, se você tiver apenas "ponteiro para char" - por exemplo, apenas char *-, para acessar o enésimo item, precisará escanear os primeiros itens n-1 para encontrar o início do enésimo item. (E isso também imporia o requisito de que cada uma das seqüências de caracteres seja armazenada contiguamente.)

Com a matriz de ponteiros, você pode indexar diretamente o enésimo item - portanto (embora não seja estritamente necessário - assumindo que as strings são contíguas), geralmente é muito mais conveniente.

Ilustrar:

./program olá mundo

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

É possível que, em uma matriz de caracteres fornecida pelo sistema operacional:

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

se argv fosse apenas um "ponteiro para char", você poderá ver

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

No entanto (embora provavelmente pelo design do sistema operacional), não há garantia real de que as três strings "./program", "hello" e "world" sejam contíguas. Além disso, esse tipo de "ponteiro único para várias seqüências contíguas" é uma construção de tipo de dados mais incomum (para C), especialmente em comparação com a matriz de ponteiros para sequência.

Erik Eidt
fonte
e se, em vez de, argv --> "hello\0world\0"você tiver argv --> index 0 of the array(olá), como uma matriz normal. por que isso não é possível? então você continua lendo os argctempos da matriz . então você passa o próprio argv e não um ponteiro para o argv.
um usuário
@auser, é isso que argv -> "./program\0hello\0\world\0" é: um ponteiro para o primeiro caractere (ou seja, o ".") Se você passar esse ponteiro após o primeiro \ 0, então você tenha um ponteiro para "olá \ 0" e depois para "mundo \ 0". Após os tempos de argc (pressionando \ 0 "), você está pronto. Claro, pode ser feito para funcionar e, como eu disse, uma construção incomum.
Erik Eidt
Você esqueceu de afirmar que no seu exemplo argv[4]éNULL
Basile Starynkevitch
3
Há uma garantia de que (pelo menos inicialmente) argv[argc] == NULL. Nesse caso argv[3], não é argv[4].
Miral 21/01
1
@ Hill, sim, obrigado porque eu estava tentando ser explícito sobre os terminadores de caracteres nulos (e perdi esse).
Erik Eidt 21/01
13

Por que o argv principal do C / C ++ é declarado como "char * argv []"

Uma resposta possível é porque o padrão C11 n1570 (em §5.1.2.2.1 Inicialização do programa ) e o padrão C ++ 11 n3337 (em §3.6.1 função principal ) exigem que para ambientes hospedados (mas observe que o padrão C menciona também §5.1.2.1 ambientes independentes ) Veja também isso .

A próxima pergunta é por que os padrões C e C ++ optaram mainpor ter essa int main(int argc, char**argv)assinatura? A explicação é amplamente histórica: C foi inventado com o Unix , que possui um shell que faz globbing antes de fazer fork(que é uma chamada do sistema para criar um processo) e execve(que é a chamada do sistema para executar um programa) e que execvetransmite uma matriz dos argumentos do programa de seqüência de caracteres e está relacionado ao mainprograma executado. Leia mais sobre a filosofia Unix e sobre as ABI .

E o C ++ tentou seguir as convenções do C e ser compatível com ele. Não foi possível definir maincomo incompatível com as tradições C.

Se você projetou um sistema operacional a partir do zero (ainda com uma interface de linha de comando) e uma linguagem de programação para ele a partir do zero, você poderá inventar diferentes convenções de início de programa. E outras linguagens de programação (por exemplo, Common Lisp ou Ocaml ou Go) possuem diferentes convenções de início de programa.

Na prática, mainé invocado por algum código crt0 . Observe que no Windows o globbing pode ser realizado por cada programa no equivalente a crt0, e alguns programas do Windows podem iniciar através do ponto de entrada não padrão do WinMain . No Unix, o globbing é feito pelo shell (e crt0está adaptando a ABI e o layout inicial da pilha de chamadas que ele especificou, às convenções de chamada da sua implementação em C).

Basile Starynkevitch
fonte
12

Em vez de pensar nele como "ponteiro para ponteiro", ajuda pensar nele como "matriz de cadeias", com []denotando matriz e char*denotando cadeia. Quando você executa um programa, pode passar um ou mais argumentos da linha de comando e estes são refletidos nos argumentos para main: argcé a contagem de argumentos e argvpermite acessar argumentos individuais.

Casablanca
fonte
2
+1 Isto! Em muitas linguagens - bash, PHP, C, C ++ - argv é uma matriz de strings. Sobre isso você tem que pensar quando vê char **ou char *[], o que é o mesmo.
rexkogitans 20/01
1

Em muitos casos, a resposta é "porque é um padrão". Para citar o padrão C99 :

- Se o valor de argc for maior que zero, os membros da matriz argv [0] a argv [argc-1] inclusive deverão conter ponteiros para as strings , que recebem valores definidos pela implementação pelo ambiente host antes da inicialização do programa.

Obviamente, antes de ser padronizado, ele já estava em uso pelo K&R C nas implementações iniciais do Unix, com o objetivo de armazenar parâmetros de linha de comando (algo que você precisa cuidar no shell do Unix, como /bin/bashou /bin/shnão em sistemas embarcados). Para citar a primeira edição da "The C Programming Language" da K&R (pág. 110) :

O primeiro (convencionalmente chamado argc ) é o número de argumentos da linha de comando com os quais o programa foi chamado; o segundo ( argv ) é um ponteiro para uma matriz de seqüências de caracteres que contêm os argumentos, um por sequência.

Sergiy Kolodyazhnyy
fonte