To be honest, I never learned how to think in Object Oriented Programming (OOP) models.
Simply, your programs aren't big enough, or you're not sharing the code amongst independent programs much.
If you were, you'd find the OOP stuff much more intuitive as it solves these needs readily and easily.
The simplest example, especially for a C developer, is probably standard file I/O, ye old FILE *fp.
In C, you pass the context to all of the stdio IO routines (fread, fwrite, fopen, fclose). In a (typical) OOP system, rather than passing the context to the function, you call the function on the context. The context has code attached to the instance of the object.
You also have the stdio routines in your global namespace. Which is why we have fread, fwrite etc, because read and write were already taken by low level I/O.
Consider this simple routine (apologies if this is imperfect it's representative, rather than normative)
Code:
int count(FILE* fp) {
char ch;
int cnt;
ch = getc(fp);
while(ch != EOF) {
cnt++;
ch = getc(fp);
}
return cnt;
}
Given a file pointer, you can count the number of characters in the file.
The issue here is, as is, this is as far as you can go.
Here's two use cases:
Code:
FILE *fp = fopen("file.dat", "r");
printf("file had %d characters", count(fp));
and
Code:
printf("stdin has %d characters", count(STDIN));
STDIN is predefined FILE pointer to represent standard input.
But that's as far as you can go.
Now let's consider Java.
Code:
public int count(InputStream is) {
int cnt = 0;
int b;
b = is.read();
while(b != -1) {
cnt++;
b = is.read();
}
return cnt;
}
Very similar to the C code. The key difference is we're using InputStream vs a FILE *.
Here's how it comes in to play as a difference:
Code:
FileInputStream fis = new FileInputStream("file.dat");
int cnt = count(fis);
System.out.println ("file has " + cnt + " bytes");
cnt = count(System.in); // System.in is the STDIN as an InputStream
System.out.println ("stdin has " + cnt + " bytes");
String str = "This is a test";
StringBuffer sb = new StringBuffer(str);
StringInputStream sis = new StringBufferInputStream(s);
cnt = count(sis);
System.out.println("StringBuffer has " + cnt + " bytes");
ByteArray ba = str.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream();
cnt = count(bis);
// This number may be different than the previous example,
// for reasons out of scope in this discussion
System.out.println("String byte array has " + cnt + " bytes");
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
cnt = count(socket.getInputStream());
System.out.println ("socket client sent " + cnt + " bytes");
Here you can see InputStream as the universal abstraction across all of these different higher level concepts. Files, buffers, sockets, etc. The actual code is the same. This "count" code can be shared readily across programs in these different domains.
But an important concept is that the "read" method from InputStream, while named the same across all of the constructs, all do different things. Any byte level data structure can have expose an InputStream. An image, a sound, etc. Anything that's worth iterating over at a byte level can be made manifest as an InputStream. But the namespace is tied to the implementation class, rather than the entire of the file.
C++ solved these kind of problems two different ways. One was through standard OOP inheritance and method dispatch, much like Java. The other is through generic programming via Templates.
Whereas in standard OOP, that "count" method would be written once and rely on dispatch via the implementation class to do the work, C++ Templates would have essentially re-written the "count" method for each use case. It's a different mechanism of abstraction.
Now, in practice, most applications themselves do very little OOP. They rely a lot on OOP frameworks (such as the java.io system mentioned here), but rarely have to implement their own. Most folks are just writing code to do work: process a file, handle some input, do some math. This is typically very rote work. Input -> Magic -> Output.
The utility space is where you'll see OOP more in action. I/O frameworks, data structures, GUIs. Common code used by lot of programs where re-use and extensibility are key long term values.
In all of my work over the years, in Java, I've created very few base classes or interfaces for our own work. There's certainly been some, and they were re-used heavily. But when 90% of your work is pushing stuff in to and pulling stuff out of SQL data bases, which is what the majority of back office work is nowadays, there's just not a lot of code sharing. The Person table isn't like the Invoice table at all when the rubber meets the road, and that's the code that I write. Someone else did the interface to the database, in a nice generic way so I could swap that out to most any database I like.
In C the Postgres C library is completely different from the MySQL C library. If you wanted to switch databases, you would you have to rewrite good chunks of your code. In Java, there's a generic layer that handles that for you, so if you wanted to switch, most of your code would not have to be rewritten.
You could write a generic layer in C, but C doesn't make that easy. It just relies on idioms (structs of function pointers, essentially). The technique is there, it's just not a first class citizen.
But most folks enjoy the benefits of OOP without necessarily writing OOP style code themselves. In fact they get themselves in to trouble trying to force it on their application that doesn't need it. OOP is great for infrastructure, most folks don't write infrastructure.
As systems get larger and more complex, when more code wants to be reused, then concepts like OOP make things easier. Obviously there are large systems in C, nothing is impossible, but OOP structure can make these systems easier. OOP rose out of frustrations with baser languages like C.