玖叶教程网

前端编程开发入门

C++|六大对象关系:不只是继承与组合

There are many different kinds of relationships two objects may have in real-life, and we use specific “relation type” words to describe these relationships. For example:

a square “is-a” shape(Inheritance).

A car “has-a” steering wheel(Aggregation).

A computer programmer “uses-a” keyboard(Association).

A flower “depends-on” a bee for pollination(Dependencies).

A student is a “member-of” a class(Container).

your brain exists as “part-of” you(Composition).

All of these relation types have useful analogies in C++:

1 Inheritance(Is a)

Inheritance allows us to model an is-a relationship between two objects. The object being inherited from is called the parent class, base class, or superclass. The object doing the inheriting is called the child class, derived class, or subclass.

When a derived class inherits from a base class, the derived class acquires all of the members of the base class.

Derived classes can add new functions, change the way functions that exist in the base class work in the derived class, change an inherited member’s access level, or hide functionality.

#include <iostream>
#include <string>
 
class Fruit
{
private:
	std::string m_name;
	std::string m_color;
 
public:
	Fruit(const std::string& name, const std::string& color)
		: m_name{ name }, m_color{ color }
	{
 
	}
 
	const std::string& getName() const { return m_name; }
	const std::string& getColor() const { return m_color; }
};
 
class Apple: public Fruit
{
// The previous constructor we used for Apple had a fixed name ("apple").
// We need a new constructor for GrannySmith to use to set the name of the fruit
protected: // protected so only derived classes can access
	Apple(const std::string& name, const std::string& color)
		: Fruit{ name, color }
	{
	}
 
public:
	Apple(const std::string& color="red")
		: Fruit{ "apple", color }
	{
	}
};
 
class Banana : public Fruit
{
public:
	Banana()
		: Fruit{ "banana", "yellow" }
	{
 
	}
};
 
class GrannySmith : public Apple
{
public:
	GrannySmith()
		: Apple{ "granny smith apple", "green" }
	{
 
	}
};
 
int main()
{
	Apple a{ "red" };
	Banana b;
	GrannySmith c;
 
	std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
	std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
	std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n";
 
	return 0;
}

2 Composition(Part-of)

They are typically created as structs or classes with normal data members(primitive or composition types, such as struct or class). Because these data members exist directly as part of the struct/class, their lifetimes are bound to that of the class instance itself.

In a composition, we typically add our parts to the composition using normal member variables (or pointers where the allocation and deallocation process is handled by the composition class).

#include <iostream>
#include <string>
class Point2D
{
private:
    int m_x;
    int m_y;
 
public:
    // A default constructor
    Point2D()
        : m_x(0), m_y(0)
    {
    }
 
    // A specific constructor
    Point2D(int x, int y)
        : m_x(x), m_y(y)
    {
    }
 
    // An overloaded output operator
    friend std::ostream& operator<<(std::ostream& out, const Point2D &point)
    {
        out << "(" << point.m_x << ", " << point.m_y << ")";
        return out;
    }
 
    // Access functions
    void setPoint(int x, int y)
    {
        m_x = x;
        m_y = y;
    }
 
};
class Creature
{
private:
    std::string m_name;
    Point2D m_location;
 
public:
    Creature(const std::string &name, const Point2D &location)
        : m_name(name), m_location(location)
    {
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Creature &creature)
    {
        out << creature.m_name << " is at " << creature.m_location;
        return out;
    }
 
    void moveTo(int x, int y)
    {
        m_location.setPoint(x, y);
    }
};
int main()
{
    std::cout << "Enter a name for your creature: ";
    std::string name;
    std::cin >> name;
    Creature creature(name, Point2D(4, 7));
	
    while (1)
    {
        // print the creature's name and location
        std::cout << creature << '\n';
 
        std::cout << "Enter new X location for creature (-1 to quit): ";
        int x=0;
        std::cin >> x;
        if (x == -1)
            break;
 
        std::cout << "Enter new Y location for creature (-1 to quit): ";
        int y=0;
        std::cin >> y;
        if (y == -1)
            break;
		
        creature.moveTo(x, y);
        }
 
    return 0;
}

3 Aggregation Has-a

In an aggregation, we also add parts as member variables. However, these member variables are typically either references or pointers that are used to point at objects that have been created outside the scope of the class.

#include <iostream>
#include <string>
#include <vector>
 
class Teacher
{
private:
	std::string m_name;
 
public:
	Teacher(std::string name)
		: m_name(name)
	{
	}
 
	std::string getName() { return m_name; }
};
 
class Department
{
private:
	std::vector<Teacher*> m_teacher;
 
public:
	Department()
	{
	}
 
	void add(Teacher *teacher)
	{
		m_teacher.push_back(teacher);
	}
 
	friend std::ostream& operator <<(std::ostream &out, const Department &dept)
	{
		out << "Department: ";
		for (const auto &element : dept.m_teacher)
			out << element->getName() << ' ';
		out << '\n';
 
		return out;
	}
};
 
 
int main()
{
	// Create a teacher outside the scope of the Department
	Teacher *t1 = new Teacher("Bob"); // create a teacher
	Teacher *t2 = new Teacher("Frank");
	Teacher *t3 = new Teacher("Beth");
 
	{
		// Create a department and add some Teachers to it
		Department dept; // create an empty Department
		dept.add(t1);
		dept.add(t2);
		dept.add(t3);
 
		std::cout << dept;
 
	} // dept goes out of scope here and is destroyed
 
	std::cout << t1->getName() << " still exists!\n";
	std::cout << t2->getName() << " still exists!\n";
	std::cout << t3->getName() << " still exists!\n";
 
	delete t1;
	delete t2;
	delete t3;
 
	return 0;
}

Summarizing composition and aggregation

Compositions:

  • Typically use normal member variables
  • Can use pointer members if the class handles object allocation/deallocation itself
  • Responsible for creation/destruction of parts

Aggregations:

  • Typically use pointer or reference members that point to or reference objects that live outside the scope of the aggregate class
  • Not responsible for creating/destroying parts

4 Association(Uses-a)

Because associations are a broad type of relationship, they can be implemented in many different ways. However, most often, associations are implemented using pointers, where the object points at the associated object.

#include <iostream>
#include <string>
#include <vector>
 
// Since Doctor and Patient have a circular dependency, we're going to forward declare Patient
class Patient;
 
class Doctor
{
private:
	std::string m_name{};
	std::vector<Patient *> m_patient{};
 
public:
	Doctor(std::string name) :
		m_name(name)
	{
	}
 
	void addPatient(Patient *pat);
	
	// We'll implement this function below Patient since we need Patient to be defined at that point
	friend std::ostream& operator<<(std::ostream &out, const Doctor &doc);
 
	std::string getName() const { return m_name; }
};
 
class Patient
{
private:
	std::string m_name{};
	std::vector<Doctor *> m_doctor{}; // so that we can use it here
 
	// We're going to make addDoctor private because we don't want the public to use it.
	// They should use Doctor::addPatient() instead, which is publicly exposed
	void addDoctor(Doctor *doc)
	{
		m_doctor.push_back(doc);
	}
 
public:
	Patient(std::string name)
		: m_name(name)
	{
	}
 
	// We'll implement this function below Doctor since we need Doctor to be defined at that point
	friend std::ostream& operator<<(std::ostream &out, const Patient &pat);
 
	std::string getName() const { return m_name; }
 
	// We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor()
	friend void Doctor::addPatient(Patient *pat);
};
 
void Doctor::addPatient(Patient *pat)
{
	// Our doctor will add this patient
	m_patient.push_back(pat);
 
	// and the patient will also add this doctor
	pat->addDoctor(this);
}
 
std::ostream& operator<<(std::ostream &out, const Doctor &doc)
{
	unsigned int length = doc.m_patient.size();
	if (length == 0)
	{
		out << doc.m_name << " has no patients right now";
		return out;
	}
 
	out << doc.m_name << " is seeing patients: ";
	for (unsigned int count = 0; count < length; ++count)
		out << doc.m_patient[count]->getName() << ' ';
 
	return out;
}
 
std::ostream& operator<<(std::ostream &out, const Patient &pat)
{
	unsigned int length = pat.m_doctor.size();
	if (length == 0)
	{
		out << pat.getName() << " has no doctors right now";
		return out;
	}
 
	out << pat.m_name << " is seeing doctors: ";
	for (unsigned int count = 0; count < length; ++count)
		out << pat.m_doctor[count]->getName() << ' ';
 
	return out;
}
 
int main()
{
	// Create a Patient outside the scope of the Doctor
	Patient *p1 = new Patient("Dave");
	Patient *p2 = new Patient("Frank");
	Patient *p3 = new Patient("Betsy");
 
	Doctor *d1 = new Doctor("James");
	Doctor *d2 = new Doctor("Scott");
 
	d1->addPatient(p1);
 
	d2->addPatient(p1);
	d2->addPatient(p3);
 
	std::cout << *d1 << '\n';
	std::cout << *d2 << '\n';
	std::cout << *p1 << '\n';
	std::cout << *p2 << '\n';
	std::cout << *p3 << '\n';
 
	delete p1;
	delete p2;
	delete p3;
 
	delete d1;
	delete d2;
 
	return 0;
}

5 Dependency(Depends on)

A dependency occurs when one object invokes another object’s functionality in order to accomplish some specific task. This is a weaker relationship than an association, but still, any change to object being depended upon may break functionality in the (dependent) caller. A dependency is always a unidirectional relationship.

In a dependency, one class uses another class to perform a task. The dependent class typically is not a member of the class using it, but rather is temporarily created, used, and then destroyed, or passed into a member function from an external source.

A good example of a dependency that you’ve already seen many times is std::cout (of type std::ostream). Our classes that use std::cout use it in order to accomplish the task of printing something to the console, but not otherwise.

#include <iostream>
 
class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Point &point);
};
 
std::ostream& operator<< (std::ostream &out, const Point &point)
{
    // Since operator<< is a friend of the Point class, we can access Point's members directly.
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
 
    return out;
}
 
int main()
{
    Point point1(2.0, 3.0, 4.0);
 
    std::cout << point1;
 
    return 0;
}

6 Container(Member of)

a container class is a class designed to hold and organize multiple instances of another type (either another class, or a fundamental type).

#include <iostream>
#include <cassert>
class IntArray
{
private:
    int m_length{};
    int *m_data{};
 
public:
    IntArray() = default;
 
    IntArray(int length):
        m_length{ length }
    {
        assert(length >= 0);
        if (length > 0)
            m_data = new int[length]{};
    }
 
    ~IntArray()
    {
        delete[] m_data;
        // we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
    }
 
    void erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to nullptr here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    // reallocate resizes the array.  Any existing elements will be destroyed.  This function operates quickly.
    void reallocate(int newLength)
    {
        // First we delete any existing elements
        erase();
 
        // If our array is going to be empty now, return here
        if (newLength <= 0)
            return;
 
        // Then we have to allocate new elements
        m_data = new int[newLength];
        m_length = newLength;
    }
 
    // resize resizes the array.  Any existing elements will be kept.  This function operates slowly.
    void resize(int newLength)
    {
        // if the array is already the right length, we're done
        if (newLength == m_length)
            return;
 
        // If we are resizing to an empty array, do that and return
        if (newLength <= 0)
        {
            erase();
            return;
        }
 
        // Now we can assume newLength is at least 1 element.  This algorithm
        // works as follows: First we are going to allocate a new array.  Then we
        // are going to copy elements from the existing array to the new array.
        // Once that is done, we can destroy the old array, and make m_data
        // point to the new array.
 
        // First we have to allocate a new array
        int *data{ new int[newLength] };
 
        // Then we have to figure out how many elements to copy from the existing
        // array to the new array.  We want to copy as many elements as there are
        // in the smaller of the two arrays.
        if (m_length > 0)
        {
            int elementsToCopy{ (newLength > m_length) ? m_length : newLength };
 
            // Now copy the elements one by one
            for (int index{ 0 }; index < elementsToCopy ; ++index)
                data[index] = m_data[index];
        }
 
        // Now we can delete the old array because we don't need it any more
        delete[] m_data;
 
        // And use the new array instead!  Note that this simply makes m_data point
        // to the same address as the new array we dynamically allocated.  Because
        // data was dynamically allocated, it won't be destroyed when it goes out of scope.
        m_data = data;
        m_length = newLength;
    }
 
    void insertBefore(int value, int index)
    {
        // Sanity check our index value
        assert(index >= 0 && index <= m_length);
 
        // First create a new array one element larger than the old array
        int *data{ new int[m_length+1] };
 
        // Copy all of the elements up to the index
        for (int before{ 0 }; before < index; ++before)
            data [before] = m_data[before];
 
        // Insert our new element into the new array
        data[index] = value;
 
        // Copy all of the values after the inserted element
        for (int after{ index }; after < m_length; ++after)
            data[after+1] = m_data[after];
 
        // Finally, delete the old array, and use the new array instead
        delete[] m_data;
        m_data = data;
        ++m_length;
    }
 
    void remove(int index)
    {
        // Sanity check our index value
        assert(index >= 0 && index < m_length);
 
        // If we're removing the last element in the array, we can just erase the array and return early
        if (m_length == 1)
        {
            erase();
            return;
        }
 
        // First create a new array one element smaller than the old array
        int *data{ new int[m_length-1] };
 
        // Copy all of the elements up to the index
        for (int before{ 0 }; before  < index; ++before)
            data[before] = m_data[before];
 
        // Copy all of the values after the removed element
        for (int after{ index+1 }; after < m_length; ++after)
            data[after-1] = m_data[after];
 
        // Finally, delete the old array, and use the new array instead
        delete[] m_data;
        m_data = data;
        --m_length;
    }
 
    // A couple of additional functions just for convenience
    void insertAtBeginning(int value) { insertBefore(value, 0); }
    void insertAtEnd(int value) { insertBefore(value, m_length); }
 
    int getLength() const { return m_length; }
};
int main()
{
    // Declare an array with 10 elements
    IntArray array(10);
 
    // Fill the array with numbers 1 through 10
    for (int i{ 0 }; i<10; ++i)
        array[i] = i+1;
 
    // Resize the array to 8 elements
    array.resize(8);
 
    // Insert the number 20 before element with index 5
    array.insertBefore(20, 5);
 
    // Remove the element with index 3
    array.remove(3);
 
    // Add 30 and 40 to the end and beginning
    array.insertAtEnd(30);
    array.insertAtBeginning(40);
 
    // Print out all the numbers
    for (int i{ 0 }; i<array.getLength(); ++i)
        std::cout << array[i] << ' ';
 
    std::cout << '\n';
 
    return 0;
}

ref:https://www.learncpp.com/cpp-tutorial/10-1-object-relationships/

-End-

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言