简体   繁体   中英

Am I creating my object arrays correctly? C++

I am trying to create a program that stores up to 50 players and the amount of wins they have. ie one use could be for for keeping track of sports teams and their amount of games won or something. However, I am having an issue when changing the player score. I am a beginner at C++, as you will probably be able to tell from my code. However, I have gone from knowing nothing whatsoever about coding to finishing three different tutorials and making a couple very simple command line programs in about a week. Any help as far as coding tips and how to make the scoreEdit() function work is thankfully accepted! Please find attached the players class, the scoreEdit() function, and my main function.

// for case 1. class that the scoreEdit() function uses!
class players
{
public:
void setName(string x)
{
    name = x;
}
void addWin()
{
    amtOfWins += 1;
}
void setWins(int x)
{
    amtOfWins=x;
}
string getName()
{
    return name;
}
int getWins()
{
    return amtOfWins;
}
private:
string name;
int amtOfWins;
};

|

// for case 1. reads the file then stores each name in it's own player object and associates that with the amt of wins. Then rewrites all names and amtofwins to the file
void scoreEdit()
{
ifstream istats("stats.txt");
ofstream ostats("stats.txt");

if (istats.is_open() && ostats.is_open())
{
    players player[50];

    string tempName;
    int tempWins;

    while (istats >> tempName >> tempWins)
    {
        // reads in the name and amt of wins, and stores them in player object variables.
        for (int x=0; x<50; x++)
        {
            player[x].setName(tempName);
            player[x].setWins(tempWins);
        }
    }
    string winner;

    cout << "Who won?" << endl;
    cin >> winner;

    for (int x=0; x<50; x++)
    {
        if (player[x].getName()==winner)
        {
            player[x].addWin();
            cout << "Ok. " << player[x].getName() << " has gained 1 point." << endl;
        }
    }
    int x=0;

    while (ostats << player[x].getName() << ' ' << player[x].getWins())
    {
        x++;
    }
}
else
{
    cout << "Quitting program. Stats could not be opened." << endl;
}
}

|

// main function
int main()
{
int x=0;
cout << "\n";

while (x==0) // not really sure if this is needed. Loops until case 4 or 5. Probably didn't need to use x. Oh well.
{
    switch (choices())
{
    case 0: // clears file
    {
        string decision;

        cout << "ARE YOU SURE? This will wipe all data. (Type yes or no)\n" << endl;
        cin >> decision;

        if (decision=="yes")
        {
            clearStats();
            cout << "STATS ARE WIPED.\n" << endl;
            break;
        }
        else if (decision=="no")
        {
            cout << "Ok, stats will not be wiped.\n" << endl;
            break;
        }
        else
        {
            cout << "Your input was not recognized. Stats will not be wiped.\n" << endl;
            break;
        }
    }
    case 1: // they want to add 1 to a player's score
    {
        scoreEdit();
        break;
    }
    case 2: // they want to add a player
    {
        string name;
        cout << "What is their name?" << endl;
        cin >> name;
        addPlayer(name);
        break;
    }
    case 3: // they want to view the stats
    {
        readStats();
        break;
    }
    case 4: // they want to quit. Simple! :)
    {
        return 0;
    }
    default: // means they did not input 1 2 3 or 4
    {
        cout << "Invalid input. Quitting program. Try again." << endl;
        return 1;
    }
}
}
}

EDIT: PS all my other functions/cases work. I just can't seem to find the problem with this one! Do I need to use a vector?

EDIT 2: Thanks for all the advice. Now, is there a way to check if the file still has data left to input? Right now, it inputs the lines just so that there are 50 player objects no matter what. Can I make a variable like int linesLeftInFile and use for (int x=0; x<linesLefInFile; x++) ? Sorry for all the beginner questions.

You are trying to open the file twice, once for reading, once for writing, but in the same time.

...
ifstream istats("stats.txt");
ofstream ostats("stats.txt");
if (istats.is_open() && ostats.is_open())
...

When you open it for writing, like you do, the file content gets erased. The attempt to read the file will then fail, hence the big mess.

Open your stream first only to read, then close the stream, then open for write and put the results. Alternatively, you could consider an fstream with read and write.

By the way, it would be good practice for your players class to foresee a default constructor.

There are essentially two aspects:

A)

Am I creating my object arrays correctly? C++

which is related to persistence implementation and instance life-cycle issues, and

B)

EDIT: [...] Do I need to use a vector? [...] Right now, it inputs the lines just so that there are 50 player objects no matter what.

which is related to allocation and containers.

Both aspects of course intersect, eg a container like std::vector has much influence on the life-cycle of the instances it hosts.


Both aspects however have little to do with the functionality that is presented to the controller of the application, which is yet realized by the loop in main() .

Thus, dealing with those aspects should be delegated to a class that separates them from the rest of the application.

Let's call that class-to-be PlayerDataBase .


PlayerDataBase will hold a container. But which?

Comlile-time fixed size arrays like

Player c_arra_players[50];
// or
std::array<Player,50> std_array_players;

are out of ansatz anyway; there is an external datum of arbitrary entity count which governs the multiplicity; thus the container must support run-time re-sizing.

A std::vector<Player> is plain and simple to use, but there's a seach to be performed

   for (int x=0; x<50; x++) {
        if (player[x].getName()==winner)
        {
            player[x].addWin();
            cout << "Ok. " << player[x].getName() << " has gained 1 point." << endl;
        }
    }

which would have O(n) complexity on a sequential container like std::vector , whereas the same search on an associative like std::map (with the names used as keys) would be of O(log(n)) and about O(1) if std::unordered_map` would be used along with a perfect hash function.

On the other hand, adding and especially renaming players (use case: fixing typo in a nick) would be become more expensive for associative than sequential containers.

Especially for C++ learners it may be interesting to play a little with different internal storage representations, thus PlayerDataBase could be a template with a container template parameter.


There should only be one PlayerDataBase instance in the application and the controller should have a single point of access to that instance; thus it is meant to be a singleton.


The ifstream/ofstream/fstream issues can be dealt with quiet simple by PlayerDataBase - it's (private) ctor reads, or creates and reads, a stats file from an ifstream which is closed after the ctor completed.

Persisting the data is done by a flush() function, which opens an ofstream by using the _file_name member, writes, and that stream is closed on flush terminating.

///
///@brief modified singleton pattern
///    
template<template<typename...> class Cont>
class PlayerDataBase : private players_container_adaptor<Cont> {
  private: 
    using Traits  =  players_container_traits<Cont>;

  public: // type interface
    using container_type = typename Traits::players_container_type; 
    using Base =  players_container_adaptor<Cont>;

    struct no_such_player : public std::runtime_error {
        no_such_player(const std::string& msg) : std::runtime_error(msg) {}
    }; 

  public: // creation interface 
    static PlayerDataBase& instance(const std::string& file_name = ::FILE_NAME) 
                                                   throw (std::runtime_error) {
        // automatically dtored 
        static PlayerDataBase _instance_(file_name); // throws
        return _instance_;
    }

  public: // behaviour interface    
    void flush () const throw(std::ios::failure);
    void addWin(const std::string& key) throw (no_such_player);

  private: // types and function name resolution
    using Adaptor = Base;
    using Adaptor::getPlayer;     // throws std::runtime_error 
                                  // container specific lookup,   

    using Adaptor::make_inserter; // std::copy(..,..,,make_inserter(Cont & c));
    using Adaptor::emplace;       // dispatches to container_specific emplace 

    using _asset_type = typename Traits::asset_type; 

    constexpr static auto  BAD_AND_FAIL = std::ios::badbit | std::ios::failbit;
    constexpr static auto  BAD_ONLY     = std::ios::badbit;

    struct no_stats_file : public std::runtime_error {
        no_stats_file(const std::string& msg) : std::runtime_error(msg) {}
    }; 

  private: // lifecycle interface       
    PlayerDataBase(const std::string&) throw (std::runtime_error);
    ~PlayerDataBase() noexcept; 

    // This is a singleton
    PlayerDataBase(const PlayerDataBase&) = delete;
    PlayerDataBase(PlayerDataBase&&) = delete;
    PlayerDataBase& operator=(PlayerDataBase&&) = delete;
    PlayerDataBase& operator=(const PlayerDataBase&) = delete;
    PlayerDataBase& operator=(PlayerDataBase&) = delete;

  private: // helpers 
    void create_data_file() const throw(std::ios::failure);

  private: // state 
    container_type  _players; 
    std::string     _file_name;
}; // class PlayerDataBase

#include "playerdatabase.inl"

The required traits and adaptor templates could look like this:

template<template<typename...> class C> struct players_container_traits {};

// [...]

template<> struct players_container_traits<std::map> {
    using players_container_type = std::map<std::string, Player>;
    using player_ckv_type        = players_container_type::value_type;
    using player_kv_type         = std::pair<std::string, Player>;
    using asset_type             = player_kv_type;
}; // struct player_container_traits<std::map>

// [...]

template<template<typename...> class C> struct players_container_adaptor{};

// [...]

template<> struct players_container_adaptor<std::map> {
    using Traits = players_container_traits<std::map>;
    using players_map_type = Traits::players_container_type;

    static void post_ctor(players_map_type&) {
        /* nop  */
    }    

    static Player& getPlayer(players_map_type& m, const std::string& key) 
                                                throw (std::runtime_error) {
        auto it  = m.find(key);
        if (it == m.end())
            throw std::runtime_error(key + " unknown");

        return it->second;
    } // addWin(players_map_t&,...)  

    static auto make_inserter(players_map_type& m)
        -> decltype(std::inserter(m, m.begin())) {
        return std::inserter(m, m.begin());
    }

    template<typename... Targs, typename K, typename... Args>
    static void emplace(std::map<Targs...>& m, K&& k, Args&&... args) {
        K _k = k;
        m.emplace(std::piecewise_construct,
                    std::forward_as_tuple(_k),
                    std::forward_as_tuple(k, args...));
    }

}; // template<> struct players_container_adaptor<std::map> 

I postulated that the business class would be renamed to Player and gets a bunch of (noexcept) ctors and some related custom operators for << >>' for ostream resp. istream` output.

Happy hacking!

Addendum Jul 20th 2014 I don't see much reason for random sequence access anyway, thus if a sequential container would nevertheless be used (what I would not advocate due to the find complexity) then std::list would be more appropriate; that saves costs for possible complete, and expensive, re-allocations std::vector suffers from.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM