14. Arrays#
An array is an ordered collection of items all of the same type. Arrays are somewhat similar to the lists we used in Python. We index them with square brackets []
, and the indexing starts at zero. However, the size of an array is fixed when it is declared.
14.1. Declaring and Initializing Arrays#
Array variables are similar to normal variables, but after the variable name, one pair of square [ ] brackets is present for each dimension of the array. Uninitialized arrays must have the dimension(s) listed within the brackets.
To initialize an array, place data in curly {}
braces following the equals sign, with each value separated by commas. Arrays may be partially initialized by providing fewer elements than the size of the array. The remaining array elements are initialized to zero. If an array is completely initialized, then the dimension of the array is not required - the compiler has enough information to appropriately size the array. When explicitly declaring the size of the array, the dimensions must be positive integer constants, constant expressions, or positive integer variables.
1//filename: array_declare.cpp
2//complile: g++ -std=c++17 -o array_declare array_declare.cpp
3//execute: ./array_declare
4#include <iostream>
5
6int main(int argc, char *argv[]) {
7 int numbers[10]; /* declares an array called numbers with 10 integers, values not initialized */
8 int contestants[] = {1, 2, 3}; /* array of 3 elements initialized with the values of 1,2,3 */
9 float averages[5] = {7.6, 3.2}; /* array of 5 elements, the last 3 elements are zeros */
10
11 const int nRows = 20;
12 const int nColumns = 40;
13 int matrix[nRows][nColumns]; /* declares a matrix */
14
15 /* Create an array where the size comes from a variable */
16 int numElements = 30;
17 double data[numElements];
18 double d = 3.14;
19 std::cout << "data array size: " << sizeof(data) << "\n";
20 std::cout << "size of d: " << sizeof(d) << "\n";
21
22 return EXIT_SUCCESS;
23}
14.2. Using Arrays#
Access elements of an array by specifying their index in square brackets after the variable name. Array indices must be an integer type (char, int, size_t, etc.). Indices start at 0 and go to the array size - 1.
1//filename: factorial.cpp
2//complile: g++ -std=c++17 -o factorial factorial.cpp
3//execute: ./factorial
4#include <iostream>
5
6 int main(int argc, char *argv[] ) {
7 int factorial[7] = {1};
8
9 for (int i=1; i < 7; i++) {
10 factorial[i] = i * factorial[i-1];
11 }
12
13 for (int i = 0; i < 7; i++) {
14 std::cout << "Factorial[" << i << "] = " << factorial[i] << "\n";
15 }
16
17 return EXIT_SUCCESS;
18 }
For two-dimensional arrays, the first dimension is generally considered to be the number of rows, and the second dimension to be the number of columns. As with Python and lists of lists, two-dimensional arrays are considered to be an array of “single dimension arrays”. E.g., int a[3][4] is a single-dimension array of three elements. Each of those elements is a single-dimension array of 4 elements. At the bottom of the following example, you can see how C++ stores a two-dimensional array in memory. (The section discusses arrays and pointers.)
1//filename: add_matrix.cpp
2//complile: g++ -std=c++17 -o add_matrix add_matrix.cpp
3//execute: ./add_matrix
4#include <iostream>
5
6int main(int argc, char *argv[]) {
7 int a[3][4] = { { 5, 6, 7, 8 }, { 10, 20, 30, 40 }, {5, 15, 20, 25} };
8 int b[3][4] = { { 1, 2, 3, 4 }, { 3, 2, 1, 0 }, {0, 1, 2, 3 } };
9 int sum[3][4];
10
11 for (int row = 0; row < 3; row++) {
12 for (int column = 0; column < 4; column++) {
13 sum[row][column] = a[row][column] + b[row][column];
14 }
15 }
16
17 std::cout << "Result:\n";
18 for (int row = 0; row < 3; row++) {
19 for (int column = 0; column < 4; column++) {
20 std::cout << "\t" << sum[row][column];
21 }
22 std::cout << "\n"; /*new line to end the row */
23 }
24
25 /* demonstrates how the data is layed out in memory */
26 std::cout << "\n";
27 int *ptr = (int *) a;
28 for (int i = 0; i < 12; i++) {
29 std::cout << *ptr << " ";
30 ptr++;
31 }
32 std::cout << "\n";
33
34 return EXIT_SUCCESS;
35
36}
14.3. Arrays and Pointers#
Arrays and pointers are inextricably linked in C and C++. Passing an array to a function causes the array to decay into pointer to the relevant type. By decay, the compiler loses information about the type and dimension(s) of the array. Consider the following code - in the function f, the array has decayed/become a pointer to a double.
Note: Any function that takes a built-in array as a parameter, takes a pointer to that array as well as another argument that specifies the array’s length.
1//filename: decay.cpp
2//complile: g++ -std=c++17 -o decay decay.cpp
3//execute: ./decay
4#include <iostream>
5
6void f(double param[], size_t param_length) {
7 std::cout << "size of param: " << sizeof(param) << ". Address: " << param << "\n";
8}
9
10int main(int argc, char *argv[]) {
11 double data[30];
12 std::cout << "data array size: " << sizeof(data) << ". Address: " << data << "\n";
13 f(data,30);
14
15 return EXIT_SUCCESS;
16}
As array elements are accessed, we effectively perform pointer arithmetic.
int array[10];
int *ptr = array;
/* the following pairs of lines are equivalent */
array[0] = 100;
*ptr = 100;
array[5] = 500;
*(ptr + 5) = 500;
array[1] = 50;
ptr++; *ptr = 50; /* increment the pointer, then dereference for the assignment */
The initial declaration of a static array forms a constant that cannot be altered. The constant value is the same as the address of the first element.
int arr[10]; /* arr is "pointer constant", value equals &arr[0] */
arr = arr + 1; /* ERROR: can't re-assign to arr. Compiler message: assignment to expression with array type */
We can declare pointer variables to locations in the array and manipulate/update the pointer variable (i.e., the address that it points to).
The arithmetic that we can perform on pointers is basically adding or subtracting integers from the address values. Effectively, we move our pointer left (subtract) or right (addition) to a memory location. These moves occurs in steps of the size of the thing pointed to by the address. For example, incrementing a char pointer increases the value by one as a char variable is just a single byte. Incrementing an int pointer increases the value by four as ints use four bytes in memory.
Manipulating a pointer value that points to a regular variable, though, could change that pointer to refer to an invalid memory location. With arrays, the compiler allocates a contiguous sequence of memory locations; hence, we can manipulate pointer values as long as we stay within the bounds of that allocated space for the array.
1//filename: ptr_arithm.cpp
2//complile: g++ -std=c++17 -o ptr_arithm ptr_arithm.cpp
3//execute: ./ptr_arithm
4#include <iostream>
5
6int main(int argc, char *argv[]) {
7 int i = 10;
8 char c = 'A';
9
10 int *ip = &i;
11 char *cp = &c;
12
13 /* demonstrates the difference when adding 1 between int and char pointer types */
14 std::cout << "i: initial address " << ip << ", incremented address " << ip+1 << "\n"; /* Note: ip+1 does not point to valid memory location */
15 std::cout << "c: initial address " << cp << ", incremented address " << cp+1 << "\n"; // notice how C++ streams assumes char * is C-style string.
16 // this output line behavior is undefined
17 std::cout << "c: initial address " << (void *) cp << ", incremented address " << (void *) cp+1 << "\n"; // cast to void * to show the address
18
19 int arr[10] = {};
20 int *ptr = arr;
21
22 ptr++; /* this now points to arr[1] */
23 *ptr = 1;
24
25 *(ptr+5) = 6; /* ptr + 5 is not arr[6], we had incremented ptr in line 17 */
26
27 ptr = &arr[9]; /* point ptr to the memory address of the last element */
28 *ptr = 9;
29
30 --ptr; /* ptr now points to arr[8] */
31 *ptr = 8;
32
33 for (int i=0;i<10; i++) {
34 std::cout << arr[i] << " ";
35 }
36 std::cout << "\n";
37
38
39 return EXIT_SUCCESS;
40}
One way to think about pointers and array indexing is that the array index []
is “syntactic sugar” for pointer arithmetic. The formula to compute the index i of an array of type T, where ptr points to the array becomes: addr(ptr + i) = addr(ptr) + (sizeof(T) + i)
14.4. Array Limitations#
Arrays have a number of limitations compared to just a simple type (e.g., an int). These limitations also show the differences from Python’s lists.
We cannot assign one array to another.
We cannot directly compare arrays. We need to loop through the arrays and compare each element one by one.
Arrays do not know their own size. If the array has not decayed into a pointer, we can compute the size with the sizeof operators. However, the correct and safe thing to do is to track the number of elements in the array.
Arrays do not provide automatic bounds checking.
14.5. Invalid Array Example#
While the following code does compile (although with warning messages), we cannot return the address of the array defined in the function f. Once the function exists, any space allocated to that frame on the stack is removed and can no longer be safely accessed. Either a segmentation fault will occur or an invalid value will be printed on line 11. This demonstrates a dangling pointer.
1//filename: bad.cpp
2//complile: g++ -std=c++17 -o bad bad.cpp
3//execute: ./bad
4#include <iostream>
5
6int* f(size_t array_length) {
7 int a[array_length] ; /* allocates space for this on the call stack */
8 return a;
9}
10
11int main(int argc, char *argv[]) {
12 int *ptr = f(5); /* f's return value points to a memory address */
13 /* that has been deallocated / no longer exists */
14
15 std::cout << *ptr << "\n";
16
17 return EXIT_SUCCESS;
18}
14.6. C++11 - Standard Library Functions: begin and end#
We can also use the built-in function, sort
, available in the algorithm
include to sort arrays. Note, that the array name needs to be passed here, not a pointer to the first element.
1//filename: sort.cpp
2//complile: g++ -std=c++17 -o sort sort.cpp
3//execute: ./sort
4#include <iostream>
5#include <algorithm>
6
7int main(int argc, char *argv[]) {
8 int numbers[] = { 4820, 3130, 3124, 1992, 1842 };
9
10 std::sort(std::begin(numbers),std::end(numbers)); // try placing "&numbers[0]" as the argument to begin()
11
12 for (int num: numbers) {
13 std::cout << num << "\n";
14 }
15
16 return EXIT_SUCCESS;
17}
14.7. C++20 - Convert a Built-in Array to std::array#
Under C++20, we can easily convert a built-in array to std::array. Note: To compile this example, use g++ -std=c++20 -o convert convert.cpp
1//filename: convert.cpp
2//complile: g++ -std=c++17 -o convert convert.cpp
3//execute: ./convert
4#include <iostream>
5#include <array>
6
7int main(int argc, char *argv[]) {
8 int numbers[] = { 4820, 3130, 3124, 1992, 1842 };
9
10 // create std::array from a built-in array
11 auto array{std::to_array(numbers)};
12
13 std::cout << "array.size() = " << array.size() << "\n";
14 for (int num: numbers) {
15 std::cout << num << " ";
16 }
17 std::cout << "\n";
18
19}
14.8. C++20 - Use Span to Pass Built-In Arrays#
The C++20 standard added a new type - std::span
- that is a “non-owning” view over a contiguous sequence (or part of that sequence) and provides a way to safely access that data without owning or duplicating that data. By non-owning, std::span
does not manage the lifetime of the data that it point to - if the data is destroyed while a std::span
still references it, accessing the std::span
would lead to undefined behavior. std::span
by be using with any contiguous memory sequence such as std::vector
, std::array
, and built-in arrays. std::span
does provide bounds-checked access to the underlying data. Essentially, std::span
is a lightweight container tracking a pointer to the data and the corresponding size of that data. https://en.cppreference.com/w/cpp/container/span
Note: To compile this example, use g++ -std=c++20 -o span span.cpp
1//filename: span.cpp
2//complile: g++ -std=c++17 -o span span.cpp
3//execute: ./span
4#include <array>
5#include <iostream>
6#include <numeric>
7#include <span>
8#include <vector>
9
10// passing built-in array, need size to due to "decay"
11void displayArray(const int items[], size_t size) {
12 for (size_t i = 0; i < size; i++) {
13 std::cout << items[i] << " ";
14 }
15 std::cout << "\n";
16}
17
18void displaySpan(std::span<const int> items) {
19 for (const auto& item : items) { // spans are iterable
20 std::cout << item << " ";
21 }
22 std::cout << "\n";
23}
24
25// modify elements in the original data structure
26void square(std::span<int> items) {
27 for (int& item : items) {
28 item *= item;
29 }
30}
31
32int main() {
33 int value_array[] = {1, 2, 3, 4, 5};
34 std::array value_stdarray{11, 12, 13, 14, 15};
35 std::vector value_vector{101, 102, 103, 104, 105};
36
37 std::cout << "value_array via displayArray: ";
38 displayArray(value_array, 4);
39
40 std::cout << "values_array via displaySpan: ";
41 displaySpan(value_array); // notice how the complile automatically creates the span
42
43 std::cout << "value_stdarray via displaySpan: ";
44 displaySpan(value_stdarray);
45 std::cout << "value_vector via displaySpan: ";
46 displaySpan(value_vector);
47
48 // Show that passing a span still refers to the underlying data (a view into it)
49 square(value_array);
50 std::cout << "value_array after squaring each element: ";
51 displaySpan(value_array);
52
53 // create a span object directly, use some of the methods in span: https://en.cppreference.com/w/cpp/container/span
54 std::span mySpan{value_array}; // span<int>
55 std::cout << "mySpan's first element: " << mySpan.front() << "\nmySpan's last element: " << mySpan.back() << "\n";
56
57 // access a span element via []
58 std::cout << "The element at index 2 is: " << mySpan[2] << "\n";
59
60 // spans can be used with standard library algorithms
61 std::cout << "Sum mySpan: " << std::accumulate(std::begin(mySpan), std::end(mySpan), 0) << "\n";
62
63 // Create subviews of a container
64 std::cout << "First three elements of mySpan: ";
65 displaySpan(mySpan.first(3));
66 std::cout << "Last three elements of mySpan: ";
67 displaySpan(mySpan.last(3));
68 std::cout << "Middle three elements of mySpan: ";
69 displaySpan(mySpan.subspan(1, 3));
70
71 square(mySpan.subspan(1, 3)); // change subview's contents.
72 std::cout << "value_array after modify subspan: ";
73 displaySpan(value_array);
74}
75// Note: Adapted from C++20: How to Program by Deitel and Deitel.
14.9. C-Style Strings#
A C-style string is an array of charaters terminated by a null character '\0'
, which signifies the end of the string.
char message[] = "Hello!";
In the above example, message
is an array of 7 characters - the six visible characters plus the terminating null character. Note that we could have manually initialized each element of the array:
char message[7];
message[0] = 'H';
message[1] = 'e';
message[2] = 'l';
message[3] = 'l';
message[4] = 'o';
message[5] = '!';
message[6] = '\0';
14.10. <cstring>#
The header <cstring>
provides several functions to examine and manipulate C-style strings.
14.10.1. strlen()#
Returns the length of a string, not including the null terminator.
#include <cstring>
#include <iostream>
int main() {
char str[] = "Hello";
std::cout << strlen(str); // Outputs: 5
}
14.10.2. strpy()#
Copies a string
char source[] = "World";
char destination[6];
strcpy(destination, source);
14.10.3. strcat()#
Concatenates (appends) one string to another. When using strcat(), the destination string must have enough space to accommodate the concatenated result.
char hello[12] = "Hello "; // 6 characters + 6 more for "World" and the null terminator
char world[] = "World";
strcat(hello, world); // hello now contains "Hello World"
__Note: __strcpy()
and strcat()
as presented are problematic to use - especially as the destination must have sufficient room to store the resulting string. Using strncpy()
and strncat()
are alternatives, but not risk-free (e.g., strncpy()
can leave a string without a null terminator.)
14.10.4. Others#
strcmp()
: Compare two strings.strchr()
: Find first occurrence of a character.strstr()
: Find first occurrence of a substring.
14.11. Converting to and from std::string#
With C++, you can easily convert between both styles of strings.
char cstr[] = "Hello";
std::string cppstr = cstr; // Convert to std::string
const char* backToC = cppstr.c_str(); // Convert back to C-style string
const char* backToC = cppstr.data(); // Convert back to C-style string. Requires C++11 or greater. Prior versions did not include null terminator
14.12. Iteration with Pointers#
One of interesting things that you can do with C-style strings is to iterate over them with pointers. As you perform pointer arithmetic, the pointer can “start at” different points in the string.
In the following code block, we create a C-style string greeting
. A char pointer, p
, is then initialized to point to the start of the string. The while
loop then iterates through the string until it reaches the null terminator. Within the loop, we print out the current character, the string as it points, and then the memory address that p contains.
1//filename: iterate.cpp
2//complile: g++ -std=c++17 -o iterate iterate.cpp
3//execute: ./iterate
4#include <iostream>
5#include <iomanip>
6
7int main() {
8 char greeting[] = "Hello, World!";
9 char* p = greeting; // Pointer initialized to the start of the string
10
11 while (*p != '\0') {
12 std::cout << *p << ": " << std::setw(15) << std::left << p << " address: " << (void *) p << "\n";
13 p++;
14 }
15 std::cout << "\n";
16
17 return 0;
18}
14.12.1. Advantages of Using Pointers with C-Style Strings#
Efficiency: Direct memory access through pointers can be more efficient than array indexing because no additional arithmetic (like base address addition) is involved.
Flexibility: Pointers provide a flexible way to work with substrings. You can easily move the pointer to a desired position within the string and start operations from there. This is particularly useful in string parsing tasks.
Interoperability: Many C and C++ libraries/APIs use pointer-based access to C-style strings, so being comfortable with this approach allows for smoother integration with such libraries.
Dynamic Memory: If the C-style string is allocated dynamically (using malloc
in C or new
in C++), pointers are essential for accessing the string.Advanced Operations: Pointers can be used to implement more advanced operations, like reversing a string in-place, or performing other in-memory manipulations directly.
14.13. Risks with C-Style Strings#
C-Style strings have been extremely problematic - especially as it’s easy to have buffer overflow situations where a program writes past the allocated array and into adjacent memory or missing null terminators, which leds to undefined behavior. Wikipedia: Buffer Overflow. Whenever possible you should prefer using std::string in C++ - both safer and more versatile. If you do have to use C-style strings, be wary of buffer sizes and always ensure null termination.