paulo1205
(usa Ubuntu)
Enviado em 16/12/2019 - 02:20h
princknoby escreveu:
Acho que entendi o uso de & e *
Corrija-me se eu estiver errado:
Eu uso passagem por referência quando quero alterar o valor de uma variável na função que está chamando outra função.
Por exemplo tenho uma variável x, na main
Vou chamar uma função void alterar() {} que vai alterar o valor de x na função main, portanto devo passar a variável x por referência.
alterar(&x);
E devo colocar ela como ponteiro na função (pois quero alterar o que tem no endereço que me foi passado). Então a função ficaria:
void alterar (int *x) {
}
Até agora está correto?
Basicamente, sim. Permita-me só fazer dois esclarecimentos, o primeiro por causa de terminologia que você usou, e o segundo por ser um outro caso de uso comum.
Em C, todas as passagens de argumentos para funções são sempre e exclusivamente por valor (ou, como eu costumo dizer para ficar bem claro, por “cópia de valor”). Não existe nada parecido com a funcionalidade que algumas outras linguagens, tais como as que existem em Fortran, Pascal, C++ ou Visual BASIC, em que você determina a passagem por referência ou por valor na hora de declarar a função, e o compilador automaticamente decida se, numa invocação tal como “
func(x, y) ”, os argumentos
x e
y serão passados por valor ou por referência. Em C, se você quiser um efeito semelhante ao de passagem por referência, tem manualmente de obter uma referência (i.e. um ponteiro) para o objeto e passar,
por valor , o ponteiro obtido manualmente como parâmetro da função.
Eu enfatizo isso porque você disse algo na linha de que se você quiser “chamar uma função (...) que vai alterar o valor de
x (...), devo passar a variável
x por referência”. Eu prefiro dizer, no contexto particular do C (e também no de outras linguagens que trabalham com ponteiros diretamente), que você tem de passar “uma referência/ponteiro para a variável
x ” por (cópia de) valor (até porque, em C, não existe outra opção).
A segunda observação é que a passagem por referência não é útil apenas para modificar dentro da função um objeto existente fora dela. Ela também pode ser usada para evitar a cópia de valor, caso tal cópia seja muito custosa, como costuma ser no caso de estruturas com muitos campos ou campos que sejam grandes. No caso do C, funções que recebam ponteiros para dados que não serão modificados costumam qualificar a não-alteração por meio do modificador
const aplicado ao tipo do dado apontado.
Agora para alterar o valor, porque devo ou não usar ponteiro, e porque devo ou não usar '&'.
Por exemplo:
scanf ("%d", &x);
scanf ("%d", &*x);
scanf ("%d", x);
O primeiro scanf, pelo que entendi está errado, pois x é um ponteiro, o segundo está correto, porém o uso de "&" não é necessário, já que novamente a variável x é um ponteiro, e eu nao preciso de * pois seria redundância já que a variável x JÁ É um ponteiro!
Portanto o último scanf é o mais correto?
Para saber qual o correto, depende de qual o tipo do argumento.
Antes de falar especificamente de
scanf (), vamos ver uma outra coisa primeiro. Suponha que você tem a seguinte função, que realiza a função de trocar os valores de dois objetos inteiros (obviamente referidos por meio de ponteiros).
void swap(int *pa, int *pb){
int temp=*pa;
*pa=*pb;
*pb=temp;
}
Como a função vai ser chamada vai depender do contexto. Veja alguns exemplos válidos e inválidos, incluindo explicações breves dos porquês.
int main(void){
int
x=1, y=10,
ax[10]={1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
ay[10]={10, 20, 30, 40, 50, 60, 70, 80, 90, 100},
*px, *py // Note que eu deliberadamente não defini os valores iniciais dos ponteiros, e portanto não sei para onde apontam.
;
swap(x, y); // ERRO: tipos dos argumentos (int) não correspondem aos tipos dos parâmetros (int *), e o compilador não infere
// automaticamente a passagem por referência.
swap(&x, &y); // OK: obtém manualmente dois ponteiros que, além de satisfazem os tipos dos parâmetros, correspondem a objetos válidos.
swap(px, py); // PROBLEMA: Sintaticamente OK, pois os tipos dos valores de px e py correspondem aos dos parâmetros; contudo,
// como não se sabe para onde eles apontam, isso pode dar problema.
px=&x; py=&y; // Faço com que os ponteiros apontem para locais válidos (x e y, respectivamente).
swap(px, py); // OK. Aqui fica bem claro que eu pego os valores dos ponteiros para, indiretamente, alterar os valores de x e y.
swap(&px, &py); // ERRO: fazendo isso, eu estaria trocando os valores de px e de py, não de x e y; contudo, os tipos de &px
// e &py (int **) não são compatíveis com os dos parâmetros da função (int *).
swap(ax, ay); // OK: troca o primeiro elemento de ax com o primeiro elemento de ay (quando se usa um array numa expressão que não seja
// sizeof nem & aplicado diretamente sobre ele, “ax” é absolutamente idêntico a “&ax[0]”, ou seja, um ponteiro para
// seu primeiro elemento).
swap(&ax[0], &ay[0]); // OK: absolutamente sinônima da linha anterior.
swap(&ax, &ay); // ERRO: na presença de & , um array tal como ax e ay não tem o sentido de ponteiro para o primeiro
// elemento, mas sim um ponteiro para o array inteiro, tomado como um só elemento de dados, cujo tipo,
// no caso, é “int (*)[10]” (ponteiro para array com 10 elementos inteiros).
swap(&ax[4], &ay[4]); // OK: troca os quintos elementos dos dois vetores (lembrando que o índice 0 indica o primeiro elemento).
swap(ax+4, ay+4); // OK: sinônimo da linha acima.
swap(&ax[0]+4, &ay[0]+4); // OK: outro sinônimo das duas linhas anteriores (&ax[0] e &ay[0] têm o tipo “int *”, e a aritmética de ponteiros
// garante que o deslocamento “+4” vai apontar para o endereço correto de “4 elementos depois daquele para que
// aponto agora“ (quatro após primeiro = quinto)).
px=ax; py=&ay[0]; // Ambos apontam para os primeiros elementos dos respectivos arrays.
swap(px, py); // OK: Por referência indireta, troco os primeiros elementos dos dois arrays.
swap(&x+4, 4+&y); // PROBLEMA: sintaticamente OK, mas embora &x e &y obtenham ponteiros para x e y, respetivamente, a aritmética de
// ponteiros só deveria ser usada para ponteiros que efetivamente se refiram a arrays. O risco aqui é que os valores de
// ponteiros calculados vão apontar para memória que não pertence a nenhuma das duas variáveis envolvidas na operação,
// e que não se pode saber a priori o que está em tais endereços.
swap(&(x+4), &(4+y)); // ERRO: o operador & , de obtenção de endereço/referência/ponteiro, só pode ser aplicado sobre entidades
// que tenham um local bem definido na memória (lvalue ); x+4 e 4+y são valores temporários, obtidos por cálculo
// e não vinculados a nenhuma área de memória determinada, de modo que não faz sentido aplicar o operador & sobre eles.
swap(*(&ax+4), (&ay)[4]); // PROBLEMA: sintaticamente OK, mas o que acontece em ambos os argumentos é que eu obtenho o ponteiro
// para o array inteiro e, por meio de abuso da aritmética de ponteiros, me desloco para o quinto “array inteiro”
// e obtenho o que seria o valor de tal “array inteiro”, o qual decai automaticamente para ponteiro para seu
// primeiro elemento, cujo tipo é compatível com o do parâmetro da função, mas que reside em área de memória
// que não pertence a nenhuma das variáveis envolvidas nessas manipulações.
swap(2, 3); // ERRO: tipos inválidos (int para os argumentos, int * para os parâmetros).
swap(&2, &3); // ERRO: não é possível obter referências/ponteiros para constantes literais do programa.
swap(NULL, NULL); // PROBLEMA: sintaticamente OK, mas ponteiros nulos nunca indicam objetos válidos.
swap((int *)2, (int *)3); // PROBLEMA: sintaticamente OK, mas é improvável que os endereços 2 e 3 da memória correspondam a endereços válidos
// para o programa.
}
Note que os usos que eu indiquei com “OK” são aqueles em que os tipos dos argumentos combinam com os tipos dos parâmetros declarados da função e, além disso, se referem a ponteiros que são válidos. Os que estão marcados com “PROBLEMA” são aqueles em que os tipos dos argumentos estão corretos, mas que envolvem algum comportamento temerário, tal como usar um ponteiro não inicializado ou que sabidamente aponta para um endereço inválido e suspeito, e que em alguns casos requerem sintaxes obscuras e operações ou conversões de tipos suspeitas (tais como a que aplica deslocamento sobre endereços de objetos que não são diretamente declarados como
arrays ).
No caso de algumas coisas que eu marquei com “ERRO”, algumas são erros óbvios, que nenhum compilador deixaria passar (tal como aplicar
& sobre constantes literais ou sobre operações com
x e
y ). Outras, no entanto, apesar de produzirem argumentos com tipos incompatíveis com os dos parâmetros, não necessariamente provocam erros de compilação, mas apenas alertas sobre incompatibilidades de tipos, que o compilador, se não tiver sido instruído para deliberadamente proibir, acaba deixando passar, por motivos que, no meu entendimento, são principalmente para compatibilidade com código antigo, quando o C não tinha os mesmos mecanismos que tem hoje para a detecção de código potencialmente problemático, mas que às vezes até funciona (um exemplo desses é “
swap(&ax, &ay) ”, porque embora os argumentos sejam de um tipo que indica ponteiro para o
array inteiro, em lugar de ponteiro para o primeiro elemento, numericamente o
array inteiro e seu primeiro elementos estão na mesma posição de memória). Contudo, como eu sempre recomendo que todas as compilações de diagnóstico de código perigoso sejam ativadas e façam com que o compilador interrompa a compilação (no caso do GCC, são as opções “
-Wall -Werror -O2 -pedantic-errors ”), preferi indicar logo como erro.
E, depois dessa brincadeira, volto a sua pergunta com relação a
scanf () e a outros aspectos que não apareceram o exemplo acima.
O problema com
scanf () é parecido com o que eu procurei ilustrar, mas com uma diferença importante:
scanf () utiliza uma sintaxe do C para funções com parâmetros variáveis. Ela é declarada numa forma parecida com “
int scanf(const char *fmt, ...) ”, indicando que o tipo de retorno é
int e que o primeiro parâmetro, que indica uma
string de formatação, tem de ser um ponteiro constante para caracteres, mas aquele “
... ” indica que os demais parâmetros são opcionais e que, se estiverem presentes, seus tipos não são conhecidos de antemão. O compilador, em princípio, não tem elementos suficientes para verificar se os tipos dos argumentos que porventura venham depois da
string de formatação estão corretos ou não.
Alguns compiladores, inclusive o GCC, possuem código especializado para, durante a compilação, tratar de modo diferenciado das demais funções as que são da família de
scanf () (que inclui ela própria e também
fscanf (),
sscanf () e outras variações), no qual aplica uma heurística que procura ver se existe uma correspondência aproximada entre as conversões especificadas na
string de formatação e a quantidade e os tipos dos demais argumentos. Tal heurística tem limitações sérias, porque só funciona quando a
string de formatação é especificada diretamente como constante literal
string , não quando chega através de uma variável (o que é perfeitamente válido), e não consegue enxergar além da quantidade e tipos dos argumentos para ver, por exemplo, se o ponteiro de caracteres correspondente a uma leitura de
string é realmente um vetor de tamanho suficiente, e não um vetor muito curto ou o endereço de um único caráter, nem muito menos se tal ponteiro tem um endereço válido.
Mesmo assim, nos exemplos que você mostrou, a heurística a que me referi, se usada, deve ajudar a eliminar os erros em que os tipos dos dados forem completamente estapafúrdios. Voltando ao seu exemplo, copiado abaixo com pequenas modificações, temos o seguinte:
scanf ("%d", &x); // Caso 1
scanf ("%d", &*x); // Caso 2
scanf ("%d", x); // Caso 3
• Se o tipo de
x for
int , o caso 1 é sintaticamente válido, o caso 2 dá erro de compilação porque não se pode aplicar o operador
* sobre um inteiro, e o caso 3 seria interceptado pela heurística por causa de tipo de dados incompatível (sem ela, possivelmente o argumento seria silenciosamente aceito, e é praticamente garantido que daria falha de segmentação na hora da execução).
• Se o tipo de
x for um
array com elementos dos tipo
int , o caso 1 teria um argumento cujo tipo é “ponteiro para o
array inteiro”, o que seria um erro de tipo e que a heurística pegaria (sem ela, o compilador provavelmente seguiria em frente, e talvez até o programa funcionasse, apesar do erro conceitual e que
deve ser evitado, a despeito dos resultados , por causa da coincidência fortuita entre o ponteiro para o
array inteiro e o ponteiro para um único elemento inteiro, que é o que a conversão espera). O caso 3 é perfeitamente válido, por causa do decaimento automático do
array em ponteiro para o primeiro elemento. No caso 2, o decaimento automático também acontece, e o operador
* aplicado a esse ponteiro faz com que ele seja “derreferenciado”, isto é, com que se obtenha o
lvalue ao qual o ponteiro se referia; esse
lvalue calculado, por sua vez, é passado como argumento para o operador
& , que obtém de novo o endereço do
lvalue , produzindo um efeito final
como se o
* e o
& se anulassem mutuamente (o “como se” está em itálicos porque tal efeito é apenas aparente: as expressões envolvidas têm de fazer sentido, e elas são, nesse caso, interpretadas da direita para a esquerda, como explicado; primeiro com
* aplicado a um ponteiro válido, e depois com
& aplicado a um
lvalue ).
• Se o tipo de
x for um
int * , o caso 1 teria um argumento que seria do tipo
int ** , incompatível com o que a função espera, o que seria detectado e apontado pela heurística (sem ela, o compilador seguiria em frente, mas a execução do programa não poderia nem ao menos contar com a coincidência fortuita entre endereço de primeiro elemento e endereço do
array inteiro, porque simplesmente não existe
array vinculado a
x ). Os casos 2 e 3 são válidos, desde que, durante a execução,
x aponte para um endereço válido.
• Se
x for de qualquer outro tipo que não seja ponteiro, tem-se um erro crasso no caso 2, e incompatibilidades de tipos de dados nos casos 1 e 3, os quais a heurística pode apontar. Sem ela, o código terá erros semânticos que deveriam ser corrigidos, mas que não seriam interceptados pelo compilador, e podem produzir executáveis cujo comportamento é indefinido.
• Se
x for um
array com elementos de qualquer tipo diferente de
int ou ponteiro para qualquer outro tipo que não seja
int , a heurística vai alarmar em todos os casos se estiver em uso. Caso contrário, a compilação possivelmente se completaria, e o comportamento do programa seria indefinido em cada um dos casos.
Porém não função que criei para listar os clientes cadastrados, ainda não entendi o porque eu passei a struct por valor, e nos parametros da função eu tive que usar ponteiros... Eu não deveria usar ponteiros apenas quando faço passagem por referência?!
Ainda não entendi sobre quando usar ou não usar ponteiros no parâmetro de funções, mas creio que estou começando a entender...
Estritamente falando, como já mencionei anteriormente, você não passa nada por referência, pois todas as passagens de argumentos em C são por valor.
No caso da sua função
mostrarClientes (), aí mesmo é que a passagem não é por referência, mas por valor: o tipo do argumento
pD que será usado com a função, declarado logo no começo do bloco de
main (), é “
dados * ”, e você é obrigado a que seja assim porque está usando alocação dinâmica. O que você gostaria de como tipo do argumento de
mostrarClientes (), lembrando que os parâmetros receberão meras cópias de valores dos argumentos? Pensemos juntos:
• Se o tipo do parâmetro fosse simplesmente “
dados ”, a invocação da função teria de escolher um único elemento do
array dinamicamente alocado
pD , e passar tal elemento por cópia de valor. Mesmo que o elemento escolhido fosse o primeiro elemento de
pD , dentro da função ela receberia uma mera cópia desse elemento, de modo que ela não conseguiria voltar ao
array original mesmo que aplicasse o operador
& à cópia, porque taj cópia, como sugere o sentido mesmo da palavra, não é exatamente a mesma coisa que o original: ainda que os valores sejam iguais, é outro objeto, que reside em outra posição de memória.
• Fazer como você fez, usando o mesmo tipo para o parâmetro e para o argumento que lhe corresponde. Como esse tipo já é um tipo ponteiro, ele necessariamente já tem a função de guardar o endereço do objeto que realmente interessa. Quando você passa por valor (até porque não tem outro jeito) um endereço para uma função, o valor copiado será o mesmo endereço e pode, portanto, ser usado para chegar ao mesmo objeto, com os mesmos direitos de acesso, isto é, se o ponteiro original podia ser usado tanto para ler quanto para alterar o objeto, sua cópia também poderá ser usada tanto para ler quanto para alterar o objeto.
• Usar um parâmetro que seja um ponteiro para objeto constante (i.e. “
const dados * “) , qualificando que a função não irá modificar o objeto que lhe for passado, mesmo que o ponteiro original pudesse ser usado pudesse ser usado de modo a alterar o objeto apontado. Continua sendo uma passagem por valor (você copia o valor do ponteiro), mas a função fica restrita quanto à operações que pode fazer, e quem a usa tem a garantia de não ocorrerá nenhuma mutação nos elementos do
array dentro da função.
• Passar uma referência para o
array dinâmico a ser manipulado como parâmetro da função (e.g. “
dados ** ”), mas isso só seria realmente útil se a função precisasse realizar operações que tivessem não apenas de modificar os elementos do
array , mas também o próprio
array (por exemplo, para realocá-lo ou liberá-lo).
• Passar uma referência constante para o
array dinâmico (e.g. “
dados *const * ”) faria menos sentido ainda do que a anterior, pois eu não poderia modificar o
array dinâmico como um todo (realocando-o ou desalocando-o), mas poderia alterar livremente seus elementos.
• Passar uma referência constante para um
array com elementos constantes (e.g. “
const dados *const * ”) poderia fazer um pouco mais de sentido, mas além de não acrescentar nada em termos de funcionalidade em relação à terceira opção (além do custo de ter de obter manualmente a referência ao argumento e de ter de “derreferenciar” o parâmetro para usá-lo dentro da função), ainda demandaria uma conversão explícita do tipo resultante da expressão “
&pD ”, porque em C a conversão de
X * para
const X *const * não é automática, pois os dois tipos são considerados incompatíveis entre si (uma infelicidade que eu espero que seja remediada numa próxima versão do C, porque já o foi em C++ desde o padrão de 2011 dessa linguagem).
A terceira opção, portanto, me parece a melhor.
Não sei se eu ajudei ou atrapalhei com todo esse falatório e exemplos. Fique à vontade para perguntar qualquer coisa que você porventura não tenha entendido (mas tente pensar um pouquinho antes de perguntar, pois muita coisa se esclarece quando você consegue pensar nos tipos resultantes das operações que envolvam os operadores
[] ,
* e
& ).
... “Principium sapientiae timor Domini, et scientia sanctorum prudentia.” (Proverbia 9:10)