Por que os compiladores C e C ++ permitem comprimentos de array em assinaturas de function quando elas nunca são aplicadas?

Isto é o que eu encontrei durante o meu período de aprendizagem:

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

Então, na variável int dis(char a[1]) , o [1] parece não fazer nada e não funciona
tudo, porque eu posso usar a[2] . Apenas como int a[] ou char *a . Eu sei que o nome da matriz é um ponteiro e como transmitir uma matriz, então meu quebra-cabeça não é sobre essa parte.

O que eu quero saber é por que os compiladores permitem esse comportamento ( int a[1] ). Ou tem outros significados que eu não conheço?

É uma peculiaridade da syntax para passar matrizes para funções.

Na verdade, não é possível passar uma matriz em C. Se você escrever uma syntax que pareça que deve passar a matriz, o que realmente acontece é que um ponteiro para o primeiro elemento da matriz é passado em seu lugar.

Como o ponteiro não inclui nenhuma informação de comprimento, o conteúdo de seu [] na lista de parâmetros formais da function é realmente ignorado.

A decisão de permitir essa syntax foi tomada na década de 1970 e causou muita confusão desde então …

O comprimento da primeira dimensão é ignorado, mas o comprimento de dimensões adicionais é necessário para permitir que o compilador calcule deslocamentos corretamente. No exemplo a seguir, a function foo recebe um ponteiro para uma matriz bidimensional.

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

O tamanho da primeira dimensão [10] é ignorado; o compilador não impedirá que você indexe o final (observe que o formal quer 10 elementos, mas o real fornece apenas 2). No entanto, o tamanho da segunda dimensão [20] é usado para determinar a passada de cada linha, e aqui, o formal deve corresponder ao real. Novamente, o compilador não impedirá que você indexe o final da segunda dimensão também.

O deslocamento de byte da base da matriz para um elemento args[row][col] é determinado por:

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

Note que se col >= 20 , você irá indexar em uma linha subseqüente (ou fora do final de toda a matriz).

sizeof(args[0]) , retorna 80 na minha máquina onde sizeof(int) == 4 . No entanto, se eu tentar usar sizeof(args) , recebo o seguinte aviso do compilador:

 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. 

Aqui, o compilador está avisando que só vai dar o tamanho do ponteiro no qual o array decaiu em vez do tamanho do próprio array.

O problema e como superá-lo em C ++

O problema foi explicado extensivamente por Pat e Matt . O compilador está basicamente ignorando a primeira dimensão do tamanho da matriz, ignorando efetivamente o tamanho do argumento passado.

Em C ++, por outro lado, você pode facilmente superar essa limitação de duas maneiras:

  • usando referências
  • usando std::array (desde C ++ 11)

Referências

Se sua function está apenas tentando ler ou modificar uma matriz existente (não copiá-la), você pode facilmente usar referências.

Por exemplo, suponhamos que você queira ter uma function que redefina uma matriz de dez int definindo cada elemento como 0 . Você pode fazer isso facilmente usando a seguinte assinatura de function:

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

Não só isso vai funcionar bem , mas também reforçará a dimensão da matriz .

Você também pode usar modelos para tornar o código acima genérico :

 template void reset(Type (&array)[N]) { ... } 

E, finalmente, você pode tirar proveito da exatidão do const . Vamos considerar uma function que imprima uma matriz de 10 elementos:

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

Ao aplicar o qualificador const , estamos evitando possíveis modificações .


A class de biblioteca padrão para matrizes

Se você considerar a syntax acima tanto feia quanto desnecessária, como eu faço, podemos jogá-la na lata e usar std::array lugar (desde C ++ 11).

Aqui está o código refatorado:

 void reset(std::array& array) { ... } void show(std::array const& array) { ... } 

Não é maravilhoso? Sem mencionar que o truque de código genérico que ensinei anteriormente, ainda funciona:

 template void reset(std::array& array) { ... } template void show(const std::array& array) { ... } 

Não só isso, mas você começa a copiar e mover semântica de graça. 🙂

 void copy(std::array array) { // a copy of the original passed array // is made and can be dealt with indipendently // from the original } 

Então, o que você está esperando? Vá usar std::array .

É um recurso divertido de C que permite que você atire no próprio pé, se você estiver inclinado.

Eu acho que o motivo é que C é apenas um passo acima da linguagem assembly. Verificação de tamanho e resources de segurança semelhantes foram removidos para permitir desempenho de pico, o que não é uma coisa ruim se o programador está sendo muito diligente.

Além disso, atribuir um tamanho ao argumento de function tem a vantagem de que, quando a function é usada por outro programador, há uma chance de eles notarem uma restrição de tamanho. Apenas usando um ponteiro não transmite essa informação para o próximo programador.

Primeiro, C nunca verifica os limites da matriz. Não importa se eles são locais, globais, estáticos, parâmetros, o que for. Verificar os limites da matriz significa mais processamento, e o C deve ser muito eficiente, portanto, a verificação dos limites da matriz é feita pelo programador quando necessário.

Em segundo lugar, há um truque que possibilita a passagem por valor de uma matriz para uma function. Também é possível retornar por valor uma matriz de uma function. Você só precisa criar um novo tipo de dados usando struct. Por exemplo:

 typedef struct { int a[10]; } myarray_t; myarray_t my_function(myarray_t foo) { myarray_t bar; ... return bar; } 

Você tem que acessar os elementos como este: foo.a [1]. O extra “.a” pode parecer estranho, mas esse truque adiciona grande funcionalidade à linguagem C.

Para informar ao compilador que myArray aponta para uma matriz de pelo menos 10 ints:

 void bar(int myArray[static 10]) 

Um bom compilador deve fornecer um aviso se você acessar myArray [10]. Sem a palavra-chave “estática”, os 10 não significam nada.

Este é um “recurso” bem conhecido de C, passado para o C ++ porque o C ++ deve compilar corretamente o código C.

Problema surge de vários aspectos:

  1. Um nome de array deve ser completamente equivalente a um ponteiro.
  2. C é supostamente rápido, originalmente desenvolvido para ser um tipo de “Assembler de alto nível” (especialmente projetado para escrever o primeiro “Sistema Operacional portátil”: Unix), então não é suposto inserir código “oculto”; a verificação do intervalo de tempo de execução é, portanto, “proibida”.
  3. O código de máquina generalizado para acessar uma matriz estática ou dinâmica (na pilha ou alocada) é realmente diferente.
  4. Como a function chamada não pode conhecer o “tipo” de array passado como argumento, tudo é suposto ser um ponteiro e tratado como tal.

Você poderia dizer que os arrays não são realmente suportados em C (isso não é realmente verdade, como eu estava dizendo antes, mas é uma boa aproximação); um array é realmente tratado como um ponteiro para um bloco de dados e acessado usando aritmética de pointers. Desde C não tem qualquer forma de RTTI Você tem que declarar o tamanho do elemento da matriz no protótipo da function (para suportar a aritmética do ponteiro). Isso é ainda mais “verdadeiro” para matrizes multidimensionais.

De qualquer forma, tudo acima não é mais verdade: p

A maioria dos compiladores C / C ++ modernos suportam a verificação de limites, mas os padrões exigem que ele seja desativado por padrão (para compatibilidade com versões anteriores). Versões razoavelmente recentes do gcc, por exemplo, fazem a verificação do intervalo em tempo de compilation com “-O3 -Wall -Wextra” e a verificação completa dos limites de tempo de execução com “-fbounds-checking”.

C não apenas transformará um parâmetro do tipo int[5] em *int ; dada a declaração typedef int intArray5[5]; , ele irá transformar um parâmetro do tipo intArray5 para *int também. Existem algumas situações em que esse comportamento, embora ímpar, é útil (especialmente com coisas como a va_list definida em stdargs.h , que algumas implementações definem como uma matriz). Seria ilógico permitir como parâmetro um tipo definido como int[5] (ignorando a dimensão) mas não permitir que int[5] seja especificado diretamente.

Acho absurdo o manuseio de parâmetros do tipo array em C, mas isso é uma consequência dos esforços para se ter uma linguagem ad-hoc, grande parte dos quais não eram particularmente bem definidos ou pensados, e tentar chegar a um comportamento comportamental. especificações que são consistentes com o que implementações existentes fizeram para os programas existentes. Muitas das peculiaridades de C fazem sentido quando vistas sob essa luz, particularmente se considerarmos que, quando muitas delas foram inventadas, grandes partes da linguagem que conhecemos hoje ainda não existiam. Pelo que entendi, no predecessor de C, chamado BCPL, os compiladores não acompanharam muito bem os tipos de variables. Uma declaração int arr[5]; foi equivalente a int anonymousAllocation[5],*arr = anonymousAllocation; ; uma vez que a alocação foi reservada. o compilador não sabia nem se importava se arr era um ponteiro ou um array. Quando acessado como arr[x] ou *arr , seria considerado como um ponteiro, independentemente de como foi declarado.

Uma coisa que ainda não foi respondida é a questão real.

As respostas já fornecidas explicam que as matrizes não podem ser passadas por valor para uma function em C ou C ++. Eles também explicam que um parâmetro declarado como int[] é tratado como se tivesse o tipo int * , e que uma variável do tipo int[] pode ser passada para tal function.

Mas eles não explicam por que nunca foi feito um erro para fornecer explicitamente um tamanho de array.

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

Por que o último deles não é um erro?

Uma razão para isso é que causa problemas com typedefs.

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

Se fosse um erro para especificar o comprimento da matriz nos parâmetros da function, você não seria capaz de usar o nome myarray no parâmetro da function. E como algumas implementações usam tipos de matriz para tipos de biblioteca padrão, como va_list , e todas as implementações são necessárias para tornar um tipo de array jmp_buf , seria muito problemático se não houvesse uma maneira padrão de declarar parâmetros de function usando esses nomes: sem essa capacidade, não poderia haver uma implementação portátil de funções como o vprintf .