A class allows you to group related data together with the functions that operate on it. The data and functions together can then make guarantees (invariants) to users about the resulting "object" behavior.
For example, the standard rand() function -- for generating random numbers -- can cause problems with modern multithreaded programs since rand() was designed without regard for concurrency. There is some state (one or more numbers) that allows rand() to remember where it is in its pseudo-random sequence, and how to generate its next number, but the only access we have to that state is through rand(), and anybody can call rand().
Our first attempt at an improvement might be to expand rand() to accept its state from elsewhere:
Code:
int randInt(int & state)
{
return ((state = state * 214013 + 2531011) >> 16) & 0x7fff;
}
Now it's up to the caller to maintain state:
Code:
void printThree()
{
int seed = std::time(NULL);
int first = randInt(seed); // seed changed
std::cout << first << '\n';
int second = randInt(seed); // changed again
std::cout << second << '\n';
std::cout << randInt(first) << '\n'; // oops?
}
It's easy enough to keep track of just one integer of state, but what if there were a lot more? What if this were the state of some computer game like chess? Or Call of Duty? Never mind large software, what if you're just trying to make sure that other programmers use your function in the right way without making a mistake like above? Things can get out of hand quickly.
It would be better if there were a class for creating random numbers. An instance of the class would contain all of the information necessary to generate random numbers, and the users of the class (us) would not need to know about it, except that when we call instance.next(), it gives us another pseudo-random number.
Code:
int nextRand(int previous)
{
return ((previous * 214013 + 2531011) >> 16) & 0x7fff;
}
class RandInt
{
int previous;
public:
explicit RandInt(int seed);
int next();
};
RandInt::RandInt(int seed)
: previous(seed)
{
}
int RandInt::next()
{
previous = nextRand(previous);
return previous;
}
The class has done nothing except encapsulate what we would have done ourselves -- carry an int around everywhere and pass it into our nextRand function. Now we can just do this:
Code:
void printThree()
{
RandInt randGen(std::time(NULL));
std::cout << randGen.next() << '\n'
<< randGen.next() << '\n'
<< randGen.next() << '\n'; // no bug
}
The bigger your code gets, the more beneficial this kind of thing is. What we gain from the class RandInt is the guarantee that .next() will always be the proper next int in the pseudo-random sequence started with seed.
Better yet, we can pass instances of RandInt around, and the language allows us to specify exactly what happens when a RandInt is moved or copied. By default, it just copies that internal int, and here that's exactly what we want.
A different viewpoint on this problem is that state is the issue, and that you should aim to eliminate rather than hide it. You can read a lot online about object oriented programming versus functional programming, the problem of state... flame wars abound.
My opinion, though, is that programming is about managing complexity, and state is a major source of complexity. And classes are one abstraction in a programmer's toolbox that allows us to keep things simple.