C++_Primer_学习笔记_第十九章(特殊工具和技术)

第十九章(特殊工具与技术)

/1.控制内存分配

1).不能直接应用标准内存管理机制。

  • 某一些应用程序需要自定义内存分配的的细节,比如使用关键字new将对象放置在特定的内存空间中。
  • 为了实现这一个目的,需要重载newdelete运算符,来控制内存分配的过程。

//1.重载new和delete

1).尽管说是重载,但是重载它们和重载其他的运算符过程大不相同。
2).了解newdelete表达式的工作机理。

{
    // ---------------new
    string *sp = new string("a value");//分配初始化一个string对象
    string *arr = string[10];//分配并默认初始化10个string对象
    // 实际的操作执行了三部
    // 1.   new表达式调用一个名为operator new或者operator new[]的标准库函数,该函数分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象或者对象的数组
    // 2.   编译器运行相应的构造函数以构造这些对象,并传入初始值
    // 3.   对象被分配了空间并且构造完成,返回指向该对象的指针

    // --------------delete
    delete sp;//销毁*sp,然后释放sp指向的内存空间
    delete [] arr;//销毁数组中的元素,然后释放对应的内存空间
    // 实际执行了两步操作
    // 1.   执行 sp指向的对象或者arr所指向数组中的元素 的析构函数
    // 2.   编译器调用名称为operator delete或者operator delete []的标准库函数释放内存空间。     

    // 如若应用程序希望控制内存分配的过程
    // 则需要定义自己的operator new和operator delete函数
    // 即使标准库已经存在这两个函数的定义,我们仍旧可以定义自己的版本
    // 编译器不会对这种重复定义提出异议,相反,编译器将会使用我们自定义的版本代替标准库的版本

    // 一旦自定义了全局的operator new和operator delete函数,我们就担负其控制动态内存分配的职责。
    // 这两个函数必须是正确的,它关乎程序处理过程的至关重要的一部分

    // 应用程序可以在全局作用域定义operator new和delete,也可以将它们定义为成员函数
    // 当编译器发现new或者delete表达式后,将在程序中查找可供调用的operator函数。
    // 如何查找?
    // 如果被分配(释放的)对象是类类型,则编译器首先在类及其基类的作用域中查找。(类的作用域。)
    // 此时如果该类含有operator new或者和delete函数成员,则相应的表达式掉用这些成员
    // 否则编译器会在全局作用域中查找匹配的函数,如若找到了用户自定义的版本,则使用该版本
    // 如果没有,执行标准库的版本

    // 我们可以使用作用域运算符,使new或者delete表达式忽略定义在类中的函数
    // 直接执行在全局中用于中的版本,
    // 使用作用域运算符就可以
    // ::new,::delete    
}

3).operator newoperator delete接口

{
    // 标准库定义了operator new函数operator delete函数的八个重载版本。
    // 前四个版本可能会抛出bad_alloc异常
    // 后四个不会抛出异常
    void* operator new(size_t);//分配一个对象
    void* operator new[](size_t);//数组
    // 析构函数不允许抛出异常。书本有误
    void* operator delete(void*) noexcept;//释放一个对象
    void* operator delete[](void*) noexcept;//数组

    // 
    void* operator new(size_t, nothrow_t&) noexcept;
    void* operator new[](size_t, nothrow&) noexcept;
    void* operator delete(size_t, nothrow&) noexcept;
    void* operator delete [] (size_t, nothrow&) noexcept;

    // 类型,nothrow_t是定义在头文件new中的一个struct
    // 这个类型不包含任何成员
    // new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new的非抛出版本
    // 与析构函数类似,operator delete也不允许抛出异常
    // 必须使用noexcept异常说明符号指定其不抛出异常

    // 应用程序可以自定义上面函数版本的任意一个,前提是自定义版本必须位于全局作用域或者类作用域。
    // 当我们将上述运算符函数定义成类的成员时,它们是隐式静态的
    // 我们无需显式地声明static,这样做也不会引发错误
    // 因为operator new用在对象构造之前
    // operator delete用在对象销毁之后
    // 所以这两个成员必须是静态的,而且
    // 不可以操纵任何数据成员
    
    //--------对于operator new或者new[],
    // 它们的返回值类型必须是void*,第一个形参类型必须是size_t,且该形参不能有默认实参
    // 当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参
    // 当调用[]时,传入函数的则是存储数组中所有元素所需要的空间

    // -----------如果我们想要自定一operator new函数,可以为他提供额外的形参
    // 此时,用到这些自定义函数的new表达式必须是使用new的定位模式,将实参传给新增的形参。
    // 尽管我们可以自定义具有任何形参的operator new
    // 但是,
    void* operator new(size_t, void*);
    // 只供给标准库使用,不能被用户重新定义

    // ------------对于operator delete或者delete[]
    // 它们的返回类型必须时void
    // 第一个形参必须是void*,执行一条delete表达式将调用相应的operator函数
    // 并用指向待释放内存的指针来初始化void*形参
    // 当我们将operator delete 或者delete[]定义成类的成员时,
    // 该函数可以包含另外一个类型为size_t的形参
    // 此时,该形参的初始值是第一个形参所指对象的字节数
    // size_t形参可用于删除继承体系中的对象
    // 如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别
    // 而且,实际运行的operator delete的函数版本也由对象的动态类型决定。    

    // ------------
    // operator new和operator delete和一般的operator函数不一样
    // 这两个函数并没有重载new表达式和delete表达式
    // 实际上,我们根本无法自定义new表达式或者delete表达式的行为
    // 它们的执行过程如之前所述,我们无法改变
    // 我们提供的operator new或者operator delete函数的目的在于,改变内存分配的方式。
}

4).malloc函数和free函数

  • 当我们定义了自己的全局operator newoperator delete后,这两个函数必须以某种方式执行分配内存与释放内存的操作。
  • 可能我们只是想要使用特殊定制的内存分配器,但是这两个函数还应该同时满足某一些测试的目的,即检验分配内存的方式是否与常规的方式类似?。
  • 我们可以是哟共名称为mallocfree的函数,这是从c语言中继承而来的函数,定义在头文件cstdlib
  • malloc,接受一个表示待分配字节数的size_t返回指向分配空间的指针,或者返回0表示分配空间失败。
  • free,接受一个void*,它是malloc返回指针的副本,free将相关的内存返回给系统。调用free(0)没有任何意义。
{
    // ------编写operator new
    void* operator new(size_t size) {
        if (void *mem = malloc(size))
            return mem;
        else 
            throw bad_alloc();
    }
    // -----------编写operator delete
    void operator delete(void *mem) noexcept {
        free(mem);
    }
}

练习

  • 19.2,使用自定义的operator new/delete代替,allocator类的操作。需要显式地指出。
  1. 默认情况下,allocator类使用operator new/delete来分配和释放内存空间。

//2.定位new表达式

1).普通代码调用标准库的两个普通函数,operator new,operator delete;。尽管它们一般用于new或者delete表达式。

  • 在c++的早期版本中,allocator类,还不是标准库的一部分。应用程序如果想要把内存分配和初始化分离开的话,需要调用operator new或者operator delete。这两个函数的行为和allocatorallocate成员和deallocate成员非常的类似。它们负责分配和释放内存空间,但是不会构造和销毁对象。
  • allocator不同的是,对于operator new分配的内存空间,我们无法使用construct函数构造对象。而是,我们应该是使用定位new形式来构造对象。construct和以下介绍的版本的定位new的功能很类似,都是用来构造一个对象的,但是实际上并不分配内存(与一般的new表达式不一样。)
  • 定位new为分配函数提供了额外的信息。
{
    // 我们可以使用定位new传递一个地址,此时定位new的形式如下
    new(place_address) type
    new(place_address) type (initializers)
    new(place_address) type [size]
    new(place_address) type [size] {braced initializer list}

    // 其中place_address必须是一个指针,同时在initializers中提供一个(可以为空)以逗号分割的初始值列表,该初始值列表将用于构造新分配的对象
    // 当仅通过一个地址值调用时,定位new使用operator new(size_t, void*)“分配”它的内存
    // 这是我们无法自定义的operator new版本
    // 该函数不分配任何内存,它只是简单地返回指针实参,然后由new表达式负责在指定的地址初始化对象以完成整个工作
    // 事实上,定位new允许我们在一个特定的,预先分配的内存上构造对象
    // 当只传入一个指针类型的实参时,定位new表达式构造对象但是不会分配内存。定位new只是用来构造对象。

    // ----------------定位new和construct的异同
    // 尽管很多时候使用定位new和allocator的construct成员非常类似,但是它们之间也有一个重要的区别
    // 我们传递给construct的指针必须指向同一个allocator对象分配的空间
    // 但是传递给定位new的指针无需指向operator new分配的内存
    // 实际上,传递给定位new表达式的指针甚至不需要指向动态内存。?
}

2).显式的析构函数调用

  • 就像定位newallocateconstruct类似一样,对于析构函数的显式调用也与使用destroy很类似。
{
    string *sp = new string("a value");
    sp->~string();

    // 和调用destroy类似,调用析构函数可以清除给定的对象但是不会释放该对象所在的空间,如果需要,我们可以重新使用该空间

    // 意犹未尽.....
}

/2.运行时类型识别

1).该功能由两个运算符实现

  1. typeid,用于返回表达式的类型
  2. dynamic_cast,用于将基类的指针或引用安全地转换成派生类的指针或引用。

2).当我们将这两个运算符用于某一种类型的指针或引用时,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型(实际类型)。

  • 这两个运算符特别适合用于
  • 我们想要使用基类对象的指针或者引用执行某一个派生类操作,并且该操作不是虚函数。
  • 假设不可以使用虚函数,则可以使用RTTI(run_time_type_identification)运算符。但是使用它有更多的潜在风险。必须清楚地知道转换的目标类型并且检查类型转换是否被成功执行。
  • 使用RTTI必须加倍小心,在可能的情况下,最好定义虚函数而不是直接接管类型管理的重任。

//1.dynamic_cast运算符

1).

{
    // 使用形式
    dynamic_cast<type*>(e)
    dynamic_cast<type&>(e)
    dynamic_cast<type&&>(e)
    // 其中type必须是一个 类类型
    // 并且通常情况下,该类型含有虚函数
    // 第..种形式
    // 1.   e必须是一个有效指针
    // 2.   e必须是一个左值
    // 3.   e不能是左值

    // 在上面的所有形式中,e的实际类型必须符合以下三个条件中的任意一个
    // 1.   e的类型是目标type的公有派生类
    // 2.   e的类型是目标type的公有基类
    // 3.   e的类型就是目标type的类型

    // 如果符合,则类型转换 可能 成功,否则转换失败
    // 转换成功还需要保证,e实际指向的对象是指针类型的派生类!!!

    // ---------转换失败时
    // 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0
    // 如果是引用,并且失败,它将会抛出一个bad_cast的异常,该异常定义在头文件`typeinfo`中


    // -------------指针类型的dynamic_cast
    // 以下例子很混乱,不建议参考。翻译版本很混乱..........
    // Base类至少含有一个虚函数,Derived是Base的public派生类
    // 如果有一个指向Base的指针bp
    // 则我们可以在运行时将它转换成指向Derived的指针。实际上能不能成功,取决于bp的实际指向对象。

    if (Derived *dp = dynamic_cast<Derived*>(bp)) {
        // 使用dp指向Derived对象
    } else {
        // bp仍然指向Base对象,使用dp所指向的Base对象
    }

    // 如果bp指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指向的Derived对象,
    // 此时,if语句内部使用Derived操作的代码是安全的
    // 否则转换的结果是0,dp具有0意味着
    // if语句条件失败

    // ----------如若bp指向的是一个base对象,可以转换成功吗?不可以

    // 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针

    // 在条件中,我们定义了dp,
    //在一个操作中,同时完成类型转换和类型检查两个任务
    // 并且if外部是不能访问dp的
    // 一旦转换失败,即使后续的代码忘记做相应的判断,也不会接触到这个未绑定的指针。

    // ------------引用类型的dynamic_cast
    // 引用的错误表示方式和指针不一样,因为没有所谓的空引用
    void f(const Base &b) {
        try {
            const Derived &d = dynamic_cast<const Derived&>(b);
        } catch (bad_cast) {
            //处理类型转换的错误
        }
    }
}

练习

  • 19.3,
  1. 明确转换成功的条件,以此为标准。
  2. 二义性基类。编译失败。在给基类指针初始化派生类地址时报错。
  3. 需要打开RTTI选项。
  • 19.5,当我们无法取得基类的源码时,而又需要在派生类中添加成员函数。此时在基类添加虚函数的方法不可行,这样一来使用基类指针来调用新的成员是不可能的;此时我们可以考虑,当前类中添加即可,至于是不是虚函数,无所谓,当我们需要调用该函数时,使用dynamic_cast将基类指针转换为该类型的指针,使用该成员。所以,如果无法为基类添加虚函数时,可以使用dynamic_cast来代替虚函数。

//2.typeid运算符

1).

{
    // 它允许程序向表达式提问,你的 实际对象 是什么类型?

    // -------表达形式

    typeid(e);
    // 其中e可以是任意表达式或者类型的名字。
    // 操作结果是一个const对象的引用
    // 该对象的类型是标准库类型type_info或者type_info的公有派生类型
    // type_info类定义在头文件typeinfo中

    // -----------规则
    // 1.   顶层const被忽略
    // 2.   如果表达式是一个引用,则typeid返回该引用所引对象的类型
    // 3.   不过当typeid作用于数组或函数时,并不会向执行指针的转换

    // 当运算对象不属于类类型或者是一个不包含任何虚函数的类时
    // typeid运算符指示的是运算对象的静态类型
    // 而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运算时才会求得(就是动态类型。)

    // -------------使用typeid运算符
    //使用typeid
    // 1.   用来比较两条表达式的类型是否相同
    // 2.   比较一条表达式的类型是否与指定类型相同

    Derived *dp = new Derived;
    Base *bp = dp;

    if (typeid(*bp) == typeid(*dp))
        // 实际指向的是同一类型
    if (typeid(*bp) == typeid(Derived))
        // 是否实际指向Derived对象。

    // typeid应该作用于对象,因此要解引用
    if (typeid(bp) == typeid(Derived))
        //该检查永远是失败的
        // 一个是指针,永远一个是类对象永远不会相等。
        // 此处代码永远不会被执行

    // 当typeid作用于指针时,而不是作用于指针所指向的对象
    // 返回的指针的静态类型Base*。
    // Base*在编译时求值

    // ----------表达式是否会求值
    // typeid是否需要运行时检查决定了表达式是否会被求值
    // 只有当类型含有虚函数时,编译器才会对表达式求值。
    // 如果类型不含有虚函数,typeid返回表达式的静态类型,编译器无需对表达式求值就直到表达式的类型

    // 如
    typeid(*p)
    // 如果p所指的类型不含有虚函数,则p不必是一个有效指针,因为不需要对他进行求值;否则p必须是一个有效的指针。
    // 如果此时p是一个空指针,将会抛出bad_typeid的异常。(是否是求值时)    
}

//3.使用RTTI

1).实际应用。

  • 具有继承关系的类的相等运算。
  • 方案,定义一套虚函数。在基类中定义的相等运算符号,将实际工作委托给equal虚函数。
  • 但是,派生类的覆盖,必须是相同的形参,并且形参都是基类的引用,而此时equal只能使用基类的成员。如何解决?
  • 首先理解,相等,只有两个对象的类型都一样的情况,才可能发生。
{
    class Base {
        friend bool operator==(const Base&, const Base&);
    public:


    protected:
        virtual bool equal(const Base&) const;
    };
    class Derived : public Base {
    public:

    protected:
        bool equal(const Base&) const;
    };

    bool operator==(const Base &l, const Base &r) {
        // 先检查typeid的类型是否一致
        // 如果不一致,返回false
        // 如果一致继续虚调用equal
        return typeid(r) == typeid(l) && l.equal(r);
    }

    //设计equal时需要使用到类型的转换
    // dynamic_cast
    // 由于需要比较派生类中的成员
    // 所以转换是必然的。
    bool Derived::equal(const Base &r) const {
        // 此时两个类型必然是一样的
        // r必然是Derived,所以
        // 转换不会发生问题
        auto r = dynamic_cast<Derived&>(r);
        // .....后续的相等判断操作
    } 

    // 基类中的虚函数,可以之接比较
    bool Base::equal(const Base &r) {
        //....相等的判断操作
    }
}

//4.type_info类

1).type_info的精确定义随着编译器的不同略有差异。但是c++规定type_info类的定义必须在头文件typeinfo中,并且至少提供表19.1所示的操作。(p735)

  • 除此之外因为type_info一般是作为一个基类出现,所以他还提供一个public virtual的析构函数。当编译器希望提供额外的类型信息时,通常在type_info的派生类中完成。
  • type_info没有默认的构造函数,而且它的拷贝,移动**都是删除的。**所以我们无法定义该类型的对象。创建type_info对象的唯一方法就是使用typeid运算符。
  • typeidname成员返回一个C风格字符串,表示对象的类型名字。对于某一种给定的类型来说,name的返回值因为编译器的不同而不同,并且不一定与程序中使用的名字一致。对于name返回值的唯一要求就是,类型不同则返回的字符串必须有所区别。
{
    int arr[10];
    Derived d;
    Base *p = &d;

    typeid(42).name();//i
    typeid(arr).name();//A10_i    
    typeid(Sales_data).name();//10Sales_data
    typeid(string).name();//Ss
    typeid(p).name();//P4Base
    typeid(*p).name();//7Derived

    // type_info在有的编译器上提供了额外的成员函数来提供程序中所用类型的额外信息。
    // 详见编译器的使用手册
}

练习

  • 19.10,c中的ra是一个动态类型,需要求值,答案有错误,name返回的结果应该表示的是表示class B的字符串。

/3.枚举类型

1).它使得我们可以将一组 整型常量 组织在一起。和类一样,枚举类型定义了一种新的类型。它属于字面值常量类型。
2).c++中有两种枚举,

  1. 限定作用域,(c++11新标准引入)
  2. 不限定作用域
{
    // ---------定义限定
    enum class open_modes {input, output, append};
    enum struct open_modes {input, output, append};
    // 首先是关键字,enum class 或者
    // enum struct
    // 然后是类型名称以及花括号括起来的以逗号分割的 枚举成员列表
    // 最后是一个分号
   
    //----------定义不限定
    // 省略掉关键字class 或者struct
    // 并且枚举类型的名字是可选的
    enum color {red, yellow, green};

    enum {floatPrec  = 6, doublePrec = 10, double_doublePrec = 10}; 

    // 如果enum是未命名的,则我们只能在定义该enum的时候定义它的对象
    // 方式就是
    enum {red, green} aEnum, anotherEnum;

    // ----------枚举成员
    // 在限定作用域的枚举类型中,枚举成员的名字遵守常规的作用域准则,并且在枚举类型的作用域之外是不可访问的
    // 而在不限定作用域的枚举类型中,枚举成员的作用域和枚举类型本身的作用域相同
    enum color {red, yellow, green};//不限定
    enum stoplight {red, yellow, green};//错误,重复定义了枚举成员
    enum class peppers {red, yellow, green};//正确,内部的隐藏外部
    color eyes = green;//正确,不限定作用域的枚举类型成员位于有效的作用域中
    peppers p = green;//错误,peppers的枚举成员不在有效的作用域中
    // 但是color::green在有效的作用域中,但是类型错误

    color hair = color::red;//正确,显式访问
    peppers p2 = peppers::red;//正确

    // --------默认值
    // 默认情况下,枚举值从0开始,依次加一
    // 不过我们可以为一个或者几个枚举成员指定专门的值
    enum class intTypes {
        charType = 8, shortType = 16, intType = 16,
        longType = 32, long_longType = 64
    };
    // 枚举值不一定唯一,
    // 如果我们没有显式地指定初始值,则当前的枚举类型成员的值,等于之前枚举成员的值加一

    //-----------const属性
    // 枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式 
    // 也就是说,每一个枚举类型长远本身就是一条常量表达式,我们可以在任何需要使用常量表达式的地方,使用枚举类型成员。
    // -------应用
    // 定义枚举类型的constexpr变量
    constexpr intTypes charbits = intTypes::charType;

    // switch语句
    // 将一个enum作为switch语句的表达式
    // 枚举值作为case的标签

    // 将枚举类型作为一个非类型模板实参使用(例如数组的大小。)
    // 因为推断出来的值必须是一个常量表达式。从而允许编译器在编译时实例化模板。
    // 在类的定义中初始化枚举类型的静态数据成员
    // 在类内提供初始值,要求是初始值是常量表达式,变量是一个constexpr
    // 类外初始化没有要求。


    // ----------定义和初始化enum类型的对象
    // 只要enum有名字,我们就可以定义它并进行初始化
    // 要想初始化enum对象或者为它赋值
    // 必须使用给类型的一个枚举成员或者该类型的另一个对象
    open_modes om = 2;//错误,2不属于该类型
    om = open_modes::input;//正确,input是它的一个成员

    // 一个 不限定作用域 的枚举类型的对象或者枚举成员自动转换成整型
    int i = color::red;//隐式转换
    int j = peppers::red;//错误,限定作用域的不会进行隐式转换

    // -------------指定enum的大小
    // 实际上enum是统一由 某一种整数类型 表示的
    // 在C++11中,我们可以在enum名字后面加上冒号以及我们想要使用的类型

    // enum intValues : unsigned long long {
        charType = 255, shortType = 65535,
        longType = 4294967295Ul,
        long_longType = 18446744073709551615ULL
    };

    // 如果我们没有指定enum的潜在类型,则默认情况下
    // 限定作用域的enum成员类型是int
    // 不限定作用域的枚举类型来说,枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能容纳枚举值。

    // 如果我们指定了枚举成员的潜在类型 (包括限定作用域的enum的隐式指定),则一旦某一个成员超过该类型所能容纳的范围,将会引发错误

    // 指定enum的潜在类型,使得我们可以控制不同实现环境中使用的类型
    // 确保在不同环境中编译所产生的代码一样。

    // ----------前置声明

    enum intValues : unsigned long long;//不限定作用域的必须指定成员类型
    enum class open_modes;//限定的可以使用默认的成员类型int

    // 前置声明enum,必须指定成员的类型。可以是显式也可以是隐式。

    // 和其他声明一样,enum的声明和定义必须匹配
    // 这意味着,enum的所有声明和定义中的成员类型都必须一致
    // 而且,我们不能在同一个上下文中,先声明一个不限定作用域的enum
    // 然后又声明一个同名的限定作用域的enum
    enum class intValues;//使用默认的int
    enum intValues;//错误
    enum intValues : long;//错误


    // --------------形参匹配和枚举类型
    enum Tokens {INLINE = 128, VIRTUAL = 129};
    void ff(Tokens);
    void ff(int);
    int main() {
        Tokens = curTok = INLINE;
        ff(128);//精确匹配ff(int)
        ff(INLINE);//精确匹配ff(Tokens)
        ff(curTok);//精确匹配ff(Tokens)
        return 0;
    }
    // 注意不限定的枚举可以向整型的转换
    // 只能用枚举成员或者对象进行初始化或者赋值

    // 至于枚举向整型的转换,有枚举的潜在类型决定

    void newf(unsigned char);
    void newf(int);
    unsigned char uc = VIRTUAL;
    newf(VIRTUAL);      //调用int
    newf(uc);           //unsigned char

    // 这是因为,Tokens的最大成员就是129,该枚举类型可以用unsigned char来表示
    // 因此很多编译器使用unsigned char作为enum的潜在类型
    // 但是,它的对象和枚举成员此时会提成成int
    // 所以精确匹配int的重载版本
}

/4.类成员指针

1).成员指针,指的是可以指向类的非静态成员的指针。一般而言,指针指向的是类的对象,但是成员指针指向的是类的一个成员而不是一个对象。

  • 类的静态成员不属于任何对象,因此无需特殊的指向静态成员的指针;指向静态成员的指针和普通指针没有区别。
  • 成员指针的类型囊括了类的类型和成员的类型。当初始化一个这样的指针时,我们令其指向类的某一个成员,但是不指定成员所属的对象;直到使用成员指针时,才提供成员所属的对象。
{
    class Screen {
    public:
        typedef std::string::size_type pos;
        char get_char_cursor() const {
            return contents[cursor];
        }
        char get() const;
        char get(pos ht, pos, wd) const;
    private:
        str::string contents;
        pos cursor;
        pos height, width;
    };
}

//1.数据成员指针

1).和普通指针声明的区别在于,

{
    // 需要包含成员所属的类

    // 在声明的*之前添加classname::以表示当前定义的指针可以直指向classname的成员

    const string Screen::*pdata;
    // 指向Screen类的const string成员的指针

    // 常量对象的数据成员本身也是常量,因此我们的指针声明成指向const string成员的指针pdata可以指向
    // 任何Screen对象的一个成员,不管该对象是不是一个常量
    // 此时该指针只能读,而不能写。

    // ----------初始化与赋值
    pdata = &Screen::contents;
    // 当我们初始化一个成员指针(或者向他赋值)时,需要指定它所指的成员
    // 而对象是非特定的。
    // 以上表示,指针指向一个非特定的Screen对象的特定contents成员

    // 其中,我们将取地址运算符作用于,Screen类的成员而非内存中一个该类的对象
    // 在c++11中,声明成员指针最简单的方式就是使用auto或者decltype
    auto pdata  = &Screen::contents;

    // ------------使用数据成员指针

    // 当我们初始或者赋值一个成员指针时,该指针并没有指向任何的数据
    // 指针指定了成员但是没有指定对象
    // 只有当解引用成员指针时我们才提供对象信息。

    // 该指针也有两种成员访问运算符
    // .*
    // ->*
    // 我们可以解引用指针并获得该对象的成员
    Screen myScreen, *pScreen = &maScreen;
    // 解引用pdata以获得myScreen对象的contents成员
    auto s = myScreen.*pdata;
    // 解引用获得pScreen所指的对象的contents成员
    s = pScreen->*pdata;

    // 以上的操作经过两个步骤
    // 1.   解引用成员指针获得所需要的成员
    // 2.   然后通过成员访问运算符得到指定对象的指定成员

    // ------------返回数据成员指针的函数
    // 常规的类的访问规则对于成员指针同样有效
    // contents是私有的
    // 对于pdata的使用必须位于Screen类的成员或者友元内部,否则报错

    //因为数据成员一般是私有的,所以我们通常不能直接获得数据成员指针
    // 如果一个类希望我们访问它的数据成员
    // 最好定义一个函数
    class Screen {
    public:
        // data是一个静态成员
        static const std::string Screen::* data() {
            return &Screen::contents;
        }
    }; 
    
    // 得到一个成员指针,此时只是得到一个指针,并没有实际对象
    const string Screen:: *pdata = Screen::data();
    // 获得myScreen的contents成员
    auto s = myScreen.*pdata;
}

//2.成员函数指针

1).

{
    // 最简单的方式就是使用一个auto关键字
    auto pmf = &Screen::get_cursor;
    // pmf是一个指针,它可以指向Screen的某一个常量成员函数
    // 前提是该函数不接受任何实参,并且返回一个char
    // 即该指针指向的函数,必须和get_cursor的返回类型,形参类型,是否const,是否是引用,一致即可

    // 当我们不使用auto关键字时
    // 需要指定返回类型,形参类型,const属性,引用属性
    char (Screen:: *pmf2)(Screen::pos, Screen::pos) const;
    pfm2 = &Screen::get;//指向以上函数类型的get。

    // 并且成员函数有重载的情况,我们必须显式地指出函数类型,明确我们所要使用的函数是哪一个。即此时,不能使用auto关键字。

    //----------优先级
    char Screen:: *p(Screen::pos, Screen::pos) const;
    // 试图声明一个函数,返回指向Screen类的char数据的指针。
    // 由于是普通函数,不可以是const,故报错。

    // 和普通指针不一样的是,在成员函数和成员函数指针之间不存在自动转换的规则。
    pmf = &Screen::get;     //正确,需要显式指出&运算符。
    pmf = Screen::get;      //错误

    // -------------使用成员函数指针
    // 和使用解引用运算符
    // .*
    // ->*
    // 进行调用函数
    Screen myScreen, *pScreen = &myScreen;
    // 注意使用调用运算符()
    char c1 = (pScreen->*pmf)();
    char c2 = (myScreen.*pmf2)(0, 0);
    // 之所以需要(),就是因为调用运算符的优先级比解引用运算符更高。

    myScreen.*pmf();
    // 等价于
    myScreen.*(pmf());
    // 因为调用运算符的优先级别高
    // 所以在声明和使用时,()都必不可少
    (C::*pf)(parms);
    (c.*pf)(args);

    // -----------使用成员指针的类型别名
    // 这样作易于理解
    using Action = char (Screen::*)(Screen::pos, Screen::pos) const;

    // 从而定义一个成员函数的指针变得简单
    Action get = &Screen::get;//指向的时Screen的get成员

    // ------------函数成员指针作为某一个函数的返回值或者形参类型
    // 形参可以默认实参
    Screen& action(Screen &, Action = &Screen::get);

    // 调用
    Screen myScreen;
    action(myScreen);
    action(myScreen, get);
    action(myScreen, &Screen::get);
    // 类型别名使得,成员函数指针类型更见容易理解,更加简洁

    // ----------成员函数指针表
    // 类似于之前定义map,存放同一类型的可调用对象
    // string作为key,可调用对象作为value
    // 利用标准库的function类进行存放
    // 这里,我们使用成员函数指针实现一样的功能。

    // 将指针存入一个函数表中。
    // 如果一个类含有几个相同类型的函数成员,则这样的表可以帮助我们从这些成员中选择一个
    // 例如,以下的成员函数的可调用类型都一致
    // 返回类型,形参,const属性,引用属性
    class Screen {
    public// 每一个函数负责光标的移动
        Screen& home();
        Screen& forward();
        Screen& back();
        Screen& up();
        Screen& down();
    };
    // 这几个函数的类型一样
    // 我们希望定义一个move函数

    class Screen {
    public:
        // 定义一个成员函数指针
        using Action = Screen&(Screen::*)();
        // 指定具体要移动的方向
        enum Directions {HOME, FORWARD, BACK, UP, DOWN};
        Screen& move(Directions);
    private:
        static Action Menu[];//函数表
    };
    // 数组存放的是每一函数的指针。
    // move接受一个对应的枚举类型
    // 进行操作
    Screen& Screen::move(Directions cm) {
        // 典型错误,注意是成员函数指针
        // 还没有指定对象。
        return (Menu[cm])();
        // 调用的是this对象的成员函数
        return (this->*Menu[cm])();
    }

    // -------使用就是
    Screen myScreen;
    myScreen.move(Direction::HOME);//调用myScreen.home()
    myScreen.move(Direction::DOWN);//调用myScreen.down()

    // --------初始化函数表
    // static的类外初始化。
    Screen::Action Screen::Menu[] = {
                    &Screen::home,
                    &Screen::forward,
                    &Screen::back,
                    &Screen::up,
                    &Screen::down
                                }; 
}

练习,

  • 19.14,如果成员函数指针的类型和要指向的函数类型不一致,报错;当是重载函数时,会自动匹配到某一个函数(显式指定了成员函数指针的类型)。
  • 19.17,注意定义类型别名时,成员函数指针,不要遗漏掉类名;并且一个函数指针指向的一类函数。

//3.将成员函数用作可调用对象

1).成员函数指针本身并不是一个可调用对象。所以它并不能直接转递给一个算法。

  • 解决,使用function标准库模板,生成一个可调用对象。
{
    function<boo (const string &)> fcn = &string::empty;
    find_if(sv.begin(), sv.end(), fcn);
    // ~~通常情况下,执行成员函数的对象被隐式地传递给this形参,当我们想要使用function为成员函数生成一个可调用对象时,必须按照一样的方式,但是不需要我们操作。...p745.~~
    // 当一个function对象包含一个指向成员函数的指针时,function类知道它必须使用正确的指向成员的指针运算符来执行函数的调用。
    // 也就是说,我们可以认为在find_if当中含有类似于如下形式的代码
    if (fcn(*it))
    // it是find_if实参范围内的迭代器,*it是给定范围的一个对象
    // fcn是传递给find_if的function类型的可调用的对象

    // 其中function将正确使用指向成员函数的指针
    // 本质上,function类将调用转换成了如下的形式

    // it是迭代器
    if((*it).*p)())//p是一个fcn内部的成员函数指针

    // 我们必须根据所需要的类型来指定function的对象中包含的可调用对象
    vector<string*> pvec;
    function<bool (const string*)> fp = &string::empty;
    find_if(pvec.begin(), pvec.end(), fp);

    vector<string> sv;
    function<bool (const string&)> fp1 = &string::empty;
    find_if(sv.begin(), sv.end(), fp1);
}
  • mem_fn生成一个可调用对象
{
    // 使用function必须由用户显式地提供可调用对象的类型,
    // 使用标准库功能mem_fn,让编译器负责推断可调用对象的类型,无需用户显式指定。
    // 它和function都定义在头文件functional中,并且可以从成员指针生成一个可调用对象
    find_if(sv.begin(), sv.end(), mem_fn(&string::empty));
    // 使用mem_fn(&string::empty)生成一个可调用对象,该对象接受一个string实参,返回一个bool值

    // ---------调用形式
    auto f = mem_fn(&string::empty);//f接受一个string或者一个string*
    f(*sv.begin());//传入的是一个string,f使用.*调用empty
    f(&sv.begin());//传入的是一个string*,f使用->*调用empty
    // 实际上,我们可以认为mem_fn生成的可调用对象有一对重载的函数调用运算符,一个接受指针,一个接受对象
}
  • 使用bind生成一个可调用对象
{
    auto it = find_if(sv.begin(), sv.end(), bind(&string::empty, _1));

    // 与function类似的地方是,当我们使用bind时,必须将函数中表示执行对象的隐式形参(this)转换为显式?
    // 如何转换?迷惑
    // 和mem_fn类似的是,既可以接受指针,也可以接受一个对象
    auto f = bind(&string::empty, _1);
    f(*sv.begin());//string,f使用.*调用empty
    f(sv.begin());//string*,f使用->*调用empty

    // 具体如何是实现?什么叫做,将调用对象的隐式形参(this)转为显式?
}

练习,

  • 19,18,直接使用标准库的成员函数string::empty

/5.嵌套类

1).一个类可以定义在另一个类的内部,前者称为嵌套类或者嵌套类型。

  • 嵌套类常用于定义作为实现部分的类,例如,QueryResult
  • 嵌套类是一个独立的类,与外层类基本没有什么关系。特别是外层类的对象和嵌套类的对象是相互独立的。在嵌套类的对象中不包含任何外层类定义的成员;外层类的对象中也不包含任何嵌套类定义的成员。
  • 只是嵌套类为外层类定义了一个类型成员。并且嵌套类的和外层类是一种作用域的嵌套关系。在友元前提下,是方便名字的访问的(在类外定义嵌套函数,需要外层的限定,从而进入了外层的作用域中,减少类似line_no的再一次定义。)。

2).为什么要定义嵌套类?

  • 嵌套类的名字在外层作用域中是可见的,在外层类作用域之外不可见。和其他嵌套的名字一样,嵌套类的名字不会和别的作用域中的同一名字冲突,是否包括外层?
  • 嵌套类中成员的种类非嵌套类是一样的?和其他类类似,嵌套类也可以使用访问限定符,控制外界的访问权限。外层类对嵌套类的成员没有特殊的访问权限。同样,嵌套类对外层类的成员也没有特殊的访问权限。
  • 嵌套类在其外层类中定义了一个类型成员,和其他成员类似,该类型的访问权限由外层类决定。
  • 位于外层类public部分的嵌套类实际上定义了一种可以随机访问的类型;定义在protected部分的嵌套类定义的类型,只能被外层类以及它的友元,派生类所访问;定义在外层类的private部分的嵌套类定义的类型,只能被外层类以及它的友元所访问。

3).声明一个嵌套类

{
    // -------------声明
    // TextQuery和QueryResult类密切相关
    // QueryResult主要是作为TextQuery中函数query的结果
    // 用作其他没有意义
    // 我们可以将QueryResult定义成TextQuery的成员

    class TextQuery {
    public:
        class QueryResult;//稍后定义
    };

    // 由于,将QueryResult类声明为嵌套类,所以我们必须先声明在进行使用
    // 因为后面query需要它作为返回类型

    // ----------类外定义一个嵌套类
    // 和成员函数一样,嵌套类必须在类内声明
    // 但是定义可以在类外部,也可以在类内部

    // 需要前缀
    class TextQuery::QueryResult {
        // 此时是位于TextQuery类内
        // 不需要对QueryResult的形参进行限定
        friend std::ostream& print(std::ostream&, const QueryResult&);
    public:
        // 嵌套类可以直接使用外层类的成员,无需对该成员进行名字的限定
        // 但是前提是,外层类设置了友元,
        // 因为嵌套类对外层类的没有特殊的访问权限。
    };

    // 嵌套类在外层类之外定义完成之前,它都是一个不完全类型

    // ----------定义嵌套类的成员
    // 唯一的不同就是指明嵌套关系
    // 用外层类限定内层嵌套类
    // 以及内层类在是外层友元的前提下,可以直接使用外层类的成员,而不需要进行限定
    TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p,shared_ptr<vector<string>> f) : 
                sought(s), lines(p), file(f) {}
    

    // -------------嵌套类的静态成员定义

    // static声明只能在类内
    int TextQuery::QueryResult::static_mem = 1024;
    // 在嵌套类内声明的静态成员
    // 静态成员的定义将位于TextQuery作用域之外

    //---------嵌套类作用域的名字查找

    // 嵌套类的名字查找遵循一般的规则
    // 注意它是一个嵌套的作用域
    // 但是同时还需要注意,嵌套的类对外层类没有特殊的访问权限

    // 反过来,嵌套类作为外层类的一个类型成员,可以被外层类的成员直接使用(和其他的成员没有什么区别)。
    // 需要注意的是,在类外部定义成员函数时,返回类型是没有进入类内的。!
    // 所以还是需要类型限定符号。 
}

/6.union:一种节省空间的类

1).它是一种特殊的类。一个union可以有很多的数据成员,但是在任意时刻只有一个数据成员可以有值。

  • 当我们为union的某一个成员赋值之后,该union的其他成员就变成未定义的状态。分配给一个union对象的存储空间至少要容纳它的最大数据成员。
  • 和其他类,一样,union定义了一种新类型。

2).类的某些特性对于union同样使用。

  • 但是union不能含有引用类型。其他的类型大多可以作为它的成员类型。
  • c++新标准中,含有构造函数或者析构函数的类类型也可以作为union的成员类型。
  • union可以为其成员指定public,protected,private等多种保护标记。默认情况下,unionpublic
  • union可以定义包括构造函数和析构函数在内的成员函数。但是,union不可以作为基类和也不能继承自其他类,所以union中不能含有虚函数。

2).union

{
    // union提供了一种有效的方式,使得我们可以方便地表示一组类型不同的互斥值
    // -------------定义一个union

    union Token {
        char cval;
        int ival;
        double dval;
    };
    // 我们需要处理一组不同类型的数据
    // Token类型的对象,只有一个成员,该成员的可能是以上的任意一个

    // 注意,union后的类型名称是可选的

    // -------------使用一个union
    // 注意union的名字就是一个类型名。
    // 通常情况下,union的对象是没有初始化的
    // 我们可以使用花括号来显式初始化一个union
    Token first_token = {'a'};//初始化cval成员
    Token last_token;//未初始化的Token成员
    Token *pt = new Token;//指向一个未初始化的Token对象

    // 如果提供了初始值,则该初始值被用于初始化 第一个  成员
    // -----------使用成员访问运算符,,为其他的指定成员赋值
    last_token.cval = 'z';
    pt->ival = 42;

    //-------------注意
    // 为一个union的一个    数据    成员赋值时,会使得其他的数据成员处于未定义的状态
    // 因此当我们使用union时,必须清楚地知道,当前的union存储的数据到底是什么类型。
    // 如果我们使用错误的数据成员或者为错误的数据成员赋值
    // 程序可能会崩溃或者出现异常的行为,具体的视情况而定。

    // -------------匿名的union
    // 指的就是一个未命名的union
    // 并且在右括号和分号之间没有任何的对象声明。

// 注意,一旦,我们定义了一个匿名的union,编译器就会自动地为该union创建一个未命名的对象。
    union {         
        char cval;
        int ival;
        double dval;
    };  //定义一个未命名的对象,我们可以直接访问它的成员

    // 在匿名union的定义所在的作用域中,该union的成员都是可以直接访问的。   如何使用?
    // 匿名union不能包含protected,private成员,也不能定义成员函数。

    // ------------含有类类型成员的union
    // 早期中,不允许有定义了构造,或者拷贝控制成员类类型成员
    // 新标准中允许。但是,如果union里的成员类型定义了,自己的构造或者拷贝控制成员,则该union的用法要比只含有内置类相关的成员的union更加复杂

    // 当union包含的是内置类型的成员时,我们可以使用普通的赋值语句改变union保存的值
    // 但是对于含有特殊类类型成员的union时,就没有那么简单了
    // 如果我们想要将union的值改为类类型成员对应的值时,必须构造该类型的对象
    // 同理,如果我们想要将一个类类型的值改为一个其他的值,就必须析构该类类型的成员。

    // 当只有内置类型成员时,union的析构和构造由编译器完成
    // 当是类类型时,并且自定义了构造函数和拷贝控制成员,编译器的合成默认版本是删除的

    // 如果在一个类中含有一个union成员,并且该union成员含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作将是删除的

    // ---------------------使用类管理union成员
    // 管理含有类类型成员的union

    class Token {
    public:
        // 因为union定义了一个string成员,所以该Tokne必须定义拷贝控制成员
        // 默认的版本是删除的。??
        Token() : tok(int), ival(0) {}  //默认构造函数
        // 不接受参数就是一个默认构造函数。
        Token(const Token &t) : tok(t.tok) {
            copyUnion(t);
        }
        Token& operator=(const Token &t);
        ~Token() {
            // 必须显式地析构string成员
            if (tok == STR) sval.~string();
        }
        // 完成对应的赋值。
        // 控制类型的转换。
        Token& operator=(const string&);
        Token& operator=(char);
        Token& operator=(int);
        Token& operator=(double);
    private:
        // 追踪union中存放的值的类型
        enum {INT, CHAR, DBL, STR} tok;//判别式
        union { //匿名的union
            char cval;
            int ival;
            double dval;
            std::string sval;
        };
        // 每一个Token对象含有一个该未命名的union类型的一个未命名成员
        // 它是可以直接使用的
        void copyUnion(const Token&);
    };

    // 因为我们的union含有一个定义了析构函数的成员
    // 所以我们必须为union定义一个析构析构函数以销毁string成员。和普通的类类型成员不一样,作为union组成部分的类成员无法自动销毁
    // 因为析构函数不清楚union存放的值是什么类型,所以它无法确定应该销毁哪一个成员

    // 我们的类析构函数会进行检查,如果union里面存放的是内置类型,则类的析构析构函数什么也不做。

    // -----------------重载赋值运算符
    Token& Token::operator=(int i) {
        if (tok == STR) sval.~string();
        ival = i;
        tok = INT;
        return *this;
    }

    // ----------------string版本的,略有不同
    Token& Token::operator=(const string &s) {
        if (tok == STR) 
            sval = s;//如果是string,直接赋值即可
        else
            // 该定位new没有申请实参,而是赋值作用
            new(&sval) string(s);//否则是直接构造一个
            //  这里是使用了一个定位new
        tok = STR;
        return *this;
    }

    // ----------------管理需要拷贝控制的联合成员
    // 分清楚构造和赋值的不同
    // 初始化,对象是一个没有存放值得
    // 赋值,对象又可能有,又可能没有存放值

    void Token::copyUnion(const Token &t) {
        switch(t.tok) {
            case INT : ival = t.ival; break;
            case CHAR : cval = ...

            // 要拷贝一个string,可以使用定位new表达式来构造它
            case string : new(&sval) string(t.val); break;
        }
    }
    Token& Token::operator=(const Token&t) {
        // 针对原来union和实参得union的值类型
        // 做出三种的判断。
        if (tok == STR && t.tok != STR) sval.~string();
        if (tok == STR && t.tok == STR) 
            sval = t.sval;//无需构造一个新的string           
        else //这里需要注意,我们的第一种情况也是执行以下的函数
            copyUnion(t);
        tok = t.tok;            //更新类型标志。
        return *this;
    }
}

/7.局部类

1).类可以定义在某一个函数里面,这样的类是局部类。局部类定义的类型,只在定义它的作用域内可见。和嵌套类不一样,局部类的成员受到严格限制。

  • 局部类的所有成员包括函数在内,都必须完整定义在类的内部。因此局部类的作用比嵌套类差很远。
  • 由于需要在类内完成地定义,所以它的成员函数复杂度不会太高。一般就是几行。
  • 在局部类中,也不允许定义静态数据成员,因为没法定义这样的成员。?

2).局部类不能使用函数作用域中的变量

  • 局部类对其外层作用域的名字的访问权限受到很多限制。局部类只能访问 外层作用域 定义的类型名,静态变量,以及枚举成员。
  • 如果局部类定义在某一个函数内部,则该函数普通局部变量不能被该局部类使用
{
    int a, val;
    void foo(int val) {
        static int si;
        enum Loc {a = 1024, b};

        struct Bar {
            Loc locVal;//正确,使用局部的类型名
            int barVal;

            void fooBar(Loc l = a) {
                barVal = val;       //错误,val是foo的一个局部变量,形参
                barVal = ::val;     //正确,使用一个全局对象
                barVal = si;        //正确,使用一个静态局部对象
                barVal = b;         //正确,使用一个枚举成员
            }
        };
    }
}

3).常规的访问保护规则对局部类同样适用

  • 外层函数对于局部类的私有成员没有任何访问权限。当然可以通过将外层函数声明为友元的形式;或者声明成public。在程序中有权力访问局部类的代码非常有限。局部类已经封装在函数作用域里面,通过信息隐藏进一步封装就显得没有什么必要。

4).局部类的名字查找

  • 局部类内部的名字查找次序和其他的类似。在声明类成员时,必须先确保用的名字在当前位置是可见的,再进行使用。注意返回类型是类的成员的情况,此时还没有。如何解决?在类中的顺序?
  • 查找顺序,局部类->外层函数作用域->外层函数所在作用域。(默认是在函数中定义局部类。)

5).嵌套的局部类

{
    // 可以在局部类中,嵌套一个类
    // 此时,嵌套类的定义可以出现在局部类之外,但是
    // 嵌套类的定义必须在与局部类相同的作用域中。
    // 即只能在函数中
    void foo() {
        class Bar {
        public:
            class Nested;//声明一个类
        };

        // 定义
        class Bar::Nested {
            ...
        };
    }
    // 局部类的嵌套类,也是一个局部类,必须遵守局部类的各种规定。
    // 嵌套类的所有成员必须定义在嵌套类内部。
}

/8.固有的不可移植的特性

1).为了支持底层编程。c++定义了一些固有的不可移植的特性。所谓的不可移植就是因机器而异的特性。

  • 当我们将含有不可移植特性的程序从一台机器转移到另一台机器时,通常需要重新编写该程序。
  • 算术类型的大小在不同机器上是不一样的,这就是一个典型实例。

//1.位域

1).类可以将它的(非静态)数据成员定义成bit-field,在一个位域中含有一定数量的二进制位。

  • 一个程序需要向其他程序或者硬件设备传递二进制数据时,通常会使用到位域。
  • 位域在内存中的布局是与机器相关的。
  • 位域的类型必须是整型或者枚举类型。因为存储在带符号类型中的位域的行为是由具体实现确定的,所以通常情况下我们使用无符号类型保存一个位域。
{
    // 位域的声明就是成员名字后买你紧跟一个冒号和一个常量表达式,用来指定成员所占的二进制位数。
    typedef unsigned int Bit;
    class File {
        Bit mode: 2;//mode占2位,两个二进制位
        Bit modified: 1;//1
        Bit prot_owner: 3;
        Bit prot_group: 3;
        Bit prot_world: 3;
        // 操作和其他数据成员
    public:
        // 文件类型以八进制的形式表示
        enum modes {READ = 01, WRITE, EXECUTE};
        File &open(modes);
        void close();
        void write();
        void isRead() const;
        void setWrite();
    };
    // 如果可能的话
    // 尽量,在类的内部连续定义位域,以便压缩在同一个整数的相邻位,从而提供存储压缩
    // 例如,这5个位域可能会存储在同一个unsigned int 中。
    // 这些二进制位是否能压缩到一个整数以及如何实现,是与机器相关的。

    // &,不能作用于位域,因此任何指针都不能指向类的位域。
    // ---------------使用位域
    // 访问位域的形式和访问类的其他成员的形式很相似
    void File::write() {
        modified = 1;
        // ...
    }
    void File::close() {
        if (modified)
            // ...
            // 保存内容
    }
    // 通常使用内置的位运算符,操作超过1位的位域
    File& File::open(File::modes m) {
        mode |= READ;//设置为READ
        // 其他处理
        // ?
        if (m & WRITE)      //如果打开了READ和WRITE
        // 按读写方式打开文件
        return *this;
    }

    // 如果一个类设置了位域,通常也会定义一系列inline的操作来检验和设置位域的值
    inline bool File::isRead() {return mode & READ;}
    inline void File::setWrite() {mode |= WRITE;}
}

//2.volatile限定符

1).它的确切含义和机器有关。只能通过阅编译器文档来理解。要想使用volatile的程序在移植到新机器或者新编译器后,仍然有效,通常需要对程序做一些改变。
2).直接处理 硬件 的程序常常包含这样的元素,它们的值由程序直接控制之外的过程控制。

  • 例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序控制或者检测之外被改变时,应该将该对象声明为volatile
  • 关键字volatile告诉编译器,不应对这样的对象进行优化。
{
    // 用法和const很相似
    volatile int display_register;//该int值可能发生改变
    volatile Task *curr_task;//指向一个volatile对象
    volatile int iax[max_size];//iax中的每一个元素都是volatile
    volatile Screen bitmapBuf;//每一个成员都是volatile

    // 某一种类型既可以是const,也可是volatile
    // 也可以同时具有两种属性,
    // 这两个限定符号相互之间没有什么影响。

    // 就想一个类可以定义const成员函数一样,他也可以定义volatile的成员函数,此时只有volatile的成员函数,能被volatile对象调用。

    //------------指针和volatile
    // 这个关系和const一致

    volatile int v;
    int *ip = &v;//错误,必须使用volatile指针
    volatile int *ivp = &v;//正确
    volatile int volatile *vivp = &v;//正确

    // ----------------合成的拷贝对volatile对象无效
    // const和volatile的一个重要区别是,我们不能使用合成的拷贝/移动构造函数以及赋值操作初始化volatile对象
    // 或者从volatile对象赋值
    // 合成的成员接受的形参是非volatile的

    // 解决就是自己定义相应的函数
    class Foo {
        Foo(const volatile Foo &);
        // 赋值给非volatile对象
        Foo& operator=(volatile const Foo&);
        // 赋值给volatile对象
        Foo& operator=(volatile const Foo&) volatile;
    };
    // 拷贝一个volatile对象是否有意义呢?
    // 与使用目的相关。??
}

//3.链接指示:extern"C"

1).C++程序有时候需要调用其他语言编写的函数,最常见的就是调用C语言编写的函数。

  • 像所有的名字一样,其他的语言中的函数名字也必须在C++中声明,该声明必须指定返回类型和形参列表。
  • 对于其他语言编写的函数来说,编译器会检查其调用的方式与普通的C++函数的方式相同。但是生成的代码有所区别。
  • c++使用链接指示指出任意非c++函数所用的语言。
  • 想要把c++代码和其他的语言(包括c语言)编写的代码放在一起使用,要求我们有权访问该语言的编译器,并且该编译器与当前的c++编译器是兼容的。

2).声明一个非C++的函数

{
    // 链接指示可以有两种形式,
    // 1.   单个
    // 2.   复合
    // 链接指示不能出现在类定义或者函数定义的内部。同样的链接指示必须在的函数的每一个声明中出现

    // cstring头文件中的某一些c函数是如何声明的
    // 单个语句
    extern "C" size_t strlen(const char *);

    // 复合语句
    extern "C" {
        int strcmp(const char*, const char*);
        char* strcat(char*, const char*);
    }

    // 链接指示就是一个extern关键字,后面是一个字符串字面量
    // 然后就是一个普通的函数声明。
    // 编译器应该支持对C语言的链接指示
    // 此外,编译器也可能会支持其他语言的连接指示
    extern "Ada"
    extern "FORTRAN"
}

3).链接指示和头文件

{
    // 复合形式的链接指示
    // 加上了花括号
    // 作用在于,
    // 1.   一次性声明若干函数,建立多个链接。
    // 2.   将使用于该来链接指示的多个声明聚在一起

    // 花括号中的声明的函数名字是可见的,就好像在花括号之外声明一样的。

    // 还可以应用于整个头文件
    // 例如,cstring头文件可能形如。
    extern "C" {
        #include <string.h>     
    }
    // 当一个include指示被至于复合链接指示的花括号中时,
    // 头文件中的所有普通函数声明都被认为是来链接指示语言所编写的
    // 链接指示可以嵌套,因此如果头文件包含自带来凝结指示的函数
    // 则该函数的链接不受影响


    // C++从c语言继承的标准库可以定义成c函数
    // 但并非必须,决定使用c还是c++实现c标准库,是每一个c++实现事情。       
}

4).指向extern "C"函数的指针

{
    // 编写函数所用的语言是函数类型的一部分,因此,对于使用链接指示定义的函数来说,
    // 它的每一个声明都是需要使用相同的链接指示
    // 而且指向其他语言编写的函数的指针必须和函数本身使用一样的链接指示

    // pf指向一个c函数,该函数接受一个int返回void
    extern "C" void (*pf)(int);

    // 当我们使用pf调用函数时,编译器认定当前调用的是一个C函数。

    // 指向c函数的指针和指向c++函数的指针是不一样的类型。
    // 这就是类型不匹配的问题。
    void (*pf1)(int);
    extern "C" void (*pf2)(int);
    pf1 = pf2;      //错误。
    pf2 = pf1;      //错误。

    // 有的编译器会接受第一个赋值语句,并把它作为对语言的扩展

    // ----------------链接指示对整个声明都有效
    extern "C" void f1(void (*)(int));
    // f1是一个c函数,它的形参是一个指向c函数的指针
    // extern "c"不仅对于函数有效。
    // 对于它的返回类型,形参的函数指针类型一样有效。

    // 如果我们希望给c++传入一个指向c函数的指针
    // 使用类型别名
    extern "C" typedef void fc(int);
    // f2是一个c++函数,该函数的形参是指向c函数的指针
    void f2(fc *);

    // -------------------导出c++函数到其他语言

    // 使用链接指示,对函数 定义 ,我们可以另一个c++函数在其他语言编写的程序中可用。
    // f函数可以被c程序调用
    extern "C" double f(double d) {/*.....*/}

    // 编译器为该函数生成合适的指定语言的代码

    // 可被多种语言共享的函数的返回类型和形参类型受到很多的限制
    // 例如,我们不太可能把一个C++类的对象传给c程序,因为c程序根本无法理解构造函数,析构函数
    // 以及类特有的操作

    // ----------------预处理器??
    // 有时需要在c和c++中编译同一个源文件
    //  我们可以,在编译c++版本的程序时,预处理器定义_ _cpluspluls。
    // 利用这个变量,我们可以在编译c++程序的时候有调剂爱你地包含一些代码进来
    #ifndf __cplusplus
    // ture,我们正在编译c++程序
    extern "C"
    #endif
    int strcmp(const char*, const char*);




    // ---------------重载函数和链接指示
    // 链接指示和重载函数的相互作用依赖于目标语言(例如,"C")
    // 如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也是支持重载c++中的函数。(->c++)

    // c语言不支持重载,所以一个c链接指针只能用于一个重名的函数。
    // 错误,两个extern "C"函数的名字相同
    // c语言中不可能会同时有这两个函数。
    extern "C" void print(int);
    extern "C" void print(double);

    // 如果一组重载函数中有一个是c函数
    // 其余必定是c++函数???
    class SmallInt {...};
    class BigINt {...};

    // c函数可以在c++中调用
    extern "C" double f(double);

    //  c++中的重载函数组
    extern SmallInt f(const SmallInt &);
    extern BigNum f(const BigNum &);
    // ...为什么加上extern关键字
}

/9.小结

1).位域和volatile,使得程序容易访问硬件。链接指示使得程序易于访问其他语言编写的函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值