Classes 101: Encapsulation
What sets C++ apart from C? I'll tell you what: A bucketload of stupid angle-brackets, that's what.
But you were probably thinking "it has classes". Well, read on.
Here's some Java, because I don't really have much C++ experience:
public class DebitAccount { private int dollars; public DebitAccount(int dollars) { if(dollars < 0) throw new IllegalArgumentException( "this is not a loan"); this.dollars = dollars; } public int getDollars() { return this.dollars; } public void depositDollars(int dollars) { if(dollars < 0) throw new IllegalArgumentException( "this is not a withdrawal"); this.dollars += dollars; } public void withdrawDollars(int dollars) { if(dollars < 0) throw new IllegalArgumentException( "this is not a deposit"); if(dollars > this.dollars) throw new IllegalArgumentException( "you are not that rich"); this.dollars -= dollars; } }
The important thing to express here is that we are dealing with encapsulation. So in our *.c file, we start with this:
struct debit_account { int dollars; };
(I prefer snakes to camels.)
Sounds pretty straightforward, but what about those functions?
Make a file called debit_account.c. Add in whatever headers you need. As we're using malloc() and whatnot you'll probably want at least this one:
We will want both a constructor and a destructor:
struct debit_account *new_debit_account(int dollars) { if(dollars < 0) fprintf(stderr, "this is not a loan"); return NULL; } struct debit_account *d = malloc( sizeof(struct debit_account)); d->dollars = dollars; return d; } void delete_debit_account(struct debit_account *d) { free(d); }
Main reason to use delete_debit_account(d) instead of free(d) is so we can extend the debit account to have pointers within it and whatnot. Besides, with link-time optimisation (the -flto flag in gcc and clang), it just inlines the call anyway. (Oddly enough, the Java virtual machine does this sort of stuff at runtime.)
int debit_account_get_dollars(struct debit_account *d) { return d->dollars; }
Yes, you will have to explicitly pass your debit account as a parameter. Then again, it's really just a matter of syntactic sugar:
But what about more interesting methods?
int debit_account_deposit_dollars( struct debit_account *d, int dollars) { if(dollars < 0) { fprintf(stderr, "this is not a withdrawal\n"); return 1; } d->dollars += dollars; return 0; }
Notice how C doesn't have exceptions built into the language. Oh wait, it's got setjmp and longjmp. But it's more C-like to just return an error code. Besides, it's more predictable that way.
int debit_account_withdraw_dollars( struct debit_account *d, int dollars) { if(dollars < 0) { fprintf(stderr, "this is not a deposit\n"); return 1; } if(dollars > d->dollars) { fprintf(stderr, "you are not that rich\n"); return 1; } d->dollars -= dollars; return 0; }
You can opt to return different error codes, but the most important thing is that the operation failed.
But enough with debit_account.c. Time to move onto debit_account.h.
struct debit_account; /* construction + deconstruction */ struct debit_account *new_debit_account( int dollars); void delete_debit_account( struct debit_account *d); /* public methods */ int debit_account_get_dollars( struct debit_account *d); int debit_account_deposit_dollars( struct debit_account *d, int dollars); int debit_account_withdraw_dollars( struct debit_account *d, int dollars);
And there we go! You have all the public methods in a header file, and absolutely no public fields or any information as to what the struct contains!
And that's pretty much how you encapsulate stuff. If you have any private methods, you'd mark them as static, e.g. static int debit_account_add_dollars(struct debit_account *d, int dollars), and nothing outside of the C file will be able to use it.
Of course, you can pick whatever allocation method you like here. You could even have a bunch of them in an array and it'll dish them out as it sees fit. Heck, you don't even need to give them a pointer. You could give them an array index, and then you can use a dynamically-sized array that will most likely use realloc.
That's the beauty of C. It doesn't do everything for you. What it does, however, is give you the power to choose.
And if you can't make up your mind on how to do something, there are libraries out there. For instance, Boehm-GC is a thing.
Still, the second most important aspect of Object-Oriented Design - Encapsulation - works very nicely in C.
The most important aspect - ruining performance by scattering everything everywhere across memory - works even better. But with C, this thing is completely optional.