En C ++, hay bastantes características que pueden considerarse potencialmente peligrosas: con errores de cálculo en el diseño o codificación inexacta, pueden conducir fácilmente a errores. El artículo proporciona una selección de tales características, da consejos sobre cómo reducir su impacto negativo.
Tabla de contenidos
Praemonitus, praemunitus.
Prevenido significa armado. (lat.)
Introduccion
En C ++, hay bastantes características que pueden considerarse potencialmente peligrosas: con errores de cálculo en el diseño o codificación inexacta, pueden conducir fácilmente a errores. Algunos de ellos pueden atribuirse a la infancia difícil, algunos al estándar C ++ 98 obsoleto, pero otros ya están asociados con las características de C ++ moderno. Considere los principales e intente dar consejos sobre cómo reducir su impacto negativo.
1. Tipos
1.1. Instrucciones condicionales y operadores
La necesidad de compatibilidad con C lleva al hecho de que en la declaración if(...)
y similares, puede sustituir cualquier expresión numérica o puntero, y no solo expresiones como bool
. El problema se agrava por la conversión implícita de bool
a int
en expresiones aritméticas y la prioridad de algunos operadores. Esto lleva, por ejemplo, a los siguientes errores:
if(a=b)
cuando correctamente if(a==b)
,
if(a<x<b)
, cuando correctamente if(a<x && x<b)
,
if(a&x==0)
, cuando correctamente if((a&x)==0)
,
if(Foo)
cuando correctamente if(Foo())
,
if(arr)
cuando correctamente if(arr[0])
,
if(strcmp(s,r))
cuando es correcto if(strcmp(s,r)==0)
.
Algunos de estos errores provocan una advertencia del compilador, pero no un error. Los analizadores de código también pueden ayudar a veces. En C #, tales errores son casi imposibles, la if(...)
y similares requieren un tipo bool
, no puede mezclar tipos bool
y numéricos en expresiones aritméticas.
Cómo pelear
- Programa sin advertencias. Desafortunadamente, esto no siempre ayuda; algunos de los errores descritos anteriormente no dan advertencias.
- Use analizadores de código estático.
- Técnica de recepción anticuada: cuando se compara con una constante,
if(MAX_PATH==x)
a la izquierda, por ejemplo if(MAX_PATH==x)
. Se ve bastante condominio (e incluso tiene su propio nombre - "notación Yoda"), y ayuda en un pequeño número de casos considerados. - Use el calificador
const
más ampliamente posible. De nuevo, no siempre ayuda. - Acostúmbrate a escribir las expresiones lógicas correctas:
if(x!=0)
lugar de if(x)
. (Aunque puede caer en la trampa de las prioridades del operador aquí, vea el tercer ejemplo). - Se extremadamente atento.
1.2. Conversiones implícitas
C ++ se refiere a lenguajes fuertemente tipados, pero las conversiones de tipos implícitas se usan ampliamente para acortar el código. Estas conversiones implícitas pueden en algunos casos conducir a errores.
Las conversiones implícitas más molestas son las conversiones de un tipo numérico o puntero a bool
y de bool
a int
. Son estas transformaciones (necesarias para la compatibilidad con C) las que causan los problemas descritos en la sección 1.1. Las conversiones implícitas que potencialmente causan una pérdida en la precisión de los datos numéricos (reduciendo las conversiones), por ejemplo, de double
a int
tampoco son siempre apropiadas. En muchos casos, el compilador genera una advertencia (especialmente cuando puede haber una pérdida de precisión de los datos numéricos), pero una advertencia no es un error. En C #, las conversiones entre tipos numéricos y bool
prohibidas (incluso explícitas), y las conversiones que potencialmente causan pérdida de precisión en los datos numéricos son casi siempre un error.
El programador puede agregar otras conversiones implícitas: (1) definir un constructor con un parámetro sin la palabra clave explicit
; (2) la definición de un operador de conversión de tipo. Estas transformaciones rompen brechas de seguridad adicionales basadas en sólidos principios de escritura.
En C #, el número de conversiones implícitas incorporadas es mucho menor; las conversiones implícitas personalizadas deben declararse utilizando la palabra clave implicit
.
Cómo pelear
- Programa sin advertencias.
- Tenga mucho cuidado con los diseños descritos anteriormente, no los use sin extrema necesidad.
2. Resolución de nombre
2.1. Ocultar variables en ámbitos anidados
En C ++, se aplica la siguiente regla. Dejar
De acuerdo con las reglas de C ++, la variable
declarada en
oculta la variable
declarada en
La primera declaración x
no tiene que estar en un bloque: puede ser miembro de una clase o una variable global, solo debe ser visible en el bloque
Imagine ahora la situación en la que necesita refactorizar el siguiente código
Por error, se realizan cambios:
¡Y ahora el código "se está haciendo algo con
de
" hará algo con
de
! Está claro que todo no funciona como antes, y encontrar lo que a menudo es muy difícil. No es en vano que en C # esté prohibido ocultar variables locales (aunque los miembros de la clase sí pueden). Tenga en cuenta que el mecanismo de ocultar variables de una forma u otra se usa en casi todos los lenguajes de programación.
Cómo pelear
- Declarar variables en el alcance más estrecho posible.
- No escriba bloques largos y profundamente anidados.
- Use convenciones de codificación para distinguir visualmente identificadores de diferente alcance.
- Se extremadamente atento.
2.2. Sobrecarga de funciones
La sobrecarga de funciones es una característica integral de muchos lenguajes de programación y C ++ no es una excepción. Pero esta oportunidad debe usarse con cuidado, de lo contrario puede tener problemas. En algunos casos, por ejemplo, cuando el constructor está sobrecargado, el programador no tiene otra opción, pero en otros casos, la negativa a sobrecargarse puede justificarse. Considere los problemas que surgen al usar funciones sobrecargadas.
Si intenta considerar todas las opciones posibles que pueden surgir al resolver una sobrecarga, las reglas para resolver una sobrecarga resultan muy complicadas y, por lo tanto, difíciles de predecir. La complejidad añadida se introduce por las funciones de la plantilla y la sobrecarga de los operadores integrados. C ++ 11 agregó problemas con enlaces rvalue y listas de inicialización.
El algoritmo de búsqueda puede crear problemas para que los candidatos resuelvan la sobrecarga en áreas de visibilidad anidadas. Si el compilador encontró algún candidato en el alcance actual, entonces se termina la búsqueda adicional. Si los candidatos encontrados no son adecuados, conflictivos, eliminados o inaccesibles, se genera un error, pero no se intenta buscar más. Y solo si no hay candidatos en el alcance actual, la búsqueda se mueve al siguiente alcance, más amplio. El mecanismo de ocultación de nombres funciona, que es casi el mismo que se discutió en la sección 2.1, ver [Desvanecimiento].
Las funciones de sobrecarga pueden reducir la legibilidad del código, lo que significa provocar errores.
El uso de funciones con parámetros predeterminados se parece al uso de funciones sobrecargadas, aunque, por supuesto, hay menos problemas potenciales. Pero el problema con mala legibilidad y posibles errores persiste.
Con extrema precaución, se deben utilizar los parámetros predeterminados y de sobrecarga para las funciones virtuales, consulte la sección 5.2.
C # también admite la sobrecarga de funciones, pero las reglas para resolver sobrecargas son ligeramente diferentes.
Cómo pelear
- No abuse de la sobrecarga de funciones, así como del diseño de funciones con parámetros predeterminados.
- Si las funciones están sobrecargadas, utilice firmas que no tengan dudas al resolver sobrecargas.
- No declare funciones del mismo nombre en ámbito anidado.
- No olvide que el mecanismo de funciones remotas (
=delete
) que apareció en C ++ 11 puede usarse para prohibir ciertas opciones de sobrecarga.
3. Constructores, destructores, inicialización, eliminación.
3.1. Funciones de miembro de clase generadas por el compilador
Si el programador no ha definido las funciones miembro de la clase de la siguiente lista (el constructor predeterminado, el constructor de copia, el operador de asignación de copia, el destructor), entonces el compilador puede hacer esto por él. C ++ 11 agregó un constructor de movimientos y un operador de asignación de movimientos a esta lista. Estas funciones miembro se denominan funciones miembro especiales. Se generan solo si se usan y se cumplen condiciones adicionales específicas para cada función. Tenga en cuenta que este uso puede resultar bastante oculto (por ejemplo, al implementar la herencia). Si no se puede generar la función requerida, se genera un error. (Con la excepción de las operaciones de reubicación, se reemplazan por operaciones de copia). Las funciones miembro generadas por el compilador son públicas e integrables. Los detalles sobre funciones especiales para miembros se pueden encontrar en [Meyers2].
En algunos casos, dicha ayuda del compilador puede ser un "servicio de soporte". La ausencia de funciones miembro especiales personalizadas puede conducir a la creación de un tipo trivial, y esto, a su vez, causa el problema de las variables no inicializadas, consulte la sección 3.2. Las funciones miembro generadas son públicas, y esto no siempre es coherente con el diseño de las clases. En las clases base, el constructor debe estar protegido; a veces, para un control más fino sobre el ciclo de vida del objeto, se necesita un destructor protegido. Si una clase tiene un descriptor de recursos sin procesar como miembro y posee este recurso, entonces el programador debe implementar un constructor de copia, un operador de asignación de copia y un destructor. La llamada "regla de los Tres Grandes" es bien conocida, y establece que si un programador define al menos una de las tres operaciones: constructor de copia, operador de asignación de copia o destructor, debe definir las tres operaciones. El constructor de movimientos y el operador de asignación de movimientos generado por el compilador también están lejos de ser siempre lo que necesita. El destructor generado por el compilador en algunos casos conduce a problemas muy sutiles, cuyo resultado puede ser una pérdida de recursos, consulte la sección 3.7.
El programador puede prohibir la generación de funciones miembro especiales, en C ++ 11 es necesario usar la construcción "=delete"
al declarar, en C ++ 98 declara la función miembro correspondiente privada y no define.
Si el programador se siente cómodo con las funciones miembro generadas por el compilador, entonces en C ++ 11 puede indicar esto explícitamente, y no simplemente descartar la declaración. Para hacer esto, debe usar la construcción "=default"
al declarar, mientras que el código se lee mejor y aparecen características adicionales relacionadas con la administración del nivel de acceso.
En C #, el compilador puede generar un constructor predeterminado, generalmente esto no causa ningún problema.
Cómo pelear
- Controle el compilador que genera funciones especiales para miembros. Si es necesario, impleméntelos usted mismo o prohíbalos.
3.2. Variables no inicializadas
Los constructores y destructores pueden llamarse elementos clave del modelo de objetos C ++. Al crear un objeto, se debe llamar al constructor, y al eliminar, se llama al destructor. Pero los problemas de compatibilidad con C han forzado algunas excepciones, y esta excepción se llama tipos triviales. Se introducen para simular los tipos sichny y el ciclo de vida syshny de las variables, sin la llamada obligatoria del constructor y destructor. El código C, si se compila y ejecuta en C ++, debería funcionar igual que en C. Los tipos triviales incluyen tipos numéricos, punteros, enumeraciones, así como clases, estructuras, uniones y matrices que consisten en tipos triviales. Las clases y estructuras deben cumplir algunas condiciones adicionales: la ausencia de un constructor personalizado, destructor, copia, funciones virtuales. Para una clase trivial, el compilador puede generar un constructor predeterminado y un destructor. El constructor predeterminado pone a cero el objeto, el destructor no hace nada. Pero este constructor se generará y usará solo si se llama explícitamente cuando se inicializa la variable. Una variable de tipo trivial no se inicializará si no utiliza alguna variante de inicialización explícita. La sintaxis de inicialización depende del tipo y el contexto de la declaración de variable. Las variables estáticas y locales se inicializan cuando se declaran. Para una clase, las clases base inmediatas y los miembros de clase no estáticos se inicializan en la lista de inicialización del constructor. (C ++ 11 le permite inicializar miembros de clase no estáticos al declarar, ver más adelante). Para los objetos dinámicos, la expresión new T()
crea un objeto inicializado por el constructor predeterminado, pero la new T
para tipos triviales crea un objeto no inicializado. Al crear una matriz dinámica de tipo trivial, new T[N]
, sus elementos siempre estarán sin inicializar. Si se crea o extiende una instancia de std::vector<T>
y no se proporcionan parámetros para la inicialización explícita de los elementos, se garantiza que llamarán al constructor predeterminado. C ++ 11 introduce una nueva sintaxis de inicialización, utilizando llaves. Un par de paréntesis vacío significa inicialización utilizando el constructor predeterminado. Dicha inicialización es posible en todas partes donde se usa la inicialización tradicional, además se hizo posible inicializar miembros no estáticos de la clase al declarar, lo que reemplaza la inicialización en la lista de inicialización del constructor.
Una variable no inicializada se estructura de la siguiente manera: si se define en el ámbito del namespace
(globalmente), tendrá todos los bits cero, si es local o se crea dinámicamente, recibirá un conjunto aleatorio de bits. Está claro que el uso de dicha variable puede conducir a un comportamiento impredecible del programa.
Es cierto que el progreso no se detiene, los compiladores modernos, en algunos casos, detectan variables no inicializadas y arrojan un error. Los analizadores de código no inicializados detectan aún mejor.
La biblioteca estándar de C ++ 11 tiene plantillas llamadas propiedades de tipo (archivo de encabezado <type_traits>
). Uno de ellos le permite determinar si el tipo es trivial. La expresión std::is_trivial<>::value
es true
si T
tipo trivial y false
caso contrario.
Las estructuras síslicas a menudo también se denominan datos antiguos simples (POD). Podemos suponer que POD y el "tipo trivial" son términos casi equivalentes.
En C #, las variables no inicializadas causan un error; esto es controlado por el compilador. Los campos de objetos de un tipo de referencia se inicializan de forma predeterminada si no se realiza una inicialización explícita. Los campos de objetos de un tipo significativo se inicializan, ya sea de forma predeterminada, o todos deben inicializarse explícitamente.
Cómo pelear
- Tiene la costumbre de inicializar explícitamente una variable. Una variable no inicializada debería "cortar el ojo".
- Declarar variables en el alcance más estrecho posible.
- Use analizadores de código estático.
- No diseñe tipos triviales. Para garantizar que el tipo no sea trivial, es suficiente definir un constructor personalizado.
3.3. Procedimiento de inicialización para clases base y miembros de clase no estáticos
Al implementar el constructor de clases, se inicializan las clases base inmediatas y los miembros de clase no estáticos. El orden de inicialización está determinado por el estándar: primero, las clases base en el orden en que se declaran en la lista de clases base, luego los miembros no estáticos de la clase en el orden de declaración. Si es necesario, la inicialización explícita de las clases base y los miembros no estáticos usa la lista de inicialización del constructor. Desafortunadamente, no es necesario que los elementos de esta lista estén en el orden en que se produce la inicialización. Esto debe tenerse en cuenta si, durante la inicialización, los elementos de la lista usan referencias a otros elementos de la lista. En caso de error, el enlace puede ser a un objeto que aún no se ha inicializado. C ++ 11 le permite inicializar miembros de clase no estáticos al declarar (usando llaves). En este caso, no necesitan inicializarse en la lista de inicialización del constructor y el problema se elimina parcialmente.
En C #, un objeto se inicializa de la siguiente manera: primero se inicializan los campos, desde el subobjeto base hasta la última derivada, luego los constructores se llaman en el mismo orden. El problema descrito no ocurre.
Cómo pelear
- Mantener la lista de inicialización del constructor en orden de declaración.
- Intente hacer que la inicialización de las clases base y los miembros de la clase sean independientes.
- Utilice la inicialización de miembros no estáticos al declarar.
3.4. Procedimiento de inicialización para miembros de clase estáticos y variables globales
Los miembros de clase estática, así como las variables definidas en el espacio de namespace
ámbito (globalmente) en diferentes unidades de compilación (archivos), se inicializan en el orden determinado por la implementación. Esto debe tenerse en cuenta si durante la inicialización dichas variables usan referencias entre sí. El enlace puede ser a una variable no inicializada.
Cómo pelear
- Tome medidas especiales para prevenir esta situación. Por ejemplo, use variables estáticas locales (singleton), se inicializan en el primer uso.
3.5. Excepciones en destructores
El destructor no debe lanzar excepciones. Si viola esta regla, puede obtener un comportamiento indefinido, a menudo una terminación anormal.
Cómo pelear
- Evite lanzar excepciones en el destructor.
3.6. Eliminar objetos dinámicos y matrices
Si T
un objeto dinámico de algún tipo T
T* pt = new T();
luego se elimina con el operador de delete
delete pt;
Si se crea una matriz dinámica
T* pt = new T[N];
luego se elimina con el operador delete[]
delete[] pt;
Si no sigue esta regla, puede obtener un comportamiento indefinido, es decir, puede suceder cualquier cosa: una pérdida de memoria, un bloqueo, etc. Ver [Meyers1] para más detalles.
Cómo pelear
- Use el formulario de
delete
correcto.
3.7. Eliminación con declaración de clase incompleta
La omnivorosidad del operador de delete
puede crear ciertos problemas; se puede aplicar a un puntero de tipo void*
o a un puntero a una clase que tiene una declaración incompleta (preventiva). El operador de delete
aplicado a un puntero a una clase es una operación de dos fases; primero, se llama al destructor, luego se libera la memoria.Si el operador se aplica delete
a un puntero a una clase con una declaración incompleta, no se produce ningún error, el compilador simplemente omite la llamada al destructor (aunque se emite una advertencia). Considere un ejemplo:
class X;
Este código se compila incluso si la delete
declaración de clase completa no está disponible en el dial-peer X
. Visual Studio muestra la siguiente advertencia:warning C4150: deletion of pointer to incomplete type 'X'; no destructor called
Si hay una implementación X
y CreateX()
luego se compila el código, si CreateX()
devuelve un puntero a un objeto creado por el operador new
, la llamada se Foo()
ejecuta con éxito, no se llama al destructor. Está claro que esto puede conducir a una fuga de recursos, por lo que una vez más sobre la necesidad de tener cuidado con las advertencias.
, -. , . , , , , . [Meyers2].
:
4. ,
4.1.
++ , . . . , 1.1.
Aquí hay un ejemplo:
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.
++ , , , . Aquí hay un ejemplo:
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. Gestión 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++ . En cambio
#define XXL 32
const int XXL=32;
. inline
.
# ( ).
:
9.
- . . . , .
- .
- . ++ — ++11/14/17.
- - , - .
- .
Referencias
[Dewhurst]
, . C++. .: . del ingles — .: , 2012.
[Meyers1]
, . C++. 55 .: . del ingles — .: , 2014.
[Meyers2]
, . C++: 42 C++11 C++14.: . del ingles — .: «.. », 2016.
[Sutter]
, . C++.: . del ingles — : «.. », 2015.
[Spolsky]
, . .: . del ingles — .: -, 2008.