Ponteiros C: apontando para uma matriz de tamanho fixo

119

Esta pergunta é dirigida aos C gurus por aí:

Em C, é possível declarar um ponteiro da seguinte maneira:

char (* p)[10];

.. que basicamente afirma que este ponteiro aponta para uma matriz de 10 caracteres. O bom de declarar um ponteiro como este é que você obterá um erro em tempo de compilação se tentar atribuir um ponteiro de um array de tamanho diferente a p. Também lhe dará um erro de tempo de compilação se você tentar atribuir o valor de um ponteiro char simples para p. Tentei fazer isso com o gcc e parece funcionar com ANSI, C89 e C99.

Parece-me que declarar um ponteiro como este seria muito útil - particularmente, ao passar um ponteiro para uma função. Normalmente, as pessoas escreveriam o protótipo de uma função assim:

void foo(char * p, int plen);

Se você esperava um buffer de um tamanho específico, simplesmente testaria o valor do plen. No entanto, você não pode ter certeza de que a pessoa que passar p para você realmente fornecerá localizações de memória válidas nesse buffer. Você tem que confiar que a pessoa que chamou essa função está fazendo a coisa certa. Por outro lado:

void foo(char (*p)[10]);

..forçaria o chamador a fornecer um buffer do tamanho especificado.

Isso parece muito útil, mas nunca vi um ponteiro declarado assim em nenhum código que já encontrei.

Minha pergunta é: existe alguma razão pela qual as pessoas não declaram dicas como essa? Não estou vendo alguma armadilha óbvia?

figurassa
fonte
3
nota: desde C99, a matriz não precisa ser de tamanho fixo, conforme sugerido pelo título, 10pode ser substituída por qualquer variável no escopo
MM

Respostas:

173

O que você está dizendo em sua postagem está absolutamente correto. Eu diria que todo desenvolvedor C chega exatamente à mesma descoberta e exatamente à mesma conclusão quando (se) eles alcançam certo nível de proficiência com a linguagem C.

Quando as especificações de sua área de aplicativo exigem um array de tamanho fixo específico (o tamanho do array é uma constante de tempo de compilação), a única maneira adequada de passar esse array para uma função é usando um parâmetro de ponteiro para array

void foo(char (*p)[10]);

(na linguagem C ++ isso também é feito com referências

void foo(char (&p)[10]);

)

Isso habilitará a verificação de tipo no nível do idioma, o que garantirá que o array de tamanho correto seja fornecido como argumento. Na verdade, em muitos casos as pessoas usam essa técnica implicitamente, sem nem perceber, escondendo o tipo de array atrás de um nome de typedef

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Além disso, observe que o código acima é invariável em relação ao Vector3dtipo que é uma matriz ou a struct. Você pode mudar a definição de Vector3da qualquer momento de uma matriz para umastruct e vice-versa, e não será necessário alterar a declaração da função. Em qualquer um dos casos, as funções receberão um objeto agregado "por referência" (há exceções a isso, mas no contexto desta discussão isso é verdadeiro).

No entanto, você não verá esse método de passagem de array usado explicitamente com muita frequência, simplesmente porque muitas pessoas se confundem com uma sintaxe um tanto complicada e simplesmente não se sentem confortáveis ​​o suficiente com tais recursos da linguagem C para usá-los apropriadamente. Por esse motivo, na vida real média, passar um array como um ponteiro para seu primeiro elemento é uma abordagem mais popular. Parece apenas "mais simples".

Mas, na realidade, usar o ponteiro para o primeiro elemento para a passagem de array é uma técnica de nicho, um truque, que serve a um propósito muito específico: seu único propósito é facilitar a passagem de arrays de tamanhos diferentes (ou seja, tamanho de tempo de execução) . Se você realmente precisa ser capaz de processar arrays de tamanho de tempo de execução, então a maneira adequada de passar tal array é por um ponteiro para seu primeiro elemento com o tamanho concreto fornecido por um parâmetro adicional

void foo(char p[], unsigned plen);

Na verdade, em muitos casos, é muito útil poder processar matrizes de tamanho de tempo de execução, o que também contribui para a popularidade do método. Muitos desenvolvedores C simplesmente nunca encontram (ou nunca reconhecem) a necessidade de processar um array de tamanho fixo, permanecendo assim alheios à técnica de tamanho fixo adequada.

No entanto, se o tamanho do array for fixo, passando-o como um ponteiro para um elemento

void foo(char p[])

é um grande erro no nível da técnica, que infelizmente é bastante comum nos dias de hoje. Uma técnica de ponteiro para array é uma abordagem muito melhor nesses casos.

Outro motivo que pode impedir a adoção da técnica de passagem de matriz de tamanho fixo é o domínio da abordagem ingênua para a digitação de matrizes alocadas dinamicamente. Por exemplo, se o programa chama matrizes fixas do tipo char[10](como em seu exemplo), um desenvolvedor médio usará mallocmatrizes como

char *p = malloc(10 * sizeof *p);

Esta matriz não pode ser passada para uma função declarada como

void foo(char (*p)[10]);

o que confunde o desenvolvedor médio e o faz abandonar a declaração do parâmetro de tamanho fixo sem pensar mais nisso. Na realidade, porém, a raiz do problema está na mallocabordagem ingênua . O mallocformato mostrado acima deve ser reservado para matrizes de tamanho de tempo de execução. Se o tipo de array tiver tamanho em tempo de compilação, uma maneira melhor de mallocfazê - lo seria a seguinte

char (*p)[10] = malloc(sizeof *p);

Isso, é claro, pode ser facilmente passado para o acima declarado foo

foo(p);

e o compilador executará a verificação de tipo apropriada. Mas, novamente, isso é muito confuso para um desenvolvedor C despreparado, e é por isso que você não verá isso com muita frequência no código "típico" do dia a dia.

Formiga
fonte
2
A resposta fornece uma descrição muito concisa e informativa de como sizeof () é bem-sucedido, como frequentemente falha e como sempre falha. suas observações da maioria dos engenheiros C / C ++ não entendendo e, portanto, fazer algo que eles acham que entendem é uma das coisas mais proféticas que eu vi em algum tempo, e o véu não é nada comparado com a precisão que descreve. sério, senhor. Ótima resposta.
WhozCraig
Acabei de refatorar alguns códigos com base nesta resposta, bravo e obrigado pela Q e pela A.
Perry
1
Estou curioso para saber como você lida constcom propriedade com essa técnica. Um const char (*p)[N]argumento não parece compatível com um ponteiro para char table[N];Em contraste, um char*ptr simples permanece compatível com um const char*argumento.
Ciano,
4
Pode ser útil observar que, para acessar um elemento de sua matriz, você precisa fazer (*p)[i]e não fazer *p[i]. O último irá pular pelo tamanho da matriz, que quase certamente não é o que você deseja. Pelo menos para mim, aprender essa sintaxe causou, ao invés de prevenir, um erro; Eu teria obtido o código correto mais rápido apenas passando um float *.
Andrew Wagner
1
Sim @mickey, o que você sugeriu é um constponteiro para um array de elementos mutáveis. E sim, isso é completamente diferente de um ponteiro para uma matriz de elementos imutáveis.
Cyan
11

Eu gostaria de acrescentar à resposta de AndreyT (no caso de alguém tropeçar nesta página procurando por mais informações sobre este tópico):

À medida que começo a brincar mais com essas declarações, percebo que há uma grande desvantagem associada a elas em C (aparentemente não em C ++). É bastante comum ter uma situação em que você gostaria de dar a um chamador um ponteiro const para um buffer no qual você escreveu. Infelizmente, isso não é possível ao declarar um ponteiro como este em C. Em outras palavras, o padrão C (6.7.3 - Parágrafo 8) está em desacordo com algo assim:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Essa restrição não parece estar presente em C ++, tornando esse tipo de declaração muito mais útil. Mas no caso de C, é necessário voltar a uma declaração de ponteiro regular sempre que quiser um ponteiro const para o buffer de tamanho fixo (a menos que o próprio buffer tenha sido declarado const para começar). Você pode encontrar mais informações neste tópico de e-mail: texto do link

Esta é uma restrição severa na minha opinião e pode ser uma das principais razões pelas quais as pessoas geralmente não declaram ponteiros como este em C. O outro é o fato de que a maioria das pessoas nem mesmo sabe que você pode declarar um ponteiro como este como AndreyT apontou.

figurassa
fonte
2
Isso parece ser um problema específico do compilador. Consegui duplicar usando o gcc 4.9.1, mas o clang 3.4.2 foi capaz de ir de uma versão não const para a const sem problemas. Eu li a especificação C11 (p 9 na minha versão ... a parte falando sobre dois tipos qualificados serem compatíveis) e concordo que parece dizer que essas conversões são ilegais. No entanto, sabemos na prática que você sempre pode converter automaticamente de char * para char const * sem aviso. IMO, clang é mais consistente em permitir isso do que gcc, embora eu concorde com você que a especificação parece proibir qualquer uma dessas conversões automáticas.
Doug Richardson
4

O motivo óbvio é que este código não compila:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

A promoção padrão de uma matriz é para um ponteiro não qualificado.

Veja também esta questão , o uso foo(&p)deve funcionar.

Keith Randall
fonte
3
É claro que foo (p) não funcionará, foo está pedindo um ponteiro para um array de 10 elementos, então você precisa passar o endereço do seu array ...
Brian R. Bondy
9
Como essa é a "razão óbvia"? É, obviamente, entendido que a maneira correta de chamar a função é foo(&p).
AnT
3
Acho que "óbvio" é a palavra errada. Eu quis dizer "mais direto". A distinção entre p e & p neste caso é bastante obscura para o programador C médio. Alguém que tenta fazer o que o pôster sugeriu escreverá o que escrevi, receberá um erro de tempo de compilação e desistirá.
Keith Randall
2

Também quero usar essa sintaxe para permitir mais verificação de tipo.

Mas também concordo que a sintaxe e o modelo mental de uso de ponteiros são mais simples e fáceis de lembrar.

Aqui estão mais alguns obstáculos que encontrei.

  • O acesso à matriz requer o uso de (*p)[]:

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }

    É tentador usar um ponteiro para char local em vez disso:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }

    Mas isso anularia parcialmente o propósito de usar o tipo correto.

  • É preciso lembrar de usar o operador address-of ao atribuir o endereço de uma matriz a um ponteiro para matriz:

    char a[10];
    char (*p)[10] = &a;

    O operador address-of obtém o endereço de toda a matriz &a, com o tipo correto para atribuí-lo p. Sem o operador, aé convertido automaticamente para o endereço do primeiro elemento da matriz, igual a in &a[0], que possui um tipo diferente.

    Uma vez que essa conversão automática já está ocorrendo, sempre fico intrigado de que isso &seja necessário. É consistente com o uso de &variáveis ​​on de outros tipos, mas tenho que lembrar que um array é especial e que preciso do &para obter o tipo correto de endereço, embora o valor do endereço seja o mesmo.

    Uma razão para o meu problema pode ser que eu aprendi K&R C nos anos 80, que ainda não permitia o uso do &operador em matrizes inteiras (embora alguns compiladores ignorassem isso ou tolerassem a sintaxe). O que, a propósito, pode ser outro motivo pelo qual os ponteiros para matrizes têm dificuldade em serem adotados: eles só funcionam corretamente desde ANSI C, e a &limitação do operador pode ter sido outro motivo para considerá-los muito estranhos.

  • Quando nãotypedef é usado para criar um tipo para o ponteiro para array (em um arquivo de cabeçalho comum), um ponteiro global para array precisa de uma declaração mais complicada para compartilhá-lo entre arquivos:extern

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
Orafu
fonte
1

Bem, simplesmente, C não faz as coisas dessa maneira. Um array de tipo Té passado como um ponteiro para o primeiroT no array, e isso é tudo que você obtém.

Isso permite alguns algoritmos interessantes e elegantes, como loop através da matriz com expressões como

*dst++ = *src++

A desvantagem é que o gerenciamento do tamanho depende de você. Infelizmente, a falha em fazer isso conscienciosamente também levou a milhões de bugs na codificação C e / ou oportunidades para exploração malévola.

O que se aproxima do que você pergunta em C é passar um struct (por valor) ou um ponteiro para um (por referência). Contanto que o mesmo tipo de estrutura seja usado em ambos os lados dessa operação, tanto o código que distribui a referência quanto o código que a usa estão de acordo sobre o tamanho dos dados que estão sendo tratados.

Sua estrutura pode conter quaisquer dados que você deseja; ele pode conter seu array de um tamanho bem definido.

Ainda assim, nada impede que você ou um codificador incompetente ou malévolo usem casts para enganar o compilador para tratar sua estrutura como de tamanho diferente. A habilidade quase desenfreada de fazer esse tipo de coisa faz parte do projeto de C.

Carl Smotricz
fonte
1

Você pode declarar uma matriz de caracteres de várias maneiras:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

O protótipo de uma função que recebe uma matriz por valor é:

void foo(char* p); //cannot modify p

ou por referência:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

ou por sintaxe de array:

void foo(char p[]); //same as char*
s1n
fonte
2
Não se esqueça de que um array de tamanho fixo também pode ser alocado dinamicamente como char (*p)[10] = malloc(sizeof *p).
AnT
Veja aqui uma discussão mais detalhada entre as diferenças de char array [] e char * ptr aqui. stackoverflow.com/questions/1807530/…
t0mm13b
1

Eu não recomendaria esta solução

typedef int Vector3d[3];

uma vez que obscurece o fato de que Vector3D tem um tipo que você deve conhecer. Os programadores geralmente não esperam que variáveis ​​do mesmo tipo tenham tamanhos diferentes. Considere:

void foo(Vector3d a) {
   Vector3D b;
}

onde sizeof a! = sizeof b

Per Knytt
fonte
Ele não estava sugerindo isso como uma solução. Ele estava simplesmente usando isso como exemplo.
figurassa
Hm. Por que sizeof(a)não é o mesmo que sizeof(b)?
Sherrellbc
0

Talvez eu esteja faltando alguma coisa, mas ... já que os arrays são ponteiros constantes, basicamente isso significa que não há motivo para passar ponteiros para eles.

Você não poderia simplesmente usar void foo(char p[10], int plen);?

Fortran
fonte
4
Arrays NÃO são indicadores constantes. Leia algumas perguntas frequentes sobre arrays, por favor.
AnT
2
Para o que importa aqui (arrays unidimensionais como parâmetros), o fato é que eles decaem para ponteiros constantes. Leia um FAQ sobre como ser menos pedante, por favor.
fortran
-2

No meu compilador (vs2008), ele trata char (*p)[10]como uma matriz de ponteiros de caracteres, como se não houvesse parênteses, mesmo se eu compilar como um arquivo C. O compilador suporta esta "variável"? Nesse caso, esse é um dos principais motivos para não usá-lo.

Tyson Jacobs
fonte
1
-1 Errado. Funciona bem no vs2008, vs2010, gcc. Em particular, este exemplo funciona bem: stackoverflow.com/a/19208364/2333290
kotlomoy