Então, tenho feito alguns projetos de aprendizagem em C e estou começando a ficar mais confortável com ponteiros. No entanto, me deparei com alguns fenômenos sobre os quais não consigo encontrar muitas informações.
Basicamente, eu crio um array, para simplificar, estou começando com um array de inteiros:
int x[2] = {1, 3};
Agora, se eu quiser modificar o conteúdo deste array, descobri que posso criar uma função que recebe um ponteiro inteiro como parâmetro, passa x
como argumento e desreferencia x
especificando um índice.
#include <stdio.h>
void foo(int* input){
input[0] = 2;
input[1] = 4;
}
int main(){
int x[2] = {1, 3};
printf("x[0] before: %d\nx[1] before: %d\n", x[0], x[1]);
foo(x);
printf("x[0] after: %d\nx[1] after: %d\n", x[0], x[1]);
}
saída
x[0] before: 1
x[1] before: 3
x[0] after: 2
x[1] after: 4
o que é interessante, mas não vejo isso sendo feito com frequência, então não tenho certeza se é aceitável.
Agora, para a questão maior, por algum motivo, sempre que quero alterar o endereço para o qual o próprio ponteiro aponta, especificando um novo valor e ponteiro e, em seguida, definindo o argumento para esse novo ponteiro, isso só parece funcionar quando eu faço o seguinte:
#include <stdio.h>
#include <stdlib.h>
void foo(int** input){
int x[3] = {2, 4};
int** ptr = x;
*input = *ptr;
}
int main(){
int x[2];
x[0] = 1;
x[1] = 3;
printf("x[0] before: %d\n", x[0]);
printf("x[1] before: %d\n", x[1]);
foo(x);
printf("x[0] after: %d\n", x[0]);
printf("x[1] after: %d\n", x[1]);
return 0;
}
Saída:
x[0] before: 1
x[1] before: 3
x[0] after: 2
x[1] after: 4
Então, tenho várias perguntas para as quais não consigo encontrar uma resposta, provavelmente devido à minha falta de vocabulário técnico:
1: Sempre que passo o array com e sem referência como argumento, por que o argumento se comporta como um array de ponteiros, sem inicializar um array de ponteiros e defini-lo primeiro para o endereço do array? Há alguma consequência não intencional em fazer isso?
2: Se o argumento for um ponteiro, em vez de um ponteiro para um ponteiro, é aceitável passar um ponteiro como argumento para uma função que recebe um ponteiro duplo como parâmetro, como eu fiz? Existe algum comportamento indefinido ao fazer isso que eu deva saber?
3: Por que isso funciona?
É normal e comum fornecer um array como argumento para um parâmetro que é um ponteiro. O array é automaticamente convertido em um ponteiro para seu primeiro elemento.
Essa conversão é realizada sempre que uma matriz é usada em uma expressão diferente de como operando de
sizeof
, como operando de unário&
, como operando de um operador typeof ou como um literal de string usado para inicializar uma matriz.Sobre este código:
Isso é horrível e você nunca deveria fazer isso.
O que realmente está acontecendo aqui é:
x
como argumento. Isso foi automaticamente convertido em um ponteiro para o seu primeiro elemento. Ou seja, umint *
cujo valor é&x[0]
.foo
é declarado para receber umint **
, então o compilador converteu o ponteiro para o tipoint **
. Mas ele ainda aponta parax[0]
.int** ptr = x;
inicializaptr
para apontar para o primeiro elemento dox
local tofoo
. Novamente, o compilador converte o ponteiro para o tipoint **
.*input = *ptr;
,ptr
é desreferenciado. Como seu tipo éint **
, o compilador espera que ele se refira a umint *
. Na sua implementação em C, umint *
ocupa oito bytes, então o compilador carrega oito bytes da memória.int
são quatro bytes, então oito bytes são doisint
. Quando o compilador carrega oito bytes da memória, ele obtém os doisint
para os quais o ponteiro está apontando, portanto, ele obtém os bytes dex[0]
ex[1]
(no array localx
).*input
, o compilador espera*input
que se refira a oito bytes e grava os oito bytes carregados na memória. Esta é a memória dex[0]
ex[1]
emmain
, então esses elementos são substituídos pelos bytes dex
emfoo
.Tudo isso "funcionou" por coincidência. Na verdade, trata-se de um comportamento que não é definido pelo padrão C: essas conversões implícitas de ponteiros não são definidas, e o acesso à memória usando o tipo errado não é definido. Se você habilitar a otimização no compilador, o programa pode parar de "funcionar".
Não há nada de errado com o seu primeiro programa. No entanto, o segundo programa está invocando um comportamento indefinido devido à violação da regra de aliasing estrita , pois está acessando um
int
como se fosse umint *
.O que está acontecendo no seu segundo programa é o seguinte:
A linha
não é permitido no C padrão, pois converte a
int *
para anint **
sem uma conversão de tipo explícita. O compilador Clang apenas emite um aviso, enquanto o compilador GCC rejeitará esta linha imediatamente. Portanto, presumo que você esteja usando o compilador Clang.O compilador Clang tratará esta linha como se você tivesse usado uma conversão de tipo explícita:
Na próxima linha
Você está tratando o ponteiro
ptr
como se estivesse apontando para um objeto do tipoint *
, embora ele esteja apontando para um simplesint
. Como mencionado anteriormente, isso viola a regra estrita de aliasing (invocando, assim, um comportamento indefinido), mas, no seu caso, parece que você teve sorte e que funciona como esperado. Isso provavelmente se deve ao fato de você estar em uma plataforma na qual os ponteiros têm 64 bits de tamanho, que por acaso é o mesmo tamanho de todo o array.Para a CPU, copiar um ponteiro de 64 bits é o mesmo que copiar um array de dois valores de 32 bits, porque o tamanho total dos dados é de 64 bits em ambos os casos. No entanto, da perspectiva do compilador, não é a mesma coisa, pois você está quebrando as regras da linguagem C, e tais coisas podem facilmente dar errado se o compilador decidir reorganizar seu código para fins de otimização.
Se você quiser que a função
foo
altere a variávelx
na funçãomain
para que ela se refira a um array diferente, você deve tornar a variávelx
um ponteiro em vez de um array, para que ela tenha a capacidade de apontar para um array diferente. Se você então passar um ponteiro para o ponteirox
da funçãofoo
, a funçãofoo
poderá usar esse ponteiro para alterar o valor do ponteirox
, de modo que ele aponte para um array diferente. Aqui está um exemplo:Este programa tem a seguinte saída: