B. Stroustrup - The C++ Programming Language (794319), страница 21
Текст из файла (страница 21)
use v and v2 ...} // v2 is destroyed here// ... use v ..} // v is destroyed hereobeys the same rules for naming, scope, allocation, lifetime, etc., as does a built-in type,such as int and char. For details on how to control the lifetime of an object, see §6.4. This Vectorhas been simplified by leaving out error handling; see §2.4.3.The constructor/destructor combination is the basis of many elegant techniques. In particular, itis the basis for most C++ general resource management techniques (§5.2, §13.3). Consider agraphical illustration of a Vector:Vector64A Tour of C++: Abstraction MechanismsChapter 3Vector:elem:sz:60:01:02:03:04:05:0The constructor allocates the elements and initializes the Vector members appropriately. The destructor deallocates the elements. This handle-to-data model is very commonly used to managedata that can vary in size during the lifetime of an object.
The technique of acquiring resources in aconstructor and releasing them in a destructor, known as Resource Acquisition Is Initialization orRAII, allows us to eliminate ‘‘naked new operations,’’ that is, to avoid allocations in general codeand keep them buried inside the implementation of well-behaved abstractions. Similarly, ‘‘nakeddelete operations’’ should be avoided. Avoiding naked new and naked delete makes code far lesserror-prone and far easier to keep free of resource leaks (§5.2).3.2.1.3 Initializing ContainersA container exists to hold elements, so obviously we need convenient ways of getting elements intoa container.
We can handle that by creating a Vector with an appropriate number of elements andthen assigning to them, but typically other ways are more elegant. Here, I just mention twofavorites:• Initializer-list constructor: Initialize with a list of elements.• push_back(): Add a new element at the end (at the back of) the sequence.These can be declared like this:class Vector {public:Vector(std::initializer_list<double>);// ...void push_back(double);// ...};// initialize with a list// add element at end increasing the size by oneThe push_back() is useful for input of arbitrary numbers of elements. For example:Vector read(istream& is){Vector v;for (double d; is>>d;)v.push_back(d);return v;}// read floating-point values into d// add d to vThe input loop is terminated by an end-of-file or a formatting error. Until that happens, each number read is added to the Vector so that at the end, v’s size is the number of elements read. I used afor-statement rather than the more conventional while-statement to keep the scope of d limited to theloop.
The implementation of push_back() is discussed in §13.6.4.3. The way to provide Vector witha move constructor, so that returning a potentially huge amount of data from read() is cheap, isexplained in §3.3.2.Section 3.2.1.3Initializing Containers65The std::initializer_list used to define the initializer-list constructor is a standard-library typeknown to the compiler: when we use a {}-list, such as {1,2,3,4}, the compiler will create an object oftype initializer_list to give to the program. So, we can write:Vector v1 = {1,2,3,4,5};Vector v2 = {1.23, 3.45, 6.7, 8};Vector’s// v1 has 5 elements// v2 has 4 elementsinitializer-list constructor might be defined like this:Vector::Vector(std::initializer_list<double> lst)// initialize with a list:elem{new double[lst.size()]}, sz{lst.size()}{copy(lst.begin(),lst.end(),elem);// copy from lst into elem}3.2.2 Abstract TypesTypes such as complex and Vector are called concrete types because their representation is part oftheir definition.
In that, they resemble built-in types. In contrast, an abstract type is a type thatcompletely insulates a user from implementation details. To do that, we decouple the interfacefrom the representation and give up genuine local variables. Since we don’t know anything aboutthe representation of an abstract type (not even its size), we must allocate objects on the free store(§3.2.1.2, §11.2) and access them through references or pointers (§2.2.5, §7.2, §7.7).First, we define the interface of a class Container which we will design as a more abstract version of our Vector:class Container {public:virtual double& operator[](int) = 0;virtual int size() const = 0;virtual ˜Container() {}};// pure virtual function// const member function (§3.2.1.1)// destructor (§3.2.1.2)This class is a pure interface to specific containers defined later.
The word virtual means ‘‘may beredefined later in a class derived from this one.’’ Unsurprisingly, a function declared virtual iscalled a virtual function. A class derived from Container provides an implementation for the Container interface. The curious =0 syntax says the function is pure virtual; that is, some class derivedfrom Container must define the function.
Thus, it is not possible to define an object that is just aContainer; a Container can only serve as the interface to a class that implements its operator[]() andsize() functions. A class with a pure virtual function is called an abstract class.This Container can be used like this:void use(Container& c){const int sz = c.size();for (int i=0; i!=sz; ++i)cout << c[i] << '\n';}66A Tour of C++: Abstraction MechanismsChapter 3Note how use() uses the Container interface in complete ignorance of implementation details.
Ituses size() and [] without any idea of exactly which type provides their implementation. A classthat provides the interface to a variety of other classes is often called a polymorphic type (§20.3.2).As is common for abstract classes, Container does not have a constructor.
After all, it does nothave any data to initialize. On the other hand, Container does have a destructor and that destructoris virtual. Again, that is common for abstract classes because they tend to be manipulated throughreferences or pointers, and someone destroying a Container through a pointer has no idea whatresources are owned by its implementation; see also §3.2.4.A container that implements the functions required by the interface defined by the abstract classContainer could use the concrete class Vector:class Vector_container : public Container { // Vector_container implements ContainerVector v;public:Vector_container(int s) : v(s) { }// Vector of s elements˜Vector_container() {}double& operator[](int i) { return v[i]; }int size() const { return v.size(); }};The :public can be read as ‘‘is derived from’’ or ‘‘is a subtype of.’’ Class Vector_container is said tobe derived from class Container, and class Container is said to be a base of class Vector_container.An alternative terminology calls Vector_container and Container subclass and superclass, respectively.
The derived class is said to inherit members from its base class, so the use of base andderived classes is commonly referred to as inheritance.The members operator[]() and size() are said to override the corresponding members in the baseclass Container (§20.3.2).
The destructor (˜Vector_container()) overrides the base class destructor(˜Container()). Note that the member destructor (˜Vector()) is implicitly invoked by its class’s destructor (˜Vector_container()).For a function like use(Container&) to use a Container in complete ignorance of implementationdetails, some other function will have to make an object on which it can operate. For example:void g(){Vector_container vc {10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};use(vc);}Since use() doesn’t know about Vector_containers but only knows the Container interface, it willwork just as well for a different implementation of a Container. For example:class List_container : public Container { // List_container implements Containerstd::list<double> ld;// (standard-librar y) list of doubles (§4.4.2)public:List_container() { }// empty ListList_container(initializer_list<double> il) : ld{il} { }˜List_container() {}Section 3.2.2Abstract Types67double& operator[](int i);int size() const { return ld.size(); }};double& List_container::operator[](int i){for (auto& x : ld) {if (i==0) return x;−−i;}throw out_of_range("List container");}Here, the representation is a standard-library list<double>.
Usually, I would not implement a container with a subscript operation using a list, because performance of list subscripting is atrociouscompared to vector subscripting. However, here I just wanted to show an implementation that isradically different from the usual one.A function can create a List_container and have use() use it:void h(){List_container lc = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };use(lc);}The point is that use(Container&) has no idea if its argument is a Vector_container, a List_container,or some other kind of container; it doesn’t need to know. It can use any kind of Container. It knowsonly the interface defined by Container. Consequently, use(Container&) needn’t be recompiled if theimplementation of List_container changes or a brand-new class derived from Container is used.The flip side of this flexibility is that objects must be manipulated through pointers or references(§3.3, §20.4).3.2.3 Virtual FunctionsConsider again the use of Container:void use(Container& c){const int sz = c.size();for (int i=0; i!=sz; ++i)cout << c[i] << '\n';}How is the call c[i] in use() resolved to the right operator[]()? When h() calls use(), List_container’soperator[]() must be called.