This month, George responds to reader requests for a thorough explanation of pointers in C language. He includes several examples and homework assignments. To get you started,George explains the connection between CPU address bits and memory address bits.
I have had several requests for more information about pointers. Well, here it goes. I'll give you more than you ever wanted to know about pointers.
ADDRESSING
Pointers contain the address of an object and can be used to access that object. Traditionally, pointers have been explained by using arrays and showing their similarities. Because that explanation is available in most discussions about the C language, I‘m going to take a different path in explaining pointers.[1]
Let's look at addressing from a hardware point of view because you‘re going to be dealing with the addresses of memory locations. Assume you have an embedded system with a 32-bit CPU (this helps make the data buses large enough to be interesting), a 16-bit data bus (32-bit data fetches must be done in two operations),and a 32-bit address bus. Now assume you have a 16-bit-wide RAM connected to the CPU. The 16-bit data fetched would be done in one read operation. Consider how 8-bit data fetches would be performed. Perhaps your system would be smaller in one or more of these three features, but this discussion won‘t be much different for your particulars.
The CPU can reference 8-bit data elements and that 8-bit data element is the smallest addressable element in the address space. So, CPU address = 0 addresses is the lowest 8-bit entity. CPU address = 1 address is the next highest entity. We have the granularity required on the CPU address bus. But our 16-bit-wide memory doesn't need the least significant bit (LSB) of the CPU address bus connected to the memory device. With our 16-bit data bus, 8-bit data is received by the CPU bus using signals like byte low enable(BLE) or byte high enable (BHE) to transfer (gate) the 8 bits of data from the 16-bit data bus into the CPU registers. The 16-bit-wide memory attached to the address bus reads two 8-bit data elements at a time. The CPU would send an address of 0 to address the first 16-bit location in your memory and send an address of 2 to address the next 16-bit location. The memory would be connected so that the first address is memory location 0 and the second address is memory location 2. The physical connection for the address bus between the CPU and this memory is shifted to the right one address bit.
If an 8-bit memory is connected to the CPU, address bit 0 is used to get to the smallest element. If a 16-bit memory is connected to the CPU,address bit 1 is used to get to the smallest memory element and BHE and BLE are used to chop that 16-bit element into 8-bit elements.
This explanation can be extended to a 32-bitwide memory. This connection of CPU address bits to memory address bits is important to recognize because pointers use addresses relative to the CPU not the memory device. So much for where the addresses come from. It‘s important for you to understand the introduction before I discuss pointers.
POINTERS
A pointer is simply the address of what you are referencing. The pointer will be the CPU address and can be modified by the hardware connections as it gets out to the physical memory device. It merely points to the address of what you are referencing and contains the address of what you are referencing. Simple enough. See you next time.
If you are interested in working with pointers, then read on. I'll give you some examples, but watch out for any homework assignments that may follow. Listing 1 contains some simple statements declaring pointers to 8-bit objects and working with them. Pointer p1 is set to point to data[0]-or,said another way, p1 contains the address of data[0]. This is done using two different statements. Remember the & operator is the "address of" operation. And the address of the entire data array is the same as the address of the first element in the array &data[0]. If you declared an array in memory, what would the linkage map look like? Even in assembly language,that map would tell you the starting address of the data array or the address of the first element in that array. Also in Listing 1 you see that you copy the value of data array element 0 in variable x. This compiles and works with no errors because p1 and x are the same type of storage. Specifically, they both are of type INT8.
In Listing 1, I tried to put the address of data[99] into p3. The compiler should give a warning. It will compile and produce executable code that does what you told it to do, but the compiler knows that p3 and data[99] are different types. The next statement corrects the warning by informing the compiler to cast the &data[99] to a type of UINT8. So, for just this one statement, you would like to treat data[99] not as an INT8 type,but as a UINT8 type. This second statement would compile with no errors. But now you are mixing data types and give up some of the error checking that the compiler would provide. Finally, look at the statement differencing pointers p1 and p2. It looks like we are just subtracting the two addresses. This should make sense to the more hardware-orientated readers. And from my introduction on addressing, I hope you recognize that it‘s just address math because you are working on the finest element the CPU can address.
In Listing 2, the first for loop zeroes the contents of the data array using an index to access the array. The second for loop zeroes the contents of the data array using a pointer to access the array. Not so long ago, that first method would have compiled to a lot more assembly language code than the second. Think about it: for each loop iteration, the compiler would have to load the address of data[0] and then add i × the size of each data element to that address. In the second method,the pointer is merely advanced by the size of the element the pointer is pointing to. But I'm finding modern compilers treat either approach with the same efficient code. Your first assignment is to check your compiler.
Listing 3 contains the same operations as Listing 1, but now we‘re pointing to INT16-type elements-and those 16-bit elements are found every two CPU address. All the comments about the code remain the same until we get to the differencing of the pointers. p2 is pointing to the twenty-fifth element. p1 is pointing to the 0 element,so the difference is 25 elements, not 50, which would be the numerical difference in the value of the address. Your second assignment is to verify this in your system.
Another way to look at this is found in Listing 4. In the for loop, the i++ statement must advance the pointer to the next address for the data type being referenced. That must work that way to make sense. The differencing of pointers must also follow those rules. Remember i++ is the same as i = i + 1. But because we‘re referencing 16-bit elements, the i gets converted by the size of the element when the compiler compiles data[i]. Also the p++ statement must advance the pointer p by two since we're referencing 16-bit elements. Compile this code and watch the values of i and p as you step through the instructions.
The same extension can be made if you reference INT32 elements (or float elements)。 The address is incremented by four (or the size of the float element) for an increment of the pointer. A pointer is just the address of what you‘re referencing. The compiler knows what you're pointing to and helps out.
Now that you understand pointers a bit more, look at Listing 5. I defined a structure (AIRPLANE_SEATING) that makes room for the names of a pilot, copilot, flight engineer,and 200 passengers. I capitalized it because it is more of a constant than a variable. So now you‘re working with a custom data type of some nonstandard size. Let's see how pointers will work in this situation. Notice how I defined MAX_PLANES but didn‘t use defines for the other array sizes. Perhaps this wasn't such a good idea. What happens when marketing says there‘s a new plane coming with room for 300 passengers?
点击查看Listing 5
You defined a structure to contain all the data (AIRPLANE _SEATING) and set aside memory for (MAX_PLANES) such structures. Next, I defined two pointers p and q. They are going to point to members of the array of structures(AirPlaneSeating[MAX_PLANES])。
The first task is to initialize this array and NULL out the names of pilots. Using an array approach, the first for loop sets the pilot name in each array to NULL by setting the first element in the name to NULL. Remember a string is NULL terminated. This loop uses arrays and array elements to define the exact memory location you‘re operating on.
Using a pointer approach, the next for loop sets all of the pilot names to NULL and all of the passenger names also to NULL. This loop starts with a pointer p being set to the first element (0 element)。 Elements are accessed using the pointer p and the -> operator. That operator accesses structure members using the pointer and the known offset in memory to the desired element. Remember, a pointer is just the address of the object and all the point manipulation is just resolving or calculating the next address.
Next, the inner loop gets a reference for each passenger name (all 200) on that plane. After all the operations are complete, the pointer p is incremented. Think about this a bit. The pointer p is pointing to a user-defined data structure. So, incrementing that pointer would just advance that pointer by the size of each data element. And that size is known at compile time. Thus, it‘s just adding a constant to p. The operator p-> adds a number to the pointer to calculate a specific memory address. Again, if that number is known at compile time, the additional amount will also be a constant.
Look at the differencing of the pointers p and q. The values for j and i again represent the delta in the objects these pointers are referencing, not the numerical difference of the values of the addresses. While this might surprise you (after this time), now you can see what's really going on.
WRAP UPI hope you‘re much more comfortable using pointers. One of the most common uses for pointers is to process an incoming line on a serial port. Processing involves parsing out the different fields and performing appropriate operations. Typically, a pointer p (of type INT8) is set to the start of the line and advanced as characters are decoded.
Next time, I will look at a process for a complete design. I‘ll use UML to help capture and explain the design, and I'll work with the C language for the code. I‘ll tackle a CompactFlash interface design.
If you have requests or questions, let me know. I'll try to work them into a column.
Author‘s note: This is a continuation of my description of the C language and how it's used in embedded systems. If there are terms or concepts in this article that you don‘t understand, please refer to my previous articles about C language. The articles appear in the following issues of Circuit Cellar: 198, 200, 202, 204, 206, 208, 210,212, and 214. If there is anything else that you would like explained, please e-mail me and I will try to explain it in a column so that all of our readers may benefit.
George Martin (gmm50@att.net) began his career in the aerospace industry in 1969. After five years at a real job, he set out on his own and co-founded a design and manufacturing firm (www.embedded-designer.com)。 His designs typically include servo-motion control, graphical input and output, data acquisition,and remote control systems. George is a charter member of the Ciarcia Design Works Team. He‘s currently working on a mobile communications system that announces highway info. He is also a nationally ranked revolver shooter.
REFERENCE
[1] B. Kernighan and D. Ritchie, The C Programming Language (K&R),Prentice Hall PTR, Upper Saddle River, NJ, 1988.