More About Dynamic Arrays
Welcome back to Objective-C Tuesdays. Last time we were talking about
dynamic arrays in C
and NSMutableArray
in Objective-C. We'll continue
looking at both today.
We looked at using malloc()
or calloc()
to
dynamically allocate a block of memory that you can treat like an
array. This is very useful when you don't know the number of items you
need to store until run time or when you need the array to exist longer
than the current function call.
But what happens when you need to grow or shrink the memory block?
Well, the straightforward way would be to allocate a new memory block,
copy over the contents and free the old block. You use the
memcpy()
function in the C standard library to do the
copying.
// resizing a memory block
int *lotteryNumbers = malloc(sizeof(int) * 6);
if ( ! lotteryNumbers) {
// memory allocation failed
exit(EXIT_FAILURE);
}
lotteryNumbers[0] = 7;
// ...lotteryNumbers[5] = 29;
// now we need to store more numbers
// so allocate a bigger memory block
int *biggerMemoryBlock = malloc(sizeof(int) * 12);
if ( ! biggerMemoryBlock) {
// memory allocation failed
exit(EXIT_FAILURE);
}
// copy lotteryNumbers into biggerMemoryBlock
memcpy(biggerMemoryBlock, lotteryNumbers, sizeof(int) * 6);
// free old memory block
free(lotteryNumbers);
// replace old memory block pointer with new one
lotteryNumbers = biggerMemoryBlock;
lotteryNumbers[6] = 31;
// ...lotteryNumbers[11] = 49;
When copying blocks of memory, be careful to specify the correct size
in bytes. In the example above, we use the same expression to calculate
the size, sizeof(int) * 6
for both the original call to
malloc()
and to memcpy()
. Using the
sizeof
operator like this serves two purposes. It makes
the code portable, since the size of an int
may be
different on some systems (it's four bytes on iOS). More importantly,
it tells anyone reading the code that you intended to allocate six
int
s. If you had specified simply malloc(24)
or even malloc(6 * 4)
, your intention is not as clear.
Resizing a memory block with realloc()
Allocating a larger or smaller memory block and copying over the
contents works perfectly, but it is tedious. Fortunately, the C
standard library includes the realloc()
function which
does exactly this for you in one function call. It can operate on any
memory block allocated with malloc()
or
calloc()
. In addition, because it's part of the standard
library, realloc()
can do things that you can't: it will
try to grow or shrink the memory block in place, skipping the copy
step. Sometimes the memory manager will give you a block of memory
somewhat bigger than you requested, other times the memory area after
your memory block is currently unused. Because realloc()
is privy to the implementation details of the memory manager, it can
figure out when it's safe to grow your memory block in place.
The realloc()
function returns a pointer to either the
original memory block or a new memory block on success, or
NULL
if memory allocation fails. If the new memory block
is bigger than the original, the new items will be set to zero, just as
if you allocated the memory block with calloc()
. Like
malloc()
, realloc()
requires you to calculate
the size of the memory block in bytes.
// using realloc()
int *lotteryNumbers = malloc(sizeof(int) * 2);
if ( ! lotteryNumbers) {
// out of memory
exit(EXIT_FAILURE);
}
lotteryNumbers[0] = 7;
lotteryNumbers[1] = 11;
// ...
int *resizedLotteryNumbers = realloc(lotteryNumbers, sizeof(int) * 6);
if ( ! resizedLotteryNumbers) {
// out of memory but lotteryNumbers memory block
// is still valid and unchanged
exit(EXIT_FAILURE);
}
// replace the old pointer with the new one
lotteryNumbers = resizedLotteryNumbers;
lotteryNumbers[2] = 19;
// ...
If realloc()
fails, it leaves the original memory block
unchanged. Just as for calloc()
and malloc()
,
you must remember to call free()
on the memory block when
you're done with it.
Inserting items into a C array
It should be obvious by now how to add items to the end of a C array
(if it's big enough) or a dynamically allocated memory block that
you've enlarged using realloc()
. But how do you insert an
item at the beginning or middle of an array while keeping all of the
current items? There's no magic here, you have to shift items to higher
indices to make space for the new one.
// insert an item at the start of a C array
int lotteryNumbers[6] = { 11, 17 }; // has room for six items
// shift current items up
lotteryNumbers[2] = lotteryNumbers[1];
lotteryNumbers[1] = lotteryNumbers[0];
// array now holds { 11, 11, 17 }
lotteryNumbers[0] = 7; // insert new item
// array now holds { 7, 11, 17 }
Note that you need to shift items starting at the end of the array,
otherwise you'll overwrite items you want to preserve. Obviously
writing a line of code for each item you need to shift isn't a
practical solution. You might be tempted to use the
memcpy()
function to shift items, but memcpy()
has an important restriction: the source and destination memory blocks
may not overlap. (This is because memcpy()
may copy items
front to back and thus overwrite items you want to preserve.) The
memmove()
function is meant to be used when the memory
blocks overlap and will always shift items in the correct order to
preserve the contents.
// using memmove() to insert an item
int lotteryNumbers[6] = { 11, 17 }; // has room for six items
// shift current items up
int *destination = lotteryNumbers + 1;
int *source = lotteryNumbers;
size_t size = sizeof(int) * 2;
memmove(destination, source, size);
// array now holds { 11, 11, 17 }
lotteryNumbers[0] = 7; // insert new item
// array now holds { 7, 11, 17 }
Because memmove()
can operate on any kind of array or
memory block, we need to do some pointer arithmetic to determine the
source and destination addresses and the number of bytes to move.
Again, the sizeof
operator helps make our intentions
clear. An alternate way specify the source and destination addresses is
to use the address of operator (&
) on indexed items in
the array.
// determine source and destination using address of
// ...
int *destination = &lotteryNumbers[1];
int *source = &lotteryNumbers[0];
size_t size = sizeof(int) * 2;
memmove(destination, source, size);
// array now holds { 11, 11, 17 }
// ...
These are equivalent notations and choosing one over the other is a
matter of style.
You can use this technique to insert items into both C arrays and
dynamically allocated memory blocks. Just make sure that there is
enough space to move items around and that you don't write items past
the end of your array or memory. As with all things in C, there is no
safety net. To remove items from the beginning or middle of an array,
follow the same procedure using memmove()
, only shift
items down and intentionally overwrite the items you want to delete.
I'll leave this one as an exercise for the reader ☺.
Inserting items into an NSMutableArray
While using memmove()
to shift items around isn't
too terrible, the equivalent operations using
NSMutableArray
are naturally much more direct. To insert a
single item at a particular index, you naturally use
-insertObject:atIndex:
// inserting an item into an NSMutableArray
NSMutableArray *colors = [NSMutableArray arrayWithObjects:@"red",
@"green",
@"blue",
nil];
[colors insertObject:@"orange" atIndex:1];
// colors now holds "red", "orange", "green" and "blue"
Inserting multiple items at one time is trickier. With the
-insertObjects:atIndexes:
method, you can insert items
from an NSArray
into your NSMutableArray
. The
tricky part here is that you need to also pass in an
NSIndexSet
(or NSMutableIndexSet
) along with
the array. Here's how you would insert an NSArray
containing one item.
// inserting an item into an NSMutableArray
NSMutableArray *colors = [NSMutableArray arrayWithObjects:@"red",
@"green",
@"blue",
nil];
NSArray *newColors = [NSArray arrayWithObject:@"orange"];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:1];
[colors insertObjects:newColors atIndexes:indexSet];
// colors now holds "red", "orange", "green" and "blue"
You must make sure that there is an index in the NSIndexSet
for each item you are inserting. If you want to insert the new items
all in one spot, the +indexSetWithIndexesInRange:
method
of NSIndexSet
makes this easy.
// inserting multiple items into an NSMutableArray
NSMutableArray *colors = [NSMutableArray arrayWithObjects:@"red",
@"green",
@"blue",
nil];
NSArray *rainbowColors = [NSArray arrayWithObjects:@"orange",
@"yellow",
@"indigo",
@"violet",
nil];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)];
[colors insertObjects:rainbowColors atIndexes:indexSet];
// colors now holds "red", "orange", "yellow", "indigo", "violet", "green" and "blue"
Sometimes you want to insert different items at different points in
your NSMutableArray
. This is where
-insertObjects:atIndexes:
really shines. You'll want to
use NSMutableIndexSet
to create the index set in this
case. Lets modify the previous example to insert the colors in standard
rainbow order.
// inserting multiple items into an NSMutableArray
NSMutableArray *colors = [NSMutableArray arrayWithObjects:@"red",
@"green",
@"blue",
nil];
NSArray *rainbowColors = [NSArray arrayWithObjects:@"orange",
@"yellow",
@"indigo",
@"violet",
nil];
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
[indexSet addIndex:1];
[indexSet addIndex:2];
[indexSet addIndex:5];
[indexSet addIndex:6];
[colors insertObjects:rainbowColors atIndexes:indexSet];
// colors now holds "red", "orange", "yellow", "green", "blue", "indigo" and "violet"
To figure out the correct indices can be tricky. Inside the
-insertObjects:atIndexes:
method, new items will be
inserted one at a time, starting with the lowest index in the index
set. While inserting a group of items is a very powerful capability of
NSMutableArray
, often times I think it's easier and faster
to write correct code that uses -subarrayWithRange:
to
break the items up into contiguous blocks before inserting them, or
even to use -insertObject:atIndex:
repeatedly to insert
items one by one. But when it is straightforward to figure out the
index set, -insertObjects:atIndexes:
makes your code very
succinct.
Next time, we'll look at
sorting C arrays,
NSArray
s and NSMutableArray
s.