C++ Primer第五版(4)——高级主题

4.高级主题

4.1 标准库特殊设施

  • tuple类型
    希望将一些数据合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple是非常有用的。



    由于tuple定义了<和==运算符 ,可以将tuple序列传递给算法,并且在无序容器将tuple作为关键字类型。

  • 使用tuple返回多个值

  • bitset类型定义和初始化



    注意:string的下标编号与bitset恰好相反。string下标最大用来初始化bitset的低位。

  • bitset操作
    bitset支持位运算符。



  • 正则表达式组件库




    默认情况下,regex使用的正则表达式语言是ECMAScript,[[:alpha:]]匹配任意字母。



    注意:一个正则表达式语法是否正确是在运行时解析的。正则表达式的编译是一个非常慢的操作,特别是在使用了扩展的正则表达式语法或是复杂的正则表达式时。
    如果存在错误,标准库会抛出一个类型为regex_error的异常。

    使用的RE库类型必须与输入序列类型匹配。


string pattern("[^c]ei");
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"  ;
regex r(pattern);
smatch results;
strint test_str = "receipt freind theif receive";
if (regex_search(test_str, results, r)) {
    cout << results.str() << endl;
}
  • 匹配与Regex迭代器类型




    下面的end_it是一个空sregex_iterator,起到尾后迭代器的作用。


string pattern("[^c]ei");
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"  ;
regex r(pattern, regex::icase);
for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) {
    cout << it->str() << endl;
}


for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) {
    auto pos = it->prefix().length(); 
    pos = pos > 40 ? pos - 40 : 0;
    cout << it->prefix().str().substr(pos)
            << "\n\t\t>>> "  << it->str() << " <<<\n"
            << it->suffix().str().substr(0, 40)
            << endl;
}
  • 使用子表达式
    一个子表达式是模式的一部分,正则表达式语法通常用括号表示子表达式。
    匹配对象除了提供匹配整体的相关信息外,还提供访问模式中每个子表达式的能力。子匹配是按位置来访问的。第一个子匹配位置是0,表示整个模式对应的匹配,随后是每个子表达式对应的匹配。
//r有两个字表达式,第一个表示文件名,第二个表示扩展名
//foo.cpp
//results.str(0)保存foo.cpp
//results.str(1)保存foo
//results.str(2)保存cpp
regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex::icase);
if (regex_search(filename, results, r)) {
    cout << results.str(1) <<endl; 
}

ECMAScript正则表达式语言的一些特性:



反斜线是C++的特殊字幕,所以需用一个额外的反斜线来告知C++需要一个反斜线而不是一个特殊符号。
模式的子表达式分析:



子匹配操作:
  • 使用regex_replace
    在输入序列中查找并替换一个正则表达式。



    fmt中用一个符号$跟子表达式的索引号来表示一个特定的子表达式。
    匹配和格式化标识的类型为match_flag_type,定义在std::regex_constants命名空间里。
    默认情况下,regex_replace输出整个输入序列。未与正则表达式匹配的部分会原样输出:匹配的部分按格式字符串指定的格式输出。


int main() {
    string phone = 
        "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])?(\\d{3})";
    regex r(phone);
    smatch m;
    string s;
    string fmt = "$2.$5.$7";
    while (getline(cin, s)) {
        cout << regex_replace(s, r, fmt) << endl;
    }
    return 0;
}


  • rand库函数有一些问题:很多程序需要不同范围的随机数,或者需要随机浮点数,一些程序需要非均匀分布的数。为了解决这些问题而试图转换rand生成的随机数的范围、类型或分布时,常常会引入非随机性。
  • 定义在头文件random中的随机数库通过一些协作类来解决这些问题:随机数引擎类和随机数分布类。
    C++程序不应该使用库函数rand,而应该使用default_random_engine类和恰当的分布类对象。


  • 随机数引擎和分布
    随机数引擎是函数对象类,调用运算符不接受参数并返回一个随机unsigned整数。
    标准库定义了多个随机数引擎类,区别在于性能和随机性质质量不同。



    在大多数场合,随机数引擎输出的原始随机数是不能直接使用的,正确转换随机数的范围是极其困难的。
    分布类型也是函数对象类,分布类型定义了一个调用运算符,接受一个随机数引擎作为参数。分布对象使用它的引擎参数生成随机数,将其映射到指定的分布。
    随机数发生器——是指分布对象和引擎对象的组合。

uniform_int_distribution<unsigned> u(0, 9);
default_random_engine e;
for (size_t i = 0; i < 10; ++i) {
    cout << u(e) << " ";
}
  • 程序每次调用生成不同随机结果的两种方法
    1)一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
    或者再调用随机数发生器的外面定义,在里面使用。
    2)提供一个种子来达到这一目的。
    种子是一个数值,引擎可以利用它从序列中一个新位置重新开始生成随机数。
    利用时间作为种子,time返回以秒计的时间,适用于间隔为秒级或更长的应用。
default_random_engine e1(time(0));
  • 分布类型所支持的操作


  • 生成随机实数
    使用uniform_real_distribution
  • 生成非均匀分布的随机数
    正态分布normal_distribution
  • bernoulli_distribution类,非模板


  • IO库再探——格式控制
    标准库定义了一组操纵符来修改流的格式状态,操纵符返回所处理的流对象。
    操纵符用于两大类输出控制:
    1)控制数值的输出形式
    2)控制补白的数量和位置
    当操作符改变流的格式状态时,通常改变后的状态对所有后续IO都生效,因此大多数操纵符多是设置/复原成对的。




  • IO库再探——未格式化IO
    标准库还提供一组低层操作,支持未格式化IO。这些操作允许将一个流当做一个无解释的字节序列来处理。





    应该在任何后续未格式化输入操作之前调用gcount,peek unget putback会将gcount的返回值置为0.
    注意:低层函数容易出错。因此如果可以使用标准库提供的高层类型操作,就应该使用它们,更加安全。

  • IO库再探——随机访问
    随机IO本质上依赖于系统。
    由于istream和ostream类型不支持随机访问,所以讨论的主要是fstream和sstream类型的随机访问



    在一个流中只维护单一的标记——并不存在独立的读标记和写标记。因此只要在读写操作间切换,必须进行seek操作来重定位标记。

4.2 用于大型程序的工具

  • 大规模应用程序的特殊要求:
    1)在独立开发的子系统之间协同处理错误的能力)——异常处理
    2)使用各种库(可能包括独立开发的库)进行协同开发的能力——命名空间
    3)对比较复杂的应用概念建模的能力——多重继承

  • 异常处理
    异常处理机制允许程序中独立开发的部分能够在运行时对出现的问题进行通信并做出相应的处理。
    异常使得我们能够将问题的检测与解决过程分离开来。

  • 抛出异常
    通过抛出一条表达式来引发一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码将用来处理该异常。被选中的处理代码是在调用链中与抛出对象类型匹配的最近的处理代码。
    执行throw时,程序控制权转移到catch模块。表明:
    1)沿着调用链的函数可能会提早退出
    2)一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
    查找匹配的catch语句:栈展开。栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句为止;或者也可能一直没有找到匹配的catch,则退出主函数后查找过程终止,也会终止当前的程序。
    找到catch并执行完后,找到与try关联的最后一个catch子句之后的点,从这里继续执行。

  • 如果异常发生在构造函数,或者数组、标准库容器的元素初始化的过程中,应该确保已构造的元素被正确地销毁。

  • 在函数中负责释放资源的代码可能被跳过(在此delete之前发生异常)。如果用类来控制资源的分配,就能保证资源能被正确地释放。
    出于栈展开可能使用析构函数的考虑,析构函数不应该抛出不能被它自身处理的异常。实际中,析构函数仅仅是释放资源,不太可能抛出异常,所有标准库类型都能确保它们的析构函数不会引发异常。
    一旦在栈展开过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止。

  • 编译器使用异常抛出表达式对异常对象进行拷贝初始化。
    异常对象位于由编译器管理的空间中,确保无论最终调用的是哪个catch子句都能访问该空间。当异常处理完,异常对象被销毁。
    当抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。
    抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在。

  • 捕获异常
    声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型。可以是左值引用,但不能使右值引用。
    如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。异常声明的静态类型将决定catch语句所能执行的操作,如果catch的参数时基类类型,则catch无法使用派生类特有的任何成员。

  • 查找匹配的处理代码
    派生类异常的处理代码出现在基类异常的处理代码之前。
    只有如下类型转换是允许的,其他的要求异常的类型和catch声明的类型精确匹配:


  • 重新抛出
    一个单独的catch语句不能完整地处理某个异常。可以通过重新抛出的操作将异常传递给另外一个catch语句。
    重新抛出是一个空的throw;语句。


  • 捕获所有异常的处理代码
    catch(...)捕获所有的异常,与其他catch一起出现,放在最后。

  • 函数try语句块与构造函数
    要想处理构造函数初始值抛出的异常,必须将构造函数写成函数try语句块的形式。
    如果在初始化构造函数的参数发生了异常,则该异常属于调用表达式的一部分,并将在调用者所在的上下文处理。不属于构造函数执行的异常。

template <typename T>
Blob<T>::Blob(std::initilalizer_list<T> il) try:
    data(std::make_shared<std::vector<T>>(il)) {
    //空函数体
} catch(const std::bad_alloc &e) {
    handle_out_of_memory(e);
}
  • noexcept异常说明
    预先知道函数不会抛出异常的益处:
    1)有助于简化调用该函数的代码
    2)编译器能执行某些特殊的优化操作
    noexcept要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。
    一旦一个noexcept函数抛出了异常,程序就会调用terminate以确保不在运行时抛出异常的承诺。
  • noexcept运算符
    返回一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。
noexcept(e)
//当e调用的所有函数都做了不抛出说明
//且e本身不含有任何throw语句
//为true
//否则为false

void f() noexcept(noexcept(g())); //f和g的异常说明一致
//里面的noexcept是运算符
//外面的是异常说明符
  • 异常说明与指针、虚函数和拷贝控制
    函数指针及该指针指向的函数必须具有一致的异常说明。不抛出的指针只能指向不抛出异常的函数。可能抛出异常的指针可以指向任何函数。
    虚函数与派生的虚函数类似。
    合成拷贝控制成员时,如果所有成员和基类的所有操作都承诺不抛出异常,则合成的成员是noexcept。否则是noexcept(false)。

  • 异常类的层次



    运行是错误表示的是只有在程序运行时才能检测到的错误;
    逻辑错误一般是可以在程序代码中发现的错误。



  • 命名空间
    多个库将名字放置在全局名字空间中将引发命名空间污染。
    命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中。
    命名空间作用域后面无需分号。
    每个命名空间都是一个作用域。
    命名空间可以是不连续的。
    命名空间的组织方式类似于管理自定义类和函数的方式:
    1)命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中
    2)命名空间成员的定义部分则置于另外的源文件中
    注意:通常不把#include放在命名空间内部。

  • 模板特例化
    模板特例化必须定义在原始模板所属的命名空间中。只要在命名空间中声明了特例化,就可以在命名空间外部定义它。

  • 全局命名空间
    ::member_name表示全局命名空间中的一员。

  • 内联命名空间
    内联命名空间中的名字可以被外层命名空间直接使用。
    当应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到内联命名空间。
    代码可以直接获得新版本的成员,如果想使用老版本的成员,必须加上完整的外层命名空间名字。

  • 未命名的命名空间
    在里面定义的变量拥有静态生命周期。
    一个未命名的命名空间可以在文件内不连续,但不能跨越多个文件。每个文件可以定义自己的未命名空间,并且相互无关联。
    未命名空间的名字直接使用。
    未命名空间定义的名字作用域与该命名空间所在的作用域相同。所以在最外层的名字一定要与全局作用域的名字有所区别。
    注意:在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。

  • 使用命名空间成员
    1)using声明
    一条using声明一次只引入命名空间的一个成员。有效范围从using声明的地方开始,到其所在的作用域结束为止。在此过程,外层作用域的同名实体将被隐藏。
    using声明语句可以出现在全局作用域、局部作用域、命名空间作用域以及类作用域。在类作用域中,声明语句只能指向基类成员。
    2)命名空间的别名
    可以指向一个嵌套的命名空间。
    3)using指示
    所有名字都是可见的。using指示可以出现在全局作用域、局部作用域和命名空间作用域,不能出现在类的作用域中。
    using指示如果不做控制,会重新引入名字冲突问题。

  • using指示与作用域
    using指示一般被看作是出现在最近的外层的作用域中。
    未加限定的相同名字会产生二义性错误,但是这种冲突是允许的,使用时只要明确指出名字的版本即可。



    注意:避免using指示,头文件最多只能在它的函数或命名空间使用using指示或using声明。
    在命名空间本身的实现文件中可以使用using指示。

  • 类、命名空间与作用域
    当给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。
    查找规则的这个例外允许概念上作为类接口一部分的非成员函数无需单独地using声明就能被程序使用。

  • std::move和std::forward
    由于右值引用新参可以匹配任何类型,所以move/forward名字冲突会比其他标准库函数的冲突频繁得多。
    冲突很多,move/forward执行的是非常特殊的类型操作,所以应用程序专门修改函数原有的行为的概率非常小。因此建议使用完整版本std::move。

  • 友元声明与实参相关的查找
    当类声明一个友元时,该友元并没有使得友元本身可见。
    但是涉及到类时,会有例外。(下面的例子只是为说明问题)



  • 重载与命名空间
    命名空间对函数匹配过程的影响:
    1)using声明或using指示将某些函数添加到候选的函数集
    using声明将该函数的所有版本都引入到当前作用域,引入形参完全相同的函数会报错。
    using指示引入完全相同形参的函数不报错,但是要区分版本。
    2)对于接受类类型实参的函数来说,名字查找将在实参所属的命名空间中进行。这些命名空间中与被调用函数同名的函数都将被添加到候选集中


  • 多重继承与虚继承
    多重继承:多个基类相互交织产生的细节可能会带来错综复杂的设计问题与实现问题。


class Bear: pulic ZooAnimal {};
class Panda: public Bear, pulic Endangered {};
  • 假如从多个基类继承了相同的构造函数(即形参列表完全相同),则产生错误。此时该类必须为该构造函数定义它自己的版本。



  • 类型转换与多个基类
    可以令某个可访问基类的指针或引用直接指向一个派生类对象。
    编译器不会在派生类向基类的几种转换中进行比较和选择,转换到任意一种基类都一样好。此时如果有不同基类引用或指针为形参的重载函数,会产生二义性错误。
    对象、指针和引用的静态类型决定了能够使用哪些成员。

  • 多重继承下的类作用域
    在多重继承的情况下,查找过程会在所有直接基类中同时进行。如果名字在多个基类中都被找到,不加前缀限定符直接使用该名字将引发二义性。
    避免潜在的二义性最好的方法是在派生类中为该函数定义一个新版本。

  • 虚继承
    默认情况下,派生类中含有继承链上每个类对于的子部分。某个类在派生过程中出现多次,则派生类将包含该类的多个子对象。这种情况可能有问题。(iostream)
    虚继承机制可以解决该问题。不论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共性的虚基类子对象。


class Raccon: public virtual ZooAnimal {};
class Bear: virtual public ZooAnimal {};
  • 构造函数与虚继承
    虚基类总是由最底层的派生类初始化。
    1)创建Bear或Raccoon对象,此时它们已经位于派生的最底层



    2)创建Panda对象



    如果Panda没有显式初始化ZooAnimal基类,则ZooAnimal的默认构造函数将被调用。如果没有默认构造函数,则代码将发生错误。
    虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。

4.3 特殊工具与技术

  • 控制内存分配
    某些应用程序对内存分配有特殊需求,可以重载new和delete以控制内存分配的过程。
  • new操作的三个步骤
    1)调用一个operator new或operator new[]的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间
    2)编译器运行相应的构造函数以构造这些对象
    3)对象被分配了空间并构造完成,返回一个指向该对象的指针
  • delete操作的两个步骤
    1)对所指对象或数组的元素执行对应的析构函数
    2)编译器调用operator delete或者operator delete[]的标准库函数释放内存空间
  • 应用程序可以在全局作用域中定义operator new和operator delete函数,也可以定义为成员函数。根据作用域查找规则,优先使用自定义版本。
  • 标准库定义的8个重载版本


  • 重载相应的运算符时,如delete,必须使用noexcept异常说明符指定其不抛出异常;
    自定义版本必须位于全局或类作用域;
    当定义在类作用域时,隐式是静态的。因为用在对象构造之前、销毁之后。不能操纵类的任何数据成员;
    用到自定义的new表达式时必须使用new的定位形式。
    void *operator new(size_t, void*); //不允许重新定义这个版本。
void *operator new(size_t size) {
    if (void *mem = malloc(size)) {
        return mem;
    } else {
        throe bad_alloc();
    }
}

void operator delete(void *mem) noexcept { free(mem); }
  • 总之:我们不能改变new和delete运算符的基本含义,只能改变operator new和operator delete改变内存分配的方式。new和delete会调用operator new和operator delete。

  • 定位new表达式
    operator new和operator delete和allocator类的allocate与deallocate成员非常相似,它们负责分配或释放内存空间,但是不会构造或销毁对象。
    operator new分配的内存空间无法使用construct函数构造对象,需要使用定位new形式构造对象。



    place_address必须是一个指针,initializers提供一个可能为空的以逗号分隔的初始值列表,用于构造新分配的对象。
    当仅通过一个地址值调用时,定位new使用void *operator new(size_t, void*)在指定的地址初始化对象以完成整个工作。也即定位new表达式构造对象而不分配内存。

  • construc的指针必须指向同一个allocator对象分配的空间,定位new的指针无须指向operator new分配的内存,甚至不需要指向动态内存。

  • 需要显式的析构函数调用,与destroy类似。与destory一样,析构函数会销毁对象,但不会释放内存。

string *sp = new string("a value");
sp->~string();

  • 运行时类型识别(run-time type identification, RTTI)
    该功能由两个运算符实现:
    1)typeid,用于返回表达式类型
    2)dynamic_cast,用于将基类的指针或引用安全地转换成派生类的指针或引用
    当将这两个运算符用于某种类型指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

  • 这两个运算符特别适用于以下情况:使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。
    使用RTTI运算符蕴含着更多潜在的风险:程序员必须清楚地知道转换的目标类型并且检查类型转换是否被成功执行。
    在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。

  • dynamic_cast运算符
    在下面所有形式中,e类型必须符合以下三种条件的任意一个:
    1)e的类型是模板type的公有派生类
    2)e的类型是目标type的公有基类
    3)e是type的类型
    指针转换事变,返回0;引用转换失败抛出bad_cast异常。
    在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式完成。
    可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。

//type必须是一个类类型,通常该类型应该含有虚函数
dynamic_cast<type*>(e) //e必须是一个有效指针
dynamic_cast<type&>(e)//e必须是一个左值
dynamic_cast<type&&>(e)//e不能是左值

//Base含有虚函数,Derived是Base的公有派生类
if (Derived *dp = dynamic_cast<Derived*>(bp)) {
    //使用dp指向的Derived对象
} else {
    //使用bp指向的Base对象
}

void f(const Base &b) {
    try {
        const Derived &d = dynamic_cast<const Derived&>(b);
        //使用b引用的Derived对象
    } catch (bad_cast) { //不存在空引用,只能处理异常
        //处理失败的情况
    }
}
  • typeid运算符
    typeid(e)
    e可以是任意表达式或类型的名字。操作的结果是一个常量对象的引用。该对象的类型是标准库类型type_info或type_info的公有派生类型。
    1)顶层const被忽略
    2)若是引用,则返回该引用所引对象的类型
    3)作用于数组或函数时,不会执行向指针的标准类型转换
    4)不含虚函数的类,则是静态类型;否则直到运行时才知道结果
    typeid应该作用于对象。当typeid作用于指针时,返回的结果是该指针的静态编译时类型。
  • 使用RTTI
    一种很容易想到的解决方案是定义一套虚函数,令其在继承体系的各个层次上分别执行相等性判断。
    实际上不可以,因为虚函数的基类版本与派生类版本必须具有相同的形参,都是基类引用。此时equal只能比较基类的成员,不能比较派生类成员。



    如果类型相等,则将工作委托给虚函数equal:



    派生类所有函数的第一件事是将实参的类型转换为派生类,这样函数才能返回派生类成员。

  • type_info类



    type_info类一般作为一个基类,应提供一个公有的虚析构函数。没有默认构造函数,拷贝移动构造函数和赋值运算符被定义为删除。
    创建type_info对象的唯一途径是使用typeid运算符。


  • 枚举类型——字面值常量类型
    两种枚举:限定作用域和不限定作用域。
    限定作用域遵循常规的作用域准则,不会自动转换成整型。
    不限定,枚举成员的作用域与枚举类型本身的作用域相同,可以自动转换成整型。
    不能直接将整型值传递给enum形参,可以将不限定作用域的枚举类型传递给整型形参。
//限定作用域
enum class open_modes {input, output, append};

//不限定租用
enum color {red, yellow, green};

enum intValues : unsigned long long { //冒号后制定enum使用的类型(大小)
};

  • 类成员指针
    成员指针是指可以指向类的非静态成员的指针。
    一般情况,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象。
    声明时必须加上classname::表示当前指针可以指向classname的成员。
    为指针赋值时,该指针并没有指向任何数据。只有解引用成员指针时才提供对象的信息。
    常规的访问控制对成员指针同样有效。
    如果希望可以访问私有数据成员,可以顶一个函数,返回值是指向该成员的指针。
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, witdth;
};

const string Screen::*pdata;
pdata = &Screen::contents;
//简单方法是:
auto pdata = &Screen::contents;

Screen myScreen, *pScreen = &myScreen; //这些访问在类内部或友元内部
auto s = myScreen.*pdata;
s = pScreen->*pdata;

class Screen { //破坏了封装性
pulic:
    static const std::string Screen::*data() {
        return &Screen::contents;
    }
}

const string Screen::*pdata = Screen::data();
auto s = myScreen.*pdata;
  • 成员函数指针
    1)成员函数如果有重载,必须显式地声明函数类型以明确指出使用哪个函数
    2)成员函数和指向该成员函数指针之间不存在自动转换规则。
    成员函数指针可以作为函数返回类型或形参类型。
auto pmf = &Screen::get_cursor;

char (Screen::pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;

Screen myScreen, *pScreen = &myScreen;
char c1 = (pScreen->*pmf)();
char c2 = (myScreen.*pmf2)(0, 0);

using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get;

Screen& action(Screen&, Action = &Screen::get);
  • 成员指针函数表
class Screen {
public:
    Screen& home();
    Screen& forward();
    Screen& back();
    Screen& up();
    Screen& down();

    using Action = Screen& (Screen::*)();
    enum Directions { HOME, FORWARD, BACK, UP, DOWN };
    Screen& move(Directions);
private:
    static Action Menu[]; //函数表
};

Screen& Screen::move(Directions cm) {
    return (this->*Menu[cm])();
}

Screen::Action Screen::Menu[] = {
    &Screen::home,
    &Screen::forward,
    &Screen::back,
    &Screen::up,
    &Screen::down,
};

//使用方法:
Screen myScreen;
myScreen.move(Screen::HOME);
myScreen.move(Screen::DOWN);
  • 将成员函数用作可调用对象
    成员指针不是一个可调用对象,不支持函数调用运算符。
    1)使用function生成一个可调用对象
    执行成员函数的对象被传给隐式的this形参。
    2)使用mem_fn生成一个可调用对象——functional头文件
    mem_fn可以根据成员指针的类型推断可调用对象的类型,而无须用户显式地指定。
    mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用。
    3)使用bind生成一个可调用对象
    bind必须将函数中用于表示执行对象的隐式形参转换成显式;
    bind生成的可调用对象的第一个实参可以是指针或引用。
vector<string*> pvec;
function<bool (const string*)> fp = &string::empty;
find_if(pvec.begin(), pvec.end(), fp);

find_if(svec.begin(), svec.end(), mem_fn(&string::empty());
auto f = mem_fn(&string::empty);
f(*svec.begin());
f(&svec[0]);

auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
auto f = bind(&string::empty, _1); //类的this可以通过对象或者指针来绑定
f(*svec.begin());
f(&svec[0]);

  • 嵌套类
    嵌套类可以访问外层类的成员。
    外层类的成员可以像使用任何其他类型成员一样使用嵌套类的名字。
    嵌套类和外层类是相互独立的,外层类对象只包含外层类定义的成员,不会有任何嵌套类的成员。反之亦然。
//声明一个嵌套类
class TextQuery {
pulic:
    class QueryResult;
};

//在外层类之外顶一个嵌套类
class TextQuery::QueryResult {
    friend std::ostream& print(std::ostream&, const QueryResult&);
pulic:
    QueryResult(std::string,
                          std::shared_ptr<std::set<line_no>>,
                          std::shared_ptr<std::vector<std::string>>);
};

//定义嵌套类的成员
TextQuery::QueryResult::QueryResult(string s, 
     shared_ptr<std::set<line_no>> p,
     shared_ptr<std::vector<std::string>> f):
      sought(s), lines(p), file(f) {}

//嵌套类的静态成员定义
int TextQuery::QueryResult::static_mem = 1024;

  • union:一种节省空间的类
    union不能继承自其他类,也不能作为基类使用,因此union不能含有虚函数。
    如果提供了初始值,则该初始值别用于初始化第一个成员。
    在匿名union的定义所在的作用域内该union的成员都是可以直接访问的。
  • 使用类管理union成员
    作为union组成部分的类成员无法自动销毁,因为析构函数不清楚union存储的值是什么类型,所以无法确定应该销毁哪个成员。所以要在管理类的析构函数显式调用union类的析构函数。




    管理需要拷贝控制的联合成员:



    对于左侧运算对象的union是string时,需要特别处理。
Token &Token::operator=(int i) {
    if (tok == STR) sval._string();
    ival = i;
    tok = INT:
    return *this;
}

Token &Token::operator=(const std::string &s) {
    if (tok == STR) {
        sval = s;
    } else {
        new(&sval) string(s); //利用定位new表达式
    }
    tok = STR;
    return *this;
}

  • 局部类
    定义在函数内部的类。和嵌套类不同,局部类的成员受到严格限制。
    局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此局部类不允许声明静态数据成员。
    局部类不能使用函数作用域中的变量。只能访问外层作用域中定义的类型名、静态变量以及枚举成员。




    常规的访问包含规则对局部类同样使用。
    嵌套的局部类:可以在局部类的内部再嵌套一个类。嵌套类必须定义在与局部类相同的作用域中。局部类内的嵌套类也是一个局部类,必须遵循局部类的各种规定。



  • 固有的不可移植特性

  • 1.位域
    类可以将其(非静态)数据成员定义成位域。在一个位域中含有一定数量的二进制位。当一个程序需要其他程序或硬件设备传递二进制数据时,通常会用到位域。
    位域在内存中的布局是与机器相关的。
    通常情况下最好将位域设为无符号类型,存储在带符号类型中的位域行为将因具体实现而定。



    使用位域,通常使用位运算符操作超过1位的位域:





    如果一个类定义了位域成员,则通常会定义一组内联的成员函数以检验或设置位域的值:
  • 2.volatile限定符
    volatile的确切含义与机器有关,只能通过阅读编译器文档来理解。
    程序可能包含一个有系统时钟定时更新的变量,当对象的值可能在程序的控制或检测之外被改变时,应该将对象声明为volatile。关键字volatile告诉编译器不应该对这样的对象进行优化。
    volatile限定符的用法和const很相似。
    const和volatile一个重要区别是不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。因为合成的成员接受的是(非volatile)常量引用,不能将一个非volatile引用绑定到volatile对象上。

  • 3.链接指示:extern "C"
    要想把C++代码和其他语言编写的代码放在一起使用,要求必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的。
    链接指示与函数声明、头文件:




    指向extern "C"函数的指针:
    编写函数所用的语言是函数类型的一部分。如下这种赋值严格意义上来说是非法的,有点编译器可能会接受这种赋值。



    链接指示对整个声明都有效,包括返回值和形参类型的函数指针,如果希望给C++函数传入一个指向C函数的指针,必须使用类型别名:

    使用链接指示导出C++函数到其他语言:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,142评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • 点点星光赠万家, 声声爆竹耀华夏。 戌狗迎春举国欢, 美丽神州盛世花。 祝福祖国,祝福简友。新年快乐!
    公子江沐阅读 294评论 9 24
  • 对于我最亲最爱的人——我的母亲。不是仅能用“感谢”两个词能表达我的情感。 但我仍然要感谢我的母亲,感谢母亲不仅仅是...
    月满钟秀阅读 259评论 0 4
  • 第二次蓝桥杯,原本并不准备参加的,最后还是有点随大流的报名了。 今年的题目个人认为比去年的难了一点点,对于我这种用...
    minlover阅读 256评论 0 2