美文网首页
《C++Primer》第十九章

《C++Primer》第十九章

作者: TOMOCAT | 来源:发表于2020-12-02 01:01 被阅读0次

    第十九章 特殊工具与技术

    控制内存分配

    1. 重载new和delete

    重载这两个运算符与重载其他运算符的过程大不相同。想要真正重载new和delete的方法,首先要对new表达式和delete表达式的工作机制足够了解:

    // new表达式
    string *sp = new string("a value");  // 分配并初始化一个string对象
    string *arr = new string[10];        // 分配10个默认初始化的string对象
    

    当我们使用一条new表达式时,实际上执行了三步操作:

    • 第一步:new表达式调用一个名为operator new或者operator new[]的标准库函数,该函数分配一块足够大的、原始的、未命名的空间以便存储特定类型的对象(或者对象的数组)
    • 第二步:编译器运行相应的构造函数以构造这些对象,并为其传入初始值
    • 第三步:对象被分配了空间并构造完成,返回一个指向该对象的指针
    delete sp;       // 销毁*sp, 然后释放sp指向的内存空间
    delete [] arr;   // 销毁数组中的元素, 然后释放对应的内存空间
    

    当我们使用一条delete表达式删除一个动态分配的对象时,实际上执行了两步操作:

    • 第一步:对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数
    • 第二步:编译器调用名为operator delete或者operator delete[]的标准库函数释放内存空间

    应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以把它们定义为成员函数。当编译器发现一条new表达式或者delete表达式后,将在程序中查找可供调用的operator函数:

    • 如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找
    • 否则在全局作用域中查找,如果找到了用户自定义的版本,则使用该版本执行new或者delete表达式
    • 没找到的话,则使用标准库定义的版本

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

    2. operator new接口和operator delete接口

    标准库定义了operator new函数和operator delete函数的8个重载版本。其中前4个可能抛出bad_alloc异常,后4个版本不会抛出异常:

    // 这些版本可能抛出异常
    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_t&) noexcept;
    void *operator delete(void*, nothrow_t&) noexcept;
    void *operator delete[](void*, nothrow_t&) noexcept;
    

    标准库函数operator new和operator delete的名字让人容易误解。和其他operator函数不同,这两个函数并没有重载new表达式或者delete表达式。实际上我们根本无法自定义new表达式或者delete表达式的行为。一条new表达式的执行过程总是先调用operator new函数以获取内存空间,然后在得到的内存空间中构造对象。与之相反,一条delete表达式的执行过程总是先销毁对象,然后调用operator delete函数释放对象所占空间。

    我们提供新的operator new和operator delete函数的目的在于改变内存分配的方式。

    3. malloc函数和free函数

    malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针或者返回0以表示分配失败。free函数接受一个void*,它是malloc返回的指针的副本,free将相关内存返回给系统。调用free(0)没有任何意义。

    下面给出了operator new和operator delete的简单方式:

    void *operator new(size_t size) {
        if (void *mem = malloc(size))
            return mem;
        else
            throw bad_alloc();
    }
    void operator delete(void *mem) noexcept { free(mem); }
    

    4. 定位new表达式

    C++早期版本中,allocator类还不是标准库一部分。应用程序如果想把内存分配和初始化分离开的话,需要调用operator new和operator delete。这两个函数的行为与allocator的allocate成员和deallocate成员非常类似,它们负责分配或释放内存空间,但是不会构造或销毁对象

    与allocator不同的是,对于operator new分配的内存空间,我们不能使用construct函数构造对象。相反我们应该用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与allocator的construct非常相似,但是有一个重要的区别:我们传给construct的指针必须指向同一个allocator对象分配的空间,但是传给定位new的指针无须指向operator new分配的内存,甚至不需要指向动态内存。

    5. 显式的析构函数调用

    就像定位new与使用allocate类似一样,对析构函数的显式调用也与使用destroy很类似。

    string *sp = new string("a value");   // 分配并初始化一个string对象
    sp->~string();
    

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

    调用析构函数会销毁对象,但是不会释放内存。

    运行时类型识别

    运行时类型识别run-time type identification, RRTTI的功能由两个运算符实现:

    • typeid运算符,用于返回表达式的类型
    • dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用

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

    这两个运算符特别适用于如下情况:当我们想使用几类对象的指针或者引用执行某个派生类操作并且该操作不是虚函数。一般来说,只要有可能我们应该尽量使用虚函数,当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。

    然而并非任何时候都能定义一个虚函数。假设我们无法使用虚函数,那么可以使用一个RTTI运算符。另一方面,与虚成员函数相比,使用RTTI运算符蕴涵着更多潜在的风险:程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。

    使用RTTI必须加倍小心,在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。

    1. dynamic_cast运算符

    dynamic_cast运算符的使用形式如下所示:

    dynamic_cast<type*>(e)   // e必须是一个有效的指针
    dynamic_cast<type&>(e)   // e必须是一个左值
    dynamic_cast<type&&>(e)  // e不能是左值
    

    在上面的所有形式中,e的类型必须符合以下三个条件的任意一个:

    • e的类型是目标type的公有派生类
    • e的类型是目标type的公有基类
    • e的类型是目标type本身

    如果符合则转换可以成功,否则转换失败。如果一条dynamic_cast的转换目标是指针类型并且失败了,则结果为0;如果转换目标是引用类型并且失败了,则抛出一个bad_cast异常。

    1.1 指针类型的dynamic_cast

    假定Base类至少含有一个虚函数,Derived是Base的公有派生类。如果有一个指向Base的指针bp,则我们在运行时将它转换成指向Derived的指针:

    if (Derived *dp = dynamic_cast<Derived*>(bp))
    {
        // 使用dp指向的Derived对象
    } else {  // bp指向一个Base对象
        // 使用bp指向的Base对象
    }
    
    1.2 引用类型的dynamic_cast
    void f(const Base &b)
    {
        try {
            const Derived &d = dynamic_cast<const Derived&>(b);
            // 使用b引用的Derived对象
        } catch (bad_cast) {
            // 处理类型转换失败的情况
        }
    }
    

    2. typeid运算符

    typeid可以作用于任意类型的表达式。和往常一样,顶层const被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。不过当typeid作用于数组或者函数时,并不会执行向指针的标准类型转换。比如我们对数组a执行typeid(a)。所得的结果是数组类型而非指针类型。

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

    通常情况下我们使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同:

    Derived *dp = new Derived;
    Base *bp = dp;   // 两个指针都指向Derived对象
    
    // 在运行时比较两个对象的类型
    if (typeid(*bp) == type(*dp)) {
        // bp和dp指向通医药类型对象 
    }
    // 检查类型是否是某种指定类型
    if (typeid(*bp) == typeid(Derived)) {
        // bp实际指向Derived类型
    }
    

    注意typeid应该作用于对象,因此我们使用*bp而不是bp:

    // 下面检查永远失败: bp类型是指向Base的指针
    if (typeid(bp) == typeid(Derived)) {
        // 此处代码永远不会执行
    }
    

    当typeid作用于指针时(而非指针指向的对象),返回的结果是该指针的静态编译时类型。

    3. 使用RTTI

    在某些情况下RTTI非常有用,比如我们想为具有继承关系的类实现相等运算符时。对于两个对象来说,如果他们的类型相同并且对应的数据成员取值相同,则我们说这两个类是相等的。

    我们定义两个示例类:

    class Base {
        friend bool operator==(const Base&, const Base&);
    public:
        // Base的接口成员
    protected:
        virtual bool equal(const Base&) const;
        // Base的数据成员和其他用于实现的成员
    };
    
    class Derived: public Base {
    public: 
        // Derived的其他接口成员
    protected:
        bool equal(const Base&) const;
        // Derived的数据成员和其他用于实现的成员
    };
    

    类型敏感的相等运算符:

    bool operator==(const Base &lhs, const Base &rhs)
    {
        // 如果typeid不相同,则返回fallse; 否则虚调用equal
        // 当运算对象是Base的对象时调用Base::equal, 否则调用Derived::equal
        return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
    }
    

    虚equal函数:继承体系中的每个类都必须定义自己的equal函数,派生类的所有函数要做的第一件事情就是将实参的类型转换为派生类类型:

    bool Derived::equal(const Base &rhs) const
    {
        // 我们清楚两个类型是相等的, 所以转换不会抛出异常
        auto r = dynamic_cast<const Derived&>(rhs);
        // 执行比较两个Derived对象的操作并返回结果
    }
    

    基类equal函数:

    bool Base::equal(const Base &rhs) const
    {
        // 执行比较Base对象的操作    
    }
    

    4. type_info类

    type_info的操作包括:

    • t1 == t2:如果type_info对象t1和t2表示同一种类型,则返回true
    • t1 != t2:如果type_info对象t1和t2表示不同的类型,则返回true
    • t.name():返回一个C风格字符串,表示类型名字的可打印形式
    • t1.before(t2):返回一个bool值,表示t1是否位于t2之前,顺序关系依赖于编译器

    type_info类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义为删除的。因此,我们无法定义或者拷贝type_info类型的对象,也不能为type_info对象赋值。创建type_info对象的唯一途径就是使用typeid运算符。

    枚举类型

    C++包含两种枚举:限定作用域和不限定作用域的。C++新标准引入了限定作用域的枚举类型。

    定义限定作用域的枚举类型:

    enum class open_modes {input, output, append};
    // 等价
    enum struct open_modes {input, output, append};
    

    定义不限定作用域的枚举类型:

    • 省略掉关键字class
    • 枚举名字是可选的
    enum color {red, yellow, green};
    enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
    

    1. 枚举也可以定义新的类型

    enum color {red, yellow, green};          // 不限定作用域的枚举类型
    enum stoplight {red, yellow, green};      // 错误: 重复定义了枚举成员
    enum class peppers {red, yellow, green};  // 正确: 枚举成员被隐藏了
    
    int i = color::red;     // 正确: 不限定作用域的枚举类型的枚举成员隐式地转换成int
    int j = peppers::red;   // 错误: 限定作用域的枚举类型不会进行隐式转换
    

    2. 指定enum的大小

    尽管每个enum都定义了唯一的类型,但是实际上enum是由某种整数类型表示的。在C++11新标准中,我们可以在enum的名字后加上冒号以及我们想在该enum使用的类型:

    enum intValues : unsigned long long {
        charTyp = 255, shortTyp = 65535, intTyp = 65535,
        longTyp = 4394967295UL,
        long_longTyp = 18446744073709551615ULLL
    };
    

    3. 形参匹配与枚举类型

    // 不限定作用域的枚举类型,潜在类型因机器而异
    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;
    }
    

    类成员指针

    成员指针是指可以指向类的非静态成员的指针。一般情况下,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象。类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针和普通指针没有任何区别。

    成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才给提供成员所属的对象。

    为了解释成员指针的原理,我们使用该Screen类:

    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;
    };
    

    1. 数据成员指针

    与普通指针不同的是,成员指针还必须包含成员所属的类。因此,我们必须在*之前添加classname::以表示当前定义的指针可以指向classname的成员,例如:

    // pdata可以指向一个常量(非常量)Screen对象的string成员
    // 将pdata声明为"一个指向Screen类的const string成员的指针"
    const string Screen::*pdata;
    

    当我们初始化一个成员指针(或者向它赋值)时,需要指定它所指的成员。例如我们可以令pdata指向某个非特定Screen对象的contents成员:

    pdata = &Screen::contents;
    

    在C++11新标准中声明成员指针最简单的方法是使用auto或者decltype:

    auto pdata = &Screen::contents;
    
    1.1 使用数据成员指针

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

    我们可以通过.*->*两个运算符解引用指针以获得该对象的成员:

    Screen myScreen, *pScreen = &myScreen;
    // .*解引用pdata以获得myScreen对象的contents成员
    auto s = myScreen.*pdata;
    // ->*解引用pdata以获得pScreen所指对象的contents成员
    s = pScreen->*pdata;
    
    1.2 返回数据成员指针的函数

    Screen的contents成员是私有的,因此之前对于pdata的使用必须位于Screen类的成员或友元内部,否则程序将发生错误。如果像Screen这样的类希望我们可以访问它的contents成员,最好定义一个函数:

    class Screen {
    public:
        // data是一个静态成员, 返回一个成员指针
        static const std::string Screen::*data()
            { return &Screen::contents; }
    }
    
    // 我们调用data函数时, 将得到一个成员指针
    // data()返回一个指向Screen类的contents成员的指针
    const string Screen::*pdata = Screen::data();
    
    // pdata指向Screen类的成员而非实际数据, 要想使用pdata必须把它绑定到Screen类型的对象上
    auto s = myScreen.*pdata;
    

    2. 成员函数指针

    我们也可以定义指向类的成员函数的指针:

    // pmf是一个指针, 它可以指向Screenn的某个常量成员函数
    // 前提是该函数不接受任何实参, 并且返回一个char
    auto pmf = &Screen::get_cursor;
    
    • 指向成员函数的指针也需要指定目标函数的返回类型和形参列表
    • 如果成员函数是const成员或引用成员,我们必须将const限定符或者引用限定符包含进来
    • 如果成员存在重载的问题,那么我们必须显式地声明函数类型以明确指出来我们想要使用的是哪个函数
    // 例如我们可以声明一个指针, 令其指向含有两个形参的get:
    char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
    pmf2 = &Screen::get;  // 必须加取地址符&, 在成员函数和指针之间不存在自动转换规则
    
    2.1 使用成员函数指针
    Screen myScreen, *pScreen = &myScreen;
    // 通过myScreen所指的对象调用pmf所指的函数
    char c1 = (pScreen->*pmf)();
    // 通过myScreen对象将实参0, 0 传给含有两个形参的get函数
    char c2 = (myScreen.*pmf2)(0, 0);
    
    2.2 使用成员指针的类型别名

    使用类型别名或者typedef可以让成员指针更容易理解,例如下面的类型别名将Action定义为两个参数get函数的同义词:

    // Action是一种可以指向Screen成员函数的指针, 它接收两个pos实参, 返回一个char
    using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
    

    通过使用Action,我们可以简化指向get的指针定义:

    Action get = &Screen::get;  // get指向Screen的get成员
    

    我们可以将指向成员函数的指针作为某个函数的返回类型或者形参类型:

    // action接受一个Screen的引用和一个指向Screen成员函数的指针
    Screen& action(Screen&, Action = &Screen::get);
    
    Screen myScreen;
    // 等价调用
    action(myScreen);                // 使用默认实参
    action(myScreen, get);           // 使用我们之前定义的变量get
    action(myScreen, &Screen::get);  // 显式地传入地址 
    
    2.3 成员指针函数表

    对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中。如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个。假定Screen类中含有几个成员,每个函数负责将光标向指定的方向移动:

    class Screen {
    public:
        // 其他接口和实现成员与之前一致
        // 这几个函数共同点: 不接受任何参数, 并且返回值是发生光标移动的Screen的引用
        Screen& home();        // 光标移动函数
        Screen& froward();
        Screen& back();
        Screen& up();
        Screen& down();
    }
    

    我们希望定义一个move函数,使其可以调用上面任意一个函数并执行对应的操作。为了支持这个新函数,我们将在Screen中添加一个静态成员,该成员是指向光标移动函数的指针的数组:

    class Screen {
    public:
        // Action是一个指针, 可以用任意一个光标移动函数对其赋值
        using Action = Screen& (Screen::*)();
        // 指定具体要移动的放共享
        enum Directions { HOME, FORWARD, BACK, UP, DOWN };
        Screen& move(Directions);
    private:
        static Action Menu[];   // 函数表
    };
    
    Screen& Screen::move(Directions cm)
    {
        // 运行this对象中索引值为cm的元素
        return (this->*Menu[cm])();  // Menu[cm]指向一个成员函数
    }
    
    Screen::Action Screen::Menu[] = {
        &Screen::home,
        &Screen::forward,
        &Screen::back,
        &Screen::up,
        &Screen::down,
    };
    

    当我们调用move函数式,给它传入一个表示光标移动方向的枚举成员:

    Screen myScreen;
    myScreen.move(Screen::HOME);   // 调用myScreen.home
    myScreen.move(Screen::DOWN);   // 调用myScreen.down
    

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

    要想通过有一个指向成员函数的指针进行函数调用,必须首先利用.*或者->*运算符将该指针绑定到特定的对象上。因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。

    因为成员指针不是可调用对象,因此我们不能直接将一个指向成员函数的指针传递给算法。比如我们想在一个string的vector中找到第一个空string,显然不能这么写:

    auto fp = &string::empty;   // fp指向string的empty函数
    // 错误, 必须使用.*或者->*调用成员指针
    find_if(svec.begin(), svec.end(), fp);
    
    // 在find_if内部试图执行如下代码, 但是要想通过成员指针调用函数, 必须使用该->*运算符, 所以失败
    if (fp(*it))
    
    3.1 使用fuction生成一个可调用对象
    vector<string> svec;
    function<bool (const string&)> fcn = &string::empty;
    find_if(svec.begin(), svec.end(), fcn);
    
    vector<string*> pvec;
    function<bool (const string*)> fp = &string::empty;
    find_if(pvec.begin(), pvec.end(), fp);
    
    3.2 使用mem_fn生成一个可调用对象
    find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
    
    // mem_fn生成的对象可以通过对象调用, 也可以通过指针调用
    auto f = mem_fn(&string::empty);  // f接收一个string或者一个string*
    f(*svec.begin());    // 正确: 传入一个string对象, f使用.*调用empty
    f(&svec[0]);         // 正确: 传入一个string指针, f使用->*调用empty
    
    3.3 使用bind生成一个可调用对象
    auto it = find_if(svec.begin(), svec.end(), bing(&string::empty, _1));
    
    // bind生成的可调用对象第一个实参既可以是string的指针, 也可以是string的引用
    auto f = bind(&string::empty, _1);
    f(*svec.begin());
    f(&svec[0]);
    

    嵌套类

    一个类可以定义在另一个类的内部,前者被定义为嵌套类。嵌套类的名字在外层类作用域中是可见的,在外层作用域之外不可见。

    1. 声明一个嵌套类

    我们为TextQuery类定义了一个名为QueryResult的配套类。QueryResult类的主要作用是表示TextQuery对象上query操作的结果,显然将QueryResult用作其他目的没有任何意义。

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

    2. 在外层类之外定义一个嵌套类

    // QueryResult是TextQuery的成员
    class TextQuery::QueryResult {
        // 位于类的作用域内, 因此我们不必对QueryResult形参进行限定
        friend std::ostream& print(std::ostream&, const QueryResult&);
    public:
        // 嵌套类可以直接使用外层类的成员, 无须对该名字进行限定
        QueryResult(std::string, std::shared_ptr<std::set<line_no>>,
                   std::shared_ptr<std::vector<std::string>>);
    };
    

    3. 定义嵌套类的成员

    TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p,
                                       std::shared_ptr<std::vector<std::string>> f) :
        sought(s), lines(p), file(f) { }
    

    union: 一种节省空间的类

    联合union是一种特殊的类,一个union可以有多个数据成员,但是在任意时刻只有一个数据成员有值。当我们给union的某个成员赋值之后,该union的其他成员就变成未定义的状态了。

    1. 定义union

    union提供了一种有效的途径使得我们可以方便地表示一组类型不同的互斥值。举个例子,假设我们需要处理一些不同种类的数字数据和字符数据,则可以定义一个union来保存这些值:

    // Token类型的对象只有一个成员, 该成员的类型可能是下列类型中的任意一个
    union Token {
        // 默认情况下成员是公有的
        char cval;
        int ival;
        double dval;
    };
    

    2. 使用union类型

    和其他内置类型一样,默认情况下union是未初始化的,我们可以像显式地初始化聚合类一样用一对花括号内的初始值显式地初始化一个union:

    Token first_token = {'a'};      // 初始化cval成员, 如果提供初始值则用于初始化第一个成员
    Token last_token;               // 未初始化的Token对象
    Token *pt = new Token;          // 指向一个未初始化的Token对象的指针
    

    3. 匿名union

    union {
        char cval;
        int ival;
        double dval;
    };  // 未命名对象, 我们可以直接访问它的成员
    cval = 'a';    // 为匿名union赋一个新值
    ival = 42;     // 该对象当前保存的值是42
    

    4. 其他

    由于现在电脑普遍内存较大,使用union的地方比较少,故这一块后续碰上了再学习

    局部类

    类可以定义在某个函数的内部,我们称这样的类为局部类local class

    • 局部类的成员必须完整定义在类的内部,所以成员函数的复杂性不能太高,一般只有几行代码
    • 在局部类中不允许声明静态数据成员

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

    局部类只能访问外层作用于定义的类型名、静态变量以及枚举成员。

    int a, val;
    void foo(int val)
    {
        static int si;
        enum Loc { a = 1024, b };
        // Bar是foo的局部类
        struct Bar {
            Loc locVal;
            int barVal;
            
            void fooBar(Loc l = a)
            {
                barVal = val;    // 错误, val是foo的局部变量
                barVal = ::val;  // 正确: 使用一个全局变量
                barVal = si;     // 正确: 使用一个静态局部对象
                locVal = b;      // 正确: 使用一个美剧成员
            }
        };
        // ...
    }
    

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

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

    固有的不可移植的特性

    为了支持低层编程,C++定义了一些固有的不可移植的特性。所谓不可移植的特性是指因机器而异的特性,当我们将不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。

    1. 位域

    类可以将其(非静态)数据成员定义成位域bit-field,在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或者硬件设备传递二进制数据时,通常会用到位域。

    typedef unsigned int Bit;
    class File {
        Bit mode: 2;         // mode占两位
        Bit modified: 1;     // modified占1位
        Bit prot_owner: 3;   // 占3位
        Bit prot_group: 3;   // 占3位
        Bit prot_world: 3;   // 占3位
    public:
        // 文件以八进制的形式表示
        enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };
        File &opne(modes);
        void close();
        void write();
        bool isRead() const;
        void setWrite();
    };
    
    • 如果可能的话,在类的内部连续定义的位域液压锁在同一整数的相邻位,这意味着前面五个位域可能会存储在一个unsigned int中,这些二进制位能否压缩到一个整数中以及如何压缩是与机器相关的
    • 取地址运算符&不能作用域位域,因此任何指针都无法指向类的位域
    • 最好将位域设为无符号类型,存储在带符号类型中的位域的行为将因具体实现而定

    2. volatile限定符

    直接处理硬件的程序通常包含这样的数据元素,例如程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile,告诉编译器不应对这样的对象进行优化。

    volatile int display_register;    // 该int值可能发生改变
    

    3. 链接指示: extern "C"

    C++程序有时候需要调用其他语言编写的函数(比如C语言)。其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返回类型和形参类别。

    3.1 声明一个非C++函数
    // cstring头文件中C函数的声明
    // 单语句链接指示
    extern "C" size_t strlen(const char *);
    // 复合语句链接指示
    extern "C" {
        int strcmp(const char*, const char*);
        char *strcat(char*, const char*);
    }
    
    3.2 链接指示与头文件
    // 复合语句链接指示
    extern "C" {
    #include <string.h>  // 操作C风格字符串的C函数
    }
    

    上面的写法意味着头文件中所有普通函数声明都被认为是由链接指示的语言编写的。

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

    指向C函数的指针和指向C++函数的指针是不一样的类型

    3.4 链接指示对整个声明都有效
    // f1是一个C函数, 它的形参是一个指向C函数的指针
    extern "C" void f1(void(*)(int));
    
    3.5 导出C++函数到其他语言

    通过链接指针对函数进行定义,我们可以令一个C++函数在其他语言编写的程序中可用:

    // calc函数可以被C程序调用
    extern "C" double calc(double dparm) { /*...*/) }
    
    3.6 重载函数与链接指示

    C语言不支持函数重载,因为也就不难理解一个C链接指示只能用于说明一组重载函数中的某一个了:

    // 错误: 两个extern "C"函数的名字相同
    extern "C" void print(const char*);
    extern "C" void print(int)
    

    相关文章

      网友评论

          本文标题:《C++Primer》第十九章

          本文链接:https://www.haomeiwen.com/subject/hizewktx.html