In C ++, there are a lot of features that can be considered potentially dangerous - when they are miscalculated in design or inaccurate coding, they can easily lead to errors. The article provides a selection of such features, given tips on how to reduce their negative impact.
Introduction
1. Types
1.1. Conditional instructions and operators
1.2. Implicit conversions of type (implicit conversions)
2. Name resolution
2.1. Hiding variables in nested scopes
2.2. Function overload
3. Constructors, destructors, initialization, removal
3.1. Compiler generated member functions
3.2. Uninitialized variables
3.3. The order of initialization of base classes and non-static class members
3.4. Initialization order for static class members and global variables
3.5. Destructor exceptions
3.6. Removing dynamic objects and arrays
3.7. Deletion on incomplete class declaration
4. Operators, expressions
4.1. Operators priority
4.2. Operator Overloading
4.3. The procedure for calculating subexpressions
5. Virtual Functions
5.1 Redefining Virtual Functions
5.2 Overloading and using default parameters
5.3 Calling virtual functions in constructor and destructor
5.4 Virtual Destructor
6. Direct work with memory
6.1 Going beyond the buffer
6.2 Z-terminated strings
6.3 Functions with a variable number of parameters
7. Syntax
7.1 Complicated Ads
7.2 Syntax ambiguity
8. Miscellaneous
8.1 Keyword inline and ODR
8.2 Header Files
8.3 switch statement
8.4 Passing parameters by value
8.5 Resource Management
8.6 Owning and not owning links
8.7 Binary compatibility
8.8 Macros
9. Results
Bibliography
Praemonitus, praemunitus.
Forewarned is forearmed. (lat.)
In C ++, there are a lot of features that can be considered potentially dangerous - when they are miscalculated in design or inaccurate coding, they can easily lead to errors. Some of them can be attributed to difficult childhood, some to the obsolete C ++ 98 standard, but others are already related to the features of modern C ++. Consider the main ones and try to give advice on how to reduce their negative impact.
The need for compatibility with C leads to the fact that in the if(...)
and similar instructions you can substitute any numeric expression or pointer, and not just expressions of the bool
type. The problem is aggravated by the implicit conversion from bool
to int
in arithmetic expressions and the priority of some operators. This leads, for example, to such errors:
if(a=b)
when if(a==b)
correct,if(a<x<b)
, if(a<x && x<b)
correct if(a<x && x<b)
,if(a&x==0)
when if((a&x)==0)
correct,if(Foo)
when correct if(Foo())
,if(arr)
, if correctly if(arr[0])
,if(strcmp(s,r))
when correct if(strcmp(s,r)==0)
.
Some of these errors cause a compiler warning, but not an error. Also, code analyzers can sometimes help. In C #, such errors are almost impossible, if(...)
and similar instructions require the bool
type, you cannot mix bool
and numeric types in arithmetic expressions.
How to fight:
if(MAX_PATH==x)
. It looks pretty kondovo (and even has its own name - «Yoda notation»), and it helps in a small number of the considered cases.const
qualifier as widely as possible. Again, it does not always help.if(x!=0)
instead of if(x)
. (Although it is possible to fall into the trap of the priorities of the operators, see the third example.)C ++ refers to languages ​​with strong typing, but implicit type conversions are quite widely used to make the code shorter. These implicit conversions can in some cases lead to errors.
The most annoying implicit conversions are conversions of a numeric type or pointer to bool
and from bool
to int
. It is these transformations (necessary for compatibility with C) that cause the problems described in section 1.1. Also implicit conversions are not always appropriate, potentially causing a loss of precision of numeric data (narrowing conversions), for example, from double
to int
. In many cases, the compiler issues a warning (especially when there may be a loss of precision in numeric data), but a warning is not an error. In C #, conversions between numeric types and bool
forbidden (even explicit), and conversions that potentially cause a loss of precision of numeric data are almost always an error.
A programmer can add other implicit conversions: (1) a single-parameter constructor definition without the explicit
keyword; (2) the definition of a type conversion operator. These transformations make additional security holes based on strong typing principles.
In C #, the number of embedded implicit conversions is much smaller; user-defined implicit conversions must be declared using the implicit
keyword.
How to fight:
In C ++, the following rule applies. Let be
// { int x; // ... // , { int x; // ... } }
According to the rules of C ++, the variable , declared in
, hides (hide) the variable
, declared in
The first declaration of
x
does not have to be in a block: it can be a member of a class or a global variable, it should just be visible in block
Imagine now the situation when you need to refactor the following code.
// { int x; // ... // , { // - } }
By mistake, changes are made:
// { // , : int x; // - // ... // : // - }
And now the code “something is done with from
” will have something to do with
from
! It is clear that everything does not work the way it used to, and it is often not easy to find what is happening. Not for nothing in C # to hide local variables is prohibited (though members of the class can). Note that the mechanism for hiding variables in one way or another is used in almost all programming languages.
How to fight:
Function overloading is an integral feature of many programming languages, and C ++ is no exception. But this opportunity should be used deliberately, otherwise you can come across trouble. In some cases, for example, when the constructor is overloaded, the programmer has no choice, but in other cases, refusing the overload can be quite justified. Consider the problems that arise when using overloaded functions.
If you try to consider all the possible options that may arise during overload resolution, the overload resolution rules turn out to be very complex and, therefore, difficult to predict. Additional complexity is introduced by template functions and overloading of built-in operators. C ++ 11 added problems with rvalue links and initialization lists.
Problems can create an algorithm for finding candidates for overload resolution in nested scopes. If the compiler has found some candidates in the current scope, then the further search is terminated. If the found candidates are not suitable, conflicting, deleted or inaccessible, an error is generated, but no further search is attempted. And only if there are no candidates in the current scope, the search moves to the next, wider scope. The name hiding mechanism works, almost the same as that discussed in section 2.1, see [Dewhurst].
Overloading of functions can reduce the readability of the code, and thus provoke errors.
Using functions with default parameters looks similar to using overloaded functions, although, of course, there are fewer potential problems. But the problem with the deterioration of readability and possible errors remains.
Be especially careful to use overload and default parameters for virtual functions, see section 5.2.
C # also supports function overloading, but the rules for overload resolution are slightly different.
How to fight:
=delete
), which appeared in C ++ 11, can be used to prohibit certain overload options.If the programmer has not defined the class member functions from the following list — the default constructor, the copy constructor, the copy assignment operator, the destructor — then the compiler can do it for him. C ++ 11 added a relocation constructor and a move assignment operator to this list. These member functions are called special member functions. They are generated only if they are used, and additional conditions specific to each function are fulfilled. Note that this usage may be quite hidden (for example, when implementing inheritance). If the requested function cannot be generated, an error is generated. (With the exception of moving operations, they are replaced by copying ones.) The member functions generated by the compiler are public and embedded. Details about special member functions can be found in [Meyers2].
In some cases, such help from the compiler may turn out to be a “disservice.” The absence of custom special member functions can lead to the creation of a trivial type, and this, in turn, causes the problem of uninitialized variables, see section 3.2. The generated member functions are public, and this is not always consistent with the design of the classes. In base classes, the constructor must be protected; sometimes, for a more subtle management of the life cycle of an object, a protected destructor is needed. If a class has a raw resource descriptor as a member and owns that resource, then the programmer must implement the copy constructor, copy assignment operator, and destructor. The so-called “Big Three Rule” is well known, which states that if a programmer has defined at least one of the three operations — a copy constructor, a copy assignment operator, or a destructor — then he must define all three operations. The displacement constructor and the displacement assignment operator generated by the compiler are also far from always what is needed. The destructor generated by the compiler in some cases leads to very subtle problems, which may result in resource leaks, see section 3.7.
The programmer can prohibit the generation of special member functions, in C ++ 11, you must apply the construction "=delete"
when declaring, in C ++ 98, declare the corresponding member function closed and not define it.
If the programmer is satisfied with the member functions generated by the compiler, then in C ++ 11 he can designate this explicitly, rather than simply dropping the declaration. To do this, the declaration should use the construction "=default"
, while the code is better readable and additional features related to access level control appear.
In C #, the compiler can generate a default constructor, usually it does not cause any problems.
How to fight:
Constructors and destructors can be called key elements of the C ++ object model. When an object is created, the constructor is invoked, and when it is deleted, the destructor is called. But compatibility issues with C forced to make some exceptions, and this exception is called trivial types. They are introduced to model the sish types and the life cycle of variables, without necessarily calling the constructor and destructor. Critical code, if compiled and executed in C ++, should work in the same way as in C. Trivial types include numeric types, pointers, enumerations, as well as classes, structures, unions, and arrays consisting of trivial types. Classes and structures must satisfy some additional conditions: the absence of a custom constructor, destructor, copy, virtual functions. For a trivial class, the compiler can generate a default constructor and destructor. The default constructor resets the object, the destructor does nothing. But this constructor will be generated and used, only if it is explicitly called when the variable is initialized. A variable of the trivial type will be uninitialized if no option of explicit initialization is used. The initialization syntax depends on the type and context of the declaration of the variable. Static and local variables are initialized upon declaration. For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression new T()
creates an object initialized by the default constructor, while new T
for trivial types creates an uninitialized object. When creating a dynamic array of the trivial type, new T[N]
, its elements will always be uninitialized. If an instance of std::vector<T>
is created or expanded and no parameters are provided for explicitly initializing elements, then they are guaranteed to invoke the default constructor. In C ++ 11, a new initialization syntax has appeared - using curly braces. An empty pair of parentheses means initialization using the default constructor. Such initialization is possible wherever traditional initialization is used, except that it became possible to initialize non-static members of the class when declaring, which replaces initialization in the initialization list of the constructor.
An uninitialized variable is arranged as follows: if it is defined in the scope of the namespace
(globally), it will have all the bits zero, if it is local, or dynamically created, then it will receive a random set of bits. It is clear that the use of such a variable can lead to unpredictable program behavior.
True progress does not stand still, modern compilers, in some cases, find uninitialized variables and give an error. Uninitialized code analyzers detect even better.
In the standard C ++ 11 library, there are templates called type properties (header file <type_traits>
). One of them allows you to determine whether a type is trivial. The std::is_trivial<>::value
expression is true
if T
trivial type and false
otherwise.
Cinnamon structures are also often referred to as Plain Old Data (POD). We can assume that POD and “trivial type” are practically equivalent terms.
In C #, uninitialized variables cause an error, this is controlled by the compiler. Fields of reference type objects are initialized by default if explicit initialization is not performed. Fields of objects of a meaningful type are initialized either by default or all must be explicitly initialized.
How to fight:
When implementing a class constructor, immediate base classes and non-static class members are initialized. The initialization order defines the standard: first, the base classes in the order in which they are declared in the list of base classes, then the non-static members of the class in the order of declaration. If necessary, explicit initialization of base classes and non-static members is used to list the constructor initialization. Unfortunately, the elements of this list are not required to be in the order in which initialization occurs. This must be taken into account if, when initializing, the elements of the list use references to other elements of the list. If an error occurs, the link may be to an object that has not yet been initialized. C ++ 11 allows you to initialize non-static class members when they are declared (using curly braces). .
C# : , , . .
:
, , namespace
() (), , . , . .
:
. , .
:
T
T* pt = new T(/* ... */);
delete
delete pt;
T* pt = new T[N];
delete[]
delete[] pt;
, , , : , .. . [Meyers1].
:
delete
.Certain problems can be created by the operator’s “omnivorous nature” delete
; it can be applied to a type pointer void*
or to a pointer to a class that has an incomplete (proactive) declaration. The operator delete
applied to the pointer to the class is a two-phase operation, first the destructor is called, then the memory is released. If the operator is applied delete
to a pointer to a class with an incomplete error declaration, the compiler simply skips the destructor call (although a warning is issued). Consider an example:
class X; // X* CreateX(); void Foo() { X* p = CreateX(); delete p; }
delete
full class declaration is not available at the call point X
. Visual Studio gives the following warning:warning C4150: deletion of pointer to incomplete type 'X'; no destructor called
X
CreateX()
, , CreateX()
, new
, Foo()
, . , , .
, -. , . , , , , . [Meyers2].
:
++ , . . . , 1.1.
Let's give an example:
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.1. — +
+=
. . , : ,
(), &&
, ||
. , (-), (short-circuit evaluation semantics), , . & ( ). & , .. .
, - (-) , . .
- , , . . [Dewhurst].
C# , , , .
:
++ , . ( : ,
(), &&
, ||
, ?:
.) , , , . :
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].
:
++98 , ( ), , ( , ). virtual
, , . ( ), , , . , , . , ++11 override
, , , . .
:
override
.. , , . . . [Dewhurst].
:
, , . , , post_construct pre_destroy. , — . . , : ( ) . (, , .) , ( ), ( ). . [Dewhurst]. , , .
— - .
, C# , , , . C# : , , . , ( , ).
:
, , delete
. , - .
:
— C/C++, . . . « ».
C# unsafe mode, .
/++ , : strcpy()
, strcat()
, sprinf()
, etc. ( std::vector<>
, etc.) , . (, , , . . Checked Iterators MSDN.) , : , , ; , .
C#, unsafe mode, .
:
_s
(. )., . , :
strncpy(dst,src,n);
strlen(src)>=n
, dst
(, ). , , . . — . if(*str)
, if(strlen(str)>0)
, . [Spolsky].
C# string
.
:
_s
(. )....
. printf
- , C. , , , , . , .
C# printf
, .
:
printf
- /.++ , , , . Here is an example:
const int N = 4, M = 6; int x, // 1 *px, // 2 ax[N], // 3 *apx[N], // 4 F(char), // 5 *G(char), // 6 (*pF)(char), // 7 (*apF[N])(char), // 8 (*pax)[N], // 9 (*apax[M])[N], // 10 (*H(char))(long); // 11
:
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# , .
:
.
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{}; // C++11
, , , . [Sutter].
, , C++ ( ). . ( C++ .)
, , , , .
C# , , .
:
inline
ODR, inline
— . , . inline
(One Defenition Rule, ODR). . , . , ODR. static
: , , . static
inline
. , , ODR, . , . - , -. .
:
inline
. namespace
. , .namespace
.. . , , , , .
:
using
-: using namespace
, using
-.switch
— break
case
. ( .) C# .
:
++ , — , — . ( class
struct
) , . ( , # Java.) — , .
std::string
, std::vector
, etc.), , ., , , . . , , . , . . — ( =delete
), — explicit
.
C# , .
:
++ . , . - ( ), ++11 , , , .
C# , . , . (using-) Basic Dispose.
:
«» . , , C++ , STL- - .
. . , . . «», . COM- . (, .) , C++ . — . . . , («» ) , . .
# , . — .
:
C++ , : , , . ( !) . , . , . , , . (, .)
C ( ), C++ C ( extern "C"
). C/C++ .
-. #pragma
- , , .
, , , .
, , COM. COM-, , ( , ). COM , , .
C# . , — , C#, C# C/C++.
:
, . , . C++ . Instead
#define XXL 32
const int XXL=32;
. inline
.
# ( ).
:
[Dewhurst]
, . C++. .: . from English — .: , 2012.
[Meyers1]
, . C++. 55 .: . from English — .: , 2014.
[Meyers2]
, . C++: 42 C++11 C++14.: . from English — .: «.. », 2016.
[Sutter]
, . C++.: . from English — : «.. », 2015.
[Spolsky]
, . .: . from English — .: -, 2008.
Source: https://habr.com/ru/post/428898/
All Articles