• C++ 关键字浅谈


      这里有一个游戏:要求写一个符合C++标准的程序,包含至少十个连续而且不同的关键字。连续是指不能被标识符、运算符、标点符号分割。注意这里的“不同”要求,别想用

    int main() { return sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof (int); }

    这个交卷,而且这个可以任意长。动动脑经,应该是可以想出来的。我们从很久很久以前(long long ago)开始吧,

    unsigned long long int ago;
    const volatile unsigned long long int ago;
    extern const volatile unsigned long long int ago;
    extern inline const volatile unsigned long long int f();
    template extern inline const volatile unsigned long long int operator *(const Type&);
    template extern inline const volatile unsigned long long int operator and(const Type& lhs, const Type& rhs); // 如果允许运算符&变成关键字and

    我们强调了不同的关键字,所以只好用 long int 而不是 long long int
    typeid 也可以级联,与 sizeof 不同的是,必须添加括号。如果参数是表达式而非数据类型,sizeof 可以不用加括号。new 则没有这个限制。

    #include <typeinfo>
    
    int main()
    {
        const std::type_info& info = typeid(typeid(typeid(typeid(int))));
        return 0;
    }

    C++规范里还要求使用 typeid 关键字的时候必须 #include <typeinfo>,否则任何使用到的地方都是 ill-formed。new 有时候也需要 #include <new> 头文件。

    sizeof new const volatile signed long int();

    限定不同的关键字,目前最好的答案是:

    int main()
    {
        if(false);
            else
        do
            throw sizeof new const volatile signed long int();
        while(false);
        return 0;
    }
    spoiler alert

      回到正题,C++ 诞生于1983年,现在有很多的关键字了,这里有详细的列表。


      C++ 的一些运算符和标点符号需要 ISO 646 的代码集之外的字符:{, }, [, ], #, \, ^, |, ~。要能够使用不存在这些字符编码的字符集(如德国的 DIN 66003),C++定义了两种替代方案:额外的关键字对应这些操作符,使用 ISO 646 兼容的字符构成的特殊的两元组或三元组解释成一个非 ISO 646 字符。

    首选 && &= & | ~ ! != || |= ^ ^=
    替代方案 and and_eq bit_and bitor compl not not_eq or or_eq xor xor_eq

    使用两元组和三元组可能会遇到一些奇怪的问题,而且也影响代码阅读。C++标准打算在C++17版本废除掉三元组符号 ??< ??> ??( ??) ??= ??/ ??' ??! ??- <: 会被替换成 [ 符号,于是 std::vector<::std::string> 会被错误的当成 std::vector[:std::string> 对待。现在的键盘都有这些符号,所以不用用他们的替代符号。毕竟符号比字符串易懂,想想数学运算中的加减乘除符号如果用 add/subtract/multiply/devide 英文替换表示,读起来就不舒服,这也是为什么C++引入操作符重载,矩阵的运算可以直接写 (A+B)/(C*D); 而不是 divide(add(A, B), multiply(C, D));

      基本数据类型 void bool char wchar_t short int long unsigned float double,布尔值 true false,以及 C++11 新增的 char16_t char32_t nullptr 关键字。
    char、signed char、unsigned char 是不同的类型,这个需要注意一下。GCC 编译的时候,可以用编译选项 -fsigned-char 或 -funsigned-char,它们分别将 char 指定为 signed char 或 unsigned char。很多程序直接写char,希望它是有符号的或者无符号的。程序员喜欢简短,跟 int 关键字一样,不写就默认有符号的类型;但有时候处理二进制流,表示成[0, 256)区间的整数。
      看到这个表格,原来整形有这么多。其实我更喜欢用简短的 uint8_t/int8_t/.../int32_t 等。你会看到很多代码工程都自己定义了一套整形数据,避免 16位整形的程序移植到32位系统、32位整形的程序移植到64位系统 出现问题。而且C++ 对关键字的修饰顺序也没有要求,这让一些程序员甚是纠结。

    GNU 的 STL 库也有这种顺序的 long unsigned int
    #ifndef __SIZE_TYPE__
    #define __SIZE_TYPE__ long unsigned int
    #endif

      true/false 的类型是 bool,那 nullptr 的类型是什么?

    typedef decltype(nullptr) nullptr_t;

    这种类型定义妙不可言,也横空推出新的关键字 decltype。如果两个重载的函数 fun 可以接受不同的指针类型,比如 void fun(int*); void fun(float*); 那么编译 fun(NULL) 会有二义性错误。不妨再定义一个 std::nullptr_t 空指针类型的函数 void fun(std::nullptr_t nullp),这次 fun(NULL) 编译失败但是 fun(nullptr) 可以通过。

      wchar_t 用来表示一个 Unicode 字符集中的编码,在 Windows 上是 UTF-16,类 Unix 系统上是 UTF-32。sizeof(wchar_t) 是 implementation defined,移植性差。一般在Windows上是2个字节,在类 Unix 系统上是4个字节。Windows 接受了宽字符并使之成为标准,可以看到许多Windows API 有两个版本,functionNameA 和 functionNameW 分别对应ANSI版本和宽字符版本。这篇博客列举了使用wchar_t可能犯的错误。

      C规范中并没有写明宽字符 wchar_t 的具体类型,与编译器实现相关,可能8/16/32位,也可能是signed 或者 unsigned。关键在于选用的编码字符集,注意字符集(Charset)与字符编码(Character Encoding)的区别,ASCII/GBK/BIG5/GB18030 是字符集,Unicode 是字符集。当时各个国家都做了自己的编码方案,中国有 GBK/GB18030/BIG5 等,这在国内没有问题,但是网络遍及世界各地,外面的人访问就出现乱码。于是 Unicode 应运而生。Unicode 字符集有 UTF-7/UTF-8/UTF-16/UTF-32 这几种编码。
    推荐使用 Unicode 字符,不推荐使用 wchar_t,取而代之使用固定长度的 char16_t/char32_t 类型。

    ISO/IEC 10646:2003 Unicode 标准 4.0 里讲到:
    "The width of wchar_t is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compiler should not use wchar_t for storing Unicode text. The wchar_t type is intended for storing compiler-defined wide characters, which may be Unicode characters in some compilers."

      C 语言关键字 int 是用的最多的,我经常觉得 short/long 的修饰让 int 显得多余。short int 与 short 语义一样,long int 与 int 语义一样。long 与 short 是相对来讲的,于是又出现了 long long int 这一类型。long long 是1995年提议加没加成,C++ 因为 C 没有加所以也不肯加入。很多编译器都自己实现了,10年之后,大家觉得有必要标准化了。(说不定将来某一天,整数的计算范围需要扩充到 128 位,于是要唤出 long long long long int?好囧)个人觉得,程序语言诞生的时候,就应该尽量让关键字语义正交,虽然标准规定了 sizeof(short) <= sizeof(int) <= sizeof(long),但是很多编译器还是将 int 与 long int 等同看待。C 语言可移植好不由争辩;Java 很聪明,直接规定了各自的长度。short/int/long 分别为 2/4/8 个字节。
      有一个头文件 <stdint.h> 专门定了各种整形数 int8_t/int16_t/int32_t/int64_t 等,还有 unsigned 类型。其实 short/long/signed 都是形容词,int 是名词,所以声明整形没有以 int 结尾也是对的,与其写 long long int,不如写 long long,甚至 int64_t。我也想过如果 sizeof(int) == sizeof(long int),在 32 位机器上,要么 long 多余,要么 int 多余。然后我想到了 long double 类型,觉得还是 int 多余吧。

      选取关键字要尽量做到语法正交,尽量能表达所有的意思,小部分可以留给扩展。从 C++11 的改变可以看出,尽量不添加新的关键字,双中括号扩展 [[]] 和右值符号 &&;不得已时添加新的关键字 alignas/alignof/thread_local 等。对差不多不用的关键字回收,auto 重新焕发光芒,template 能做的活,auto 现在也可以简单完成。
      添加新的关键字的时候,需要考虑几乎不会在历史代码中用到的。一些上下文相关的(contex-sensitive)关键字 final,override 的出现。它们不算关键字,但是放到函数末尾可以让编译器查错。可想而知,如果像 final override 这样常见的单词选入关键字,会有多少历史代码需要修改。很显然,final 和 override 是从 Java 语言中学过来的。Python 从 2.x 升到 3.x,各种不兼容,让人对这个语言又爱又恨,虽然有帮助迁移代码的文档和工具。
      因为 final 与 override 是上下文相关的,所以你这么写代码也会编译通过。

    class Base
    {
    public:
        virtual int override(int )
        {
            std::cout << "please override me" << std::endl;
            return 0;
        }
    };
    
    class Fun: public Base
    {
    public:
        virtual int override(int ) override final
        {
            int override = 42;
            return override;
        }
    };
    
    int main()
    {
        int me = 0x3e;
        Fun hey;
        hey.override(me);
    
        return 0;
    }

      在 C++11 之前,C++ 标准交了很多东西给编译器自行处理,比如是否自行产生构造函数,内存对齐,RVO 优化,enum 的类型等等,现在都可以显式要求,避免各搞各的一套。enum 的类型是 implementation defined,很多编译器都是用能容纳的最小的整形范围来表示。所以你会在微软的很多代码里看到很多这样的写法:

    typedef enum D3DTEXTUREADDRESS { 
        D3DTADDRESS_WRAP         = 1,
        D3DTADDRESS_MIRROR       = 2,
        D3DTADDRESS_CLAMP        = 3,
        D3DTADDRESS_BORDER       = 4,
        D3DTADDRESS_MIRRORONCE   = 5,
        D3DTADDRESS_FORCE_DWORD  = 0x7fffffff
    } D3DTEXTUREADDRESS, *LPD3DTEXTUREADDRESS;

    最后一个 enum 常量是  XXX_FORCE_DWORD  = 0x7fffffff,对其取值 sizeof(D3DTEXTUREADDRESS) 长度固定为 4。

      对齐本来是语言设计者想掩盖的细节,不过在C++11编程方式越发复杂的情况下,提供给用户更底层的手段往往是必不可少的。在一些情况下,用户虽然不能保证总是写出平台无关,或者说各平台习惯你能最优的代码,但只需要改造 alignas 之后的对齐值参数就可以保证程序的移植性及性能良好,也不失为一种好的选择。而 C++11 对对齐方式的支持从语法规则到库,基本上考虑了各种情况,可以说是相当完备的。

    template <typename T>
    class alignas(sizeof(T)<<2) Color
    {
        T r, g, b, a;
    };

      让关键字增加功能。以前 default 和 delete 只有一种用途:default 就是 switch 语句里的默认分支,delete 就是释放内存。现在都多了一种用法 = delete 表示删除函数,= default 表示使用编译期默认产生的。以前 using 只能与 namespace 为伴,而现在,typedef 所干的活,全部可以接手过来,而且在写法上更直观易懂。以前写 typedef long long int int64; 现在你可以写 using int64 = long long int; 怎么样?数据类型也可以用等号“赋值”了。
      C++11 出来后,你不用看编译器的脸色行事了。这里不需要定义构造函数,好让编译器自己生成;那里需要定义构造函数,否则编译器会做 shallow copying。现在你可以显示指示编译器怎么做。

    class noncopyable
    {
    private:
        noncopyable(const noncopyable& );            // not defined
        noncopyable& operator=(const noncopyable&);  // not defined
    };

      之前,我们不想让对象之间赋值,会讲赋值构造函数和 operator = 限制为 private,并不提供定义。现在我们有更好的写法:

    class noncopyable
    {
    public:
        noncopyable(const noncopyable& ) = delete;
        noncopyable& operator=(const noncopyable&) = delete;
    };

      声明为 private 可以阻止被调用。故意不实现这些函数,意味着如果外部通过成员函数或者 friend 类想访问它们,虽然编译可以通过,但是链接的时候会因未定义的符号而报错。在 C++11 里,情况就不一样了。使用 = delete 表示编译期不会产生这个方法,所以这个可以扼杀在编译期,不用推迟到链接的时候。虽然这里的访问权限对 C++11 不重要,但一般来说,标记 delete 的函数最好声明 public,而不是 private。因为有的编译器在检查成员函数的调用时,只报访问权限错误(错误如下)。很显然,这里 delete 才是重点,或者说错误的优先级高些。当然,并不是只有成员函数才能 delete,非成员函数和模版实例化的函数也是可以的。相比 C++98 而言,算是一项改进。

    error: ‘pea::Log::Log(const pea::Log&)’ is private
    Log(const Log& log) = delete;

      提醒一下,using namespace std; 不要写在 .h 文件里,要写也是写在 .cpp 文件里。出于头文件会被其他头文件包含的可能,污染命名空间,但是 .cpp 文件不会。除非你故意想 #include "XXX.cpp" 这么写。在cocos2d-x 里的 class HelloWorld 里面添加方法 void onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event); 会编译不过,报错如下:

    1>classesHelloWorldScene.h(17): error C2653: 'EventKeyboard' : is not a class or namespace name (..ClassesAppDelegate.cpp)
    1>classesHelloWorldScene.h(17): error C2061: syntax error : identifier 'KeyCode' (..ClassesAppDelegate.cpp)
    1>classesHelloWorldScene.h(17): error C2653: 'EventKeyboard' : is not a class or namespace name (..ClassesHelloWorldScene.cpp)
    1>classesHelloWorldScene.h(17): error C2061: syntax error : identifier 'KeyCode' (..ClassesHelloWorldScene.cpp)
    1>classesHelloWorldScene.cpp(72): error C2276: '&' : illegal operation on bound member function expression

    其实修改很简单,添加 EventKeyboard 和 KeyCode 所在的命名空间就好了。

    void onKeyReleased(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);

    然而在 HelloWorldScene.cpp 里不用添加,因为开头有这么一句 USING_NS_CC; 也就是 using namespace cocos2d; 引入了 cocos2d 所有的东西。

    void HelloWorld::onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event)
    {
      //TODO
    }

      virtual 关键字用来申明虚函数,在虚函数的末尾添加 =0 表示纯虚函数。在函数名和参数可能动态变化的情况下,需要手动检查基类与派生类的函数是否一致,是否派生类的成员函数覆盖了基类的成员函数。这种枯燥的工作应该交给编译器来做的,于是 C++11 引入了上下文相关的关键字 override,在需要覆盖的地方没有覆盖,编译器就会报错。

      在 C++98,空指针用字面值 0 来表示, C++11 标准出来后,最好使用 nullptr,于是有人会问纯虚函数的声明 = 0 是不是应该更正为 = nullptr,尝试了下是不行的。语法上规定必须是 0, 而不能是表达式(42 - 42),甚至 0L,0u 0x0 (clang 编译器以前支持这样的写法)等。有些人选择用宏 #define pure = 0 ,这个从声明上似乎容易识别虚函数,这样写不是很好。C++ 之父 Bjarne Stroustrup 解释了没有添加 abstract/pure 关键字。(Java 里有关键字 abstract)

      Bjarne Stroustrup 在他的书 The Design & Evolution of C++, section 13.2.3 中描述

    The curious =0 syntax was chosen over the obvious alternative of introducing a new keyword pure or abstract because at the time I saw no chance of getting a new keyword accepted. Had I suggested pure, Release 2.0 would have shipped without abstract classes. Given a choice between a nicer syntax and abstract classes, I chose abstract classes. Rather than risking delay and incurring the certain fights over pure, I used the tradition C and C++ convention of using 0 to represent "not there." The =0 syntax fits with my view that a function body is the initializer for a function also with the (simplistic, but usually adequate) view of the set of virtual functions being implemented as a vector of function pointers. 

      = 0 似乎让人迷惑,认为纯虚函数就是未实现的函数,以为是将该函数指针置空(nullptr),而实际上,纯虚函数也是可以实现的。不能在声明时实现,而是必须在外部给出定义。C++11 引入了上下文相关的关键字概念,现在开来,引入 pure 作为上下文相关的关键字,将 = 0 替换成 pure 的写法会更直观。

    class Base
    {
    public:
        virtual void fun() = 0;
    };
    
    void Base::fun()
    {
        std::cout<<"pure virtual implementation
    ";
    }
    
    class Derived: public Base
    {
    public:
        virtual void fun() { Base::fun(); }
    };

      这里引出另外两个话题:析构函数可以是纯虚函数,但必须给出定义,否则会出现链接错误,因为继承的类在析构的时候会调用基类的析构函数,找不到符号。纯虚函数要求非抽象子类给出实现,所以基类的构造函数最好给 protected 权限。




      C++11 增加的 operator "" 的重载,用户自定义语义(user literal限制在以下几种类型,

    ( const char * )
    ( unsigned long long int )
    ( long double )
    ( char )
    ( wchar_t )
    ( char16_t )
    ( char32_t )
    ( const char * , std::size_t )
    ( const wchar_t * , std::size_t )
    ( const char16_t * , std::size_t )
    ( const char32_t * , std::size_t )

      发现没有,比如浮点类型只有 long double,却没有 float 和 double 类型,为什么呢?假设我们有一个角度制和弧度制转换的函数语义。

    constexpr long double operator"" _deg ( long double deg )
    {
        return deg*M_PI/180;
    }

    我们要表达 M_PI 弧度时候,可以写成 180_deg ,注意 180 和 _deg 是一个整体,之间不能有空格。(就像 C/C++ 语言内的后缀语义 long int degree = 180L; float pi = 3.14159265f 一样)。如果要区分 float、double、long double 数据类型,我们的定义应该是这样的:

    float radian1 = 180f_deg;
    double radian2 = 180_deg;
    long double radian3 = 180L_deg;
    很显然,编译器会把没有空格的合法字符当成一个单词(token)解析,于是 f_deg 变成了一个单词,前面也说了 f 与 _deg 之间不能有空格,所以我们无法满足不同浮点类型的重载,也包括整数类型。为了不截断或窄收,于是采取最大表示范围的类型,整形用 unsigned long long int,浮点类型用 long double。整形是 long long int 而不是 unsigned long long int 是因为有符号和无符号数相加减,会扩充到无符号类型。这里似乎暗示了 unsigned long long int 是最大范围的整形,所以应该不会出现 128 位的整形。

      尽量让计算或可能发生的错误推前到编译期而不是运行期,在 C++11 增加了新的关键字 static_cast、constexpr。boost 库里面有模拟 static_cast 的功能。
    之前我们用宏定义 FOURCC

    #define MAKE_FOURCC(ch0, ch1, ch2, ch3) 
        ((uint32_t)(uint8_t)(ch0) |         
        ((uint32_t)(uint8_t)(ch1) << 8) |   
        ((uint32_t)(uint8_t)(ch2) << 16) |  
        ((uint32_t)(uint8_t)(ch3) << 24))   

    现在我们可以直接用 constexpr 函数来写,而不是类型不安全的宏了。因为是编译器计算出值,所以算出来的整形值可以直接用于 switch case 语句中而不会出错。

    constexpr uint32_t makeFourCC(uint8_t ch0, uint8_t ch1, uint8_t ch2, uint8_t ch3)
    {
        return static_cast<uint32_t>(ch0 | (ch1<<8) | (ch2<<16) | (ch3<<24));
    }
    
    constexpr uint32_t RIFF = makeFourCC('R', 'I', 'F', 'F');
    constexpr uint32_t WAVE = makeFourCC('W', 'A', 'V', 'E');
    constexpr uint32_t FMT_ = makeFourCC('F', 'M', 'T', ' ');
    constexpr uint32_t DATA = makeFourCC('D', 'A', 'T', 'A'); 

    让派生类不要继承父类的行为,大概有三种做法。
    1. virtual void fun1() {}
    2. virtual void fun2() { assert(false); }
    3. virtual void fun3() = 0;
    这些写法我都遇到过,fun1 实现留空,表示派生类可以覆盖其行为;fun3 纯虚函数,表示派生类必须覆盖其行为;而 fun2 如果没有覆盖,在运行时断言失败。
    很显然 fun2 的做法不是很好,错误要等到运行时才能发现。

      类型转换关键字 const_cast / dynamic_cast / reinterpret_cast / static_cast
      不要用C的带括号的强制转换。不要在指针和整形之间转换,也不要在数据指针和函数指针之间转换。类型之间转换时,首先考虑使用 static_cast。整形到 enum 枚举类型、浮点数到整形用 static_cast。从 void* 类型转换到特定的指针类型,虽然 static_cast 与 reinterpret_cast 都能用,最好还是用 static_cast。在不相关的指针类型之间转换时,用 reinterpret_cast,这个相当于C语言里面的转换。const_cast 不要被名字迷惑了,它是可以 cast 掉 CV-qualifier,而不仅仅是 const,可以是 volatile 修饰的类型。

    // Possible implementation of std::addressof in <memory>
    template<typename T>
    T* addressof(T& arg) noexcept
    {
        return reinterpret_cast<T*>(
                   &const_cast<char&>(
                      reinterpret_cast<const volatile char&>(arg)));
    }

      上面是C++11标准里的 std::addressof 的实现。注意这里的hack技巧,最里层需要有const volatile关键字。reinterpret_cast 转换可以增加 const volatile 修饰符,但不能移除。所以如果 arg 是 const 变量的话,reinterpret_cast 就会编译失败。


      对于函数传入的指针或引用,如果不会修改其内容,尽量用 const 修饰这个参数。C语言中释放内存的函数是 void free(void *ptr);,释放本来是不会修改指针指向的内容的,而是让操作系统回收分配的内存。函数签名似乎应该为 void free(const void *ptr);,这样可以直接释放 const 指针类型而不用 const_cast 转换一次。但是从另一方面讲,申请内存一般就会修改内存,修改就不能直接用 const 修饰,谁申请谁释放,释放内存也就不是 const 指针的责任了。
      基类到派生类的转换,需要用到 dynamic_cast,我们称之为下转(downcast)。指针下转失败为空 nullptr,引用下转失败则抛出 std::bad_cast 异常。在 C++11 的 noexcept 出现之前,C++ 的异常处理机制不是很好使,很多程序员也是尽量不去碰。在含有大量层次继承关系的游戏或对性能有要求的一些工程里,会关闭 C++ 的 RAII (Run-Time Type Information) 机制。做法是 debug 版本开启检查类型安全,release 版本关闭运行时的查找以提升性能。

    template <typename To, typename From>
    To downcast(From p)
    {
    #ifdef NDEBUG
        return static_cast<To>(p);
    #else
        To to = dynamic_cast<To>(p);
        assert(to != nullptr);
        return to;
    #endif
    }


      C++ 十几年的时间没有动静,造就了 boost 优秀库的出现。又从较自己晚出身的语言身上也学到了很多,比如 Java 类的成员在定义的时候给定默认值,委托构造函数(delegate constructor)、Python 的原生字符串常量,也算方便了程序员对 C++ 字符串的学习和使用。

  • 相关阅读:
    VS 无法命中断点解决
    sharepoint 什么开始系列呢?从哪里开始写呢?
    sqlserver 登录名与用户的关系
    Exchange 开启模拟用户
    Exchange(DC服务器开启防火墙,造成的问题)
    Exchange 2010 RPC 揭秘
    二维码生成
    【转】使用 Entity Framework + ListView + DataPager 实现数据分页
    24款超实用的Web 2.0风格翻页代码
    精心收集的jQuery常用的插件1000
  • 原文地址:https://www.cnblogs.com/Martinium/p/cpp_keyword.html
Copyright © 2020-2023  润新知