Por que os compiladores C e C ++ permitem comprimentos de matriz em assinaturas de funções quando eles nunca são impostos?

131

Foi o que encontrei durante meu período de aprendizado:

#include<iostream>
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
, porque eu posso usar a[2]. Assim como int a[]ou char *a. Eu sei que o nome do array é um ponteiro e como transmitir um array, 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?

Fanl
fonte
6
Isso porque você não pode realmente passar matrizes para funções.
27414 Ed Ed
37
Eu acho que a pergunta aqui foi por que C permite que você declare um parâmetro como do tipo array, quando ele se comportará exatamente como um ponteiro de qualquer maneira.
Brian
8
@ Brian: Não tenho certeza se este é um argumento a favor ou contra o comportamento, mas também se aplica se o tipo de argumento for um typedeftipo de matriz. Assim, a "decadência para ponteiro" em tipos de argumento não é apenas açúcar sintático substituindo []com *, isso realmente está acontecendo através do sistema tipo. Isso tem consequências reais para alguns tipos padrão como va_listesse que podem ser definidos com o tipo de matriz ou sem matriz.
R .. GitHub Pare de ajudar o gelo
4
@songyuanyao Você pode fazer algo não totalmente diferente em C (e C ++), utilizando um ponteiro: int dis(char (*a)[1]). Em seguida, você passar um ponteiro para um array: dis(&b). Se você estiver disposto a usar recursos C que não existem no C ++, também pode dizer coisas como void foo(int data[static 256])e int bar(double matrix[*][*]), mas essa é outra lata de worms.
Stuart Olsen
1
@StuartOlsen A questão não é qual padrão definiu o quê. A questão é por que quem a definiu definiu dessa maneira.
user253751

Respostas:

156

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

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

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

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

MILÍMETROS
fonte
21
Como programador não-C, acho esta resposta muito acessível. 1
asteri 27/03
21
+1 para "A decisão de permitir essa sintaxe foi tomada na década de 1970 e causou muita confusão desde então ..."
NoSenseEtAl
8
isso é verdade, mas também é possível transmitir uma matriz desse tamanho usando a void foo(int (*somearray)[20])sintaxe. nesse caso, 20 é aplicado nos sites de chamada.
v.oddou
14
-1 Como programador C, acho esta resposta incorreta. []não são ignorados em matrizes multidimensionais, como mostra a resposta de pat. Portanto, a inclusão da sintaxe da matriz era necessária. Além disso, nada impede o compilador de emitir avisos, mesmo em matrizes unidimensionais.
user694733
7
Por "o conteúdo do seu []", estou falando especificamente sobre o código na pergunta. Essa peculiaridade de sintaxe não era necessária, o mesmo pode ser alcançado usando a sintaxe do ponteiro, ou seja, se um ponteiro for passado, exija que o parâmetro seja um declarador de ponteiro. Por exemplo, no exemplo de pat, void foo(int (*args)[20]);Além disso, estritamente falando, C não possui matrizes multidimensionais; mas possui matrizes cujos elementos podem ser outras matrizes. Isso não muda nada.
MM
143

O comprimento da primeira dimensão é ignorado, mas o comprimento de dimensões adicionais é necessário para permitir que o compilador calcule as compensações corretamente. No exemplo a seguir, a foofunção passa um ponteiro para uma matriz bidimensional.

#include <stdio.h>

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á a indexação final (observe que o formal deseja 10 elementos, mas o real fornece apenas 2). No entanto, o tamanho da segunda dimensão [20]é usado para determinar o passo de cada linha e, aqui, o formal deve corresponder ao real. Novamente, o compilador também não impedirá que você indexe o final da segunda dimensão.

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

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

Observe que col >= 20, se você realmente indexará em uma linha subsequente (ou fora do final de toda a matriz).

sizeof(args[0]), retorna 80na minha máquina onde sizeof(int) == 4. No entanto, se eu tentar fazer isso 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 avisa que fornecerá apenas o tamanho do ponteiro no qual a matriz se deteriorou, em vez do tamanho da própria matriz.

tapinha
fonte
Muito útil - a consistência com isso também é plausível como o motivo da peculiaridade no caso 1-d.
GTC
1
É a mesma ideia que o caso 1-D. O que parece uma matriz 2-D em C e C ++ é na verdade uma matriz 1-D, sendo que cada elemento é outra matriz 1-D. Neste caso, temos uma matriz com 10 elementos, cada um dos quais é "matriz de 20 polegadas". Conforme descrito no meu post, o que realmente é passado para a função é o ponteiro para o primeiro elemento de args. Nesse caso, o primeiro elemento de args é uma "matriz de 20 ints". Ponteiros incluem informações de tipo; o que é passado é "ponteiro para uma matriz de 20 polegadas".
MM
9
Sim, é esse o int (*)[20]tipo; "ponteiro para uma matriz de 20 polegadas".
pat
33

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, efetivamente ignorando 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 função estiver apenas tentando ler ou modificar uma matriz existente (sem copiá-la), você poderá usar facilmente as referências.

Por exemplo, suponha que você queira ter uma função que redefina uma matriz de dez ints configurando cada elemento para 0. Você pode fazer isso facilmente usando a seguinte assinatura de função:

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

Não apenas isso funcionará bem , mas também aplicará a dimensão da matriz .

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

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

E, finalmente, você pode tirar proveito da constcorreção. Vamos considerar uma função que imprime uma matriz de 10 elementos:

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

Ao aplicar o constqualificador, estamos impedindo possíveis modificações .


A classe de biblioteca padrão para matrizes

Se você considera a sintaxe acima feia e desnecessária, como eu, podemos jogá-la na lata e usá-la std::array(desde C ++ 11).

Aqui está o código refatorado:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

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

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Não apenas isso, mas você copia e move a semântica gratuitamente. :)

void copy(std::array<Type, N> 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.

Sapato
fonte
2
@ kietz, desculpe sua edição sugerida ter sido rejeitada, mas assumimos automaticamente que o C ++ 11 está sendo usado , a menos que especificado de outra forma.
Shoe
isso é verdade, mas também devemos especificar se alguma solução é apenas C ++ 11, com base no link que você forneceu.
trlkly
@trlkly, eu concordo. Eu editei a resposta de acordo. Obrigado por apontar isso.
307 /
9

É uma característica divertida de C que permite que você atire no seu pé com eficiência, se você estiver tão inclinado.

Eu acho que a razão é que C é apenas um passo acima da linguagem assembly. A verificação de tamanho e os recursos de segurança semelhantes foram removidos para permitir o desempenho máximo, o que não é ruim se o programador estiver sendo muito diligente.

Além disso, atribuir um tamanho ao argumento da função tem a vantagem de que quando a função é usada por outro programador, há uma chance de eles perceberem uma restrição de tamanho. Apenas usar um ponteiro não transmite essas informações para o próximo programador.

conta
fonte
3
Sim. C foi projetado para confiar no programador no compilador. Se você está tão flagrantemente indexando o final de uma matriz, deve estar fazendo algo especial e intencional.
John
7
Eu cortei meus dentes na programação em C 14 anos atrás. De tudo o que meu professor disse, a única frase que ficou mais comigo do que todas as outras "C foi escrita por programadores, para programadores". A linguagem é extremamente poderosa. (Prepare-se para o clichê) Como o tio Ben nos ensinou: "Com grande poder, vem grande responsabilidade".
Andrew Falanga
6

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

Segundo, existe um truque que torna possível passar por valor uma matriz para uma função. Também é possível retornar por valor uma matriz de uma função. 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 ".a" extra pode parecer estranho, mas esse truque adiciona grande funcionalidade à linguagem C.

user34814
fonte
7
Você está confundindo a verificação de limites de tempo de execução com a verificação de tipo em tempo de compilação.
Ben Voigt
@ Ben Voigt: Eu estou falando apenas de verificação de limites, como é a pergunta original.
user34814
2
@ user34814 A verificação de limites em tempo de compilação está dentro do escopo da verificação de tipo. Vários idiomas de alto nível oferecem esse recurso.
Leushenko
5

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

void bar(int myArray[static 10])

Um bom compilador deve avisar se você acessar o myArray [10]. Sem a palavra-chave "estática", os 10 não significariam nada.

gnasher729
fonte
1
Por que um compilador deve avisar se você acessa o 11º elemento e a matriz contém pelo menos 10 elementos?
Nwellnhof 27/03
Presumivelmente, isso ocorre porque o compilador só pode impor que você tenha pelo menos 10 elementos. Se você tentar acessar o 11º elemento, ele não poderá ter certeza de que ele existe (mesmo que exista).
Dylan Watson
2
Não acho que seja uma leitura correta do padrão. [static]permite que o compilador avise se você chamar bar com um int[5]. Não determina o que você pode acessar dentro bar . O ônus é inteiramente do lado do chamador.
tab
3
error: expected primary-expression before 'static'nunca vi essa sintaxe. é improvável que seja C ou C ++ padrão.
v.oddou
3
@ v.oddou, está especificado em C99, em 6.7.5.2 e 6.7.5.3.
Samuel Edwin Ward
5

Esse é um "recurso" conhecido do C, passado para o C ++ porque o C ++ deve compilar corretamente o código C.

O problema surge de vários aspectos:

  1. Um nome de matriz deve ser completamente equivalente a um ponteiro.
  2. C deveria ser rápido, originalmente desenvolvido para ser uma espécie de "Assembler de alto nível" (especialmente projetado para escrever o primeiro "Sistema operacional portátil": Unix), portanto, não é necessário inserir código "oculto"; a verificação do intervalo de tempo de execução é "proibida".
  3. O código de máquina gerado para acessar um array estático ou dinâmico (na pilha ou alocado) é realmente diferente.
  4. Como a função chamada não pode conhecer o "tipo" de matriz passada como argumento, tudo deve ser um ponteiro e tratado como tal.

Você poderia dizer que matrizes não são realmente suportadas em C (isso não é verdade, como eu estava dizendo antes, mas é uma boa aproximação); uma matriz é realmente tratada como um ponteiro para um bloco de dados e acessada usando a aritmética do ponteiro. Como C NÃO possui nenhuma forma de RTTI, é necessário declarar o tamanho do elemento da matriz no protótipo da função (para suportar a aritmética do ponteiro). Isso é ainda "mais verdadeiro" para matrizes multidimensionais.

Enfim, tudo acima não é mais verdade: p

A maioria dos compiladores moderno C / C ++ fazer limites de apoio a verificação, mas normas requerem 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 compilação com "-O3 -Wall -Wextra" e a verificação completa dos limites de tempo de execução com "-fbounds -ecking".

ZioByte
fonte
Talvez C ++ foi supostamente para compilar o código C há 20 anos, mas certamente é não, e não tem por um longo tempo (C ++ 98? C99, pelo menos, que não foi "corrigido" por qualquer novo C ++ padrão).
Hyde
@hyde Isso parece um pouco duro demais para mim. Para citar o Stroustrup "Com pequenas exceções, C é um subconjunto do C ++." (O C ++ PL quarta ed., Seção 1.2.1). Enquanto C ++ e C evoluem ainda mais, e existem recursos da versão mais recente em C que não estão na versão mais recente em C ++, no geral, acho que a citação do Stroustrup ainda é válida.
MVW
@mvw A maioria dos códigos C escritos neste milênio, que não é intencionalmente compatível com C ++, evitando recursos incompatíveis, usará a sintaxe dos inicializadores designados C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) para inicializar estruturas, porque é uma maneira muito mais clara de inicializar uma estrutura. Como resultado, a maioria dos códigos C atuais será rejeitada pelos compiladores C ++ padrão, porque a maioria dos códigos C estará inicializando estruturas.
Hyde
@mvw Pode-se dizer, talvez, que C ++ seja compatível com C, para que seja possível escrever código que será compilado com os compiladores C e C ++, se forem feitos alguns compromissos. Mas isso requer o uso de um subconjunto de ambos C e C ++, não apenas subconjunto de C ++.
Hyde #
@hyde Você ficaria surpreso com o quanto do código C é compilável em C ++. Alguns anos atrás, o kernel Linux inteiro era compilável em C ++ (não sei se ainda é verdade). Rotineiramente eu compilo o código C no compilador C ++ para obter uma verificação de aviso superior, apenas a "produção" é compilada no modo C para reduzir o máximo de otimização.
ZioByte 10/04
3

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 intArray5de *intbem. Existem algumas situações em que esse comportamento, embora estranho, é útil (especialmente em itens como o va_listdefinido 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 int[5]que seja especificado diretamente.

Acho absurdo o manuseio de C por parâmetros do tipo array, mas é uma conseqüência dos esforços para adotar uma linguagem ad-hoc, grande parte da qual não foi particularmente bem definida ou pensada, e tentar criar comportamentos. especificações consistentes com o que as implementações existentes fizeram para os programas existentes. Muitas das peculiaridades de C fazem sentido quando vistas sob essa luz, especialmente se considerarmos que, quando muitas delas foram inventadas, grande parte da linguagem que conhecemos hoje ainda não existia. Pelo que entendi, no antecessor de C, chamado BCPL, os compiladores não acompanharam muito bem os tipos de variáveis. Uma declaração int arr[5];era equivalente aint anonymousAllocation[5],*arr = anonymousAllocation; ; uma vez que a alocação foi anulada. o compilador não sabia nem se importava searrera um ponteiro ou uma matriz. Quando acessado como um arr[x]ou *arr, seria considerado um ponteiro, independentemente de como foi declarado.

supercat
fonte
1

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

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

Mas eles não explicam por que nunca foi cometido um erro ao fornecer explicitamente um comprimento de matriz.

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

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

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

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

Se fosse um erro especificar o comprimento da matriz nos parâmetros da função, você não conseguiria usar o myarraynome no parâmetro da função. 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 criar jmp_bufum tipo de matriz, seria muito problemático se não houvesse uma maneira padrão de declarar parâmetros de função usando esses nomes: sem essa capacidade, poderia não deve ser uma implementação portátil de funções como vprintf.


fonte
0

É permitido que os compiladores possam verificar se o tamanho da matriz passada é o mesmo que o esperado. Compiladores podem avisar um problema, se não for o caso.

hamidi
fonte