2 Basic 4

8 minute read

Published:


Member Functions

Adding functions in a struct

struct Pt {
	double x;
	double y;
	
	void print() {
		std::cout << this->x << ",", y << std::endl
	}
};

Refer object members

  • Explicit: y
  • Implicit: this->x (this is implicit pointer)

Refer member function

int main() {
	struct Pt p1 = { 1.0, 2.0 };     // you can drop struct in cpp
	p1.print();
	(&p1)->print();
}

Class

Replace struct with class

  • class: private by default
  • struct: public by default
class Pt {
public: 
	double x;
	double y;
	void print();
};

Early cpp compiler translates member functions as

void Pt_print(struct Pt *this);

int main() {
	Pt_print(&p1);
}

Stack Allocation

Constructor

Same name as the class

class Pt {
public:
	double x; double y;
	Pt() {
		x = 4.0; y = 5.0;
	}
};

int main() {
	Pt p1;      // constructor automatically invoked
}

When declared, allocate space and call the constructor

Can overload with different parameters and default parameters

	Pt();
	Pt(double _x);
	Pt(double _x = 4.0);

Construction syntax

Pt p1(6, 7);     // old cpp style
Pt p1();         // does NOT compile, taken as a FUNCTION PROTOTYPE
Pt p1 = {6, 7};  // C style
Pt p1 {6, 7};    // modern cpp
Pt p1;           // implied no argument

Destructor

Use ~class_name (negation)

	~Pt() {
		std::cout << "bye" << std::endl;
	}
  • Called when the object goes out of scope
    • If enclose object in {}, will go away early
    • Always called when you leave the scope (exception)
  • Constructor: malloc(), fopen()
  • Destructor: free(), fclose()

Heap Allocation

In cpp, void * cannot be assigned as a regular pointer, unless you cast it

	Pt *p2 = (Pt *)malloc(sizeof(Pt));
	p2->print();
	free(p2);

Constructor and Destructor did not involve, since malloc() and free() are for void*

new() and delete()

Wrapper around malloc() and free()

	Pt* p2 = new Pt {6, 7};
	p2->print();
	delete p2;
  • No delete in Java

Array Allocation

Pt* p3 = new Pt[5] {{6, 7}, {8, 9}, {10}};

delete[] p3;
  1. Allocate for an array of 5 (80 bytes)
    • On the heap, an 8-byte integer pads in front to hold 5
    • Can access through ((int_64_t *)p3) - 1
  2. Each constructor is called
  3. At delete[], each destructor called
    • Passing p3 (pointer to the first element)
    • If not delete[], will leak 88 bytes!

Why doesn’t cpp do this when allocating a single object? Efficiency.


Value vs Reference

Passing by Value

void transpose(Pt p) { // no hi
	double t = p.x;
	// something
	p.print();         // (5, 4)
}                      // bye

int main() {
	Pt p4 = {4, 5};
	p4.print();        // (4, 5)
	transpose(p4);     // passing by value
	p4.print();        // (4, 5), unchanged
}

When you pass by value, p is copied

  • Our own constructor not called. An internal Copy Constructor called!!!
  • After transpose() done, its destructor called

Passing by Reference in C

void transpose(Pt* p) {
	double t = p->x;
	// something
}

int main() {
	Pt p4 = {4, 5};
	p4.print();        // (4, 5)
	transpose(&p4);    // passing by pointer
	p4.print();        // (5, 4)!
}
  • No copying

When passing Pt *, technically it’s still passing an address value, simulating reference

Passing by Reference in Cpp

CPP supports this natively. Change parameter from Pt to Pt&

void transpose(Pt& p) {   // type Pt&
	double t = p.x;
}

Can use reference as if using the value.

  • Automatic dereferencing
  • Think p as a reference (alias) for p4
  • Pointer, but without deref

When seeing transpose(p4) in cpp, less readable. Can’t tell if value/reference

    Pt p1 {100. 5}, p2 {200, 5}; 

    Pt* p;       // we declare a pointer variable, uninitialized
    p = &p1;     // p points to p1
    p = &p2;     // p now points to p2

    Pt& r = p1;  // we create a reference to p1 named r
    // Pt& r;    // error: references must be initialized when declared
    r = p2;      // this does NOT change r to refer to p2
                 // instead, it is equivalent to p1 = p2;

    p1.print();  // will print "(200,5)"
  • You have to bind a reference to an object at creation
  • aka stuck pointer
  • In reassignment, will reassign the referenced objects
    • For all purposes of r, it means p1
    • but avoids making a separate object
  • Always stack-allocated

Copy Constructor

Regular constructor that takes another object reference ONLY CALLED AT INITIALIZATION

	Pt(Pt& orig) {    // copy constructor
		x = orig.x;
		y = orig.y;
		std::cout << "copy" << std::endl;
	}

When copying by value, copy constructor invoked

  • If not defined, compiler will generate one for you that mimics the C behavior

cpp invented reference for copy constructor If you call Pt(Pt other), the copy constructor has to be recursively called…

const

If you take something by const reference, the value won’t be changed

	Pt(const Pt& orig);

If you declare const on p4, but:

	void print();
	const Pt p4;    // p4 won't be modified
	p4.print();     // WON'T COMPILE

this.print() will execute Pt* this = &p4;, but const Pt* &p4 can’t be converted to regular Pt* this!

  • print() suggests possible mutation

Need to declare as const member function!!!

	void print() const;
  • implies this as a const Pt*
  • If member function not modifying objects, always declare as const.

Direct Invocation

Directly make a copy

	{
		// copy construction semantics
		Pt p5 {p4};    
		Pt p5(p4);
		Pt p5 = {p4}; // C
		Pt p5 = p4;   // C init
		
		// COPY ASSIGNMENT!
		Pt p5; p5 = p4;
	}

Pass values of p4 to p5

Return by Value

Compile of -fno-elide-constructors -std=c++14
Also invokes copy constructor
When expand() returns by value, it creates a temporary unnamed object, which is again copy constructed to be assigned

Pt expand(Pt p) {    // copy construction by value
    Pt q;            // construction
    q.x = p.x * 2;
    q.y = p.y * 2;
    return q;        // copy construction: return value
                     // q destroyed
}					 // p destroyed

int main() {
    const Pt p4;
    cout << "*** p6: returning object by value" << endl;
    
    {
        Pt p6 
        {      
	        // tmp created here // copy of q when expand() returns
	        expand(p4) 
	    };  // copy construction on p6
	        // tmp destroyed
    }     // p6 destroyed
    
	cout << "Thats all folks!" << endl;
}         // p4 destroyed
g++ -fno-elide-constructors -std=c++14 # ...

hi                                 # p4's constructor
*** p6: returning object by value
copy                               # call expand()
hi                                 # q's constructor
copy                               # copy of tmp
bye                                # q's destructor
copy                               # p6 copy from tmp
bye                                # tmp or p
bye                                # tmp or p
bye                                # p6 destroyed
Thats all folks!
bye                                # p4's destructor

3 copy calls:

  1. Passing p4 to expand(), copying to p
  2. expand() return q, creating a tmp object (different from q)
    • main() allocates space for such tmp.
  3. Copy of tmp to p6

5 destructor calls

  1. q destroyed after expand() returns
  2. tmp destroyed after p6 constructed
  3. p destroyed after expand() returns
  4. p6 destroyed
  5. p4 destroyed

Now compile without -no-elide-constructors

hi                                 # p4's constructor
*** p6: returning object by value
copy                               # p construct
hi                                 # q construct
bye
bye
Thats all folks!
bye                                # p4's destructor

p6 and tmp not constructed

  • When returning object, it is typically constructed in the stack frame of the caller main()
  • When compiler is allowed to elide, this temporary (just used to copy into another object), is omitted
  • p6, tmp, q get collapsed to one

By cpp 17, elide is forced


Copy Assignment

Assign one struct to another, so each member is assigned

int main() {
    const Pt p4;

    cout << "*** p7: copy assignment" << endl;

    {
        Pt p7 {8,9};
        p7.print();
        p7 = p4;
        p7.print();
    }

    cout << "That's all folks!" << endl;
}
hi                       // p4's constructor
*** p7: copy assignment
hi                       // p7's constructor                 
(8,9)                    // p7.print() before the assignment
(4,5)                    // p7.print() after the assignment
bye                      // p7's destructor
That's all folks!
bye                      // p4's destructor

Same behavior as C

You can use operator=() to define the copy assignment operator

  • Invoked from b = a;
  • Assignment is an expression with a value
    Pt& operator=(const Pt& rhs) {
      x = rhs.x;                // (this.)x = rhs.x
      y = rhs.y;
      std::cout << "op()" << std::endl;
      return *this;             
    }
    
  • LHS comes as this this object
  • RHS comes as parameter rhs
  • It you return by value Pt, it will make another copy

Compiler Default

  • Constructor: generated if no constructor or copy constructor. Invoke member constructors
  • Destructor: invoke member destructor
  • Copy constructor: generated if not present. Will copy by member (invoke their CC)
  • Copy assignment: same