































Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Community
Ask the community for help and clear up your study doubts
Discover the best universities in your country according to Docsity users
Free resources
Download our free guides on studying techniques, anxiety management strategies, and thesis advice from Docsity tutors
C++ is the first language to introduce OOP. Key concepts of OOP with example codes in C++ language.
Typology: Study notes
1 / 39
This page cannot be seen from the preview
Don't miss anything!
MSc in Mathematics and Finance, Imperial College London Dr Robert N¨urnberg
Object oriented programming is a fairly new way to approach the task of programming. It supersedes the so called procedural or structured programming languages like Algol, Pascal or C, that have been around since the 1960s. The essence of structured programming is to reduce a program into smaller parts and then code these elements more or less independently from each other. Although structured programming has yielded excellent results when applied to moderately complex programs, it fails when a program reaches a certain size. To allow for more complex programs to be written, the new approach of OOP was invented. OOP, while allowing you to use the best ideas from structured programming, encourages you to decompose a problem into related subgroups, where each subgroup becomes a self-contained object that contains its own instructions and data that relate to that object. In this way complexity is reduced, reusability is increased and the programmer can manage larger programs more easily. All OOP languages, including C++, share the following three capabilities:
1 v/2016/03/
Before we embark on the topic of object oriented programming, let’s remind ourselves of some other important features of C++.
In C++^ this standard example should look like
#include
int main() { cout << "Hello World!" << endl; return 0; }
The first two lines of the above example show the new ANSI-C++^ compliant way to include the standard C++^ libraries. Although many compilers still support the old style, eventually all C++^ compilers will only accept the ANSI standard. Note that without the second line the code would have to look something like
#include
int main() { std::cout << "Hello World!" << std::endl; return 0; }
See §7.1 below for further details on namespaces.
All of the old C standard header files, like stdlib.h, math.h, etc. can still be used. This should be done as follows.
#include
int main() { cout << "exp(1.0) = " << exp(1.0) << endl; return 0; }
For more details on the ANSI-C++^ way to include header files from the standard library visit www.cplusplus.com/doc/ansi/hfiles.html.
The C/C++^ compiler evaluates certain preprocessing directives before actually starting to compile a source code. These preprocessing directives are lines in your program that start with #. An example is the #include statement, which tells the compiler what kind of system calls and libraries you want to use in your program. A macro is another preprocessing directive. In its simplest form, a macro is just an abbreviation. It is a name which stands for a fragment of code. Usually, this will be some specific constant, such as Pi and DEBUG in the example below. Before you can use a macro, you must define it explicitly with the #define directive. It is followed by the name of macro and then the code it should be an abbreviation for. Macros can also be defined to take arguments. In this case, any expansion you define for the macro, will be applied with the arguments substituted by the values you are giving. The function macros SQR() and MAX() in the example below demonstrate the usage. Finally, you can also use conditional directives that allow a part of the program to be ignored during compilation, on some conditions. A conditional can test either an arithmetic expression or whether a
name is defined as a macro. Check the usage of #if, #else, #endif, and #ifdef, #ifndef in the example below.
#include
#define SQR(a) ((a)*(a)) #define MAX(a,b) ((a) > (b)? (a) : (b)) #ifndef Pi #define Pi 3. #endif #define DEBUG 1
int main() {
#ifdef MAX if (MAX(2.0,3.4) > 2.5) cout << "Square of pi is: " << SQR(Pi) << endl; #endif
#if DEBUG cout << "Debugging switched on." << endl; cout << " --- " << FILE << " line " << LINE << "--- Compiled on " << DATE << "." << endl; #else cout << "Debugging switched off." << endl; #endif return 0; }
Note that the example also uses some standard predefined macros. They are available with the same meanings regardless of the machine or operating system on which you are using C/C++. Their names all start and end with double underscores, e.g. FILE, LINE and DATE. More information on the C/C++^ preprocessor can be found at doc.ddart.net/c/gcc/cpp toc.html.
As in C, enumerations provide a convenient way to define variables that have only a small set of meaningful values. Here is a simple example.
enum colour {red, green, blue, black}; colour foreground = black;
C++^ may use the C string functions, where a string is represented by a char*, but they rely on a null termination and proper memory allocation to hold the string. The C++^ string class attempts to simplify string manipulation by automating much of the memory allocation and management.
#include
int main() { string a("abcd efg"); string b = "xyz ijk"; string c = a + b; // concatenation cout << "First character of ’"<< c <<"’ is : " << c[0] << endl; // as in C return 0; }
In C++, in contrast to C, it is possible that two different functions share exactly the same name. That is because in C++^ a function is identified by its signature which comprises both the function’s name and
variable. When passing large data structures this is usually undesirable. A way out is to pass the variable by reference using the keyword const. This ensures that the compiler will not allow any change to the passed variable within the function. The following two functions demonstrate the two concepts.
#include
void print_by_val(double x) { cout << x << endl; } void print_by_ref(const double &x) { cout << x << endl; }
int main() { double p = 3.14; print_by_val(p); print_by_ref(p); return 0; }
1.7.3 Keyword const
Similar to the above usage of const in connection with passing arguments by reference, it can be employed when passing arguments by pointers as well.
1.7.4 Default arguments
C++^ offers the possibility to implement default arguments for functions, in case that they are called with less parameters than originally specified. Here is a simple example.
#include
void print(int i = 10, int j = 5) { cout << i << " " << j << endl; }
int main () { print(2); // ’2 5’ return 0; }
Note that all default parameters must be to the right of any parameters that do not have defaults. Also, once you define a default parameter, all the following parameters have to have a default as well.
Input and output from and to a file needs the header file fstream and is similar to using cin and cout. E.g.
#include
int main() { ofstream fout("output.txt"); fout << "Hello World!" << endl; fout.close(); return 0; }
When dealing with variable length input files, it should be noted that the insertion operator >> itself returns true whenever the last input operation has been successful. See the example below for an illus- tration.
#include
int main() { ifstream fin("data.dat"); if (! fin.is_open()) {
cout << "Error opening the file data.dat." << endl; return -1; } double mean = 0.0, x; int n = 0; while ( fin >> x ) { mean += x; n++; } fin.close(); if (n > 0) mean /= n; cout << "The mean of the " << n << " numbers is " << mean << "." << endl; return 0; }
The term object in C++^ refers to an instance of a class. In the example below, studlist[0] is an instance of the struct student, where we have noted that a struct (as known from C) is simply a class with special properties, see §2.2 for details. The support for classes is the most significant difference between C and C++. A class is essentially an abstract data type. It is a collection of data together with functions that operate on the data. We will refer to these as member data and member functions of a class, respectively. By declaring a class we can create a new data type that, if we are careful, is as powerful as any of the basic types. We start our considerations with a simpler concept known from C.
Structures allow us to bundle data that naturally belong together into one object. E.g.
#include
struct student { string name, firstname; int year; };
int main() { int N; cout << "Number of students: "; cin >> N; student *studlist = new student[N]; for (int i = 0; i < N; i++) { cin >> studlist[i].name >> studlist[i].firstname >> studlist[i].year; } // etc return 0; }
Note that a struct defines a type that can be used (almost) as freely as any of the basic types, such as double, int, bool etc. In particular, we can pass variables of this newly defined type to functions, use references and pointers to and declare arrays with these variables.
2.1.1 Pointers to structures
At times it may be preferable to use (arrays of) pointers to structures. In this case the main() program above would have to be changed to something like
int main() { student s1; s1.create("Smith", "Alan", 1); s1.print(); return 0; }
The big advantage of defining all the member data as private is that firstly we can easily trace an error if something goes wrong, since the only place where the data is changed is in some of the member functions. And secondly, it is very easy to enrich and modify the class later on. For example, we could choose to include a variable address in the class. All we have to do in order to make sure that this new variable is always properly initialized in our program is to change the create method to something like
void student::create(const string &n, const string &fn, int y) { name = n; firstname = fn; year = y; address = "unknown"; }
and maybe provide another create method to initialize all four variables, e.g.
void student::create(const string &n, const string &fn, int y, const string &a) { name = n; firstname = fn; year = y; address = a; }
In order to properly initialize an object, C++^ allows to define a constructor function that is called each time an object of that class is created. A constructor function has the same name as the class of which it is a part and has no return type. On recalling default arguments from §1.7.4, the constructor for the original student class can be defined as follows.
class student { string name, firstname; int year; public: student(const string &n = "unknown", const string &fn = "unkown", int y = 1) { name = n; firstname = fn; year = y; } void print(); };
2.4.1 Copy constructors
A constructor that takes as its only argument an object of its own class is called copy constructor. It is also used whenever a parameter is passed by value and when an object is returned by a function. If there is no copy constructor defined for the class, C++^ uses the default built-in copy constructor, which creates a bit wise copy of the object. Hence one only needs to define this special constructor if the class has a pointer to dynamically allocated memory. The declaration of the copy constructor in the case of the student class would have to look as follows.
class student { public: student(const student &s); // etc };
Observe that we have to pass the parameter by reference, since otherwise a copy of the argument would have to be made, which would invoke the copy constructor, for which another copy would have to be made... etc. Note also that we used the keyword const to ensure that the argument is not changed inside the function.
The complement of a constructor is a destructor. This function is called automatically whenever an object is destroyed, i.e. for local variables when they go out of scope and for global variables when the program ends. This special member function should never be called explicitly. A destructor needs to be
implemented only for classes that have a pointer to dynamically allocated memory, for all other classes the default destructor is sufficient. The destructor function has the name of the class preceded with a ~. It has no return type and takes no parameters. Here is an example.
class array { double *a; int len; public: array(int l = 0) { len = l; a = new double[len]; } // constructor ~array() { delete[] a; } // destructor };
C++^ contains a special pointer that is called this. this is a pointer that is automatically passed to any member function when it is called, and it is a pointer to the object that generates the call. E.g. given the statement foo.bar(), the function bar() is automatically passed a pointer to foo, which is the object that generates the call. This pointer is referred to as this. Hence an equivalent definition of the function student::print() from §2.3 is
void student::print() { cout << this->name << ", " << this->firstname << " (YEAR " << this->year << ")" << endl; }
Of course, there is no reason why one should use the this pointer as shown here, because the shorthand form is much easier. However, the this pointer has several uses, including aiding in overloading operators. See e.g. §3.4 for more details.
When passing objects by using the keyword const, as described in §1.7.3, as a parameter to a function that possibly calls some of the object’s member functions, one has to tell C++^ that these methods will not change the passed object. To do this one has to insert the keyword const in the methods’ declarations. Here is a short example.
class myclass { int a; public: void showvalues(const myclass &A) { this->print(); A.print(); } void print() const { cout << a << endl; } // const needed! };
It is always advisable to use the keyword const for member functions that you as the programmer know will not change the object’s data. For instance, it makes debugging easier and it helps to avoid some errors already at the compilation stage.
Apart from member data and member functions, classes can also define their own data types. These new class definitions, inside the given class, can then be used by the class’s member functions as well as from outside the class. In the latter case, one has to use the scope operator. See the following example for details.
#include
class myclass { int a; public: class local_type { double t; public: local_type(double x) { t = x; }
name of the header file in inverted commas, is like copying and pasting the contents of the header file into the C++^ source file. Below you can find a simple example.
Contents of "script_header.h" #ifndef SCRIPT_HEADER_H #define SCRIPT_HEADER_H
class A { int a; public: A(int = 0); void print() const; private: void check() const; }; #endif // SCRIPT_HEADER_H
Contents of "script_header.cc" #include
A::A(int i) { a = i; } void A::print() const { cout << a << endl; check(); } void A::check() const { if (a == 5) cout << "5!!" << endl; }
int main() { A x(2), y(5), z; x.print(); y.print(); z.print(); return 0; } Note that the macro definition inside the header file prevents the class to be defined twice, in case the header file is included in more than one source file. Note furthermore, that a header file usually only provides the declaration of member functions, whereas the actual implementation takes places in the C++ source file.
Now that we are familiar with the concept of class member functions, we can have another look at the C++ string class. In §1.5 we already saw how to use the basic features of the C++^ string class. In the following program we demonstrate the use of some other useful member functions of this class. For further details visit e.g. www.cppreference.com/cppstring.html.
#include
int main() { string f(" Leading and trailing blanks "); cout << "String f : " << f << endl; cout << "String length : " << f.length() << endl; cout << "String f : " << f.append("ZZZ") << endl; cout << "String length : " << f.length() << endl; cout << "substr(5,9) : " << f.substr(5,9) << endl; cout << "assign("xyz",0,3) : " << f.assign("xyz",0,3) << endl; cout << "insert(1,"abc") : " << f.insert(1,"abc ") << endl;
string g("abc abc abd abc"); cout << "String g : " << g << endl; cout << "replace(12,1,"xyz") : " << g.replace(12,1,"xyz") << endl; cout << "replace(4,3,"xyz",2): " << g.replace(4,3,"xyz",2) << endl; cout << "replace(4,3,"ijk",1): " << g.replace(4,3,"ijk",1) << endl; cout << "find("abd",1) : " << g.find("abd",1) << endl; cout << "find("xyzb") : " << g.find("xyzb") << endl; return 0; }
The above code produces the following output:
String f : Leading and trailing blanks String length : 37 String f : Leading and trailing blanks ZZZ String length : 40 substr(5,9) : eading an assign("xyz",0,3) : xyz insert(1,"abc") : xabc yz String g : abc abc abd abc replace(12,1,"xyz") : abc abc abd xyzbc replace(4,3,"xyz",2): abc xy abd xyzbc replace(4,3,"ijk",1): abc iabd xyzbc find("abd",1) : 5 find("xyzb") : 9
A further important member function is string::c_str() which returns a pointer to a character array with the same characters as the string that generates the call. This method is needed if you want to use some “older” library routines such as sscanf.
As already indicated in §1.6 and §1.7.4, in C++^ it is possible to define several functions with the same name, provided that the type of their arguments or the number of the arguments they take differ. After classes, this is perhaps the next most important feature of C++.
Sometimes it might be necessary to pass the address of an overloaded function to a pointer. E.g. when passing a function as a parameter to another function. The way to tell C++^ which function’s address we would like to access is to include the list of parameters that the function takes into the definition of the pointer. Below is a simple example.
#include
void print(int i) { cout << "i = " << i << endl; }
void print(double a, double b) { cout << "a = " << a << ", b = " << b << endl; }
int main() { void (p)(int) = print; void (q)(double, double) = print; p(2); // ’i = 2’ q(1.0,2.0); // ’a = 1, b = 2’ return 0; }
As we have seen in §2.4, a very common example of function overloading is to provide several different versions of a class’s constructor. This is only natural, since a class needs a constructor for each way that an object of that class is declared in a program. In particular, when dynamically allocating memory for objects of a class, one has to define a default constructor that takes no arguments. Here is an example for the student class.
student *p; p = new student[1]; // calls student::student(); p->print(); // ’unknown, unkown (YEAR 1)’
delete [] p;
In C++, operators like +, *, <, >>, [] and () are just a very special type of functions. However, when overloading operators some special rules apply. In particular, it is not possible to change the number of arguments an operator takes. Furthermore, it is not possible to redefine an operator for the basic
public: Point(double a = 0, double b = 0) { x = a; y = b; } Point &operator-= (const Point &p) { x -= p.x; y -= p.y; return *this; } Point operator- (const Point &p) const { Point r(x-p.x,y-p.y); return r; } void print() const { cout << x << "," << y << endl; } };
int main() { Point p(1.0,2.0), q(0.0,0.5), r; r = p -= q; p.print(); // ’1,1.5’
r = p - q; // ’1,1’ r.print(); return 0; }
Observe that the operator-= () function could have been defined to return void instead. But then statements like r = p -= q; would no longer work.
3.4.1 Overloading relational operators
A special family of binary operators are the relational and logical operators. Naturally, they too can be overloaded for newly defined classes. Here is an example.
class Point { double x,y; public: bool operator< (const Point &p) const { return (x < p.x || (x == p.x && y < p.y)); } // etc };
The C++^ compiler treats the relational operators like any other binary operator. In particular, overloading the operator < does not give meaning to the operators >, <= and >=. That means that if you want to make use of them, you have to overload them individually.
Overloading a unary operator is similar to overloading a binary operator. The only difference is that there is only one operand to deal with. Hence when overloading a unary operator using a member function, the function takes no parameters. Some unary operators have both a prefix and a postfix version. In order to differentiate between these two when overloading the operator, the postfix version of the operator is defined as if it takes a (dummy) parameter of type int. In the following example you can see how to overload unary operators, and in particular how to define both the prefix and postfix version of the ++ operator.
#include
class Point { double x,y; public: Point(double a = 0, double b = 0) { x = a; y = b; } Point &operator++ () { x++; y++; return *this; } // prefix Point operator++ (int) { Point r(x,y); x++; y++; return r; } // postfix Point operator- () const { Point r(-x,-y); return r; } operator double() const { return x; } // casting void print(ostream &os) const { os << "(" << x << "," << y << ")"; } };
ostream &operator<< (ostream &os, const Point &p) {
p.print(os); return os; }
int main() { Point p(0,0); cout << p++ << endl; // ’(0,0)’ // postfix cout << p << endl; // ’(1,1)’
cout << ++p << endl; // ’(2,2)’ // prefix cout << p << endl; // ’(2,2)’
cout << -p << endl; // ’(-2,-2)’ // always prefix cout << p << endl;
cout << (double) p << endl; // ’2’ return 0; }
Note also the example for overloading the type casting operator, in the example above for the casting to type double.
The () function call operator allows you to define functions as freely as you can outside classes. The function call operator can take any number of parameters and return any type you want. Here are two simple examples.
#include
class Point { double x,y; public: Point(double a = 0, double b = 0) { x = a; y = b; } void operator() (double x) const { cout << "Function called with double x=" << x << "." << endl; } int operator() () const { cout << "Function called with ()." << endl; return 1; } };
int main() { Point p; p(2.0); // ’Function called with double x=2.’ int i = p(); // ’Function called with ().’ return 0; }
It is also possible to overload an operator relative to a class by using a (global) friend rather than a member function. In this case, a binary operator has to be passed both operands explicitly, while a unary operator has to be passed the single operand. Friend operator functions are usually used when defining operations involving built-in types, where the built-in type is on the left hand side of the operator. Hence the call to the operator function cannot be generated by an object of your newly defined class. The most common example is to overload the inserter operator <<, where usually an object of type ostream is on the left hand side of the operator. See e.g. the following example.
#include
To allow for private members in the base class that will be private and accessible in the derived class, the access specifier protected is provided. It is equivalent to the private specifier with the sole exception that protected members of a base class are accessible to members of any class derived from that base. Hence the full general form of a class declaration looks something like this:
class class_name { // private members protected: // optional // protected members public: // public members }
How members of the base class can be accessed in the derived class is shown in the following table.
access level base class members public protected private : public public protected inaccessible : protected protected protected inaccessible : private private private inaccessible
Table 1: Accessibility of base class members in derived class.
When a base and derived class both have constructor and destructor functions, the constructor functions are executed in order of derivation. The destructor functions are executed in reverse order. I.e. the base class constructor is executed before the constructor in the derived class. The reverse is true for destructor functions. Furthermore, it is possible for the derived class constructor to pass arguments to the base class constructor. This is done in the following fashion.
class base { protected: int a; public: base(int i) { a = i; } void print() const { cout << a << endl; } };
class sub : public base { int b; public: sub(int i, int j) : base(i) { b = j; } // calls base constructor void print() const { cout << a << "," << b << endl; } };
Note that data members that are inherited from the base class will always be initialized through the base class constructor. This guarantees that even data that is inaccessible in the derived class (cf. §4.1) is properly initialized when an object of the derived class is created.
In C++, a pointer declared as a pointer to a base class can also be used to point to any class derived from that base class. For example, with the above definitions of base and sub, the following statements are correct:
base *p; // base class pointer base b(1); sub s(1,2);
p = &b; // p points to base object p = &s; // p points to derived object
Although you can use a base pointer to point to a derived object, you can access only those members of the derived object, that were inherited from the base class. This is because the pointer has knowledge only of the base class. It knows nothing about the members changed or added by the derived class. Note that a pointer to a derived class cannot be used to access an object of the base class. Finally, everything mentioned here about pointers to derived classes also holds true for passing arguments by reference. I.e. a function that expects a reference to a base class object can also be passed a reference to a derived class object. The following example illustrates that.
void show(const base &b); // reference to base class
int main() { sub s(1,2); show(s); // pass reference to derived object // etc }
Virtual functions implement the “one interface, multiple methods” philosophy that underlies polymor- phism. A virtual function is a class member function that is declared within a base class and redefined by a derived class. The virtual function within the base class defines the form of the interface to that function. Each redefinition of the virtual function by the derived class implements its operation as it relates specifically to the derived class and hence creates a specific method. In order to declare a function virtual, precede its declaration in the base class with the keyword virtual. What makes a virtual function special and interesting is that it supports so called run-time polymorphism. That is, when a base class pointer (or a base class reference) points to a derived object that contains a virtual function and that virtual function is called by using that pointer, C++^ determines which version of that function to call based upon the type of the object that is pointed to. This idea is best illustrated with an example.
#include
class base { public: virtual void print() const { cout << "base" << endl; } // virtual! };
class sub : public base { public: void print() const { cout << "sub" << endl; } };
class subsub : public sub { public: void print() const { cout << "subsub" << endl; } };
int main() { base b; sub s; subsub ss; b.print(); s.print(); ss.print(); // "base", "sub", "subsub"
base *p[3]; // pointers to base class p[0] = &ss; p[1] = &s; p[2] = &b; for (int i = 0; i < 3; i++) p[i]->print(); // "subsub", "sub", "base" return 0; }
Observe that without declaring the print() member function in the base class as virtual, the last three outputs would all read "base". Only by making the function virtual do we force the compiler to select