No C ++, existem alguns recursos que podem ser considerados potencialmente perigosos - com erros de cálculo no design ou codificação imprecisa, eles podem facilmente levar a erros. O artigo fornece uma seleção desses recursos, fornece dicas sobre como reduzir seu impacto negativo.
Sumário
Praemonitus, praemunitus.
Prevenido significa armado. (lat.)
1. Introdução
No C ++, existem muitos recursos que podem ser considerados potencialmente perigosos - ao calcular mal no design ou na codificação imprecisa, eles podem facilmente levar a erros. Alguns deles podem ser atribuídos à infância difícil, outros ao padrão C ++ 98 desatualizado, mas outros já estão associados aos recursos do C ++ moderno. Considere os principais e tente dar conselhos sobre como reduzir seu impacto negativo.
1. Tipos
1.1 Instruções condicionais e operadores
A necessidade de compatibilidade com C leva ao fato de que, na declaração if(...)
e similares, você pode substituir qualquer expressão numérica ou ponteiro, e não apenas expressões como bool
. O problema é agravado pela conversão implícita de bool
para int
em expressões aritméticas e a prioridade de alguns operadores. Isso leva, por exemplo, aos seguintes erros:
if(a=b)
quando corretamente if(a==b)
,
if(a<x<b)
, quando corretamente if(a<x && x<b)
,
if(a&x==0)
, quando corretamente if((a&x)==0)
,
if(Foo)
quando corretamente if(Foo())
,
if(arr)
quando corretamente if(arr[0])
,
if(strcmp(s,r))
quando correto if(strcmp(s,r)==0)
.
Alguns desses erros causam um aviso do compilador, mas não um erro. Às vezes, os analisadores de código também podem ajudar. Em C #, esses erros são quase impossíveis, a if(...)
e similares exigem um tipo bool
, você não pode misturar tipos bool
e numéricos em expressões aritméticas.
Como lutar:
- Programa sem avisos. Infelizmente, isso nem sempre ajuda; alguns dos erros descritos acima não dão avisos.
- Use analisadores de código estático.
- Técnica de recepção antiquada: ao comparar com uma constante, coloque-a à esquerda, por exemplo,
if(MAX_PATH==x)
. Parece um condomínio bonito (e até tem seu próprio nome - "notação Yoda") e ajuda em um pequeno número de casos considerados. - Use o qualificador
const
mais amplamente possível. Mais uma vez, nem sempre ajuda. - Acostume-se a escrever as expressões lógicas corretas:
if(x!=0)
vez de if(x)
. (Embora você possa cair na armadilha das prioridades do operador aqui, veja o terceiro exemplo.) - Seja extremamente atencioso.
1.2 Conversões implícitas
C ++ refere-se a linguagens fortemente tipadas, mas as conversões implícitas de tipos são amplamente usadas para tornar o código mais curto. Essas conversões implícitas podem, em alguns casos, levar a erros.
As conversões implícitas mais irritantes são conversões de um tipo numérico ou ponteiro para bool
e de bool
para int
. São essas transformações (necessárias para compatibilidade com C) que causam os problemas descritos na seção 1.1. Conversões implícitas que potencialmente causam uma perda na precisão dos dados numéricos (restringindo conversões), por exemplo, de double
para int
também nem sempre são adequadas. Em muitos casos, o compilador gera um aviso (especialmente quando pode haver uma perda de precisão dos dados numéricos), mas um aviso não é um erro. Em C #, conversões entre tipos numéricos e bool
proibidas (mesmo explícitas), e as conversões que potencialmente causam perda de precisão nos dados numéricos são quase sempre um erro.
O programador pode adicionar outras conversões implícitas: (1) definir um construtor com um parâmetro sem a palavra-chave explicit
; (2) a definição de um operador de conversão de tipo. Essas transformações quebram lacunas de segurança adicionais baseadas em fortes princípios de digitação.
Em C #, o número de conversões implícitas internas é muito menor; as conversões implícitas personalizadas devem ser declaradas usando a palavra-chave implicit
.
Como lutar:
- Programa sem avisos.
- Tenha muito cuidado com os desenhos descritos acima, não os use sem extrema necessidade.
2. Resolução de nomes
2.1 Ocultando variáveis em escopos aninhados
No C ++, a regra a seguir se aplica. Vamos
De acordo com as regras do C ++, a variável
declarada em
oculta a variável
declarada em
A primeira declaração x
não precisa estar em um bloco: ela pode ser membro de uma classe ou variável global, apenas precisa estar visível no bloco
Imagine agora a situação em que você precisa refatorar o código a seguir
Por engano, são feitas alterações:
E agora o código “algo está sendo feito com
de
” fará algo com
de
! É claro que tudo não funciona como antes, e encontrar o que geralmente é muito difícil. Não é em vão que em C # é proibido ocultar variáveis locais (embora os membros da classe possam). Observe que o mecanismo de ocultar variáveis de uma forma ou de outra é usado em quase todas as linguagens de programação.
Como lutar:
- Declarar variáveis no escopo mais estreito possível.
- Não escreva blocos longos e profundamente aninhados.
- Use convenções de codificação para distinguir visualmente identificadores de escopo diferente.
- Seja extremamente atencioso.
2.2 Sobrecarga de função
A sobrecarga de funções é um recurso integrante de muitas linguagens de programação e o C ++ não é uma exceção. Mas essa oportunidade deve ser usada com cuidado, caso contrário, você poderá ter problemas. Em alguns casos, por exemplo, quando o construtor está sobrecarregado, o programador não tem escolha, mas em outros casos, a recusa em sobrecarregar pode ser justificada. Considere os problemas que surgem ao usar funções sobrecarregadas.
Se você tentar considerar todas as opções possíveis que possam surgir ao resolver uma sobrecarga, as regras para resolver uma sobrecarga serão muito complicadas e, portanto, difíceis de prever. A complexidade adicionada é introduzida pelas funções do modelo e pela sobrecarga de operadores internos. O C ++ 11 adicionou problemas com links rvalue e listas de inicialização.
Os problemas podem ser criados pelo algoritmo de pesquisa para candidatos resolverem sobrecargas em áreas de visibilidade aninhadas. Se o compilador encontrou algum candidato no escopo atual, outras pesquisas serão encerradas. Se os candidatos encontrados não forem adequados, conflitantes, excluídos ou inacessíveis, um erro será gerado, mas nenhuma pesquisa adicional será tentada. E somente se não houver candidatos no escopo atual, a pesquisa passará para o próximo escopo mais amplo. O mecanismo de ocultação de nomes funciona, que é quase o mesmo que discutido na seção 2.1, consulte [Dewhurst].
As funções de sobrecarga podem reduzir a legibilidade do código, o que significa provocar erros.
Usar funções com parâmetros padrão parece usar funções sobrecarregadas, embora, é claro, haja menos problemas em potencial. Mas o problema com baixa legibilidade e possíveis erros permanece.
Com extrema cautela, sobrecarga e parâmetros padrão para funções virtuais devem ser usados, consulte a seção 5.2.
O C # também suporta sobrecarga de função, mas as regras para resolver sobrecargas são um pouco diferentes.
Como lutar:
- Não abuse da sobrecarga de funções, nem projete funções com parâmetros padrão.
- Se as funções estiverem sobrecarregadas, use assinaturas que não estão em dúvida ao resolver sobrecargas.
- Não declare funções com o mesmo nome no escopo aninhado.
- Não esqueça que o mecanismo de funções remotas (
=delete
) que apareceu no C ++ 11 pode ser usado para banir certas opções de sobrecarga.
3. Construtores, destruidores, inicialização, exclusão
3.1 Funções de membro de classe geradas pelo compilador
Se o programador não definiu as funções de membro da classe da lista a seguir - construtor padrão, construtor de cópias, operador de atribuição de cópias, destruidor -, o compilador pode fazer isso por ele. O C ++ 11 adicionou um construtor de movimentação e um operador de atribuição de movimentação a esta lista. Essas funções de membro são chamadas de funções de membro especiais. Eles são gerados apenas se forem usados e condições adicionais específicas para cada função são atendidas. Chamamos a atenção para o fato de que esse uso pode ser bastante oculto (por exemplo, ao implementar a herança). Se a função necessária não puder ser gerada, um erro será gerado. (Com exceção das operações de realocação, elas são substituídas por operações de cópia.) As funções de membro geradas pelo compilador são públicas e incorporáveis. Detalhes sobre funções-membro especiais podem ser encontrados em [Meyers2].
Em alguns casos, essa ajuda do compilador pode ser um "serviço de suporte". A ausência de funções-membro especiais personalizadas pode levar à criação de um tipo trivial, e isso, por sua vez, causa o problema de variáveis não inicializadas, consulte a seção 3.2. As funções-membro geradas são públicas e isso nem sempre é consistente com o design das classes. Nas classes base, o construtor deve ser protegido; às vezes, para um controle mais preciso do ciclo de vida do objeto, é necessário um destruidor protegido. Se uma classe tiver um descritor de recursos brutos como membro e possuir esse recurso, o programador precisará implementar um construtor de cópias, um operador de atribuição de cópias e um destruidor. A chamada "regra das Três Grandes" é bem conhecida, que afirma que se um programador define pelo menos uma das três operações - construtor de cópias, operador de atribuição de cópias ou destruidor - ele deve definir as três operações. O construtor de movimentação e o operador de atribuição de movimentação gerados pelo compilador também estão longe de ser sempre o que você precisa. O destruidor gerado pelo compilador, em alguns casos, leva a problemas muito sutis, cujo resultado pode ser um vazamento de recurso, consulte a seção 3.7.
O programador pode proibir a geração de funções-membro especiais; em C ++ 11, é necessário usar a construção "=delete"
ao declarar; em C ++ 98, declarar a função de membro correspondente privada e não definir.
Se o programador estiver confortável com as funções de membro geradas pelo compilador, no C ++ 11 ele poderá indicar isso explicitamente, e não apenas descartar a declaração. Para fazer isso, você deve usar a construção "=default"
ao declarar, enquanto o código é melhor lido e recursos adicionais aparecem relacionados ao gerenciamento do nível de acesso.
Em C #, o compilador pode gerar um construtor padrão, geralmente isso não causa problemas.
Como lutar:
- Controlar o compilador gerando funções membro especiais. Se necessário, implemente-os ou proíba.
3.2 Variáveis não inicializadas
Construtores e destruidores podem ser chamados de elementos-chave do modelo de objeto C ++. Ao criar um objeto, o construtor deve ser chamado e, ao excluir, o destruidor é chamado. Mas os problemas de compatibilidade com C forçaram algumas exceções, e essa exceção é chamada de tipos triviais. Eles são introduzidos para simular tipos sichny e ciclo de vida syshny de variáveis, sem a chamada obrigatória do construtor e destruidor. O código C, se compilado e executado em C ++, deve funcionar como em C. Os tipos triviais incluem tipos numéricos, ponteiros, enumerações, bem como classes, estruturas, uniões e matrizes que consistem em tipos triviais. Classes e estruturas devem satisfazer algumas condições adicionais: a ausência de um construtor personalizado, destruidor, cópia, funções virtuais. Para uma classe trivial, o compilador pode gerar um construtor padrão e um destruidor. O construtor padrão zera o objeto, o destruidor não faz nada. Mas esse construtor será gerado e usado apenas se for explicitamente chamado quando a variável for inicializada. Uma variável de um tipo trivial não será inicializada se você não usar alguma variante de inicialização explícita. A sintaxe de inicialização depende do tipo e contexto da declaração da variável. Variáveis estáticas e locais são inicializadas quando declaradas. Para uma classe, as classes base imediatas e os membros não estáticos da classe são inicializados na lista de inicialização do construtor. (O C ++ 11 permite que você inicialize membros de classe não estáticos ao declarar, consulte posteriormente.) Para objetos dinâmicos, a expressão new T()
cria um objeto inicializado pelo construtor padrão, mas o new T
para tipos triviais cria um objeto não inicializado. Ao criar uma matriz dinâmica de um tipo trivial, o new T[N]
, seus elementos sempre serão não inicializados. Se uma instância do std::vector<T>
for criada ou estendida e os parâmetros não forem fornecidos para a inicialização explícita dos elementos, eles garantem a chamada do construtor padrão. O C ++ 11 apresenta uma nova sintaxe de inicialização - usando chaves. Um par de colchetes vazio significa inicialização usando o construtor padrão. Essa inicialização é possível em qualquer lugar em que a inicialização tradicional é usada, além de ter sido possível inicializar membros não estáticos da classe ao declarar, o que substitui a inicialização na lista de inicialização do construtor.
Uma variável não inicializada é estruturada da seguinte maneira: se for definida no escopo do namespace
(globalmente), terá todos os bits zero, se for local ou dinamicamente criada, receberá um conjunto aleatório de bits. É claro que o uso de tal variável pode levar a um comportamento imprevisível do programa.
É verdade que o progresso não pára, os compiladores modernos, em alguns casos, detectam variáveis não inicializadas e geram um erro. Analisadores de código não inicializados detectam ainda melhor.
A biblioteca padrão do C ++ 11 possui modelos chamados de propriedades de tipo (arquivo de cabeçalho <type_traits>
). Um deles permite que você determine se o tipo é trivial. A expressão std::is_trivial<>::value
é true
se T
tipo trivial e false
caso contrário.
Estruturas sisílicas também são chamadas de Dados antigos simples (POD). Podemos assumir que POD e o "tipo trivial" são termos quase equivalentes.
Em C #, variáveis não inicializadas causam um erro, que é controlado pelo compilador. Os campos de objetos de um tipo de referência são inicializados por padrão se a inicialização explícita não for executada. Os campos de objetos de um tipo significativo são inicializados todos por padrão ou todos devem ser inicializados explicitamente.
Como lutar:
- Tenha o hábito de inicializar explicitamente uma variável. Uma variável não inicializada deve "cortar os olhos".
- Declarar variáveis no escopo mais estreito possível.
- Use analisadores de código estático.
- Não projete tipos triviais. Para garantir que o tipo não seja trivial, basta definir um construtor personalizado.
3.3 Procedimento de inicialização para classes base e membros não estáticos da classe
Ao implementar o construtor da classe, as classes base imediatas e os membros não estáticos da classe são inicializados. A ordem de inicialização é determinada pelo padrão: primeiro, as classes base na ordem em que são declaradas na lista de classes base, depois membros não estáticos da classe na ordem de declaração. Se necessário, a inicialização explícita de classes base e membros não estáticos usa a lista de inicialização do construtor. Infelizmente, os itens desta lista não precisam estar na ordem em que a inicialização ocorre. Isso deve ser levado em consideração se, durante a inicialização, os itens da lista usarem referências a outros itens da lista. Por erro, o link pode estar em um objeto que ainda não foi inicializado. O C ++ 11 permite que você inicialize membros de classe não estáticos ao declarar (usando chaves). Nesse caso, eles não precisam ser inicializados na lista de inicialização do construtor e o problema é parcialmente removido.
Em C #, um objeto é inicializado da seguinte maneira: primeiro os campos são inicializados, do subobjeto base até a última derivada, depois os construtores são chamados na mesma ordem. O problema descrito não ocorre.
Como lutar:
- Mantenha a lista de inicialização do construtor em ordem de declaração.
- Tente tornar a inicialização das classes base e dos membros independentes.
- Use a inicialização de membros não estáticos ao declarar.
3.4 Procedimento de inicialização para membros de classe estática e variáveis globais
Membros de classe estática, bem como variáveis definidas no namespace
do escopo (globalmente) em diferentes unidades de compilação (arquivos), são inicializados na ordem determinada pela implementação. Isso deve ser levado em consideração se, durante a inicialização, essas variáveis usarem referências entre si. O link pode estar em uma variável não inicializada.
Como lutar:
- Tome medidas especiais para evitar essa situação. Por exemplo, use variáveis estáticas locais (singleton), elas são inicializadas no primeiro uso.
3.5 Exceções em destruidores
O destruidor não deve lançar exceções. Se você violar essa regra, poderá obter um comportamento indefinido, com mais frequência travando.
Como lutar:
- Evite lançar exceções no destruidor.
3.6 Removendo objetos e matrizes dinâmicos
Se um objeto dinâmico de algum tipo T
T* pt = new T();
então ele é excluído com o operador de delete
delete pt;
Se uma matriz dinâmica for criada
T* pt = new T[N];
então ele é excluído com o operador delete[]
delete[] pt;
Se você não seguir esta regra, poderá obter um comportamento indefinido, ou seja, tudo pode acontecer: vazamento de memória, falha etc. Veja [Meyers1] para detalhes.
Como lutar:
- Use o formulário de
delete
correto.
3.7 Excluindo quando a declaração de classe está incompleta
A onívora do operador de delete
pode criar certos problemas; pode ser aplicada a um ponteiro do tipo void*
ou a um ponteiro para uma classe que possui uma declaração incompleta (preemptiva). O operador de delete
aplicado a um ponteiro para uma classe é uma operação de duas fases: primeiro, o destruidor é chamado e a memória é liberada.Se o operador for aplicado delete
a um ponteiro para uma classe com uma declaração incompleta, nenhum erro ocorrerá, o compilador simplesmente ignorará a chamada para o destruidor (embora um aviso seja emitido). Considere um exemplo:
class X;
Esse código compila mesmo se a delete
declaração de classe completa não estiver disponível no dial-peer X
. Visual Studio exibe o seguinte aviso:warning C4150: deletion of pointer to incomplete type 'X'; no destructor called
Se houver uma implementação X
e CreateX()
, o código for compilado, se CreateX()
retornar um ponteiro para um objeto criado pelo operador new
, a chamada será Foo()
executada com êxito, o destruidor não será chamado. É claro que isso pode levar a uma drenagem de recursos, portanto, mais uma vez, sobre a necessidade de ter cuidado com os avisos.
, -. , . , , , , . [Meyers2].
:
4. ,
4.1
++ , . . . , 1.1.
Aqui está um exemplo:
std::out<<c?x:y;
(std::out<<c)?x:y;
std::out<<(c?x:y);
, , .
. <<
?:
std::out
void*
. ++ , . -, , . ?:
. , ( ).
: x&f==0
x&(f==0)
, (x&f)==0
, , , . - , , , , .
. / . / , /, . , x/4+1
x>>2+1
, x>>(2+1)
, (x>>2)+1
, .
C# , C++, , - .
:
4.2.
++ , . . , , . 4.1. — +
+=
. . , : ,
(), &&
, ||
. , (-), (short-circuit evaluation semantics), , . & ( ). & , .. .
, - (-) , . .
- , , . . [Dewhurst].
C# , , , .
:
4.3.
++ , . ( : ,
(), &&
, ||
, ?:
.) , , , . :
int x=0; int y=(++x*2)+(++x*3);
y
.
, . .
class X; class Y; void Foo(std::shared_ptr<X>, std::shared_ptr<Y>);
Foo()
:
Foo(std::shared_ptr<X>(new X()), std::shared_ptr<Y>(new Y()));
: X
, Y
, std::shared_ptr<X>
, std::shared_ptr<Y>
. Y
, X
.
:
auto p1 = std::shared_ptr<X>(new X()); auto p2 = std::shared_ptr<Y>(new Y()); Foo(p1, p2);
std::make_shared<Y>
( , ):
Foo(std::make_shared<X>(), std::make_shared<Y>());
. [Meyers2].
:
5.
5.1
++98 , ( ), , ( , ). virtual
, , . ( ), , , . , , . , ++11 override
, , , . .
:
5.2
. , , . . . [Dewhurst].
:
5.3
, , . , , post_construct pre_destroy. , — . . , : ( ) . (, , .) , ( ), ( ). . [Dewhurst]. , , .
— - .
, C# , , , . C# : , , . , ( , ).
:
5.4
, , delete
. , - .
:
6.
— C/C++, . . . « ».
C# unsafe mode, .
6.1.
/++ , : strcpy()
, strcat()
, sprinf()
, etc. ( std::vector<>
, etc.) , . (, , , . . Checked Iterators MSDN.) , : , , ; , .
C#, unsafe mode, .
:
- , .
- .
- z-terminated ,
_s
(. ).
6.2. Z-terminated
, . , :
strncpy(dst,src,n);
strlen(src)>=n
, dst
(, ). , , . . — . if(*str)
, if(strlen(str)>0)
, . [Spolsky].
C# string
.
:
6.3.
...
. printf
- , C. , , , , . , .
C# printf
, .
:
7.
7.1.
++ , , , . Aqui está um exemplo:
const int N = 4, M = 6; int x,
:
int
;int
;N
int
;N
int
;- ,
char
int
; - ,
char
int
; - ,
char
int
; N
, char
int
;N
int
;M
N
int
;- ,
char
, long
int
.
, . ( .)
*
&
. ( .)
typedef
( using
-). , :
typedef int(*P)(long); PH(char);
, .
C# , .
:
7.2.
.
class X { public: X(int val = 0);
X x(5);
x
X
, 5.
X x();
x
, X
, x
X
, . X
, , :
X x; X x = X(); X x{};
, , , . [Sutter].
, , C++ ( ). . ( C++ .)
, , , , .
C# , , .
:
8.
8.1. inline
ODR
, inline
— . , . inline
(One Defenition Rule, ODR). . , . , ODR. static
: , , . static
inline
. , , ODR, . , . - , -. .
:
- «»
inline
. namespace
. , . - —
namespace
.
8.2.
. . , , , , .
:
- , .
- , : () , -.
using
-: using namespace
, using
-.- .
8.3. switch
— break
case
. ( .) C# .
:
8.4.
++ , — , — . ( class
struct
) , . ( , # Java.) — , .
- , . (
std::string
, std::vector
, etc.), , . - , , .
- , (slicing), , .
, , , . . , , . , . . — ( =delete
), — explicit
.
C# , .
:
8.5. Gerenciamento de recursos
++ . , . - ( ), ++11 , , , .
C++ .
C# , . , . (using-) Basic Dispose.
:
8.6
«» . , , C++ , STL- - .
. . , . . «», . COM- . (, .) , C++ . — . . . , («» ) , . .
# , . — .
:
8.7.
C++ , : , , . ( !) . , . , . , , . (, .)
C ( ), C++ C ( extern "C"
). C/C++ .
-. #pragma
- , , .
, , , .
, , COM. COM-, , ( , ). COM , , .
C# . , — , C#, C# C/C++.
:
8.8.
, . , . C++ . Em vez disso
#define XXL 32
const int XXL=32;
. inline
.
# ( ).
:
9.
- . . . , .
- .
- . ++ — ++11/14/17.
- - , - .
- .
Referências
[Dewhurst]
, . C++. .: . . — .: , 2012.
[Meyers1]
, . C++. 55 .: . . — .: , 2014.
[Meyers2]
, . C++: 42 C++11 C++14.: . . — .: «.. », 2016.
[Sutter]
, . C++.: . . — : «.. », 2015.
[Spolsky]
, . .: . . — .: -, 2008.