COMP282 (Advanced Object-Oriented C Languages)
Model-View-Controller
This is a design pattern that splits a user interface into three parts:
- Model - The internal state and data of the application.
- View - The GUI elements and how they appear to the user.
- Controller - Create events that modify the internal state of the application.
MVC Advantages
- Different parts can be done by different people.
- High cohesion.
- Low coupling
- Easier to maintain.
- Greater code reuse.
MVC Disadvantages
- Mode code and higher levels of abstraction.
- Easier to forget to do a part because the parts are at different places.
Factories
Consider that you have a set of classes and you want to create one depending on an input.
- We have a set of animal classes and want to create an animal.
We can use the following as a solution:
- Give the classes a common interface.
- Create a new class named
<ClassName>Factory
.
- This class has a single method,
get<ClassName>()
, that takes a class and produces a new instance of that class.
Decorators
This pattern can solve the following problems:
- You want to add a responsibility to an object at run time.
- You want to do sub-classes but with multiple choice.
We can use the following as a solution:
- Create an interface for the class
<ClassName>
and call it I<Classname>
.
- Create an
<ClassName>Decorator
class, implementing I<ClassName>
and having an I<ClassName>
field.
- The constructor takes an
I<ClassName>
element and puts it into the field.
- All methods are just called on that field.
- We can then create subclasses of
<ClassName>Decorator
that will overwrite methods and then do some operations before calling methods on the field.
We can implement this in the following example:
- Consider that we are making a text editor that has a
Text
object. We can implement an IText
interface that allows us to define a draw
method.
- We can make a class
TextDecorator
that implements IText
with an IText
field.
TextDecorator
has an IText
field and takes IText
in the constructor.
-
We can then create subclasses of TextDecorator
and use them like so:
TextFirst(new TextUpper(text));
Rule of Three
This rule states that we should wait until we have three of something before we generalise using a design pattern.
- This saves us putting in extra effort in generalising when we have no need, or too many classes.
C# is a fully managed language (like Java). Memory is garbage collected and the compiler handles the allocation of resources automatically.
Hello, world!
A hello world program would look something like so:
using System;
class Hello {
static void Main(string[] args) {
Console.WriteLine("Hello World!");
}
}
Getters & Setters in C#
Getters and setters are used like so in C#:
class Person {
private string _name;
public string Name {
get {
return _name;
}
set {
_name = value;
}
}
}
class Program {
public static void Main(string [] args) {
Person p = new Person();
p.Name = "Charles";
System.Console.WriteLine(p.Name);
}
}
It is convention that private variables start with _
.
private
Getters & Setters
Using one of these outside of the scope of the class will result in a compilation error:
class Person {
private string _name;
public string Name {
private get {
return _name;
}
private set {
_name = value;
}
}
}
Interfaces in C#
C# has interfaces like in Java. We can make and use them like so:
using System;
class Program : IBla {
static void Main(string[] args) {
Console.WriteLine("Hello World!");
}
}
interface IBla {
bool test();
}
Inheritance in C#
Inheritance in C# is similar to in C++:
using System;
class Program : Bla {
static void Main(string[] args) {
Console.WriteLine("Hello World!");
}
}
class Bla {
public bool test() {
return true;
}
}
Constructors in C#
Constructors look like so in C#:
using System;
class Program : Bla {
public Program(int b) : base(b) {}
public Program() : this(0) {}
}
class Bla {
public Bla(int a) {}
}
base
is used to refer to the class we inherit (Bla
).
this
is used to refer to the current class (Program
).
Inheritance & Interfaces in C#
We can declare both interfaces and inheritance at once:
using System;
class Program : Bla, IBla {
static void Main(string[] args) {
Console.WriteLine("Hello World!");
}
}
interface IBla {
bool test();
}
class Bla {
public bool test() {
return true;
}
}
The class must be listed before any interfaces.
Polymorphism in C#
Polymorphism is similar to C++:
using System;
class Program : Bla {
public override bool test() {
return false;
}
}
class Bla {
public virtual bool test() {
return true;
}
}
Our methods can be set in three ways:
- Using both
virtual
and override
:
- This will override as expected.
- Without
virtual
or override
:
- This will take the function of the parent.
- With both
abstract
:
- This works similarly to in Java.
Access Modifiers in C#
We have the standard access modifiers:
We also have access modifiers that are used to inform that creation of .dll
files:
internal
- Only from the same compilation.
- If you compile to a
.dll
or .exe
file, you can only access these from .cs
files used to compile it.
protected
or internal
- From the same compilation or from children.
protected internal
- From children in the same compilation.
Overloading in C#
Generally people don’t overload operators - instead of overloading +
we make use add()
. This will be shown anyway.
Requirements:
- Operator overloading must be defined in a class that defines one of the parameters.
- It must be
public
and static
(meaning call-able from outside an object).
using System;
class Fraction {
int n;
int d;
public Fraction(int n, int d) {
this.n = n;
this.d = d;
}
public static Fraction operator +(Fraction a) {
return a;
}
public static Fraction operator +(Fraction a, Fraction b) {
return new Fraction(a.d*b.n + b.d*a.n,a.d*b.d);
}
}
If we overload true
we must also overload false
.
Templates (Generics) in C#
We can use generics like so:
class Pair<T,U> {
public T first { get; private set; }
public U second { get; private set; }
public Pair(T t, U u) {
first = t;
second = u;
}
}
File $\rightarrow$ Class Correspondence
In C# you can have any number of files per class and any number of classes per file.
It is preferred to still have one class per file.
To use multiple flies for one class, you need to add the keyword partial
in front of class
.
Types
All types in C# are objects we can use int
and string
as shorthand for their full class name.
You can see the full list of types here: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types.
STL in C#
In C# we have many collection types:
Array
List
SortedDictionary
SortedSet
LinkedList
foreach
We can iterate over all the items in a collection with the following structure:
LINQ
LINQ is a query language in C#. We can use is like so:
using System;
using System.Linq;
using System.Collections.Generic;
class Program {
public static void Main(string[] args) {
string[] names = { "Birgit", "Brian", "Victor", "Finn", "Denise" };
IEnumerable<string> bnames = //query variable
from name in names //required
where name.Substring(0,1)=="B" // optional
orderby name descending // optional
select name; //must end with select or group
foreach (var name in bnames) Console.WriteLine(name);
}
}
Max()
in C#
We can calculate a maximum value using LINQ:
using System;
using System.Linq;
using System.Collections.Generic;
class Program {
public static void Main(string[] args) {
string[] names = { "Birgit", "Brian", "Victor", "Finn", "Denise" };
Console.WriteLine(names.Max());
}
}
Distinct()
in C#
using System;
using System.Linq;
using System.Collections.Generic;
class Program {
public static void Main(string[] args) {
string[] names = { "Birgit", "Brian", "Victor", "Finn", "Denise" };
foreach (var name in names.Distinct()) Console.WriteLine(name);
}
}
Shuffle with orderby
We can implement a shuffle by sorting at random:
using System;
using System.Linq;
using System.Collections.Generic;
class Program {
public static void Main(string[] args) {
string[] names = { "Birgit", "Brian", "Victor", "Finn", "Denise" };
Random rng = new Random();
IEnumerable<string> scoreQuery = //query variable
from name in names //required
orderby rng.Next() // optional
select name; //must end with select or group
foreach (var testScore in scoreQuery) Console.WriteLine(testScore);
}
}
There are the following types of algorithms:
- Min/Max:
- Sorting:
- Non-modifying Sequence:
all_of
, any_of
, none_of
, find_if
, count_if
- Modifying Sequence:
copy
, shuffle
, unique
, reverse
- Misc
- Helper functions for searches, sorts and tree structures.
min
We can write our own simple algorithm like so:
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
bool compare(int i1, int i2) {
return i1 < i2;
}
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++) {
arr.push_back(i);
}
cout << *min_element(arr.begin(), arr.end(), compare);
}
This functionality is also available in functional
:
#include <vector>
#include <algorithm>
#include <funcitonal>
#include <iostream>
using namespace std;
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++) {
arr.push_back(i);
}
cout << *min_element(arr.begin(), arr.end(), less<int>());
}
Functors
Functors are classes with ()
overloaded, like less
. We can use them instead of pointers.
Comparator.hpp:
class Comparator {
int c;
public:
Comparator(int c) : c(c) {}
bool operator()(int i1, int i2);
}
Comparator.cpp:
#include "Comparator.h"
#include <cmath>
bool Comparator::operator()(int i1, int i2) {
if (std::abs(c - i1) < std::abs(c - i2)) {
return true;
}
return false;
}
We can then use the comparator in our min
program:
#include <vector>
#include <algorithm>
#include <funcitonal>
#include <iostream>
using namespace std;
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++) {
arr.push_back(i);
}
int c;
cin >> c;
cout << *min_element(arr.begin(), arr.end(), Comparator(c));
}
sort
& stable_sort
#include <vector>
#include <algorithm>
#include <iostream>
#include Comparator.h
using namespace std;
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++ ) {
arr.push_back(i);
}
int c;
cin >> c;
sort(arr.begin(), arr.end(), Comparator(c));
for (auto x : arr) {
cout << x << " ";
}
}
We can replace sort
with stable_sort
if we want to sort stably.
shuffle
#include <vector>
#include <algorithm>
#include <iostream>
#include <random>
using namespace std;
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++ ) {
arr.push_back(i);
}
unsigned seed;
cin >> seed; // we can also set the seed using the time
shuffle(arr.begin(), arr.end(), default_random_engine(seed));
for (auto x : arr) {
cout << x << " ";
}
}
unique
This algorithm uses ==
to remove duplicates:
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++ ) {
arr.push_back(1);
}
int c;
cin >> c;
unique(arr.begin(), arr.end());
for (auto x : arr) {
cout << x << " ";
}
}
copy
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
vector<int> arr;
for (int i = 0; i < 100; i++ ) {
arr.push_back(i);
}
vector<int> arr2;
for (int i = 0; i < 100; i++ ) {
arr2.push_back(-i);
}
move(arr.begin() + 5, arr.end(), arr2.begin() + 5);
for (auto x : arr2) {
cout << x << " ";
}
cout << endl;
cout << arr.size();
for (auto x : arr) {
cout << x << " ";
}
}
Standard Template Library
Each template consists of three main parts:
- Containers - Standard data structures.
- Iterators - Standard ways to move through the standard data structures.
- Algorithms - Use iterators to complete a task with data.
Containers
An example of a container is a vector
this is how you would initialise a vector
:
#include <vector>
using namespace std;
int main() {
vector<int> store;
}
A vector is an automatically sizing array.
You can use the algorithms of a container using dot noation: store.back()
.
Iterators
Iterators point to an object in an array, or container. They can iterate through the objects in a logical way, by using overloaded operators such as ++
.
See https://www.cplusplus.com/reference/iterator/ for the properties of each iterator category. We can use the functions at the bottom of the page to control the iterator.
If a type is well-defined then we can replace the standard iterator declaration:
multiset<int>::iterator it = store.begin();
with either of the following:
auto it = store.begin();
for (int x : store) {
cout << x;
}
for (auto x : store) {
cout << x;
}
STL Arrays
There is also an array
container, in C++, that enable the use of iterators and algorithms:
#include <array>
#include <iostream>
using namespace std;
int main() {
array<long, 5> arr;
arr.fill(1); // all elements are 1
for (auto x : arr) {
cout << x;
}
}
Overloading
Types of Operators
There are three types of operator in C++:
- Prefix Unary -
--counter
- Postfix Unary -
counter++
- Binary -
num1 - num2
Unary Operators
To overload prefix unary operators we write:
This will override -
.
To overload postfix unary operators we write:
returntype operator++(int);
The int
type only means that we are defining the postfix version instead of the prefix one.
Binary Operators
To overload a binary operator we can do the following:
returntype operator+(righthandtype& r);
Friends
A friends declaration appears in a class body and grants a function, or another class, access to private and protected members of the class where the friend declaration appears.
Friendship is only one-way and is not inherited.
Redefining <<
for cout
To print our ruler objects we need to override <<
from ostream
so that it knows how to output the type.
To start define the function in the class:
friend std::ostream& operator<<(std:ostream& output, const Ruler& ru);
We can then define that function elsewhere:
std::ostream& operator<<(std::ostream& output, const Ruler& ru) {
output << ru.feet << "'" << ru.inches << '"';
return output;
}
We can do a similar thing for overriding >>
.
There will be additional examples of this in the labs.
Templates
Consider that we want to write a function to compare many different types of classes. Instead of writing many functions to do this, we can write one function to rule them all:
template <class T>
bool compare(T& t1, T& t2) {
return t1 < t2;
}
We can then use this template like so:
compare<Ruler>(r1, r2);
compare<string>(s1, s2);
You can also make templates that take multiple classes:
template <class T, class U>
bool compare(T& t1, U& u2) {
return t1 < u2;
}
Class Templates
To return two different objects from a single function we can use a class template:
template <class T, class U>
class TwoObjects {
public
T first;
U second;
TwoObjects(T f, U s) : first(f), second(s) {}
}
TwoObjects<Animal, int> method();
This is implemented by Pair
in the standard template library.
Inheritance
- c++ has access modifiers on inheritance.
- There are no interfaces like in Java.
Basic inheritance has the following syntax:
class Dog : public Animal();
which represents the following:
classDiagram
Animal <|-- Dog
The access modifiers change the access modifiers of inherited methods etc. public
makes no change.
Polymorphism
#include <iostream>
using namespace std;
class Animal {
public:
void makeNoise() { cout << "Grunt"; }
};
class Dog : public Animal {
public:
void makeNoise() { cout << "Woof"; }
};
int main()
{
Dog rover;
Animal* pet = &rover;
pet->makeNoise();
}
This will make the Grunt
noise of Animal
.
Virtual Polymorphism
We can override methods properly like so:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void makeNoise() { cout << "Grunt"; }
};
class Dog : public Animal {
public:
void makeNoise() { cout << "Woof"; }
};
The keyword here is virtual
.
Initialisation
Memory for the parent class is always initialised before the memory before the child.
To pass data from the child to the parent constructor, we would do the following:
#include <iostream>
using namespace std;
class Animal {
protected:
int weight;
string name;
public:
Animal(int w, string n) : weight(w), name(n) {}
virtual void output() {
cout << name << " weighs " << weight << endl; }
};
class Dog : public Animal {
public:
Dog(int w, string n) : Animal(w,n) {}
};
int main() {
Dog rover(5, “Rover");
}
Slicing
We can turn a child object into it’s parent by doing the following:
class Cat : public Animal {
private:
int lives;
public:
Cat(int w, string n, int v) : Animal(w, n) {
lives = v;
}
void output() {
cout << name << " weighs " << weight << " and has ";
cout << lives << " lives" << endl;
}
};
int main() {
Cat fluffy(5, "Fluffy", 9);
Animal pet = fluffy;
pet.output();
}
This will remove any attributes of the child.
Accessing a Child Through the Parent
We can have a parent pointer that points to a child object like so:
int main() {
Cat grumpy(5, "Grumpy Cat", 9);
Animal* pet = &grumpy;
cout << ((Cat*)pet)->getLives(); // This will work fine
}
Passing Object Examples
We have the following methods ob passing objects to functions:
-
Directly by copying the object:
void eat(Animal toBeEaten) {
weight += toBeEaten.weight;
toBeEaten.weight = 0;
}
-
Passing a pointer (must use &
):
void eat(Animal* toBeEaten) {
weight += toBeEaten->weight;
toBeEaten->weight = 0;
}
-
Pass by reference:
void eat(Animal& toBeEaten) {
weight += toBeEaten.weight;
toBeEaten.weight = 0;
}
Permanent Objects
To keep objects once we leave their scope we can allocate memory on the heap with the following syntax:
Dog* myDog = new Dog(10, "Rover");
Cat* myCat = new Cat(5, "Grumpy", 9);
- This creates a new pointer where we will have to use
->
to access methods and so on.
To free the memory after we have finished with an object we can delete
it:
delete(myDog); // this calls the destructor for Dog
Overloading
#include <iostream>
#include "Animal.h"
#include "Dog.h"
using namespace std;
void name(Animal *a) {
cout << "Animal";
}
void name(Dog *d) {
cout << "Dog";
}
int main()
{
Dog rover;
Animal *pet = &rover;
name(rover);
name(pet);
}
Constructor Overloading
We can write the following header file:
class Ruler {
public://public right now to make it easier to demonstrate
int feet;
int inches;
public:
Ruler(int i);
Ruler(int f, int i);
};
and implement our constructors like so:
#include "Ruler.h"
Ruler::Ruler(int f, int i) {
feet = f + i / 12
inches = i % 12;
}
Ruler::Ruler(int i) : Ruler(0,i) {}
Operator Overloading
To add our new measurement with the +
operator we can overload the +
operator. We declare this as a member function of a class:
Ruler operator+(const Ruler& ru);
and implement this like so:
Ruler Ruler::operator+(const Ruler& ru) {
Ruler temp(this->feet + ru.feet, this->inches + ru.inches);
return temp;
}
We can then add Ruler
objects using +
.
We can also do a similar thing for equality operators.
The nice thing about C++ being compatible with C is that you don’t have to use classes if you don’t want to.
Classes
A basic example of a class looks like so:
#include <string>
class Animal {
int weight;
std::string name;
};
Remember to include the semicolon ;
at the end of classes.
Access Modifiers
We can use the following access modifiers:
public
- You can access it from anywhere.
private
- You can only access them from within the class or from friends.
protected
- You can only access them from this class, sub-classes or from friends.
The default is private
.
The syntax is like so:
#include <string>
class Animal {
public:
int weight;
std::string name;
};
Getters & Setters
#include <string>
#include <iostream>
using namespace std;
class Animal {
private:
int weight;
string name;
public:
void setWeight(int value) {
weight = value;
}
int getWeight(void) {
return weight;
}
};
int main() {
Animal example; // Creates the object
example.setWeight(55);
cout << example.getWeight();
}
We should put the following in header files:
- Class Definitions
- Constants
This will split our code like so:
Animal.h:
#include <string>
namespace ani {
class Animal {
private:
int weight;
std::string name;
public:
void setWeight(int value);
int getWeight(void);
};
}
You should not using namespace ...
in header files.
Animal.cpp
#include "Animal.h"
int ani::Animal::getWeight() {
return weight
}
void ani::Animal::setWeight(int value) {
weight = value;
}
main.cpp
#include <iostream>
#include "Animal.h"
using namespace ani;
using namespace std;
int main() {
Animal example;
cout << example.getWeight();
}
We can declare multiple namespaces (provided they don’t clash).
Constructors & Destructors
Constructors are used to allocate memory and initialise objects, whereas destructors free the memory. We can extend our header files to allow for this functionality:
Animal.h:
#include <string>
namespace ani {
class Animal {
private:
int weight;
std::string name;
public:
void setWeight(int value);
int getWeight(void);
Animal(int value);
~Animal();
};
}
Animal.cpp
#include "Animal.h"
int ani::Animal::getWeight() {
return weight
}
void ani::Animal::setWeight(int value) {
weight = value;
}
ani::Animal::Animal(int value) {
weight = value;
}
ani::Animal::~Animal() {} // You can skip this as this does the same as the default
Member Initialisation
To save us manually initialising variables in our constructors, we can use the following syntax:
ani::Animal::Animal(int value) : weight(value) {}
Hello, world!
C is also valid C++ code:
#include <stdio.h>
int main() {
printf("Hello World!\n");
}
but we can also use C++ features:
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
}
We can use C header files in C++ but there are often C++ versions of the same. For example, we can find the length of a string like so:
#include <string>
#include <iostream>
int main() {
std::string hello = "hello";
int x = hello.size();
std::cout << hello << " is of length " << x;
}
In C you would use strlen()
.
Namespaces
Namespaces are like packages, for example:
means that cout
is defined inn the std
namespace.
We can set a default namespace like so:
#include <string.h>
#include <iostream>
using namespace std;
int main() {
string hello = "hello";
int x = hello.length();
cout << hello << " is of length " << x;
}
This saves us writing std::
all the time.
We can use something like the following to read input:
#include <iostream>
int main() {
std::string bla;
int num;
std::cin >> num >> bla;
std::cout << bla << " " << num;
}
This can work with integers and strings, and reads up to the first space for each variable.