The value returned from main is accessed in a system-dependent manner. On both UNIX and Windows systems, after executing the program, you must issue an appropriate echo command.
On UNIX systems, we obtain the status by writing
$ echo $?
To see the status on a Windows system, we write
$ echo %ERRORLEVEL%
Fundamental to the iostream library are two types named istream and ostream, which represent input and output streams, respectively. A stream is a sequence of characters read from or written to an IO device. The term stream is intended to suggest that the characters are generated, or consumed, sequentially over time.
Standard Input and Output Objects
Exercises 1.3
Write a program to print Hello, World on the standard output.
#include
using namespace std;
int main()
{
cout<<"Hello, World"<return 0;
}
1.4.1 The WHILE Statement
Exercises 1.11
Write a program that prompts the user for two integers.
Print each number in the range specified by those two integers.
#include
using namespace std;
int main()
{
int s,l;
cout<<"Please input smaller number:";
cin>>s;
cout<<"Please input larger number:";
cin>>l;
while(s<=l)
{
cout<" ";
}
cout<return 0;
}
1.4.3. Reading an Unknown Number of Inputs
#include
int main()
{
int sum = 0, value = 0;
// read until end-of-file, calculating a running total of all values read
while (std::cin >> value)
sum += value; // equivalent to sum = sum + value
std::cout << "Sum is: " << sum << std::endl;
return 0;
}
When we use an istream as a condition, the effect is to test the state of the stream. If the stream is valid—that is, if the stream hasn’t encountered an error—then the test succeeds. An istream becomes invalid when we hit end-of-file or encounter an invalid input, such as reading a value that is not an integer. An istream that is in an invalid state will cause the condition to yield false.
A compound type is a type that is defined in terms of another type. C++ has several compound types, two of which—references and pointers—we’ll cover in this chapter.
2.3.1. References
int ival = 1024;
int &refVal = ival; // refVal refers to (is another name for) ival
int &refVal2; // error: a reference must be initialized
Ordinarily, when we initialize a variable, the value of the initializer is copied into the object we are creating. When we define a reference, instead of copying the initializer’s value, we bind the reference to its initializer. Once initialized, a reference remains bound to its initial object. There is no way to rebind a reference to refer to a different object. Because there is no way to rebind a reference, references must be initialized.
2.3.2. Pointers
A pointer is a compound type that “points to” another type. Like references, pointers are used for indirect access to other objects. Unlike a reference, a pointer is an object in its own right. Pointers can be assigned and copied; a single pointer can point to several different objects over its lifetime. Unlike a reference, a pointer need not be initialized at the time it is defined. Like other built-in types, pointers defined at blockscope have undefined value if they are not initialized.
Null Pointers
Can be defined as following: define as p1 is direct.(C++ 11)
int *p1 = nullptr; // equivalent to int *p1 = 0;
int *p2 = 0; // directly initializes p2 from the literal constant 0
// must #include cstdlib
int *p3 = NULL; // equivalent to int *p3 = 0;
Assignment and Pointers
int i = 42;
int *pi = 0; // pi is initialized but addresses no object
int *pi2 = &i; // pi2 initialized to hold the address of i
int *pi3; // if pi3 is defined inside a block, pi3 is uninitialized
pi3 = pi2; // pi3 and pi2 address the same object, e.g., i
pi2 = 0; // pi2 now addresses no object
Other Pointer Operations
int ival = 1024;
int *pi = 0; // pi is a valid, null pointer
int *pi2 = &ival; // pi2 is a valid pointer that holds the address of ival
if (pi) // pi has value 0, so condition evaluates as false
// ...
if (pi2) // pi2 points to ival, so it is not 0; the condition evaluates as true
// ...
Any nonzero pointer evaluates as true
2.3.3. Understanding Compound Type Declarations
As we’ve seen, a variable definition consists of a base type and a list of declarators. Each declarator can relate its variable to the base type differently from the other declarators in the same definition. Thus, a single definition might define variables of different types:
// i is an int; p is a pointer to int; r is a reference to int
int i = 1024, *p = &i, &r = i;
Pointers to Pointers
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = π // ppi points to a pointer to an int
References to Pointers
A reference is not an object. Hence, we may not have a pointer to a reference.
However, because a pointer is an object, we can define a reference to a pointer:
int i = 42;
int *p; // p is a pointer to int
int *&r = p; // r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i to 0
Sometimes we want to define a variable whose value we know cannot be changed. For example, we might want to use a variable to refer to the size of a buffer size. Using a variable makes it easy for us to change the size of the buffer if we decided the original size wasn’t what we needed. On the other hand, we’d also like to prevent code from inadvertently giving a new value to the variable we use to represent the buffer size. We can make a variable unchangeable by defining the variable’s type as const:
const int bufSize = 512; // input buffer size
defines bufSize as a constant. Any attempt to assign to bufSize is an error:
bufSize = 512; // error: attempt to write to const object
Because we can’t change the value of a const object after we create it, it must be initialized. As usual, the initializer may be an arbitrarily complicated expression:
const int i = get_size(); // ok: initialized at run time
const int j = 42; // ok: initialized at compile time
const int k; // error: k is uninitialized const
Initialization and const
By Default, const Objects Are Local to a File
const int bufSize = 512; // input buffer size
the compiler will usually replace uses of the variable with its corresponding value during compilation. That is, the compiler will generate code using the value 512 in the places that our code uses bufSize.
To substitute the value for the variable, the compiler has to see the variable’s initializer.When we split a program into multiple files, every file that uses the const must have access to its initializer.In order to see the initializer, the variable must be defined in every file that wants to use the variable’s value (§ 2.2.2, p. 45).
To support this usage, yet avoid multiple definitions of the same variable, const variables are defined as local to the file. When we define a const with the same name in multiple files, it is as if we had written definitions for separate variables in each file.
SOLUTION
// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize; // same bufSize as defined in file_1.cc
2.4.1. References to const
To do so we use a reference to const, which is a reference that refers to a const type. Unlike an ordinary reference, a reference to const cannot be used to change the object to which the reference is bound:
const int ci = 1024;
const int &r1 = ci; // ok: both reference and underlying object are const
r1 = 42; // error: r1 is a reference to const
int &r2 = ci; // error: non const reference to a const object
Terminology: const Reference is a Reference to const
C++ programmers tend to abbreviate the phrase “reference to const” as “const reference.” This abbreviation makes sense—if you remember that it is an abbreviation.
Technically speaking, there are no const references. A reference is not an object, so we cannot make a reference itself const. Indeed, because there is no way to make a reference refer to a different object, in some sense all references are const. Whether a reference refers to a const or nonconst type affects what we can do with that reference, not whether we can alter the binding of the reference itself.
Initialization and References to const
In § 2.3.1 (p. 51) we noted that there are two exceptions to the rule that the type of a reference must match the type of the object to which it refers. The first exception is that we can initialize a reference to const from any expression that can be converted (§ 2.1.2, p. 35) to the type of the reference. In particular, we can bind a reference to const to a nonconst object, a literal, or a more general expression:
int i = 42;
const int &r1 = i; // we can bind a const int& to a plain int object
const int &r2 = 42; // ok: r1 is a reference to const
const int &r3 = r1 * 2; // ok: r3 is a reference to const
int &r4 = r * 2; // error: r4 is a plain, non const reference
The easiest way to understand this difference in initialization rules is to consider what
happens when we bind a reference to an object of a different type:
double dval = 3.14;
const int &ri = dval;
Here ri refers to an int. Operations on ri will be integer operations, but dval is a
floating-point number, not an integer. To ensure that the object to which ri is bound
is an int, the compiler transforms this code into something like
const int temp = dval; // create a temporary const int from the double
const int &ri = temp; // bind ri to that temporary
In this case, ri is bound to a temporary object. A temporary object is an unnamed object created by the compiler when it needs a place to store a result from evaluating an expression. C++ programmers often use the word temporary as an abbreviation for temporary object.
Now consider what could happen if this initialization were allowed but ri was not const. If ri weren’t const, we could assign to ri. Doing so would change the object to which ri is bound. That object is a temporary, not dval. The programmer who made ri refer to dval would probably expect that assigning to ri would change dval. After all, why assign to ri unless the intent is to change the object to which ri is bound? Because binding a reference to a temporary is almost surely not what the programmer intended, the language makes it illegal.
A Reference to const May Refer to an Object That Is Not const
It is important to realize that a reference to const restricts only what we can do through that reference. Binding a reference to const to an object says nothing about whether the underlying object itself is const. Because the underlying object might be nonconst, it might be changed by other means:
int i = 42;
int &r1 = i; // r1 bound to i
const int &r2 = i; // r2 also bound to i; but cannot be used to change i
r1 = 0; // r1 is not const; i is now 0
r2 = 0; // error: r2 is a reference to const
Binding r2 to the (nonconst) int i is legal. However, we cannot use r2 to change i. Even so, the value in i still might change. We can change i by assigning to it directly, or by assigning to another reference bound to i, such as r1.
2.5.1. Type Aliases
A type alias is a name that is a synonym for another type.
typedef double wages; // wages is a synonym for double
typedef wages base, *p; // base is a synonym for double, p for double*
C++11: The new standard introduced a second way to define a type alias, via an alias declaration:
using SI = Sales_item; // SI is a synonym for Sales_item
An alias declaration starts with the keyword using followed by the alias name and an =. The alias declaration defines the name on the left-hand side of the = as an alias for the type that appears on the right-hand side.
Declarations that use type aliases that represent compound types and const can yield surprising results. For example, the following declarations use the type pstring, which is an alias for the the type char*:
typedef char *pstring;
const pstring cstr = 0; // cstr is a constant pointer to char
const pstring *ps; // ps is a pointer to a constant pointer to char
The base type in these declarations is const pstring. As usual, a const that appears in the base type modifies the given type. The type of pstring is “pointer to char.” So, const pstring is a constant pointer to char—not a pointer to const char.
2.5.2. The auto Type Specifier
Unlike type specifiers, such as double, that name a specific type, auto tells the compiler to deduce the type from the initializer.
As with any other type specifier, we can define multiple variables using auto. Because a declaration can involve only a single base type, the initializers for all the variables in the declaration must have types that are consistent with each other:
auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi
2.5.3. The decltype Type Specifier
2.6.1. Defining the Sales_data Type
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
The class body is surrounded by curly braces and forms a new scope (§ 2.2.4, p. 48).
The close curly that ends the class body must be followed by a semicolon.
C++11: Under the new standard, we can supply an in-class initializer for a data member. When we create objects, the in-class initializers will be used to initialize the data members. Members without an initializer are default initialized (§ 2.2.1, p. 43).
In-class initializers are restricted as to the form (§ 2.2.1, p. 43) we can use: They must either be enclosed inside curly braces or follow an = sign. We may not specify an in-class initializer inside parentheses.
2.6.2. Using the Sales_data Class
2.6.3. Writing Our Own Header Files
Whenever a header is updated, the source files that use that header must be recompiled to get the new or changed declarations.
A Brief Introduction to the Preprocessor
The most common technique for making it safe to include a header multiple times relies on the preprocessor. The preprocessor—which C++ inherits from C—is a program that runs before the compiler and changes the source text of our programs.
Our programs already rely on one preprocessor facility, #include. When the preprocessor sees a #include, it replaces the #include with the contents of the specified header.
C++ programs also use the preprocessor to define header guards. Header guards rely on preprocessor variables (§ 2.3.2, p. 53). Preprocessor variables have one of two possible states: defined or not defined. The #define directive takes a name and defines that name as a preprocessor variable. There are two other directives that test whether a given preprocessor variable has or has not been defined: #ifdef is true if the variable has been defined, and #ifndef is true if the variable has not been defined. If the test is true, then everything following the #ifdef or #ifndef is processed up to the matching #endif.
e.g: sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
Each using declaration introduces a single namespace member. This behavior lets us be specific about which names we’re using.
#include
// using declarations for names from the standard library
using std::cin;
using std::cout; using std::endl;
int main()
{
cout << "Enter two numbers:" << endl;
int v1, v2;
cin >> v1 >> v2;
cout << "The sum of " << v1 << " and " << v2<< " is "
<< v1 + v2 << endl;
return 0;
}
Headers Should Not Include using Declarations
3.2.1. Defining and Initializing strings
Table 3.1 lists the most common ways to initialize strings. Some
examples:
Direct and Copy Forms of Initialization
When we initialize a variable using =, we are asking the compiler to copy initialize the object by copying the initializer on the right-hand side into the object being created. Otherwise, when we omit the =, we use direct initialization.
string s5 = "hiya"; // copy initialization
string s6("hiya"); // direct initialization
string s7(10, 'c'); // direct initialization; s7 is cccccccccc
string s8 = string(10, 'c'); // copy initialization; s8 is cccccccccc
// s8 equals to following 2 lines:
string temp(10, 'c'); // temp is cccccccccc
string s8 = temp; // copy temp into s8
3.2.2. Operations on strings
Table 3.2 (overleaf) lists the most common string operations.
Reading and Writing strings
The string input operator reads and discards any leading whitespace (e.g., spaces, newlines, tabs). It then reads characters until the next whitespace character is encountered.
// Note: #include and using declarations must be added to compile this code
int main()
{
string s; // empty string
cin >> s; // read a whitespace-separated string into s
cout << s << endl; // write s to the output
return 0;
}
The string input operator reads and discards any leading whitespace (e.g., spaces, newlines, tabs).It then reads characters until the next whitespace character is encountered.
So, if the input to this program is Hello World! (note leading and trailing spaces), then the output will be Hello with no extra spaces.
Reading an Unknown Number of strings
int main()
{
string word;
while (cin >> word) // read until end-of-file
cout << word << endl; // write each word followed by a new line
return 0;
}
In this program, we read into a string, not an int. Otherwise, the while condition executes similarly to the one in our previous program. The condition tests the stream after the read completes. If the stream is valid—it hasn’t hit end-of-file or encountered an invalid input—then the body of the while is executed. The body prints the value we read on the standard output. Once we hit end-of-file (or invalid input), we fall out of the while.
Using getline to Read an Entire Line
Sometimes we do not want to ignore the whitespace in our input. In such cases, we can use the getline function instead of the >> operator. The getline function takes an input stream and a string.This function reads the given stream up to and including the first newline and stores what it read—not including the newline—in its string argument.
After getline sees a newline, even if it is the first character in the input, it stops reading and returns. If the first character in the input is a newline, then the resulting string is the empty string.
Like the input operator, getline returns its istream argument. As a result, we can use getline as a condition just as we can use the input operator as a condition (§ 1.4.3, p. 14). For example, we can rewrite the previous program that wrote one word per line to write a line at a time instead:
int main()
{
string line;
// read input a line at a time until end-of-file
while (getline(cin, line))
cout << line << endl;
return 0;
}
Because line does not contain a newline, we must write our own. As usual, we use endl to end the current line and flush the buffer.
The string empty and size Operations
// read input a line at a time and discard blank lines
while (getline(cin, line))
if (!line.empty())
cout << line << endl;
another example of getline, also use str.size()
string line;
// read input a line at a time and print lines that are longer than 80 characters
while (getline(cin, line))
if (line.size() > 80)
cout << line << endl;
The string::size_type Type
It might be logical to expect that size returns an int or, thinking back to § 2.1.1 (p.34), an unsigned. Instead, size returns a string::size_type value. This type requires a bit of explanation.
The string class—and most other library types—defines several companion types. These companion types make it possible to use the library types in a machine independent manner. The type size_type is one of these companion types. To use the size_type defined by string, we use the scope operator to say that the name size_type is defined in the string class.
Although we don’t know the precise type of string::size_type, we do know that it is an unsigned type (§ 2.1.1, p. 32) big enough to hold the size of any string. Any variable used to store the result from the string size operation should be of type string::size_type.
Comparing strings
The equality operators (== and !=) test whether two strings are equal or unequal, respectively.
Adding Two strings
s1 = "hello, ", s2 = "world\n";
string
// s3 is hello, world\n
string s3 = s1 + s2;
s1 += s2; // equivalent to s1 = s1 + s2
When we mix strings and string or character literals, at least one operand to each + operator must be of string type:
string s4 = s1 + ", "; // ok: adding a string and a literal
string s5 = "hello" + ", "; // error: no string operand (when compile)
string s6 = s1 + ", " + "world"; // ok: each + has a string operand
string s7 = "hello" + ", " + s2; // error: can't add string literals
The subexpression s1 + “, ” returns a string, which forms the left-hand operand of the second + operator. It is as if we had written
string tmp = s1 + ", "; // ok: + has a string operand
s6 = tmp + "world"; // ok: + has a string operand
3.2.3. Dealing with the Characters in a string
key of dealing with the characters:
#include
Advice: Use the C++ Versions of C Library Headers
In particular, the names defined in the cname headers are defined inside the std namespace, whereas those defined in the .h versions are not.
C++11:Processing Every Character? Use Range-Based for
If we want to do something to every character in a string, by far the best approach is to use a statement introduced by the new standard: the range for statement.
for (declaration : expression)
statement
e.g:
string str("some string");
// print the characters in str one character to a line
for (auto c : str) // for every char in str
cout << c << endl; // print the current character followed by a newline
another exmple:
string s("Hello World!!!");
// punct_cnt has the same type that s.size returns; see § 2.5.3 (p. 70)
//s.size() return size_type (not int)
decltype(s.size()) punct_cnt = 0;
// count the number of punctuation characters in s
for (auto c : s) // for every char in s
if (ispunct(c)) // if the character is punctuation
++punct_cnt; // increment the punctuation counter
cout << punct_cnt
<< " punctuation characters in " << s << endl;
Using a Range for to Change the Characters in a string
An example:
string s("Hello World!!!");
// convert s to uppercase
for (auto &c : s) // for every char in s (note: c is a reference)
c = toupper(c); // c is a reference, so the assignment changes the char
in s
cout << s << endl;
Processing Only Some Characters?
Using a Subscript for Iteration
Using a Subscript for Random Access
A vector is a class template.
Templates are not themselves functions or classes. Instead, they can be thought of as instructions to the compiler for generating classes or functions. The process that the compiler uses to create classes or functions from templates is called instantiation. When we use a template, we specify what kind of class or function we want the compiler to instantiate.
e.g:
vector<vector<string>> file; // vector whose elements are vectors
C++11: In the past, we had to supply a space between the closing angle bracket of the outer vector and its element type— vector
rather than vector
.
vector<vector<int> > //old style
vector<vector<int>> //c++ 11
Sometimes some compiler demands old-style declarations.
3.3.1. Defining and Initializing vectors
Table 3.4. Ways to Initialize a vector
List Initializing a vector
C++11:
e.g:
vector<string> articles = {"a", "an", "the"};
vector<string> v1{"a", "an", "the"}; // list initialization
vector<string> v2("a", "an", "the"); // error
Creating a Specified Number of Elements
vector<int> ivec(10, -1); // ten int elements, each initialized to -1
vector<string> svec(10, "hi!"); // ten strings; each element is "hi!"
Value Initialization
vector<int> ivec(10); // ten elements, each initialized to 0
vector<string> svec(10); // ten elements, each an empty string
List Initializer or Element Count?
vector<int> v1(10); // v1 has ten elements with value 0
vector<int> v2{10}; // v2 has one element with value 10
vector<int> v3(10, 1); // v3 has ten elements with value 1
vector<int> v4{10, 1}; // v4 has two elements with values 10 and 1
vector<string> v5{"hi"}; // list initialization: v5 has one element
vector<string> v6("hi"); // error: can't construct a vector from a string literal
vector<string> v7{10}; // v7 has ten default-initialized elements
vector<string> v8{10, "hi"}; // v8 has ten elements with value "hi"
3.3.2. Adding Elements to a vector
vector<int> v2; // empty vector
for (int i = 0; i != 100; ++i)
v2.push_back(i); // append sequential integers to v2
// at end of loop v2 has 100 elements, values 0 . . . 99
We use the same approach when we want to create a vector where we don’t know until run time how many elements the vector should have.
e.g:
// read words from the standard input and store them as elements in a vector
string word;
vector<string> text; // empty vector
while (cin >> word) {
text.push_back(word); // append word to text
}
Programming Implications of Adding Elements to a vector
We must ensure that any loops we write are correct even if the loop changes the size of the vector.
3.3.3. Other vector Operations
Table 3.5. vector Operations
vector<int> v{1,2,3,4,5,6,7,8,9};
for (auto &i : v) // for each element in v (note: i is a reference)
i *= i; // square the element value
for (auto i : v) // for each element in v
cout << i << " "; // print the element
cout << endl;
In the first loop, we define our control variable, i, as a reference so that we can use i to assign new values to the elements in v.
The size member returns a value of the size_type defined by the corresponding vector type.
Note: To use size_type, we must name the type in which it is defined. A vector type always includes its element type (§ 3.3, p. 97):
vector<int>::size_type // ok
vector::size_type // error
Computing a vector Index
Subscripting Does Not Add Elements
Must use push_back();
Attempting to subscript elements that do not exist is, unfortunately, an extremely common and pernicious programming error. So-called buffer overflow errors are the result of subscripting elements that don’t exist. Such bugs are the most common cause of security problems in PC and other applications.
A good way to ensure that subscripts are in range is to avoid subscripting altogether by using a range for whenever possible.
3.4.1. Using Iterators
// the compiler determines the type of b and e; see § 2.5.2 (p. 68)
// b denotes the first element and e denotes one past the last element in v
auto b = v.begin(), e = v.end(); // b and e have the same type
vector<int>:: iterator iter = ivec.begin();
The iterator returned by end is often referred to as the off-the-end iterator or abbreviated as “the end iterator.” If the container is empty, begin returns the same iterator as the one returned by end.
Iterator Operations
Table 3.6. Standard Container Iterator Operations
Moving Iterators from One Element to Another
an example:
// process characters in s until we run out of characters or we hit a whitespace
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it); // capitalize the current character
Key Concept: Generic Programming
C++ programmers use != as a matter of habit. They do so for the same reason that they use iterators rather than subscripts: This coding style applies equally well to various kinds of containers provided by the library.
As we’ve seen, only a few library types, vector and string being among them, have the subscript operator. Similarly, all of the library containers have iterators that define the == and != operators. Most of those iterators do not have the < operator. By routinely using iterators and !=, we don’t have to worry about the precise type of container we’re processing.
Iterator Types
vector<int>::iterator it; // it can read and write vector elements
string::iterator it2; // it2 can read and write characters in a string
vector<int>::const_iterator it3; // it3 can read but not write elements
string::const_iterator it4; // it4 can read but not write characters
If a vector or string is const, we may use only its const_iterator type. With a nonconst vector or string, we can use either iterator or const_iterator.
The begin and end Operations
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1 has type vector::iterator
auto it2 = cv.begin(); // it2 has type vector::const_iterator
Often this default behavior is not what we want. For reasons we’ll explain in § 6.2.3(p. 213), it is usually best to use a const type (such as const_iterator) when we need to read but do not need to write to an object.
To let us ask specifically for the const_iterator type, the new standard introduced two new functions named cbegin and cend:
C++11:
auto it3 = v.cbegin(); // it3 has type vector::const_iterator
Combining Dereference and Member Access
Some vector Operations Invalidate Iterators
3.4.2. Iterator Arithmetic
Table 3.7. Operations Supported by vector and string Iterators
Arithmetic Operations on Iterators
Using Iterator Arithmetic
3.5.1. Defining and Initializing Built-in Arrays
Explicitly Initializing Array Elements
Character Arrays Are Special
No Copy or Assignment
Understanding Complicated Array Declarations
3.5.2. Accessing the Elements of an Array
Checking Subscript Values
3.5.3. Pointers and Arrays
Pointers Are Iterators
The Library begin and end Functions
Pointer Arithmetic
Interaction between Dereference and Pointer Arithmetic
Subscripts and Pointers
3.5.4. C-Style Character Strings
C Library String Functions
Comparing Strings
Caller Is Responsible for Size of a Destination String
3.5.5. Interfacing to Older Code
Mixing Library strings and C-Style Strings
Using an Array to Initialize a vector
3.6. Multidimensional Arrays
Initializing the Elements of a Multidimensional Array
Subscripting a Multidimensional Array
Using a Range for with Multidimensional Arrays
Pointers and Multidimensional Arrays
Type Aliases Simplify Pointers to Multidimensional Arrays