1 The Basics: How Memory and Its Allocation work

May 3, 2018 | Author: Anonymous | Category: Engineering & Technology, Computer Science, Operating Systems
Share Embed Donate


Short Description

Download 1 The Basics: How Memory and Its Allocation work...

Description

CS104: Data Structures and Object-Oriented Design (Fall 2013) August 29, 2013: A Refresher on Dynamic Memory Scribes: CSCI104 Teaching Team

Lecture Summary In this lecture, we begin a brief review of some of the basics, by talking about dynamic memory allocation. Being able to handle dynamically allocated memory and pointers (and recursion, the topic of the next lecture) are the key points to most data structures, and they are essential to object-oriented design in C++ as well.

1

The Basics: How Memory and Its Allocation work

At the physical level, computer memory consists of a large number of flip flops. Each flip flop consists of a few transistors, and is capable of storing one bit. Individual flip flops are addressable by a unique identifier, so we can read and overwrite them. Thus, conceptually, we can think of all of our computer’s memory as just one giant array of bits that we can read and write. Since as humans, we are not that good at doing all of our thinking and arithmetic in bits, we group them into larger groups, which together can be used to represent numbers. 8 bits are called 1 byte; beyond bytes, there are words (which are sometimes 16, sometimes 32 bits). Thus, more often, we conceptually regard our computer’s memory as one large (size 232 or so) array of bytes. A lot of things are stored in this memory. 1. All variables and other data used by all programs. 2. But also the programs’ code, including the operating system’s. The compiler and the operating system work together to take care of most memory management for you, but it is instructive to see what is going on under the hood. When you compile your code, the compiler can examine primitive data types and calculate how much memory they will need ahead of time. The required amount is then allocated to the program in the stack 1 space. For example, consider the following declarations: int n; int x[10]; double m; The compiler can immediately see that the code requires 4 + 4 × 10 + 8 = 52 bytes2 . It will insert code that will interact with the operating system to request the necessary number of bytes on the stack for your variables to be stored. In the example above, the compiler knows that the memory will look as follows: It knows the exact memory address of each variable; in fact, whenever we write n, this gets translated into something like “memory address 4127963” internally. Notice that if we attempted to access x[10] here, we would access the data associated with m, and may end up reading (or overwriting) some of its bits. This would almost certainly have very undesired consequences for the rest of the program. 1 This space is called the stack space because as functions get called, their memory gets added on top of existing memory. As they terminate, they are removed in a LIFO (last in, first out) order. 2 That’s with the current sizes for integers and doubles. About 20 years ago, int were typically 2 bytes, and double 4 bytes. Your code should never have to depend on what is at this moment the size of the basic data types.

1

n

x[0]

x[5]

m

Figure 1: The layout of memory on the stack for the declaration above. When functions call other functions, each function gets its own chunk of stack when it is called; it keeps all its local variables there, but also a program counter that remembers where in its execution it was. When the function finishes, its memory block is made available for other purposes again. Again, for statically allocated variables, the compiler can take care of all of this.

2

Dynamic allocation

Unfortunately, things aren’t quite as easy when we don’t know at compile time how much memory a variable will need. Suppose we want to do something like the following: int n; cin>>n; // create an array a of n integers Here, at compile time, the compiler does not know how much memory the array will need. It can therefore not allocate room for a variable on the stack. Instead, our program needs to explicitly ask the operating system for the right amount of space at run-time. This memory is assigned from the heap 3 space. The difference between static and dynamic memory allocation is summarized in the following table. To fully understand how dynamic memory allocation works, we need to spend some time on pointers. Static allocation Size must be known at compile time Performed at compile time Assigned to the stack First in last out

Dynamic allocation Size may be unknown at compile time Performed at run time Assigned to the heap No particular order of assignment

Table 1: Differences between statically and dynamically allocated memory.

3

Pointers

A pointer is an “integer” that points to a location in memory — specifically, it is an address of a byte4 . In C/C++, pointer types are declared by placing a star ‘*’ behind a regular type name. Thus, int *p; char *q; int **b; void *v; all declare pointers. In principle, all these are just addresses of some memory location, and C/C++ does not care what we store there. Declaring them with a type (such as int) is mostly for the programmer’s benefit: it may prevent us from messing up the use of the data stored in the location. It also affects the way some arithmetic on memory locations is done, which we explain below. Two of the ways in which “regular” variables and pointers often interact are the following: 3 This is called the heap space since it can be selected from any portion of the space that has not been allocated already. While the stack remains nicely organized, memory in the heap tends to be more messy and all over the place. Hence the name. 4 This means that all pointers are of the same size, and that the size of pointer used by a computer places a limit on the size of memory it can address. For example, a computer using typical 32-bit pointers can only use up to 232 bytes or 4 gigabytes of memory. The modern shift to 64-bit architectures turns this to 264 bytes, which will be enough for a while.

2

1. We want to find out where in memory a variable resides, i.e., get the pointer to that variable’s location. 2. We want to treat the location a pointer points to as a variable, i.e., access the data at that location, by reading it or overwriting it. The following piece of code illustrates some of these, as well as pitfalls we might run into. int* p, *q; //Pointers to two integers int i,j; i = 5; j = 10; p = &i; //Obtain the address of i and save it to p cout>n; b = new int[n];

4

Notice that there are no parentheses, but instead, we have brackets for the number of items. new figures out by itself how much memory is needed, and returns the correct type of pointer. If we wanted space for just one integer, we could write int *p = new int; While this is not really very useful for a single integer, it will become very central to allocating objects later, where we often allocate one at a time dynamically. To release memory, the equivalent of free is the delete operator, used as follows: delete [] b; delete p; The first example deallocates an array, while the second deallocates a single instance of a variable (a single int in our example). This deallocates the memory pointed to by the pointers. As with free, it still leaves the pointers themselves pointing to the same memory location, so it is good style to write b = NULL or p = NULL after the delete commands.

5

View more...

Comments

Copyright � 2017 NANOPDF Inc.
SUPPORT NANOPDF