Pointers |
|
Consider the declaration, int i = 3 ; This declaration tells the C
compiler to:
Reserve space in memory to hold the
integer value. Associate the name i with this memory
location. Store the value 3 at this location.
We may represent i 's location in the memory by the following memory map:
We see that the
computer has selected memory location 6485 as the place to store the value
3. This location number 6485 is not a number to be relied upon, because
some other time the computer may choose a different location for storing
the value 3.
We can print this address through the following statement
: Look at the printf( ) statement carefully. '&'
used in this statement is C's 'address of' operator. The expression
&i returns the address of the variable i, which in this
case happens to be 6485.
The other pointer operator available in C is '*', called
'value at address' operator. It returns the value stored at a
particular address. The 'value at address' operator is also called
'indirection' operator.
Observe carefully the following statements :
printf ( "\nAddress of i =
%u", &i ) ; /* o/p : 6485 */
printf ( "\nValue of i =
%d", i ) ; /* o/p : 3 */
printf ( "\nValue of i =
%d", *( &i ) ) ; /* o/p : 3 */
Note that printing the value of *( &i ) is
same as printing the value of i.
Let us now see what are pointers and how they can be used
in various expressions. We have seen in the above section that the
expression &i returns the address of i. If we so desire
this address can be collected in a variable by saying,
j = &i ;
But remember that j is not an ordinary variable
like any other integer variable. It is a variable which contains the
address of another variable ( i in this case ).
Since j is a variable, the compiler must provide
it space in memory. The following memory map illustrates the contents of
i and j.
As you can see, i 's value is 3 and j 's
value is i 's address.
But, we can't use j in a program without declaring
it. And since j is a variable which contains the address of
i, it is declared as,
int *j ;
This declaration tells the compiler that j will be
used to store the address of an integer value - in other words j
points to an integer.
How do we justify the usage of * in the
declaration,
int *j ;
Let us go by the meaning of *. It stands for 'value at
address'. Thus, int *j would mean:
value at address stored in j is an
int j contains address of an
int j points to an int
j is a pointer which points in the
direction of an int So we can conclude that pointer is a variable which contains address of
another variable. More pointer
types char ch = 'A' ;
float a = 3.14
;
long int j = 40000L ;
char *dh ;
float *b ;
long int *k ;
dh = &ch ;
b = &a ;
k = &j ; Pointer to
Pointer int i = 10 ;
int *j ; // integer
pointer
int **k ; // pointer to an
integer pointer
int ***l ; // pointer to a
pointer to an integer pointer
j = &i ;
k = &j ;
l = &k ;
// print 10 using i, j, k,
l
printf ( "%d %d %d %d", i,
*j, **k, ***l ) ; Pointer
Arithmetic Addition of a number to a pointer. For
example, int i = 4, *j, *k ;
j = &i ;
j = j + 1 ;
j = j + 9 ;
k = j + 3 ;
Subtraction of a number from a pointer.
For example, int i = 4, *j, *k ;
j = &i ;
j = j - 2 ;
j = j - 5 ;
k = j - 6 ;
Subtraction of a pointer from a pointer.
For example, int i = 4, j = 5, *p, *q, d
;
p = &i ;
q = &j ;
d = q - p ;
The following program catches the essence of all that we have said
about pointers so far. main( )
{
float a
= 3.14, *b ;
char ch
= 'z', *dh ;
int i =
25, *j ;
printf (
"%u%u%u%u", &a, &ch, &i ) ; /* prints addresses of a, ch and i
*/
b =
&a ; /* assigns address of a to b */
dh =
&ch ; /* assigns address of ch to dh */
j =
&i ; /* assigns address of i to j */
printf (
"%u%u%u", *b, *dh, *j ) ; /* prints value at address stored in b,
dh and j
respectively */
printf (
"%u%u%u%u", b, dh,j ) ; /* prints the value in b, dh, j which is
the
address of a, ch, j respectively*/
b++
; /* makes the pointer b point to a location adjacent to
variable a */
dh++ ;
/* makes the pointer dh point to a location adjacent to variable ch
*/
j++ ;/*
makes the pointer j point to the location adjacent to variable i */
/* += -=
operators can be used in pointer arithmetic */
b+= 3 ;
dh += 8 ;
j-= 3 ;
printf (
"%u%u%u%u", b, dh, j );
}
near, far and huge Pointer While working under DOS only 1 mb (10,48,580 bytes) of
memory is accessible. Any of these memory locations are accessed using CPU
registers. Under DOS the CPU registers are only 16 bits long. Therefore,
the minimum value present in a CPU register could be 0, and maximum
65,535. Then how do we access memory locations beyond 65535th byte? By
using two registers (segment and offset) in conjunction. For this the
total memory (1 mb) is divided into a number of units each comprising
65,536 (64 kb) locations. Each such unit is called a segment. Each segment
always begins at a location number which is exactly divisible by 16. The
segment register contains the address where a segment begins, whereas the
offset register contains the offset of the data/code from where the
segment begins. For example, let us consider the first byte in B block of
video memory. The segment address of video memory is B0000h (20-bit
address), whereas the offset value of the first byte in the upper 32K
block of this segment is 8000h.
Since 8000h is a 16-bit address it can be easily placed
in the offset register, but how do we store the 20-bit address B0000h in a
16-bit segment register? For this out of B0000h only first four hex digits
(16 bits) are stored in segment register. We can afford to do this because
a segment address is always a multiple of 16 and hence always contains a 0
as the last digit. Therefore, the first byte in the upper 32K chunk of B
block of video memory is referred using segment:offset format as
B000h:8000h. Thus, the offset register works relative to segment register.
Using both these, we can point to a specific location anywhere in the 1 mb
address space.
Suppose we want to write a character `A' at location
B000:8000. We must convert this address into a form which C understands.
This is done by simply writing the segment and offset addresses side by
side to obtain a 32 bit address. In our example this address would be
0xB0008000. Now whether C would support this 32 bit address or not depends
upon the memory model in use. For example, if we are using a large data
model (compact, large, huge) the above address is acceptable. This is
because in these models all pointers to data are 32 bits long. As against
this, if we are using a small data model (tiny, small, medium) the above
address won't work since in these models each pointer is 16 bits
long.
What if we are working in small data model and still want
to access the first byte of the upper 32K chunk of B block of video
memory? In such cases both Microsoft C and Turbo C provide a keyword
called far, which is used as shown below,
char far *s = 0XB0008000;
A far pointer is always treated as 32 bit pointer
and contains both a segment address and an offset.
A huge pointer is also 32 bits long, again containing a
segment address and an offset. However, there are a few differences
between a far pointer and a huge pointer.
A near pointer is only 16 bits long, it uses the contents
of CS register (if the pointer is pointing to code) or contents of DS
register (if the pointer is pointing to data) for the segment part,
whereas the offset part is stored in the 16-bit near pointer. Using near
pointer limits your data/code to current 64 kb segment.
The following table captures the essence of these
different types of pointers along with the pointer type supported by each
memory model.
A far pointer (32 bit) contains the segment as well as the offset. By using far pointers we can have multiple code segments, which in turn allow you to have programs longer than 64 kb. Likewise, with far data pointers we can address more than 64 kb worth of data. However, while using far pointers some problems may crop up as is illustrated by the following program. main( )
{
char far
*a = OX00000120;
char far
*b = OX00100020;
char far
*c = OX00120000;
if ( a
== b )
printf (
"Hello" ) ;
if ( a
== c )
printf (
"Hi" ) ;
if ( b
== c )
printf (
"Hello Hi" ) ;
if ( a
> b && a > c && b > c )
printf (
"Bye" ) ;
}
Note that all the 32 bit addresses stored in variables
a, b, and c refer to the same memory location. This
deduces from the method of obtaining the 20-bit physical address from the
segment:offset pair. This is shown below.
00000 segment address left
shifted by 4 bits
0120 offset
address
--------
00120 resultant 20 bit
address
00100 segment address left
shifted by 4 bits
0020 offset
address
--------
00120 resultant 20 bit
address
00120 segment address left
shifted by 4 bits
0000 offset
address
--------
00120 resultant 20 bit
address
Now if a, b and c refer to same
location in memory we expect the first three ifs to be satisfied.
However this doesn't happen. This is because while comparing the
far pointers using == (and !=) the full 32-bit value is used and
since the 32-bit values are different the ifs fail. The last
if however gets satisfied, because while comparing using > (and
>=, <, <= ) only the offset value is used for comparison. And the
offset values of a, b and c are such that the last
condition is satisfied.
These limitations are overcome if we use huge
pointer instead of far pointers. Unlike far pointers
huge pointers are `normalized' to avoid these problems. What is a
normalized pointer? It is a 32- bit pointer which has as much of its value
in the segment address as possible. Since a segment can start every 16
bytes, this means that the offset will only have a value from 0 to
F.
How do we normalize a pointer? Simple. Convert it to its
20-bit address then use the the left 16 bits for the segment address and
the right 4 bits for the offset address. For example, given the pointer
500D:9407, we convert it to the absolute address 594D7, which we then
normalize to 594D:0007.
huge pointers are always kept normalized. As a
result, for any given memory address there is only one possible huge
address - segment:offset pair for it. Run the above program using
huge instead of far and now you would find that the first
three ifs are satisfied, whereas the fourth fails. This is more
logical than the result obtained while using far.
But then there is a price to be paid for using
huge pointers. Huge pointer arithmetic is done with calls to
special subroutines. Because of this, huge pointer arithmetic is
significantly slower than that of far or near pointers.
Tips:
Addresses must always be printed using
%u or %p. If %p is used address is printed in
segment:Offset form. If %u is used only offset
address is printed (specific to DOS). In DOS there are three types of pointers
: near (2 bytes), far (4 bytes) and
huge (4 bytes). >
In Unix and Windows every pointer is a 4 byte
entity.
A pointer when incremented always points
to an immediately next location of its
type. The only legal pointer arithmetic is
: pointer
+ number,
pointer
- number,
pointer
- pointer.
Don't attempt the following arithmetic
operations on pointer. They won't work : Dividing a pointer with a
number |
|
Designed and
Managed by |
|