"C++ Programming Principles and Practice" Notes Chapter 18 Vectors and Arrays

This chapter describes how to copy and access vectors via subscripts. To this end, we discuss copying techniques in general and consider the relationship between vector and underlying array representations. We will show the relationship of arrays to pointers and the problems raised by their use. We'll also discuss the five basic operations that must be considered for each type: construction, default construction, copy construction, copy assignment, and destruction. Additionally, containers require a move constructor and move assignment.

18.1 Introduction

In this chapter, we'll learn about programming language features and techniques that free us from the constraints and difficulties of low-level computer memory. We want to be able to program with types that provide the properties we happen to want based on logical requirements.

vectorA type controls all access to its elements and provides operations that seem "natural" from the user's point of view rather than the hardware's.

This chapter focuses on the concept of copying . This is an important but rather technical question: what does it mean to copy a nontrivial (that is, a inttype more complex than etc., eg string, vectoretc.) object? How independent are the copies after the copy operation? How many copy operations are there? How do I specify which one to use? What is the relationship between copying and other primitive operations such as initialization and destruction?

When there are no higher-level types (such as vectorand string), we inevitably need to discuss how to manipulate memory. We'll look at arrays and pointers, their relationships, uses, and pitfalls. This is very important for anyone who needs to write low-level C/C++ code.

Note that although the implementation details of C++ are specific to C++, the construction of "high-level" types ( , , , etc.) vectorin each language reflects solutions to some of the fundamental problems presented in this chapter.stringvectorlistmap

18.2 Initialization

The ones in Chapter 17 vectorcan only be initialized to default values ​​and then assigned:

vector v(2); // tedious and error-prone
v.set(0, 1.2);
v.set(1, 7.89);
v.set(2, 12.34);

This method is not only cumbersome but also error-prone (the above code got the wrong number of elements). We wish to initialize the vector directly with a set of values, for example:

vector v = {
    
    1.2, 7.89, 12.34};

To do this, we need to write a constructor that accepts an initializer list as a parameter. Starting with C++11, the list of elements of the {}enclosed type is an object ofTinitializer_list<T> a standard library type , defined in the header file <initializer_list>. So it can be written like this:

// initializer-list constructor
vector(initializer_list<double> lst) :sz(lst.size()), elem(new double[sz]) {
    
    
    copy(lst.begin(), lst.end(), elem);    // initialize (using std::copy())
}

The standard library algorithm is used here copy()to copy the elements in the initialization list to elemthe array.

Simple Vector v2 - initializer list constructor

There are now two ways to initialize vectors:

vector v1 = {
    
    1,2,3};  // three elements 1.0, 2.0, 3.0
vector v2(3);         // three elements each with the (default) value 0.0

Note that ()it is used to specify the number of elements (call the constructor vector(int)) and {}to specify the list of elements (call the constructor vector(initializer_list<double>)). For example:

vector v1{
    
    3};  // one element with the value 3.0
vector v2(3);  // three elements each with the (default) value 0.0

Note: If there is no initialization list constructor, it vector v{3};is equivalent to vector v(3);, see Section 9.4.2 "C++ Initialization Syntax" for details.

In most cases, {}the preceding =can be omitted and is thus vector v = {1, 2, 3};equivalent to vector v{1, 2, 3};.

Note that initializer_list<double>it is passed by value, which is required by the language rules: initializer_listit just refers to the elements allocated "elsewhere" (the bottom layer is a pointer to an array element, similar vector, but copying initializer_listthe object does not copy these elements).

18.3 Copy

Consider ours again vector, trying to copy a vector:

void f(int n) {
    
    
    vector v(3);    // define a vector of 3 elements
    v.set(2,2.2);   // set v[2] to 2.2
    vector v2 = v;  // what happens here?
    // ...
}

Ideally, v2that would be va copy (copy) of , ie v2.size() == v.size(), and satisfies for v.size()all in the range [0, ) (although no operator is defined yet). Also, when exiting, all memory is returned to free storage. The standard library does this, but our (still simplistic) one doesn't.iv2[i] == v[i][]f()vectorvector

For a class, the default meaning of copy is "copy all data members" . For simple classes with only built-in type members (for example Point), this is usually sufficient. But with pointer members, just copying the member (the value of the pointer) creates problems. For example vector, this means after copying v2.sz == v.szand v2.elem == v.elem, as shown in the image below:

bad copy construction

In other words, v2there are no copied velements, only shared velements (only pointers are copied), which leads to two problems:

  • Assigning v2a value to the element affects vthe element and vice versa, which is not what we want.
  • When f()returning, the destructors of vand are called implicitly, causing the pointed-to memory to be freed twice, with disastrous results (see Section 17.4.6).v2elem

18.3.1 Copy constructor

We should provide a copy operation to copy elements and guarantee that this copy operation will be invoked when we use one vectorto initialize the other .vector

For this, we need a copy constructor whose argument is a reference to the object being copied. For vector:

vector(const vector&);

When an object is used to initialize another object of the same type, the copy constructor is called. include:

  • initialization: T a = b;or T a(b);, where bis Tthe type
  • Function parameter passing: f(a), where aand function parameters are Tboth types
  • Function return value: return a;, where the function return value is Ttype and there Tis no move constructor

The improved one vectoris as follows:

simple vector v2 - copy constructor

Considering the example again given the copy constructor vector v2 = v;, the result now looks like this:

correct copy construction

Obviously, now that the two vectors are independent of each other, changing vthe value of an element in has no effect v2, and vice versa. The destructor works fine too.

vector v2 = v;Equivalent to vector v2{v};or vector v2(v);. When vand v2are the same type, and the type defines a copy constructor, the meanings of these writing methods are exactly the same.

18.3.2 Copy assignment

In addition to copy construction, objects can also be copied by assignment. Like copy construction, the default meaning of copy assignment is "copy all members" , which is currently vectorthe case. For example:

void f2(int n) {
    
    
    vector v(3);  // define a vector
    v.set(2,2.2);
    vector v2(4);
    v2 = v;       // assignment: what happens here?
    // ...
}

We want to v2call it va copy, but the result of the default copy assignment is to make v2.sz == v.szand v2.elem == v.elem, as shown in the following figure:

bad copy assignment

This causes two problems:

  • Deduplication: Like the default copy constructor, when f2()returning, vthe v2co-pointed element is freed twice.
  • Memory leak: v2The memory of the 4 elements originally pointed to has not been released.

The improvements to copy assignment are essentially the same as copy construction, requiring the definition of a copy assignment operator :

vector& operator=(const vector& v);

The copy assignment operator is invoked when an object appears on the left side of an assignment expression.

simple vector v2 - copy assignment

Assignment is a little more complicated than construction, because old elements have to be dealt with. Here, the new element is copied first, then the old element is released, and finally elemthe new element is pointed to, as shown in the following figure:

correct copy assignment

Now vectorit will not leak memory and free memory repeatedly.

Note that when implementing assignment operations, releasing the memory of the old element first and then copying may simplify the code, but this cannot handle the vectorcase of assigning a value to itself:

vector v(10);
v = v;  // self-assignment

Unless first checking whether the copied object and the current object are the same object, ie if (&v == this) return;. Although the performance of the implementation in the book is not optimal (the element is also copied once for self-assignment), it handles this situation correctly.

18.3.3 Copy term

The basic question of copying is whether to copy a pointer (or reference) or to copy the data pointed to (or referenced):

  • Shallow copy : Only the pointer is copied so that both pointers point to the same object.
  • Deep copy (deep copy): Copy the data pointed to by the pointer, so that the two pointers point to different objects. The standard library's vector, stringetc. are all like this. When we want to make a deep copy of an object of our own class, we need to define a copy constructor and copy assignment.

Here is an example of a shallow copy:

int* p = new int(77);
int* q = p;  // copy the pointer p
*p = 88;     // change the value of the int pointed to by p and q

shallow copy

Instead, here's an example of a deep copy:

int* p = new int(77);
int* q = new int(*p);  // allocate a new int, then copy the value pointed to by p
*p = 88;               // change the value of the int pointed to by p

deep copy

Using this term, the original vectorproblem was that only shallow copies were made, while the improved vector(and standard library vector) made deep copies.

A type that provides a shallow copy is said to have pointer semantics or reference semantics (only the address is copied); a type that provides a deep copy is said to have value semantics (the value pointed to is copied).

18.3.4 Mobile

If one vectorhas many elements, then copying it can be expensive. Consider an example:

vector fill(istream& is) {
    
    
    vector res;
    for (double x; is>>x; ) res.push_back(x);
    return res;
}

void use() {
    
    
    vector vec = fill(cin);
    // ... use vec ...
}

Assuming res100,000 elements, copying it to vecis expensive. But we don't want to copy, after all, we will never use it res.

An alternative is to pass by reference parameters:

void fill(istream& is, vector& v) {
    
    
    for (double x; is>>x; ) v.push_back(x);
}

void use() {
    
    
    vector vec;
    fill(cin, vec);
    // ... use vec ...
}

The disadvantage is that the return value syntax cannot be used, and the variable must be declared first.

Another way is to return newthe created pointer:

vector* fill(istream& is) {
    
    
    vector* res = new vector;
    for (double x; is>>x; ) res->push_back(x);
    return res;

}
void use() {
    
    
    vector* vec = fill(cin);
    // ... use vec ...
    delete vec;
}

The disadvantage is that this vector must be remembered delete(as described in Section 17.4.6).

We want to use return value syntax while avoiding copies . For this reason, C++11 introduces move semantics (move semantics): By "stealing" resources, directly moveres (move) resources to , as shown in the following figure:vec

move

After the move, the vecreferenced reselement reswill be nulled (in other words, move = "steal" resource = shallow copy + null the original pointer). Thus moving int100,000 elements from to at the cost of just copying one and one pointer .resvec

In order to express move semantics in C++, a move constructor and a move assignment operator need to be defined:

vector(vector&& v);             // move constructor
vector& operator=(vector&& v);  // move assignment

The &&symbols are called rvalue references . Note that the parameters of the move operation are not const, because one of the purposes of the move operation is to nullify the source object.

The move constructor is called when an rvalue is used to initialize an object of the same type. include:

  • initialization: T a = std::move(b);or T a(std::move(b));, where bis Tthe type
  • Function parameter passing: f(std::move(a)), where aand function parameters are Tboth types
  • Function return value: return a;, where the function return value is Ta type and Thas a move constructor

Note:

  • Named variables are lvalues, and literal values, arithmetic expressions, temporary objects, and function calls whose return value is a value type are all rvalues, see Value categories for details .
  • An lvalue cannot be assigned to an rvalue reference (and thus cannot be moved) unless std::move()converted to an rvalue reference using a function, which means that the object may be resource-stealed and therefore no longer usable.
  • Starting from C++17, the compiler will enforce copy elimination (copy elision), and if the initial value is a prvalue (prvalue), the move constructor call will be optimized away.

The move assignment operator is invoked when an object appears on the left side of an assignment expression, and the right side is an rvalue of the same type.

Simple Vector v2 - move constructor and move assignment

Considering the previous example again, upon fill()return, vectorthe move constructor of 's will be called implicitly ( neither the code of 's fill()nor use()'s code needs to be modified).

Note: See also [C++] rvalue references and move semantics

18.4 Basic Operation

Now we can discuss how to decide which constructors a class should have, whether it should have a destructor, and whether it should provide copy and move operations. There are 7 basic operations to consider:

  • parameterized constructorT(A1 a1, A2 a2, ...)
  • default constructorT()
  • copy constructorT(const T&)
  • copy assignmentT& operator=(const T&)
  • move constructorT(T&&)
  • mobile assignmentT& operator=(T&&)
  • destructor~T()

Usually, we need one or more constructors with parameters to initialize the object, and the meaning and purpose of the initial values ​​(parameters) depends entirely on the constructor. Usually we use constructors to establish invariants (Section 9.4.3).

If we wish to create an object of a class without specifying an initial value, a default constructor is required. The most common example is putting objects vectorin the standard library (for example vector vs(10);). Default constructors make sense when we can establish invariants for a class with a meaningful, obvious default value. For example, the default value is 0 for a numeric type, stringan empty string for an empty vector, and vectoran empty vector for that. For types T, or the default value if a default constructor exists .T{}T()

If a class acquires a resource, a destructor is required. Resources include free storage, files, locks, thread handles, sockets, etc. Another characteristic of a class that requires a destructor is that it has pointer or reference members.

A class that requires a destructor almost always requires copy and move operations. The reason is simple: if an object acquires a resource (has a pointer member to it), then the default meaning of copy (a shallow copy) is almost certainly wrong. vectoris a typical example.

In addition, the base class requires a virtual destructor if the derived class has a destructor (§17.5.2).

18.4.1 Explicit constructors

A constructor that takes a single argument defines a conversion from the argument's type to that type. For example:

class complex {
    
    
public:
    complex(double);  // defines double-to-complex conversion
    complex(double,double);
    // ...
};

complex z1 = 3.14; // OK: convert 3.14 to (3.14,0)
complex z2 = complex(1.2, 3.4);

However, this implicit conversion should be used with caution because it may cause unexpected results. For example, vectorthere is an accepting intconstructor, so it can be used like this:

vector v = 10;  // odd: makes a vector of 10 doubles
v = 20;         // eh? Assigns a new vector of 20 doubles to v

void f(const vector&);
f(10);          // eh? Calls f with a new vector of 10 doubles

We can prohibit the use of constructors for implicit conversions through explicit constructors . Explicit constructors explicitare defined using keywords:

class vector {
    
    
    // ...
    explicit vector(int);
    // ...
};

vector v = 10;  // error: no int-to-vector conversion
v = 20;         // error: no int-to-vector conversion
vector v0(10);  // OK

void f(const vector&);
f(10);          // error: no int-to-vector<double> conversion
f(vector(10));  // OK

simple vector v2 - explicit constructor

18.4.2 Debugging constructors and destructors

Both constructors and destructors are called at well-defined, predictable points during a program's execution:

  • A constructor of the type will be called whenever an object of the type Xis created . XFor example, variable initialization, passing parameters, using to newcreate objects, copying objects, etc.
  • Whenever an object of the type Xis destroyed, Xthe destructor will be called. For example, a variable goes out of scope, deletea pointer to an object, or the end of the program.

A good way to get a feel for this problem is to include print statements in constructors, assignments, and destructors. For example:

try it

★Be sure to run this example, and make sure you understand the output, so you will understand the general process of object construction and destruction.

The output corresponding to each line of the program is shown in the figure below:

sample program output

The copy process of some of the more complex statements is shown in the figure below (the "c" on the arrow means copy construction, and "=" means copy assignment):

Copy process of complex statement

  • loc = X(5): ① Create a temporary object X(5)(address 0xae19bffa90); ② Copy and assign the temporary object to loc(address 0xae19bffa8c); ③ Destroy the temporary object.
  • loc2 = copy(loc): ① Copy the formal parameters of locthe constructor (address 0xae19bffa98); ② Construct the return value temporary object by copying the formal parameters (address 0xae19bffa94); ③ Copy and assign the temporary object to (address 0xae19bffa88); ④ Destroy the temporary object; ⑤ Destroys the parameter .copy()xxloc2x
  • loc2 = copy2(loc): ① Copy the formal parameters of locthe constructor function (address 0xae19bffaa0); ② Construct a local variable from the formal parameters copy (address 0xae19bffa9c); ③ Return a temporary object from the copy construction; ④ Copy and assign the temporary object to (address 0xae19bffa88) ; ⑤ Destroy temporary objects; ⑥ Destroy local variables ; ⑦ Destroy formal parameters . Note that the return value temporary object here may be optimized by the compiler, that is, the copy is directly assigned to it , and the above output is like this.copy2()xxxxxxloc2xxxxxloc2
  • X& r = ref_to(loc): Both parameters and return value are references, so there is no constructor call.
  • vector<X> v(4): Each element calls the default constructor (this also shows vectorthat this usage requires that the element type must be default-constructable).
  • Before the program ends, the destructors of all objects are called, and the order of destruction is the reverse of the order of creation.

Some compilers are smart enough to eliminate unnecessary copies. However, if you want to be portable (that is, you can get the same effect on different platforms and using different compilers), consider moving operations.

18.5 Accessing vector elements

So far we have been using set()and get()member functions to access elements. In order to use the usual subscript syntax v[i], []the operator needs to be overloaded, i.e. define a operator[]member function named:

double operator[](int i) {
    
     return elem[i]; }

This implementation looks nice, but is simplistic - the value returned by the subscript operator is only readable and not writable (since the return value doubleis an rvalue, not an lvalue):

vector v(10);
double x = v[2];  // fine
v[3] = x;         // error: v[3] is not an lvalue

which v[i]is interpreted as v.operator[](i).

Returning a reference to the element solves this problem:

double& operator[](int n) {
    
     return elem[n]; }

Now the code above works fine because double&is an lvalue.

Note: []operator can replace set()and get().

18.5.1 const overloading

operator[]There is still a problem with the current one : it cannot be const vectorcalled. For example:

void f(const vector& cv) {
    
    
    double d = cv[1];  // error, but should be fine
    cv[1] = 2.0;       // error (as it should be)
}

The first one cv[1]only reads the value of the element and does not modify it vector, but the compiler will still report an error, because operator[]the return type of double&is itself modifiable. The workaround is to provide a constmember function version:

double& operator[](int n);       // for non-const vectors
double operator[](int n) const;  // for const vectors

constA version operator[]can also be returned const double&, doublebut is a very small object, so there is no need to return a reference (see Section 8.5.6). Now it can be used like this:

void ff(const vector& cv, vector& v) {
    
    
    double d = cv[1];  // fine (uses the const [])
    cv[1] = 2.0;       // error (uses the const [])
    d = v[1];          // fine (uses the non-const [])
    v[1] = 2.0;        // fine (uses the non-const [])
}

Simple Vector v2 - Subscript Operator

18.6 Arrays

An array is a fixed-size, contiguous sequence of elements.

  • T a[N];declares an Narray Tof elements of type, which Nmust be integer constant expressions.
  • T* p = new T[n];NAn array Tof elements of type is allocated on free storage , where nany integer expression can be.

[]Array elements are numbered 0 to N-1 and can be accessed using the subscript operator , ie a[0], , ..., a[N-1].

Arrays have significant limitations (e.g. arrays don't know their own size ) and are therefore preferred whenever possible vector. However, arrays vectorhave existed long before, and are roughly equivalent to arrays in other languages ​​(especially C), so you have to understand arrays to be able to cope with code that was long ago, or written by someone who couldn't vectoruse .

18.6.1 Pointers to Array Elements

Pointers can point to array elements. For example:

double ad[10];
double* p = &ad[5];  // point to ad[5]

The pointer now ppoints to ad[5]:

pointer to array element

Array elements can be accessed using subscripting and dereferencing on pointers:

*p = 7;  // equivalent to p[0] = 7
p[2] = 6;
p[3] = 9;

get

Access array elements through pointers

The subscript can be positive or negative, as long as the corresponding element is within the range [0, N) of the array. However, accessing data outside the bounds of the array is illegal (see Section 17.4.3).

When a pointer points to an array, addition and subtraction operations can make the pointer point to other elements. For example:

p += 2;  // move p 2 elements to the right

pointer plus operation

p –= 5;  // move p 5 elements to the left

pointer decrement operation

Operations using +, -, +=and -=moving pointers are called pointer arithmetic. When performing this operation, we must ensure that the result does not exceed the bounds of the array:

p += 1000;      // insane: p points into an array with just 10 elements
double d = *p;  // illegal: probably a bad value (definitely an unpredictable value)
*p = 12.34;     // illegal: probably scrambles some unknown data

Unfortunately, not all bugs involving pointer arithmetic are easy to find. It's usually best to avoid pointer arithmetic.

The most common usage of pointer arithmetic is using ++point to next element and using --point to previous element. For example, adthe element value can be printed like this:

for (double* p = &ad[0]; p < &ad[10]; ++p) cout << *p << '\n';
for (double* p = &ad[9]; p >= &ad[0]; ––p) cout << *p << '\n';

Note:

  • p+iequivalent to &p[i], *(p+i)equivalent top[i]
  • When adding and subtracting pointers and integers, the multiples of the object size will be automatically scaled. For example, the difference p+ifrom pthe actual address value is i * sizeof(*p), so it points exactly to the object pafter that .i

Most practical uses of pointer arithmetic are passing pointers as function arguments. In this case, the compiler does not know how many elements the array points to, and you must provide it (see Section 18.7.2). We should try to avoid this situation.

18.6.2 Pointers and Arrays

The array name represents all elements of the array. For example:

char ch[100];

then sizeof(ch)100.

However, an array name is the address of the first element , and thus can be converted ("degenerated") into a pointer. For example:

char* p = ch;

where pis initialized to &ch[0], sizeof(p)but 4 (or 8) instead of 100.

This is very useful. One reason is to avoid copying when passing an array to a pointer parameter. For example, the function strlen()counts the number of characters in a 0-terminated character array (i.e. the length of a C-style string):

// similar to the standard library strlen()
int strlen(const char* p) {
    
    
    int count = 0;
    while (*p) {
    
     ++count; ++p; }
    return count;
}

This is equivalent to strlen(ch)calling the function strlen(&ch[0]). Unlike vector, passing an array by parameter does not copy the array elements (only a pointer is copied). Parameter declarations char* pare equivalent to char p[].

Note that array names cannot be assigned , for example:

char ac[10];
ac = new char[20];      // error: no assignment to array name
&ac[0] = new char[20];  // error: no assignment to pointer value

Arrays cannot also be copied using assignment :

int x[100];
int y[100];
x = y;           // error
int z[100] = y;  // error

If an array needs to be copied, complex code must be written. For example:

for (int i=0; i<100; ++i) x[i]=y[i];  // copy 100 ints
memcpy(x,y,100*sizeof(int));          // copy 100*sizeof(int) bytes
copy(y,y+100, x);                     // copy 100 ints

Note: See also for the relationship between pointers and arrays

18.6.3 Array initialization

Character arrays can be initialized with string constants. For example:

char ac[] = "Beorn";  // array of 6 chars

The string has only 5 characters, but acits length is 6, because the compiler automatically adds 0 at the end of the string constant, as shown in the following image:

character array

This 0-terminated character array is called a C-style string. All string constants are C-style strings. For example:

char* pc = "Howdy";  // pc points to an array of 6 chars

string constant

Note that the end is character '\0'(value 0) not '0'(value 48). The purpose of the trailing 0 is to let the function find the end of the string (for example strlen()), since the array doesn't know its own size.

The standard library header file <cstring> defines strlen()functions. This function counts the number of characters, excluding the trailing 0. Therefore, a C-style string of n characters requires a character array of length n+1 to store.

Note: A function stringis provided c_str()to return the pointer to the underlying character array of the string, which can be stringconverted to a C-style string.

Only character arrays can be initialized with string constants, but all arrays can be initialized with an initializer list for the corresponding element type. For example:

int ai[] = {
    
    1, 2, 3, 4, 5, 6};         // array of 6 ints
int ai2[100] = {
    
    0,1,2,3,4,5,6,7,8,9};  // the last 90 elements are initialized to 0
double ad[100] = {
    
    };                   // all elements initialized to 0.0
char chars[] = {
    
    'a', 'b', 'c'};        // no terminating 0!

Note that charsthe length of is 3 (not 4) - the "add 0 at the end" rule only applies to strings. If no array size is specified, it is inferred from the initializer list. If the number of initial values ​​is less than the array size (eg ai2and ad), the remaining elements will be initialized to the default value of the element type.

18.6.4 Pointer Problems

Pointers and arrays are often overused and misused, and this section summarizes frequently occurring problems. All serious problems with pointers involve trying to access data of an object of an unexpected type , and many of these problems involve accessing data outside the bounds of an array (out-of-bounds access). In this section we mainly consider:

  • access via null pointer
  • Access through uninitialized pointers (wild pointers)
  • access data past the end of the array
  • access to a freed object
  • accessing an object that has gone out of scope

In all cases, the real problem for the programmer is that the code looks fine. Worse (when writing data through pointers), the problem may not manifest until long after some seemingly unrelated object has been corrupted. Consider some examples below.

(1) Do not access data through a null pointer

int* p = nullptr;
*p = 7;  // ouch!

Obviously, in real-world programs, there is usually other code between initialization and use, and passing pto a function or accepting a function return value is a very common example. Try not to pass a null pointer, and if you must, check for a null pointer before using it :

int* p = fct_that_can_return_a_nullptr();

if (p == nullptr) {
    
    
    // do something
}
else {
    
    
    // use p
    *p = 7;
}
void fct_that_can_receive_a_nullptr(int* p) {
    
    
    if (p == nullptr) {
    
    
        // do something
    }
    else {
    
    
        // use p
        *p = 7;
    }
}

Using references and using exceptions to catch errors are the main tools for avoiding null pointers.

Note: A reference cannot express the meaning of "non-existence" like a null pointer (equivalent to leaving the work of checking the null pointer to the caller). Introduced in C++17 optional, it represents a "possible value" and helps avoid null pointer problems.

(2) Initialize the pointer

int* p;
*p = 9;  // ouch!

In particular, don't forget to initialize class member pointers.

(3) Do not access array elements that do not exist

int a[10];
int* p = &a[10];
*p = 11;     // ouch!
a[10] = 12;  // ouch!

Be careful with the first and last elements of the loop (such as the example in Section 18.6.1 that prints the array element at the end). Try not to pass arrays as a pointer to the first element, use instead vector. If you must do this, you should be extremely careful and pass the array size along with it (see Section 18.7.2).

(4) Do not access data through deleted pointers

int* p = new int(7);
// ...
delete p;
// ...
*p = 13;  // ouch!

delete pOr the code after that may have been rewritten *por used for other objects (Note: Some compilers may deletefill pthe pointed memory area with some meaningless bytes, and then this memory may be allocated by the memory manager to other objects). Of all problems, this one is the most difficult to systematically avoid. The most effective way to prevent this problem is to avoid "naked" sums new-- only use sums deletein constructors and destructors , or use containers to handle them .newdeletedelete

(5) Do not return pointers to local variables

int* f() {
    
    
    int x = 7;
    // ...
    return &x;
}

// ...

int* p = f();
// ...
*p = 15;  // ouch!

Local variables are freed when the function exits. Few compilers will catch problems related to returning pointers to local variables.

Consider a logically equivalent example:

vector& ff() {
    
    
    vector x(7);  // 7 elements
    // ...
    return x;
}  // the vector x is destroyed here

// ...

vector& p = ff();
// ...
p[4] = 15;  // ouch!

Many compilers can catch this return problem.

Programmers often underestimate these problems. However, many experienced programmers have been defeated by countless variations and combinations of these simple array and pointer problems. The solution is to not pollute your code with pointers, arrays, newand . deleteIf you do, "careful" is simply not enough in real-scale programs. Instead, rely on vectors, RAII ("Resource Acquisition Is Initialization", Section 19.5), and other system approaches to manage memory and other resources.

18.7 Example: Palindrome

A palindrome is a word that is spelled the same from both ends. For example, anna, petep, and malayalam are palindromes, but ida and homesick are not. There are two basic ways to tell if a word is a palindrome:

  • Constructs a reversed copy and compares it with the original word.
  • Check if the first and last letters are the same, then check if the second and penultimate letters are the same, until you reach the middle of the word.

Here we will use the second method. There are many ways to express this idea in code, depending on how words are represented and how you keep track of where to compare.

18.7.1 Implementation using strings

First use the standard library stringalong with intan index to keep track of where to compare:

is_palindrome function (overload 1)

Return if the middle is reached and no different characters are found true. It is recommended to test with empty strings, strings with only one character, and strings with even and odd numbers of characters.

18.7.2 Implementation using arrays

If not string, you'll have to use an array to store characters:

is_palindrome function (overload 2)

To test the function, c_str()the function converts stringto a 0-terminated character array, or the istreamoperator >>(which provides const char*a version that accepts an argument). For example:

// read at most max–1 characters from is into buffer
istream& read_word(istream& is, char* buffer, int max) {
    
    
    is.width(max);  // read at most max–1 characters in the next >>
    is >> buffer;   // read whitespace-terminated word,
                    // add zero after the last character read into buffer
    return is;
}

The code for the array version is stringmuch more complex than the version, and cannot handle strings of arbitrary length (the maximum length must be specified).

18.7.3 Implementation using pointers

Instead of indexes, pointers can also be used to identify character positions:

is_palindrome function (overload 3)

We can also rewrite is_palindrome()the function like this (just for fun):

// first points to the first letter, last to the last letter
bool is_palindrome(const char* first, const char* last) {
    
    
    if (first<last) {
    
    
        if (*first!=*last) return false;
        return is_palindrome(first+1,last–1);
    }
    return true;
}

Here is the recursive implementation version. This code becomes obvious when we redefine the definition of a palindrome (recursive definition): If the first and last characters of a word are the same, and the substring after removing the first and last characters is a palindrome, then This word is a palindrome.

simple exercise

drill18

exercise

Guess you like

Origin blog.csdn.net/zzy979481894/article/details/130692897