paulo1205
(usa Ubuntu)
Enviado em 04/02/2023 - 06:27h
ApprenticeX escreveu:
paulo1205 escreveu: O GCC até tem como emitir alerta a respeito desse problema, mas, para tanto, ele tem de fugir do que diz o padrão da linguagem C, alterando o tipo resultante de uma constante literal de string de char [N+1 ] (onde N é o número de caracteres entre aspas) para const char[N+1 ] , como é em C++. Para habilitar essa alteração, a opção a ser usada é -Wwrite-strings .
Confuso mesmo! Fiz uns testes baseados na sua resposta.
Compilando em c++ ele avisa do erro!
g++ test.cpp -o test -march=native -Ofast -Wextra -pedantic -pedantic-errors -Werror
error: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
Modificando a função para const
void Change(const char *Text) {
Alterar o tipo do parâmetro para poder receber uma
string constante não parece fazer muito sentido aqui, já que a função tem o nome de “
change ”. O certo seria você, como programador, garantir que o objeto que você vai passar como argumento pode realmente ser alterado.
Conforme sua instrução em adicionar -Wwrite-strings em C
Não foi instrução. Foi apenas uma informação de como obter o diagnóstico, pagando o preço de fazer o compilador alterar o tipo de dados da
string (para aquilo que deveria ser o tipo certo desde o início, na minha opinião...).
gcc test.c -o test -march=native -Ofast -Wextra -pedantic -pedantic-errors -Werror -Wwrite-strings
Porém a informação do erro não é tão esclarecedor qto a de c++
error: passing argument 1 of 'Change' discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
Engraçado... Eu acho até mais esclarecedora.
Passarei a adicionar -Wwrite-strings ao compilar em C
Ainda estou me familiarizando com o que deve ou não ser constante.
Uma constante literal não me parece deixar muita dúvida.
Mas intriga o fato que de uma forma geral, a maioria dos valores são variáveis ! Fico pensando, se passo um valor para uma variável, subentende-se que ele vai variar!
Apesar do nome, nem toda variável varia. Se você disse que o tipo de dados de uma variável é constante (por exemplo:
const int ,
const double ou mesmo
void *const ), ela continuará sendo classificada como variável, de acordo com a nomenclatura dos padrões do C e do C++, mas frequentemente o compilador vai tomar medidas para que seus valores não possam ser alterados.
Além disso, eu não sei se você está com a visão correta sobre algumas características da linguagem.
Em particular, quando você tem funções em C, a passagem de argumentos é sempre por valor — ou, como eu costumo falar, na expectativa de deixar ainda mais claro, por
cópia de valor . Isso significa que quando você chama uma função em alguma parte do seu programa, os valores dos argumentos passados à função são calculados e
copiados para os respectivos parâmetros da função.
Isso implica que o programa
#include <stdio.h>
int f(int x, int y){
x+=5;
y*=2;
return x+y;
}
int main(void){
int a=0;
int r=f(a, 3);
printf("a=%d; r=%d\n", a, r);
} vai imprimir como saída “
a=0; r=11 ”, e vai funcionar da seguinte forma (num PC de 32 bits†):
• Ao iniciar a execução em
main (), será alocado espaço para avariável
a , e nele será guardado o valor
0 .
• Será alocado espaço para a variável
r , e o valor atribuído a ela dependerá da invocação da função
f (), o que é feito do seguinte modo:
• Para invocar
f (), o compilador primeiro
faz uma cópia do valor da constante literal
3 para a pilha do processador.
• Em seguida, o valor de
a (que é
0 ) é lido do local em que é armazenado, e é feita uma cópia dele na pilha do processador.
• A função
f () é invocada.
• Uma vez dentro de
f , o que acontece é o seguinte:
• O último argumento colocado na pilha é associado ao parâmetro
x e o penúltimo argumento colocado na pilha ao valor de
y (no nosso caso, portanto,
x tem, por enquanto, uma cópia do valor de
a (que é
0 ), e
y tem o valor
3 ).
• O valor de
x , que está guardado na pilha, é incrementado em
5 (passando, portanto, a valer
5 ).
• O valor de
y , que também está guardado na pilha, é multiplicado por
2 (passando, portanto, a valer
6 ).
• Os valores de
x (
5 ) e de
y (
6 ) são somados (resultando em
11 ) e colocados no acumulador (registrador
EAX do processador), que é convencionalmente a forma de indicar o valor de retorno da função.
• A função retorna ao ponto em que foi chamada.
• Após o retorno da função
f (), o espaço que havia sido usado na pilha para copiar os argumentos é devolvido (isto é: soma-se
sizeof x + sizeof y ao indicador do topo da pilha).
• O valor de retorno da função (
11 ), que ainda está no acumulador, é copiado para a variável local
r .
• Através da chamada a
printf (), os valores de
a (que ainda vale
0 ) e de
r ) são impressos, de acordo com a
string de formatação da saída.
O que eu desejo que fique claro com a ilustração acima é que ocorre dentro da função não afeta aquilo que foi usado como argumento, porque aquilo que a função recebe em seus parâmetros é apenas uma cópia do que quer que tenha sido usado como argumentos. A função até pode alterar esses parâmetros, mas como eles estão fisicamente separados dos argumentos usados para produzir seus valores originais, tais alterações seguramente não vão alterar esses originais.
Entretanto, uma possibilidade que o uso de ponteiros oferece é poder chegar a um dado original mesmo quando o programa tem um desvio de fluxo no qual aquele dado não estaria visível, tal como quando uma função é invocada. Ao se copiar o mesmo endereço de um determinado dado
N vezes, todas as
N cópias podem ser usada para chegar ao mesmo dado.
Essa é, aliás, uma das razões pelas quais o uso de ponteiros em C é tão importante e tão aparentemente mais frequente do que em outras linguagens. Em C, se você quiser, por exemplo, que uma função tenha acesso a um objeto que foi declarado fora dela, e não simplesmente a uma cópia desse objeto, não adianta tentar passar tal objeto como argumento diretamente, já que argumentos de funções são sempre copiados; entretanto, se você passar como argumento uma cópia do endereço no qual tal objeto reside, é evidente que essa cópia do endereço pode ser usada para chegar ao mesmo objeto.
Outras linguagens têm mecanismos para especificar quando o parâmetro de uma função deve receber cópias dos valores do argumentos (passagem de argumentos por valor) ou quando devem poder ser manipulados diretamente por essas funções, tipicamente através de passagem de argumentos por referência. Eis alguns exemplos completamente equivalentes em Pascal e em C++ de programas que imprimem como saída o valores
5 6 , obtidos a partir de duas variável inicialmente definidas com valor
5 , mas com uma delas sendo passada a uma função por valor, e outra passada a outra função por referência.
{ Código em Pascal }
program x;
procedure p1(n: integer);
begin
n:=succ(n);
end;
procedure p2(var n: integer);
begin
n:=succ(n);
end;
var m, n: integer;
begin
m:=5; n:=5;
p1(m);
p2(n);
writeln(m, ' ', n);
end. // Código em C++
#include <iostream>
void p1(int n){
++n;
}
void p2(int &n){
++n;
}
int main(){
int m=5, n=5;
p1(m);
p2(n);
std::cout << m << ' ' << n << '\n';
}
Uma curiosidade de ambos os programas acima é que as chamadas
p1(m) e a
p2(n) são visualmente muito parecidas: em ambas, o argumento é simplesmente uma variável, sem nada que indique que a primeira usa passagem por valor e a segunda usa passagem por referência. Em ambos os casos, o programador tem de conhecer a semântica das funções para saber quem pode modificar o valor do argumento ou não (neste caso, é simples olhar a implementação de cada função nos corpos dos programas; entretanto, num caso geral, o programa pode ser muito longo, dificultando olhar outras partes, ou mesmo usar uma bibliotecas externas, às quais o leitor do código poderia não ter acesso para inferir qual tipo de passagem de argumentos é usada).
C, por sua vez, simplesmente não possui passagem de argumentos por referência, mas apenas por valor. Assim sendo, se você quiser o efeito de poder modificar um objeto declarado fora de uma função, tem de escrever a função de modo a explicitamente receber (uma cópia de) o endereço do objeto a ser modificado, e, ao chamar a função, você tem de explicitamente obter o endereço do objeto. Assim sendo, a forma de escrever uma versão em C dos programas acima seria a seguinte.
#include <stdio.h>
void p1(int n){
++n;
}
void p2(int *n){ // Recebo uma cópia do endereço (ponteiro) para o objeto.
++*n; // Explicito que quero incrementar o conteúdo apontado pelo parâmetro (“*n”), não meramente o parâmetro em si mesmo.
}
int main(){
int m=5, n=5;
p1(m);
p2(&n); // Indicação explícita de que se está passando um endereço (“&n” produz um ponteiro (ou referência) para o dado representado por n).
printf("%d %d\n", m, n);
}
A grande diferença está já realçada nos comentários: na falta de um mecanismo de passagem por referência, o programa em C é forçado obter referências explícitas para os dados que têm de ser alterados e usar essas referência explícitas como argumentos, que são passados por valor (isto é: são copiados) para as funções,que, por sua vez, são obrigadas a usar argumentos com tipos de dado que são ponteiros a fim de receber (por valor, ou seja, cópias) os endereços dos objetos e a indicar também explicitamente cada vez que quiserem manipular o conteúdo apontado, em lugar de apenas o parâmetro em si.
Cada forma de fazer tem suas vantagens e desvantagens aparentes: as de Pascal e de C++ são visualmente mais simples, mas dão margem a confusão na hora de ler o código (principalmente para quem o lê pela primeira vez, ou volta a ele depois de muito tempo sem vê-lo); já em C, ter de usar um
& antes do nome do objeto já serve como sinal de que a função pode vir a alterar o valor desse objeto, mas também obriga o programador a escrever um pouco a mais de código. Entretanto, não existe mágica: as três versões são rigorosamente equivalentes, e é bem provável que, exceto pelo momento de escrever a saída final, os códigos executáveis das três sejam extremamente parecidos, se não completamente idênticos.
OK. Falei um monte de coisa e dei exemplos que eu espero que tenham sido claros, mas todos eles foram com tipos de dados simples, não com tipos compostos.
No caso em questão, temo que você esteja achando, quer pelo seu entendimento anterior, quer depois de minha insistência, acima, de que a passagem de argumentos em C é sempre por (cópia de) valor, que algo como
puts("Esta é uma string"); passaria uma cópia de todos os caracteres da
string para dentro da função. Não é o caso, porque o tipo de
"Esta é uma string" é
array com 18 elementos do tipo
char (supondo codificação UTF-8, na qual o caráter “
é ” ocupa dois
bytes ), e, sendo um
array usado numa expressão que espera um
rvalue , ele decai automaticamente para um ponteiro para seu primeiro elemento (§6.3.2.1, parágrafo 3 do padrão do C de 2011; §7.3.2 do padrão de 2020 do C++). Assim sendo, a cópia de valor do argumento é tão-somente desse endereço do primeiro elemento, não é do conteúdo do
array .
Então quando a sua função original
Change () é chamada com uma constante literal de
string , ela recebe o endereço de um
array que é constante, e quando ela usa o endereço para alterar um elemento indexado a partir desse endereço, vai tentar alterar uma posição de memória que é, na prática, constante, e isso causa a falha de segmentação.
Por isso acho intrigante, o compilador armazenar um valor constante, sem saber se ele será ou não constante!
O compilador sabe que é constante, sim. Mas o padrão do C manda (e, como eu disse em outra mensagem, eu discordo radicalmente dessa escolha) que o sistema de tipos não carregue a informação de que é constante. Tanto o compilador sabe, que, quando você usa aquela opção
-Wwrite-strings , ele ignora o mandato do padrão do C a esse respeito, plenamente considerando o tipo dos elementos como constantes.
Uma abordagem que eu recomendo é a seguinte: prefira C++ em lugar do C.
Ou seja, ele decidiu por conta própria que os dados que passei são constantes e que eu não os mudaria, mesmo eu informando que minha função não é constante void Change(char *Text)
Não é por conta própria. Como eu expliquei anteriormente, o padrão permite que constantes literais de
strings sejam alocadas em memória que não possa ser posteriormente alterada, e o compilador faz isso porque fazê-lo tem diversas vantagens (de cara, o fato de que uma
string que é uma
constante literal parece tornar muito adequado seu armazenamento em memória que seja inalterável; mas há outras potenciais vantagens, tais como permitir reutilizar a totalidade ou partes desse mesmo
array para outras
strings que ocorram ao longo do programa, a fim de economizar espaço). O problema todo é o conflito entre essa permissão vantajosa e o tipo de dados não-constante que é preferido em função de manter compatibilidade com código antigo.
E parte do problema é também o fato de você não conhecer esses detalhes sobre a linguagem. Contudo, entendo que você, por estar começando, tem o direito de errar, e o fato de você ter cometido esse erro abriu a porta para o aprendizado, através desta discussão realizada aqui.
Não sei se estou errado, mas com meu pouco conhecimento, me parece que quem está programando é o compilador neste caso!
Discordo. O compilador está seguindo à risca as regras da linguagem que você se propôs a usar. Quem não as seguiu, por não as conhecer, foi você.
Se as regras referentes a esse aspectos são estapafúrdias, é outra discussão.
Eu criei uma função não-constante! Passo um dado para minha função que não deveria ser constante, pq em momento algum, eu disse ao meu programa que estou trabalhando com valores constantes!
Eu diria que uma constante literal deveria ser constante, sim.
Se você não quer que ela seja constante, então não deve usar uma constante literal, mas sim um
array . Você, aliás, fez isso duas linhas acima, quando declarou o
array Text , com tipo de caracteres não-constante (note que, naquela declaração, o texto entre aspas não é uma constante literal, mas sim uma representação dos valores iniciais do
array não constante (i.e. cujos elementos não são constantes) que está sendo declarado. Apenas recapitulando, mas com comentários explicativos, compare as duas linhas no seguinte código.
char text[]="Teste"; // O array ‘text’ não é constante, e este texto entre as aspas representa os elementos iniciais do array.
char *ptr="Teste"; // Este texto entre aspas é uma constante literal de strings, que pode ser (e geralmente é) alocado em memória somente de leitura
// Atribuí-lo a (ou passá-lo como argumento para uma função com um parâmetro que seja) um ponteiro para dados não constantes é
// um risco, mas é algo que o padrão do C diz que tem de ser assim. Com -Wwrite-strings (ou em C++, que tem regras diferentes do C),
// esta segunda linha provocaria erro.
MAS o Compilador do Supremo, decidiu sem me consultar que é constante e pronto! É estranho!
Nada disso! Você que começou a usar uma ferramenta que não entendia totalmente — a linguagem C — e que tem umas idiossincrasias não intuitivas.
De novo meu conselho: prefira C++.
Talvez exista um motivo pra colocarem essa situação como constante, mas sai um pouco da lógica qdo trabalhamos com variáveis, são variáveis pq obviamente a todo instante estamos variando seus valores por um ou outro motivo. No meu caso, eu apenas enviei uma string a um ponteiro, que compreendo ele ao invés de armazenar um outro local da memória não constante e apontar para lá, o que ocorreu foi ele apontar para o local onde o compilador por conta própria salvou como constante
Como dito acima, o motivo é compatibilidade com código antigo, de uma época em que não havia a palavra-chave
const nem o atributo que ela designa. Não tem vontade nem conta própria do compilador interferindo com seu código: tem uma regra que faz parte da definição da linguagem, que o compilador simplesmente está seguindo.
Também já falei (eu acho) sobre a questão de terminologia. O conceito de variável em programação não é necessariamente exatamente o mesmo que tem em matemática. Grosseiramente falando, em programação, variável é uma notação de um objeto que representa um dado de um determinado tipo. Ponto. Se o tipo do dado representado tem um atributo que o faz constante, o valor dessa variável, depois de inicializado, não vai poder ser alterado, e, para tanto, o compilador vai tomar medidas para que evitar que você viole esse atributo do dado (inclusive, eventualmente, armazenando-o numa memória protegida contra escrita). Mas, tirando esses cuidados, do ponto de vista de funcionamento (como dispor em memória, qual a quantidade de memória ocupada, como fazer acessos, como converter para outros tipos, etc.), uma variável “comum” e uma que não pode ser alterada são parecidas demais para serem chamadas de nomes distintos.
Além disso, note o seguinte: na declaração
const char *nome="ApprenticeX"; a variável
nome não é constante — o que é constantes são os dados para os quais ela aponta. Compare as seguintes declarações.
char ac[]="Teste"; // Array com 6 caracteres não-constantes.
const char acc[]="Teste"; // Array com 6 caracteres constantes.
char *pc=ac; // Ponteiro não-constante para caracteres não-constantes.
const char *pcc=acc; // Ponteiro não-constante para caracteres constantes.
char *const cpc=ac; // Ponteiro constante para caracteres não-constantes.
const char *const cpcc=acc; // Ponteiro constante para caracteres constantes.
Ponto curioso é que mesmo usando: gcc test.c -o test -std=c89
Ele dá o problema, o que significa que o compilador aparentemente SEMPRE salvou como constante desde aquela 1989 acredito
Errado. Se você reparar na mensagem de erro dessa compilação, verá que o erro se refere ao fato de que o C89 não aceitava comentários iniciados por
// . Não tem nada a ver com usar ponteiros para dados não-constantes para referir-se a constantes literais de
strings .
----
† Eu descrevi a chamada de função típica em plataforma Intel de 32 bits (i386) porque ela é (ou era) bem conhecida e relativamente uniforme (havia variações, mas não eram tão comuns assim) entre vários sistemas operacionais diferentes (desde a época do 8086, de 16 bits, na verdade). Nossos sistemas de 64 bits (amd64 ou x86-64) são menos uniformes a esse respeito, com o mundo UNIX usando uma convenção diferente do Windows: ambos preferem passar dados através de registradores, em vez de usar somente a pilha, mas a quantidade, o conjunto e a ordem dos registradores em cada um desses mundos é diferente, assim como são diferentes os critérios para usar a pilha, quer seja quando os registradores não são suficientes para armazenar todos os parâmetros, quer porque os tipos dos parâmetros não permitem o uso de registradores.
... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)