《C++ Primer第五版》读书笔记(17)-Specialized Tools and Techniques
19.1 Controlling Memory Allocation
Some applications have specialized memory allocation needs that cannot be met by the standard memory management facilities. They can overload the new and delete operators to control memory allocation.
When we define the global operator new and operator delete functions, we take over responsibility for all dynamic memory allocation. These functions must be correct: They form a vital part of all processing in the program.
The operator new and operator delete Interface
The library defines eight overloaded versions of operator new and delete functions. The first four support the versions of new that can throw a bad_alloc exception. The next four support nonthrowing versions of new:// these versions might throw an exception
void *operator new(size_t); // allocate an object
void *operator new[](size_t); // allocate an array
void *operator delete(void*) noexcept; // free an object
void *operator delete[](void*) noexcept; // free an array
// versions that promise not to throw; see § 12.1.2 (p. 460)
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;
The malloc and free Functions
If you define your own global operator new and operator delete, those functions must allocate and deallocate memory somehow. Even if you define these functions in order to use a specialized memory allocator, it can still be useful for testing purposes to be
able to allocate memory similarly to how the implementation
To this end, we can use functions named malloc and free that C++ inherits from C. These functions, are defined in cstdlib.
19.1.2 Placement new Expressions
In earlier versions of the language—before the allocator(§ 12.2.2, p. 481) class was part of the library—applications that wanted to separate allocation from initialization did so by calling operator newand operator delete. These functions behave analogously
to the allocate and deallocate members of allocator. Like those members, operator new and operator delete functions allocate and deallocate memory but do not construct or destroy objects.
We use the placement new form of new to construct an object.
new (place_address) type
new (place_address) type(initializers)
new (place_address) type[size]
new (place_address) type[size] { braced initializer list}
When passed a single argument that is a pointer, a placement new expression constructs an object but does not allocate memory.
19.2 Run-Time Type Identification
Run-time type identification(RTTI) is provided through two operators:
•The typeid operator, which returns the type of a given expression
•The dynamic_cast operator, which safely converts a pointer or reference to a base type into a pointer or reference to a derived type
Ordinarily, we should use virtual functions if we can. When the operation is virtual, the compiler automatically selects the right function according to the dynamic type of the object. However,it is not always possible to define a virtual. If we cannot use a virtual, we can use one of the RTTI operators. On the other hand, using these operators is more error-prone than using virtual member functions: The programmer must know to which type the object should be cast and must check that the cast was performed successfully.
19.2.1 The dynamic_cast Operator
A dynamic_cast has the following form:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
where type must be a class type and (ordinarily) names a class that has virtual functions. In the first case, emust be a valid pointer (§ 2.3.2, p. 52); in the second, e must be an lvalue; and in the third, emust not be an lvalue.
In all cases, the type of e must be either a class type that is publicly derived from the target type, a public base class of the target type, or the same as the target type.
Pointer-Type dynamic_casts
if(Derived *dp = dynamic_cast<Derived*>(bp))
{
// use the Derived object to which dp points
} else { // bp points at a Base object
// use the Base object to which bp points
}
Reference-Type dynamic_casts
void f(const Base &b)
{
try {
const Derived &d = dynamic_cast<const Derived&>(b);
// use the Derived object to which b referred
} catch (bad_cast) {
// handle the fact that the cast failed
}
}
19.2.2 The typeid Operator
Derived *dp = new Derived;
Base *bp = dp; // both pointers point to a Derived object
// compare the type of two objects at run time
if (typeid(*bp) == typeid(*dp)) {
// bp and dp point to objects of the same type
}
// test whether the run-time type is a specific type
if (typeid(*bp) == typeid(Derived)) {
// bp actually points to a Derived
}
Note that the operands to the typeidare objects—we used *bp, not bp. The typeidof a pointer (as opposed to the object to which the pointer points) returns the static, compile-time type of the pointer:
// test always fails: the type of bp is pointer to Base
if (typeid(bp) == typeid(Derived)) {
// code never executed
}
19.2.3 The type_info Class
The exact definition of the type_info class varies by compiler. However, the standard guarantees that the class will be defined in the typeinfoheader and that the class will provide at least the operations listed in Table 19.1.
19.3 Enumerations
Enumerations let us group together sets of integral constants. Like classes, each enumeration defines a new type. Enumerations are literal types.
C++ has two kinds of enumerations: scoped and unscoped. The new standard introduced scoped enumerations. We define a scoped enumeration using the keywords enum class(or, equivalently, enum struct), followed by the enumeration name and a comma-separated list
of enumerators enclosed in curly braces. A semicolon follows the close curly:
enum class open_modes {input, output, append};
Here we defined an enumeration type named open_modes that has three enumerators: input, output, and append.
We define an unscoped enumeration by omitting the class(or struct) keyword. The enumeration name is optional in an unscoped enum:
enum color {red, yellow, green}; // unscoped enumeration
// unnamed, unscoped enum
enum {floatPrec = 6, doublePrec = 10, double_doublePrec =10};
enum color {red, yellow, green}; // unscoped enumeration
enum stoplight {red, yellow, green}; // error: redefines enumerators
enum class peppers {red, yellow, green}; // ok: enumerators are hidden
color eyes = green; // ok: enumerators are in scope for an unscoped enumeration
peppers p = green; // error: enumerators from peppers are not in scope
// color::green is in scope but has the wrong type
color hair = color::red; // ok: we can explicitly access the enumerators
peppers p2 = peppers::red; // ok: using red from peppers
By default, enumerator values start at 0 and each enumerator has a value 1 greater than the preceding one. However, we can also supply initializers for one or more enumerators:
enum class intTypes {
charTyp = 8, shortTyp = 16, intTyp = 16,longTyp = 32, long_longTyp = 64
};
When we omit an initializer, the enumerator has a value 1 greater than the preceding enumerator.
Like Classes, Enumerations Define New Types
So long as the enumis named, we can define and initialize objects of that type.
An enum object may be initialized or assigned only by one of its enumerators or by another object of the same enum type:
open_modes om = 2; // error: 2 is not of type open_modes
om = open_modes::input; // ok: input is an enumerator of open_modes
Objects or enumerators of an unscoped enumeration type are automatically converted to an integral type. As a result, they can be used where an integral value is required:
int i = color::red; // ok: unscoped enumerator implicitly converted to int
int j = peppers::red; // error: scoped enumerations are not implicitly converted
Specifying the Size of an enum
Although each enumdefines a unique type, it is represented by one of the built-in integral types.Under the new standard, we may specify that type by following the enum name with a colon and the name of the type we want to use:
enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535,
longTyp = 4294967295UL,
long_longTyp = 18446744073709551615ULL
};
If we do not specify the underlying type, then by default scoped enums have int as the underlying type. There is no default for unscoped enums; all we know is that the underlying type is large enough to hold the enumerator values.
Being able to specify the underlying type of an enumlets us control the type used across different implementations. We can be confident that our program compiled under one implementation will generate the same code when we compile it on another.
Forward Declarations for Enumerations
Parameter Matching and Enumerations
Because an object of enumtype may be initialized only by another object of that enum type or by one of its enumerators (§ 19.3, p. 833), an integral value that happens to have the same value as an enumerator cannot be used to call a function expecting an enum
argument.
From “Thinking in C++”:
Enumeration Types的引入目的:提供一种方法,它不仅能够定义而且能够将一系列整形常量放在一个组内,这可以避免无法控制值的范围的问题。例如在函数入参或者返回值时,我们说它只有那么几个入参或者几个返回值,为了保证其不会超过这个范围,那么最好的办法就是将入参或者返回值设置为enumeration对象。
特性(与其引入目的紧密相关):一个Enumeration对象只能用另一个相同的enumeration对象或者其enumerator set中的一个来初始化或者赋值,而不能使用int型的literal constant 直接为其赋值。但是如果需要,它可以转换为int类型。
19.4 Pointer to Class Member
A pointer to member is a pointer that can point to a nonstatic member of a class. Normally a pointer points to an object, but a pointer to member identifies a member of a class, not an object of that class.
static class members are not part of any object, so no special syntax is needed to point to a staticmember. Pointers to staticmembers are ordinary pointers.
19.4.1 Pointers to Data Members
class Screen {
public:
typedef std::string::size_type pos;
char get_cursor() const { return contents[cursor]; }
char get() const;
char get(pos ht, pos wd) const;
private:
std::string contents;
pos cursor;
pos height, width;
};
// pdata can point to a string member of a const (or non const) Screen object
const string Screen::*pdata;
pdata= &Screen::contents;
Of course, under the new standard, the easiest way to declare a pointer to member is to use auto or decltype:
auto pdata = &Screen::contents;
Using a Pointer to Data Member
Screen myScreen, *pScreen = &myScreen;
// .* dereferences pdata to fetch the contents member from the object myScreen
auto s = myScreen.*pdata;
// ->* dereferences pdata to fetch contents from the object to which pScreen points
s = pScreen->*pdata;
19.4.2 Pointers to Member Functions
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;
// pmf points to a Screen member that takes no arguments and returns char
pmf = &Screen::get; // must explicitly use the address-of operator
pmf = Screen::get; // error: no conversion to pointer for member functions
Using a Pointer to Member Function
Screen myScreen,*pScreen = &myScreen;
// call the function to which pmf points on the object to which pScreen points
char c1 = (pScreen->*pmf)();
// passes the arguments 0, 0 to the two-parameter version of get on the object myScreen
char c2 = (myScreen.*pmf2)(0, 0);
Using Type Aliases for Member Pointers
Type aliases make code that uses pointers to members much easier to read and write.
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get; // get points to the get member of Screen
// action takes a reference to a Screen and a pointer to a Screen member function
Screen& action(Screen&, Action = &Screen::get);
Screen myScreen;
// equivalent calls:
action(myScreen); // uses the default argument
action(myScreen, get); // uses the variable get that we previously defined
action(myScreen, &Screen::get); // passes the address explicitly
Pointer-to-Member Function Tables
One common use for function pointers and for pointers to member functions is to store them in a function table (§ 14.8.3, p. 577). For a class that has several members of the same type, such a table can be used to select one from the set of these members.
19.4.3 Using Member Functions as Callable Objects
Unlike ordinary function pointers, a pointer to member is nota callable object; these pointers do not support the function-call operator
Using function to Generate a Callable
One way to obtain a callable from a pointer to member function is by using the library function template:
function<bool(const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
Here we tell function that empty is a function that can be called with a string and returns a bool.
Using mem_fn to Generate a Callable
To use function, we must supply the call signature of the member we want to call. We can, instead, let the compiler deduce the member’s type by using another library facility, mem_fn, which, like function, is defined in the functional header.
find_if(svec.begin(),svec.end(), mem_fn(&string::empty));
Using bind to Generate a Callable
// bind each string in the range to the implicit first argument to empty
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
19.5 Nested Classes
A class can be defined within another class. Such a class is a nested class, also referred to as a nested type. Nested classes are most often used to define implementation classes.
The name of a nested class is visible within its enclosing class scope but not outside the class.
Declaring a Nested Class
class TextQuery {
public:
class QueryResult; // nested class to be defined later
// other members as in § 12.3.2 (p. 487)
};
Defining a Nested Class outside of the Enclosing Class
// we're defining the QueryResult class that is a member of class TextQuery
class TextQuery::QueryResult {
…
};
Defining the Members of a Nested Class
TextQuery::QueryResult::QueryResult(string s,shared_ptr<set<line_no>> p,shared_ptr<vector<string>> f):sought(s), lines(p), file(f) { }
The Nested and Enclosing Classes Are Independent
19.6 union: A Space-Saving Class
Defining a union
// objects of type Token have a single member, which could be of any of the listed types
union Token {
// members are public by default
char cval;
int ival;
double dval;
};
Using a unionType
Token first_token = {'a'}; // initializes the cval member
Token last_token; // uninitialized Token object
Token *pt = new Token; // pointer to an uninitialized Token object
19.7 Local Classes
A class can be defined inside a function body. Such a class is called a local class. All members, including functions, of a local class must be completely defined inside the class body. As a result, local classes are much less useful than nested classes.
In practice, the requirement that members be fully defined within the class limits the complexity of the member functions of a local class. Functions in local classes are rarely more than a few lines of code. Beyond that, the code becomes difficult for the
reader to understand.
19.8 Inherently Nonportable Features
To support low-level programming, C++ defines some features that are inherently nonportable. A nonportable feature is one that is machine specific. Programs that use nonportable features often require reprogramming when they are moved from one machine to another.
19.8.1 Bit-fields
A class can define a (nonstatic) data member as a bit-field. A bit-field holds a specified number of bits. Bit-fields are normally used when a program needs to pass binary data to another program or to a hardware device.
Ordinarily it is best to make a bit-field an unsignedtype. The behavior of bit-fields stored in a signed type is implementation defined.
19.8.2 volatile Qualifier
The precise meaning of volatile is inherently machine dependent and can be understood only by reading the compiler documentation. Programs that use volatile usually must be changed when they are moved to new machines or compilers.
volatile对于单片机开发等技术相当重要。编译器在优化代码时,看到该寄存器一直没有再次被写入过,就会认为该变量是最新的,直接从该寄存器区数读据;但是增加了volatile后,它每次都会重新读取数据。它的用法与const完全相同,也可以用于变量、指针、函数参数与返回值、类等。而且volatile与const是可以一起使用的,这种用法并没有任何矛盾之处。
19.8.3 Linkage Directives: extern "C"
C++ programs sometimes need to call functions written in another programming language. Most often, that other language is C. Like any name, the name of a function written in another language must be declared. As with any function, that declaration must specify
the return type and parameter list. The compiler checks calls to functions written in another language in the same way that it handles ordinary C++ functions.
However, the compiler typically must generate different code to call functions written in other languages. C++ uses linkage directives to indicate the language used for any non-C++ function.
Declaring a Non-C++ Function
// illustrative linkage directives that might appear in the C++ header <cstring>
// single-statement linkage directive
extern "C" size_t strlen(const char *);
// compound-statement linkage directive
extern "C" {
int strcmp(const char*, const char*);
char *strcat(char*, const char*);
}
Linkage Directives and Headers
When a #include directive is enclosed in the braces of a compound-linkage directive, all ordinary function declarations in the header file are assumed to be functions written in the language of the linkage directive.
// compound-statement linkage directive
extern "C" {
#include <string.h> // C functions that manipulate C-style strings
}
Preprocessor Support for Linking to C
To allow the same source file to be compiled under either C or C++, the preprocessor defines _ _cplusplus(two underscores) when we compile C++. Using this variable, we can conditionally include code when we are compiling C++:
#ifdef__cplusplus
// ok: we're compiling C++
extern "C"
#endif
int strcmp(const char*, const char*);
Overloaded Functions and Linkage Directives
// error: two extern "C" functions with the same name
extern "C" void print(const char*);
extern "C" void print(int);
链接:要使用一个库,首先需要包括该库的头文件,然后可以使用它的函数和变量。最后在link的时候,linker先检索源代码中是否有该声明的定义,如果有该声明的定义则直接连接进去,否则根据顺序到指定的库中去查找该函数的定义,并将该函数定义部分的代码放入要生成的可执行程序中,注意仅仅是该部分定义,而不是整个库。库会存有对该函数的索引,所以不需要在整个库中搜索,而是先在该索引中定位。这就是说,你可以定一个与库中函数一样的函数,编译到工程中的时候,它有较高的优先级,所以它能替代库中的函数代码段。