Создание и просмотр семейного древа

В пятницу у меня задание сделать генеалогическое древо. Начал кодить в январе. Мои отзывы учителя об этом коде заключались в том, что использование указателей было ужасным и недостаточно ООП. Этот код компилируется и работает. Что я могу сделать лучше, чтобы сделать код более объектно-ориентированным?

Лучшим комментарием было: «Ужасно видеть такие указатели в дикой природе». В то же время он учил нас, поэтому мы с моим партнером по заданию не знаем, что еще делать. Кажется, что каждый раз, когда мы обращаемся за помощью, код становится сложнее, чем необходимо, и мы все дальше уходим от C++. Я знаю, что мне нужно создать узел, который принимает человека, но есть ли что-нибудь еще?

Этот код создан для того, чтобы пользователь мог ввести себя и добавить родителей, бабушек и дедушек и т. д. Вы можете редактировать, удалять и искать.

class person
{
private:
public:
    std::string firstName;  // variable to hold the firstname of the person
    std::string lastName;   // variable to hold the lastname of the person
    int yearOfBirth;        // variable to hold the year of birth
    int age;                // variable to hold the age of person
    int alive;              // variable to hold data
    int sex;                // variable to hold the gender of the person

    person *left;
    person *right;
    person();
    ~person();
    void getData();
};

person::person() // constructor outside the class
{
    left = right = nullptr;
    firstName = "";
    lastName = "";
    yearOfBirth = 0;
    age = 0;
    alive = 0;
    sex = 0;
}

person::~person() = default; //destructor of person::person (check if this code runs and are okey!!)


void ignoreLine()
{
    std::cin.clear();
    std::cin.ignore(INT_MAX, '\n');
}

void person::getData() // get information about a person
{
    char gender;

    std::cout << "\nFirstname of the person: " << std::endl;
    std::cin >> firstName;
    std::cout << "Lastname of the person: " << std::endl;
    std::cin >> lastName;
    std::cout << "Age of " << firstName << ": " << std::endl;
    while (!(std::cin >> age) or (age < 0) or std::cin.fail()) // check if the inputs only contains positives int.
    {
        std::cin >> age;
        std::cout << "Only positives number allowed: " << std::endl;
        ignoreLine();
    }
    std::cout << "What is the sex of " << firstName << " (m/f): " << std::endl;
    std::cin >> gender;
    switch (gender) // all other int, char etc, than m/M is considered as female. Maybe change later, but good for now.
    {
        case 'm':
        case 'M':
            sex = 1;
            break;
    }
    ignoreLine(); // "pause" the program until the user press "Enter"
}



class familyTree
{
private:
public:

    person *root;
    person *search(const std::string&);
    person *traverseLeft(person *, const std::string&);
    person *traverseRight(person *, const std::string&);

    familyTree();
    void addNewPerson();
    void addMother(person *, person *);
    void addFather(person *, person *);

    static void show(person *);

    void printInOrder(person *, int);
};

familyTree::familyTree()
{
    root = nullptr;
}

void familyTree::addMother(person *a, person *b) // to add b as the mother of a
{
    if (a->left == nullptr)
    {
        a->left = b;
    }

    else
    {
        addMother(a->left, b);
    }
}

void familyTree::addFather(person *a, person *b)// to add b as the father of a
{
    while (a->right != nullptr)
        a = a->right;

    a->right = b;
}

person *familyTree::traverseLeft(person *ptr, const std::string& person)
{
    ptr = ptr->left;

    while (ptr != nullptr)
    {
        if ((ptr->firstName) == person) {
            return ptr;
        }

        else if (traverseRight(ptr, person) != nullptr)
        {
            return traverseRight(ptr, person);
        }
        else
        {
            ptr = ptr->left;
        }
    }
    return nullptr;
}

person *familyTree::traverseRight(person *ptr, const std::string& person)
{

    ptr = ptr->right;

    while (ptr != nullptr)
    {
        if ((ptr->firstName) == person)
        {
            return ptr;
        }
        else if (traverseLeft(ptr, person) != nullptr)
        {
            return traverseLeft(ptr, person);
        }
        else
            ptr = ptr->right;
    }
    return nullptr;
}


void familyTree::addNewPerson() 
{
    auto *temp = new struct person; 
    temp->getData();

    std::string personChild;

    if (root == nullptr)
    {
        std::cout << "\nFirst person added to the family!" << std::endl;
        root = temp;
    }

    else // if there exists a person in the tree, add the new person as a relative to a previous
    {
        std::cout << "Enter the name of the person " << temp->firstName << " is the parent to: " << std::endl;
        std::cin >> personChild;

        if (familyTree::search(personChild))  //tests if the person entered is found int the tree, if yes it continues.
        {
            int opt;

            std::cout << "\nWhat is the family relation? " << std::endl;
            std::cout << "(1) if " << temp->firstName << " is the Mother to " << personChild << std::endl;
            std::cout << "(2) if " << temp->firstName << " is the Father to " << personChild << std::endl;
            std::cout << "Enter: " << std::endl;
            std::cin >> opt;
            ignoreLine(); // "pause" the program until the user press "Enter"

            while (std::cin.fail() or opt < 1 or opt > 2)
            {
                std::cout << "\nOnly a number of 1 or 2 is accepted!" << std::endl;
                std::cout << "Enter: " << std::endl;
                std::cin >> opt;
                ignoreLine(); // "pause" the program until the user press "Enter"
            }

            switch (opt)
            {
                case 1:
                    addMother(search(personChild), temp);
                    std::cout << temp->firstName << " is now added as the mother!" << std::endl;
                    break;
                case 2:
                    addFather(search(personChild), temp);
                    std::cout << temp->firstName << " is now added as the father!" << std::endl;
                    break;
            }
        }

//        else
//        {
//          maybe make a code that loops the "enter name" until it's correct? Or just go straight back to main menu?
//        }
    }
}

void familyTree::printInOrder (person *person, int space) //print its persons using in order traversal
{
    auto count = 7; //using count for how much space between generations (horizontal)
    space += count;

    if (person != nullptr)
    {
        if(person->left != nullptr)
        {
            printInOrder(person->left, space);
        }

        std::cout << std::endl;
        for (int i = count; i < space; i++)
            std::cout << " ";
        std::cout << person->firstName << "\n";

        if(person->right != nullptr)
        {
            printInOrder(person->right, space);
        }
    }

    else
    {
        std::cout << "No tree exist!" << std::endl;
        ignoreLine(); // "pause" the program until the user press "Enter"
        return;
    }
}

person *familyTree::search(const std::string& per) // to search for a person
{
    person *ptr = root;

    if ((ptr->firstName) == per)
    {
        return ptr;
    }

    else if (traverseRight(root, per) != nullptr)
    {
        return traverseRight(root, per);
    }

    else if (traverseLeft(root, per) != nullptr)
    {
        return traverseLeft(root, per);
    }

    else
    {
        std::cout << "\nNo person found with the given name" << std::endl;
        ignoreLine(); // "pause" the program until the user press "Enter"
        return nullptr;
    }
}

void familyTree::show(person *ptr) // to show the information of a particular person
{
    std::string sex = "Female";
    if (ptr->sex)
    {
        sex = "Male";
    }

    std::cout << "\nName: " << ptr->firstName << " " << ptr->lastName << std::endl;
    std::cout << "Age: " << ptr->age << std::endl;
    std::cout << "Sex: " << sex << std::endl;
    std::cin.ignore();
}

void showMainMenu() // hold the output for the main menu
{
    std::cout << "Welcome" << std::endl;
    std::cout << "Please enter a number for your choice below:\n" << std::endl;
    std::cout << "(1) Add new person to tree" << std::endl;
    std::cout << "(2) Show information for a person" << std::endl;
    std::cout << "(3) Print complete family-tree" << std::endl;
    std::cout << "(4) Used for testing new choices" << std::endl;
    std::cout << "(0) Quit" << std::endl;
    std::cout << "\nYour choice: " << std::endl;
}

int main() 
{
    familyTree fT; // used to access/init. familytree class.

    int option, exit = 0;
    std::string temp, str;

    while (exit == 0)
    {
        showMainMenu();
        std::cin >> option;

        while (std::cin.fail())
        {
            ignoreLine();
            std::cout << "\nOnly a number between 0 and 10 is allowed: ";
            std::cin >> option;
        }

        switch (option)
        {
            case 1:
                fT.addNewPerson();
                break;
            case 2:
                std::cout << "Enter name of person to show information: ";
                std::cin >> temp;
                fT.show(fT.search(temp));
                break;
            case 3:
                fT.printInOrder(fT.root, 0);
                break;
            case 4:
                /* n/a */
                break;
            case 0:
                exit = 1;
                break;
        }
        std::cout << "\nPress enter to continue.." << std::endl;
        ignoreLine();
    }
    return 0;
}

1 ответ
1

Это ни в коем случае не исчерпывающий, скорее список общих моментов, на которых следует сосредоточиться, а не глубокий анализ реализации.

Сначала несколько общих советов:

  1. Используйте некоторые соглашения о кодировании, которые позволяют легко идентифицировать типы, переменные и т. д. Что такое person здесь: person *familyTree::traverseRight(person *ptr, const std::string& person)? Ну, это неоднозначно или, по крайней мере, контекстно-зависимо. Писать так — это огромная медвежья услуга будущим специалистам по сопровождению кода.
  2. Необработанные указатели, как правило, зло, особенно если принять во внимание современный C++. Однако использование умных для деревьев, списков и т. д. может привести к переполнению стека при разрушении структуры данных, если она слишком велика, из-за рекурсивного вызова деструкторов. Так что либо ручное управление ресурсами со всеми его недостатками, либо корректность по умолчанию за счет ограниченного размера. Это решать автору. Я не могу сказать это вам.
  3. Включите какой-нибудь фреймворк для тестирования, будь то Catch2, Google Test, BoostTest; напишите модульные тесты для защиты существующих функций. Даже поверхностно протестированный код становится намного легче просматривать и поддерживать.
  4. (не полностью связанный с обзором) Ознакомьтесь с контролем версий и выработайте привычку часто совершать коммиты, это экономит много времени, особенно при тестировании новых функций, поскольку возврат к предыдущей версии становится проще. Это предложение:

код становится более сложным, чем необходимо, и мы уходим еще дальше от C++. предполагает, что у вас есть проблемы с отслеживанием того, что было изменено. Любая VCS должна упростить этот путь.

Что касается самого кода: во-первых, я бы попытался определить обязанности классов и разделить их соответствующим образом. Учебный класс person имеет смешанные обязанности: это и человек, и узел дерева.

Давайте сначала извлечем человека: если он лишен своих узловых обязанностей, его становится намного проще поддерживать; все его специальные операции (перемещение, копирование, его респектабельные назначения и т. д.) становятся более автоматическими. Должна ли (де) сериализация быть частью функции-члена класса или автономной, это открытый вопрос. Я предпочитаю автономные (перегружая потоковые операторы << а также >>), но это вопрос стиля, условностей, вкуса и потребностей… что угодно. Я пропустил это для краткости.

Кроме того, рассмотрите возможность использования более подходящих типов данных, например логических для aliveперечисление пола, может быть unsigned вместо int для года (если вы не достигнете BC; или, может быть, даже выберите совершенно другой тип? Я не знаю, зависит от вашего конкретного случая).

Лично я бы пошел +- таким образом.

struct Person
{
    enum struct Sex{male, female};

    std::string firstName{};  // variable to hold the firstname of the person
    std::string lastName{};   // variable to hold the lastname of the person
    unsigned yearOfBirth{};        // variable to hold the year of birth
    unsigned age{};                // variable to hold the age of person
    bool alive{};              // variable to hold data
    Sex sex{};                // variable to hold the gender of the person
};

Должен ли человек быть конструктивным по умолчанию — трудно сказать на ровном месте. Да, если вы планируете читать его из потоков. В противном случае — я бы сказал нет.

Следующим шагом будет установка этого в дереве. Кстати, насколько я понимаю генеалогические деревья, они не обязательно должны быть бинарными, но давайте предположим, что все задание посвящено бинарным деревьям, так что оно было спроектировано так намеренно 😉

Кроме того, я не уверен, должны ли вы использовать шаблоны или нет. Предполагая версию без шаблона:

struct FamilyTreeNode
{
    Person person{};
    FamilyTreeNode* left;
    FamilyTreeNode* right;
};

Обратите внимание, что здесь я использую необработанные указатели. Нет проблем с использованием умных, за счет потенциальных проблем, связанных со стеком.

И это можно заключить в генеалогическое древо:

struct FamilyTree
{
private:
    FamilyTreeNode* root;
    //I wouldn't make it public, but your call
    //also, maybe root doesn't necessarily has to be heap-allocated, but let's stick to that to keep it consistent...
 ////the rest

Теперь, что касается интерфейса генеалогического древа: мне нужно тщательно его изучить, и он также сильно зависит от потребностей. Мне трудно сказать, предпочитаете ли вы воздействовать непосредственно на людей (Person класс) или, может быть, узлы дерева. Я бы, вероятно, пошел по пути узла, сделав его чем-то вроде C++-итератора.

Кроме того, помните о константной корректности. Например, метод: person *search(const std::string&); вероятно, должен иметь свою константную перегрузку, т.е. const person *search(const std::string&); const (или же FamilyTreeNode вместо person в этом случае) нужно искать человека только для печати.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *