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.
vector
A 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 int
type more complex than etc., eg string
, vector
etc.) 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 vector
and 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.) vector
in each language reflects solutions to some of the fundamental problems presented in this chapter.string
vector
list
map
18.2 Initialization
The ones in Chapter 17 vector
can 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 ofT
initializer_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 elem
the 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_list
it just refers to the elements allocated "elsewhere" (the bottom layer is a pointer to an array element, similar vector
, but copying initializer_list
the 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, v2
that would be v
a 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.i
v2[i] == v[i]
[]
f()
vector
vector
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.sz
and v2.elem == v.elem
, as shown in the image below:
In other words, v2
there are no copied v
elements, only shared v
elements (only pointers are copied), which leads to two problems:
- Assigning
v2
a value to the element affectsv
the element and vice versa, which is not what we want. - When
f()
returning, the destructors ofv
and are called implicitly, causing the pointed-to memory to be freed twice, with disastrous results (see Section 17.4.6).v2
elem
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 vector
to 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;
orT a(b);
, whereb
isT
the type - Function parameter passing:
f(a)
, wherea
and function parameters areT
both types - Function return value:
return a;
, where the function return value isT
type and thereT
is no move constructor
The improved one vector
is as follows:
simple vector v2 - copy constructor
Considering the example again given the copy constructor vector v2 = v;
, the result now looks like this:
Obviously, now that the two vectors are independent of each other, changing v
the 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 v
and v2
are 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 vector
the 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 v2
call it v
a copy, but the result of the default copy assignment is to make v2.sz == v.sz
and v2.elem == v.elem
, as shown in the following figure:
This causes two problems:
- Deduplication: Like the default copy constructor, when
f2()
returning,v
thev2
co-pointed element is freed twice. - Memory leak:
v2
The 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 elem
the new element is pointed to, as shown in the following figure:
Now vector
it 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 vector
case 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
,string
etc. 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
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
Using this term, the original vector
problem 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 vector
has 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 res
100,000 elements, copying it to vec
is 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 new
the 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
After the move, the vec
referenced res
element res
will be nulled (in other words, move = "steal" resource = shallow copy + null the original pointer). Thus moving int
100,000 elements from to at the cost of just copying one and one pointer .res
vec
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);
orT a(std::move(b));
, whereb
isT
the type - Function parameter passing:
f(std::move(a))
, wherea
and function parameters areT
both types - Function return value:
return a;
, where the function return value isT
a type andT
has 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, vector
the 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 constructor
T(A1 a1, A2 a2, ...)
- default constructor
T()
- copy constructor
T(const T&)
- copy assignment
T& operator=(const T&)
- move constructor
T(T&&)
- mobile assignment
T& 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 vector
in 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, string
an empty string for an empty vector, and vector
an 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. vector
is 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, vector
there is an accepting int
constructor, 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 explicit
are 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
X
is created .X
For example, variable initialization, passing parameters, using tonew
create objects, copying objects, etc. - Whenever an object of the type
X
is destroyed,X
the destructor will be called. For example, a variable goes out of scope,delete
a 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:
★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:
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):
loc = X(5)
: ① Create a temporary objectX(5)
(address 0xae19bffa90); ② Copy and assign the temporary object toloc
(address 0xae19bffa8c); ③ Destroy the temporary object.loc2 = copy(loc)
: ① Copy the formal parameters ofloc
the 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()
x
x
loc2
x
loc2 = copy2(loc)
: ① Copy the formal parameters ofloc
the 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()
x
x
xx
xx
loc2
xx
x
xx
loc2
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 showsvector
that 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 double
is 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 vector
called. 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 const
member function version:
double& operator[](int n); // for non-const vectors
double operator[](int n) const; // for const vectors
const
A version operator[]
can also be returned const double&
, double
but 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 anN
arrayT
of elements of type, whichN
must be integer constant expressions.T* p = new T[n];
N
An arrayT
of elements of type is allocated on free storage , wheren
any 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 vector
have 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 vector
use .
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 p
points to ad[5]
:
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
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
p –= 5; // move p 5 elements to the left
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, ad
the 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+i
equivalent 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+i
fromp
the actual address value isi * sizeof(*p)
, so it points exactly to the objectp
after 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 p
is 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* p
are 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
- "C Programming Language" Notes Chapter 5 Pointers and Arrays
- Row and column pointers for two-dimensional 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 ac
its length is 6, because the compiler automatically adds 0 at the end of the string constant, as shown in the following image:
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
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 string
is provided c_str()
to return the pointer to the underlying character array of the string, which can be string
converted 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 chars
the 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 ai2
and 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 p
to 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 p
Or the code after that may have been rewritten *p
or used for other objects (Note: Some compilers may delete
fill p
the 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 delete
in constructors and destructors , or use containers to handle them .new
delete
delete
(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, new
and . delete
If 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 string
along with int
an 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 string
to a 0-terminated character array, or the istream
operator >>
(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 string
much 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.