Basics

  • Procedural programming

    • The focus of procedural programming is the function. We modularize our programs by creating a collection of functions that each specifies a process or action in the program. In procedural programming, we declare our data separate from the functions. Wherever we need function to process or use our data, we pass in the data to the function.

    • In many cases we have to use procedural programming since not all languages support object-oriented features. It is also very easy to learn since we’re pretty good at breaking up a task into subtasks which is exactly what procedural programming is all about.

    • Limitations: functions need to know about the structure of the data. If the structure or format for a data structure being passed around changes then this would affect many functions. All of these functions have to be modified to handle the new format of the data. Moreover, as programs become larger and more complex, they become more:

      • difficult to understand since the number of connections in the program becomes very hard to trace by hand
      • difficult to maintain the software, extend the program, find and fix bugs
      • difficult to reuse functions or data structures that we wrote for one program in another program since after time we end up with code whose behavior isn’t that easy to visualize
      • fragile and easier to break, which means that when we add new functionality or fix a bug, the chances of introducing another bug is high
  • Object-oriented programming (OOP)

    • OOP is all about modeling our software in terms of classes and objects. These classes and objects model real-world entities in our problem domain. As our programs grow more and more complex, we need ways to deal with the complexity, classes and objects are one way to do this. They allow us to think at a high level of abstraction and lead to large successful programs.

    • The major difference between procedural programming and OOP is that the data and the operations are together in the class where they belong and not spread across many functions. The fact that objects contain data and operations that work on that data is called encapsulation and it’s an extension of the abstract data type (ADT) in computer science. Encapsulation is another mechanism used by OOP to help us deal with complexity.

    • OOP allows us to hide implementation specific logic in a class so that it’s available only within the class. It allows us to provide a public interface to the class and hide everything else, then the user of the class can’t mess with the implementation specific code since they never know about it in the first place. This makes the code easier to maintain, debug and extend.

    • Limitations: if we have a small program that won’t be around for any significant amount of time (e.g., an internal program that use to automate something), then OOP might be overkill. A simple procedural or scripting program may be perfectly appropriate. Generally:

      • OOP cannot make bad code better, it will likely make it worse
      • OOP is not suitable for every application
      • not everything decomposes into a class
      • usually takes more upfront design time in order to write OOP
      • OOP makes program larger, slower and more complex
  • Constructors initialization list

    • If we initialize our data member values in the constructor body by assigning values to them, then it technically isn’t initialization because by the time the constructor body is executed these member attributes have already been created. So we’re really just assigning values to already created attributes.

    • What we really want to do is have the member data values initialized to our values before the constructor body executes. This is much more efficient since it’s true initialization. C++ allows us to do this using constructor initialization lists which is basically just the list of initializers immediately following the constructor parameter list. This will initialize the data members as the object is created:

        ClassA::ClassA() {
            attribute1 = value1;
            attribute2 = value2;
        }
      		
        // a better way using initialization list
        // this happens before the body of the constructor is ever executed
        ClassA::ClassA()
            : attribute1 {value1}, attribute2 {value2} {
        }
      

      Just keep in mind that the order in which the members are initialized is not necessarily the order we provide in the initialization list. The data members will be initialized in the order that they were declared in the class declaration.

  • Copy constructor

    When objects are copied, C++ must be able to create a new object from an existing object. It uses copy constructor to achieve this. There are several use cases where a copy constructor is used:

    • passing an object by value to a function as a parameter then we need to make a copy of that object by default

    • returning an object by value from a function

    • constructing a new object based on an existing object of the same class

    C++ must provide a way to accomplish this copying so it provides a compiler-defined copy constructor if we have not defined our own:

    • it copies the values of each class attribute to the new object (default memberwise copy)

    Note that if we are using raw pointers, then only the pointer itself will be copied but not the data that it’s pointing to. This is referred to as a shallow copy rather than a deep copy.

    Best practices with copy constructors:

    • always provide a user-defined copy constructor if our class has raw pointer members

    • always implement the copy constructor with the const reference parameter

    • use STL classes as member attributes as they already provide copy constructors

    • avoid using raw pointer data members if possible or use smart pointers

    Implementing the copy constructor (shallow copy):

      <class_name>::<class_name>(const <class_name> & src)
          : attribute1 {src.attribute1}, attribute2 {src.attribute2} {
      }
    
    • const reference is the key part which suggests that we will not modify the source object

    • note that we are still using constructor initialization list here

  • Shallow copying v.s. Deep copying

    shallow copy (default) deep copy
    memberwise copy (each data member is copied from the source object) create a copy of pointed-to data
    only the pointer itself is copied, NOT what it points to (they both point to the same area in the heap) each copy will have a pointer to unique memory area in the heap
    Problem: when we relase the memory allocated on the heap when destructor is called, the ohter object (the copy one) still refers to the released area (dangling pointer)! when we have a raw pointer as a class member, it is better to use deep copy
      class Shallow {
      private:
          int* data;
      public:
          Shallow (int d) {
              this->data = new int(d);
          }
    	    
          ~Shallow() {
          	delete this->data;
          }
    	    
          // same semantics as the default compiler generated copy constructor
          Shallow (const Shallow& src) 
          	: data(src.data) {    // simply copy the pointer itself
          }
      };
    	
      int main() {
          Shallow obj1 {10};
          foo(obj1);
          // obj1's data has been released!
          // accessing that invalid area from obj1 could crash the program
          return 0;
      } // also, when the destructor for obj1 eventually gets called, it will try to release the memory that's no longer valid and probably crash too
    

    Problem: src and the newly created object BOTH point to the SAME data area!

    • suppose we have a function that expects a shallow object by copy

    • we make the copy with the copy constructor but when that local object goes out of scope its destructor is called, and the destructor releases the memory in the heap that is pointing to

    • the object that was copied into this function still points to this area but it is no longer valid now

      class Deep {
      private:
          int* data;
      public:
          Deep (int d) {
              this->data = new int(d);
          }
    	    
          ~Deep() {
          	delete this->data;
          }
    	    
          // user-defined deep copy constructor
          Deep (const Deep& src)
          	: Deep{*src.data} { // allocate memory, not just copy pointer itself
          }
      };
    	
      // now a same main function as before will not crash
    

    No problem now: each object of Deep has its data pointer member point to an unique block of memory in heap with the same value *data as that of src.

  • R-value reference

    Sometimes when we execute code, the complier will create unnamed temporary values (unaddressable), and so called r-values; by contrast, addressable variables are called l-values:

      int total {0};    // total is l-value, addressable
      total = 100 + 200;    // 100+200 is stored in an unnamed temp value (r-value), the r-value is then assigned to the l-value total
    

    Normally, references are referred to l-value references (references for l-values), while r-value references are references to r-values (use && to declare).

      int x {100};        // l-value
      int &l_ref = x;     // l-value reference
      l_ref = 10;         // change x to 10
    	
      int &&r_ref = 200;  // r-value reference to r-value 200
      r_ref = 20;         // change r_ref to 20 (change that temp value)
    	
      int &&x_ref = x;    // complier error, assign l-value to r-value reference
    	
      void foo1(int& a);  // a function expects an l-value reference
      foo1(x);            // OK, since x is an l-value
      foo1(200);          // ERROR, since 200 is an r-value
    	
      void foo2(int&& a); // a function expects an r-value reference
      foo2(200);          // OK, since 200 is an r-value
      foo2(x);            // ERROR, since x is an l-value
    

    Note that we can overload above functions and have both of them in our code. Since they have unique signatures and the compiler will call the correct function depending on whether the parameter is an l-value or r-value.

  • Move constructor

    With objects, there can be a great amount of overhead of calling copy constructors over and over again to make copies of these temporary objects. If we have raw pointers, then we have to do deep copies and the overhead is even greater. C++11 introduces move semantics and move constructor, which moves an object rather than copies it. This can be extremely efficient.

    • move semantics is all about r-value references (references to those temporary variables or objects)

    • r-value references are used by move constructor and move assignment operator to efficiently move an object rather than copy it

    • move constructors are optional, if we don’t provide them then the copy constructor will be called automatically

    • it simply copies the address of the resource from source to the current object (default copy constructor behavior) and nulls out the pointer in the source pointer

    Implementing the move constructor:

      <class_name>::<class_name>(<class_name>&& src)
          : attribute {src.attribute} {
          src.attribute = nullptr;    // without this step, it is much like a shallow copy, not a move
      }
    
    • there’s no const qualifier for the parameter src because we need to modify it in order to null out its pointer

    • the parameter is an r-value reference

    • move constructor actually “steals” the data and nulls out the raw pointer in source object

      class Move {
      private:
          int* data;
      public:
          Move (int d) {
              this->data = new int(d);
          }
    	    
          ~Move() {
          	delete this->data;
          }
    	    
          // user-defined deep copy constructor
          Move (const Move& src)
          	: Move{*src.data} {
          }
    	    
          // user-defined move constructor
          Move(Move&& src) noexcept
          	: data(src.data) {
              src.data = nullptr;
          }
      };
    	
      int main() {
          vector<Move> vec;
          vec.push_back(Move(1));    // move the object instead of coping
      }
    
  • Static class members

    • non-const static data member must be initialized out of the class with a type specifier

    • static method has only access to static data members

    • static method has a class scope and it does not have access to the this pointer of the class

 


- End -