Basics

  • Initialization list

      int age = 21;             // C-like initialization
      int age (21);             // Constructor initialization
      int age {21};             // C++11 initialization list
      char grade {'A'};         // C++11 initialization list
      bool game_over {false};   // C++11 initialization list
    	
      // similar for arrays/vectors
      int arr1[] {1, 2, 3, 4, 5};    // 1,2,3,4,5
      int arr2[5] {1, 2};            // 1,2,0,0,0
      int arr3[5] {0};               // 0,0,0,0,0
      int arr4[5] {};                // 0,0,0,0,0
      int movie_rating [3][4]
      {
          {0, 4, 3, 5},
          {2, 3, 3, 5},
          {1, 4, 4, 5}
      };
    

    There are a lot of benefits of using initialization list, e.g., check inconsistency between type and initialization value, and sometimes it is required to use initialization list, e.g., initialize non-static const data members. In any situations, it is helpful in performance, i.e., no need to call a default constructor and then modify the default value.

  • Type of Constants

    There are many types of constants:

    • Declared constants (const keyword)

    • Constant expressions (constexpr keyword)

    • Enumerated constants (enum keyword)

    • Defined constants (#define)

      • Do NOT use defined constants in Modern C++, e.g., #define pi 3.1415926. They may appear in some legacy code.
      • It is defined as a preprocessor directive. What it actually does is to tell preprocessor that wherever it sees the text pi, replace it with 3.1415926. This is like a blind find replace as we might do in a word processor. However, since preprocessor doesn’t know C++ (this is complier’s job), it can’t help us to do a type check and thus lead to difficulty to find errors.
  • Type Casting

    A preferred way in C++ to do type casting is to use something like static_cast<type> rather than the old C-style way (type).

    • (type) is too flexible - we can pretty much cast anything to any type using this technique. Therefore it is not always safe to do so.

    • static_cast<type> is more restrictive. It does basic checking to see if the type we are casting makes sense (e.g., child class pointer to a base class pointer).

  • Alphanumerical Bool Values

    Sometimes it is handy to display the words true and false rather than 1 and 0 in the output statements. To achieve this, we can use stream manipulator boolalpha (toggle on) and noboolaplha (toggle off), which are located in standard namespace.

      bool b = true;
      std::cout << std::boolalpha << b << '\n';      // true
      std::cout << std::noboolalpha << b << '\n';    // 1
    
  • Range-based For Loop

    It allows us to loop through a collection of elements, e.g., array, vector, initialization list, string, etc, and be able to easily access each element without having to worry about the length or increment/decrement or indices.

      int arr[] {1, 2, 3};
      for (int a : arr) 
          cout << a << endl;
    	
      // We do not need to explicitly provide the type of the variable
      // Complier will figure out the type for us
      for (auto a : arr) 
          cout << a << endl;
    	    
      // use an initialization list as a collection
      for (auto a : {1, 2, 3}) 
          cout << a << endl;
    	
      // string is a collection of characters
      for (auto ch : "Hello") 
          cout << a << endl;
    
  • C++ Strings

    std::string is a class in STL. Like C-style strings, C++ strings are stored contiguously in memory; however, unlike C-style strings, C++ strings are dynamic (can grow or shrink when needed in run time).

      string s1 {"hello"};
      string s2 {"world"};
    		
      string s;
      s = s1 + " " + s2;              // "hello world"
      s = "hello" + " " + "world";    // illegal since they are C-style string literals which do not support concatenation (only works for C++ strings)
    

Functions

  • Difference between parameters and arguments:

    • Parameters refer to the parameters defined in the function header or prototype (or formal parameters).

    • Arguments refer to the parameters used in a actual function call (or actual parameters).

  • Difference between pass-by-value and pass-by-reference:
    • When we pass data into a function, it is passed-by-value (default behaviour in C++), which means a copy of data is passed in. Any changes we make to the parameters in the function will NOT affect the argument that was passed in.

      • This is good: We won’t change the original argument by mistake or intentionally.
      • This is not good: Sometimes making a copy of data can be expensive both in storage and time to actually copy that data. Also, sometimes we do want to change the original data.
    • Reference parameters will tell the compiler to pass in a reference to the actual parameter. And the formal parameter will now be an alias for the actual parameter.

      • It allows us to change an actual parameter if we need to. If the parameter is not supposed to be changed, we could use a const reference.
      • We do not make a copy of the parameter, which could be large and take time. What we actually passed in is the location or address in memory of the parameter. Therefore, we avoid the storage and copy overhead of pass-by-value.
  • Passing array to functions is different:

    • Array elements are not copied since the array name stands for the location of the first element of the array in memory - this address is what is copied indeed.

      • For the same reason, the function has no idea how many elements are in the array since all it knows is the location of the first element in memory (the name of the array).
      • A common practice is to pass the size of the array to a function at the same time. Note that we do not need to do this for passing C-style string (it has a sentinel null terminator \0) and C++ string (it has length()/size() method).
    • One important thing is, the function can modify the data in original array now.

      • If this is something we want to avoid, we can tell the compiler that the function parameters are const (read-only within the function body). Then any attempt to modify them will result in a compiler error.
  • Default argument values:

    • Default values can be set in the prototype or definition, but NOT BOTH!

      • A best practice is to always add default values in the prototype.
      • Default values must appear at the tail end of the parameter list.
      • If we have multiple default values, they must appear consecutively at the tail end of the parameter list.
    • We should be careful when we use default values and overloading together. If compiler does not know which one to use, an ambiguous call error will be generated.

  • Static local variables

    • Normally, values of local variables are not preserved between function calls. They are allocated on the function call stack. With the stack popping out, they will disappear. However, this is not the case for static local variabels.

    • A static local variable is not stored in the stack frame. Its lifetime is the lifetime of the entire program. But it is still only visible within the function body (like other local variables).

      • Its value is preserved between function calls. So it is useful when we need to know the previous value in the function.
      • Static local variables are only initialized once; if no initializer is provided, they are set to zero.
  • Inline functions

    • Usually, function calls have certain amount of overhead, e.g., create stack frame, push on a stack, pop off the stack, deal with return address and return values. Although all of this can be done very quickly and efficiently, it still happens.

    • Sometimes we have a very simple function and the function call overhead might be greater than the time spent executing the function, e.g., a very short/small function. In this case, we can suggest to the compiler that it generate inline code.

      • Just add keyword inline preceding the function return type to tell compiler treat it as an inline function (usually declared in header files). They are basically inline assembly code that avoid function call overhead.
      • Inline code is generally faster.
      • However, if we inline a function many times, we are actually duplicating a function code in many places. And this would lead to large binaries.
      • Compilers are so sophisticated now such that they will likely inline short function code even without our suggestion.

Pointers and References

Many programming languages don’t have pointers (exposed to users), e.g., Java and Python. This is arised from why those language are created and what domains are typically used in those languages.

C++ is used extensively to develop operating systems, systems software device drivers, embedded systems and so forth. With these systems, we want to be in complete control over the hardware. We don’t want a virtual machine handling memory or checking for everything that could go wrong. We just simply can’t afford that extra overhead at runtime. That’s exactly the reason pointers come in. With the power of pointer, it also comes responsibility to understand how to use and release memory efficiently and correctly.

  • Initialize pointers

    A best practice is to always initialize pointers to point nowhere using int* p {} or int* p {nullptr}. Otherwise, it contains garbage data and can point to anywhere.

    • Initializing to zero or nullptr (C++ 11) represents address zero, which implies that the pointer is pointing nowhere.
  • sizeof a pointer variable

    A pointer is just a simple variable that stores the memory address of another variable it points to.

    • Do NOT think of a pointer as what it points to! There is a big difference between the size of the pointer variable itself and the size of what it points to:

      • All pointers in a program have the same size (they are just variables store some memory address).
      • However, they may be pointing to very large/complex or very small/simple types of variables.
  • Dynamic memory allocation

    Since we often don’t know how much storage we need until we need it, techniques of allocating storage from the heap at runtime is desired.

    • Note that the allocated storage (memory address) contains garbage data until we initialize it.

    • The allocated storage does not have a name. The only way to get to that storage is via the pointer. If we lose the pointer because it goes out of scope or we reassign it then you lost our only way to get to that storage and we have a memory leak.

    • Finally when we are done using the storage then we must deallocate the storage so that it’s again available to the rest of the program. A good practice is that after we deallocate the storage, we further assign the pointer to nullptr to avoid the case of wild pointer or dangling pointer, i.e., make sure that we will not access to that particular block of memory in the future (lost that storage on purpose); otherwise we could have unpredictable results.

  • Relationship between arrays and pointers

    C++ does not really have true arrays and arrays are just the address of the first element in for a block of memory. And pointer arithmetic only makes sense with raw arrays.

  • Potential pointer pitfalls

    • Uninitialized pointers: they contain garbage, which means they can point to anywhere. If we try to access or modify the data they’re pointing to, we could run into some major problems (the pointer might be pointing to a very important area in memory and we could wipe it out).

      • Modern operating systems today are pretty good at protecting critical areas of system memory. But we could still trash an area important to our program that could cause our program to crash.
    • Dangling pointers: they point to memory that’s no longer valid. If we try to use these pointers to access that data we don’t know what the results will be. The main reasons for causing dangling pointers:

      • return addresses of function local variables on the stack that are no longer valid since the function is terminated
      • release the dynamic memory but the the pointer is still referencing to it
    • Not checking if new failed: if new fails to allocate storage, an exception is thrown and our program terminates. We can use exception handling to get more fine grained control over these exceptional situations. If we try to dereference a pointer which is pointing to null, our program would crash.

    • Leaking memory: dynamically allocate memory in a function but forget to release the memory when the function terminates will make us lost our pointer (except that the function returns the pointer in which case there is no sense to release it). There’s no way we can reference that allocated memory on the heap (we access it only through pointer since storage has no name). This memory is still considered in use by C++ so this is called a memory leak. If we leak enough memory we could run out of storage on the heap for future allocations.

  • Reference

    • Reference is an alias for a variable

    • It must be initialized when declared and cannot be null

    • Once initialized it cannot be made to refer to another variable

    • It might be helpful to think of a reference as a constant pointer that is automatically dereferenced

    • It is very useful as function parameters

  • When to use pointers or references parameters?

    • depends on whether the function modifies the actual parameter

    • depends on whether the parameter is small or efficient to copy, like primitive types (int, char, double, etc.)

      • Note that the collection types like strings, vectors and others have a certain amount of overhead involved when they’re copied. So we’d better think twice before we pass those by value.
    • depends on whether we want to pass a nullptr at some point. This is important because a lot of data structures rely on pointers becoming null at the end (lists, trees, etc.). So in those cases we really want to pass pointers and not references because references can’t be null.

      could modify the actual param? efficient/easy to copy? nullptr is allowed? could modify the pointer itself?
    Pass-by-value N N    
    Pass-by-regular-pointer Y Y Y Y
    Pass-by-pointer-to-const N Y Y Y
    Pass-by-const pointer-to-const N Y Y N
    Pass-by-reference Y Y N N
    Pass-by-const reference N Y N N

 


- End -