Preface: Explanation of Complex Types
To understand pointers, you will inevitably encounter some relatively complex types. So let me first introduce how to fully understand a complex type. Actually, understanding complex types is very simple: a type contains many operators, which, just like ordinary expressions, have precedence. Their precedence is the same as operator precedence. So I have summarized the principle: starting from the variable name, combine according to operator precedence and analyze step by step.
Below, let us start with simple types and analyze them gradually.
- int p;-- This is an ordinary integer variable.
- int *p;-- First, starting from p, first combine with `*`.*This indicates that p is a pointer; then combine with int, indicating that the type of the content pointed to by the pointer is int. So p is a pointer that returns integer data.
- int p[3]-- First, starting from p, first combine with `[]`.[]This indicates that p is an array; then combine with int, indicating that the elements in the array are integers. So p is an array composed of integer data.
- int *p[3];-- First, starting from p, first combine with [] because its precedence is higher than*`*`, so p is an array; then combine with *, indicating that the elements in the array are pointers; then combine with int, indicating that the type of the content pointed to by the pointers is integer. So p is an array composed of pointers that return integer data.
- int (*p)[3];-- First, starting from p, first combine with *, indicating that p is a pointer; then combine with [] (the "()" step can be ignored; it is only to change precedence), indicating that the content pointed to by the pointer is an array; then combine with int, indicating that the elements in the array are integers. So p is a pointer to an array composed of integer data.
- int **p;-- First, starting from p, first combine with *, saying that p is a pointer; then combine with * again, indicating that the element pointed to by the pointer is a pointer; then combine with int, indicating that the element pointed to by that pointer is integer data. Since second-level pointers and higher-level pointers are rarely used in complex types, we will not consider multi-level pointers in the more complex types later; at most, we will only consider one-level pointers.
- int p(int);-- Starting from p, first combine with (), indicating that p is a function; then analyze inside (), indicating that the function has a parameter of an integer variable; then combine with the outer int, indicating that the return value of the function is integer data.
- int (*p)(int);-- Starting from p, first combine with the pointer operator `*`, indicating that p is a pointer; then combine with (), indicating that the pointer points to a function; then combine with the int inside (), indicating that the function has an int parameter; then combine with the outermost int, indicating that the function's return type is integer. So p is a pointer to a function that has an integer parameter and whose return type is integer.
- int *(*p(int))[3];-- You can skip this type for now; it is too complex. Starting from p, first combine with (), indicating that p is a function; then go inside (), combine with int, indicating that the function has an integer variable parameter; then combine with the outer *, indicating that the function returns a pointer; then go to the outermost layer, first combine with [], indicating that the returned pointer points to an array; then combine with *, indicating that the elements in the array are pointers; then combine with int, indicating that the content pointed to by the pointers is integer data. So p is a function whose parameter is an integer and which returns a pointer variable pointing to an array composed of integer pointer variables.
That is about all there is to say; our task is only this much. Once you understand these types, other types are a piece of cake for us. However, we generally do not use overly complex types, as that would greatly reduce the readability of the program. Please use them with caution. The few types above are already sufficient for us.
1. Detailed Explanation of Pointers
A pointer is a special variable whose stored value is interpreted as an address in memory. To understand a pointer, you need to understand four aspects: the type of the pointer, the type pointed to by the pointer, the value of the pointer (or the memory area pointed to by the pointer), and the memory area occupied by the pointer itself. Let us explain each separately.
First, let us declare a few pointers as examples:
Example:
1. The Type of a Pointer
From a syntactic point of view, you only need to remove the pointer name from the pointer declaration statement; the remaining part is the type of the pointer. This is the type that the pointer itself has. Let us look at the types of the various pointers in Example 1:
- 1、int *ptr;: The type of the pointer isint*
- 2、char *ptr;: The type of the pointer ischar*
- 3、int **ptr;: The type of the pointer isint**
- 4、int (*ptr)[3];: The type of the pointer isint(*)[3]
- 5、int *(*ptr)[4]; : The type of the pointer isint*(*)[4]
How about it? The method for finding the type of a pointer is very simple, isn't it?
2. The Type Pointed to by the Pointer
When you use a pointer to access the memory area pointed to by the pointer, the type pointed to by the pointer determines what the compiler will treat the contents of that memory area as.
Syntactically, you only need to remove the pointer name and the pointer declaration operator `*` to its left from the pointer declaration statement; what remains is the type pointed to by the pointer. For example:
- 1、int*ptr;: The type pointed to by the pointer isint
- 2、char*ptr;: The type pointed to by the pointer ischar
- 3、int**ptr;: The type pointed to by the pointer isint*
- 4、int(*ptr)[3];: The type pointed to by the pointer isint()[3]
- 5、int*(*ptr)[4]; : The type pointed to by the pointer isint*()[4]
In pointer arithmetic, the type pointed to by the pointer plays a significant role.
The type of the pointer (i.e., the type of the pointer itself) and the type pointed to by the pointer are two concepts. As you become more familiar with C, you will find that dividing the concept of "type" associated with pointers into the two concepts of "type of the pointer" and "type pointed to by the pointer" is one of the key points to mastering pointers. I have read many books, and found that in some poorly written books, these two concepts are mixed together, so the books seem contradictory and the more you read, the more confused you become.
3. The Value of a Pointer — Or the Memory Area or Address Pointed to by the Pointer
The value of a pointer is the numeric value stored in the pointer itself. This value will be treated by the compiler as an address, not an ordinary numeric value. In 32-bit programs, the value of a pointer of any type is a 32-bit integer, because in 32-bit programs, memory addresses are all 32 bits long. The memory area pointed to by a pointer starts from the memory address represented by the pointer's value and has a length of sizeof(type pointed to by the pointer). Henceforth, when we say a pointer's value is XX, it is equivalent to saying that the pointer points to a memory area with XX as its starting address; when we say a pointer points to a certain memory area, it is equivalent to saying that the pointer's value is the starting address of that memory area. The memory area pointed to by a pointer and the type pointed to by the pointer are two entirely different concepts. In Example 1, the type pointed to by the pointer already exists, but because the pointer has not been initialized, the memory area it points to does not exist, or is meaningless.
From now on, every time you encounter a pointer, you should ask: What is the type of this pointer? What is the type it points to? Where does the pointer point? (Pay special attention)
4. The Memory Area Occupied by the Pointer Itself
How much memory does the pointer itself occupy? You only need to use the function sizeof(pointer type) to measure it. On a 32-bit platform, a pointer itself occupies 4 bytes. The concept of the memory occupied by the pointer itself is useful in determining whether a pointer expression (explained later) is an lvalue.
2. Arithmetic Operations on Pointers
A pointer can be added to or subtracted by an integer. The meaning of this operation on pointers is different from ordinary numeric addition and subtraction; it is in units of elements. For example:
In the above example, the type of pointer ptr is int*, and the type it points to is int. It is initialized to point to the integer variable a. In the next statement, the pointer ptr is incremented by 1. The compiler handles it as follows: it adds sizeof(int) to the value of pointer ptr. In a 32-bit program, this adds 4 because int occupies 4 bytes in a 32-bit program. Since addresses are measured in bytes, the address pointed to by ptr has increased by 4 bytes from the address of the original variable a toward higher addresses. Since the length of a char type is one byte, originally ptr pointed to the four bytes starting from element 0 of array a; at this point it points to the four bytes starting from element 4 of array a. We can use a pointer and a loop to traverse an array. Look at the example:
This example increments the value of each element in the integer array by 1. Since each loop increments the pointer ptr by one element, each loop can access the next element of the array.
Look at another example:
In this example, ptr is incremented by 5. The compiler handles it like this: add 5 times sizeof(int) to the value of the pointer ptr; in a 32-bit program, that is adding 5 times 4 = 20. Since the unit of an address is a byte, the address now pointed to by ptr has moved 20 bytes in the higher-address direction compared to the address pointed to before adding 5.
In this example, before adding 5, ptr points to the four bytes starting from element 0 of array a. After adding 5, ptr points beyond the valid range of array a. Although this may cause problems in practice, it is syntactically allowed. This also reflects the flexibility of pointers. If in the above example ptr were decreased by 5, the process would be much the same, except that the value of ptr would be decreased by 5 times sizeof(int), and the address pointed to by the new ptr would move 20 bytes in the lower-address direction compared to the address originally pointed to by ptr.
Now allow me to give another example: (a misconception)
Misconception 1. The output is Y and o.
Misunderstanding: ptr is a secondary pointer to char. When ptr++ is executed, the pointer is increased by sizeof(char), so the output is as above. This may be the result for only a few people.Misconception 2.The output is Y and a. Misunderstanding: ptr points to a char * type. When ptr++ is executed, the pointer is increased by sizeof(char *) (some may think this value is 1, which would lead to the answer in Misconception 1; this value should be 4, refer to the previous content), that is, &p+4. Then wouldn't one dereference operation point to the fifth element in the array? Wouldn't the output be the fifth element in the array? The answer is no.
Correct solution:The type of ptr is char **, and the type it points to is char *. The address it points to is the address of p (&p). When ptr++ is executed, the pointer is increased by sizeof(char*), that is, &p+4. Then where does *(&p+4) point? You can ask God about that; maybe He will tell you where. So the final output will be a random value, or perhaps an illegal operation.
. But if array is regarded as a pointer, it points to the 0th element of the array. Its type is int*, and the type it points to is the type of the array element, namely int. Therefore it is not surprising at all that *array equals 0. Similarly, array+3 is a pointer to the 3rd element of the array, so
After adding (or subtracting) an integer n to a pointer ptrold, the result is a new pointer ptrnew. The type of ptrnew is the same as the type of ptrold, and the type pointed to by ptrnew is also the same as the type pointed to by ptrold. The value of ptrnew will be increased (or decreased) by n times sizeof(type pointed to by ptrold) bytes compared to the value of ptrold. That is, the memory area pointed to by ptrnew will have moved n times sizeof(type pointed to by ptrold) bytes in the higher (or lower) address direction compared to the memory area pointed to by ptrold. Pointer-pointer addition and subtraction: two pointers cannot be added; this is an illegal operation because after addition, the result points to a place of unknown direction and is meaningless. Two pointers can be subtracted, but they must be of the same type. This is generally used with arrays; I will not go into more detail.
3. The Operators & and *
Here&is the address-of operator,*is the indirect operator.
&aThe result of the operation is a pointer. The type of the pointer is the type of a plus a *, the type pointed to by the pointer is the type of a, and the address pointed to by the pointer is, well, the address of a.
*pThe result of the operation is all kinds of things. In short, the result of *p is the thing pointed to by p. This thing has these characteristics: its type is the type pointed to by p, and the address it occupies is the address pointed to by p.
Example:
4. Pointer Expressions
If the result of an expression is a pointer, then this expression is called a pointer expression.
The following are some examples of pointer expressions:
Example:
Since the result of a pointer expression is a pointer, a pointer expression also has the four elements of a pointer: the type of the pointer, the type pointed to by the pointer, the memory area pointed to by the pointer, and the memory occupied by the pointer itself.
All right, when the resulting pointer of a pointer expression already explicitly has memory occupied by the pointer itself, then this pointer expression is an lvalue; otherwise, it is not an lvalue. In Example 7, &a is not an lvalue because it has not yet occupied explicit memory. *ptr is an lvalue, because the pointer *ptr has already occupied memory. In fact, *ptr is the pointer pa. Since pa already has its own position in memory, of course *ptr also has its own position.
5. The Relationship Between Arrays and Pointers
The array name of an array can actually be regarded as a pointer. Look at the following example:
In the above example, generally the array name array represents the array itself, with type int*(array+3)equals 3. The rest can be deduced by analogy.
Example:
In the above example, str is an array of three elements, and each element of the array is a pointer, with each pointer pointing to a string. If the pointer array name str is treated as a pointer, it points to the 0th element of the array. Its type is char **, and the type it points to is char *.
*str is also a pointer. Its type is char *, and the type it points to is char. The address it points to is the address of the first character of the string "Hello,thisisasample!", i.e., the address of 'H'. Note: a string is equivalent to an array; it is stored in memory in the form of an array. However, a string is an array constant, its content cannot be changed, and it can only be an rvalue. If regarded as a pointer, it is both a pointer to a constant and a constant pointer.
str+1 is also a pointer. It points to the 1st element of the array. Its type is char**, and the type it points to is char*.
*(str+1) is also a pointer. Its type is char*, and the type it points to is char. It points to the first character 'H' of "Hi,goodmorning.".
Now let me summarize the issue of the array name of an array (where what is stored in the array is also an array):
An array is declaredTYPE array[n], then the array name array has two meanings:
-
First, it represents the entire array; its type isTYPE[n];
-
Second, it is a constant pointer; the type of this pointer isTYPE*, and the type pointed to by this pointer isTYPE, that is, the type of the array element. The memory area pointed to by this pointer is the 0th element of the array. The pointer itself occupies a separate memory area; note that it is different from the memory area occupied by the 0th element of the array. The value of this pointer cannot be modified, that is, expressions like array++ are wrong. In different expressions, the array name array can play different roles. In the expression sizeof(array), the array name array represents the array itself, so at this time the sizeof function measures the size of the entire array.
In the expression *array, array plays the role of a pointer, so the result of this expression is the value of the 0th element of the array.sizeof(*array)What is measured is the size of an array element.
In the expression array+n (where n=0, 1, 2, ...), array acts as a pointer, so the result of array+n is a pointer, whose type isTYPE *, the type it points to is TYPE, and it points to the n-th element of the array. Therefore sizeof(array+n) measures the size of the pointer type. In a 32-bit program, the result is 4.
Example:
In the previous example, ptr is a pointer, whose type isint(*)[10], the type it points to is int
This section mentioned the function sizeof(). Now let me ask: does sizeof(pointer name) measure the size of the pointer's own type or the size of the type it points to?
The answer is the former. For example:
int(*ptr)[10];
Then in a 32-bit program, we have:
sizeof(int(*)[10])==4 sizeof(int[10])==40 sizeof(ptr)==4
Actually, sizeof(object) always measures the size of the object's own type, not the size of some other type.
6. The Relationship Between Pointers and Structure Types
You can declare a pointer to an object of a structure type. Example:
How do you access the three member variables of ss through the pointer ptr?
Answer:
ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者 ptr->b; ptr->c;
And how do you access the three member variables of ss through the pointer pstr?
Answer:
*pstr; //访问了ss 的成员a。 *(pstr+1); //访问了ss 的成员b。 *(pstr+2) //访问了ss 的成员c。
Although I debugged the above code on my MSVC++6.0, you should know that using pstr this way to access structure members is nonstandard. To explain why it is nonstandard, let's see how to access each element of an array through a pointer: (replace the structure with an array)
In form, it looks exactly like the nonstandard method of accessing structure members through a pointer.
All C/C++ compilers, when laying out array elements, always store the individual array elements in contiguous storage, with no gaps between elements. But when storing the members of a structure object, in some compilation environments, word alignment, double-word alignment, or some other alignment may be required, and several "padding bytes" may need to be inserted between adjacent members. This results in possible gaps of several bytes between members.
Therefore, in Example 12, even if *pstr accesses the first member variable a of the structure object ss, there is no guarantee that *(pstr+1) will necessarily access the structure member b. Because there may be several padding bytes between member a and member b, and perhaps *(pstr+1) happens to access exactly those padding bytes. This also proves the flexibility of pointers. If your purpose is to see whether there are padding bytes between structure members, hey, this is actually a good method.
However, the correct method for a pointer to access structure members should be the method using the pointer ptr as in Example 12.
7. The Relationship Between Pointers and Functions
You can declare a pointer as a pointer to a function.
int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。
You can use a pointer as a formal parameter of a function. In a function call statement, a pointer expression can be used as the actual argument.
The function fun in this example calculates the sum of the ASCII code values of each character in a string. As mentioned earlier, an array name is also a pointer. In the function call, when str is passed as an actual argument to the formal parameter s, the value of str is actually passed to s. The address pointed to by s becomes the same as the address pointed to by str, but str and s each occupy their own storage space. Performing a self-increment by 1 on s inside the function body does not mean that str is also self-incremented by 1 at the same time.
8. Pointer Type Conversion
When we initialize a pointer or assign a value to a pointer, the left side of the assignment operator is a pointer, and the right side of the assignment operator is a pointer expression. In most of the examples we have given earlier, the type of the pointer is the same as the type of the pointer expression, and the type pointed to by the pointer is the same as the type pointed to by the pointer expression.
In the above example, if we want the pointer p to point to the real number f, what should we do?
Is it the following statement?
p=&f;
No. Because the type of the pointer p is int *, and the type it points to is int. The result of the expression &f is a pointer; the type of the pointer is float *, and the type it points to is float.
The two are inconsistent, so direct assignment will not work. At least on my MSVC++6.0, a pointer assignment statement requires that the types on both sides of the assignment operator be the same, and the pointed-to types also be the same. I have not tried it on other compilers; you can try. To achieve our purpose, we need to perform a "forced type conversion":
p=(int*)&f;
If there is a pointer p, and we need to change its type and the type it points to toTYEP *TYPE, then the syntax is:(TYPE *)p
The result of such a forced type conversion is a new pointer. The type of the new pointer isTYPE *, and the type it points to isTYPE, and the address it points to is the address pointed to by the original pointer. None of the properties of the original pointer p are modified. (Remember this)
If a function uses pointers as formal parameters, then during the binding of actual arguments and formal parameters in a function call statement, the types must be consistent; otherwise, a forced conversion is needed:
Note that this is a 32-bit program, so the int type occupies four bytes, and the char type occupies one byte. The function fun reverses the order of the four bytes of an integer. Did you notice? In the function call statement, the result of the actual argument &a is a pointer; its type is int *, and the type it points to is int. The type of the formal parameter pointer is char *, and the type it points to is char. Thus, during the binding of the actual argument and the formal parameter, we must perform a conversion from int * to char *.
In connection with this example, we can do this:
Imagine the compiler's conversion process: the compiler first constructs a temporary pointer char *temp, then executes temp=(char *)&a, and finally passes the value of temp to s. So the final result is: the type of s is char *, the type it points to is char, and the address it points to is the starting address of a.
We already know that the value of a pointer is the address it points to. In a 32-bit program, the value of a pointer is actually a 32-bit integer.
Can we directly assign an integer as the value of a pointer to a pointer? Like the following statements:
Strictly speaking, the (TYPE *) here is not the same as the (TYPE *) in pointer type conversion. The meaning of (TYPE*) here is to treat the value of the unsigned integer a as an address. It was emphasized above that the value of a must represent a legal address; otherwise, when you use ptr, an illegal operation error will occur. Think about whether the reverse can be done: extract the address pointed to by a pointer, i.e., the value of the pointer, as an integer. Absolutely. The following example demonstrates extracting the value of a pointer as an integer, and then assigning this integer as an address to a pointer:
Now we already know that we can extract the value of a pointer as an integer, and we can also assign an integer value as an address to a pointer.
9. Pointer Safety Issues
Look at the following example:
The pointer ptr is a pointer of type int *, and the type it points to is int. The address it points to is the starting address of s. In a 32-bit program, s occupies one byte, and the int type occupies four bytes. The last statement not only changes the one byte occupied by s, but also changes the three bytes adjacent to s in the higher-address direction. What are these three bytes used for? Only the compiler knows, and the programmer is unlikely to know. Perhaps these three bytes store very important data, or perhaps these three bytes happen to be a piece of program code. Because of your careless use of the pointer, the values of these three bytes have been changed! This will cause a fatal error.
Let us look at another example:
This example can certainly pass compilation and can be executed. But do you see? After statement 3 performs a self-increment by 1 on the pointer ptr, ptr points to a storage area in the higher-address direction adjacent to the integer variable a. What is in this storage area? We do not know. It might be very important data, or even a piece of code.
Yet sentence 4 actually writes data into this memory area! This is a serious error. Therefore, when using pointers, the programmer must be very clear in their mind: where exactly does my pointer point? When using pointers to access arrays, care must also be taken not to exceed the lower or upper bounds of the array, as doing so can also cause similar errors.
In pointer forced type casting:ptr1=(TYPE *)ptr2If sizeof(type of ptr2) is greater than sizeof(type of ptr1), then it is safe to use pointer ptr1 to access the memory area pointed to by ptr2. If sizeof(type of ptr2) is less than sizeof(type of ptr1), then it is unsafe to use pointer ptr1 to access the memory area pointed to by ptr2. As for why, the reader should be able to understand it by thinking about it in conjunction with Example 18.