Introduction to Object-Oriented Programming
Procedural programming vs. Object-Oriented programmingProcedural programming:
In C++, these three properties are realized as:
Procedural Programming
Let's use this structure that represents a student: (What is sizeof(Student))?Also, notice MAXLENGTH is not a #define:
const int MAXLENGTH = 10; struct Student { char login[MAXLENGTH]; int age; int year; float GPA; }; void display_student(const Student &student) { using std::cout; using std::endl; cout << "login: " << student.login << endl; cout << " age: " << student.age << endl; cout << " year: " << student.year << endl; cout << " GPA: " << student.GPA << endl; }
This allows this code: and this: void f1() { Student st1; st1.age = 20; st1.GPA = 3.8; std::strcpy(st1.login, "jdoe"); st1.year = 3; display_student(st1); } Output: login: jdoe age: 20 year: 3 GPA: 3.8 void f2() { Student st2; st2.age = -5; st2.GPA = 12.9; std::strcpy(st2.login, "rumplestiltzkin"); st2.year = 150; display_student(st2); } Output: (May get lucky) login: rumplestiltzkin age: 7235947 year: 150 GPA: 12.9
A second attempt to "protect" the data by using functions to set the data instead of the user directly modifying it.
as well as this: void f1() { Student st3; display_student(st3); } Output: login: | age: 0 year: 4198736 GPA: 2.07362e-317
void set_login(Student &student, const char* login); void set_age(Student &student, int age); void set_year(Student &student, int year); void set_GPA(Student &student, float GPA);
void set_login(Student &student, const char* login) { std::strncpy(student.login, login, MAXLENGTH); student.login[MAXLENGTH - 1] = 0; } void set_age(Student &student, int age) { if ( (age < 18) || (age > 100) ) { std::cout << "Error in age range!\n"; student.age = 18; } else student.age = age; }
void set_year(Student &student, int year) { if ( (year < 1) || (year > 4) ) { std::cout << "Error in year range!\n"; student.year = 1; } else student.year = year; } void set_GPA(Student &student, float GPA) { if ( (GPA < 0.0) || (GPA > 4.0) ) { std::cout << "Error in GPA range!\n"; student.GPA = 0.0; } else student.GPA = GPA; }
Notes:
Now this code: results in this: void f3() { Student st3; set_age(st3, -5); set_GPA(st3, 12.9); set_login(st3, "rumplestiltzkin"); set_year(st3, 150); display_student(st3); } Error in age range! Error in GPA range! Error in year range! login: rumplesti age: 18 year: 1 GPA: 0
This code will give errors now:struct Student { // All data is inaccessible from outside this structure private: char login[MAXLENGTH]; int age; int year; float GPA; };
However, even this code will no longer work:
Illegal code: Errors: Student st1; st1.age = 20; st1.GPA = 3.8; std::strcpy(st1.login, "jdoe"); st1.year = 3; error: 'int Student::age' is private within this context error: 'float Student::GPA' is private within this context error: 'char Student::login[10]' is private within this context error: 'int Student::year' is private within this context
void set_age(Student &student, int age) { if ( (age < 18) || (age > 100) ) { std::cout << "Error in age range!\n"; student.age = 18; // ERROR, age is private } else student.age = age; // ERROR, age is private }
In C++, encapsulated functions are generally called methods or member functions (because they are members of the structure).
Encapsulating Functions and Data
Adding functions to the structure is simple. By declaring them in a public section, the functions (methods) will be accessible from outside of the structure:
Structure with private data, public methods | Client access is through the public methods | |
---|---|---|
// This code is in a header (.h) file const int MAXLENGTH = 10; struct Student { public: void set_login(const char* login); void set_age(int age); void set_year(int year); void set_GPA(float GPA); private: char login_[MAXLENGTH]; int age_; int year_; float GPA_; }; |
void f1() { // Create a Student struct (object) Student st1; // Set the fields using the public methods st1.set_login("jdoe"); st1.set_age(22); st1.set_year(4); st1.set_GPA(3.8); st1.age_ = 10; // ERROR, private st1.year_ = 2; // ERROR, private } |
// This code is in a .cpp file void Student::set_login(const char* login) { std::strncpy(login_, login, MAXLENGTH); login_[MAXLENGTH - 1] = 0; } |
void Student::set_age(int age) { if ( (age < 18) || (age > 100) ) { std::cout << "Error in age range!\n"; age_ = 18; } else age_ = age; } |
void Student::set_year(int year) { if ( (year < 1) || (year > 4) ) { std::cout << "Error in year range!\n"; year_ = 1; } else year_ = year; } |
void Student::set_GPA(float GPA) { if ( (GPA < 0.0) || (GPA > 4.0) ) { std::cout << "Error in GPA range!\n"; GPA_ = 0.0; } else GPA_ = GPA; } |
// This method belongs to the Student struct void Student::set_year(int year) { if ( (year < 1) || (year > 4) ) { std::cout << "Error in year range!\n"; year_ = 1; } else year_ = year; } |
// This is a "normal" global function // not part of any struct void set_year(int year) { // body of function } |
You generally won't see the public keyword used with structures.
Members are public by default OK, but redundant struct Student { char login_[MAXLENGTH]; int age_; int year_; float GPA_; }; struct Student { public: char login_[MAXLENGTH]; int age_; int year_; float GPA_; };
Finally, we need to get back a way to display the values. Our original display_student no longer can access the private members, so we have to make it part of the Student structure:
Now, this is how we use it:
Add the display method Modify the implementation struct Student { public: void set_login(const char* login); void set_age(int age); void set_year(int year); void set_GPA(float GPA); void display(); private: char login_[MAXLENGTH]; int age_; int year_; float GPA_; }; void Student::display() { using std::cout; using std::endl; cout << "login: " << login_ << endl; cout << " age: " << age_ << endl; cout << " year: " << year_ << endl; cout << " GPA: " << GPA_ << endl; }
void f1() { // Create a Student object Student st1; // Using the public methods st1.set_login("jdoe"); st1.set_age(22); st1.set_year(4); st1.set_GPA(3.8); // Tell the object to display itself st1.display(); }
Student st1; // Create a Student st1.display(); // Undefined behavior!
Classes
In short, a class is identical to a struct with one exception: the default accessibility is private.These will work the same:
And these will work the same:
Default for struct is public Explicit public keyword struct Student { char login_[MAXLENGTH]; int age_; int year_; float GPA_; }; class Student { public: char login_[MAXLENGTH]; int age_; int year_; float GPA_; };
Explicitly private Default for class is private struct Student { private: char login_[MAXLENGTH]; int age_; int year_; float GPA_; }; class Student { char login_[MAXLENGTH]; int age_; int year_; float GPA_; };
We will generally be using the class keyword when creating new types that have methods associated with them. We'll use the struct keyword for POD types. (Plain Old Data types).
If you think a little more in-depth about what a data-type is, you'll see it's more than just the range of values. It is also the operations that can be performed on it. (e.g. you can't use the mod operator, %, with floating point values nor can you use the + operator with two pointers.)
Initializing Objects: The Constructor
This is the problem we need to solve:We never want to have any objects that are in an undefined state. Ever.
Client code Output (random garbage, might crash) Student s; // Uninitialized student s.display(); // ??? login: PA age: 4280352 year: 4225049 GPA: 1.89223e-307
Recall how we initialize structures in C:
But with private data, using the initializer list is illegal:
struct Student { // Public by default char login[MAXLENGTH]; int age; int year; float GPA; }; void f() { // Uninitialized Student Student st1; // Set values by assignment std::strcpy(st1.login, "jdoe"); st1.age = 20; st1.year = 3; st1.GPA = 3.08; // Set values by initialization Student john = {"jdoe", 20, 3, 3.10f}; Student jane = {"jsmith", 19, 2, 3.95f}; }
You'll get errors like these:
class Student { // Private by default char login[MAXLENGTH]; int age; int year; float GPA; }; void f() { // This is now illegal (accessing private members directly) Student john = {"jdoe", 20, 3, 3.10f}; Student jane = {"jsmith", 19, 2, 3.95f}; }
GNU:
Microsoft:error: 'john' must be initialized by constructor, not by '{...}' error: 'jane' must be initialized by constructor, not by '{...}'
Clang:error C2552: 'john' : non-aggregates cannot be initialized with initializer list 'Student' : Types with private or protected data members are not aggregate error C2552: 'jane' : non-aggregates cannot be initialized with initializer list 'Student' : Types with private or protected data members are not aggregate
The error message from GNU indicates what you need to do: initialize by constructor.error: non-aggregate type 'Student' cannot be initialized with an initializer list Student john = {"jdoe", 20, 3, 3.10}; ^ ~~~~~~~~~~~~~~~~~~~~~ error: non-aggregate type 'Student' cannot be initialized with an initializer list Student jane = {"jsmith", 19, 2, 3.95}; ^ ~~~~~~~~~~~~~~~~~~~~~~~
So, we declare another method that will be called to construct (initialize) the object: (notice the order of public and private, the order is arbitrary)
We can easily implement this method by simply calling the other methods:class Student { public: // Constructor (must have the same name as the class) Student(const char * login, int age, int year, float GPA); void set_login(const char* login); void set_age(int age); void set_year(int year); void set_GPA(float GPA); void display(); private: char login_[MAXLENGTH]; int age_; int year_; float GPA_; };
Implementation | Client can initialize now |
---|---|
Student::Student(const char * login, int age, int year, float GPA) { set_login(login); set_age(age); set_year(year); set_GPA(GPA); } |
void f() { // Set values by constructor Student john("jdoe", 20, 3, 3.10f); Student jane("jsmith", 19, 2, 3.95f); } |
More on this and other initialization techniques later.void f() { // Set values by constructor (Still requires a constructor) // The = is optional, and will still call the constructor. Student john = {"jdoe", 20, 3, 3.10f}; Student jane {"jsmith", 19, 2, 3.95f}; }
Accessors and Mutators (Gettors and Settors)
Since the data in a class is usually private, the only way to gain access to it is by providing public methods that explicitly allow it.
Adding accessors | Implementations |
---|---|
struct Student { public: // Constructor Student(const char * login, int age, int year, float GPA); // Accessors (gettors) int get_age(); int get_year(); float get_GPA(); const char *get_login(); // Mutators (settors) void set_login(const char* login); void set_age(int age); void set_year(int year); void set_GPA(float GPA); void display(); private: char login_[MAXLENGTH]; int age_; int year_; float GPA_; }; |
int Student::get_age() { return age_; } int Student::get_year() { return year_; } float Student::get_GPA() { return GPA_; } const char *Student::get_login() { return login_; } |
Resource Management
The Student class so far:
Change type of login_ | Implementation change (not 100% correct yet, has at least 3 potential bugs!) |
---|---|
struct Student { public: // Public interface ... private: char *login_; int age_; int year_; float GPA_; }; |
void Student::set_login(const char* login) { int len = (int)std::strlen(login); login_ = new char[len + 1]; // Don't forget the NUL character! std::strcpy(login_, login); } |
The client doesn't even know there has been a change:
That is the only change required (sort of). What is the problem?void foo() { // Construct a Student object Student john("rumplestiltzkin", 20, 3, 3.10f); // This will display all of the data john.display(); }
How about this?
john.set_login("johnny"); john.set_login("jj"); john.set_login("doofus");
Add interface method | Add implementation |
---|---|
struct Student { public: // Public stuff ... void free_login(); private: // Private stuff ... }; |
void Student::free_login() { delete [] login_; // It's an array, requires [] } |
void foo() { // Construct a Student object Student john("jdoe", 20, 3, 3.10f); // This will display all of the data john.display(); // Release the memory for login_ john.free_login(); }
void foo() { // Construct a Student object Student john("jdoe", 20, 3, 3.10f); // This will display all of the data john.display(); // Oops, memory leak now! } |
void foo() { // Construct a Student object Student john("jdoe", 20, 3, 3.10f); // Release the memory for login_ john.free_login(); // Oops, very bad now! john.display(); } |
Original method | Modified method (correct, but there are still problems) |
---|---|
void Student::set_login(const char* login) { int len = (int)strlen(login); login_ = new char[len + 1]; std::strcpy(login_, login); } |
void Student::set_login(const char* login) { // Free the "old" login delete [] login_; // Now create a new one int len = (int)strlen(login); login_ = new char[len + 1]; std::strcpy(login_, login); } |
In order to make sure that the memory is deleted, we need something like a constructor in reverse. Let's call it a destructor.login_ = 0;
Destroying Objects: The Destructor
We'd like some code that will be called when the client is done with the object. The code is another method called a destructor and is similar to the constructor.
Add destructor (declaration) | Add implementation |
---|---|
struct Student { public: // Constructor Student(const char * login, int age, int year, float GPA); // Destructor, same name as constructor, but with a ~ in // front. Absolutely no inputs, no return (must match // this signature exactly). ~Student(); // Other public members as before ... private: // Private members as before ... }; |
Student::~Student() { // Free the memory that was allocated delete [] login_; } |
void foo() { // Construct a Student object Student john("jdoe", 20, 3, 3.10f); // This will display all of the data john.display(); } // Destructor is called here.
The destructor will be called automagically when the object goes out of scope. (The meaning of scope here is the same meaning we've been using since the beginning of C.)Note: The ~ operator is called the complement operator (or one's complement operator or bitwise complement operator). It's used to name the destructor because the destructor is the complement of the constructor.
The compiler is smart about calling the destructor for local objects:void foo() { Student john("jdoe", 20, 3, 3.10f); if (john.get_age() > 10) { Student jane("jsmith", 19, 2, 3.95f); } // jane's destructor called } // john's destructor called
void f7() { // Construct a Student object Student john("jdoe", 20, 3, 3.10f); if (john.get_age() > 10) { Student jane("jsmith", 19, 2, 3.95f); if (jane.get_age() > 2) return; // Destructor's for jane // and john called } }
What have we accomplished so far?:This makes the destructor an extremely powerful concept. This is also known as deterministic destruction because it is guaranteed to work this way every time and is one of the most powerful features of C++.
Creating Objects
Let's modify the constructor and destructor to print a message each time they are called:
Constructor | Destructor |
---|---|
Student::Student(const char * login, int age, int year, float GPA) { login_ = 0; set_login(login); set_age(age); set_year(year); set_GPA(GPA); std::cout << "Student constructor for " << login_ << std::endl; } |
Student::~Student() { std::cout << "Student destructor for " << login_ << std::endl; delete [] login_; } |
Program | Output |
---|---|
void foo() { std::cout << "***** Begin *****\n"; Student john("jdoe", 20, 3, 3.10f); Student jane("jsmith", 19, 2, 3.95f); Student jim("jbob", 22, 4, 2.76f); // Modify john john.set_age(21); john.set_GPA(3.25f); // Modify jane jane.set_age(24); jane.set_GPA(4.0f); // Modify jim jim.set_age(23); jim.set_GPA(2.98f); // Display all john.display(); jane.display(); jim.display(); std::cout << "***** End *****\n"; } |
***** Begin ***** Student constructor for jdoe Student constructor for jsmith Student constructor for jbob login: jdoe age: 21 year: 3 GPA: 3.25 login: jsmith age: 24 year: 2 GPA: 4 login: jbob age: 23 year: 4 GPA: 2.98 ***** End ***** Student destructor for jbob Student destructor for jsmith Student destructor for jdoe |
will look something like this in memory: (the addresses are arbitrary as usual)Student john("jdoe", 20, 3, 3.10f); Student jane("jsmith", 19, 2, 3.95f); Student jim("jbob", 22, 4, 2.76f);
Notes:![]()
Notice that the methods are not part of the object. This may seem surprising at first. So how does the display method know which data to show?
john.display(); jane.display(); jim.display();
// Nowhere does this code reference john, jane, or jim void Student::display() { using std::cout; using std::endl; // These members are just offsets. But offsets from what exactly? cout << "login: " << login_ << endl; cout << " age: " << age_ << endl; cout << " year: " << year_ << endl; cout << " GPA: " << GPA_ << endl; }
The this Pointer
All methods of a class/struct are passed a hidden parameter. (This is the first parameter that is passed.) This parameter is the address of the invoking object. In other words, the address of the object that you are calling a method on:Really, the display method is more like this (with the items in blue hidden):john.display(); // john is the invoking object jane.display(); // jane is the invoking object jim.display(); // jim is the invoking object
The example above would look something like this after compiling:void Student::display(Student *this) { using std::cout; using std::endl; // Members are offset from "this" cout << "login: " << this->login_ << endl; cout << " age: " << this->age_ << endl; cout << " year: " << this->year_ << endl; cout << " GPA: " << this->GPA_ << endl; }
So, in a nutshell, this (no pun intended) is how the magic works. The programmer has access to the this pointer inside of the methods. (this is a keyword.)display(&john); // Address of john object passed to display: display(100) display(&jane); // Address of jane object passed to display: display(200) display(&jim); // Address of jim object passed to display: display(300)
Both of these lines are the same within Student::display:
// Normal code cout << "login: " << login_ << endl; // Explicit use of this. (Generally only seen in beginner's code.) // But is required in certain more advanced C++ code. cout << "login: " << this->login_ << endl;
Languages such as C++, Java, C#, D, JavaScript use the keyword this. Other languages such as Pascal, Rust, and Lua use the keyword self. Visual Basic uses the keyword Me.
const Member Functions
In the "olden days", we would make our parameters const if we were not going to modify them in the global functions. Assume that all data is public in the Student structure:
void display_student(const Student &student) { using std::cout; using std::endl; cout << "login: " << student.login << endl; cout << " age: " << student.age << endl; cout << " year: " << student.year << endl; cout << " GPA: " << student.GPA << endl; // Try to modify the constant object. // This would lead to a compiler error. student.age = 100; } |
void foo() { // Create constant object const Student john = {"jdoe", 20, 3, 3.10f}; // This works just fine with const object display_student(john); } |
There is no "parameter" to mark as const:
Remember, if you marked a parameter using the const keyword, you were promising/advertising that you will not change it. Otherwise, you were promising that you were going to change it.void Student::display() { using std::cout; using std::endl; cout << "login: " << login_ << endl; cout << " age: " << age_ << endl; cout << " year: " << year_ << endl; cout << " GPA: " << GPA_ << endl; }
How does the user know what the display method is going to do?
Unless you specify the behavior, ALL member functions (methods) are assumed to change the data of the object:void Student::display(); // Prototype. Will this change anything? How do we know?
void foo() { // Create a constant object (can't change any data later) const Student john("jdoe", 20, 3, 3.10f); john.set_age(25); // Error writing age_, as expected. john.set_year(3); // Error writing year_, as expected. john.get_age(); // Error reading age_, NOT expected. john.display(); // Error printing data, NOT expected. }
You must tag both the declaration (in the class definition) and implementation with the const keyword:
struct Student { public: // Constructor Student(const char * login, int age, int year, float GPA); // Destructor ~Student(); // Mutators (settors/writing) are non-const void set_login(const char* login); void set_age(int age); void set_year(int year); void set_GPA(float GPA); // Accessor (gettors/reading) are const int get_age() const; int get_year() const; float get_GPA() const; const char *get_login() const; // Nothing will be modified void display() const; private: char *login_; int age_; int year_; float GPA_; }; |
void Student::display() const { using std::cout; using std::endl; cout << "login: " << login_ << endl; cout << " age: " << age_ << endl; cout << " year: " << year_ << endl; cout << " GPA: " << GPA_ << endl; } int Student::get_age() const { return age_; } int Student::get_year() const { return year_; } float Student::get_GPA() const { return GPA_; } const char *Student::get_login() const { return login_; } |
Now, this works as expected:
void foo() { // Create a constant object const Student john("jdoe", 20, 3, 3.10f); john.set_age(25); // Error, as expected. john.set_year(3); // Error, as expected. john.get_age(); // Ok, as expected. john.display(); // Ok, as expected. }
Note: If a method does not modify the private data members, it should be marked as const. This
will save you lots of time and headaches in the future. Unfortunately, the compiler won't remind you to do this
until you try and use the method on a constant object.
This is why you are always told to "Use const where appropriate."
Separating the Interface from the Implementation
Typically, each class will reside in its own file. In fact, most classes will generally be in two files:g++ -o project main.cpp Student.cpp -Wall -Wextra -ansi -pedantic
Default Constructors
Recall one of the reasons for a constructor:"To ensure that an object's data is not left undefined."
This means that to construct an object, a class absolutely, positively must have a constructor. If there is no constructor, you cannot create an object (instance) from the class. This neatly solves the first problem above.
First, constructors:
Here's an example:
struct Point { double x; double y; }; |
void foo() { // Create a Point object // x/y are uninitialized Point pt1; // Display random values for x/y std::cout << pt1.x << "," << pt1.y << std::endl; } Output: (random) 1.89121e-307,1.89121e-307 |
The solution is to create a default constructor. A default constructor is simply a constructor that can be called without any arguments.
Add default constructor | Implement the default constructor |
---|---|
struct Point { // Default constructor Point(); double x; double y; }; |
Point::Point() { // Give default values x = 0; y = 0; } |
Be careful not to do this:// Create a Point object // x/y are defined now Point pt1; // Display values for x/y std::cout << pt1.x << "," << pt1.y << std::endl; Output: (defined values) 0,0
The above is a declaration/prototype for a function named pt that takes no parameters (void) and returns a Point. This is described by Scott Meyers as C++'s "Most vexing parse" in his book Effective STL. BTW, Scott Meyers is one of the most knowledgeable C++ programmers on the planet!// This does NOT call the default constructor! Point pt();
It is not uncommon to provide other constructors (overloaded) in addition to a default constructor. (The example below uses the class keyword instead of struct just to demonstrate the technique works for both.)
Multiple constructors | Implementations |
---|---|
class Point { public: // Default constructor Point(); // Non-default constructor Point(double x, double y); // For convenience void display() const; private: double x_; double y_; }; |
Point::Point() { // Give default values x_ = 0.0; y_ = 0.0; } Point::Point(double x, double y) { // Give values from params x_ = x; y_ = y; } void Point::display() const { // Display values for x/y std::cout << x_ << "," << y_ << std::endl; } |
Note that we can combine the default constructor into a non-default constructor by using default arguments:// Both are accepted Point pt1; Point pt2(3.5, 7); pt1.display(); // 0,0 pt2.display(); // 3.5,7
class Point { public: // Default constructor Point(double x = 0.0, double y = 0.0); // Other stuff ... }; |
Point::Point(double x, double y) { // Use params to set values x_ = x; y_ = y; } |
Notes:// Two default constructors (illegal) Point(); Point(double x = 0.0, double y = 0.0); Point pt1; // Which one?
Point::Point() { }
struct Student { public: // Default constructor Student(); // Constructor Student(const char * login, int age, int year, double GPA); // Other public stuff ... private: char *login_; int age_; int year_; double GPA_; }; |
// Default constructor (not really a good idea here) Student::Student() { login_ = 0; set_login("Noname"); set_age(18); set_year(1); set_GPA(0.0); } // Constructor Student::Student(const char * login, int age, int year, double GPA) { login_ = 0; set_login(login); set_age(age); set_year(year); set_GPA(GPA); } |
To be clear: A default constructor is a constructor that does not require the programmer to provide any parameters. It does not mean that the constructor doesn't take any parameters. See the Point (default) constructor above.
Default Destructors
Recall one of the reasons for a destructor:"To ensure that any resources (e.g. memory) acquired are released."
Just like constructors, the compiler will provide a destructor for us if we fail to do so. Here's what the compiler will generate (sort of) if we don't provide a destructor for the Point class or the Student class:
Point::~Point() { } |
Student::~Student() { } |
void Student::set_login(const char* login) { delete [] login_; int len = std::strlen(login); login_ = new char[len + 1]; // Dynamic memory allocation, need to delete at some point std::strcpy(login_, login); }
Student::~Student() { // Since our class allocated the memory, // our class must release the memory. delete [] login_; }
class Point { public: // Default constructor Point(double x = 0.0, double y = 0.0); // Destructor ~Point(); // For convenience void display() const; private: double x_; double y_; }; |
Point::~Point() { std::cout << "Point destructor: " << x_ << "," << y_ << std::endl; } Point::Point(double x, double y) { // Assign from the parameters x_ = x; y_ = y; std::cout << "Point constructor: " << x_ << "," << y_ << std::endl; } |
Program | Output |
---|---|
void foo() { // Static allocation Point pt1; // 0,0 Point pt2(4); // 4,0 Point pt3(4, 5); // 4,5 // Similar using dynamic allocation Point *pt4 = new Point; Point *pt5 = new Point(8); Point *pt6 = new Point(7, 9); // Must delete manually (calls destructor) delete pt4; delete pt5; delete pt6; } // Destructors for pt3,pt2,pt1 called here (notice the ordering) |
Point constructor: 0,0 Point constructor: 4,0 Point constructor: 4,5 Point constructor: 0,0 Point constructor: 8,0 Point constructor: 7,9 Point destructor: 0,0 Point destructor: 8,0 Point destructor: 7,9 Point destructor: 4,5 Point destructor: 4,0 Point destructor: 0,0 |
Note: Unlike constructors, you can't overload destructors. There is exactly one destructor for every class and it doesn't take parameters and doesn't return anything. The reason is simple: you are not calling it, the compiler is, so you don't have a chance to make a choice on which one to call.
Arrays of Objects
Just like any other type, you can create arrays of objects.Built-in types:
User-defined types:// Compiler initializers elements that you don't int a[3] = {1, 2, 3}; // 1, 2, 3 int b[3] = {1, 2}; // 1, 2, 0 int c[3] = {0} // 0, 0, 0 int d[3] = {} // 0, 0, 0 (This is an error in C) double e[3] = {1.0} // 1.0, 0.0, 0.0
// Requires default constructor Student st1[2]; for (int i = 0; i < 2; i++) st1[i].display(); |
login: Noname age: 18 year: 1 GPA: 0 login: Noname age: 18 year: 1 GPA: 0 |
// Requires default constructor Student st2[3] = { Student("jdoe", 20, 3, 3.10F), Student("jsmith", 19, 2, 3.95F) }; for (int i = 0; i < 3; i++) st2[i].display(); |
login: jdoe age: 20 year: 3 GPA: 3.1 login: jsmith age: 19 year: 2 GPA: 3.95 login: Noname age: 18 year: 1 GPA: 0 |
// No default constructor required Student st3[2] = { Student("jdoe", 20, 3, 3.10F), Student("jsmith", 19, 2, 3.95F) }; for (int i = 0; i < 2; i++) st3[i].display(); |
login: jdoe age: 20 year: 3 GPA: 3.1 login: jsmith age: 19 year: 2 GPA: 3.95 |
If the Student class does not have a default constructor, then the first two examples above would cause a compiler error. This is something that you must understand completely.
Understanding the Big Picture™
What problems are solved by