The Big Ideas of CISC-2000-L11: a Midterm Review

Definitions, curly braces, and lifespans

  1. A definition creates a variable and gives it a name.
    	int i {10};   //curly braces, semicolon.
    
    	int j = 20;   //how they used to define a variable before 2011
    
  2. A variable created with a definition inside of a pair of {curly braces} stays alive as long as the computer remains withing the curly braces.
    The variable dies when the computer reaches the closing curly brace.
    1. Example: a pair of curly braces surrounding the definition of a function.
      Please write these curly braces in column 1 to make them easy to find.
      void f()   //the definition of a function
      {
      	int i {10};
      	cout << "Hello\n";
      
      }	//i dies here.
      
    2. Another example: a pair of curly braces surrounding the body of a loop.
      The following code creates a series of variables, all with the name j.
      Each j hold a different value, and each j dies before the next j is born.
      The i dies when we escape from the loop.
      	int a[] {
      		 0,
      		10,
      		20,
      		30,
      		40
      	};
      
      	const size_t n {size(a)};   //the number of array elements
      
      	for (int i {0}; i < n; ++i) {
      		int j {a[i] + 1};
      		cout << j << "\n";
      	}
      
    3. A variable defined outside of all pairs of curly braces (e.g., up above the main function) stays alive until the end of the program.
  3. Create a series of variables in a given order by writing a series of definitions.
    void f()
    {
    	int i {10};
    	int j {20};
    	int k {30};
    	cout << "Hello\n";
    
    }	//Die here in the order k, j, i
    
    In the above example, the birth and death of j happen in between the birth and death of i.
    We therefore say that the birth and death of j are nested inside of the birth and death of i.
    Similarly, the birth and death of k are nested inside of the birth and death of j.

    An analogy from presidential biography: JFK (1917–1963) was born after, and died before, LBJ (1908–1973).
    JFK’s lifespan was therefore nested inside of LBJ’s lifespan.
    Ditto for Mozart (1756–1791) and Haydn (1732–1809).

  4. Here’s why it’s important to know the exact point of a variable’s birth and death.
    1. If the variable is an object belonging to a class that has a constructor, a constructor is called when the variable is born.
    2. If the variable is an object belonging to a class that has a destructor, the constructor is called when the variable dies.

Write integers in decimal, binary, and hexadecimal notation

  1. Each hexadecimal digit is an abbreviation for a series of four binary digits (four bits).

    decimal binary hexadecimal
    0 0 0
    1 1 1
    2 10 2
    3 11 3
    4 100 4
    5 101 5
    6 110 6
    7 111 7
    8 1000 8
    9 1001 9
    10 1010 A
    11 1011 B
    12 1100 C
    13 1101 D
    14 1110 E
    15 1111 F
    16 10000 10
    17 10001 11

Pointers, dereferencing, and pointer arithmetic

  1. We can store the address of a variable into another variable, called a pointer.
    	int i {10};
    	int *p {&i};   //Store the address of i into p.
    	               //p is a "pointer" that "points to" i.
    
    	cout << *p << \n";   //Use an asterisk to "dereference" p:
    	                     //output the value of i.
    
  2. A pointer can be incremented or decremented only if it points to an element in an array.
    	int a[] {     //an array of 5 ints
    		 0,   //p will point at this zero.
    		10,
    		20,
    		30,
    		40
    	};
    
    	int *p {a};   //Store the address of a[0] into p.
    	              //a means &a[0]
    	              //p "points to" a[0]
    
    	++p;          //Now p points to a[1]
    	++p;          //Now p points to a[2]
    	--p;          //Now p points back to a[1]
    
    This technology gives us a faster way to loop through an array:
    	int a[] {   //an array of 5 ints
    		 0,
    		10,
    		20,
    		30,
    		40
    	};
    	const size_t n {size(a)};   //the number of elements in the array
    
    	//The [square brackets] do a hidden multiplication and addition:
    	for (int i {0}; i < n; ++i) {
    		cout i << " " << a[i] << "\n";
    	}
    
    	for (int *p {a}; p < a + n; ++p) { //a means &a[0]; a+n means &a[n]
    		cout << *p << "\n";
    	}
    
  3. A pointer can be dereferenced with [square brackets] instead of with * only if it points to an element in an array.
    	int a[] {      //an array of 5 ints
    		 0,
    		10,
    		20,    //p will point at this 20
    		30,
    		40
    	};
    
    	int *p {a+2};  //point at the 20; a+2 means &a[2]
    
    	//Output the 20 and its left and right neighbors.
    	//Looks like an array whose subscripts go from -2 to +2 inclusive.
    
    	cout << p[-2] << "\n";   //output the 0
    	cout << p[-1] << "\n";   //output the 10
    	cout << p[0]  << "\n";   //output the 20; p[0] means *p
    	cout << p[1]  << "\n";   //output the 30
    	cout << p[2]  << "\n";   //output the 40
    
    #include <string>
    using namespace std;
    
    	const string a[] {
    		"Staten Island",
    		"Brooklyn",
    		"Queens",
    		"Manhattan",
    		"Bronx"
    	};
    
    	const size_t n {size(a)};   //the number of burroughs
    
    	for (const string *p {a}; p < a + n - 1; ++p) {  //a means &a[0]; a+n-1 means &a[n-1]
    		cout << p[0] << " and " << p[1]  << " are connected by a bridge.\n";
    	}
    
    Staten Island and Brooklyn are connected by a bridge.
    Brooklyn and Queens are connected by a bridge.
    Queens and Manhattan are connected by a bridge.
    Manhattan and Bronx are connected by a bridge.
    
  4. A pointer can be subtracted from another pointer only if they point to elements of the same array:
    	int a[] {     //an array of 5 ints
    		 0,
    		10,   //p will point at this 10
    		20,
    		30,
    		40    //q will point at this 40
    	};
    
    	int *p {a+1}; //point to the 10 (a+1 means &a[1])
    	int *q {a+4}; //point to the 40 (a+4 means &a[4])
    
    	cout << "p and q are pointing at elements that are " << q - p
    		<< " elements apart.\n";
    
    p and q are pointing at elements that are 3 elements apart.
    
  5. A pointer can be dereferenced with -> only if it points to a struct (or to an object).
    	struct point {
    		double x;
    		double y;
    	};
    
    	point origin {0.0, 0.0};
    	point *p {&origin};     //p points to the origin
    
    	cout << p->x << "\n";   //output origin.x
    	cout << p->y << "\n";   //output origin.y
    
    	//How we would have to output origin.x using p if they hadn't invented ->
    	cout << (*p).x << "\n"; //output origin.x
    
  6. The value of a pointer (or any integer) can be written in binary. For example,
    11011110 10101101 10111110 11101111 (blanks inserted for legibility)
    but it’s easier to read (and only one quarter as long) in hexadecimal:
    DEADBEEF
  7. You can insert the keyword const at the start of a declaration, or immediately after any asterisk.
    That means there are two ways to make a pointer const.
    We could even do both of them simultaneously.
    	int i {10};
    	int *p1 {&i};       //p1 points to i.  A plain old (non-constant) pointer
    
    	int *const p2 {&i}; //p2 points to i
    	//++p2;             //The const prevents this.
    	                    //The const keeps p2 pointing to the same variable.
    
    	const int *p3 {&i}; //p3 points to i
    	//++*p3;            //The const prevents this.
    	                    //The const prevents us from using p3 to change the value of i.
    
    	const int *const p4 {&i};  //p4 points to i
    	//++p4;             //The righthand const prevents this.
    	//++*p4             //The lefthand const prevents this.
    

Why use pointers?

  1. We saw two reasons above to use pointers: to loop faster (without the hidden multiplication and addition), and to access two or more consecutive (or nearby) elements of an array.
  2. Make it obvious that a function has the power to change the value of its argument.
    void f(int& a, int *p)   //function definition
    
    int main()
    {
    	int i {10};
    	int j {20};
    
    	f(i, &j);   //Suppose this was the only statement you glanced at.
    }
    
    void f(int& a, int *p)   //function definition
    {
    	++a;   //Change the value of i.
    	++*p;  //Change the value of j.
    }
    
  3. Pass an array (or part of an array) to a function: array.C, array.txt.
  4. A function (in this case, localtime) can return a pointer to a structure in order to return more than one result:
    #include <chrono>   //for class system_clock
    #include <ctime>    //for the function localtime and structure tm
    using namespace std;
    
    	const auto now {chrono::system_clock::now()};
    	const time_t t {chrono::system_clock::to_time_t(now)};
            const tm *const p {localtime(&t)};
    
    	cout << p->tm_year + 1900 << "\n";
    	cout << p->tm_mon + 1 << "\n";
    	cout << p->tm_mday << "\n";
    

Dynamically allocated blocks of memory

  1. Why else do we need pointers? Isn’t it simpler to access a variable by name?
    	int i {10};
    	cout << i << "\n";  //simple way to output the value of i
    
    	int *p {&i};        //p points to i
    	cout << *p << "\n"; //complicated way to output the value of i
    
    Well, not every variable has a name. The only variables that have names are the variables created by definitions. But in three cases variables cannot be created by definitions.
    1. We can create an array with a definition only if we know “at compile time” (i.e., when we are writing the program) how many elements the array will have.
      If we don’t know this, we can’t create the array with a definition.

    2. We can create a variable with a definition only if we are content to have the variable die at the closing curly brace of the pair of braces that enclose the definition.
      If we want the variable to die at a later (or earlier) point, we can’t create the variable with a definition.

    3. We can create a series of variables with a series of definitions only if we are content to have the variables die all together, in the opposite order. If we want the variables to die in some other order, we can’t create them with a series of definitions.
  2. Instead of creating a variable with a definition, we can put the variable in a block of dynamically allocated memory. See here.
    If the operating system can satisfy our request for a block of memory, the operating system gives us a pointer that points to the block.
  3. If the operating system cannot satisfy our request for a block of memory, the new operator can throw an exception to carry the bad news to another part of the program.
    We saw how to catch that exception here.
  4. To hold a series of ints, is there any reason not to use an object of class vector<int> instead of a dynamically allocated block of memory?
    You can apply the operators * and ++ to an “iterator” such as it just as you apply them to a pointer.
    #include <iostream>
    #include <iomanip>  //for the i/o manipulator setw
    #include <vector>   //for class vector
    using namespace std;
    
    	vector<int> v {   //You can make a vector just like you make an array.
    		 0,
    		10,
    		20,
    		30,
    		40
    	};
    
    	v.push_back(50);
    	v.push_back(60);
    
    	vector<int>::size_type n {size(v)};
    
    	for (int i {0}; i < n; ++i) {
    		cout << i << "   " << setw(2) << v[i] << "\n";
    	}
    
    	for (auto it {begin(v)}; it != end(v); ++it) {
    		cout << setw(2) << *it << "\n";
    	}
    

Classes and objects

  1. An object containing data members is like a structure containing fields, but with better security in two ways:
    1. If the class has a constructor member function, then the constructor will be unavoidably called whenever an object of this class is created.
      The constructor can make sure that a valid value has been installed into every data member of the object being born.

    2. If garbage creeps into the private data members of an object later, then there is a well-defined list of functions that could be guilty: the functions declared in the curly braces of the class declaration (in other words, the member functions and friend functions of the class).
      No other functions in the program can even mantion the name of any private member of the class.
  2. Calling a member function of an object is like calling a function with an argument that is a pointer to a structure.
    struct mystruct {
    	double x;
    	double y;
    };
    
    void f(mystruct *p);  //function declaration
    
    int main()
    {
    	mystruct origin {0.0, 0.0};
    	f(&origin);   //Pass the address of origin to f.
    }
    
    void f(mystruct *p)   //function definition
    {
    	cout << p->x << "\n";
    }
    
    class myclass {
    private:
    	double x;
    	double y;
    public:
    	myclass(double init_x, double init_y); //constructor declaration
    	void f();                              //member function declaration
    };
    
    int main()
    {
    	myclass origin {0.0, 0.0};
    	origin.f();   //Invisibly pass the address of origin to f.
    }
    
    myclass:myclass(double init_x, double init_y) //constructor definition
    	: x {init_x}, y {init_y}
    {
    }
    
    void myclass::f()                             //member function definition
    {
    	cout << x << "\n";
    }
    
  3. Dichotomies involving a class of objects:
    1. member functions vs. data members

    2. public members vs. private members.
      For the time being, let your member functions be public and your data members private.

    3. const member functions vs.
      non-const member functions

    4. member functions vs. friend functions.
      A member function concentrates on just one object.
      A friend function deals evenhandedly with two or more objects of the same class.
      Exceptions due to the synatax of the language: operator<<, operator>>.

    5. plain old data members vs. static data members.
      Remember our first exampple of a static data member: the array date::length.

    6. constructors vs. destructor.
      What they can be used for: open/close, lock/unlock, etc.
      Not every class needs a destructor (class date vs. class ofstream).

    7. header (.h) file vs. implementation (.C) file for a class

Operator Overloading

If an operator function needs to mention the privatre members of a class, it will have to be a member function or a friend of the class.
(This is true of any function.)

The usual rules are:

One exception to the above rule:
If an operator function is a member function, it is always a member function of the object to its right (i.e., its right operand).

	date d;
	d += 10;     //means d.operator+=(10);  The operator+ is a member function of the d.
	d -= 10;     //means d.operator-=(10);  The operator- is a member function of the d.

	cout << d;   //If this operator<< was a member function,
	             //it would be a member function of the cout.

	cin >> d;    //If this operator>> was a member function,
	             //it would be a member function of the cin.

Therefore if the above operator<< needs to mention the private members of class date, the operator<< will have to be a friend function of class date.
Ditto for operator>>