Joel Spolsky caracterizou C ++ como "corda suficiente para se enforcar" . Na verdade, ele estava resumindo "Effective C ++" de Scott Meyers:
É um livro que diz basicamente que C ++ é corda suficiente para se enforcar, e depois alguns quilômetros extras de corda e, em seguida, algumas pílulas suicidas disfarçadas de M & Ms ...
Eu não tenho uma cópia do livro, mas há indicações de que grande parte do livro está relacionada a armadilhas do gerenciamento de memória que parecem renderizadas em c # porque o tempo de execução gerencia esses problemas para você.
Aqui estão as minhas perguntas:
- O C # evita armadilhas que são evitadas no C ++ apenas por uma programação cuidadosa? Em caso afirmativo, em que grau e como são evitados?
- Existem novas armadilhas diferentes em C # que um novo programador de C # deve estar ciente? Se sim, por que eles não poderiam ser evitados pelo design do C #?
c#
programming-languages
c++
alx9r
fonte
fonte
Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.
. Eu acredito que isso se qualifica como uma pergunta ... #Respostas:
A diferença fundamental entre C ++ e C # decorre de um comportamento indefinido .
Não tem nada a ver com o gerenciamento manual de memória. Nos dois casos, esse é um problema resolvido.
C / C ++:
No C ++, quando você comete um erro, o resultado é indefinido.
Ou, se você tentar fazer certos tipos de suposições sobre o sistema (por exemplo, excesso de número inteiro assinado), é provável que seu programa seja indefinido.
Talvez leia esta série de três partes sobre comportamento indefinido.
Isto é o que faz C ++ tão rápido - o compilador não precisa se preocupar com o que acontece quando as coisas dão errado, para que ele possa evitar a verificação de correção.
C #, Java, etc.
No C #, você tem a garantia de que muitos erros surgirão na sua cara como exceções, e você tem muito mais garantia sobre o sistema subjacente.
Essa é uma barreira fundamental para tornar o C # tão rápido quanto o C ++, mas também é uma barreira fundamental para tornar o C ++ seguro e facilita o trabalho de depuração e depuração.
Tudo o resto é apenas molho.
fonte
A maioria faz, outras não. E, claro, faz alguns novos.
Comportamento indefinido - A maior armadilha do C ++ é que há muita linguagem indefinida. O compilador pode literalmente explodir o universo quando você faz essas coisas, e tudo ficará bem. Naturalmente, isso é incomum, mas é bastante comum que seu programa funcione bem em uma máquina e, por uma boa razão, não funcione em outra. Ou pior, a ação sutilmente diferente. O C # possui alguns casos de comportamento indefinido em sua especificação, mas são raros e em áreas do idioma que raramente são percorridas. O C ++ tem a possibilidade de executar um comportamento indefinido toda vez que você faz uma declaração.
Vazamentos de memória - Isso é menos preocupante para o C ++ moderno, mas para iniciantes e durante cerca de metade de sua vida útil, o C ++ tornou super fácil o vazamento de memória. O C ++ eficaz veio ao redor da evolução das práticas para eliminar essa preocupação. Dito isto, o C # ainda pode vazar memória. O caso mais comum que as pessoas encontram é a captura de eventos. Se você tiver um objeto e colocar um de seus métodos como manipulador de um evento, o proprietário desse evento precisará ser submetido à GC para que o objeto morra. A maioria dos iniciantes não percebe que o manipulador de eventos conta como referência. Também há problemas em não descartar recursos descartáveis que podem vazar memória, mas esses não são tão comuns quanto os indicadores no C ++ pré-eficaz.
Compilação - C ++ possui um modelo de compilação retardado. Isso leva a uma série de truques para jogar bem com ele e manter os tempos de compilação baixos.
Strings - O C ++ moderno torna isso um pouco melhor, mas
char*
é responsável por ~ 95% de todas as violações de segurança antes do ano 2000. Para programadores experientes, eles se concentrarãostd::string
, mas ainda há algo a ser evitado e um problema em bibliotecas antigas / piores . E isso está rezando para que você não precise de suporte unicode.E realmente, essa é a ponta do iceberg. A questão principal é que o C ++ é uma linguagem muito ruim para iniciantes. É bastante inconsistente, e muitas das velhas armadilhas realmente, muito ruins, foram tratadas com a mudança dos idiomas. O problema é que os iniciantes precisam aprender os idiomas com algo como C ++ eficaz. O C # elimina muitos desses problemas e torna o resto menos uma preocupação até que você avance no caminho do aprendizado.
Mencionei o evento "vazamento de memória". Este não é um problema de linguagem, mas o programador espera algo que a linguagem não possa fazer.
Outra é que o finalizador de um objeto C # não é tecnicamente garantido para ser executado pelo tempo de execução. Isso geralmente não importa, mas faz com que algumas coisas sejam projetadas de maneira diferente do que você poderia esperar.
Outra armadilha que eu já vi programadores é a semântica de captura de funções anônimas. Quando você captura uma variável, você captura a variável . Exemplo:
Não faz o que ingenuamente é pensado. Isso imprime
10
10 vezes.Tenho certeza de que há muitos outros que estou esquecendo, mas a questão principal é que eles são menos difundidos.
fonte
char*
. Sem mencionar que você ainda pode vazar memória em C # muito bem.enable_if
Na minha opinião, os perigos do C ++ são um pouco exagerados.
O perigo essencial é este: enquanto o C # permite executar operações de ponteiro "inseguras" usando a
unsafe
palavra - chave, o C ++ (sendo principalmente um superconjunto de C) permite usar ponteiros sempre que lhe apetecer. Além dos perigos usuais inerentes ao uso de ponteiros (que são iguais a C), como vazamentos de memória, estouros de buffer, ponteiros pendentes, etc., o C ++ apresenta novas maneiras de você estragar seriamente as coisas.Essa "corda extra", por assim dizer, sobre a qual Joel Spolsky estava falando , basicamente se resume a uma coisa: escrever aulas que gerenciam internamente sua própria memória, também conhecida como " Regra dos 3 " (que agora pode ser chamada de Regra de 4 ou regra de 5 em C ++ 11). Isso significa que, se você quiser escrever uma classe que gerencia internamente suas próprias alocações de memória, é necessário saber o que está fazendo ou o programa provavelmente falhará. É necessário criar cuidadosamente um construtor, construtor de cópias, destruidor e operador de atribuição, que é surpreendentemente fácil de errar, geralmente resultando em falhas bizarras no tempo de execução.
CONTUDO , na programação C ++ diária real, é muito raro escrever uma classe que gerencia sua própria memória; portanto, é enganoso dizer que os programadores de C ++ sempre precisam ser "cuidadosos" para evitar essas armadilhas. Normalmente, você estará fazendo algo mais como:
Essa classe é bem parecida com o que você faria em Java ou C # - não requer gerenciamento explícito de memória (porque a classe da biblioteca
std::string
cuida de tudo isso automaticamente), e nenhum material da "Regra de 3" é necessário desde o padrão O construtor de cópias e o operador de atribuição estão bem.É somente quando você tenta fazer algo como:
Nesse caso, pode ser complicado para os novatos obterem o construtor de atribuição, destruidor e cópia correto. Mas, na maioria dos casos, não há razão para fazer isso. O C ++ facilita muito evitar o gerenciamento manual de memória em 99% do tempo, usando classes de biblioteca como
std::string
estd::vector
.Outro problema relacionado é o gerenciamento manual de memória de uma maneira que não leva em consideração a possibilidade de uma exceção ser lançada. Gostar:
Se
some_function_which_may_throw()
realmente não lançar uma exceção, você é deixado com um vazamento de memória porque a memória alocada paras
nunca mais ser recuperado. Mas, novamente, na prática, isso não é mais um problema pela mesma razão que a "Regra de 3" não é mais um problema. É muito raro (e geralmente desnecessário) gerenciar realmente sua própria memória com ponteiros brutos. Para evitar o problema acima, tudo o que você precisa fazer é usar umstd::string
oustd::vector
, e o destruidor será automaticamente chamado durante o desenrolamento da pilha após a exceção ser lançada.Portanto, um tema geral aqui é que muitos recursos do C ++ que eram não herdados do C, como inicialização / destruição automática, construtores de cópias e exceções, forçam o programador a ter um cuidado extra ao executar o gerenciamento manual de memória no C ++. Porém, novamente, isso é apenas um problema se você planeja gerenciar manualmente a memória, o que quase nunca é mais necessário quando você possui contêineres padrão e indicadores inteligentes.
Portanto, na minha opinião, enquanto o C ++ oferece muita corda extra, quase nunca é necessário usá-lo para se enforcar, e as armadilhas sobre as quais Joel estava falando são trivialmente fáceis de evitar no C ++ moderno.
fonte
Does C# avoid pitfalls that are avoided in C++ only by careful programming?
. A resposta é "não realmente, porque é tão fácil de evitar as armadilhas Joel estava falando em C ++ moderno"Eu realmente não concordo. Talvez menos armadilhas que o C ++, como existia em 1985.
Na verdade não. Regras como a regra de três perderam significado enorme em C ++ 11 graças a
unique_ptr
eshared_ptr
sendo padronizado. Usar as classes Standard de maneira vagamente sensível não é "codificação cuidadosa", é "codificação básica". Além disso, a proporção da população de C ++ que ainda é suficientemente estúpida, desinformada ou ambas para fazer coisas como gerenciamento manual de memória é muito menor do que antes. A realidade é que os palestrantes que desejam demonstrar regras como essa precisam passar semanas tentando encontrar exemplos onde ainda se aplicam, porque as classes Standard cobrem praticamente todos os casos de uso imagináveis. Muitas técnicas eficazes de C ++ foram da mesma maneira - o caminho do dodo. Muitos dos outros não são realmente tão específicos de C ++. Deixe-me ver. Ignorando o primeiro item, os próximos dez são:final
eoverride
ajudaram a mudar esse jogo em particular para melhor. Faça seu destruidoroverride
e você garante um bom erro de compilador se herdar de alguém que não o destruiuvirtual
. Faça sua aulafinal
e nenhum scrub ruim possa aparecer e herdar dela acidentalmente sem um destruidor virtual.Obviamente, eu não vou passar por todos os itens eficazes do C ++, mas a maioria deles está simplesmente aplicando conceitos básicos ao C ++. Você encontraria o mesmo conselho em qualquer linguagem de operador sobrecarregável orientada a objeto e com valor de tipo. Os destruidores virtuais são o único que é uma armadilha de C ++ e ainda é válida - embora, sem dúvida, com o
final
classe do C ++ 11, não seja tão válido quanto era. Lembre-se de que o C ++ efetivo foi escrito quando a idéia de aplicar o OOP e os recursos específicos do C ++ ainda era muito nova. Esses itens dificilmente são sobre as armadilhas do C ++ e mais sobre como lidar com a alteração do C e como usar o OOP corretamente.Edit: As armadilhas do C ++ não incluem coisas como as armadilhas do
malloc
. Quero dizer, por exemplo, todas as armadilhas que você pode encontrar no código C, você também pode encontrar no código C # inseguro, o que não é particularmente relevante e, em segundo lugar, apenas porque o Padrão o define para interoperação não significa que o uso seja considerado C ++ código. O Padrão também definegoto
, mas se você escrever uma pilha gigante de bagunça de espaguete usando-o, considero que o seu problema, não o idioma. Há uma grande diferença entre "codificação cuidadosa" e "seguindo expressões básicas da linguagem".using
é uma merda. Realmente faz. E não tenho ideia de por que algo melhor não foi feito. Além disso,Base[] = Derived[]
e praticamente todo uso de Object, que existe porque os designers originais não perceberam o enorme sucesso que os modelos tiveram em C ++, e decidiram que "vamos deixar tudo herdar de tudo e perder toda a segurança de tipos" era a escolha mais inteligente . Eu também acredito que você pode encontrar algumas surpresas desagradáveis em coisas como condições de corrida com os delegados e outras coisas divertidas. Depois, há outras coisas gerais, como os genéricos são horríveis em comparação com os modelos, a colocação realmente desnecessária de tudo em umclass
e outras coisas.fonte
malloc
não significa que você deve fazê-lo, mais do que apenas porque você pode se prostituirgoto
como uma cadela significa que é uma corda com a qual você pode se enforcar.unsafe
em C #, o que também é ruim. Eu também poderia listar todas as armadilhas da codificação de C # como C, se você quiser.C # tem as vantagens de:
char
,string
, etc é definido pela implementação. O cisma entre a abordagem do Windows para Unicode (wchar_t
para UTF-16,char
para "páginas de código" obsoletas) e a abordagem * nix (UTF-8) causa grandes dificuldades no código de plataforma cruzada. C #, OTOH, garante que astring
é UTF-16.Sim:
IDisposable
Há um livro chamado Effective C #, que é semelhante em estrutura ao Effective C ++ .
fonte
Não, C # (e Java) são menos seguros que C ++
C ++ é verificável localmente . Posso inspecionar uma única classe em C ++ e determinar que a classe não vaza memória ou outros recursos, supondo que todas as classes referenciadas estejam corretas. Em Java ou C #, é necessário verificar todas as classes referenciadas para determinar se ela requer finalização de algum tipo.
C ++:
C #:
C ++:
C #:
fonte
auto_ptr
(ou alguns de seus parentes). Essa é a corda proverbial.auto_ptr
é tão simples quanto saber usarIEnumerable
ou saber usar interfaces, ou não usar ponto flutuante para moeda ou algo assim. É uma aplicação básica do DRY. Ninguém que sabe o básico de como programar cometeria esse erro. Ao contráriousing
. O problemausing
é que você precisa saber para todas as classes se é ou não descartável (e espero que nunca mude) e se não é descartável, você bane automaticamente todas as classes derivadas que possam ter que ser descartáveis.Dispose
método, deve implementarIDisposable
(da maneira 'adequada'). Se sua classe faz isso (que é o equivalente à implementação de RAII para sua classe em C ++) e você usausing
(que é como os ponteiros inteligentes em C ++), tudo funciona perfeitamente. O finalizador é destinado principalmente a evitar acidentes -Dispose
é responsável pela correção, e se você não o estiver usando, bem, a culpa é sua, não dos C #.Sim 100% sim, pois acho impossível liberar memória e usá-la em C # (assumindo que seja gerenciado e você não entra no modo inseguro).
Mas se você sabe programar em C ++ que um número inacreditável de pessoas não sabe. Você está bem. Como as aulas de Charles Salvia realmente não gerenciam suas memórias, pois tudo é tratado em aulas de STL preexistentes. Eu raramente uso ponteiros. Na verdade, eu fui para projetos sem usar um único ponteiro. (O C ++ 11 facilita isso).
Quanto a erros de digitação, erros tolos e etc (ex:
if (i=0)
bc, a tecla ficou presa quando você pressionou == muito rapidamente) o compilador reclama, o que é bom, pois melhora a qualidade do código. Outro exemplo é o esquecimentobreak
nas instruções switch e não permitir que você declare variáveis estáticas em uma função (que eu não gosto às vezes, mas é uma boa ideia).fonte
=
/==
problema ainda pior, usando==
a igualdade de referência e introdução.equals
para a igualdade de valor. O programador ruim agora precisa acompanhar se uma variável é 'double' ou 'Double' e não se esqueça de chamar a variante correta.struct
você pode fazer o==
que funciona incrivelmente bem, já que na maioria das vezes alguém só teria strings, ints e floats (ou seja, apenas membros struct). No meu próprio código, nunca recebo esse problema, exceto quando quero comparar matrizes. Eu não acho que eu jamais comparar tipos de lista ou não struct (string, int, float, DateTime, KeyValuePair e muitos outros)==
a igualdade de valor e a igualdadeis
de referência.