PS,1880后程序员

看不完的牙,写不完的程序,跑不完的步。
随笔 - 97, 文章 - 34, 评论 - 10, 引用 - 0
数据加载中……

C++ Primer 之 读书笔记 第十八章 特殊工具与技术

 

Chapter 18. 特殊工具与技术

 

18.1 优化内存分配Optimizing Memory Allocation

分配内存和创建对象是两个不同的概念。分配内存,并不一定会立即创建对象。同样,内存释放和对象撤销也是两个不同的概念。

 

18.1.1. C++ 中的内存分配

new操作:给指定的类型分配内存并在新分配的内存上构造指定类型的对象。

在未构造的内存上给对象赋值,而不是初始化这个对象,这种赋值是未定义的。对很多类来说,这样做会导致运行期崩溃。赋值包括删除现有的对象,如果不存在现有对象,那么这个赋值操作就会导致灾难性的结果。Assigning to an object in unconstructed memory rather than initializing it is undefined. For many classes, doing so causes a crash at run time. Assignment involves obliterating the existing object. If there is no existing object, then the actions in the assignment operator can have disastrous effects.

C++提供两种方法分配和释放未构造的内存

  1. allocator
  2. newdelete操作符

C++提供两种方法创建和销毁对象:

  1. allocator
  2. new操作符
  3. 直接调用对象的析构函数(但是此时只是把对象占用的内存变成了为构造内存,但是这个内存并没有释放哦)
  4. uninitialized_fill and uninitialized_copy,是拷贝不是赋值

18.1.2. allocator

这是一个模板类,提供类型化的内存分配,对象创建和撤销。它把内存分配和对象创建这两件事情分开来。当一个allocator对象分配内存时间,它只是根据给定的类型分配出空间,这个空间的大小可以用来保存给定类型的对象。但是这时,所分配的空间还没有构建。这就相当于预先分配preallocation

allocator<T> a;

定义allocator对象a,按类型T分配内存。

a.allocate(n)

分配未构造内存,这个内存的大小是n个类型T的对象

使用allocator管理类成员数据

这里是用vector类来举例说明。从vector的工作机制中可以看到:如果没有空闲的单元,vector重新分配内存。这样获得新的空间后,vector拷贝当前现有的对象到新的内存空间,然后释放旧的内存空间。If there isn't a free element, then the vector is reallocated: The vector obtains new space, copies the existing elements into that space, adds the new element, and frees the old space.因此从这个工作机理中可以看出vector的效率是比较差的。

下面的这段代码很好的说明了alloctor是如何管理类成员数据的。

  1. allocator分配内存空间一定是和类型相关的。因此在Vector里面定义了静态成员alloc

static std::allocator<T> alloc; // object to get raw memory

  1. alloc.allocate()返回值是一个指定类型的指针。
  2. 先调用析构函数alloc.destroy(),再释放空间alloc.deallocate()

template <class T> void Vector<T>::reallocate()

 {

     std::ptrdiff_t size = first_free - elements;

     std::ptrdiff_t newcapacity = 2 * max(size, 1);

     // allocate space to hold newcapacity number of elements of type T

     T* newelements = alloc.allocate(newcapacity);

     // construct copies of the existing elements in the new space

     uninitialized_copy(elements, first_free, newelements);

     // destroy the old elements in reverse order

     for (T *p = first_free; p != elements; /* empty */ )

        alloc.destroy(--p);

     // deallocate cannot be called on a 0 pointer

     if (elements)

         // return the memory that held the elements

         alloc.deallocate(elements, end - elements);

     // make our data structure point to the new elements

     elements = newelements;

     first_free = elements + size;

     end = elements + newcapacity;

 }

18.1.3. operator new 函数和 operator delete 函数

operator newoperator delete和标准库中的其它操作符有些不同,它们是不能重载的。

operator newoperator delete有两种重载版本:

void *operator new(size_t);       // allocate an object

void *operator new[](size_t);     // allocate an array

void *operator delete(void*);     // free an object

void *operator delete[](void*);   // free an array

可以用operator newoperator delete模拟allocator

  • 分配内存空间

T* newelements = alloc.allocate(newcapacity);

改编成

T* newelements = static_cast<T*>(operator new[](newcapacity * sizeof(T)));

  • 释放内存空间

alloc.deallocate(elements, end - elements);

改编成

operator delete[](elements);

一般而言,使用 allocator 比直接使用 operator new operator delete 函数更为类型安全。In general, it is more type-safe to use an allocator rather than using the operator new and operator delete functions directly.

18.1.4. 定位 new 表达式Placement new Expressions

定位new表达式实际上就是要实现在已经分配的内存空间上构造对象的功能。

基本形式:

new (place_address) type

new (place_address) type (initializer-list)

对比construct

alloc.construct(first_free, t);

等价于:

new (first_free) T(t);

·         定位new表达式更加灵活。通过下面的Example可以看出二者之间的区别:

allocator<string> alloc;

string *sp = alloc.allocate(2); // allocate space to hold 2 strings

// two ways to construct a string from a pair of iterators

new (sp) string(b, e);                    // construct directly in place

alloc.construct(sp + 1, string(b, e));   // build and copy a temporary

1.                当定位new表达式初始化一个对象时,它使用构造函数,直接创建对象。When placement new initializes an object, it can use any constructor, and builds the object directly.

2.                alloc.construct()函数总是使用拷贝构造函数The construct function always uses the copy constructor.

·         从性能角度考虑,alloc.construct()函数总是要构造临时对象然后再拷贝它。

·         另外有些类是不支持copy构造函数的,这种情况下就只能用定位new了。

18.1.5. 显式析构函数的调用(Explicit Destructor Invocation)

既然可以通过定位new调用构造函数,那么对应于此,还可以通过显示调用析构函数来取消对象。

调用操作符delete不会执行析构函数,而只是释放所指向的内存。Calling the operator delete function does not run the destructor; it only frees the indicated memory.

18.1.6. 类特定的 new deleteClass Specific new and delete

这部分要说明的是类如何优化new表达式的行为(optimizing the behavior of new expressions.)。

这件事的理论依据是,类通过定义自己的成员,操作符newdelete,就可以管理其对象使用的内存。A class may manage the memory used for objects of its type by defining its own members named operator new and operator delete.

当编译器看到类类型的newdelete表达式的时候,它先看这个类是不是定义了操作符newdelete成员。如果类定义自己的这两个操作符函数,编译器就直接调用这些函数分配和释放对象的内存。否则,编译器调用标准库里的newdelete版本。When the compiler sees a new or delete expression for a class type, it looks to see if the class has a member operator new or operator delete. If the class defines (or inherits) its own member new and delete functions, then those functions are used to allocate and free the memory for the object. Otherwise, the standard library versions of these functions are called.

newdelete成员函数

重载newdelete包括一些限定条件:

  1. 首先必须是成对出现,就是说如果重载new,也必须重载deleteIf a class defines either of these members, it should define both of them.
  2. 必须定义为静态函数static

L需要复习下下virtual析构函数,不virtual那又怎么样?

当一个基类指针指向的是一个派生类的对象时,如果析构函数是virtual的,那么编译器在运行期动态绑定到底调用哪个类的析构函数。但是如果析构函数没有声明为virtual,编译器在编译期就确定调用基类的析构函数,这就是静态绑定

new

返回值类型是void*,形参类型是size_t

delete

返回值类型是void,形参类型是一个参数的void*,或者是两个参数的void*size_t

不得不说这部分讲的太简单了,只好从Google上再找些资料学习哈。

定义:

#include <malloc.h>

struct base

{

    base()

    {

        throw int(3);

    }

    ~base() {}

    void* operator new( size_t nsize, const char*,int)

    {

        void* p = malloc( nsize );

        return p;

    } 

    void operator delete( void *p)

    {

        free(p);

    }

    void operator delete( void* p,const char*,int)

    {

        free( p );

    }

};

调用:

nt main( void )

{

    base* p = null;

    try

    {

        p = new base;

        delete p;

    }

    catch(...)

    {

    }

    return 0;

}

数组操作符new[]delete[]

覆盖类特定的内存分配(Overriding Class-Specific Memory Allocation

如何强制调用全局的操作符newdelete

Type *p = ::new Type; // uses global operator new

::delete p;           // uses global operator delete

18.1.7. 一个内存分配器基类(A Memory-Allocator Base Class)

这个部分就是这一节所讲内容的一个完整的实例。

CachedObj可以看做是一种freelist

freelist是啥?Google了一下下,KAO,原来有N多种freelist的实现。这是一种内存管理技术,包括预先分配没有构造对象的内存,对象只在需要时在这些内存上创建。当对象释放时,它们所占用的内存返回给预先分配的内存而不是直接返回给系统。Memory management technique that involves preallocating unconstructed memory to hold objects that will be created as needed. When objects are freed, their memory is put back on the free list rather than being returned to the system.

定义代码:

template <class T> class CachedObj {

 public:

      void *operator new(std::size_t);

      void operator delete(void *, std::size_t);

      virtual ~CachedObj() { }

 protected:

      T *next;

 private:

      //add_to_freelist的作用就是想freelist中添加对象

      static void add_to_freelist(T*);

      //静态成员负责管理freelist,对于每种类型只需要一个freelist

      static std::allocator<T> alloc_mem;

      //freestor就是指向freelist表头的指针。

      static T *freeStore;

      //chunk是一次预分配内存空间的大小。

      static const std::size_t chunk;

 };

使用CachedObj - 派生类定义

1):

class Screen: public CachedObj<Screen> {

   // interface and implementation members of class Screen are unchanged

 };

2):

template <class Type>

 class QueueItem: public CachedObj< QueueItem<Type> > {

   // remainder of class declaration and all member definitions unchanged

 };

分配(Allocation)如何工作

new表达式:QueueItem<Type> *pt = new QueueItem<Type>(val);

  1. 使用QueueItem<T>::operator newfreelist为对象分配空间。
  2. 在分配的空间上,使用类型Tcopy构造函数构建对象。

deletedelete pt;

  1. 执行QueueItem的析构函数。
  2. 把对象所占用的内存返还给freelist

定义操作符new

功能:从freelist里获得一个对象的内存。

template <class T>

 void *CachedObj<T>::operator new(size_t sz)

 {

       // 链表中的数据类型必须一致

       if (sz != sizeof(T))

           throw std::runtime_error

            ("CachedObj: wrong size object in operator new");

       if (!freeStore) {

           // 链表为空,分配新的内存

           T * array = alloc_mem.allocate(chunk);

           // 把新分配的内存单元追加到freelistfreelist是个链表,所以把数组转换成链表,这样每个对象的next指针都指向下一个对象

           for (size_t i = 0; i != chunk; ++i)

                 add_to_freelist(&array[i]);

       }

       // freestore总是指向下一个有效的单元

       T *p = freeStore;

       freeStore = freeStore->CachedObj<T>::next;

       return p;   // constructor of T will construct the T part of the object

 }

定义delete操作符

功能:就是要把对象占用的内存还给freelist

template <class T> void CachedObj<T>::operator delete(void *p, size_t)

 {

     if (p != 0)

         // put the "deleted" object back at head of freelist

         add_to_freelist(static_cast<T*>(p));

 }

add_to_freelist成员

template <class T> void CachedObj<T>::add_to_freelist(T *p)

 {

    //这是一个小技巧,为的是避免调用派生类的next成员(如果存在)

    p->CachedObj<T>::next = freeStore;

    freeStore = p;

 }

18.2 运行期类型识别Run-Time Type Identification

通过两个操作符提供RTTI

  1. typeid:返回指针或者引用指向的对象的实际类型。
  2. dynamic_cast:把指向基类的对象的指针或者引用转换成派生类的指针或引用。

对于具有虚函数的类,RTTI在运行期执行;对于其它的类型是在编译期执行的。The RTTI operators execute at run time for classes with virtual functions, but are evaluated at compile time for all other types.

dynamic_cast:动态强制类型转换

使用动态强制类型转换要小心。在任何可能的情况下,定义和使用虚函数比直接接管类型管理好得多。Dynamic casts should be used with caution. Whenever possible, it is much better to define and use a virtual function rather than to take over managing the types directly.

18.2.1. dynamic_cast 操作符

dynamic_cast操作符实际上执行了两种操作:

  1. 验证要执行的强制类型转换是不是有效。It begins by verifying that the requested cast is valid.
  2. 只有当强制类型转换有效时,操作符才执行实际的转换操作。Only if the cast is valid does the operator actually do the cast.

使用dynamic_cast 操作符

大师给出了dynamic_cast操作符的使用方法,并例举出这样做的三大好处,总而言之就是尽量把代码出错的几率降到最小。

if (Derived *derivedPtr = dynamic_cast<Derived*>(basePtr))

 {

     // use the Derived object to which derivedPtr points

 } else { // BasePtr points at a Base object

     // use the Base object to which basePtr points

 }

在条件语句中使用dynamic_cast 操作符保证强制转换以及转换结果测试在一个表达式中。Performing a dynamic_cast in a condition ensures that the cast and test of its result are done in a single expression.

这看上去很简单,但是很重要,因为这样可以降低程序出错的概率。

使用 dynamic_cast 和引用类型

对于引用类型的强制转换,和指针的略有不同,这是因为引用不能是空null

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

    }

 }

18.2.2. typeid 操作符

typeid操作符的操作数不是类类型或者是类类型但是不包含虚函数,typeid操作符指定的是操作数的静态类型。当操作数是定义了至少一个虚函数的类类型时,类型是在运行期计算出来的。When the operand is not of class type or is a class without virtual functions, then the typeid operator indicates the static type of the operand. When the operand has a class-type that defines at least one virtual function, then the type is evaluated at run time.

使用typeid操作符

  •  

Base *bp;

 Derived *dp;

 // compare type at run time of two objects

 if (typeid(*bp) == typeid(*dp)) {

     // bp and dp point to objects of the same type

 }

  •  

// test whether run time type is a specific type

 if (typeid(*bp) == typeid(Derived)) {

     // bp actually points to a Derived

 }

18.2.3. RTTI 的使用

这是一个RTTI使用的简单的例子:

问题提出:如何定义基类、派生类的相等操作符

相等的含义是:

(1) 类型相同

(2) 指定数据成员的值相同

愚蠢的方法是针对每个派生类的组合都去重载操作符“==”。至于多愚蠢想象一下就够了哦J

18.2.4. type_info

  • 这个类和编译器相关,不同的编译器对这个类的定义可能存在差异
  • type_info类的构造函数和拷贝各种哦函数都是私有的private,唯一获得type_info对象的方法就是通过typeid操作符。
  • 成员函数name的返回值依赖于编译器。但是对于同一个类型,它的返回值是唯一的。这句话的含义是代码中是不能出现if(typeid(obj).name()==”string”)这样的代码,因为不同的编译器name函数的返回值是不一样的。

std::string obj;

if(typeid(obj).name()==”string”)     //Error

if(typeid(obj)== typeid(std::string) ) //OK

18.3 类成员指针Pointer to Class Member

定义

指向数据成员的指针

string Screen::*ps_Screen = &Screen::contents;

指向成员函数的指针

char (Screen::*pmf)() const = &Screen::get;

pmf是一个指向Screen类的无形参的get函数的指针。

Screenget函数的定义:

char get() const;

char get(index ht, index wd) const;

带形参的定义:

char (Screen::*pmf2)(Screen::index, Screen::index) const;

pmf2 = &Screen::get;

使用typedef为成员指针定义别名

// Action is a type name

typedef char (Screen::*Action)(Screen::index, Screen::index) const;

这样上面的定义就可以简化成:

Action get = &Screen::get;

使用类成员指针

类成员指针说来说去的,其实还是指针,是指针,就对应有解引用操作(*)和箭头操作(->)

使用指向成员函数的指针

// pmf points to the Screen get member that takes no arguments

char (Screen::*pmf)() const = &Screen::get;

Screen myScreen;

char c1 = myScreen.get();      // call get on myScreen

char c2 = (myScreen.*pmf)();   // equivalent call to get

Screen *pScreen = &myScreen;

c1 = pScreen->get();     // call get on object to which pScreen points

c2 = (pScreen->*pmf)(); // equivalent call to get

使用指向数据成员的指针

Screen::index Screen::*pindex = &Screen::width;

Screen myScreen;

// equivalent ways to fetch width member of myScreen

Screen::index ind1 = myScreen.width;      // directly

Screen::index ind2 = myScreen.*pindex;    // dereference to get width

Screen *pScreen;

// equivalent ways to fetch width member of *pScreen

ind1 = pScreen->width;        // directly

ind2 = pScreen->*pindex;      // dereference pindex to get width

应用

成员指针的一个具体的应用就是成员指针函数列表(Pointer-to-Member Function Tables)。说来也很简单,就是把函数指针保存成Array,根据下标来索引调用哪一个函数。因为这些函数被定义成了类成员函数,这就用到了成员函数指针。这也算是一种典型的应用了。

18.4 内嵌类Nested Classes

这内嵌类实际上是在它的外围类enclosing class里定义了一种新的类型成员。A nested class defines a type member in its enclosing class.

哦。。。这让我想到的是Java中的内部类,这两个东西是不是类似L

18.4.1 实现内嵌类

定义内嵌类

template <class Type> class Queue {

     // interface functions to Queue are unchanged

 private:

     // public members are ok: QueueItem is a private member of Queue

     // 只有Queue和它的友元可以访问QueueItem

     struct QueueItem {

         QueueItem(const Type &);

         Type item;            // value stored in this element

         QueueItem *next;      // pointer to next element in the Queue

     };

     QueueItem *head;      // pointer to first element in Queue

     QueueItem *tail;      // pointer to last element in Queue

 };

定义内嵌类成员

在哪里定义?

  1. 必须和外围类的定义在同一个作用域里。
  2. 如果内嵌类的成员在自身类的外面定义,那么她是不能定义在外围类里的。很好理解哦,内嵌类的成员不是外围类的成员嘛。

下面这个例子(噢噢,不是花生),就是定义QueueItem的构造函数:

// defines the QueueItem constructor

// for class QueueItem nested inside class Queue<Type>

//这是QueueItem的构造函数定义它是内嵌在Queue<Type>作用域的

template <class Type>

 Queue<Type>::QueueItem::QueueItem(const Type &t): item(t), next(0) { }

在外围类之外定义内嵌类

定义Queue类,只要前向声明QueueItem是内嵌类。

template <class Type> class Queue {

 // interface functions to Queue are unchanged

 private:

      struct QueueItem; // forward declaration of nested type QueueItem

      QueueItem *head; // pointer to first element in Queue

      QueueItem *tail; // pointer to last element in Queue

 };

在另一个文件中定义QueueItem,注意一定要用限定struct Queue<Type>

 template <class Type>

 struct Queue<Type>::QueueItem {

      QueueItem(const Type &t): item(t), next(0) { }

      Type item;        // value stored in this element

      QueueItem *next; // pointer to next element in the Queue

 };

内嵌模板实例化

当外围类模板实例化时,内嵌类是不会自动实例化的。A nested class of a class template is not instantiated automatically when the enclosing class template is instantiated.内嵌类只有在上下文中用到时才实例化。就是说:Queue<int> qi;只是实例化了Queue<int>而没有实例化 QueueItem<int>

18.4.2 内嵌类作用域的名字查找

当处理类成员声明时,。当处理定义时,完整的内嵌类或者外围类必须在同一作用域里。When processing the declarations of the class members, any name used must appear prior to its use. When processing definitions, the entire nested and enclosing class(es) are in scope.

18.5 联合Union

定义Union

union TokenValue {

     char   cval;

     int    ival;

     double dval;

 };

限制条件:

  1. 不能包含有静态成员。
  2. 不能包含引用。
  3. 不能包含那些具有构造函数、析构函数和赋值操作符的类类型的对象。
  4. 不能包含有虚函数。

使用

定义

// 初始化TokenValue,但是只能为第一个成员使用初始化式

TokenValue first_token = {'a'}; 

// 未初始化TokenValue对象

TokenValue last_token;         

// 定义指向TokenValue对象的指针

TokenValue *pt = new TokenValue;

使用

last_token.cval = 'z';

pt->ival = 42;

嵌套Union

class Token {

 public:

     // indicates which kind of value is in val

     enum TokenKind {INT, CHAR, DBL};

     TokenKind tok;

     union {             // unnamed union

         char   cval;

         int    ival;

         double dval;

     } val;              // member val is a union of the 3 listed types

 };

匿名Union

因为匿名的Union没有提供访问其成员的方法,因此Union的成员作为定义的作用域的一部分直接访问。Because an anonymous union provides no way to access its members, the members are directly accessible as part of the scope where the anonymous union is defined.

class Token {

 public:

     // indicates which kind of token value is in val

     enum TokenKind {INT, CHAR, DBL};

     TokenKind tok;

     union {                 // anonymous union

         char   cval;

         int    ival;

         double dval;

     };

 };

访问:

Token token;

 switch (token.tok) {

 case Token::INT:

      token.ival = 42; break;

 case Token::CHAR:

      token.cval = 'a'; break;

 case Token::DBL:

      token.dval = 3.14; break;

 }

18.6 局部类Local Classes

跳过,跳过。。。

18.7固有的不可移植的特征( Inherently Nonportable Features

portable:可移植的。

这一节所涉及的内容:

·         如何更加容易的和硬件接口:位域和volatile

·         如何更加容易的于其它语言的程序接口:链接指示linkage directives

18.7.1. 位域

位域在内存中的存储和机器相关。The layout in memory of a bit-field is machine-dependent.

typedef unsigned int Bit;

 class File {

     Bit mode: 2;

     Bit modified: 1;

     Bit prot_owner: 3;

     Bit prot_group: 3;

     Bit prot_world: 3;

     // ...

 };

关于位域有很多特殊性,罗列出来:

  1. 位域最好是无符号类型。it is best to make a bit-field an unsigned type.
  2. 地址操作符(&)不能应用于位域,因此也就不存在有指向位域的指针。The address-of operator (&) cannot be applied to a bit-field
  3. 位域也不能是类的静态成员。Nor can a bit-field be a static member of its class.
  4. 超过一个比特的位域通常要使用内置的位操作符来控制。Bit-fields with more than one bit are usually manipulated using the built-in bitwise operators

enum { READ = 01, WRITE = 02 }; // File modes

 int main() {

     File myFile;

     myFile.mode |= READ; // set the READ bit

     if (myFile.mode & READ) // if the READ bit is on

         cout << "myFile.mode READ is set"n";

 }

18.7.2. volatile 限定符

当一个对象是通过编译器控制或者检测以外的方式修改,这个对象就声明为volatile 。这意味着编译器不会对这个对象进行优化。An object should be declared volatile when its value might be changed in ways outside either the control or detection of the compiler.

volatile Task *curr_task; //执行volatile Task对象的指针

volatile int ixa[max_size]; 

volatile Screen bitmap_buf; //数据成员都是volatile

罗列出噩梦般的N多种写法的含义L

volatile int v;     // v is a volatile int

int *volatile vip; // vip是指向intvolatile指针

volatile int *ivp; // ivp是指向volatile int的指针

volatile int *volatile vivp;// vivp是指向volatile intvolatile指针

int *ip = &v; // error: must use pointer to volatile

*ivp = &v;    // ok: ivp is pointer to volatile

vivp = &v;    // ok: vivp is volatile pointer to volatile

18.7.3. 链接指示: extern "C"        

目的是为了解决程序调用其它语言的程序的函数。

C++使用连接指示linkage directives来指明非C++函数的语言。

声明非C++函数

// 单个声明

extern "C" size_t strlen(const char *);

// 复合声明

extern "C" {

    int strcmp(const char*, const char*);

    char *strcat(char*, const char*);

}

导出C++函数到其它语言

extern "C" double calc(double dparm) { /* ... */ }

重载函数和链接指示

这取决于编程语言是不是支持重载,如果是C,不支持重载,那当然就不行喽。

在一组重载函数中只能为一个 C 函数指定链接指示。a linkage directive can be specified only for one C function in a set of overloaded functions.

// error: two extern "C" functions in set of overloaded functions

extern "C" void print(const char*);

extern "C" void print(int);

函数指针

声明:

注意:因为这是一个C指针,因此它是不能指向C++函数的。A pointer to a C function does not have the same type as a pointer to a C++ function.

//pf是一个指向C函数的指针,函数的返回值是void;形参是int

extern "C" void (*pf)(int);

posted on 2009-08-28 08:35 amenglai 阅读(657) 评论(0)  编辑  收藏 所属分类: C++ Primer 之 读书笔记


只有注册用户登录后才能发表评论。


网站导航: