Дата реализации класса в C++


Вот моя реализация класса дата.

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

Создание класса-это только для учебных целей, использование продукции не по назначению.

Дата.ч:

#pragma once
#ifndef _DATE_H_
#define _DATE_H_

#include <iostream>
#include <string>

class Date {
public:
    struct BadDate;
    enum Month {
        jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
    };

private:
    static std::string monthNames [12];
    static bool defaultSet;
    static Date defaultDate;

    int _day, _month, _year;
    bool leapYear(const int) const;
    void fillDate(int d, Month m, int y);
    void checkIllFormed();
    bool isIllFormed() const;
    void setVal(int&, const int, const int);

public:
    static void setDefault(const int d = 1, Month = Month(1), const int y = 2000);
    static void showDefault(std::ostream& os);
    static const std::string monthNameByNumber(const int);

    Date(
        int d = (Date::defaultSet)?Date::defaultDate.day():1,
        Month m = (Date::defaultSet)?Date::defaultDate.month():Month(1),
        int y = (Date::defaultSet)?Date::defaultDate.year():2000
    );
    Date(int d, int m, int y);
    Date(const Date&);
    Date& operator=(const Date&);
    ~Date();

    int day() const;
    Month month() const;
    int year() const;

    const std::string getMonthName() const;

    void setDay(const int);
    void setMonth(const int);
    void setYear(const int);

    const Date& operator++();
    const Date  operator++(int);
    const Date& operator--();
    const Date  operator--(int);
};

struct Date::BadDate {
    int _day, _month, _year;
    BadDate(int d, int m, int y);
};

std::ostream& operator<<(std::ostream&, const Date&);
std::ostream& operator<<(std::ostream&, const Date::BadDate&);

#endif

Date.cpp:

#include "Date.h"

Date::BadDate::BadDate(int d, int m, int y)
    : _day(d), _month(m), _year(y) {

};

std::string Date::monthNames[12] = {
    "January", "February", "March",
    "April", "May", "June",
    "July", "August", "September",
    "October", "November", "December"
};
bool Date::defaultSet = true;
Date Date::defaultDate = Date(1, 1, 2000);

void Date::setDefault(const int d, Month m, const int y) {
    Date::defaultDate.setDay(d);
    Date::defaultDate.setMonth(m);
    Date::defaultDate.setYear(y);
    Date::defaultSet = true;
}
void Date::showDefault(std::ostream& os) {
    os << Date::defaultDate;
}
const std::string Date::monthNameByNumber(const int n) {
    return Date::monthNames[n-1];
}

Date::Date(int d, Month m, int y)
    : _day(d), _month(m), _year(y) {
        checkIllFormed();
}
Date::Date(int d, int m, int y)
    : _day(d), _month(Month(m)), _year(y) {
        checkIllFormed();
}
Date::Date(const Date& that)
    : _day(that.day()), _month(that.month()), _year(that.year()) {
        checkIllFormed();
}
void Date::checkIllFormed() {
    if(isIllFormed()) {
        BadDate bd = BadDate(day(), month(), year());
        *this = Date::defaultDate;
        throw bd;
    }
}
Date& Date::operator=(const Date& that) {
    fillDate(that.day(), that.month(), that.year());
    return *this;
}
Date::~Date(void) {

}

int Date::day() const {
    return _day;
}
Date::Month Date::month() const {
    return Month(_month);
}
int Date::year() const {
    return _year;
}

const std::string Date::getMonthName() const {
    return Date::monthNameByNumber(month());
}

void Date::setDay(const int d) {
    setVal(_day, d, day());
}
void Date::setMonth(const int m) {
    setVal(_month, m, month());
}
void Date::setYear(const int y) {
    setVal(_year, y, year());
}
void Date::setVal(int& val, const int newVal, const int prevVal) {
    val = newVal;
    if(isIllFormed()) {
        BadDate bd = BadDate(day(), month(), year());
        val = prevVal;
        throw bd;
    }
}

const Date& Date::operator++() {
    setDay(day() + 1);
    return *this;
}
const Date Date::operator++(int) {
    setDay(day() + 1);
    return *this;
}
const Date& Date::operator--() {
    setDay(day() - 1);
    return *this;
}
const Date Date::operator--(int) {
    setDay(day() - 1);
    return *this;
}

std::ostream& operator<<(std::ostream& os, const Date& d) {
    os << d.day() << '.' << d.getMonthName() << '.' << d.year();
    return os;
}
std::ostream& operator<<(std::ostream& os, const Date::BadDate& bd) {
    os << bd._day << '.' << Date::monthNameByNumber(bd._month) << '.' << bd._year;
    return os;
}

bool Date::leapYear(const int y) const {
    /*
    1.If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5.
    2.If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4.
    3.If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5.
    4.The year is a leap year (it has 366 days).
    5.The year is not a leap year (it has 365 days).
    */
    if(y%4) {
        return false;
    }
    if(y%100) {
        return true;
    }
    if(y%400) {
        return false;
    }
    return true;
}
void Date::fillDate(int d, Month m, int y) {
    setDay(d);
    setMonth(m);
    setYear(y);
}

bool Date::isIllFormed() const {
    const int d(day()), m(month()), y(year());

    //check year
    if(y < 0) {
        return true;
    }

    //check month
    if(m < 1 || 12 < m) {
        return true;
    }

    //check day
    int maxDay((7<m)?31-m%2:30+m%2);
    if(m == 2) {
        if(leapYear(y)) {
            maxDay = 29;
        } else {
            maxDay = 28;
        }
    }
    if(d<1 || maxDay<d) {
        return true;
    }

    return false;
}

Я видел этот вопрос, но мой Date класс разный и может иметь свои подводные камни, которых я не знаю и хотелось бы узнать.



1853
13
задан 24 февраля 2018 в 11:02 Источник Поделиться
Комментарии
4 ответа

1. Заголовок охранники

#pragma once
#ifndef _DATE_H_
#define _DATE_H_
#endif

В "классических" заголовков охраны и #pragma once являются избыточными. Используйте один из них. Если ваш код должен быть максимально портативным, снять #pragma once это не поддерживается всеми компиляторами, пока #ifndef #define #endif последовательности.

Также обратите внимание, что символы начиная с префиксом _ зарезервированы для компилятора внедрение ВКУ в C++.

2. Ненужные рамки квалификации

Заявления типа

 Date::defaultDate.setDay(d);

внутри любой функции-члена класса не нужно квалифицировать объем члена класса, если вам нужно, чтобы претендовать наследуемый член класса или.

Вы можете просто написать

 defaultDate.setDay(d);

есть.

3. Правильное использование const

Использование const в этой функции определение

const std::string Date::monthNameByNumber(const int n) {
return Date::monthNames[n-1];
}

это неправильно, или, по крайней мере, не имеет благотворное влияние.

Это должен быть скорее

const std::string& Date::monthNameByNumber(int n) const {
// ^^^^^ No change of Date's
// internal state is
// guaranteed.
return Date::monthNames[n-1];
}

или сделал static класса функция-член на всех:

static const std::string& monthNameByNumber(unsigned int n) {
static const std::string monthNames[] = {
"January"s, "February"s, "March"s,
"April"s, "May"s, "June"s,
"July"s, "August"s, "September"s,
"October"s, "November"s, "December"s
};
if(n >= (sizeof(monthNames)/sizeof(std::string))) {
std::ostringstream msg;
msg << "Valid range of n is [0-" << sizeof(monthNames)/sizeof(std::string) << "]";
throw std::out_of_range(msg.str());
}
return monthNames[n];
}

4. Избежать ненужных преобразований

Вы объявили enum тип для представления месяцев, но затем вы предпочитаете, чтобы бросить, а не просто использовать его:

Month = Month(1),

Что должно быть

Month = jan,

Литье там побеждает целую цель использования enum значения для повышения безопасности и читаемость кода.

5. Использовать unsigned для значений всегда больше или равен нулю

Здесь вас не ожидают отрицательных значений

int _day, _month, _year;

таким образом, эти переменные должны быть объявлены как unsigned:

unsigned int _day, _month, _year;

Также избежать многократного определения переменных в одной строке для наглядности и читабельности:

unsigned int day_;
unsigned int month_;
unsigned year_;

Обратите внимание, я перенес _ до конца именами, те же самые рассуждения, как говорится в моей точке 1.

6. Геттер/сеттер стиль именования

Вы должны улучшить стиль именования вашего геттера/сеттера. Они могут быть просто перегруженная функция подписи:

 class Date {
unsigned int day_;
// ...
public:
unsigned int day() const; // Getter signature
unsigned int day(unsigned int); // Setter signature (returns the old value)
};

Стандартная библиотека использует этот стиль очень много, посмотрите, например, трансляция флаг геттеры и сеттеры.

7. Использовать std::exception потомки, когда throwИнг

Когда вы собираетесь throw исключений использовать std::exception или классу, наследующему его.

Поэтому вместо того, чтобы

    BadDate bd = BadDate(day(), month(), year());
val = prevVal;
throw bd;

вы должны создать класс

 class BadDateException : public std::exception {
std::string message;
Date badDate;
public:
BadDateException(const Date& d) : badDate(d) {
std::ostringstream msg;
msg << "Date " << d << " isn't a valid date.";
message = msg.str();
}
const char* what() {
return message.c_str();
}
}

таким образом, клиенты могут обрабатывать исключения в более общем виде и не должны знать о вашем BadDate класс:

 try {
Date d;
std::cin >> d;
}
catch(const std::exception& e) {
std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
exit(1);
}

Если нужно обрабатывать свои исключения, конкретно это может быть сделано как

 try {
Date d;
std::cin >> d;
}
catch(const BadDateException& e) {
std::cerr << "The date value is invalid '" << e.what() << "'" << std::endl;
exit(42); // Do something specific for that exception
}
catch(const std::exception& e) {
std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
exit(1);
}

8. Следовать принципу наименьшего удивления

Ваш путь к предварительно настроить по умолчанию дата нарушает пола политике.

Я оставлю это абсолютно особенность. Это способ более читаемым и понятным, чтобы использовать что-то вроде

const Date defaultDate(1,Jan,200);

Date d;
if(!std::cin >> d) {
d = defaultDate;
}

или

const Date defaultDate(1,Jan,200);
Date d(defaultDate);

в локальном контексте.

Кто-то мог бы использовать setDefaultDate() в другом месте в ином контексте, и клиент удивляется, почему просто объявив Date d; использует эти конкретные значения.

18
ответ дан 24 февраля 2018 в 11:02 Источник Поделиться

1. Имена Заголовков

Я рекомендую использовать .hpp для чисто c++ заголовочные файлы, и резервирования .h для c-совместимая заголовочные файлы.

2. Заголовок Охранники

Как уже упоминалось, вы должны #pragma once а не #ifndef.

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

3. Пространство имен

Определяя символ в глобальное пространство имен, вы открываете свой код до конфликтов с библиотеками C, или другие неотесанные библиотеки C++.

Вместо этого, определиться в пространстве имен, как правило, имя проекта, и любого одного элемента можно определить в этом пространстве имен.

4. Использовать enum classи выбор базового типа.

Любой новый enum действительно должны быть enum class, который представляет собой область для переписчиков, а не впрыскивать их в окружающую сферу.

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

Я также призываю вас, чтобы выбрать тип. По умолчанию компилятор будет использовать int, который является... 3 байта слишком широкий в вашем случае. Вы можете легко использовать int8_t вместо этого, он достаточно большой.

Также, вы могли бы также обойтись без бессмысленных сокращений.

Это дает:

#include <cstdint>

namespace mydate {
enum class Month: std::int8_t {
January = 1, February, March, April, May, June,
July, August, September, October, November, December,
};
}

В качестве бонуса, enum class можно вперед заявленных пользователями.

5. Исключения должны наследовать std::exception.

Это Конвенции, что большинство все следуют, которая позволяет ловить все исключения и позволяют демонстрировать что-то с .what() который бьет ... руки вниз.

6. Общественный первый, пожалуйста.

Файлы заголовков часто печатали в ваших клиентов.

Поэтому, это хорошая идея, чтобы разоблачить ваш интерфейс класса в следующем порядке: public, protected, private. Это минимизирует количество ненужных линий, которые пользователь должен пропустить.

7. Не должно быть никакого глобального состояния.

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

Он также страдает от данных гонок, однако только пощечины thread_local не панацея: он не решает местные рассуждения проблему и позволяет клиентам, используя различные модели потоков, такие как сопрограммы.

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

8. Глобальные константы-это хорошо.

Но они должны быть константами, следовательно:

static std::array<std::string, 12> const monthNames;

Используя std::array является предпочтительным. Вы получаете некоторые плюсы, такие как границы, проверяющие, легкая добыча sizeи т. д...

9. Выберите ваши типы.

int это типа по умолчанию, но это не очень разумно.

У вас уже есть Month перечисление, почему не используется для _month?

Я также рекомендую использовать Day тип, или, по крайней мере, используя std::int8_t опять же, поскольку нет никаких причин, чтобы иметь в день больше, чем 127.

10. Вспомогательные функции без гражданства.

В leapYear функция не имеет доступа к thisтак он не должен быть метод класса.

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

11. Никогда не вернуть const значение.

Это довольно бессмысленно, так как клиент может все равно копирует его.

Вы, возможно, пожелают вернуть T const& (константная ссылка). К сожалению, это означает, что клиенты узнают, что ваш класс содержит Tтак что если вы хотите, чтобы изменить внутреннее устройство класса, клиенты будут ломаться.

12. По умолчанию это тяжело.

Если нет значения по умолчанию для своего типа, рассмотреть не определен конструктор по умолчанию.

Это довольно ограничительным, конечно, так можно рассмотреть:


  • определение недопустимого значения, хотя это означает, что (1) Ваш класс должен теперь иметь isValid метод и (2) на каждый геттер ваш класс должен проверить, является ли он допустимым.

  • определение значения по умолчанию, тем не менее, и документ. В данном случае, это может быть хорошо, чтобы выбрать вряд ли значение, например, 1 января года 1, чтобы помочь клиентам понять, когда они забыли, чтобы установить значение.

13. Остерегайтесь интерфейсы типизации.

Формат для нас даты: ММ/ДД/гггг, в ISO формате гггг/ММ/ДД. Когда вы подарите конструктор Date(int, int, int)это очень легко для клиента, чтобы случайно не передать аргументы в неправильном порядке.

В этом смысле Date(int, Month, int) это шаг вперед, поскольку американцы не могут случайно использовать их странный формат, но все равно можно случайно использовать ISO вместо ДД/ММ/гггг, как вы предполагаете.

Есть 2 возможности оттуда:


  • Только предложение со строгой типизацией аргументов: Date(Day, Month, Year),

  • Использовать именованные конструкторы вместо этого.

Последний делается с static методы:

static Date from_date_month_year(int day, Month month, int year);

Примечание: хотя достаточно ясно, что int day будет иметь диапазон [1, 31]неясно, будет ли int month имеет ряд [0, 11] или [1, 12]; не искушать пользователей.

14. Назовите ваши параметры.

Здесь d наверное, понял, но в общем я прошу дать параметры полные имена. Аббревиатуры не нужно вообще.

15. Правило ноль

Правило нулевого утверждает, что нужно редко (если вообще) определить копирующий конструктор, копирующий оператор присваивания, конструктор перемещения, перемещения и оператор присваивания или деструктор.

Надо только на технических ресурсов классов, таких как unique_ptr или vector. Если вы когда-нибудь окажетесь написание такого класса, будьте уверены, чтобы сделать его как можно более минимальный; без каких-либо бизнес-логики.

16. Будьте последовательны.

Ваш интерфейс иногда использовать int за месяц (где это неоднозначно), а иногда Month. Придерживаться Month.

17. Следуйте конвенциям.

Тип возвращаемого operator++ и operator++(int) никогда не как const-квалифицированный, чтобы обеспечить сцепление.

18. Быть редкостью в вашем распоряжении.

Вы никогда не используете ostream, так что вы можете также включать только <iosfwd> в вашем заголовке.

Минимизируя количество включает в заголовках помочь с времени компиляции.


Я буду останавливаться здесь, как это много исправить уже.

Поздравления на написание Date класс :)

8
ответ дан 24 февраля 2018 в 07:02 Источник Поделиться

[код] заголовок охранника

Лично я бы порекомендовал просто используя #pragma once. Он поддерживается большинством компиляторов, это меньше набирать, меньше работы для препроцессора, и гораздо меньше подвержен ошибкам (переименование файлов и забыв переименовать определить, проблемы с зарезервированными именами, опечатки в определении / чек, и т. д.)

[дизайн] по умолчанию

Статические функции по умолчанию, вероятно, не должно быть внутри класса, так как она склонна ошибки и берет на себя пользователи потребностей (например, что если им нужны два разных дефолтов? Им придется постоянно менять его вдоль и поперек, что делает его бессмысленным).

Если пользователь хочет по умолчанию, они могут хранить постоянно Date экземпляр где-то сами, и передайте его в конструктор достаточно легко. Это также делает его мгновенно очевидно, что значение объекта intialized является:

const Date defaultDate(23, Date::Month::mar, 3);

Date d1; // no idea what value this has... it depends what the default is at the moment! we now have to find out if this is set anywhere else in the codebase, AND then understand which code paths lead from there to here...
Date d2(defaultDate); // it's set to the value of defaultDate!

[дизайн] проверка значений

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

(Ошибка:) тем не менее, Способ fillDate В настоящее время определяется, то попробуйте установить каждый компонент по отдельности, а затем проверить, если весь объект Date по-прежнему действует на каждом шагу. Это может вызвать проблемы, потому что промежуточную дату может быть действительна (и так мы успешно изменить день или месяц), но не окончательная дата. Это означает, что хотя мы и выдаст ошибку, начальной датой валютирования до сих пор не изменилась.

Кроме того, установив значение, затем проверка, затем установить его обратно и бросать неэффективно.

Вместо этого мы могли бы просто сдать номера должны быть проверены на проверяя функцию (т. е. isIllFormed) перед установкой ничего. Это может также упростить конструкцию и назначение:

Date::Date(): // default constructor provided
_day(1), _month(1), _year(2000) { }

Date::Date(int d, int m, int y)
: Date() { // use delegating constructor then call a single point of checking / setting

set(d, m, y);
}

Date::Date(const Date& that)
: Date() {

set(that.day(), that.month(), that.year());
}

Date& Date::operator=(const Date& that) {

set(that.day(), that.month(), that.year());

return *this;
}

void Date::set(int day, int month, int year)
{
if (!isValidDate(day, month, year)) // this check and the throw could be perhaps abstracted into a throwIfInvalid function...
throw BadDate(day, month, year);

_day = day;
_month = month;
_year = year;
}

void Date::setDay(const int d) {

if (!isValidDate(d, month(), year()))
throw BadDate(d, month(), year());

_day = d;
}
void Date::setMonth(const int m) {

if (!isValidDate(day(), m, year()))
throw BadDate(day(), m, year());

_month = m;
}
void Date::setYear(const int y) {

if (!isValidDate(day(), month(), y))
throw BadDate(day(), month(), y);

_year = y;
}

// isValidDate is just isIllFormed with the logic reversed

isValidDate и isLeapYear может быть статической функции, а также может быть публичной, поскольку они были бы полезны для пользователей, которым необходимо проверить входной сигнал перед созданием класса date:

static bool isValidDate(int d, int m, int y);
static bool isLeapYear(int y);

6
ответ дан 24 февраля 2018 в 01:02 Источник Поделиться

В дополнение к многие вопросы, затронутые в 3 существующие ответы, есть еще много других вопросов в этой конструкции.

Назначение


  1. Оператор = является неэффективным. Если проверка была должным образом сделано, за исключением так, что невозможно создать недопустимую дату, тогда вам не нужна проверка... и даже, если хотите проверку, вам не нужно делать это 3 раза.

  2. Отдельные функции, такие как setDay, setMonth и setYear не имеет смысла на сегодняшний день. Вы почти всегда хотите установить все 3 компонента сразу. И за те несколько раз, что вы хотите обновить только некоторые компоненты, вы всегда можете прочитать детали, которые не изменить.

Строительство


  1. Заказать конструктор необычен. Почти все будут считать, что порядок-это год, месяц и день и делать что-либо только вызвать больше ошибок.

  2. Настройки значение по умолчанию не имеет смысла. Кроме того, он не является потокобезопасным. Если вам нужны стандартные значения, можно сделать некоторые константы. Дата по умолчанию обычно минимальная поддерживаемая даты (например. 0001-01-01).

  3. Кроме того, вы не должны иметь значения по умолчанию для каждого параметра в конструктор принимать int, а Month и еще int. По умолчанию было очень редко быть соответствующие, Поэтому, если кто-то создать дату с некоторыми значениями по умолчанию, то код будет труднее понять. Также как ваш конструктор не определен явно, то это может привести к нежелательным конструктор преобразования называют в некоторых контекстах.

Операторы инкремента и декремента


  1. Ваши операторы постфиксного не возвращает старое значение, как ожидалось. Как уже упоминалось в других отзывах, возвращаемый тип должен быть const. Вы должны почти всегда имитируют то, что делается на предопределенных типов.

  2. Ваши операторы не правильно обрабатывать месяц и год изменения. Если вы предоставляете таких операторов, ТО, по крайней мере, вы должны убедиться, что приращение последний день месяца вернется первый день следующего месяца. И для декрементирования, вы должны пойти в последний день предыдущего месяца. И вы наверное также хотите, чтобы убедиться, что ваше свидание остается в пределах допустимого диапазона.

  3. Рекомендуется код оператора постфиксного с помощью оператора префикс, чтобы избежать дублирования кода. Вы можете легко написать что-то вроде:

    Date operator++(int)
    {
    Date result(*this); // Make a copy
    ++*this; // Increment this object
    return result; // Return old value
    }

Читабельность


  1. Используйте отступы в выражение. Ваш должен иметь пространство вокруг каждого бинарного оператора в выражении (а также после разветвления ключевые слова). Так что для экс. if(y%4) действительно должны быть if (y % 4).

  2. Вы должны избегать коротких имен переменных, как d. Написав day вместо этого код намного легче читать.

  3. Вы не должны писать void для пустой параметр, списки в C++, как в: Date::~Date(void). Еще хуже, ваше определение не согласуется с вашим заявлением.

Вопросы проверки


  1. Если вы пытаетесь изменить несколько частей свидание, как идти от 2017-03-31 для 2016-02-29это будет ошибкой, если вы делаете проверку после установки день и месяц, но не год.

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

Модульное тестирование


  1. Прежде чем писать библиотеку, как это, вы должны написать юнит-тесты. Многие проблемы, как ошибочные постфикс результате оператор может легко быть обнаружены с правильно написанным тестом.

Заключение

Учитывая большое количество вопросов (между 30 и 45 вопросов), я бы порекомендовал вам прочитать кучу хороших книг по c++, прежде чем писать более "библиотека" кодекс. Я настоятельно рекомендую Мейерс и Саттер книги среди других.

Также другие библиотеки могут быть использованы в качестве источника вдохновения даже на другом языке, как DateTime struct в .Чистая. В этом случае значение является неизменяемым, который является еще более ценный дизайн.

2
ответ дан 27 февраля 2018 в 01:02 Источник Поделиться