Introduction to classes

Slides:



Advertisements
Similar presentations
Throwing and catching exceptions
Advertisements

Recursive binary search
For loops.
Templates.
Introduction to classes
Default values of parameters
Pointers.
Dynamically allocating arrays
Anatomy of a program.
Binary search.
Command-line arguments
Throwing exceptions.
Console input.
Dangling pointers.
This.
Sorted arrays.
Dynamically allocating arrays within structures
Dynamic memory allocation
Break statements.
Linked Lists.
Wild pointers.
The comma as a separator and as an operator
Bucket sort.
The call stack and recursion and parameters revisited
The ternary conditional operator
Dynamically allocating structures
Memory leaks.
Pushing at the back.
Sorting algorithms.
Command-line arguments
Passing pointers as parameters to and from functions
Templated Linked Lists
Polymorphism.
Dynamically allocating arrays
Insertion sort.
Problems with pointers
A list-size member variable
Protecting pointers.
Dynamically allocating arrays
Code-development strategies
Throwing exceptions.
Anatomy of a program.
Insertion sort.
Pointers as arguments and return values
Reference variables, pass-by-reference and return-by-reference
Addresses and pointers
Default values of parameters
Class variables and class functions
Operator overloading.
The std::string class.
Dynamic allocation of arrays
Templates.
This.
Dynamic memory allocation
Insertion sort.
Sorted arrays.
Sorting algorithms.
Issues with classes.
Dangling pointers.
Dynamic allocation of classes
Encapsulation.
Destructors.
Counting sort.
Searching and sorting arrays
Protecting pointers.
Data structures: class
An array class: constructor and destructor
Constructors.
This.
Recursive binary search
Presentation transcript:

Introduction to classes

Outline In this lesson, we will: Introduce the concept of classes Describe the visibility of member variables Initialization through constructors Clean-up through a destructor Templates

The idea Suppose you write the following string class You are using null-character-terminated strings struct string_t { char *string; std::size_t capacity; }; You publish this class, and developers are now using it… These developers may be internal to a company you work for Alternatively, you may have published this class

The idea You get a bug-report: your string class is not working as expected You look through a huge bug report, and you find the following: string_t *p_str{new string_t{}}; // Do something... if ( p_str->capacity = n ) { // Do semething else... } // Something goes wrong after a while... What happened here?

The idea Suppose you have an initialization function: void string_init( string_t &str, std::size_t length ) { str.array_capacity = length + 1; std.string = new char[str.array_capacity]; std.string[0] = '\0'; } What happens if a programmer forgets to call this function? Suppose you have a destroy function: void string_destroy( string_t &str ) { delete[] str.string; std.string = nullptr;

The idea Suppose, however, to be significantly more efficient, you determine that is necessary to store the length: Instead, you will record the length of the string: struct string_t { char *string; std::size_t capacity; std::size_t length; }; Unfortunately: The developers using your code wrote code without length They are not expecting to update a length member variable… This breaks 1000s of lines of code, but this is for the best, right?

The problem The primary issue here are that users have access to the member variables Solution: Require users to only call functions: std::size_t string_length( string_t const &str ) { return str.length; } std::size_t string_capacity( string_t const &str ) { return str.capacity; Problem: Programmers will not listen to you… Especially if they just “need” that value

The solution One solution is an approach called object-oriented programming Data is stored in such a way that only the author can access that data All other programmers must use member functions Other programmers do not have access to the member variables When a new instance of the structure is created, an initialization function is automatically called When an instance of the structure is deleted, a destroy function is automatically called

Class declarations A class declaration is similar to a structure: class String; class Linked_list; The identifier is class is a keyword The naming convention of classes we will use is a capitalized first letter with no trailing _t Compare with: struct string_t; struct linked_list_t;

Class definition The definition of a class is similar to a structure: class String { public: private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; The ideas of public and private describe the visibility of member variables Labels: public and private are keywords

Class definition Any member variables declared after a private label are simply not accessible by (they are not visible to) any other programmer other than the author of the class class String { public: private: std::size_t array_capacity; std::size_t string_length; char *character_array; };

Class definition Any member variables declared after a public label are accessible (or visible) just like a structure: class String { public: std::size_t array_capacity; std::size_t string_length; char *character_array; private: };

Class definition In fact, these two are essentially identical in all ways: class String { public: std::size_t array_capacity; std::size_t string_length; char *character_array; private: }; struct string_t { std::size_t array_capacity; std::size_t string_length; char *character_array; };

Constructors for initialization For structures: Initialization must be performed after declarations There must be an explicit call to the initialization function Using an instance before it is initialized may be disastrous This is a common source of errors

Constructors for initialization Consequently, initializers are automatically included with classes: class String; class String { public: // Constructor--called to initialize the instance String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; };

Constructors for initialization These initialization functions are called constructors class String; class String { public: // Constructor String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; String::String(): array_capacity{32}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; } The String:: is there to indicate that this constructor is associated with the String class and not just a function called String

Constructors for initialization Each of the member variables is assigned an initial value class String; class String { public: // Constructor String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; String::String(): array_capacity{32}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; } Every member variable should be given an initial value

Constructors for initialization Each of the member variables is assigned an initial value class String; class String { public: // Constructor String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; String::String(): array_capacity{32}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; } Peculiar C++ idiosyncrasy: These must be initialized in the same order the member variables are declared in the class definition – If you mix them up, C++ will use the class definition order anyway

Constructors for initialization Finally the constructor body may finish the initialization class String; class String { public: // Constructor String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; String::String(): array_capacity{32}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; } After all member variables are initialized, the constructor body is executed

Constructors for initialization When you create an instance of the string class, the constructor is automatically called: #include <iostream> int main(); class String; int main() { String str{}; // The constructor String() is automatically called std::cout << " –"end of 'String str{}'" << std::endl; String *p_str{nullptr}; p_str = new String{}; // The constructor String() is // automatically called std::cout << " –"end of 'new String{}'" << std::endl; // Use 'str' and 'p_str'... delete p_str; p_str = nullptr; return 0; } Output: Finished calling 'String()' – end of 'String str{}' – end of 'new String{}'

Constructors for initialization We cannot see the effect, but we could if there was a side-effect in the constructor: String::String(): array_capacity{32}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; std::cout << "Finished calling 'String()'" << std::endl; }

Constructors for initialization Now, when we execute this code, we see the output: #include <iostream> int main(); class String; int main() { String str{}; // The constructor String() is automatically called std::cout << " – end of 'String str{}'" << std::endl; String *p_str{nullptr}; p_str = new String{}; // The constructor String() is // automatically called std::cout << " – end of 'new String{}'" << std::endl; // Use 'str' and 'p_str'... delete p_str; p_str = nullptr; return 0; } Output: Finished calling 'String()' – end of 'String str{}' – end of 'new String{}'

Constructors for initialization We can have multiple constructors: class String; class String { public: // Constructors String(); String( std::size_t max_length ); private: std::size_t array_capacity; std::size_t string_length; char *character_array; };

Constructors for initialization Alternatively, we can use default values: class String; class String { public: // Constructor String( std::size_t max_length = 32 ); private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; Note: The default value must be given in the constructor declaration, not the definition!

Constructors for initialization Each constructor can be given its own definition: String::String( std::size_t max_length ): array_capacity{max_length + 1}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; std::cout << "Finished calling 'String( " << max_length << " )" << std::endl; }

Constructors for initialization Now we can call a different constructor: int main() { // The constructor String( std::size_t max_length ) // is automatically called String str{256}; String *p_str{nullptr}; p_str = new String{1024}; // The constructor // String( std::size_t max_length ) // Use 'str' and 'p_str'... delete p_str; p_str = nullptr; return 0; } Output: Finished calling 'String( 256 )' Finished calling 'String( 1024 )'

Destructors for cleanup What is the problem here? int main() { int main() { String *p_str{new String{}}; String str{256}; delete p_str; return 0; p_str = nullptr; } return 0; } We still need to deallocate the memory for the dynamically allocated array The call to delete only deallocates the memory for the three member variables Previously, we defined a string_destroy() function that must be called by the programmer

Destructors for cleanup Classes also have clean-up functions associated with the class These are called destructors class String; class String { public: // Constructor String( std::size_t max_length = 32 ); // Destructor ~String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; };

Destructors for cleanup A few critical comments: This is the one and only exception to the definition of an identifier The “~” in front of the class name can be read as “not” Recall the bitwise complement unary operator Thus, “the destructor” is “not the constructor”  There is only one destructor and it never takes any arguments The destructor is called either when: A local variable goes out of scope The delete operator is called on an instance of the class

Destructors for cleanup In this case, the destructor would clean up the memory: String::~String() { std::cout << "Calling ~String() with " << array_capacity << std::endl; delete[] character_array; character_array = nullptr; }

Destructors for cleanup We can now execute this code: int main(); int main() { String str{256}; String *p_str{new String{}}; // Do something with 'str' and 'p_str' delete p_str; p_str = nullptr; return 0; } Why in this order? Output: Finished calling String( 256 ) Finished calling String( 32 ) Calling ~String() with 33 Calling ~String() with 257

The Array class Try this yourself: #include <iostream> // Class declaration class String; // Function declaration int main(); // Class definition class String { public: // Constructor String( std::size_t max_length = 32 ); // Destructor ~String(); private: std::size_t array_capacity; std::size_t string_length; char *character_array; }; // Constructor definition String::String( std::size_t max_length ): array_capacity{max_length + 1}, string_length{0}, character_array{new char[array_capacity]} { character_array[0] = '\0'; std::cout << "Finished calling 'String( " << max_length << " )" << std::endl; } // Destructor definition String::~String() { std::cout << "Calling ~String() with " << array_capacity << std::endl; delete[] character_array; character_array = nullptr; int main() { String str{256}; String *p_str{new String{}}; // Do something with 'str' and 'p_str' delete p_str; p_str = nullptr; return 0;

No constructor or destructor? If you do not need a constructor or destructor: No problem, don’t declare any and C++ will leave newly created objects alone and deleted objects will simply have the memory associated with them cleaned up

A simple templated class Consider the following class for a three-dimensional vector: // Class declaration template <typename T> class Vector_3d; // Class definition class Vector_3d { public: // No constructors or destructor private: T x; T y; T z; };

A simple templated class You can now instantiate an instance of this class: #include <iostream> // Declarations template <typename T> class Vector_3d; int main(); int main() { Vector_3d<float> v{}; Vector_3d<double> w{}; return 0; }

A simple templated class Note that explicitly calling a constructor when none is defined will initialize all member variables to their default value: Vector_3d<float> v{}; An uninitialized local variable will leave the member variables with whatever values are currently stored in the allocated memory: Vector_3d<float> v; Important: A default constructor is only provided if you do not explicitly define at least one constructor yourself

A templated class Consider the following class for an array: // Class declaration template <typename T> class Array; // Class definition class Array { public: Array( std::size_t cap = 32 ); ~Array(); private: std::size_t array_capacity; T *array; };

A templated class The constructor and destructor are: template <typename T> Array::Array( std::size_t cap ): array_capacity{cap}, array{new T[array_capacity]} { // Do nothing } Array::~Array() { delete[] array; array = nullptr;

A templated class You may consider adding additional functionality that initializes the array: // Class declaration template <typename T> class Array; // Class definition class Array { public: Array( std::size_t cap = 16 ); Array( std::size_t cap, T default_value ); ~Array(); private: std::size_t array_capacity; T *array; };

A templated class This second constructor would initialize the entries of the array: template <typename T> Array<T>::Array( std::size_t cap ): array_capacity{cap}, array{new T[array_capacity]} { // Empty constructor } Array<T>::Array( std::size_t cap, T default_value ): for ( std::size_t k{0}; k < array_capacity; ++k ) { array[k] = default_value;

A templated class The destructor would delete the array template <typename T> Array<T>::~Array() { delete[] array; array = nullptr; }

A templated class Try this yourself: // Class declaration template <typename T> class Array; // Function definition int main(); // Class definition class Array { public: Array( std::size_t cap = 16 ); Array( std::size_t cap, T default_value ); ~Array(); private: std::size_t array_capacity; T *array; }; // Constructors Array<T>::Array( std::size_t cap ): array_capacity{cap}, array{new T[array_capacity]} { // Empty constructor } Array<T>::Array( std::size_t cap, T default_value ): for ( std::size_t k{0}; k < array_capacity; ++k ) { array[k] = default_value; // Destructor Array<T>::~Array() { delete[] array; array = nullptr; int main() { Array<int> integer_array; Array<double> double_array; return 0;

Why so many template <typename T>? Why do we have so many template <typename T> some-class-declaration; some-class-definition { // Class definition }; some-member-function-definition { // Member function definition }

Why so many template <typename T>? The text template <typename T> is a declaration that in the following code, the identifier T is a templated type This is no different from a function parameter: double fast_sin() { return x - x*x*x/6.0 + x*x*x*x*x/120.0; } It is obvious to the reader that x must be a parameter, but you must still declare it…

Summary Following this lesson, you now Understand how classes can be used to hide member variables Know that one or constructors can be used to initialize member variables Like functions, they can have different arguments They are called either during variable declaration or during calls to the new operator Know that a destructor can be defined to clean up after an object is deallocated They are called either when a variable goes out of scope or during calls to the delete operator

References [1] No references?

Colophon These slides were prepared using the Georgia typeface. Mathematical equations use Times New Roman, and source code is presented using Consolas. The photographs of lilacs in bloom appearing on the title slide and accenting the top of each other slide were taken at the Royal Botanical Gardens on May 27, 2018 by Douglas Wilhelm Harder. Please see https://www.rbg.ca/ for more information.

Disclaimer These slides are provided for the ece 150 Fundamentals of Programming course taught at the University of Waterloo. The material in it reflects the authors’ best judgment in light of the information available to them at the time of preparation. Any reliance on these course slides by any party for any other purpose are the responsibility of such parties. The authors accept no responsibility for damages, if any, suffered by any party as a result of decisions made or actions based on these course slides for any other purpose than that for which it was intended.