cpp-array

¡Supera tus tareas y exámenes ahora con Quizwiz!

C- style string

1). Modern C++ supports two different types of strings: std::string (as part of the standard library), and C-style strings (naturally, as inherited from the C language). It turns out that std::string is implemented using C-style strings. 2). To define a C-style string, simply declare a char array and initialize it with a string literal: char myString[] = "string"; 3). C++ automatically adds a null terminator to the end of the string for us (we don't need to include it ourselves). Consequently, myString is actually an array of length 7! 4). One important point to note is that C-style strings follow all the same rules as arrays. char myString[] = "string"; // ok myString = "rope"; // not ok! 5). The recommended way of reading strings using cin is as follows: #include <iostream> int main() { char name[255]; // declare array large enough to hold 255 characters std::cout << "Enter your name: "; std::cin.getline(name, 255); std::cout << "You entered: " << name << '\n'; return 0; } This call to cin.getline() will read up to 254 characters into name (leaving room for the null terminator!). Any excess characters will be discarded. In this way, we guarantee that we will not overflow the array! 6). Manipulating C-style strings: C++ provides many functions to manipulate C-style strings as part of the <cstring> library. a). strcpy() allows you to copy a string to another string. More commonly, this is used to assign a value to a string: #include <cstring> int main() { char source[] = "Copy this!"; char dest[50]; strcpy(dest, source); std::cout << dest; // prints "Copy this!" return 0; } However, strcpy() can easily cause array overflows if you're not careful! for example, if char dest[5]; // note that the length of dest is only 5 chars! strcpy(dest, source); // overflow! In C++11, strcpy_s() is preferred, which adds a new parameter to define the size of the destination. However, not all compilers support this function, and to use it, you have to define __STDC_WANT_LIB_EXT1__ with integer value 1. #define __STDC_WANT_LIB_EXT1__ 1 #include <cstring> // for strcpy_s int main() { char source[] = "Copy this!"; char dest[5]; // note that the length of dest is only 5 chars! strcpy_s(dest, 5, source); // An runtime error will occur in debug mode std::cout << dest; return 0; } b). Another useful function is the strlen() function, which returns the length of the C-style string (without the null terminator). Note the difference between strlen() and std::size(). strlen() prints the number of characters before the null terminator, whereas std::size (or the sizeof() trick) returns the size of the entire array, regardless of what's in it. c).strcat() -- Appends one string to another (dangerous) strncat() -- Appends one string to another (with buffer length check) strcmp() -- Compare two strings (returns 0 if equal) strncmp() -- Compare two strings up to a specific number of characters 6). Don't use C-style strings It is important to know about C-style strings because they are used in a lot of code. However, now that we've explained how they work, we're going to recommend that you avoid them altogether whenever possible! Unless you have a specific, compelling reason to use C-style strings, use std::string (defined in the <string> header) instead. std::string is easier, safer, and more flexible. In the rare case that you do need to work with fixed buffer sizes and C-style strings (e.g. for memory-limited devices), we'd recommend using a well-tested 3rd party string library designed for the purpose instead.

Pointer

1). a variable is a name for a piece of memory that holds a value. For example: int x; When this statement is executed by the CPU, a piece of memory from RAM will be set aside 2). The address-of operator (&) allows us to see what memory address is assigned to a variable. 3). The dereference operator (*) allows us to access the value at a particular address: #include <iostream> int main() { int x = 5; std::cout << x << '\n'; // print the value of variable x std::cout << &x << '\n'; // print the memory address of variable x std::cout << *&x << '\n'; /// print the value at the memory address of variable x return 0; } On the author's machine, the above program printed: 5 0027FEA0 5 4). A pointer is a variable that holds a memory address as its value. Pointer variables are declared just like normal variables, only with an asterisk between the data type and the variable name. int *iPtr; // a pointer to an integer value double *dPtr; // a pointer to a double value int* iPtr2; // also valid syntax (acceptable, but not favored) int * iPtr3; // also valid syntax (but don't do this) int *iPtr4, *iPtr5; // declare two pointers to integer variables Best practice: When declaring a pointer variable, put the asterisk next to the variable name. When declaring a function, put the asterisk of a pointer return value next to the type. 5). Assigning a value to a pointer: To get the address of a variable, we use the address-of operator: int value = 5; int *ptr = &value; // initialize ptr with address of variable value The type of the pointer has to match the type of the variable being pointed to: int iValue = 5; double dValue = 7.0; int *iPtr = &iValue; // ok double *dPtr = &dValue; // ok iPtr = &dValue; // wrong -- int pointer cannot point to the address of a double variable dPtr = &iValue; // wrong -- double pointer cannot point to the address of an int variable Note that the following is also not legal: int *ptr = 5; This is because pointers can only hold addresses, and the integer literal 5 does not have a memory address. 6). The address-of operator returns a pointer: std::cout << typeid(&x).name(); this printed: int * or "pi" with gcc 7). Dereferencing pointers: Once we have a pointer variable pointing at something, the other common thing to do with it is dereference the pointer to get the value of what it's pointing at. int *ptr = &value; // ptr points to value std::cout << ptr; // prints address held in ptr, which is &value std::cout << *ptr; // dereference ptr (get the value that ptr is pointing to) Once assigned, a pointer value can be reassigned to another value: ptr = &value1; // ptr points to value1 std::cout << *ptr; // prints 5 ptr = &value2; // ptr now points to value2 std::cout << *ptr; // prints 7 8). When the address of variable value is assigned to ptr, the following are true: ptr is the same as &value *ptr is treated the same as value 9). A warning about dereferencing invalid pointers Pointers in C++ are inherently unsafe, and improper pointer usage is one of the best ways to crash your application. 10). What good are pointers? At this point, pointers may seem a little silly, academic, or obtuse. Why use a pointer if we can just use the original variable? It turns out that pointers are useful in many different cases: 1) Arrays are implemented using pointers. Pointers can be used to iterate through an array (as an alternative to array indices) (covered in lesson 6.8). 2) They are the only way you can dynamically allocate memory in C++ (covered in lesson 6.9). This is by far the most common use case for pointers. 3) They can be used to pass a large amount of data to a function in a way that doesn't involve copying the data, which is inefficient (covered in lesson 7.4) 4) They can be used to pass a function as a parameter to another function (covered in lesson 7.8). 5) They can be used to achieve polymorphism when dealing with inheritance (covered in lesson 12.1). 6) They can be used to have one struct/class point at another struct/class, to form a chain. This is useful in some more advanced data structures, such as linked lists and trees.

std::sort

Because sorting arrays is so common, the C++ standard library includes a sorting function named std::sort. std::sort lives in the <algorithm> header, and can be invoked on an array like so: #include <algorithm> // for std::sort #include <iostream> int main() { const int length = 5; int array[length] = { 30, 50, 20, 10, 40 }; std::sort(array, array+length); for (int i=0; i < length; ++i) std::cout << array[i] << ' '; return 0; }

Bubble sort in C++

#include <algorithm> // for std::swap, use <utility> instead if C++11 #include <iostream> int main() { int array[] = { 6, 3, 2, 9, 7, 1, 5, 4, 8 }; const int length = sizeof(array) / sizeof(array[0]); // Step through each element of the array except the last for (int iteration = 0; iteration < length-1; ++iteration) { // Search through all elements up to the end of the array - 1 // The last element has no pair to compare against int update_length = length -iteration; for (int currentIndex = 0; currentIndex < update_length - 1; ++currentIndex) { // If the current element is larger than the element after it, swap them if (array[currentIndex] > array[currentIndex+1]) std::swap(array[currentIndex], array[currentIndex + 1]); } } // Now print our sorted array as proof it works for (int index = 0; index < length; ++index) std::cout << array[index] << ' '; return 0; }

std::array

1). Fixed arrays decay into pointers, losing the array length information when they do, and dynamic arrays have messy deallocation issues and are challenging to resize without error. 2). std::array provides fixed array functionality that won't decay when passed into a function. std::array is defined in the array header, inside the std namespace. Declaring a std::array variable is easy: #include <array> std::array<int, 3> myArray; // declare an integer array with length 3 3). std::array can be initialized using an initializer lists or uniform initialization: std::array<int, 5> myArray = { 9, 7, 5, 3, 1 }; // initialization list std::array<int, 5> myArray2 { 9, 7, 5, 3, 1 }; // uniform initialization 4). Unlike built-in fixed arrays, with std::array you can not omit the array length when providing an initializer: std::array<int, > myArray = { 9, 7, 5, 3, 1 }; // illegal, array length must be provided 5). You can also assign values to the array using an initializer list std::array<int, 5> myArray; myArray = { 0, 1, 2, 3, 4 }; // okay myArray = { 9, 8, 7 }; // okay, elements 3 and 4 are set to zero! myArray = { 0, 1, 2, 3, 4, 5 }; // not allowed, too many elements in initializer list! 6). Accessing std::array values using the subscript operator works just like you would expect: std::cout << myArray[1]; myArray[2] = 6; 7). std::array supports a second form of array element access (the at() function) that does bounds checking: std::array<int, 5> myArray { 9, 7, 5, 3, 1 }; myArray.at(1) = 6; // array element 1 valid, sets array element 1 to value 6 myArray.at(9) = 10; // array element 9 is invalid, will throw error 8). Size and sorting The size() function can be used to retrieve the length of the std::array: std::array<double, 5> myArray { 9.0, 7.2, 5.4, 3.6, 1.8 }; std::cout << "length: " << myArray.size(); This prints: length: 5 9). Because the length is always known, for-each (ranged for) loops work with std::array: std::array<int, 5> myArray { 9, 7, 5, 3, 1 }; for (auto &element : myArray) std::cout << element << ' '; 10). You can sort std::array using std::sort, which lives in the algorithm header: 1 2 #include <iostream> #include <array> #include <algorithm> // for std::sort int main() { std::array<int, 5> myArray { 7, 3, 1, 9, 5 }; std::sort(myArray.begin(), myArray.end()); // sort the array forwards // std::sort(myArray.rbegin(), myArray.rend()); // sort the array backwards for (const auto &element : myArray) std::cout << element << ' '; return 0; } This prints: 1 3 5 7 9 11). Manually indexing std::array via size_type Pop quiz: What's wrong with the following code? #include <iostream> #include <array> int main() { std::array<int, 5> myArray { 7, 3, 1, 9, 5 }; // Iterate through the array and print the value of the elements for (int i{ 0 }; i < myArray.size(); ++i) std::cout << myArray[i] << ' '; return 0; } The answer is that there's a likely signed/unsigned mismatch in this code! In all common implementations of std::array, size_type is a typedef for std::size_t. So it's somewhat common to see developers use size_t instead. While not technically correct, in almost all implementations, this will work: #include <iostream> #include <array> int main() { std::array<int, 5> myArray { 7, 3, 1, 9, 5 }; for (std::size_t i{ 0 }; i < myArray.size(); ++i) std::cout << myArray[i] << ' '; return 0; } A better solution is to avoid manual indexing of std::array in the first place. Instead, use range-based for loops (or iterators) if possible.

Arrays and enum classes

Enum classes don't have an implicit conversion to integer, so if you try the following: enum class StudentNames { KENNY, // 0 KYLE, // 1 STAN, // 2 BUTTERS, // 3 CARTMAN, // 4 WENDY, // 5 MAX_STUDENTS // 6 }; int main() { int testScores[StudentNames::MAX_STUDENTS]; // allocate 6 integers testScores[StudentNames::STAN] = 76; return 0; } You'll get a compiler error. This can be addressed by using a static_cast to convert the enumerator to an integer: int main() { int testScores[static_cast<int>(StudentNames::MAX_STUDENTS)]; // allocate 6 integers testScores[static_cast<int>(StudentNames::STAN)] = 76; return 0; } However, doing this is somewhat of a pain, so it might be better to use a standard enum inside of a namespace: namespace StudentNames { enum StudentNames { KENNY, // 0 KYLE, // 1 STAN, // 2 BUTTERS, // 3 CARTMAN, // 4 WENDY, // 5 MAX_STUDENTS // 6 }; } int main() { int testScores[StudentNames::MAX_STUDENTS]; // allocate 6 integers testScores[StudentNames::STAN] = 76; return 0; }

Sorting an array using selection sort

To swap two elements, we can use the std::swap() function from the C++ standard library, which is defined in the algorithm header. For efficiency reasons, std::swap() was moved to the utility header in C++11. #include <algorithm> // for std::swap, use <utility> instead if C++11 #include <iostream> int main() { int x = 2; int y = 4; std::cout << "Before swap: x = " << x << ", y = " << y << '\n'; std::swap(x, y); // swap the values of x and y std::cout << "After swap: x = " << x << ", y = " << y << '\n'; } Selection sort There are many ways to sort an array. Selection sort is probably the easiest sort to understand, which makes it a good candidate for teaching even though it is one of the slower sorts. Selection sort performs the following steps to sort an array from smallest to largest: 1) Starting at array index 0, search the entire array to find the smallest value 2) Swap the smallest value found in the array with the value at index 0 3) Repeat steps 1 & 2 starting from the next index Selection sort in C++ Here's how this algorithm is implemented in C++: #include <algorithm> // for std::swap, use <utility> instead if C++11 #include <iostream> int main() { const int length = 5; int array[length] = { 30, 50, 20, 10, 40 }; // Step through each element of the array // (except the last one, which will already be sorted by the time we get there) for (int startIndex = 0; startIndex < length - 1; ++startIndex) { // smallestIndex is the index of the smallest element we've encountered this iteration // Start by assuming the smallest element is the first element of this iteration int smallestIndex = startIndex; // Then look for a smaller element in the rest of the array for (int currentIndex = startIndex + 1; currentIndex < length; ++currentIndex) { // If we've found an element that is smaller than our previously found smallest if (array[currentIndex] < array[smallestIndex]) // then keep track of it smallestIndex = currentIndex; } // smallestIndex is now the smallest element in the remaining array // swap our start element with our smallest element (this sorts it into the correct place) std::swap(array[startIndex], array[smallestIndex]); } // Now that the whole array is sorted, print our sorted array as proof it works for (int index = 0; index < length; ++index) std::cout << array[index] << ' '; return 0; }

sizeof and arrays

sizeof and arrays The sizeof operator can be used on arrays, and it will return the total size of the array (array length multiplied by element size). Note that due to the way C++ passes arrays to functions, this will not work properly for arrays that have been passed to functions! void printSize(int array[]) { std::cout << sizeof(array) << '\n'; // prints the size of a pointer, not the size of the array! } int main() { int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 }; std::cout << sizeof(array) << '\n'; // will print the size of the array printSize(array); return 0; } On a machine with 4 byte integers and 4 byte pointers, this printed: 32 4 One neat trick: we can determine the length of a fixed array by dividing the size of the entire array by the size of an array element: int main() { int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 }; std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << "elements\n"; return 0; }

std::vector

1). std::vector provides dynamic array functionality that handles its own memory management. This means you can create arrays that have their length set at runtime, without having to explicitly allocate and deallocate memory using new and delete. std::vector lives in the <vector> header. 2). Declaring a std::vector is simple: #include <vector> // no need to specify length at initialization std::vector<int> array; std::vector<int> array2 = { 9, 7, 5, 3, 1 }; // use initializer list to initialize array std::vector<int> array3 { 9, 7, 5, 3, 1 }; // use uniform initialization to initialize array (C++11 onward) 3). Just like std::array, accessing array elements can be done via the [] operator (which does no bounds checking) or the at() function (which does bounds checking): array[6] = 2; // no bounds checking array.at(7) = 3; // does bounds checking 4). As of C++11, you can also assign values to a std::vector using an initializer-list: array = { 0, 1, 2, 3, 4 }; // okay, array length is now 5 array = { 9, 8, 7 }; // okay, array length is now 3 4). What's wrong with the following code? void doSomething(bool earlyExit) { int *array = new int[5] { 9, 7, 5, 3, 1 }; if (earlyExit) return; // do stuff here delete[] array; // never called } If earlyExit is set to true, array will never be deallocated, and the memory will be leaked. 5). Self-cleanup prevents memory leaks: In the above code, However, if array is a vector, this won't happen, because the memory will be deallocated as soon as array goes out of scope (regardless of whether the function exits early or not). This makes std::vector much safer to use than doing your own memory allocation. 6). Vectors remember their length Unlike built-in dynamic arrays, which don't know the length of the array they are pointing to, std::vector keeps track of its length. We can ask for the vector's length via the size() function: #include <vector> #include <iostream> int main() { std::vector<int> array { 9, 7, 5, 3, 1 }; std::cout << "The length is: " << array.size() << '\n'; return 0; } The above example prints: The length is: 5 7). Resizing an array Resizing a built-in dynamically allocated array is complicated. Resizing a std::vector is as simple as calling the resize() function: #include <vector> #include <iostream> int main() { std::vector<int> array { 0, 1, 2 }; array.resize(5); // set size to 5 std::cout << "The length is: " << array.size() << '\n'; for (auto const &element: array) std::cout << element << ' '; return 0; } This prints: The length is: 5 0 1 2 0 0 8). Compacting bools std::vector has another cool trick up its sleeves. There is a special implementation for std::vector of type bool that will compact 8 booleans into a byte! This happens behind the scenes, and is largely transparent to you as a programmer. #include <vector> #include <iostream> int main() { std::vector<bool> array { true, false, false, true, true }; std::cout << "The length is: " << array.size() << '\n'; for (auto const &element: array) std::cout << element << ' '; return 0; } This prints: The length is: 5 1 0 0 1 1

Passing arrays to functions

Although passing an array to a function at first glance looks just like passing a normal variable, underneath the hood, C++ treats arrays differently. When a normal variable is passed by value, C++ copies the value of the argument into the function parameter. Because the parameter is a copy, changing the value of the parameter does not change the value of the original argument. void passValue(int value) // value is a copy of the argument { value = 99; // so changing it here won't change the value of the argument } void passArray(int prime[5]) // prime is the actual array { prime[0] = 11; // so changing it here will change the original argument! prime[1] = 7; prime[2] = 5; prime[3] = 3; prime[4] = 2; } As a side note, if you want to ensure a function does not modify the array elements passed into it, you can make the array const: // even though prime is the actual array, within this function it should be treated as a constant void passArray(const int prime[5]) { // so each of these lines will cause a compile error! prime[0] = 11; prime[1] = 7; prime[2] = 5; prime[3] = 3; prime[4] = 2; }

What is array? How to do array declarations?

An array is an aggregate data type that lets us access many variables of the same type through a single identifier. Each of the variables in an array is called an element. Arrays can be made from any data type. In C++, array subscripts must always be an integral type. When declaring a fixed array, the length of the array (between the square brackets) must be a compile-time constant: // using a literal constant int array[5]; // Ok // using a macro symbolic constant #define ARRAY_LENGTH 5 int array[ARRAY_LENGTH]; // Syntactically okay, but don't do this // using a symbolic constant const int arrayLength = 5; int array[arrayLength]; // Ok // using an enumerator enum ArrayElements { MAX_ARRAY_LENGTH = 5 }; int array[MAX_ARRAY_LENGTH]; // Ok Note that non-const variables or runtime constants cannot be used: // using a non-const variable int length; std::cin >> length; int array[length]; // Not ok -- length is not a compile-time constant! // using a runtime const variable int temp = 5; const int length = temp; // the value of length isn't known until runtime, so this is a runtime constant, not a compile-time constant! int array[length]; // Not ok

How to initialize fixed array?

Array elements are treated just like normal variables, and as such, they are not initialized when created. One way to initialize an array is to do it element by element. int prime[5]; // hold the first 5 prime numbers prime[0] = 2; prime[1] = 3; prime[2] = 5; prime[3] = 7; prime[4] = 11; Fortunately, C++ provides a more convenient way to initialize entire arrays via use of an initializer list. The following example is equivalent to the one above: int prime[5] = { 2, 3, 5, 7, 11 }; // use initializer list to initialize the fixed array If there are more initializers in the list than the array can hold, the compiler will generate an error. if there are less initializers in the list than the array can hold, the remaining elements are initialized to 0. This is called zero initialization. int array[5] = { 7, 4, 5 }; // only initialize first 3 elements // Initialize all elements to 0 int array[5] = { }; // Initialize all elements to 0.0 double darray[5] = { }; In C++11, the uniform initialization syntax can be used instead: int prime[5] { 2, 3, 5, 7, 11 }; // use uniform initialization to initialize the fixed array // note the lack of the equals sign in this syntax Omitted length: If you are initializing a fixed array of elements using an initializer list, the compiler can figure out the length of the array for you, and you can omit explicitly declaring the length of the array. The following two lines are equivalent: int array[5] = { 0, 1, 2, 3, 4 }; // explicitly define length of the array int array[] = { 0, 1, 2, 3, 4 }; // let initializer list set length of the array

Array and enum

One of the big documentation problems with arrays is that integer indices do not provide any information to the programmer about the meaning of the index. Consider a class of 5 students: const int numberOfStudents(5); int testScores[numberOfStudents]; testScores[2] = 76; Who is represented by testScores[2]? It's not clear. This can be solved by setting up an enumeration where one enumerator maps to each of the possible array indices: enum StudentNames { KENNY, // 0 KYLE, // 1 STAN, // 2 BUTTERS, // 3 CARTMAN, // 4 MAX_STUDENTS // 5 }; int main() { int testScores[MAX_STUDENTS]; // allocate 5 integers testScores[STAN] = 76; return 0; } Note that an extra enumerator named MAX_STUDENTS has been added. This enumerator is used during the array declaration to ensure the array has the proper length (as the array length should be one greater than the largest index). This is useful both for documentation purposes, and because the array will automatically be resized if another enumerator is added. enum StudentNames { KENNY, // 0 KYLE, // 1 STAN, // 2 BUTTERS, // 3 CARTMAN, // 4 WENDY, // 5 MAX_STUDENTS // 6 }; int main() { int testScores[MAX_STUDENTS]; // allocate 6 integers testScores[STAN] = 76; // still works return 0; } Note that this "trick" only works if you do not change the enumerator values manually!

Arrays and off-by-one errors

One of the trickiest parts of using loops with arrays is making sure the loop iterates the proper number of times. Off-by-one errors are easy to make, and trying to access an element that is larger than the length of the array can have dire consequences. Consider the following program: int scores[] = { 84, 92, 76, 81, 56 }; const int numStudents = sizeof(scores) / sizeof(scores[0]); int maxScore = 0; // keep track of our largest score for (int student = 0; student <= numStudents; ++student) if (scores[student] > maxScore) maxScore = scores[student]; std::cout << "The best score was " << maxScore << '\n'; The problem with this program is that the conditional in the for loop is wrong! The array declared has 5 elements, indexed from 0 to 4. However, this array loops from 0 to 5. Consequently, on the last iteration, the array will execute this: if (scores[5] > maxScore) maxScore = scores[5]; But scores[5] is undefined! This can cause all sorts of issues, with the most likely being that scores[5] results in a garbage value. In this case, the probable result is that maxScore will be wrong. However, imagine what would happen if we inadvertently assigned a value to array[5]! We might overwrite another variable (or part of it), or perhaps corrupt something -- these types of bugs can be very hard to track down!

Indexing an array out of range

Remember that an array of length N has array elements 0 through N-1. So what happens if you try to access an array with a subscript outside of that range? Consider the following program: int main() { int prime[5]; // hold the first 5 prime numbers prime[5] = 13; return 0; } C++ does not do any checking to make sure that your indices are valid for the length of your array. So in the above example, the value of 13 will be inserted into memory where the 6th element would have been had it existed. When this happens, you will get undefined behavior -- For example, this could overwrite the value of another variable, or cause your program to crash. Although it happens less often, C++ will also let you use a negative index, with similarly undesirable results. Rule: When using arrays, ensure that your indices are valid for the range of your array!.


Conjuntos de estudio relacionados

Chapter 5: Data and Knowledge Management

View Set

The Natural Rate of Interest and Zero Lower Bound

View Set

SmartBook Chapter 3: Digital Marketing: Online, Social, and Mobile

View Set

Illinois Permit Test study guide

View Set

CPT Certification Test {Section 5}

View Set

Duty to Disclose: Segment 3: Natural Hazards Disclosure

View Set

Peds PrepU Chapter 1:The Nurse's Role in a Changing Maternal-Child Health Care Environment

View Set