Cheat Sheet

Beginning Programming with C++ For Dummies

C++ isn't an easy programming language to master. Only through experience will the myriad combinations of symbols start to seem natural to you. This Cheat Sheet, however, gives you some solid tips on easing that transition from C++ beginner to C++ guru: Know how to read complex C++ expressions; learn how to avoid pointer problems; and figure out how and when to make deep copies.

How to Read a Complex C++ Expression

C++ is full of little symbols, each of which adds to the meaning of expressions. The rules of C++ grammar are so flexible that these symbols can be combined in almost impenetrably complex combinations. Expressions in the simpler C language can get so obtuse that there used to be an annual contest for who could write the most obscure program and who could understand it.

It's never a good idea to try to write complex code but you will sometimes run across expressions in C++ that are a bit bewildering at first glance. Just use the following steps to figure them out:

  1. Start at the most embedded parentheses.

    Start looking for the outer most parentheses. Within those, look for embedded parentheses. Repeat the process until you've worked your way to the deepest pair of parentheses. Start evaluating that subexpression first using the following rules. Once you understand that expression, pop back out to the next level and repeat the process.

  2. Within the pair of parentheses, evaluate each operation in order of precedence.

    The order that operators are evaluated is determined by the operator's precedence shown in the table. Indirection comes before multiplication which comes before addition thus the following adds 1 plus 2 times the value pointed at by *ptr.

        int i = 1 + 2 * *ptr;
Operators in Order of Precedence
Precedence Operator Meaning
1 () (unary) Invoke a function
2 * and -> (unary) Dereference a pointer
2 - (unary) Returns the negative of its argument
3 ++ (unary) Increment
3 -- (unary) Decrement
4 * (binary) Multiplication
4 / (binary) Division
4 % (binary) Modulo
5 + (binary) Addition
5 - (binary) Subtraction
6 && (binary) Logical AND
6 !! Logical OR
7 =, *=,%=,+=,-= (special) Assignment types
  1. Evaluate operations of the same precedence from left to right (except assignment, which goes the other way).

    Most operators of the same precedence evaluate from left to right. Thus the following adds 1 to 2 and adds the result to 3:

            int i = 1 + 2 + 3;

    The order of evaluation of some operators doesn't matter. For example, addition works the same from left to right as it does from right to left. The order of evaluation makes a lot of difference for some operations like division. The following divides 8 by 4 and divides the result by 2:

            int i = 8 / 4 / 2;

    The main exception to this rule is assignment, which is evaluated from right to left:

            a = b = c;

    This assigns c to b and the result to a.

  2. Evaluate subexpressions in no particular order.

    Consider the following expression:

            int i = f() + g() * h();

    Multiplication has higher precedence, so you might assume that the functions g() and h() are called before f(), however, this isn't the case. Function call has the highest precedence of all, so all three functions are called before either the multiplication or the addition is performed. (The results returned from g() and h() are multiplied and then added to the results returned from f().)

    The only time that the order that functions are called makes a difference is when the function has side effects such as opening a file or changing the value of a global variable. You should definitely not write your programs so that they depend upon these type of side effects.

  3. Perform any type conversions only when necessary.

    You should not make more type conversions than absolutely necessary. For example, the following expression has at least three and possibly four type conversions:

            float f = 'a' + 1;

    The char 'a' must be promoted to an int to perform the addition. The int is then converted to a double and then down converted to a single precision float. Remember that all arithmetic is performed either in int or double. You should generally avoid performing arithmetic on character types and avoid single precision float altogether.

5 Ways to Avoid Pointer Problems in C++

In C++, a pointer is a variable that contains the address of an object in the computer's internal memory. Use these steps to avoid problems with pointers in C++:

  1. Initialize pointers when declared.

    Never leave pointer variables uninitialized - things wouldn't be too bad if uninitialized pointers always contained random values - the vast majority of random values are illegal pointer values and will cause the program to crash as soon as they are used. The problem is that uninitialized variables tend to take on the value of other, previously used pointer variables. These problems are very difficult to debug.

    If you don't know what else to initialize a pointer to, initialize it to nullptr. nullptr is guaranteed to be an illegal address.

  2. Zero out pointers after you use them.

    Similarly, always zero a pointer variable once the pointer is no longer valid by assigning it the value nullptr. This is particularly the case when you return a block of memory to the heap using delete; always zero the pointer after returning heap memory.

  3. Allocate memory from the heap and return it to the heap at the same "level" to avoid memory leaks.

    Always try to return a memory block to the heap at the same level of abstraction as you allocated it. This generally means trying to delete the memory at the same level of function calls.

  4. Catch an exception to delete memory when necessary.

    Don't forget that an exception can occur at almost any time. If you intend to catch the exception and continue operating (as opposed to letting the program crash), make sure that you catch the exception and return any memory blocks to the heap before the pointers that point to them go out of scope and the memory is lost.

  5. Make sure that the types match exactly.

    Always make sure that the types of pointers match the required type. Don't recast a pointer without some specific reason. Consider the following:

void fn(int* p);
void myFunc()
{
    char c = 'a';
    char* pC = &c;
    fn((int*)pC);
}

The above function compiles without complaint since the character pointer pC has been recast to an int* to match the declaration of fn(int*); however, this program will almost surely not work. The function fn() is expecting a pointer to a full 32-bit integer and not some rinky-dink 8 bit char. These types of problems are very difficult to sort out.

How and When to Make Deep Copies in C++

Classes that allocate resources in their constructor should normally include a copy constructor to create copies of these resources. Allocating a new block of memory and copying the contents of the original into this new block is known as creating a deep copy (as opposed to the default shallow copy). Use the following steps to determine how and when to make deep copies in C++:

  1. Always make a deep copy if the constructor allocates resources.

    By default, C++ makes so-called "shallow" member-by-member copies of objects when passing them to functions or as the result of an assignment. You must replace the default shallow copy operators with their deep copy equivalent for any class that allocates resources in the constructor. The most common resource that gets allocated is heap memory that is returned by the new operator.

  2. Always include a destructor for a class that allocates resources.

    If you create a constructor that allocates resources, you must create a destructor that restores them. No exceptions.

  3. Always declare the destructor virtual.

    A common beginner error is to forget to declare your destructor virtual. The program will run fine until some unsuspecting programmer comes along and inherits from your class. The program still appears to work, but because the destructor in the base class may not be invoked properly, memory leaks from your program like a sieve until it eventually crashes. This problem is difficult to find.

  4. Always include a copy constructor for a class that allocates resources.

    The copy constructor creates a proper copy of the current object by allocating memory off of the heap and copying the contents of the source object.

  5. Always override the assignment operator for a class that allocates resources.

    Programmers should be discouraged from overriding operators, but the assignment operator is an exception. You should override the assignment operator for any class that allocates resources in the constructor.

    The assignment operator should do three things:

    1. Make sure that the left and right hand object aren't the same object. In other words, be sure that the application programmer didn't write something like (a = a). If they are, do nothing.

    2. Invoke the same code as the destructor on the left hand object to return its resources.

    3. Invoke the same code as a copy constructor to make a deep copy of the right hand object into the left hand object.

  6. If you can't do that, then delete the copy constructor and the assignment operator so that the program can't make copies of your object.

  7. If you can't even do that because your compiler doesn't support the C++ 2011 delete constructor feature, create an empty copy constructor and assignment operator and declare them protected to keep other classes from using them.

  • Add a Comment
  • Print
  • Share
blog comments powered by Disqus
Advertisement

Inside Dummies.com