This article analyzes the C + + compiler can not capture 8 kinds of errors, shared for everyone to refer to the use. Help to understand the operating principles of C + +, the specific analysis is as follows:
As we all know, C + + is a complex programming language, which is full of subtle pitfalls. There are almost countless ways to screw things up in C + +. Fortunately, today's compilers are intelligent enough to detect quite a few of these programming traps and notify programmers by compiling errors or compiling warnings. Eventually, if handled properly, any errors that the compiler can check will not be a big problem because they are captured at compile time and resolved before the program actually runs. At worst, a compiler's ability to catch bugs can only cause a programmer some time loss because they will look for ways to fix the compilation errors and fix them.
Errors that the compiler cannot catch are the most dangerous. Such errors are less susceptible to detection, but can lead to serious consequences, such as incorrect output, corrupted data, and program crashes. As the project expands, the complexity of the code logic and the number of execution paths cover up the bugs, causing them to be intermittent, making it difficult to track and debug such bugs. Although this list of this article is largely retrospective for experienced programmers, the consequences of such bugs are often enhanced in varying degrees depending on the size and commercial nature of the project.
All of these examples were tested on visual Studio Express, using the default alert level. Depending on the compiler you choose, you may get a different result. I strongly recommend that all programmer friends use the highest level of alarm! Some compile hints may not be labeled as a potential problem under the default alarm level, and will be captured at the highest level of alarm levels!
1) Variable not initialized
Variable initialization is one of the most common and easy to make mistakes in C + + programming. In C + +, the memory space allocated for a variable is not completely "clean" and does not automatically do 0 processing when allocating space. As a result, an uninitialized variable will contain a value, but there is no way to know exactly what the value is. In addition, each time the program is executed, the value of the variable may change. This has the potential to produce intermittent episodes that are particularly difficult to track. Look at the following code fragment:
if (bvalue)
//Do an
else
//do B
If the bvalue is an uninitialized variable, the result of the IF statement cannot be determined and two branches may execute. In general, the compiler prompts for uninitialized variables. The following code fragment raises a warning message on most compilers.
int foo ()
{
int nX;
return NX;
}
However, there are some simple examples that do not produce a warning:
void increment (int &nvalue)
{
++nvalue;
}
int foo ()
{
int nX;
Increment (NX);
return NX;
}
The code fragment above may not produce a warning because the compiler will not normally be able to track the function increment () to see if the Nvalue is assigned to the value.
Uninitialized variables are more commonly found in classes, and the initialization of members is typically done through the implementation of constructors.
Class Foo
{
private:
int m_nvalue;
Public:
Foo ();
int GetValue () {return m_bvalue;}
};
Foo::foo ()
{
//Oops, we forgot to initialize the M_nvalue
}
int main ()
{
Foo cfoo;
if (Cfoo.getvalue () > 0)
//do something
else
//does something else
}
Note that M_nvalue has never been initialized. As a result, GetValue () returns a garbage value that may be executed by two branches of the IF statement.
Novice programmers typically make the following error when defining multiple variables:
int nValue1, nValue2 = 5;
The intention here is that both nValue1 and nValue2 are initialized to 5, but in fact only nValue2 are initialized and nValue1 has never been initialized.
Because an uninitialized variable may be any value, causing the program to behave differently each time it executes, the problem caused by an uninitialized variable is difficult to find the source of the problem. At one time, the program might work properly, and the next time it might crash, the next time it might produce the wrong output. When you run the program under the debugger, the defined variables are usually handled by the 0. This means that your program may work correctly under the debugger every time, but it may break down intermittently in the release version! If you encounter such a strange thing, the culprit is often an uninitialized variable.
2) Integer Division
Most two-dollar operations in C + + require both operands to be of the same type. If the operands are of different types, one of the operands is elevated to the type that matches the other operand. In C + +, the division operator can be viewed as 2 different operations: one operates over an integer and the other is manipulated above a floating-point number. If the operand is a floating-point type, the division returns the value of a floating-point number:
float FX = 7;
float FY = 2;
float fvalue = fx/fy; Fvalue = 3.5
If the operand is an integer type, the division discards any fractional part and returns only the integer portion.
int nX = 7;
int NY = 2;
int nvalue = nx/ny; Nvalue = 3
If one operand is an integral type and the other operand is a floating-point type, the integral type is promoted to a floating-point type:
float FX = 7.0;
int NY = 2;
float fvalue = fx/ny;
NY is promoted to floating-point type, the division returns the floating-point value
//Fvalue = 3.5
A lot of novice programmers will try to write down the following code:
int nX = 7;
int NY = 2;
float fvalue = nx/ny; Fvalue = 3 (not 3.5 Oh!) )
The idea here is that nx/ny will produce a floating-point division operation because the result is assigned to a floating-point variable. But that is not the case. Nx/ny is evaluated first, and the result is an integer value that is then promoted to a floating-point type and assigned to Fvalue. But before assigning a value, the decimal part is discarded.
To force two integers to use floating-point division, one of the operands requires a type conversion to a floating-point number:
int nX = 7;
int NY = 2;
float Fvalue = static_cast<float> (NX)/NY; Fvalue = 3.5
Because the NX is explicitly converted to float, NY is implicitly promoted to float, so the division operator performs floating-point division, and the result is 3.5.
It's often hard to see how a division operator performs integer division or floating-point division:
z = x/y; Is this an integer division or a floating-point division?
But using the Hungarian nomenclature can help us eliminate this confusion and prevent errors from happening:
int nZ = nx/ny; Integer division
Double DZ = dx/dy;//Floating-point division
Another interesting thing about integer division is that when one operand is negative, the C + + standard does not specify how to truncate the result. As a result, the compiler is free to choose to truncate up or down! For example, -5/2 can be calculated as either-3 or 2, which is related to whether the compiler is rounding down or rounding 0. Most modern compilers are rounded to 0.
3) = vs = =
It's an old problem, but it's valuable. Many C + + beginners will mix assignment operators (=) and equality operators (= =) meaning. But even programmers who know the difference between the two operators make keyboard-knocking errors, which may result in unintended results.
If Nvalue is 0, return 1, otherwise return nvalue
int foo (int nvalue)
{
if (nvalue = 0)///This is a keyboard knock error! return
1;
else return
nvalue;
}
int main ()
{
std::cout << foo (0) << Std::endl;
Std::cout << foo (1) << Std::endl;
Std::cout << foo (2) << Std::endl;
return 0;
}
The intent of the function foo () is to return 1 if the nvalue is 0, otherwise return the Nvalue value. However, by inadvertently using the assignment operator instead of the equality operator, the program produces unintended results:
When the If statement in Foo () executes, the Nvalue is assigned a value of 0. if (Nvalue = 0) is actually an if (nvalue). The result is that if the condition is false, it causes the code under else to be executed, returning the value of Nvalue, which is exactly the value assigned to Nvalue 0! So this function will always return 0.
The alarm level is set to the highest in the compiler, a warning message is given when an assignment operator is used in a discovery conditional statement, or if, in addition to the conditional judgment, the assignment operator should be misused as an equality test, the statement is prompted to do nothing. As long as you use a higher alarm level, the problem is essentially repairable. Some programmers like to use a technique to avoid the confusion of = and = =. That is, the constants are written to the left in the conditional judgment, and if you mistakenly write = =, a compilation error will be thrown because the constants cannot be assigned.
4) mixed signed and unsigned numbers
As we mentioned in the section on integer division, most of the two-dollar operators in C + + require that the operands at both ends be of the same type. If the operand is of a different type, one of the operands will raise its own type to match the other operand. This can result in unexpected results when mixed with signed and unsigned numbers! Consider the following example:
cout << 10–15u; 15u is an unsigned integer
Some would say the result is-5. Since 10 is a signed integer, and 15 is an unsigned integer, the type elevation rule needs to work here. The type elevation hierarchy in C + + looks like this:
Long double (highest)
Double
Float
unsigned long int
Long int
unsigned int
Int (lowest)
Because the int type is lower than the unsigned int, the int is promoted to unsigned int. Fortunately, 10 is already a positive integer, so type promotion does not change the way that the value is interpreted. Therefore, the above code is equivalent to:
Well, it's time to take a look at this trick. Because all are unsigned integers, the result of the operation should also be a variable of unsigned integer! 10u-15u = -5u. However, unsigned variables do not include negative numbers, so-5 will be interpreted as 4,294,967,291 (assuming a 32-bit integer). Therefore, the code above will print out 4,294,967,291 instead of-5.
This can be a more misleading form:
int NX;
unsigned int nY;
if (Nx–ny < 0)
//Do something
Because of the type conversion, this if statement will always be judged as false, which is obviously not the original intention of the programmer!
5) Delete vs Delete []
Many C + + programmers forget that the new and delete operators actually have two forms: a version for a single object, and a version of an array of objects. The new operator is used to allocate the memory space of a single object on the heap. If the object is a class type, the constructor of the object is invoked.
The delete operator is used to reclaim the memory space allocated by the new operator. If the object being destroyed is a class type, the destructor of the object is invoked.
Now consider the following code fragment:
Foo *parray = new FOO[10];
This line of code allocates memory space for an array of 10 Foo objects, because the subscript [10] is placed after the type name, and many C + + programmers are unaware that the operator new[is actually called to complete the task of allocating space instead of new. New[] operator ensures that each created object invokes the constructor of that class once. Instead, to delete an array, you need to use the delete[] operator:
This ensures that each object in the array invokes the destructor of the class. What happens if the delete operator acts on an array? Only the first object in the array will be destructor, causing heap space to be corrupted!
6 side effects of compound expressions or function calls
Side effects are those in which an operator, expression, statement, or function continues to do something after the operator, expression, statement, or function completes the specified operation. Side effects are sometimes useful:
The side effect of the assignment operator is that you can permanently change the value of X. Other side-effects of C + + operators include *=,/=,%=, + =, =, <<=, >>=, &=, |=, ^=, and notorious + + and-operator characters. However, there are several places in C + + where the order of operations is undefined, which can result in inconsistent behavior. Like what:
void multiply (int x, int y)
{
using namespace std;
cout << x * y << Endl;
}
int main ()
{
int x = 5;
Std::cout << Multiply (x, ++x);
}
Because the order of the arguments for the function multiply () is undefined, the above program may print out 30 or 36, depending entirely on the X and ++x who first calculates and who calculates.
Another slightly odd example of the operator:
int foo (int x)
{return
x;
}
int main ()
{
int x = 5;
Std::cout << foo (x) * foo (++x);
}
Because in the operator of C + +, the order in which the operands are calculated is undefined (for most operators, of course, there are exceptions), and the example above may print out 30 or 36, depending on whether the left operand is calculated first or the right-hand operand.
In addition, consider the following composite expression:
if (x = = 1 && ++y = 2)
//Do something
The programmer's intention might be to say: "If X is 1, and Y's forward value is 2, then some processing is done." However, if X is not equal to 1,c++ will take the short-circuit evaluation rule, which means that ++y will never be counted! So, only if x equals 1 o'clock, Y will increase itself. This is probably not the programmer's intention! A good rule of thumb is to put any operator that can cause side effects into their own separate statements.
7 switch statement with no break
Another classic mistake that novice programmers often make is to forget to add a break to the switch statement block:
Switch (nvalue)
{case
1:ecolor = color::blue;
Case 2:ecolor = Color::P urple;
Case 3:ecolor = Color::green;
Default:ecolor = color::red;
}
When the switch expression calculates the same result as the label value of the case, the execution sequence executes from the first case statement that is satisfied. The execution sequence continues until either the end of the switch statement block is reached or a return, Goto, or break statement is encountered. All the other tags will be ignored!
Consider the code above, what happens if Nvalue is 1. Case 1 is satisfied, so EColor is set to Color::blue. Continue with the next statement, which sets the EColor to color::P urple. The next statement sets it to Color::green. In the end, it is set to color::red in default. In fact, regardless of the value of Nvalue, the above code fragment will set EColor to color::red!
The correct way to do this is to write in the following way:
Switch (nvalue)
{case
1:ecolor = color::blue;
Case 2:ecolor = Color::P urple; break;
Case 3:ecolor = Color::green; break;
Default:ecolor = color::red; break;
The break statement terminates execution of the case statement, so the value of the EColor will remain as expected by the programmer. Although this is a very basic switch/case logic, it is easy to avoid the inevitable "waterfall" flow of execution by omitting a break statement.
8 calling virtual functions in constructors
Consider the following procedure:
Class Base
{
private:
int m_nid;
Public:
Base ()
{
M_nid = ClassID ();
}
ClassID returns a class-related ID number
virtual int ClassID () {return 1;}
int GetID () {return m_nid;}
};
Class Derived:public Base
{public
:
Derived ()
{
}
virtual int ClassID () {return 2;}
};
int main ()
{
Derived cderived;
cout << Cderived.getid (); Print out 1, not 2! return
0;
}
In this program, the programmer invokes the virtual function in the constructor of the base class, expecting it to be derived::classid () of the derived class by resolution. But that's not actually the case--the result of the program is to print out 1 instead of 2. When a derived class that inherits from a base class is instantiated, the base class object is constructed before the derived class object. This is done because a member of a derived class may have dependencies on a base class member that has already been initialized. The result is that when the constructor of the base class is executed, the derived class object is not constructed at all! Therefore, any call to a virtual function at this point will only be resolved as a member function of the base class, not a derived class.
According to this example, when the base class portion of the cderived is constructed, that part of its derived class does not exist. Therefore, the call to the function ClassID the resolution to BASE::CLASSID () (not Derived::classid ()), which sets the M_nid to 1. Once CDerived's derived class part is also constructed, any invocation of ClassID () on the CDerived object will be Derived::classid () as expected.
Note that other programming languages such as C # and Java will invoke the virtual function to call the resolution as the deepest inheritance level, even if the derived class has not been initialized! The C + + approach differs from this in that it is considered for the safety of the programmer. This is not to say that one way is necessarily better than another, just to show that different programming languages may behave differently on the same issue.
Conclusion:
I think it would be more appropriate to start with a basic problem that novice programmers might encounter. Regardless of the level of experience of a programmer, errors are unavoidable, whether due to lack of knowledge, input errors, or just general carelessness. Aware of the problems that are most likely to cause trouble, this can help reduce the likelihood of them being disruptive. While there is no substitute for experience and knowledge, good unit testing can help us capture these bugs before they are buried deep in our code.
I believe that this article on the C + + program design has a certain learning value.