Pointers and free storage space

Table of contents

3.7.1 Declaring and initializing pointers

3.7.2 The dangers of pointers

3.7.3 Pointers and numbers

3.7.4 Use new to allocate memory

3.7.5 Use delete to release memory

3.7.6 Use new to create dynamic arrays


There are three basic properties that computers must keep track of when storing data:
(1) Where the information is stored
(2) What value is stored
(3) What type of information is stored

        One strategy can be used to achieve this: define a simple variable. The declaration statement indicates the type and symbolic name of the value. It also lets the program allocate memory for the value and keep track of that memory location internally. Let's take a look at another strategy, which is particularly important when developing C++ classes. This strategy is based on pointers. A pointer is a variable that stores the address of a value rather than the value itself.
        Before discussing pointers, let's look at how to find the address of a regular variable. Just use the address operator & on a variable to get its location. For example, if home is a variable, &home is its address.
        The following program demonstrates the use of this operator:

#include<iostream>

int main()
{
    using namespace std;
    int donuts = 6;
    double cups = 4.5;
    cout << "donuts value = " << donuts;
    cout << " and donuts adress = " << &donuts << endl;

    cout << "cups value = " << cups;
    cout << " and cups adress = " << &cups << endl;
    return 0;
}

The following is the output of the program on win10 system:

donuts value = 6 and donuts adress = 000000A5B0B4FAA4
cups value = 4.5 and cups adress = 000000A5B0B4FAC8

D:\Programme\VisualStudio\data\Project2\x64\Debug\Project2.exe (进程 14100)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

        When displaying addresses, this implementation's cout uses hexadecimal notation because this is the notation commonly used for memory (some implementations may use decimal notation). In this implementation, the storage location of donuts is lower than that of cups, and the difference between the two addresses is 000000A5B0B4FAC8-000000A5B0B4FAA4 (that is, 4). This makes sense because donuts are of type int and this type uses 4 bytes. Of course, the address values ​​given by different systems may be different. Some systems may store cups first and then donuts, so the difference between the two address values ​​will be 8 bytes, because the value of the cups type is double. Additionally, on some systems, these two variables may not be stored in adjacent memory locations.
        When using regular variables, the value is the specified quantity and the address is the derived quantity. Let's take a look at pointer strategy, which is the core of the C++ memory management programming concept.
Basic principles of pointers and C++:
        The difference between object-oriented programming and traditional procedural programming is that OOP emphasizes decision-making during the runtime phase (rather than the compile phase). The run phase refers to when the program is running, and the compile phase refers to when the compiler puts the program together. Run-time decisions are like going on vacation, choosing which places to visit depending on the weather and your mood at the time; compile-time decisions are more like sticking to a preset schedule no matter what the conditions.
        Run-time decisions provide flexibility and can be adapted to the situation at the time. For example, consider the case of allocating memory for an array. The traditional approach is to declare an array. To declare an array in C++, you must specify the length of the array. Therefore, the array length is set when the program is compiled; this is a compile-time decision.
        You might think that an array of 20 elements is enough in 80% of cases, but sometimes a program needs to handle 200 elements. To be on the safe side, an array of 200 elements is used. This way, the program wastes memory most of the time. OOP makes programs more flexible by deferring such decisions to the runtime phase. After the program runs, you can tell it that it only needs 20 elements this time, and you can tell it that it needs 205 elements the next time.
        In summary, when using OOP, you probably determine the length of the array at runtime. To use this approach, the language must allow array creation while the program is running. As you'll see later, C++ uses the w keyword to request the correct amount of memory and a pointer to keep track of the location of the newly allocated memory. Making decisions at runtime is not unique to OOP, but writing such code is simpler in C++ than in C.

        The new strategy for handling stored data is just the opposite, treating addresses as specified quantities and values ​​as derived quantities. A special type of variable - a pointer is used to store the address of a value. Therefore, the pointer name represents the address.
        The * operator, known as the indirection or dereference operator, is used on a pointer to get the value stored at that address (this is the same notation used for multiplication; C++ uses the context to determine whether multiplication or dereference is referred to) ).
        For example, assuming that many is a pointer, then many represents an address, and *manly represents the value stored at that address. *manly is equivalent to a regular int variable.
        The following program illustrates these points and also demonstrates how to declare pointers:

#include<iostream>

int main() {
    using namespace std;
    int updates = 6;
    int* p_updates;
    p_updates = &updates;

    //express values two ways
    cout << "Value: updates = " << updates;
    cout << ", *p_updates = " << *p_updates << endl;

    //express address two ways
    cout << "Address: &address = " << &updates;
    cout << ", p_updates = " << p_updates << endl;

    //uses pointer to change value
    *p_updates = *p_updates + 1;
    cout << "Now updates = " << updates << endl;
    return 0;
}

Here is the output of this program:

Value: updates = 6, *p_updates = 6
Address: &address = 0000007D64BBF5C4, p_updates = 0000007D64BBF5C4
Now updates = 7

D:\Programme\VisualStudio\data\Project2\x64\Debug\Project2.exe (进程 20016)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

        It can be seen from this that the int variable updates and the pointer variable p_updates are just two sides of the same coin. The variable updates represents the value and uses the & operator to get the address; while the variable p_updates represents the address and uses the * operator to get the value. Since p_updates points to updates, *p_updates and updates are completely equivalent. *p_updates can be used like int variables.
        You can even assign values ​​to *p_updates like in the program above. Doing so will modify the value pointed to, i.e. updates.

3.7.1 Declaring and initializing pointers

        The computer needs to keep track of the type of value pointed to by the pointer. For example, the address of char and the address of double look the same, but the number of bytes used by char and double is different, and the internal format they use when storing values ​​is also different. Therefore, a pointer declaration must specify the type of data pointed to by the pointer.
        For example, the above example contains a statement like this:

int* p_updates;

        This indicates that *p_updates is of type int. Since the * operator is used with pointers, the p_updates variable itself must be a pointer. We say that p_updates points to type int, and we also say that p_updates is a pointer to type int, or int*. It can be said that p_updates is a pointer, and *p_updates is an int, not a pointer.

        By the way, the spaces around the * operator are optional. Traditionally, C programmers have adopted this format.

int *ptr;

        This emphasizes that *ptr is a value of type int. And many C++ programmers use this format:

int* ptr;

        This emphasizes that int* is a type - a pointer to int. It makes no difference to the compiler where you add the spaces, and you can even write:

int*ptr;

        But be aware that the following declaration creates a pointer (p1) and an int variable (p2):

int* p1, p2;

        For each pointer variable name, a * is required.
        Note: In C++, int* is a composite type, which is a pointer to int. The same syntax can be used to declare pointers to other types:

double* tax_ptr;
char * str;

        Since tax_ptr has been declared as a pointer to a double, the compiler knows that *tax_ptr is a value of type double. That is, he knows that *tax_ptr is a value stored in floating point format, which (on most systems) occupies 8 bytes.
        Pointer variables are not just pointers, but pointers to specific types. The type of tax_ptr is a pointer to double type (or double* type), and str is a pointer type to char (or char* type). Although they are both pointers, they are different types of pointers. Like arrays, pointers are based on other types.
        Although tax_ptr and str point to two data types with different lengths, the lengths of the two variables themselves are usually the same. In other words, the address of char is the same length as the address of double, just like 1016 may be the street address of a supermarket, and 1024 may be the street address of a small village.
        The length or value of an address neither indicates anything about the length or type of the variable nor what building is at that address. Generally speaking, addresses require 2 or 4 bytes, depending on the computer system (some systems may require larger addresses, and systems may use addresses of different lengths for different types).
        Pointers can be initialized in the declaration statement. In this case, the pointer is initialized, not the value it points to. That is, the following statement sets the value of pt (not *pt) to &higgens:

int higgens = 5;
int* pt = &higgens; //将pt(而不是*pt)的值设置为&higgens,其实按照对指针的第二种理解即可

        The following demonstrates how to initialize a pointer to an address:

#include<iostream>

int main() {
    using namespace std;
    int higgens = 5;
    int* pt = &higgens;
    cout << "Value of higgens = " << higgens
        << "; Address of higgens = " << &higgens << endl;
    cout << "Value of *pt = " << *pt
        << "; Value of pt = " << pt << endl;
    return 0;
}

        Here is the output of this program:

Value of higgens = 5; Address of higgens = 00000060D5BDFC34
Value of *pt = 5; Value of pt = 00000060D5BDFC34

D:\Programme\VisualStudio\data\Project2\x64\Debug\Project2.exe (进程 23748)已退出,代码为 0。
按任意键关闭此窗口. . .

        It can be seen from this that the program initializes pt (not *pt) to the address of higgens.

3.7.2 The dangers of pointers

        It is extremely important to note that when you create a pointer in C++, the computer allocates memory to store the address, but it does not allocate memory to store the data pointed to by the pointer. Providing space for your data is a separate step, and ignoring this step is asking for trouble, as shown below:

long * fellow;
*fellow = 223323;

        Fellow is indeed a pointer, but where does it point? The above code does not assign the address to fellow. So where will 223323 be placed? we do not know.
        Since fellow is not initialized, it may have any value. No matter what the value is, the program interprets it as the address where 223323 is stored. If the value of fellow happens to be 1200, the computer will place the data at 1200, even though this happens to be the address of the program code.
The place pointed by fellow is probably not the place where 223323 is stored. This kind of error is likely to lead to some of the most hidden and difficult to track bugs.
        Warning: Be sure to initialize the pointer to a specific, appropriate address before applying the dereference operator (*) to it. This is the golden rule about using pointers!

3.7.3 Pointers and numbers

        Pointers are not integers, although computers usually treat addresses as integers. Conceptually, pointers and integers are distinct types. Integers are numbers that can be added, subtracted, multiplied and divided, while pointers describe locations, and multiplying two addresses doesn't make any sense. They are also different from each other in terms of the operations that can be performed on integers and pointers. Therefore, you cannot simply assign an integer to a pointer.

int* pt;
pt = 0xB8000000;    //type mismatch

        Here, the left hand side is a pointer to an int, so we can assign it to the address, but the right hand side is an integer. 0xB8000000 is the combined segment offset address of video memory in old computer systems, but this statement does not tell the program that this number is an address. Before the release of the C99 standard, the C language allowed such assignment. But C++ has stricter requirements for type consistency, and the compiler will display an error message announcing a type mismatch.
        To use a numeric value as an address, cast the number to the appropriate address type:

int* pt;
pt = int*(0xB8000000);

        In this way, both sides of the assignment statement are the addresses of integers, so the assignment is valid. Note: pt is the address of an int value, which does not mean that the type of pt itself is int. For example, on some platforms, the int type is a 2-byte value, and the address is a 4-byte value.

3.7.4 Use new to allocate memory

        Previously we initialized the pointer to the address of the variable; the variable is a named memory allocated at compile time, and the pointer just provides an alias for direct access to the memory through the name. The real use of pointers is to allocate unnamed memory at runtime to store values. In this case, memory can only be accessed through pointers.
        In C language, you can allocate memory through malloc(); you can still do this in C++, but C++ has a better way-the new operator. Let's try this new technique, which allocates unnamed memory for an int value at runtime and uses a pointer to access this value. The key here is C++'s new operator. The programmer has to tell new which data type needs to allocate memory; new will find a memory block of the correct length and return the address of the memory block.
        It is the programmer's responsibility to assign that address to a pointer. Here is an example of this:

int* pn = new int;

        new int tells the program that it needs memory suitable for storing ints. The new operator determines how many bytes of memory are required based on the type. Then he finds such memory and returns its address. Next, assign the address to pn, which is a declared pointer to int. Now pn is the address and *pn is the value stored there. Compare this approach to assigning the address of a variable to a pointer:

int higgens;
int* pt = &higgens;

        In both cases (pn and pt), the address of an int variable is assigned to the pointer. In the second case the int can be accessed via the name higgens, in the first case it can only be accessed via the pointer. This leads to a question: the memory pointed to by pn has no name, how to call it? We say that pn points to a data object. The "object" here is not an object in "object-oriented programming", but the same "thing". The term "data object" is more general than "variable" and refers to an inner block allocated for a data item. Therefore, variables are also data objects, but the memory pointed to by pn is not a variable. Pointers give programs greater control over managing memory.
        The general format for obtaining and specifying allocated memory for a data object (which can be a structure or a basic type) is as follows:

typeName* pointer_name = new type;

        The data type needs to be specified in two places: to specify what kind of memory is required and to declare the appropriate pointer. Of course, if a pointer of the corresponding type has been declared, you can use that pointer without declaring a new pointer.
        The following code demonstrates how to use new with two different types:

#include<iostream>

int main()
{
    using namespace std;
    int nights = 1001;
    int* pt = new int;    //为int类型分配空间
    *pt = 1001;    //在这里存储一个int值
    
    cout << "nights value = " << nights << ": location = " << &nights << endl;
    cout << "int value = " << *pt << ": location = " << pt << endl;

    double* pd = new double;
    *pd = 100000001.0;
    
    cout << "double value = " << *pd << ": location = " << pd << endl;
    cout << "location of pointer pd: " << &pd << endl;
    
    cout << "size of pt = " << sizeof(pt) << ": size of *pt = " << sizeof(*pt) << endl;
    
    cout << "size of pd = " << sizeof pd << ": size of *pd = " << sizeof(*pd) << endl;
}

        Here is the output of this program:

nights value = 1001: location = 000000212B3AFBA4
int value = 1001: location = 0000015FD071E3E0
double value = 1e+08: location = 0000015FD0722C40
location of pointer pd: 000000212B3AFBE8
size of pt = 8: size of *pt = 4
size of pd = 8: size of *pd = 8

D:\Programme\VisualStudio\data\Project2\x64\Debug\Project2.exe (进程 23536)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

Program description:
        This program uses new to allocate memory for int type and double type data objects respectively. This is specified when the program is running. The pointers pt and pd point to these two data objects. Without them, these memory units cannot be accessed. With these two pointers, you can use *pt and *pd like variables to assign these values ​​​​to new data objects. This allows the values ​​to be displayed by printing *pt and *pd.

        The program also points out one of the reasons why the type pointed to by a pointer must be declared. The address itself only indicates the beginning of the object's storage address, not its type (number of bytes used). As you can tell from the addresses of these two values, they are just numbers, with no type or length information provided.
        Also, a pointer to an int has the same length as a pointer to a double. They are both addresses, but because use_new.cpp declares the type of the pointer, the program knows that *pd is an 8-byte double value and *pt is a 4-byte int value. When use_new.cpp prints the value of *pd, cout knows how many bytes to read and how to interpret them.

3.7.5 Use delete to release memory

        When using memory, you can use C++ to request it. Another aspect is the delete operator, which allows the memory to be returned to the memory pool after it is used. This is a key step towards the most efficient use of memory.
        The returned or freed memory is available for use by other parts of the program. When using delete, follow it with a pointer to a memory block (these memory blocks were originally allocated with new):

int* ps = new int;
...
delete ps;

        This will free the memory pointed to by ps but will not delete the pointer ps itself. For example, ps can be repointed to another newly allocated memory block.
        Be sure to use new and delete in pairs; otherwise a memory leak will occur, that is, the allocated memory can no longer be used. If the memory leak is severe, the program will terminate because it is constantly looking for more memory.
        Do not try to free a memory block that has already been freed. The C++ standard states that the results of doing so will be undefined, which means that anything can happen. In addition, you cannot use delete to release the memory obtained by declaring a variable:

int* ps = new int;    //ok
delete ps;    //ok
delete ps;    //not ok now
int jugs = 5;    //ok
int* pi = &jugs;    //ok
delete jugs;    //not allowed,memory not allocated by new

        Warning: Memory allocated with new can only be released with delete. However, it is safe to use delete on a null pointer.
        Note that the key to using delete is to use it for the allocated memory allocated by new. This does not mean using the pointer for new, but the address of new:

int* ps = new int;    //分配内存
int* pq = ps;    //创建第二个指针指向同一个内存块
delete pq;    //使用第二个指针删除

        In general, do not create two pointers to the same memory block, as this will increase the likelihood of mistakenly deleting the same memory block twice.

3.7.6 Use new to create dynamic arrays

        Generally, for large data (such as arrays, strings, and structures), you should use new, which is where new comes in.
Suppose you are writing a program whose need for an array depends on the information provided by the user at runtime. If you create an array through declaration, memory space will be allocated for it when the program is compiled. Regardless of whether the program ultimately uses the array, the array is there, occupying memory. Allocating memory for an array at compile time is called static binding, which means that the array is added to the program at compile time.
        But when using new, if the array is needed during the running phase, it will be created; if it is not needed, it will not be created. You can also choose the length of the array while the program is running. This is called dynamic binding, which means the array is created while the program is running. This kind of array is called a dynamic array. When using static binding, the length of the array must be specified when writing the program; when using dynamic binding, the program will determine the length of the array at run time.
        There are two basic questions about dynamic arrays: how to create an array using C++'s new operator, and how to access array elements using pointers.
1. Use new to create a dynamic array.
        In C++, it is easy to create a dynamic array; you only need to tell new the element type and number of elements of the array. The type name must be followed by square brackets containing the number of elements. For example, to create an array of 100 int elements, you would do:

int* psome = new int [10];    //得到一个10个int值长度的内存块

        The new operator returns the address of the first element. In this example, the address is assigned to the pointer psome. When the program has finished using the memory blocks allocated by new, it should use delete to release them. However, for arrays created by new, another format of delete should be used to release them:

delete [] psome;

        The square brackets tell the program that the entire array should be freed, not just the element pointed to by the pointer. Note the square brackets between the pointer and delete. If you use new without square brackets, you should not use square brackets with delete. If you use new with square brackets, you should also use delete with square brackets.
In short, when using new and delete, you should follow the following rules:
(1) Do not use delete to release memory that is not allocated by new
(2) Do not use delete to release the same memory block twice
(3) If you use new [] for an array To allocate memory, delete [] should be used to release it
(4) If new is used to allocate memory for an entity, delete (without square brackets) should be used to release it
(5) It is safe to apply delete to a null pointer.
        Now let's go back to dynamics Array, psome is a pointer to int (the first element of the array). Your responsibility is to keep track of the number of elements in the memory block. That is, since the compiler cannot keep track of the fact that psome points to the first of 10 integers, you must write your program so that it keeps track of the number of elements.
        In fact, the program does keep track of the amount of allocated memory so that it can be released correctly when the delete[] operator is used later. But this information is not public; for example, you cannot use the sizeof operator to determine the number of bytes a dynamically allocated array contains.
The general format for allocating memory for an array is as follows:

type_name* pointer_name = new type_name[num_elements];

        Using the new operator ensures that the memory block is enough to store num_elements elements of type type_name, and pointer_name will point to the first element.
2.
        After using dynamic array to create a dynamic array, how to use it? The following statement will create a pointer psome that points to the first element in a memory block containing 10 int values:

int* psome = new int [10];

        Think of it as a finger pointing towards the element. Assuming that int occupies 4 bytes, move your finger 4 bytes in the correct direction and your finger will point to the second element. There are 10 elements in total, and that's how far the finger can move. Therefore, the new statement provides all the information to identify each element in the memory block.
        Now think about how to access the elements within it from a practical perspective. The first element is not a problem, since psome points to the first element in the array, *psome is the value of the first element. This way, there are still 9 elements. To access a dynamic array in C++, you only need to use the pointer as the array name. That is, for the first element, you can use psome[0] instead of *psome; for the second element, you can use psome[1], and so on.
        In this way, it is simple to use pointers to access dynamic arrays, although it is not yet clear why this method works. The reason this is possible is that both C and C++ use pointers internally to handle arrays. The basic equivalence of arrays and pointers is one of the advantages of C and C++.
        The following program demonstrates how to use new to create a dynamic array and use array notation to access elements; it also points out the fundamental difference between pointers and real array names:

#include<iostream>

int main()
{
    using namespace std;
    double* p3 = new double[3];
    p3[0] = 0.2;
    p3[1] = 0.5;
    p3[2] = 0.8;
    cout << "p3[1] is " << p3[1] << ".\n";
    p3 = p3 + 1;
    cout << "Now p3[0] is " << p3[0] << " and ";
    cout << "p3[1] is " << p3[1] << ".\n";
    p3 = p3 - 1;
    delete[] p3;
    return 0;
}

Here is the output of this program:

p3[1] is 0.5.
Now p3[0] is 0.5 and p3[1] is 0.8.

D:\Programme\VisualStudio\data\Project2\x64\Debug\Project2.exe (进程 8656)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

        It can be seen that the above program uses p3 as an array name, but the following line of code points out the fundamental difference between array names and pointers:

p3 = p3 + 1;

        The value of the array name cannot be modified, but the pointer is a variable, so its value can be modified.
        Please note the effect of adding 1 to p3. The expression p3[0] now refers to the second value of the array. So adding 1 to p3 causes it to point to the second element instead of the first. After decrementing it by 1, the pointer will point to the original value, so that the program can provide the correct address to delete[]. Adjacent int addresses usually differ by 2 bytes or 4 bytes, and after adding 1 to p3, it will point to the address of the next element, which shows that there is something special about pointer arithmetic.

Guess you like

Origin blog.csdn.net/m0_56312629/article/details/126205820