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>classes\HelloWorldScene.h(17): error C2653: ‘EventKeyboard‘ : is not a class or namespace name (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier ‘KeyCode‘ (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2653: ‘EventKeyboard‘ : is not a class or namespace name (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier ‘KeyCode‘ (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.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
}

  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‘);

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

未完待续。。

时间: 2024-10-18 03:19:58

C++ 关键字浅谈的相关文章

5.C#知识点:ref和Out关键字浅谈

首先我们要知道ref和out在C#里面是什么? 答:它们俩是C#里面的关键字. 他们俩是干啥的呢? 答:他们俩是方法参数的修饰符号,一但使用,方法定义和方法都用都要使用这个关键字,这一点是死规定. 好了废话不多说,既然是修饰符,使用了他们一定是要有意义的对吧.我们先来看一手微软官方定义. 如果大家通过上面微软官方的定义明白了这个参数.那很好,下面的我写的就不需要看啦!还有疑惑就继续往下看.保证你能懂. 这里我先不直接讲解,我先掏出一手代码,然后通过代码讲解. using System; usin

浅谈C#中new、override、virtual关键字的区别

OO思想现在已经在软件开发项目中广泛应用,其中最重要的一个特性就是继承,最近偶简单的学习了下在设计模式中涉及到继承这个特性时,所需要用到的关键字,其中有一些关键点,特地整理出来. 一.New 在C#中,new这个关键字使用频率非常高,主要有3个功能: a)   作为运算符用来创建一个对象和调用构造函数. b)   作为修饰符. c)   用于在泛型声明中约束可能用作类型参数的参数的类型. 在本文中,只具体介绍new作为修饰符的作用,在用作修饰符时,new关键字可以在派生类中隐藏基类的方法,也就说

转载 浅谈C/C++中的static和extern关键字

浅谈C/C++中的static和extern关键字 2011-04-21 16:57 海子 博客园 字号:T | T static是C++中常用的修饰符,它被用来控制变量的存贮方式和可见性.extern "C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明.本文主要介绍C/C++中的static和extern关键字. AD: static是C++中常用的修饰符,它被用来控制变量的存贮方

浅谈js中的this关键字

浅谈js中的this关键字 全局作用域中的this 函数作用域中的this 不同函数调用方法下的this 直接调用 作为对象的方法调用 作为构造函数调用 通过call或apply方法调用 嵌套函数作用域中的this 浅谈js中的this关键字 this是JavaScript中的关键字之一,在编写程序的时候经常会用到,正确的理解和使用关键字this尤为重要.接下来,笔者就从作用域的角度粗谈下自己对this关键字的理解,希望能给到大家一些启示,权当交流之用. 全局作用域中的this 本文将以作用域由

.net中对象序列化技术浅谈

.net中对象序列化技术浅谈 2009-03-11 阅读2756评论2 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数 据.例如,可以序列化一个对象,然后使用 HTTP 通过 Internet 在客户端和服务器之间传输该对象.反之,反序列化根据流重新构造对象.此外还可以将对象序列化后保存到本地,再次运行的时候可以从本地文件 中“恢复”对象到序列化之前的状态.在.net中有提供了几种序列化的方式:二进制序列化

浅谈算法和数据结构

: 一 栈和队列 http://www.cnblogs.com/yangecnu/p/Introduction-Stack-and-Queue.html 最近晚上在家里看Algorithems,4th Edition,我买的英文版,觉得这本书写的比较浅显易懂,而且“图码并茂”,趁着这次机会打算好好学习做做笔记,这样也会印象深刻,这也是写这一系列文章的原因.另外普林斯顿大学在Coursera 上也有这本书同步的公开课,还有另外一门算法分析课,这门课程的作者也是这本书的作者,两门课都挺不错的. 计算

浅谈asp.net通过本机cookie仿百度(google)实现搜索input框自动弹出搜索提示

对于通过用户输入关键词实现自动弹出相关搜索结果,这里本人给两种解决方案,用于两种不同的情形. 常见方法是在数据库里建一个用户搜索关系表,然后通过用户搜索框输入的关键字异步调用数据表中的相关数据,显示在一个隐藏div中. 第二种方式也就是我现在着重讨论的方式,适用于单个用户,基于此用户以往的搜索数据来实现搜索提示功能.技术关键是记录下用户的以往搜索数据,写入cookie,然后页面从用户本机cookie调用数据. ok,下面进入正题.本文主要讲实现步骤,代码可根据自己实际需要更改. 一,如何写入co

java serialize 浅谈

对象的串行化(Serialization) 一.串行化的概念和目的 1.什么是串行化             对象的寿命通常随着生成该对象的程序的终止而终止.有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复.我们把对象的这种能记录自己的状态以便将来再生的能力.叫作对象的持续性(persistence).对象通过写出描述自己状态的数值来记录自己 ,这个过程叫对象的串行化(Serialization) .串行化的主要任务是写出对象实例变量的数值.如果交量是另一对象的引用,则引用的对象也要串

浅谈oracle中rowid和rownum

[ 概要 ] 刚刚接触oracle的同学可能常常会被rowid和rownum这两个词弄混, 弄清楚这两个家伙对于我们写sql会有很大的帮助, 下面偶就抛砖引玉, 简单地谈谈他们之间的区别吧. [ 比较 ] rowid和rownum都是oracle中的伪列, 但他们还是存在本质区别: rowid: 是物理地址, 用于定位数据表中数据的位置, 它是唯一的且不会改变. rownum: 是根据查询的结果集给每行分配的一个逻辑编号, 查询结果不同, rownum自然不同. 对于同一条记录, 查询条件不同,