Lecture 3. UML Diagrams, Object-Oriented Design
Date: 2023-05-09
UML Diagrams
There are a lot of YouTube videos on UML diagrams. They do a much better job of explaining the concepts than I can.
1. Four Pillars of OOP
Encapsulation: Keeping data safe
Encapsulation ensures that the internal representation of an object is hidden from the outside. It can be achieved using access specifiers.
Code Example:
class Dog {
private:
int age; // Private member, can't be accessed outside the class
public:
// Getter
int getAge() {
return age;
}
// Setter
void setAge(int a) {
if(a > 0) {
age = a;
} else {
age = 0; // Default value if provided age is negative
}
}
};
int main() {
Dog myDog;
myDog.setAge(5);
int dogAge = myDog.getAge(); // Gets the age using public method
}
Abstraction: Hiding complex reality while exposing only necessary parts
Abstraction is about hiding the complex implementation and showing only the necessary features of an object.
Code Example:
abstract class Shape {
public:
virtual double area() = 0; // Pure virtual function
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14 * radius * radius;
}
};
Inheritance: Deriving a new class from an existing one
Inheritance allows a new class to inherit properties and behaviors from an existing class.
Code Example:
class Animal {
public:
void eat() {
cout << "This animal eats food." << endl;
}
};
class Bird : public Animal {
public:
void fly() {
cout << "This bird can fly." << endl;
}
};
int main() {
Bird myBird;
myBird.eat();
myBird.fly();
}
Polymorphism: Using one interface for different data types
Polymorphism allows objects of different classes to be treated as objects of a common superclass.
Compile-time polymorphism (Function & Operator Overloading)
class Box {
public:
int length;
Box() : length(0) {}
Box(int l) : length(l) {}
// Operator Overloading
Box operator + (const Box& b) {
Box box;
box.length = this->length + b.length;
return box;
}
};
int main() {
Box box1(10), box2(20);
Box box3 = box1 + box2; // Uses overloaded + operator
}
Run-time polymorphism (Function Overriding using virtual functions)
class Animal {
public:
virtual void sound() {
cout << "Animal makes a sound." << endl;
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "Dog barks." << endl;
}
};
int main() {
Animal* animal;
Dog doggy;
animal = &doggy;
animal->sound(); // Outputs "Dog barks."
}
2. Friend in C++: Functions, Classes, and More
The friend
keyword in C++ is used to grant non-member functions or other classes access to private and protected members of a class. This mechanism allows you to break the encapsulation in a controlled way.
Friend Functions
A friend
function isn't a member of the class but can access its private and protected members. It's defined outside the scope of the class but needs to be declared inside the class with the friend
keyword.
Example:
class Circle {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// Declare addArea function as a friend of Circle
friend double addArea(const Circle& c1, const Circle& c2);
};
double addArea(const Circle& c1, const Circle& c2) {
return 3.14 * (c1.radius + c2.radius) * (c1.radius + c2.radius);
}
Friend Classes
A friend
class can access the private and protected members of the class in which it's declared as a friend.
Example:
class Box {
private:
int width;
public:
Box(int w) : width(w) {}
// Declare Container as a friend class of Box
friend class Container;
};
class Container {
private:
Box box1;
Box box2;
public:
Container(int w1, int w2) : box1(w1), box2(w2) {}
int totalWidth() {
return box1.width + box2.width; // Can access private member of Box
}
};
Friend Member Functions
You can also specify that only certain member functions of another class are friends of a class.
Example:
class Rectangle {
private:
int length;
public:
Rectangle(int l) : length(l) {}
// Only the specific member function of Container is a friend
friend int Container::totalLength();
};
class Container {
private:
Rectangle rect1;
Rectangle rect2;
public:
Container(int l1, int l2) : rect1(l1), rect2(l2) {}
int totalLength() {
return rect1.length + rect2.length; // Can access private member of Rectangle
}
};
Why Use friend
?
While friend
can seemingly violate the principles of OOP by breaking encapsulation, there are use cases where it's beneficial:
- Operator Overloading: For some operators like
<<
or>>
, it's often necessary to have access to private members of the class without making the function a member itself. - Utility Functions: When a utility function needs access to multiple classes' private members but doesn't logically belong to any one class.
- Boosting Efficiency: In some cases, granting direct access to a class's internals can lead to more efficient code.
However, it's crucial to use friend
judiciously. Overusing it can lead to maintenance issues and can compromise the robustness of your code. Always ask yourself if there's a way to achieve what you want without breaking encapsulation.
3. Type Casting & Inheritance: Upcasting, Downcasting, and Slicing
In C++, inheritance introduces a hierarchy of classes. Often, there's a need to move up and down this hierarchy, converting base class pointers/references to derived class pointers/references and vice versa. However, this type of conversion, if done incorrectly, can lead to issues like object slicing.
Upcasting
Definition: Upcasting is the process of converting a pointer/reference of a derived class to a pointer/reference of its base class.
It's a safe operation, as every derived class object is an instance of its base class. Upcasting usually happens implicitly.
Real-world Example: Consider a Vehicle
class and a derived class Car
. Every car is a vehicle, but not every vehicle is a car.
class Vehicle {
public:
virtual void start() {
cout << "Starting vehicle..." << endl;
}
};
class Car : public Vehicle {
public:
void start() override {
cout << "Starting car..." << endl;
}
};
void ShowStart(Vehicle* v) {
v->start();
}
int main() {
Car myCar;
ShowStart(&myCar); // Implicit upcasting to Vehicle*
}
Downcasting
Definition: Downcasting is converting a base class pointer/reference to a derived class pointer/reference. It's not safe unless you're sure about the actual type of the object the base pointer/reference points to. To safely downcast, use dynamic_cast
which checks the type at runtime.
Real-world Example: Consider a zoo application. Every animal can make a sound, but only a bird can fly. To let a bird fly from a collection of animals, you'd need to downcast.
class Animal {
public:
virtual void sound() {
cout << "Animal makes sound" << endl;
}
};
class Bird : public Animal {
public:
void sound() override {
cout << "Bird chirps" << endl;
}
void fly() {
cout << "Bird flies" << endl;
}
};
void MakeSoundAndFly(Animal* a) {
a->sound();
Bird* maybeBird = dynamic_cast<Bird*>(a);
if(maybeBird) {
maybeBird->fly();
}
}
int main() {
Bird tweety;
MakeSoundAndFly(&tweety); // Downcasting to Bird*
}
Object Slicing
Definition: Object slicing happens when a derived class object is assigned to a base class object. The derived class attributes are "sliced off", and we're left with only the base class parts.
Real-world Example: Consider a Person
class with derived Student
class. If you tried to store a Student
as a Person
, you'd lose the student-specific attributes.
class Person {
public:
string name;
};
class Student : public Person {
public:
int rollNo;
};
void Display(Person p) {
cout << "Name: " << p.name << endl;
// p.rollNo is not accessible here
}
int main() {
Student s;
s.name = "Alice";
s.rollNo = 101;
Display(s); // Object slicing: Only the name will be displayed
}
4. Advanced Polymorphism: Covariant Return Types & Dynamic Binding
Polymorphism, in its essence, allows entities of different types to be treated as if they are of the same type. Beyond the basics, there are more nuanced ways to employ polymorphism in C++.
Covariant Return Types
Definition: In C++, when overriding a function in a derived class, the return type of the derived class's function can be a pointer or reference to a class derived from the return type of the base class's function. This feature is known as covariant return types.
Example:
class Animal {
public:
virtual Animal* getClone() {
return new Animal();
}
};
class Dog : public Animal {
public:
// Covariant Return Type: returns Dog* instead of Animal*
Dog* getClone() override {
return new Dog();
}
};
int main() {
Dog d;
Dog* clonedDog = d.getClone();
delete clonedDog;
}
In the example above, getClone()
in the Dog
class returns a Dog*
, which is a covariant return type as Dog
is derived from Animal
.
Dynamic Binding
Definition: Dynamic binding (or late binding) is a mechanism by which a call to an overridden function is resolved at runtime, based on the object's runtime type, rather than the variable's compile-time type. It's achieved using virtual functions in C++.
Example:
class Base {
public:
virtual void show() {
cout << "In Base" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "In Derived" << endl;
}
};
int main() {
Base* ptr;
Derived d;
ptr = &d;
// Function call is resolved at runtime
ptr->show(); // Output: In Derived
}
In the example, the show
method is declared as virtual
in the base class, so the call to ptr->show()
is resolved at runtime based on the actual object type (which is Derived
).
Why Use Advanced Polymorphism Techniques?
-
Flexibility: Covariant return types allow for more specific and meaningful return types in derived classes, enhancing code clarity.
-
Runtime Decisions: Dynamic binding permits decisions about which function to call to be made at runtime based on the actual type of the object. This enables more dynamic and flexible code behavior.
-
Code Reusability: These techniques further the goal of code reusability and abstraction in object-oriented programming.
5. Other Advanced Topics: Multiple Inheritance, Templates, Exception Handling
Multiple Inheritance
Definition: Multiple inheritance is a feature where a class can inherit from more than one classes. This introduces a potential for the "diamond problem" due to which certain precautions and usage of virtual inheritance may be necessary.
Example:
class Base {
public:
void sayHello() {
cout << "Hello from Base" << endl;
}
};
class Derived1 : public Base {
public:
void sayFromDerived1() {
cout << "Hello from Derived1" << endl;
}
};
class Derived2 : public Base {
public:
void sayFromDerived2() {
cout << "Hello from Derived2" << endl;
}
};
class FinalDerived : public Derived1, public Derived2 {};
int main() {
FinalDerived obj;
// obj.sayHello(); This would be ambiguous without precautions.
}
The above code would lead to ambiguity if you tried to call sayHello()
on FinalDerived
object. It's unclear which path to take – through Derived1
or Derived2
. To resolve such issues, C++ offers virtual
inheritance.
Templates
Definition: Templates allow functions and classes to operate with generic types. This way, a single function or class can work on different data types without being rewritten for each one.
Example:
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add<int>(10, 20) << endl; // Output: 30
cout << add<double>(10.5, 20.5) << endl; // Output: 31
}
Templates can be incredibly powerful and can be used for both functions and classes.
Exception Handling
Definition: Exception handling provides a way to handle runtime errors in a graceful manner. The main keywords used are try
, catch
, and throw
.
Example:
void mightGoWrong() {
bool errorOccurred = true; // for illustration purpose
if (errorOccurred) {
throw runtime_error("Something went wrong!");
}
}
int main() {
try {
mightGoWrong();
}
catch (runtime_error &e) {
cout << "Caught an exception: " << e.what() << endl;
}
}
This mechanism allows you to "throw" exceptions when something goes wrong and "catch" them in a controlled way, ensuring that your program can fail gracefully instead of crashing unexpectedly.
Wrap-Up
-
Multiple Inheritance provides the ability for a class to inherit from more than one base class. However, it needs to be used with caution, especially due to potential ambiguities.
-
Templates allow for type-agnostic programming, ensuring that functions or classes can operate on different data types without being re-written.
-
Exception Handling is vital for robust software development, ensuring that runtime errors are gracefully managed, offering potential recovery or at least meaningful error messages.
6. Q&A
1. Class Design & Relationships: Encapsulation
Q: What is the primary purpose of making class members private
and how is access to them typically granted in a controlled manner?
A: The primary purpose is encapsulation, which helps in bundling the data (attributes) and the methods (functions) that operate on the data into a single unit, and restricting direct access to some of the object's components. Controlled access to these private
members is typically granted using public
methods known as getters (for retrieval) and setters (for modification).
Example Code:
class Circle {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getRadius() const { return radius; }
void setRadius(double r) { radius = r; }
};
2. Type Casting & Inheritance: Downcasting
Q: When might you use dynamic_cast
and why?
A: dynamic_cast
is used for downcasting (casting from base pointer/reference to derived pointer/reference). It's employed when you're uncertain about the type of the object, but want to try casting it. The primary benefit is that it's safe; if the cast fails, it returns a nullptr
(for pointers) or throws an exception (for references).
Example Code:
class Base {};
class Derived : public Base {};
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // Safe downcasting
3. Advanced Polymorphism: Dynamic Binding
Q: Explain dynamic binding and provide an example where it might be useful.
A: Dynamic binding, or late binding, means that the code associated with a given function call is not decided until the time of the call at runtime. It's typically used with polymorphism, where base pointers or references can point to derived objects.
Example Code:
class Shape {
public:
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing a circle!" << endl;
}
};
void render(const Shape& shape) {
shape.draw(); // Dynamic binding in action
}
int main() {
Circle c;
render(c);
}
4. Memory & Object Management: Smart Pointers
Q: What is a shared_ptr
in C++ and why might you use it?
A: A shared_ptr
is a type of smart pointer in the C++ Standard Library that retains shared ownership of an object. Multiple shared_ptr
objects can own the same object, ensuring it doesn't get deleted until the last shared_ptr
that points to it is destroyed or reset.
Example Code:
std::shared_ptr<int> p1 = std::make_shared<int>(5);
std::shared_ptr<int> p2 = p1; // p1 and p2 both share ownership
5. Other Advanced Topics: Exception Handling
Q: How does the try-catch
mechanism work in C++?
A: The try-catch
mechanism allows for exceptions to be thrown and caught in a controlled way. Code that might throw an exception is put inside a try
block. The catch
block(s) following the try
block catch and handle exceptions of the specified type.
Example Code:
try {
// Code that might throw an exception
throw std::runtime_error("A problem occurred!");
}
catch (const std::runtime_error &e) {
std::cout << "Caught an exception: " << e.what() << std::endl;
}
6. Class Design & Relationships: Inheritance
Q: How do base and derived classes relate, and when might you use protected
access?
A: Inheritance allows a class (derived class) to inherit properties and methods from another class (base class). The protected
access specifier ensures that members are accessible in the derived classes but not outside the classes. It's often used when you want the derived class to have more direct access to certain properties or methods without exposing them to the outside world.
Example Code:
class Animal {
protected:
int age;
public:
void setAge(int a) { age = a; }
};
class Dog : public Animal {
public:
void barkYears() {
cout << "Bark for " << age << " years!" << endl;
}
};
7. Type Casting & Inheritance: Upcasting
Q: What is upcasting and is it safe in C++?
A: Upcasting is the process of converting a derived-class reference or pointer to a base-class. In C++, upcasting is always safe because a derived class object is also a base class object.
Example Code:
class Base {};
class Derived : public Base {};
Derived d;
Base* b = &d; // Upcasting
8. Advanced Polymorphism: Covariant Return Types
Q: What is the primary advantage of using covariant return types in overridden methods?
A: Covariant return types allow a derived class's overridden method to have a return type that is a derived type of the base class's method return type. This provides more specific and meaningful return types, enhancing code clarity.
Example Code:
class Animal {
public:
virtual Animal* clone() { return new Animal(); }
};
class Cat : public Animal {
public:
Cat* clone() override { return new Cat(); }
};
9. Memory & Object Management: Slicing
Q: What is object slicing in C++ and how can you prevent it?
A: Object slicing occurs when a derived class object is assigned to a base class object, causing the derived class-specific attributes and behaviors to be "sliced off". To prevent slicing, always use pointers or references when working with polymorphism.
Example Code:
class Base {
public:
int baseValue;
};
class Derived : public Base {
public:
int derivedValue;
};
void doSomething(Base b) { /* ... */ }
int main() {
Derived d;
doSomething(d); // Slicing happens here!
}
10. Other Advanced Topics: Templates
Q: How can you define a generic data structure in C++ that works with different data types?
A: C++ offers templates which allow you to define a blueprint for a data structure or function that can then work with various data types without needing a separate version for each type.
Example Code:
template <typename T>
class Box {
private:
T content;
public:
Box(T c) : content(c) {}
T getContent() { return content; }
};
int main() {
Box<int> intBox(10);
Box<string> strBox("Hello");
}
Sure thing! Let's explore more questions and answers around those topics:
11. Class Design & Relationships: Polymorphism
Q: How does polymorphism improve software design, and what's the connection with virtual functions?
A: Polymorphism enables objects of different classes to be treated as objects of a common super class. With virtual functions, this allows different classes to provide specific implementations for those functions, making the code more modular and flexible.
Example Code:
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing Circle!" << endl;
}
};
class Square : public Shape {
public:
void draw() override {
cout << "Drawing Square!" << endl;
}
};
void render(Shape* s) {
s->draw();
}
12. Type Casting & Inheritance: static_cast
Q: When might you use static_cast
?
A: static_cast
is used for conversions between related types, like converting a float
to an int
or casting pointers within an inheritance hierarchy (when you're certain about the nature of the object). However, unlike dynamic_cast
, it performs no runtime checks.
Example Code:
float f = 5.6;
int i = static_cast<int>(f); // i will be 5
13. Advanced Polymorphism: Function Overloading
Q: How does function overloading relate to polymorphism?
A: Function overloading is a type of compile-time polymorphism where two or more functions in the same scope have the same name but different parameters. The correct function to call is determined at compile time based on the function signature.
Example Code:
void print(int i) {
cout << "Printing int: " << i << endl;
}
void print(double d) {
cout << "Printing double: " << d << endl;
}
14. Memory & Object Management: Raw Pointers
Q: Why is it recommended to use smart pointers over raw pointers in modern C++?
A: Raw pointers can lead to memory leaks if not managed properly. Smart pointers, like unique_ptr
or shared_ptr
, automatically manage the memory they own, ensuring proper cleanup when they're no longer needed. This reduces the risk of memory leaks and simplifies memory management.
Example Code:
std::unique_ptr<int> ptr(new int(5));
// No need to delete, it's handled automatically when ptr goes out of scope
15. Other Advanced Topics: Multiple Inheritance
Q: What's a potential drawback of using multiple inheritance in C++?
A: Multiple inheritance, where a class can inherit from more than one base class, can lead to the "diamond problem". This problem occurs when a class inherits from two classes that have a common base class. This can cause ambiguity in method resolution and data member access. It's often recommended to use interfaces (abstract classes with only pure virtual functions) or composition over multiple inheritance to avoid these issues.
Example Code:
class A { public: void foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {}; // Diamond problem, D has two paths to A