You are plagued with the same bug I am ;) I'm sure there is information out there on this sort of thing, but I have never seen anything specific on how stuff works. Hopefully, someone else can post something they've come across, but I doubt there's much of a market for such books.
Much of this knowledge comes from studying computer architecture, and not from higher languages or programming technologies. For better or worse, the trend for some time has been away from the basics, such as assembler, and more towards "easier" languages.
I can touch on a few things, however (naturally, I'll carry on for a ways :). Structures are actually simple, and form the basis for most "complex", "compound", or "user-defined" types. They are the basis for classes (in C++, structs are classes, with few differences). In C, structs contain only data, and hence the phrase "plain old data" (POD). In C++, functions can also be associated with a struct of data, but the Win32 API is traditionally only in C.
The often-used RECT struct is a good example. Forgetting its actual C-style declaration for the moment, in C++ it looks like:
struct RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
};
In this case, since all member data (fields) are the same size, it's similar to an array (sequence of bytes) in memory:
| left | top | right | bottom |
where each name (identifier) is associated with a memory offset (on a 32-bit machine, these members are each 4-bytes in size). This could just as easily be done in an array, and it would look virtually identical in memory. If this were an array called rcArr (with 4 elements), we would have rcArr[0] for left, rcArr[1] for top, rcArr[2] for right, and rcArr[3] for bottom. As you can tell, one advantage of structs, is that each element has a unique name, rather than a numeric index (offset). Very programmer-friendly, even though the compiler translates top to mean '4 bytes in from the starting address of the RECT'. That's the same way the compiler would translate rcArr[1]
| rcArr[0] | rcArr[1] | rcArr[2] | rcArr[3] |
So we know all data types, including structs (compound data) and arrays (regular sequences of the same data type), are really just strips of memory, where the compiler has already calculated the position of each member, and we don't have to think about it (unlike in assembler). The real beauty of structs is that they can be a mix of all sorts of types:
struct MyStruct {
int int_;
char c;
short sh;
double double__;
};
Ignoring the realities of padding (I really don't have room for that here, pardon the pun), in memory it would look something like this:
| int_ | c | sh | double__ |
The size of each member is different (on a 32-bit machine, 4, 1, 2, and 8 bytes, respectively). The compiler again allocates a strip of memory, and calculates the position (offset) of each named member, except the members are not evenly sized and spaced as with arrays.
Like an array, structs/classes are usually too big to pass (copy) in their entirety (by value), so a pointer to one (its address) is usually passed (by reference). This makes a RECT, for example, more efficient to use than 4 separate longs. Passing a RECT pointer takes only 4 bytes, and all the data stays in memory you already allocated (why copy everything?). Passing 4 longs means 4*4=16 bytes must be copied (passed).
Here's a fun snippet to "see" the byte mapping of structs/classes...
#include
#include //for getch
#include //for RECT
int main() {
const int iSize = sizeof(RECT);
char buf[32];
//anonymous union
union {
RECT rc;
char chArr[iSize];
};
rc.left = 0;
rc.top = 1;
rc.right = 2;
rc.bottom = 3;
for (int i=0; i < iSize; i++) {
printf("%d,", chArr[i]);
}
printf("\naddress of RECT = %X\n", (int)&rc);
printf("address of left = %X\n", (int)&rc.left);
printf("address of top = %X\n", (int)&rc.top);
printf("address of right = %X\n", (int)&rc.right);
printf("address of bottom = %X\n", (int)&rc.bottom);
getch(); //pause
return 0;
}
You can substitute any struct/class for RECT and rc, and change the printf statements as appropriate. This is easier than staring at memory dumps, though that can be useful too. For those not familiar with unions, they just allow differently named things to occupy the same memory strip, so that they overlap, and anonymous means unnamed.
In VC++, it's easy to get to type definitions. Place the cursor on the named thing, press F12, and you will jump to the file and definition. Often definitions are made up of other definitions, so repeat the process for those in turn. Many handles in Win32 wind up being void pointers or structs with a single int member, for example.