Last Updated:

C++ Pointers

C++ Pointers

Pointers are, without a doubt, one of the most important and challenging aspects of C++. Much of the power of many C++ tools is determined by the use of pointers. For example, they provide support for linked lists and dynamic memory allocation, and they allow functions to modify the contents of their arguments. However, we'll talk about this in later chapters, but for now (i.e., in this chapter) we'll look at the basics of using pointers and show you how to avoid some of the potential problems associated with using them.


When considering the topic of pointers, we will have to use concepts such as the size of the basic C++ data types. In this chapter, we assume that characters occupy one byte in memory, integer values four, floating-point values four, and double floating-point values eight (these sizes are typical of a typical 32-bit environment).


Pointers are variables


that store memory addresses. Most often, these addresses indicate the location in memory of other variables. For example, if x contains the address of the variable y, then the variable, x is said to "point" to y.


A pointer is a variable that contains the address of another variable.
Pointer variables (or pointer variables) must be declared accordingly. The format for declaring a pointer variable is as follows:

type *variable_name;


Here, type means the base type of the pointer, and it must be a valid C++ type. VariableName is the name of the pointer variable. Let's look at an example. To declare variable p with a pointer to an int value, use the following statement.

int *p;


To declare a pointer to a float value, use this statement.

float *p;


In general, using an asterisk (*) before a variable name in a declaration statement turns that variable into a pointer.


The base type of a pointer defines the type of data it will reference.


The type of data that the pointer will reference is determined by its base type. Let's look at another example.

int *ip; pointer to an integer value

double *dp; pointer to double


value As noted in the comments, the variable ip is a pointer to an int value because its base type is the int type, and the variable dp is a pointer to a double value because its base type is double. However, remember: there is no real remedy that can prevent the pointer from referring to "god-knows-what". That's why pointers are potentially dangerous.


Operators used with pointers


Two operators are used with pointers: "*" and "&" The operator "&" is unary. It returns the memory address at which its operand is located. (Remember: an unary operator only needs one operand.) for example when you run the following code snippet

balptr = &balance;


the address of the balance variable is placed in the balptr variable. This address corresponds to a region in the internal memory of the computer that belongs to the balance variable. Execution of this statement had no effect on the value of the balance variable. The purpose of the operator can be "translated" into Russian as the "address of the variable" in front of which it stands. Therefore, the above assignment statement can be expressed as "the balptr variable receives the address of the balance variable". To better understand this assignment, assume that the balance variable is located in memory at address 100. Therefore, after executing this statement, the balptr variable will have a value of 100.


The second pointer operator (*) complements the first (&). It is also an unary operator, but it refers to the value of a variable located at the address specified by its operand. In other words, it refers to the value of a variable addressable by a given pointer. If (continuing with the previous assignment statement) the balptr variable contains the address of the balance variable, then when you execute the statement

value = *balptr;


the value variable will be set to the value of the balance variable pointed to by the balptr variable. For example, if the balance variable contains the value 3200, after the last statement is executed, the value variable will contain the value 3200, because this is the value that is stored at address 100. The purpose of the operator "*" can be expressed by the phrase "at the address". In this case, the previous statement can be read as follows: "The value variable receives a value (located) at the balptr address." The effect of the above two instructions is schematically shown in Fig. 6.1.
The sequence of operations shown in Fig. 6.1, implemented in the following program.

#include <iostream>

using namespace std;

int main ()

{

int balance;

int *balptr;

int value;

balance = 3200;

balptr = &balance;

value = *balptr;

cout << 'Balance equals:' << value <<'\n';

return 0;

}


When implementing this program, we get the following results:

The balance is: 3200


Unfortunately, the multiplication sign (*) and the operator with the value "at the address" are indicated by the same "asterisk" characters, which is sometimes confusing for beginners in the C++ language. These operations are in no way related to each other. Keep in mind that the "*" and "&" operators have a higher priority than any of the arithmetic operators, except for the unary minus, the priority of which is the same as that of the operators used to work with pointers.


Pointer operations are often referred to as indirect access operations because we indirectly access a variable through some other variable.


An indirect access operation is the process of using a pointer to access an object.


On the importance of the base type of pointer


On the example of the previous program, the possibility of assigning the value variable to the value of the balance variable through an indirect access operation, i.e. using a pointer, was shown. You may have a glimpse of the question:

"How does the C++ compiler know how many bytes need to be copied to the variable value from the memory area addressed by the balptr pointer?".

Let's formulate the same question in a more general way: how does the C++ compiler pass the proper number of bytes when performing an assignment operation using a pointer? The answer is this.

 

The data type addressed by the pointer is determined by the base pointer type. In this case, since balptr is a pointer to an integer type, the C++ compiler will copy four bytes of information to the variable value from the memory area addressed by the balptr pointer (which is true for a 32-bit environment), but if we were dealing with a double pointer, eight bytes would be copied in a similar situation.


Pointer variables must always point to the appropriate data type. For example, if you declare a pointer of type int, the compiler "assumes" that all values referenced by that pointer are of type int. The C++ compiler simply will not allow you to perform an assignment operation involving pointers (on both sides of the assignment operator) if the types of these pointers are incompatible (in fact, not the same).
For example, the following code snippet is incorrect.

int *p;

double f;

// ...

p = &f; ERROR!


The incorrectness of this fragment consists in the inadmissibility of assigning a double pointer to an int pointer. The expression &f generates a pointer to a double value, and p generates a pointer to an integer type int. These two types are incompatible, so the compiler will mark this instruction as erroneous and will not compile the program.
Although, as stated above, when assigning two pointers must be type-compatible, this serious limitation can be overcome (albeit at one's own risk) by the conversion operation. For example, the following code snippet is now formally correct.

int *p;

double f;

// ...

p = (int *) &f; Now formally everything is OK!


A casting operation (int *) will cause a conversion of double- to an int pointer. However, the use of a casting operation for such purposes is somewhat questionable, since it is the base type of pointer that determines how the compiler will handle the data it references. In this case, even though p (after executing the last instruction) actually points to a floating-point value, the compiler still "believes" that it is pointing to an integer value (since p is by definition an int pointer).


To better understand why using a type conversion operation when assigning one pointer to another is not always acceptable, consider the following program.

This program will not run correctly.

#include <iostream>

using namespace std;

int main ()

{

double x, y;

int *p;

x = 123.23;

p = (int *) &x; Use a type conversion operation to assign a double pointer to an int pointer.

y = *p; What happens when I run this instruction?

cout << y; What will this instruction output?

return 0;

}


As you can see, in this program, the variable p (more precisely, a pointer to an integer value) is assigned the address of the variable x (which is of type double). Therefore, when the variable y is assigned a value addressed by the pointer p, the variable y receives only four bytes of data (and not all eight required for a double value), since p is a pointer to an integer type int. Thus, when executing a cout instruction, not the number 123.23 will be displayed on the screen, but, as programmers say, "garbage". (Run the program and see for yourself.)


Assign values using pointers


When assigning a value to a pointer-addressable memory area, you can use it (the pointer) on the left side of the assignment operator. for example when you run the following statement (if p is a pointer to an integer type)

*p = 101;


the number 101 is assigned to the memory area addressed by the pointer p. Thus, this instruction can be read as follows: "at address p place the value 101". To increment or decrement a value located in the pointer-addressable memory area, you can use an instruction such as the following.

(*p)++;


Parentheses are required here because the "*" operator has a lower priority than the "++" operator.


Assigning values using pointers is demonstrated in the following program.

#include <iostream>

using namespace std;

int main ()

{

int *p, num;

p = &num;

*p = 100;

cout << num << ' ';

(*p)++;

cout << num << ' ';

(*p) --;

cout << num << '\n';

return 0;

}


These are the results generated by this program.

100 101 100


Using pointers in expressions


Pointers can be used in most valid expressions in C++. However, some special rules should be applied and it should not be forgotten that some parts of such expressions must be enclosed in parentheses to ensure that the desired result is obtained.


Pointer Arithmetic Operations


Only four arithmetic operators can be used with pointers: ++, — , + and — . To better understand what happens when you perform pointer arithmetic, let's start with an example. Let p1 be a pointer to an int variable with the current value of 2 LLC (i.e. p1 contains the address 2 LLC). After you run (in a 32-bit environment) the expression

p1++;


the contents of the pointer variable p1 will be 2,004 instead of 2,001! The fact is that with each increment, the pointer p1 will point to the following int-value. For the decrement operation, the opposite statement is true, i.e. with each decrementation, the p1 value will decrease by 4. for example after you execute a statement

p1--;


the pointer p1 will have a value of 1,996 if it was previously 2,000. So, each time a pointer is incremented, it will point to the memory area that contains the next base type element of that pointer. And with each decrement, it will point to the memory area that contains the previous base type element of that pointer.


For pointers to symbolic values, the result of incrementing and decrementing operations will be the same as with "normal" arithmetic, since characters occupy only one byte. However, if you use any other type of pointer, when incrementing or decrementing, the value of the pointer variable will increase or decrease by an amount equal to the size of its base type.


Pointer arithmetic operations are not limited to the use of increment and decrement operators. You can perform addition and subtraction operations on pointer values using integer values as the second operand. Expression

p1 = p1 + 9;


causes p1 to refer to the ninth element of the base pointer type p1 relative to the element that p1 referenced before executing this instruction.


Although pointers cannot be folded, you can subtract one pointer from another (if they both have the same base type). The difference will show the number of elements of the base type that separate the two pointers.


In addition to adding a pointer to an integer value and subtracting it from the pointer, as well as calculating the difference between two pointers, no other arithmetic operations are performed on pointers. For example, you cannot add float or double values with pointers.


To understand how the result of performing arithmetic operations on pointers is formed, we will perform the following short program. It displays real physical addresses, which contain a pointer to an int value (i) and a pointer to a float value (f). Note each address change (depending on the underlying pointer type) that occurs when the loop repeats. (For most 32-bit compilers, the value of i will increase by 4 and the value of f by 8.) Note also that when you use a pointer in a cout instruction, its address is automatically displayed in the addressing format used for the current processor and runtime.

Demonstration of arithmetic operations on pointers.

 

#include <iostream>

using namespace std;

int main ()

{

int *i, j[10];

double *f, g[10];

int x;

i = j;

f = g;

for (x=0; x<10; x++)

cout << i+x << ' ' << f+x << '\n';

return 0;

}


Here's what the possible execution of this program looks like (your results may differ from those given, but the intervals between the values should be the same).

0012FE5C 0012FE84

0012FE60 0012FE8C

0012FE64 0012FE94

0012FE68 0012FE9C

0012FE6C 0012FEA4

0012FE70 0012FEAC

0012FE74 0012FEB4

0012FE78 0012FEBC

0012FE7C 0012FEC4

0012FE80 0012FECC


Nodule on memory. All arithmetic operations on pointers are performed relative to the base type of pointer.


Pointer Comparison


Pointers can be compared using the relationship ==, <, and > operators. However, in order for the result of the pointer comparison to be interpretable, the pointers being compared must be related in some way. For example, if p1 and p2 are pointers that refer to two separate and unrelated variables, then any comparison of p1 and p2 is generally meaningless. But if p1 and p2 point to variables between which there is some connection (as, for example, between elements of the same array), then the result of comparing the pointers p1 and p2 may make some sense. Later in this chapter, we'll look at an example of a program that uses pointer comparisons.


Pointers and arrays


In C++, pointers and arrays are closely related, so much so that often the concepts of "pointer" and "array" are interchangeable. In this section, we will try to trace this connection. First, consider the following fragment of the program.

char str[80];

char *p1;

p1 = str;


Here, str is the name of an array containing 80 characters, and p1 is a pointer to the char type. Of particular interest is the third line, when executed, the variable p1 is assigned the address of the first element of the str array. (In other words, after this assignment, p1 will point to the str[0] element.) The fact is that in C++, using the name of an array without an index generates a pointer to the first element of this array. Thus, when assigning p1 = str, the address stg[0] is assigned to the pointer p1. This is the key point that must be clearly understood: the unindexed array name used in the expression means a pointer to the beginning of this array.


The name of an array without an index forms a pointer to the beginning of that array.
Since, after the above assignment, p1 will indicate the beginning of the str array, the pointer p1 can be used to access the elements of this array. For example, if you want to access the fifth element of the str array, use one of the following expressions:

str[4]

or

*(p1+4)


In both cases, the fifth element will be invoked. Remember that array indexing starts at zero, so an index of four provides access to the fifth element. Exactly the same effect is produced by summing the value of the original pointer (p1) with the number 4, since p1 points to the first element of the str array.


The need to use parentheses, in which the expression p1 + 4 is enclosed, is due to the fact that the operator "*" has a higher priority than the operator "+". Without these parentheses, the expression would be reduced to the value addressed by the pointer p1, i.e. the value of the first element of the array, which would then be increased by 4.
Importantly! Make sure that parentheses are used correctly in the expression with pointers. Otherwise, the error will be difficult to find, because externally the program may look quite correct. If you have doubts about the need for their use, make a decision in their favor - there will be no harm from this.


In fact, C++ provides two ways to access array elements: array indexing and pointer arithmetic. The fact is that arithmetic operations on pointers sometimes perform faster than indexing arrays, especially when accessing elements whose location is strictly ordered. Since performance is often a determining factor when choosing certain solutions in programming, the use of pointers to access the elements of an array is a characteristic feature of many C++ programs. In addition, sometimes pointers allow you to write more compact code compared to using array indexing.


To better understand the difference between using array indexing and pointer arithmetic, consider two versions of the same program. In this program, words separated by spaces are selected from a line of text. For example, from the line "Hello friend" the program should highlight the words "Hello" and "friend". Programmers usually refer to such delimited character sequences as tokens. When the program runs, the input string is character-by-character copied to another array (named token) until a space is encountered. The selected token is then displayed on the screen, and the process continues until the end of the line is reached. For example, if you use the string This is only a simple test as the input string, the program displays the following:

This is

only

simple

test.


Here's what the word-breaking version of the word-breaking program looks like using pointer arithmetic.

Word breaking program:

A pointer-based version.

 

#include <iostream>

#include <cstdio>

using namespace std;

int main ()

{

char str[80];

char token[80];

char *p, *q;

cout << "Enter a sentence: ";

gets (str);

p = str;

Read the token from the line.

while (*p) {

q = token; Set q to point to the beginning of the token array.

/* Read the characters until you encounter either a space or a null character (a sign of line termination). */

while (*p != ' ' && *r) {

*q = *p;

q++; p++;

}

if (*p) p++; Moving beyond the gap.

*q = '\0'; End the lexeme with a zero symbol.

cout << token << '\n';

}

return 0;

}


Here's what the version of the same program looks like using array indexing.

Word breaking program:

Version using array indexing.

 

#include <iostream>

#include <cstdio>

using namespace std;

int main ()

{

char str[80];

char token[80];

int i, j;

cout << "Enter a sentence: ";

gets (str);

Read the token from the line.

for (i=0; ; i++) {

/* Read the characters until you encounter either a space or a null character (a line terminator). */

for (j=0; str[i]!=' ' && str[i]; j++, i++)

token[j] = str[i];

token[j] = '\0'; End the lexeme with a zero symbol.

cout << token << '\n';

if (!str[i]) break;

}

return 0;

}


These programs can have different speeds, which is due to the peculiarities of code generation by C++ compilers. Typically, array indexing generates longer code (with more machine instructions) than when performing arithmetic on pointers. Therefore, it is not surprising that in professionally written C++ code, versions focused on pointer processing are more common. But if you are a novice programmer, feel free to use array indexing until you learn how to handle pointers freely.


Indexing


A pointer As shown above, you can access an array using pointer arithmetic. Interestingly, in C++, a pointer that refers to an array can be indexed as if it were the name of an array (this indicates a close relationship between pointers and arrays). The syntax corresponding to this approach provides an alternative to pointer arithmetic operations, as it is more convenient in some situations. Let's look at an example.

Indexing a pointer is like an array.

 

#include <iostream>

#include <cctype>

using namespace std;

int main ()

{

char str[20] = «I love you»;

char *p;

int i;

p = str;

Index the pointer.

for (i=0; p[i]; i++) p[i] = toupper (p[i]);

cout << p; Display the string.

return 0;

}


When running, the program will display the following on the screen:

I LOVE YOU


Here's how this program works. First, the string "I love you" is entered into the str array. The address of the beginning of this string is then assigned to the pointer p. After that, each character of the str string using the toupper () function is converted to its uppercase equivalent by indexing the pointer p. Remember that the expression p[i] is identical in its action to the expression *(p+i).


On the interchangeability of pointers and arrays


, it was shown above that pointers and arrays are very closely related. Indeed, in many cases they are interchangeable. For example, using a pointer that contains the address of the beginning of an array, you can access the elements of that array either by arithmetic on the pointer or by indexing the array. However, in general, pointers and arrays are not interchangeable. Consider, for example, such a code snippet.

 

int num[10];

int i;

for (i=0; i<10; i++) {

*num = i; It's okay here.

num++; ERROR — the num variable cannot be modified.

}


It uses an array of integer values named num. As noted in the comment, although it is perfectly acceptable to apply the "*" operator to the name num (which is usually applied to pointers), it is absolutely unacceptable to modify the value of num. The fact is that num is a constant that indicates the beginning of an array. And it, therefore, can not be incremented. In other words, although the array name (without the index) does generate a pointer to the beginning of the array, its value cannot be changed.


Although an array name generates a pointer constant, it can still (like pointers) be included in expressions, unless, of course, it is modified. For example, the following statement, which sets the num[3] element to 100, is perfectly valid.

*(num+3) = 100; It's okay here, as num doesn't change.


Pointers and String Literals


It may surprise you to see how C++ compilers handle string literals like the following.

cout << strlen (C++ compiler);


If the C++ compiler detects a string literal, it stores it in the program's string table and generates a pointer to the desired string. Therefore, the following program is completely correct and when executed displays the phrase: Working with pointers is a real pleasure!.

#include <iostream>

using namespace std;

int main ()

{

char *s;

s = "Working with pointers is a real pleasure!\n";

cout << s;

return 0;

}


When you run this program, the characters that make up the string literal are stored in a table of strings, and the variable s is assigned a pointer to the corresponding string in that table.


A row table is a table generated by the compiler to store rows used in a program.
Since a pointer to a table of rows of a particular program is automatically generated when using a string literal, you can try to use this fact to modify the contents of this table. However, such a decision can hardly be called successful. The fact is that C++ compilers create optimized tables in which one string literal can be used in two (or more) different places in the program. Therefore, "forcibly" changing a string can cause unwanted side effects. Moreover, string literals are constants, and some modern C++ compilers simply will not allow you to change their contents. And if you try to do this, a run-time error will be generated.


Everything is known in comparison It was noted above that the value of one pointer can be compared with another. But, for pointer comparisons to make sense, the pointers being compared must be related to each other in some way. Most often, this relationship is established when both pointers point to elements of the same array. For example, there are two pointers (named A and B) that refer to the same array. If A is less than B, then pointer A points to an element whose index is less than the index of the element addressed by pointer B. This comparison is especially useful for determining boundary conditions.


A comparison of pointers is demonstrated in the following program. This program creates two variables of type pointer. One (named start) initially points to the beginning of the array, and the second (named end) points to the end of the array. As the user enters numbers, the array is sequentially populated from beginning to end. Each time a regular number is entered into the array, the start pointer is incremented. To determine whether the array is populated, the program simply compares the values of the start and end pointers. When start exceeds the end, the array will be populated "to failure". The program will only have to display the contents of the filled array on the screen.

An example of pointer comparison.

 

#include <iostream>

using namespace std;

int main ()

{

int num[10];

int *start, *end;

start = num;

end = &num[9];

while (start <= end) {

cout << "Enter a number: ";

cin >> *start;

start++;

}

start << num; /* Restore the pointer to its original value */

while (start <= end) {

cout << *start << ' ';

start++;

}

return 0;

}


As shown in this program, since start and end both point to a common object (in this case, the num array), comparing them might make sense. Such a comparison is often used in professionally written C++ code.
Pointer arrays
, like other types of data, can be stored in arrays. Here, for example, is what declaring a 10-element array of pointers to int values looks like.

int *ipa[10];


Here, each element of the ipa array contains a pointer to an integer value.
To assign an int variable address named var to the third element of this pointer array, note the following:

ipa[2] = &var;


Remember that here ipa is an array of pointers to integer values. The elements of this array can contain only values that represent the addresses of variables of an integer type. That's why the var variable is preceded by a statement To assign the value of the var variable to the integer variable x using the ipa array, use this syntax.

x = *ipa[2];


Since the address of the var variable is stored in the ipa element[2], applying the "*" operator to this indexed variable will return the value of the var variable.
Like other arrays, pointer arrays can be initialized. Typically, initialized arrays of pointers are used to store pointers to strings. For example, to create a function that outputs happy predictions, you can define a fortunes array as follows:

char *fortunes[] = {

"Soon the money will flow to you like a river.\n",

"Your life will be illuminated by a new love.\n",

"You will live happily ever after.\n",

"The money invested now in the business will bring income.\n",

"A close friend will look for your location.\n"

};


Keep in mind that C++ stores all the string literals in the string table associated with a particular program, so the array is only needed to store pointers to those strings. Thus, to display a second message, it is sufficient to use an instruction such as the following.

cout << fortunes[1];


Below is the entire prediction program. To obtain random numbers, the rand () function is used, and to obtain random numbers in the range from 0 to 4, the modulo division operator is used, since these numbers can be used to access the elements of the array by index.

#include <iostream>

#include <cstdlib>

#include <conio.h>

using namespace std;

char *fortunes[] = {

"Soon the money will flow to you like a river.\n",

"Your life will be illuminated by a new love.\n",

"You will live happily ever after.\n",

"The money invested now in the business will bring income.\n",

"A close friend will look for your location.\n"

};

int main ()

{

int chance;

cout <<"To find out your fate, press any key: ";

Randomize the random number generator.

while (!kbhit ()) rand ();

cout << '\n';

chance = rand ();

chance = chance % 5;

cout << fortunes[chance];

return 0;

}


Notice the while loop, which calls the rand() function until a key is pressed. Since the rand () function always generates the same sequence of random numbers, it is important to be able to programmatically use this sequence from some arbitrary position. (Otherwise, the program will give the same "prediction" every time it starts.) The randomness effect is achieved through repeated calls to the rand () function. When the user presses the key, the loop will stop at some random position of the sequence of generated numbers, and this position will determine the number of the message that will be displayed on the screen. As a reminder, the kbhit() function is a fairly common extension of the C++ function library provided by many compilers, but is not included in the standard C++ library feature package.


The following example uses a two-dimensional array of pointers to create a program that displays a syntax memo for C++ keywords. The program initializes a list of pointers to strings. The first dimension of the array is intended to indicate the C++ keywords, and the second dimension is intended to indicate a brief description of them. The list ends with two null lines, which are used as an indication of the end of the list. The user enters a keyword, and the program should display its description. As you can see, this list contains only a few keywords. Therefore, its continuation remains with you.

A simple memo on C++ keywords.

 

#include <iostream>

#include <cstring>

using namespace std;

char *keyword[][2] = {

"for", "for (initialization; condition; increment)",

"if", "if (condition) ... else ... ",

"switch", "switch (value) {case-list}",

"while", "while(condition)...",

Add the rest of the C++ keywords here.

"", "" // The list must end with zero lines.

};

int main ()

{

char str[80];

int i;

cout << "Enter keyword: ";"

cin >> str;

Display the syntax.

for (i=0; *keyword[i][0]; i++)

if (!strcmp (keyword[i][0], str))

cout << keyword[i][1];

return 0;

}


Here's an example of how to run this program.

Enter keyword: for

for (initialization; condition; increment)


In this program, notice the expression that controls the for loop. It ends the loop when the keyword[i][0] element contains a pointer to zero, which is interpreted as FALSE. Therefore, the loop stops when there is a zero string that completes the array of pointers.


Null Pointer Convention


A declared but not initialized pointer will contain an arbitrary value. If you try to use a pointer before assigning it a specific value, you can destroy not only your own program, but even the operating system (the most disgusting, I must say, type of error!). Since there is no guaranteed way to avoid using an uninitialized pointer, C++ programmers have adopted a procedure that avoids such terrible errors. By convention, if a pointer contains a null value, it is considered to refer to nothing. This means that if you assign null values to all unused pointers and avoid using null pointers, you can avoid accidentally using an uninitialized pointer. You should stick to this programming practice.
When declaring a pointer of any type, you can initialize it with a null value, for example, as is done in the following statement,

float *p = 0; p is now a null pointer.


To test the pointer, use an if statement (any of its following variants):

if (p) // Do something if p is not a zero pointer.

if (!p) // Execute something if p is a null pointer.


By adhering to the zero pointer convention mentioned above, you can avoid many of the serious problems that arise when using pointers.


Pointers and 16-bit environments


Although most computing environments are now 32-bit, many users still work in 16-bit (mainly DOS and Windows 3.1) and, of course, with 16-bit code. These operating systems were developed for the Intel 8086 processor family, which include modifications such as the 80286, 80386, 80486, and Pentium (when operating in 8086 processor emulation mode). And although programmers tend to focus on using a 32-bit runtime when writing new code, some programs are still created and maintained in more compact 16-bit environments. Since some topics are relevant only for 16-bit environments, it will be useful for programmers who work in them to get information on how to adapt the "old" code to the new environment, i.e. reorient 16-bit code to 32-bit code.


When writing 16-bit code for the Intel 8086 processor family, the programmer can count on six ways to compile programs, which differ in the organization of computer memory. Programs can be compiled for miniature, small, medium, compact, large, and huge memory models. Each of these models optimizes the space reserved for data, code, and stack in its own way. The difference in the organization of computer memory is explained by the use of segmented architecture by the Intel 8086 processor family when executing 16-bit code. In 16-bit segmented mode, the Intel 8086 processor family divides memory into 16K segments.


In some cases, the memory model can affect the behavior of pointers and your ability to use them. The main problem occurs when the pointer is incremented outside the segment. A look at the specifics of each of the 16-bit memory models is beyond the scope of this book. The key is to let you know that if you have to work in a 16-bit environment and target the Intel 8086 processor family, you should review the documentation that came with the compiler you are using and understand the memory models and their effects on pointers in detail.


One last thing. When writing programs for a modern 32-bit environment, you need to know that it uses a single model of memory organization, which is called a single-level, unsegmented or linear (flat model).


Multi-level indirect addressing


You can create a pointer that references another pointer and that pointer to the final value. This situation is called multiple indirection or the use of a pointer to a pointer. The idea of multi-level indirect addressing is schematically illustrated in Fig. 6.2. As you can see, the value of a regular pointer (with single-level indirect addressing) is the address of a variable that contains some value. If you use a pointer to a pointer, the first contains the address of the second, and the pointer refers to a variable that contains a certain value.
When using indirect addressing, you can organize any desired number of levels, but are usually limited to only two, since the increase in the number of levels is frequent.o leads to conceptual errors.
A variable that is a pointer to a pointer must be declared accordingly. To do this, just put an additional symbol "asterisk" (*) before its name. For example, the following declaration tells the compiler that balance is a pointer to a pointer to an int value.

int **balance;


It must be remembered that the balance variable here is not a pointer to an integer value, but a pointer to a pointer to an int value.


To access the value addressed by a pointer pointer, you must apply the "*" operator twice, as shown in the following short example.

Use multi-level indirect addressing.

 

#include <iostream>

using namespace std;

int main ()

{

int x, *p, **q;

x = 10;

p = &x;

q = &p;

cout << **q; Display the value of the variable x.

return 0;

}


Here, the variable p is declared as a pointer in the int-value, and the variable q as a pointer to the pointer on the int-value. When we run this program, we will get the value of the variable x, i.e. the number 10.


Problems associated with the use of pointers For a programmer, there is nothing more terrible than "infuriating" pointers! Pointers can be compared to the energy of an atom: they are both extremely useful and extremely dangerous. If the problem is related to the pointer receiving an incorrect value, then such an error is the most difficult to find.
The difficulty in identifying pointer errors is due to the fact that the pointer itself does not detect a problem. The problem can only manifest itself indirectly, perhaps even as a result of executing several instructions after a "seditious" pointer operation. For example, if one pointer accidentally receives the address of the "wrong" data, then when performing an operation with this "dubious" pointer, the addressed data may undergo an undesirable change, and, most unpleasantly, this "secret" change, as a rule, becomes apparent much later. Such a "delay" significantly complicates the search for an error, since it sends you on a "false trail". By the time the problem becomes apparent, it's possible that the culprit pointer will look like a "harmless sheep", and you will have to spend a lot of time to find the true cause of the problem.


Since for many to work with pointers means potentially dooming yourself to the search for an answer to the question "Who is to blame?", we will try to consider possible "ravines" on the way of a brave programmer and show workarounds to avoid exhausting "torments of creativity".


Uninitialized pointers


A classic example of an error made when working with pointers is the use of an uninitialized pointer. Consider the following code snippet.

This program is incorrect.

 

int main ()

{

int x, *r;

x = 10;

*p = x; What does the variable p indicate?

return 0;

}


Here, the pointer p contains an unknown address because it is not defined anywhere. You do not have the opportunity to know where the value of the variable x is recorded. With small program sizes (for example, as in this case), its strangeness (which consists in the fact that the pointer p contains an address that does not belong to either the program code or the data area) may not appear in any way, i.e. the program will outwardly work normally. But as it develops and, accordingly, increases its volume, the likelihood that p will indicate either the program code or the data area will increase. One day, the program will stop working altogether. The way to prevent the creation of such programs is obvious: before using the pointer, make sure that it refers to something valid!


Incorrect comparison of pointers Comparing


pointers that do not refer to elements of the same array is generally incorrect and often leads to errors. You should never rely on the fact that different objects will be placed in memory in a certain way (somewhere nearby) or that all compilers and operating environments will process your data in the same way. Therefore, any comparison of pointers that refer to different objects can lead to unexpected consequences. Let's look at an example.

 

char s[80];

char y[80];

char *p1, *p2;

p1 = s;

p2 = y;

if (p1 < p2) . . .


Incorrect pointer comparison is used here because C++ does not provide any guarantees about the placement of variables in memory. Your code should be written in such a way that it works equally stable regardless of where the data is located in memory.
It would be a mistake to assume that the two declared arrays will be arranged in memory "shoulder to shoulder", and therefore you can access them by indexing them using the same pointer. The assumption that the incremented pointer after going beyond the boundaries of the first array will refer to the second is completely unfounded and therefore incorrect. Let's look at this example carefully.

int first[101;

int second[10];

int *p, t;

p = first;

for (t=0; t<20; ++t) {

*p = t;

p++;

}


The purpose of this program is to initialize the elements of the first and second arrays with numbers from 0 to 19. However, this code does not allow you to hope to achieve the desired result, despite the fact that in some conditions and when using certain compilers, this program will work as intended by the author. Do not rely on the fact that the first and second arrays will be located sequentially in the computer's memory, and the first array will necessarily be the first array. The C++ language does not guarantee a specific location of objects in memory, and therefore this program cannot be considered correct.


Remember to set pointers


The next (incorrect) program should accept the string entered from the keyboard, and then display the ASCII code for each character of that string. (Note that a type conversion operation is used to display ASCII codes.) However, this program contains a serious error.

This program is incorrect.

 

#include <iostream>

#include <cstdio>

#include <cstring>

using namespace std;

int main ()

{

char s [80];

char *p1;

p1 = s;

do {

cout << "Enter the string: ";

gets (p1); Read the string.

Display the ASCII values of each character.

while (*p1) cout << (int) *p1++ << ' ';

cout << ' \n';

}while (strcmp (s, "end"));

return 0;

}


Can you find the error here yourself?


In the above version of the program, the pointer p1 is assigned the address of the array s only once. This assignment is performed outside the loop. When entering a do-while loop (i.e., when it is first iterated), p1 does point to the first character of the array s. But on the second pass of the same cycle p1, the pointer will contain the value that will remain after the previous iteration of the loop, since the pointer p1 is not set again at the beginning of the array s. Sooner or later, the boundary of the array s will be violated.
Here's what the correct version of the same program looks like.

This program is correct.

 

#include <iostream>

#include <cstdio>

#include <cstring>

using namespace std;

int main ()

{

char s[80];

char *p1;

do {

p1 = s; Set p1 at each iteration of the loop.

cout << "Enter the string: ";

gets (p1); Read the string.

Display the ASCII values of each character.

while (*p1) cout << (int) *p1++ <<' ';

cout << '\n';

}while (strcmp (s, "end"));

return 0;

}


So, in this version of the program, at the beginning of each iteration of the cycle, the pointer p1 is set to the beginning of the line.


A nodule for memory. For pointers to be safe, you need to know at any time what they are referencing.