O que torna esse uso de ponteiros imprevisível?

108

No momento, estou aprendendo dicas e meu professor forneceu este trecho de código como exemplo:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

Ele escreveu nos comentários que não podemos prever o comportamento do programa. O que exatamente o torna imprevisível? Eu não vejo nada errado com isso.

Trungnt
fonte
2
Tem certeza de que reproduziu o código do professor corretamente? Embora seja formalmente possível argumentar que esse programa pode produzir um comportamento "imprevisível", não faz sentido fazê-lo. E duvido que qualquer professor usaria algo tão esotérico para ilustrar "imprevisível" para os alunos.
AnT
1
@Lightness Races in Orbit: Compiladores podem "aceitar" códigos malformados após a emissão das mensagens de diagnóstico necessárias. Mas a especificação da linguagem não define o comportamento do código. Ou seja, devido ao erro na inicialização do sprograma, se aceito por algum compilador, formalmente tem um comportamento imprevisível.
ANT
2
@TheParamagneticCroissant: Não. A inicialização é mal formada nos tempos modernos.
Lightness Races in Orbit
2
@O Croissant Paramagnético: Como eu disse acima, a linguagem não requer código malformado para "falhar ao compilar". Os compiladores são simplesmente necessários para emitir um diagnóstico. Depois disso, eles podem continuar e compilar o código "com sucesso". No entanto, o comportamento desse código não é definido pela especificação do idioma.
AnT
2
Adoraria saber qual foi a resposta que seu professor deu a você.
Daniël W. Crompton

Respostas:

125

O comportamento do programa é inexistente porque está malformado.

char* s = "My String";

Isso é ilegal. Antes de 2011, ele estava obsoleto por 12 anos.

A linha correta é:

const char* s = "My String";

Fora isso, o programa está bom. Seu professor deveria beber menos uísque!

Lightness Races in Orbit
fonte
10
com -pedantic ele faz: main.cpp: 6: 16: aviso: ISO C ++ proíbe converter uma constante de string em 'char *' [-Wpedantic]
marcinj
17
@black: Não, o fato da conversão ser ilegal torna o programa malformado. Foi obsoleto no passado . Não estamos mais no passado.
Lightness Races in Orbit
17
(O que é bobo porque esse era o propósito da suspensão de uso de 12 anos)
Lightness Races in Orbit
17
@ preto: O comportamento de um programa malformado não está "perfeitamente definido".
Lightness Races in Orbit
11
Independentemente disso, a questão é sobre C ++, não sobre alguma versão particular do GCC.
Corridas de leveza em órbita
81

A resposta é: depende de qual padrão C ++ você está compilando. Todo o código está perfeitamente bem formado em todos os padrões ‡ com exceção desta linha:

char * s = "My String";

Agora, a string literal tem tipo const char[10]e estamos tentando inicializar um ponteiro não const para ela. Para todos os outros tipos, exceto a charfamília de literais de string, essa inicialização sempre foi ilegal. Por exemplo:

const int arr[] = {1};
int *p = arr; // nope!

No entanto, no pré-C ++ 11, para literais de string, havia uma exceção em §4.2 / 2:

Um literal de string (2.13.4) que não é um literal de string largo pode ser convertido em um rvalue do tipo “ ponteiro para char ”; [...]. Em qualquer dos casos, o resultado é um ponteiro para o primeiro elemento da matriz. Essa conversão é considerada apenas quando há um tipo de alvo de ponteiro apropriado explícito e não quando há uma necessidade geral de converter de um lvalue em um rvalue. [Nota: esta conversão está obsoleta . Consulte o Anexo D. ]

Portanto, em C ++ 03, o código está perfeitamente bem (embora obsoleto) e tem um comportamento claro e previsível.

No C ++ 11, esse bloco não existe - não existe essa exceção para literais de string convertidos em char*, e portanto o código é tão malformado quanto o int*exemplo que acabei de fornecer. O compilador é obrigado a emitir um diagnóstico e, idealmente, em casos como este, que são violações claras do sistema de tipo C ++, esperaríamos que um bom compilador não apenas estivesse em conformidade a este respeito (por exemplo, emitindo um aviso), mas também falhasse completamente.

O código idealmente não deve ser compilado - mas sim no gcc e no clang (presumo porque provavelmente há muito código por aí que seria quebrado com pouco ganho, apesar desse tipo de falha no sistema estar obsoleto por mais de uma década). O código está malformado e, portanto, não faz sentido raciocinar sobre qual poderia ser o comportamento do código. Mas, considerando este caso específico e a história de ser permitido anteriormente, não acredito que seja um exagero irracional interpretar o código resultante como se fosse implícito const_cast, algo como:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

Com isso, o resto do programa está perfeitamente bem, já que você nunca smais tocará . Ler um constobjeto criado por meio de um não- constponteiro é perfeitamente normal. Escrever um constobjeto criado por meio de tal ponteiro é um comportamento indefinido:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Como não há modificação em nenhum slugar em seu código, o programa funciona em C ++ 03, deve falhar ao compilar em C ++ 11, mas falha mesmo assim - e dado que os compiladores permitem, ainda não há um comportamento indefinido nele † . Com a permissão de que os compiladores ainda estão interpretando [incorretamente] as regras do C ++ 03, não vejo nada que possa levar a um comportamento "imprevisível". No sentanto, escreva e todas as apostas serão canceladas. Em C ++ 03 e C ++ 11.


† Embora, novamente, por definição, código malformado não produza expectativa de comportamento razoável
‡ Exceto que não, veja a resposta de Matt McNabb

Barry
fonte
Acho que aqui "imprevisível" pretende significar que não se pode usar o padrão para prever o que um compilador fará com um código malformado (além de emitir um diagnóstico). Sim, ele poderia tratá-lo como o C ++ 03 diz que deveria ser tratado e (sob o risco da falácia do "Não, True Scotsman") o bom senso nos permite prever com alguma confiança que esta é a única coisa que um compilador-redator sensato sempre escolherá se o código será compilado. Então, novamente, ele poderia tratá-lo como significando a reversão da string literal antes de convertê-lo em não-const. O C ++ padrão não se importa.
Steve Jessop
2
@SteveJessop Não acredito nessa interpretação. Este não é um comportamento indefinido nem da categoria de código malformado que o padrão rotula como nenhum diagnóstico necessário. É uma violação do sistema de tipo simples que deve ser muito previsível (compila e faz coisas normais em C ++ 03, falha ao compilar em C ++ 11). Você realmente não pode usar bugs do compilador (ou licenças artísticas) para sugerir que o código é imprevisível - caso contrário, todo o código seria tautologicamente imprevisível.
Barry
Não estou falando sobre bugs do compilador, estou falando sobre se o padrão define ou não o comportamento (se houver) do código. Suspeito que o professor esteja fazendo o mesmo, e "imprevisível" é apenas uma forma desajeitada de dizer que o padrão atual não define o comportamento. De qualquer forma, isso me parece mais provável do que o fato de o professor acreditar erroneamente que se trata de um programa bem elaborado com comportamento indefinido.
Steve Jessop
1
Não, não tem. O padrão não define o comportamento de programas malformados.
Steve Jessop
1
@supercat: é um ponto justo, mas não acredito que seja o motivo principal. Acho que o principal motivo pelo qual o padrão não especifica o comportamento de programas malformados é para que os compiladores possam oferecer suporte a extensões da linguagem adicionando sintaxe que não é bem formada (como o Objective C faz). Permitir a implementação para fazer uma limpeza total após uma falha na compilação é apenas um bônus :-)
Steve Jessop
20

Outras respostas cobriram que este programa está mal formado em C ++ 11 devido à atribuição de um const chararray a a char *.

No entanto, o programa também estava malformado antes do C ++ 11.

As operator<<sobrecargas estão em alta <ostream>. O requisito para iostreamincluir ostreamfoi adicionado no C ++ 11.

Historicamente, a maioria das implementações iostreamincluía ostreammesmo assim, talvez para facilitar a implementação ou talvez para fornecer um melhor QoI.

Mas seria conformar-se por iostreamdefinir apenas a ostreamclasse sem definir as operator<<sobrecargas.

MILÍMETROS
fonte
13

A única coisa um pouco errada que vejo com este programa é que você não deve atribuir um literal de string a um charponteiro mutável , embora isso seja geralmente aceito como uma extensão do compilador.

Caso contrário, este programa parece bem definido para mim:

  • As regras que ditam como as matrizes de caracteres se tornam ponteiros de caracteres quando passadas como parâmetros (como com cout << s2) são bem definidas.
  • A matriz é terminada em nulo, o que é uma condição para operator<<com a char*(ou a const char*).
  • #include <iostream>inclui <ostream>, que por sua vez define operator<<(ostream&, const char*), então tudo parece estar no lugar.
zneak
fonte
12

Você não pode prever o comportamento do compilador, pelas razões mencionadas acima. ( Deve falhar ao compilar, mas pode não.)

Se a compilação for bem-sucedida, o comportamento está bem definido. Você certamente pode prever o comportamento do programa.

Se não conseguir compilar, não há programa. Em uma linguagem compilada, o programa é o executável, não o código-fonte. Se você não tem um executável, não tem um programa e não pode falar sobre o comportamento de algo que não existe.

Então, eu diria que a declaração do seu professor está errada. Você não pode prever o comportamento do compilador quando confrontado com esse código, mas isso é diferente do comportamento do programa . Então, se ele vai pegar lêndeas, é melhor ter certeza de que está certo. Ou, é claro, você pode tê-lo citado incorretamente e o erro está em sua tradução do que ele disse.

Graham
fonte
10

Como outros notaram, o código é ilegítimo em C ++ 11, embora fosse válido em versões anteriores. Consequentemente, um compilador para C ++ 11 é necessário para emitir pelo menos um diagnóstico, mas o comportamento do compilador ou do restante do sistema de construção não é especificado além disso. Nada no padrão proibiria um compilador de sair abruptamente em resposta a um erro, deixando um arquivo objeto parcialmente escrito que um vinculador poderia pensar que era válido, resultando em um executável corrompido.

Embora um bom compilador deva sempre garantir, antes de sair, que qualquer arquivo-objeto que se espera que tenha produzido seja válido, não existente ou reconhecível como inválido, tais questões estão fora da jurisdição do Padrão. Embora tenha havido historicamente (e ainda possa haver) algumas plataformas em que uma compilação com falha pode resultar em arquivos executáveis ​​de aparência legítima que travam de forma arbitrária quando carregados (e tive que trabalhar com sistemas onde erros de link costumavam ter esse comportamento) , Eu não diria que as consequências dos erros de sintaxe são geralmente imprevisíveis. Em um bom sistema, uma tentativa de construção geralmente produzirá um executável com o melhor esforço do compilador na geração de código ou não produzirá nenhum executável. Alguns sistemas deixarão para trás o antigo executável após uma construção com falha,

Minha preferência pessoal seria que os sistemas baseados em disco renomeassem o arquivo de saída, para permitir as raras ocasiões em que esse executável seria útil, evitando a confusão que pode resultar de acreditar erroneamente que está executando um novo código, e para a programação incorporada sistemas para permitir que um programador especifique para cada projeto um programa que deve ser carregado se um executável válido não estiver disponível com o nome normal [de preferência algo que indique com segurança a falta de um programa utilizável]. Um conjunto de ferramentas de sistemas embarcados geralmente não teria como saber o que tal programa deveria fazer, mas em muitos casos alguém escrevendo código "real" para um sistema terá acesso a algum código de teste de hardware que poderia ser facilmente adaptado ao objetivo. Não sei se vi o comportamento de renomeação, no entanto,

supergato
fonte