Lecture 11 - Scope, Friends, Destructors, Composition, and UML | CMSC 240 Software Systems Development - Spring 2024

Lecture 11 - Scope, Friends, Destructors, Composition, and UML

Live Code Link

Objectives

In this lecture, we will delve into key concepts of C++ and object-oriented design. We’ll explore the nuances of different scopes, the role of friends in access control, the importance of destructors in resource management, the principles of composition in system design, and the visualization power of UML diagrams.

Instructions

Complete the in-class exercises inside the module-6 repository here: https://classroom.github.com/a/9r7yX0xl

Lecture Topics

Scope

Scope refers to the region or portion of the code where a particular identifier (like a variable, function, class, etc.) is accessible and can be used. The scope of an identifier determines its visibility and lifespan. Scope is fundamental to understanding program organization, memory management, and encapsulation.

Managing scope helps you:

Remember, understanding and managing scope is key to writing effective C++ programs. It helps in structuring code logically and ensuring variables and functions are used appropriately.

Here are the main types of scopes:

Block Scope

Block scope refers to the region of the code where an identifier (like a variable) is defined and can be accessed. This region is limited to the block in which the identifier is declared. A block is generally a set of statements enclosed by curly braces { }.

Key points about block scope:

Example

In this example, the inner block creates its own variable height which hides the height variable from the outer block. Once the inner block ends, its height goes out of scope, and the outer height is accessible again.

block.cpp

#include <iostream>
using namespace std;

int main() 
{
    // The height variable has block scope within the main() function
    float height = 177.8;  

    cout << height << endl;  // Prints 177.8

    {
        // This height has block scope limited to this block 
        // and hides the outer height
        float height = 142.6;  

        cout << height << endl;  // Prints 142.6
    }

    // Prints 177.8 again, because we're referring to the outer height
    cout << height << endl;  

    
    // Note: The height that was set to 142.6 is 
    // now out of scope and can't be accessed

    return 0;
}

Understanding and making use of block scope is crucial for writing clean, efficient, and bug-free code. It allows for:

File Scope

File scope (also frequently referred to as global scope) refers to the visibility and lifespan of variables, functions, and other identifiers that are declared outside of any function or class, directly within a source file.

Here’s a breakdown of file scope:

Example

In this example, globalVar has file scope in file1.cpp, but it’s made available to file2.cpp using the extern keyword. However, fileScopedVar is strictly limited to file1.cpp due to the use of the static keyword, ensuring it won’t conflict with any other fileScopedVar in other source files.

file1.cpp

#include <iostream>
using namespace std;

// This variable has file scope in file1.cpp
int globalVar = 42;            

// This variable is strictly limited to file1.cpp due to 'static'
static int fileScopedVar = 10; 

void printVarsFile1() 
{
    cout << "In printVarsFile1" << endl;
    cout << "globalVar == " <<  globalVar << endl;
    cout << "fileScopedVar == " <<  fileScopedVar << endl;
}

// Defined in File2.
void printVarsFile2();

int main()
{
    printVarsFile1();
    printVarsFile2();

    cout << endl << "In main" << endl;
    cout << "globalVar == " <<  globalVar << endl;
    cout << "fileScopedVar == " <<  fileScopedVar << endl;
    return 0;
}

file2.cpp

#include <iostream>
using namespace std;

// Tells the compiler that globalVar is declared in another file
extern int globalVar;  

// This variable is strictly limited to file2.cpp due to 'static'
static int fileScopedVar = 123; 

void printVarsFile2() 
{
    // Update the global variable.
    globalVar = 50;

    cout << endl << "In printVarsFile2" << endl;
    cout << "globalVar == " <<  globalVar << endl;
    cout << "fileScopedVar == " <<  fileScopedVar << endl;
}

Note: While file (or global) scope variables and functions can be convenient, excessive use of them can lead to issues such as:

It’s a good programming practice to limit the use of file-scope variables, relying more on local variables and passing values as function arguments.

Class Scope

Class scope refers to the region in which identifiers (like data members and member functions) declared within a class are accessible. Understanding class scope is fundamental to designing classes, encapsulating data, and ensuring clean, modular, and effective object-oriented programming.

Key aspects of class scope:

Example

In the example below, year, month, and day have class scope and are private, so they are only accessible within the Date class. On the other hand, the addDay() method is publicly accessible and can be accessed using an instance of Date from outside the class.

Date.h

#ifndef DATE_H
#define DATE_H

class Date
{
public:
    Date(int yyyy, int mm, int dd);  // constructor
    void addDay(int num);           
    int getYear() { return year; }   // inline method declarations
    int getMonth() { return month; }
    int getDay() { return day; }
private:
    int year, month, day;
    bool isValid();
};

#endif

Date.cpp

#include "Date.h"

// Use the scope resolution operator :: to implement Date methods.

// Implementation of the constructor
Date::Date(int yyyy, int mm, int dd) 
    : year{yyyy}, month{mm}, day{dd}  // member initializer list
{
    // Validate the new date.
    isValid();
}

// Implementation of the addDay() public method
void Date::addDay(int num)
{
    day += num;
}

// Implementation of the isValid() private method
bool Date::isValid()
{
    // Validate date here...
    return true;
}

Namespace Scope

A namespace is a declarative region that provides a scope for the identifiers (such as variables, functions, classes) inside it. Namespaces are used to organize code into logical units and prevent naming collisions that can occur especially when your code base becomes large or when integrating code from multiple libraries.

Key aspects of namespace scope:

Example

In the example below, two separate namespaces are create and they are accessed using both the scope resolution operator :: and directly after the using directive.

namespace.cpp

#include <iostream>
#include <string>
#include <vector>

// Define a namespace named 'TeamCake'
namespace TeamCake 
{  
    std::string team = "Cake";
    std::vector<std::string> desserts = {"Angel Food", "Birthday", "Chocolate", "Red Velvet", "Sponge"};
}

// Define a namespace named 'TeamPie'
namespace TeamPie
{
    std::string team = "Pie";
    std::vector<std::string> desserts = {"Apple", "Blueberry", "Cherry", "Pumpkin", "Strawberry"};

    namespace NestedNamespace
    {
        int num = 50;
    }
}

int main() 
{
    // Access using scope resolution operator
    std::cout << TeamCake::team << std::endl;  

    // Access nested namespace member
    std::cout << TeamPie::NestedNamespace::num << std::endl; 

    {
        // Using directive
        using namespace TeamCake;   

        // Can now access directly
        std::cout << team << std::endl;

        for (std::string dessert : desserts)
        {
            std::cout << dessert << std::endl; 
        }
    }

    // Using directive
    using namespace TeamPie;  

    // Can now access directly
    std::cout << team << std::endl; 

    for (std::string dessert : desserts)
    {
        std::cout << dessert << std::endl; 
    }
    
    return 0;
}

Advantages of using namespaces:

It’s worth noting the potential pitfalls of overusing the using directive, as it might lead to ambiguities and defeats the purpose of namespaces in avoiding name collisions. It’s generally considered good practice to use the using directive sparingly, especially in header files.

References:

Exercise 1

  1. Create a new file called TestScope.cpp
  2. Add a int main() function.
  3. Create some new variables in the top of main().
  4. Create a new block scope in main() and add some variables.
  5. Add various cout statements to test the block scope.
  6. Add some variables in the file scope.
  7. Create a new namespace and add some variables and functions.
  8. Test the file scope and namespace scope with various cout statements.

Friends

The friend keyword is used to grant specific external functions or classes access to the private and protected members of a class. Essentially, it allows you to break the encapsulation barrier in a controlled manner.

Here’s a breakdown of the friend keyword and its applications:

Example 1

In this example, the averageGrade function can access the private member grades of the Student class due to it being declared as a friend. This allows us to compute the average grade for the student without compromising the encapsulation of the Student class as a whole.

Student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>
#include <vector>

class Student
{
public:
    Student(std::string name, int id);  // constructor
    
    void addGrade(float grade);           
    std::string getName() { return name; }  

    // Declare the averageGrade function as a friend
    friend float averageGrade(const Student& student); 

private:
    std::string name;
    int id;
    std::vector<float> grades;
};

#endif

Student.cpp

#include "Student.h"
#include <iostream>
using namespace std;

// Constructor
Student::Student(string name, int id) : name{name}, id{id} { } 

// Implementation of the add grade method
void Student::addGrade(float grade)
{
    grades.push_back(grade);
}

// Define the friend function
// Note: this function is not in the Student class scope.
float averageGrade(const Student& student) 
{
    float total = 0.0;

    // Function has access to the student private grades vector.
    for (float grade : student.grades)
    {
        total += grade;
    }
    return total / student.grades.size();
}

Example 2

In this example, the AcademicAdvisor class can access the private member grades of the Student class due to it being declared as a friend. This allows us to share the grades for the student without compromising the encapsulation of the Student class as a whole.

Student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>
#include <vector>

class Student
{
public:
    Student(std::string name, int id);  // constructor
    
    void addGrade(float grade);           
    std::string getName() { return name; }  

    // Declare the AcademicAdvisor class as a friend
    friend class AcademicAdvisor; 

private:
    std::string name;
    int id;
    std::vector<float> grades;
};

#endif

Use cases and considerations:

References:

Exercise 2

  1. Create a new file called TestFriends.cpp
  2. Add a int main() function.
  3. Create a new class called Diary in the files Diary.h and Diary.cpp.
  4. Add a private secret string to the Diary class.
  5. Add a friend function to the Diary class called readSecret().
  6. In the TestFriends.cpp define the function readSecret().
  7. In the main() functino test the readSecret() function with various cout statements.

Destructors

A destructor is a special member function of a class that is executed whenever an object of that class goes out of scope or is explicitly destroyed. Its main purpose is to release resources and perform cleanup tasks for an object before it is removed from memory.

Example 1

In this example the StringArray class’s constructor allocates memory for the specified number of strings. The destructor ensures that the dynamically allocated memory for the strings is released when the object is destroyed. As the StringArray object goes out of scope at the end of the block in main, the destructor is automatically invoked, ensuring the memory is freed.

StringArray.h

#ifndef STRINGARRAY_H
#define STRINGARRAY_H

#include <iostream>
#include <string>

class StringArray 
{
public:
    // Constructor: allocate memory for the array
    StringArray(int size);

    // Destructor: free the allocated memory
    ~StringArray();

    void setStringAt(int index, const std::string& value);
    std::string getStringAt(int index) const;
private:
    std::string* strings;
    int length;    
};

#endif

StringArray.cpp

#include <iostream>
#include <string>
#include "StringArray.h"
using namespace std;

// Constructor: allocate memory for the array
StringArray::StringArray(int size) : length(size) 
{
    strings = new string[size];
}

// Destructor: free the allocated memory
StringArray::~StringArray() 
{
    delete[] strings;
    cout << "Memory for string array released." << endl;
}

// Set a string at a specific index
void StringArray::setStringAt(int index, const string& value) 
{
    // Code to set string in the string array...
}

// Get a string from a specific index
string StringArray::getStringAt(int index) const 
{
    // Code to get a string in the string array...
}

int main() 
{
    // Start a block scope
    {
        // Allocate space for 5 strings in a StringArray instance
        StringArray strArray(5); 
        strArray.setStringAt(0, "Hello");
        strArray.setStringAt(1, "Spiders");
        cout << strArray.getStringAt(0) << " " << strArray.getStringAt(1) << endl;

        // Because we are leaving the block scope the strArray's  
        // destructor will be called here, freeing the allocated memory
    }

    cout << "Back in main function after the block." << endl;

    return 0;
}

Example 2

In this example, the Logger class’s constructor tries to open the specified file for logging. The destructor ensures that the log file is closed when the object is destroyed or goes out of scope. As the Logger object goes out of scope at the end of the block in main, the destructor is automatically invoked, ensuring the file is closed.

Logger.h

#ifndef LOGGER_H
#define LOGGER_H

#include <fstream>
#include <string>

class Logger 
{
public:
    // Constructor: open the file for logging
    Logger(const std::string& filename);

    // Destructor: close the log file
    ~Logger();

    void writeLog(const std::string& message);
private:
    std::ofstream logFile;    
};

#endif

Logger.cpp

#include <iostream>
#include "Logger.h"
using namespace std;

// Constructor: open the file for logging
Logger::Logger(const string& filename) 
{
    logFile.open(filename, ios::app);  // Open in append mode
}

// Destructor: close the log file
Logger::~Logger() 
{
    logFile.close();
}

// Function to write a message to the log
void Logger::writeLog(const string& message) 
{
    if (logFile.is_open()) 
    {
        logFile << message << endl;
    }
}


int main() {

    // Start a block scope
    {
        Logger appLog("application.log");
        appLog.writeLog("Application started.");
        appLog.writeLog("Performing some operations...");

        // Because we are leaving the block scope the appLog's  
        // destructor will be called here, closing the log file
    }

    cout << "Back in main function after the block." << endl;

    return 0;
}

Exercise 3

  1. Create a new class that add uses dynamic memory allocation to create a member variable on the heap with new.
  2. Add a destructor method to the class.
  3. Provide a cout statement in the destructor method to prove that the destructor was called.
  4. Create a TestDestructor.cpp file.
  5. In the file in a main() function create a new instance of your class in a block scope where you can test your destructor.

Composition

In object-oriented programming (OOP), composition is a design principle that describes a “has-a” relationship between objects. Composition allows you to build complex objects by combining simpler ones, essentially “composing” an object out of several other objects.

Here are some key points to understand about composition:

Example

In this example, the Car class is composed of the Engine, Tire, and Radio classes. We’ve effectively broken down the complex Car class into simpler, more manageable parts, each with its own responsibilities. The Car class doesn’t inherit from Engine, Tire, or Radio; instead, it has an Engine, four Tires, and a Radio.

Engine.h

#ifndef ENGINE_H
#define ENGINE_H

class Engine 
{
public:
    Engine(int hp) : horsepower(hp) {}
    void start() { }
    void stop() { }
private:
    int horsepower;
};

#endif

Radio.h

#ifndef RADIO_H
#define RADIO_H

class Radio 
{
public:
    Radio(int v) : volume(v) {}
    void switchOn() { }
    void switchOff() { }
private:
    int volume;
};

#endif

Tire.h

#ifndef TIRE_H
#define TIRE_H

class Tire 
{
public:
    Tire(int d) : diameter(d) {}
    void inflate(int psi) { }
private:
    int diameter;
};

#endif

Car.h

#include "Engine.h"
#include "Radio.h"
#include "Tire.h"

class Car 
{
public:
    Car();
    void start();
    void stop();
    void inflateTire(int index, int psi);
private:
    // Car is composed of an Engine class, Radio class, and four Tire classes.
    Engine engine;
    Radio radio;
    Tire tires[4];
};

Car.cpp

#include "Car.h"

Car::Car() : 
    // Initialize engine with 256 horsepower
    engine(256),
    // Initialize all tires with a diameter of 19 inches               
    tires{ Tire(19), Tire(19), Tire(19), Tire(19) }, 
    // Initialize radio with volume level 11
    radio(11)                  
{ }

void Car::start() 
{
    engine.start();
    radio.switchOn();
}

void Car::stop() 
{
    engine.stop();
    radio.switchOff();
}

void Car::inflateTire(int index, int psi) 
{
    if (index >= 0 && index < 4) 
    {
        tires[index].inflate(psi);
    }
}

To summarize, composition in OOP is about building complex objects from simpler ones. It provides flexibility, promotes code reuse, and is a powerful alternative or complement to inheritance.

Exercise 4

  1. Create a new class that will be composed of another class.

UML Diagrams

UML, which stands for Unified Modeling Language, is a standardized modeling language used to visualize, design, and document software systems. UML is not a programming language, but rather a set of graphical notations for creating visual models of object-oriented systems. It provides a common vocabulary and set of conventions for describing software systems, and allows developers to communicate more effectively about the design and architecture of a system

Here’s a brief introduction:

UML is a powerful tool in a software engineer’s toolkit, useful for both planning and explaining complex software architectures. If you’re entering the field of software engineering or systems design, having a good grasp of UML will be beneficial.

Class Diagrams

A UML class diagram describes the structure of a system by visualizing the system’s classes, their attributes, methods, and the relationships between them.

Here are the main parts of a UML class diagram:

UML Class Diagram

UML Class Visibility

Date Class in UML

Relationships between classes can be depicted in UML class diagrams.

UML Associations

Exercise 5

  1. Draw the UML diagram for your lab4 classes Enigma and Rotor.
  2. Take a picture of the drawing and add it to the repository.