Notes for C++ (2)

Memory

Stack Memory

Stack memory is used for function calls.

Stack memory starts near top of memory (e.g. 0xffff00f0, starts from ffff something, and not 0000 something) and moves down, automatically managed by systems with function calls.

The memory only lasts as long as the function goes.

#include Square.h
using namespace test123::Square;

Square *CreateSquare() { // * before function name means it returns a pointer
    Square s(5);
    return &s;
}

int main(){
    Square *s = CreateSquare(); // doesn't really work
    // the function returns the address of created square instance
    // but when *s receives the value, the square instance is already wiped
    // because the function CreateSquare ends
    // the address left, but the content could be changed
    
    double area = s->getArea(); // it's not working as we expected
}

Heap Memory

Heap memory: starts from the bottom and goes up, runtime allocated memory. We can use heap memory where lifecycle of variables exceeds lifecycle of functions. The keyword new creates heap memory.

Using new will:

  • allocate memory
  • construct allocated object
  • returns a pointer of type to allocated memory

Heap memory is not automatically reclaimed, even if it goes out of scope. Any memory lost but not freed, is considered "leaked memory". The only way to free heap memory is to use keyword delete, it:

  • destructs object pointed to by pointer
  • deallocate memory

Notes:

  • It only delete the memory in heap (what the pointer points), but not the pointer returned by new (which is probably still stored in another variable outside of the function call in stack)
  • It can only delete objects created by new
  • Deleting the thing in one address (in heap memory) twice will trigger error (can not delete what's already deleted)
  • Deleting things on address in stack memory will trigger error (trying to delete memory not managed by heap memory)
  • Deleting/accessing things not belong to this program (or the program doesn't own) will trigger error (segmentation fault)
  • When newing an array, the address of the first item in array is returned. When deleting an array, we should use delete[]
int *arr;
int size = 5;

arr = new int[size]; // arr stores address of the head of the int array
// allocate array and construct each object

for(int i=0; i<size; i++){
    arr[i] = i+1;
}

delete[] arr; // destruct each object and then release memory

//delete arr would only delete the first item

Other Notes:

int *p, *q, *r; // 3 pointers
// when accessing with * dereference operator, the content will be ints

//WARNING
int* p, q, r; // this creates a pointer p, and an int q, an int r
// not 3 pointers

// * is on variable name, and not type

Function Parameter

In C and C++, by default function parameters are passing values.

When a function starts, a copy of all parameters (they could be copies of objects or copies of pointers) are created in stack, and when the function ends, they are wiped.

// if I have square s1, s2
// Square *s1 = new Square(1);
// Square *s2 = new Square(2);

int SomeFunction1(Square param1, Square param2) {...}
int a = SomeFunction1(*s1, *s2);
// this passes copies of the squares (values)
// when changing param1 or param2, (*s1) and (*s2) are not affected

int SomeFunction2(Square *param1, Square *param2) {...}
int b = SomeFunction2(s1, s2);
// this passes copies of s1 and s2's addresses
// changing (*param1) would change the squares outside
// when address is smaller than object, this is faster than the first function

int SomeFunction3(Square &param1, Square &param2) {...}
int c = SomeFunction3(*s1, *s2);
// this passes references of the squares
// param1 and param2 has direct access to squares outside
// it's faster than function1, but the params must exists when function is called

int SomeFunction4(const Square &param1, const Square &param2){...}
int d = SomeFunction4(*s1, *s2);
// this passes references, but won't allow any modification to the originals
// but with const, param1.method(), method need to be defined const:
// e.g. in header file: int getArea() const;
// and in cpp: Square::getArea() const {...}

Return is supposed to makes a copy (if you store the returned value of the called function in a variable, it's the copy, not the same one returned in the function) of the final value and wipes everything in stack for calling that function. But in reality, the compiler by default, when realizing a variable's going to be initialized by a function, instead of making a copy of returned result, it directly creates the result where the variable outside (waiting to be initialized) is. Only when explicitly told to not avoid copies, the compiler will do the copy on return.

Class Lifecycle

Note:

A copy constructor is a constructor that tells compiler how to copy.

Square s1 = Square(1);
Square s2 = s1; // this copies s1
Square s3 = CombineSquares(s1,s2);// this copies s1, s2
// by default return's not copied
Square::Square(const Square &other); // this is a copy constructor

When a class has a reference variable, it needs to be initiated before the instance is created (when writing a copy constructor), or it will trigger error.

Combined::Combined(const Combine &other) : pointer_ (other.pointer), ref_ (other.ref_) { // this means initiate pointer_ and ref_ before calling constructor to create new instance
	square_ = other._square;
}

Destructor

Purpose: free resources (e.g. memory, network connections, files, etc.) maintained by class.

Automatic Destructor:

  • Exists only when no custom destructor exists
  • Invoked automatically when an object leaves scope, or when delete is called
  • Destroy any local classes by calling their destructor
// what a destructor looks like

// in header file
~Square();

// in cpp
Square::~Square(){...}

Overload Operator

// in header file
// overload +
Square operator+(const Square &ref) const;
// not going to change the param
// not going to change the squre itself

// s3 = s2.operator+(s1) ---- the same as: s3 = s2 + s1

// in cpp
Square::Square::operator+(const Square &ref){
	return joinSquare(*this, ref);
}

The rule of three:

If it is necessary to define any one of these three functions in a class, it will be necessary to define all three of these functions: 1. Assignment Operator 2.Copy Constructor 3. Destructor

The rule of zero: Either manage memory all by yourself (define all 3), or define none (let system manage it all).

An assignment operator clears the current value and then copies the value.

Square & Square::operator=(const Square &other){
	// assignment operator returns a reference
    
    if(this != &other){// if address is different
        // in case someone writes s1 = s1 and deletes itself
        _destroy();
        _copy(other);
    }
    
    return *this;
}

Template

Kind of like generic class in Java.

Used by compiler at compile time to generate code.

template <typename T>
inline T const& Max (T const& a, T const& b) { 
    return a < b ? b:a; 
} 

// line 1 is decorator to line 2, saying it's a template

// keyword inline means to not call the function, but copy the function to where it's called instead
// inline trades space for time



// template can be used like:
int a = 1;
int b = 2;
cout << "Max: " << Max<int>(a,b) << endl;

//or just
double c = 3.0;
double d = 2.9998;
cout << "Max: " << Max(c,d) << endl;

Using template to implement list:

// List.h -- interface
#pragma once

template <typename T>
class List {
    public:
        void insert(const T &t);
        void remove();
        T get();
        bool isEmpty();
        List();
    private:
        T * data_;
}

#include "List.hpp"



// ----------------------------------------------
// List.hpp --- template

template <typename T>
T List::get() {
    return data_[0];
}

Iterator

Iterator is an interface.

Every class that implements an iterator needs:

  1. In the Class, have functions:
  • begin(): returns iterator to start
  • end(): returns iterator past the end

2. Implement Class's iterator:

  • Must has base class std::iterator
  • Must implement: operator* (dereference: return data iterator references by ref), operator++ (pre-increment: move iterator to next data item in class), operator!= (not equal: compare 2 iterators)

With these, we can access everything in data structure.

How Iterator's used

// example 1
for(std::vector<Animal>::iterator it = zoo.begin(); it != zoo.end(); it++) {
    std::cout << (*it).name << " " << (*it).food << std::endl;
}

// example 2 - use 'auto' to avoid typing long class name
// 'auto': let compiler determine what's the type by things on right
// 'auto' must be initiated 
for(auto it = zoo.begin(); it != zoo.end; it++) {
    std::cout << (*it).name << std::endl;
}


// example 3 - range based loop
for(const Aminal & animal : zoo) {
    // must be const reference for range based loop
    // so it's not suitable for wanting to modify data
    std::cout << animal.name << std::endl;
}

ref: https://courses.engr.illinois.edu/cs225/fa2020/