Course list http://www.c-jump.com/bcc/
Consider normalize() function that sets both sides of the Rectangle object to 1:
|
|
As written, the normalize() function
void normalize( Rectangle rect ) { rect.set_dimensions( 1, 1 ); }
has no effect on the rectangle declared in main().
By default, in C/C++ the parameters are always passed by value:
a copy of the original object is made before the function is called
the called function works with the copy of the object
the copy is destroyed
the function returns
the original object is never modified
To make normalize() work with the original rect instantiated by main(), we must pass the object by pointer:
void normalize( Rectangle* ptr_rect ) { (*ptr_rect).set_dimensions( 1, 1 ); }
Note that the pointer has to be dereferenced (*ptr_rect) before accessing the rectange in memory.
Alternatively, a member selector operator -> is typically used with pointers to structs, because it makes code more readable:
void normalize( Rectangle* ptr_rect ) { ptr_rect->set_dimensions( 1, 1 ); }
The main() function must pass the original object by address:
int main()
{
Rectangle rect( 333, 222 );
normalize( &rect );
std::cout << "rectangle width: " << rect.width << '\n';
std::cout << "rectangle height: " << rect.height << '\n';
std::cout << "rectangle area: " << rect.area() << '\n';
return 0;
}
Recall that the & is the C/C++ "address-of" operator.
Consider an "advanced" version of the rectange:
The rectangle now has to be named when it is created:
Rectangle rect( "first", 333, 222 );
Additionally, we would like to have set_name() member function that changes the name:
Rectangle rect2 = rect; // make a copy rect2.set_name( "second" );
Named graphical primitives are very useful: they can display their name as a label when shown on the screen or identify themselves during program debugging.
A new member variable ptr_name holds the pointer to a string of characters in memory:
struct Rectangle { int width; // member variable int height; // member variable char const* ptr_name; //... };//struct Rectangle
Recall that char* pointer stores the address of the first character of the string.
Similarly, char const* is the pointer to a constant character, telling the compiler to disallow any modifications to the characters via this pointer.
Thus, the exact type of ptr_name is a pointer to const char -- well suitable for a string of characters allocated elsewhere, outside of the scope of the Rectangle struct
Newly added data members usually require changes in existing constructors
Each constructor of the Rectangle struct must be updated to accommodate and initialize the ptr_name pointer:
struct Rectangle { int width; // member variable int height; // member variable char const* ptr_name; Rectangle() { width = 1; height = 1; ptr_name = "UNKNOWN"; } Rectangle( char const* ptr_name_, int width_ ) { width = width_; height = width_ ; ptr_name = ptr_name_; } Rectangle( char const* ptr_name_, int width_ , int height_ ) { width = width_; height = height_; ptr_name = ptr_name_; } //... };//struct Rectangle
Besides naming the rectangle when we create it, we also want to copy rectangles and rename them any way we want.
Thus, new member functions provide read/write access to the object's name:
struct Rectangle { int width; // member variable int height; // member variable char const* ptr_name; //... void set_name( char const* ptr_name_ ) { ptr_name = ptr_name_; } char const* get_name() const { return ptr_name; } //... };//struct Rectangle
Since get_name() makes no changes in the object's state, it is declared const.
The main() function uses named rectangles as follows:
int main() { Rectangle rect( "first", 333, 222 ); Rectangle rect2 = rect; // make a copy rect2.set_name( "second" ); std::cout << "rectangle width: " << rect.width << '\n'; std::cout << "rectangle height: " << rect.height << '\n'; std::cout << "rectangle area: " << rect.area() << '\n'; std::cout << "rectangle name: " << rect.get_name() << '\n' << '\n'; std::cout << "rectangle width: " << rect2.width << '\n'; std::cout << "rectangle height: " << rect2.height << '\n'; std::cout << "rectangle area: " << rect2.area() << '\n'; std::cout << "rectangle name: " << rect2.get_name() << '\n' << '\n'; return 0; }
The solution to named rectangles is very simple because our Rectangle struct does not allocate/deallocate any memory.
However, let's try a new requirement:
have the Rectangle allocate space to keep its own copy of the name string in memory.
In C++, this can be done with new and delete operators. In C, you use malloc() and free().
Note that in C++, dynamic memory is allocated on the "free store", in C - on the "heap". Why the difference? Because both compilers use a dynamic memory manager that does the work, and how it's done is different in C and C++. You should not mix calls to malloc() / free() with new / delete operators in the same C++ program.
Let's try using new and delete and see how much impact it has on the internal implementation of the Rectangle struct.
However, before making any changes, we must talk about C++ constructor initializer lists ( presentation ).
#include <cstring> struct Rectangle { int width; // member variable int height; // member variable char* ptr_name; Rectangle() : width( 1 ), height( 1 ), ptr_name( nullptr ) { set_name( "UNKNOWN" ); } Rectangle( char const* ptr_name_, int width_ ) : width( width_ ), height( width_ ), ptr_name( nullptr ) { set_name( ptr_name_ ); } Rectangle( char const* ptr_name_, int width_ , int height_ ) : width( width_ ), height( height_ ), ptr_name( nullptr ) { set_name( ptr_name_ ); } ~Rectangle() { if ( ptr_name ) delete[] ptr_name; ptr_name = nullptr; } void set_name( char const* ptr_name_ ) { if ( ptr_name ) delete[] ptr_name; int name_length = strlen( ptr_name_ ) + 1; ptr_name = new char[ name_length ]; strncpy( ptr_name, ptr_name_, name_length ); } //... };//struct Rectangle
A destructor is called for an object
when the object goes out of scope;
when the object containing it is destroyed.
struct Rectangle { int width; // member variable int height; // member variable char* ptr_name; //... Rectangle( char const* ptr_name_, int width_ , int height_ ) : width( width_ ), height( height_ ), ptr_name( nullptr ) { set_name( ptr_name_ ); } ~Rectangle() { if ( ptr_name ) delete[] ptr_name; ptr_name = nullptr; } void set_name( char const* ptr_name_ ) { if ( ptr_name ) delete[] ptr_name; int name_length = strlen( ptr_name_ ) + 1; ptr_name = new char[ name_length ]; strncpy( ptr_name, ptr_name_, name_length ); } //... };//struct Rectangle
Unfortunately, there is a huge problem in our solution: the way it's written, the same memory may be deleted twice:
int main() { Rectangle rect( "first", 333, 222 ); Rectangle rect2 = rect; // this makes a "shallow" copy // this deletes memory imporperly "owned" by both objects rect2.set_name( "second" ); //... return 0; // When destructor of rect is invoked, the memory that it // thinks it is managing is already deallocated! // depending on your circumstances, you may or not crash // here.. }
In a larger program that has a potential of using hundreds or even thousands of Rectangle struct instances in memory, the code will corrupt memory and mess up the C++ free store manager that allocates and deallocates our dynamic memory.
How do we fix the problem?
First part of the solution is to write a copy constructor to prevent Rectangle from making shallow copies:
// C++ copy constructor Rectangle( Rectangle const& another_rectangle_ ) : width( another_rectangle_.width ), height( another_rectangle_.height ), ptr_name( nullptr ) { set_name( another_rectangle_.ptr_name ); }
There is one more very serious problem in current Rectangle struct implementation:
the default version of the object assignment also makes a shallow copy. For example,
int main() { Rectangle rect( "first", 333, 222 ); Rectangle rect2 = rect; // make a deep copy rect2.set_name( "second" ); std::cout << "rectangle width: " << rect.width << '\n'; std::cout << "rectangle height: " << rect.height << '\n'; std::cout << "rectangle area: " << rect.area() << '\n'; std::cout << "rectangle name: " << rect.get_name() << '\n' << '\n'; rect = rect2; // shallow assignment! std::cout << "rectangle width: " << rect2.width << '\n'; std::cout << "rectangle height: " << rect2.height << '\n'; std::cout << "rectangle area: " << rect2.area() << '\n'; std::cout << "rectangle name: " << rect2.get_name() << '\n' << '\n'; return 0; }
And the program crashes are back!
How do we fix the problem with assignment?
We must provide our own version of the assignment operator to prevent Rectangle from making shallow assignment:
// C++ overloaded assignment operator: Rectangle& operator=( Rectangle const& another_rectangle_ ) { if ( this == &another_rectangle_ ) { // avoid assignment to self: return *this; } width = another_rectangle_.width; height = another_rectangle_.height; set_name( another_rectangle_.ptr_name ); return *this; }
A C++ reference is like a pointer, except:
Usage syntax is like an object
References always refer to something
References can't be reassigned to something else
This is enforced by:
Requiring initialization in case of variables
The implicit reference initialization in case of function parameters
No syntax exists to changing the referent (the object that a reference is pointing to)
Once instantiated, the reference becomes eternal synonym for an object or a variable
See C++ reference handout ( presentation ) for details.
|
|
Because this is a pointer to the current object, *this refers to the object itself.
Sometimes we want a member function to return a reference to the object it was called on:
struct Rectangle { int width; // member variable int height; // member variable char const* ptr_name; //... // C++ overloaded assignment operator: Rectangle& operator=( Rectangle const& another_rectangle_ ) { if ( this == &another_rectangle_ ) { // avoid assignment to self: return *this; } width = another_rectangle_.width; height = another_rectangle_.height; set_name( another_rectangle_.ptr_name ); return *this; } //... };//struct Rectangle
An object that manages dynamic memory should also provide a move constructor:
// move constructor
Rectangle( Rectangle&& another_rectangle_ )
{
width = another_rectangle_.width;
height = another_rectangle_.height;
ptr_name = another_rectangle_.ptr_name;
another_rectangle_.ptr_name = nullptr;
}
The move constructor lets objects "move" from one scope to another without a potentially significant overhead of copying memory from a local object that is about to be destroyed:
Rectangle create_rectangle() { Rectangle temp; temp.set_name( "FACTORY CREATED" ); return temp; }
Here is the final version of our struct that allocates/deallocates memory dynamically:
#include <cstdlib> #include <iostream> #include <cstring> struct Rectangle { int width; // member variable int height; // member variable char* ptr_name; Rectangle() : width( 1 ), height( 1 ), ptr_name( nullptr ) { std::cout << "DEBUG default constructor " << __FUNCTION__ << '\n'; set_name( "UNKNOWN" ); } Rectangle( char const* ptr_name_, int width_ ) : width( width_ ), height( width_ ), ptr_name( nullptr ) { set_name( ptr_name_ ); } Rectangle( char const* ptr_name_, int width_ , int height_ ) : width( width_ ), height( height_ ), ptr_name( nullptr ) { set_name( ptr_name_ ); } Rectangle( Rectangle const& another_rectangle_ ) : width( another_rectangle_.width ), height( another_rectangle_.height ), ptr_name( nullptr ) { std::cout << "DEBUG copy constructor " << __FUNCTION__ << '\n'; set_name( another_rectangle_.ptr_name ); } ~Rectangle() { if ( ptr_name ) delete[] ptr_name; ptr_name = nullptr; } // C++ overloaded assignment operator: Rectangle& operator=( Rectangle const& another_rectangle_ ) { std::cout << "DEBUG assignment " << __FUNCTION__ << '\n'; if ( this == &another_rectangle_ ) { // avoid assignment to self: return *this; } width = another_rectangle_.width; height = another_rectangle_.height; set_name( another_rectangle_.ptr_name ); return *this; } // move constructor Rectangle( Rectangle&& another_rectangle_ ) { std::cout << "DEBUG move constructor " << __FUNCTION__ << '\n'; width = another_rectangle_.width; height = another_rectangle_.height; ptr_name = another_rectangle_.ptr_name; another_rectangle_.ptr_name = nullptr; } void set_name( char const* ptr_name_ ) { if ( ptr_name ) delete[] ptr_name; int name_length = strlen( ptr_name_ ) + 1; ptr_name = new char[ name_length ]; strncpy( ptr_name, ptr_name_, name_length ); } char const* get_name() const { return ptr_name; } void set_dimensions( int width_ , int height_ ) { width = width_; height = height_; } int area() const { return ( width * height ); } }; void normalize( Rectangle* ptr_rect ) { ptr_rect->set_dimensions( 1, 1 ); } Rectangle create_rectangle() { Rectangle temp; temp.set_name( "FACTORY CREATED" ); return temp; } int main() { Rectangle rect( "first", 333, 222 ); Rectangle rect2 = rect; // make a deep copy rect2.set_name( "second" ); std::cout << "rectangle width: " << rect.width << '\n'; std::cout << "rectangle height: " << rect.height << '\n'; std::cout << "rectangle area: " << rect.area() << '\n'; std::cout << "rectangle name: " << rect.get_name() << '\n' << '\n'; rect = rect2; // shallow assignment std::cout << "rectangle width: " << rect2.width << '\n'; std::cout << "rectangle height: " << rect2.height << '\n'; std::cout << "rectangle area: " << rect2.area() << '\n'; std::cout << "rectangle name: " << rect2.get_name() << '\n' << '\n'; rect2 = create_rectangle(); std::cout << "rectangle width: " << rect2.width << '\n'; std::cout << "rectangle height: " << rect2.height << '\n'; std::cout << "rectangle area: " << rect2.area() << '\n'; std::cout << "rectangle name: " << rect2.get_name() << '\n' << '\n'; system( "pause" ); return 0; }
We made a lot of effort making the Rectangle struct allocate and deallocate dynamic memory for a tiny string of characters.
In Java, every "string literal" is an object, lifting the burden of low-level memory allocation/dealocation off the programmer's shoulders.
Likewise, in C++, there is a C++ standard library string class that manages a variable-sized string of characters.
In the following demo, I replaced the pointer to a string of characters with std::string.
The copy constructor, the move constructor, and overloaded assignment operator are removed, because by default, compiler-generated version of copy, move, and assignment invoke similar operations for every data member of the struct. In our case the work is delegated to the the implementation of the standard library string class:
#include <cstdlib> #include <iostream> #include <string> struct Rectangle { int width; // member variable int height; // member variable std::string name; Rectangle() : width( 1 ), height( 1 ), name( "UNKNOWN" ) { } Rectangle( char const* ptr_name_, int width_ ) : width( width_ ), height( width_ ), name( ptr_name_ ) { } Rectangle( char const* ptr_name_, int width_ , int height_ ) : width( width_ ), height( height_ ), name( ptr_name_ ) { } void set_name( char const* ptr_name_ ) { name = ptr_name_; } char const* get_name() const { return name.c_str(); } void set_dimensions( int width_ , int height_ ) { width = width_; height = height_; } int area() const { return ( width * height ); } }; void normalize( Rectangle* ptr_rect ) { ptr_rect->set_dimensions( 1, 1 ); } Rectangle create_rectangle() { Rectangle temp; temp.set_name( "FACTORY CREATED" ); return temp; } int main() { Rectangle rect( "first", 333, 222 ); Rectangle rect2 = rect; // make a deep copy rect2.set_name( "second" ); std::cout << "rectangle width: " << rect.width << '\n'; std::cout << "rectangle height: " << rect.height << '\n'; std::cout << "rectangle area: " << rect.area() << '\n'; std::cout << "rectangle name: " << rect.get_name() << '\n' << '\n'; rect = rect2; // shallow assignment std::cout << "rectangle width: " << rect2.width << '\n'; std::cout << "rectangle height: " << rect2.height << '\n'; std::cout << "rectangle area: " << rect2.area() << '\n'; std::cout << "rectangle name: " << rect2.get_name() << '\n' << '\n'; rect2 = create_rectangle(); std::cout << "rectangle width: " << rect2.width << '\n'; std::cout << "rectangle height: " << rect2.height << '\n'; std::cout << "rectangle area: " << rect2.area() << '\n'; std::cout << "rectangle name: " << rect2.get_name() << '\n' << '\n'; system( "pause" ); return 0; }