You're confusing two kinds of efficiency. There is efficiency as in "this code is easy to maintain" or "this code isn't twice as long as it needs to be", and then there is efficiency as in "This code took 2 seconds to run"
And, with that said, pointers are never faster with regard to time efficiency (hereafter just efficiency). The compiler gets to make a lot of assumptions about non-pointer variable access that aren't necessarily true of pointers, though in the modern environment the compiler will usually be able to see through them and will often produce the exact same assembly irregardless.
Pointers are also often bigger than the thing they point to. On many 64-bit systems, an int is 4 bytes, but a pointer is always 8. So if you make a pointer to an int variable, you're using 12 bytes total instead of just 4 unless the compiler decides to eliminate it (and yes, your compiler will remove variables from your code once you turn on optimization, among other things).
Arrays are like constant pointers, which I think is another source of your confusion. They're not exactly the same syntactically, but all an array really does is asks the compiler to please allocate some space at compile time instead of relying on the OS. Indeed, arrays decay to pointers, that is to say that the following works:
int array[] = {1, 2, 3};
void foo(int *x) {
// stuff with x.
}
foo(array);
Where the last line could also be written foo(&array[0]).
The other kind of efficiency, more properly called code clarity or good code (as in the sentence "This is good code") is about how easy your code is to understand by other programmers, and how easy it is to maintain when you come back months later. This ties back into the const discussion above, since const is about making better code. And, in that aspect, using pointers unnecessarily is always bad, because (like with the do loop way at the beginning of this thread) it will cause the reader to try to find the reason.
So, let's talk about a concrete use of pointers. A long time ago, I did Libaudioverse, which was one of my attempts at 3D audio. here is the function from its header for making a new BufferNode:
Lav_PUBLIC_FUNCTION LavError Lav_createBufferNode(LavHandle serverHandle, LavHandle* destination);
So let's walk through this (I'm not showing the implementation because that's C++, though the header is C):
First is Lav_PUBLIC_FUNCTION, which can be ignored: it's to do with making functions visible in DLLs.
Then we have LavError, which is an enum, a sort of int where you get to name the values of the int. You'll get to them eventually, but for now it's fine to say it's an int. It returns what went wrong if anything, or 0 if everything was fine.
Then of course, the name of the function, and a handle to a server, which you created previously. Then, apropos to this discussion, LavHandle *destination, which is where the function puts the actual handle if it made one (and if it didn't, it just never writes the pointer).
This is idiomatic use of C pointers for functions which both do something and have errors: the error is what gets returned. This is good because you can do:
LavError error;
LavHandle bn;
error = Lav_createBufferNode(server, &bn);
if(error != 0) {
// handle error, i.e.
return;
}
// do stuff with handle.
And to see why, let's examine our alternatives. First, we could have a struct:
struct LavBufferNodereturnValue {
LavError error;
LavHandle bufferNode;
};
Which works fine except that you need a different one for every single function, and every consumer of the function has to figure out what the return type is. Now you could say that there's a struct for every unique return value, i.e.:
struct HandleError {
Lavhandle handle;
LavError error;
};
Which is fine, except that you will need a version of this for every different group of parameters that a function might want to return. Additionally, it's big, which sometimes matters, and will be copied, which also sometimes matters. Plus, every library will do this differently. Or, you use what's called an out parameter, which is what I demonstrated above.
There are many other places where pointers help. Some examples:
A situation in which you need to read and/or write from one of a bunch of different variables all of the same type, but you don't know which one at runtime. This comes up frequently in data structures like linked lists and trees.
A situation in which a function needs to modify a large variable, say a 1 kilobyte struct with 100 different fields in it. These exist and are actually pretty common when you do bigger programs. For example Libaudioverse's server is a C++ class (but for the purposes of our discussion a struct) which contains something like 4 or 5 other big objects each for a different subsystem which contain which contain and so on for quite a way. If you pass this around as a value, it gets copied every time, and if the functions want to modify it they'd have to return a copy back and copy again on the way out.
Indeed, this second case is how you do classes in C:
// UserDatabase Type comes from somewhere, we don't care right now.
UserDatabaseError MyObject_CreateUser(UserDatabase *o, char *username, char *password);
UserDatabaseErrorMyObject_deleteUser(UserDatabase *o, char *username);
// and so on
And the third most common place pointers come up is function pointers, which are pointers to functions. You will learn about them shortly and have probably seen references to them already, but the idea is this (and I'm using a real example from sqlite):
Say I'm making a database library that's going to store data in the filesystem, but the filesystem could be anything: a disk, a cloud storage offering, a segment of memory, whatever. And maybe the implementation of it doesn't even come from C and the app wanting to use the library has 5 different filesystems it wants to work with simultaneously.
We solve this with pointers to functions: a filesystem only has a few operations, read/write/create/delete really, so we make the user provide function pointers to functions to do that and default to our own if they use NULL instead.
This is a lot, but let me just close with this. Don't try to figure out what is faster in terms of time. If you want some idea what compilers are like, I actually worked on one some years ago, and I did a writeup on what exactly I did here. When it comes to compilers I'm usually the least informed person in the room, so imagine hundreds to thousands of people 4 to 10 times smarter and more experienced than me spending the last 20 years working on your compiler to make it magically make decisions that make your code faster and smaller. Rule 1 of "is this faster" is to not guess but actually measure it and rule 1 of measuring it is that, without a lot of experience in how to do such measurements, you're probably measuring it in an unreliable fashion anyway.
Twitter: @ajhicks1992