CNeo编程语言概述
C语言诞生于1970年,当时在AT&T实验室由Dennis Ritchie主导开发的。据说当时仅用了一周的时间就做好了C语言编译器,所以尽管C语言从90年正式纳入ISO标准委员会,其编号为ISO/IEC 9899。尽管经历了C99与C11标准的修改历程,但为了向前兼容,有一些比较古怪的特性依然被保留。
现在除了C语言,还诞生了许许多多的其它高级编程语言,而且大多都具有面向对象的特性。而C语言以其间接、灵活、干练而得到了系统级应用开发的首选编程语言,尤其是嵌入式系统上应用更多。然而,为了使原本已经设计精良的C语言能在整个语法体系上更具完备性,并且更与时俱进,我这里畅想一下二十一世纪的新C语言应该考虑改良的语法特性,我暂时先把这个划时代的C语言称为CNeo。这里不考虑与现有C语言的语法兼容,但一定能做到二进制兼容。CNeo的源文件后缀名暂时用.cn来表示。
CNeo编程语言将更直观地基于对象,但它仍然不是一门面向对象的编程语言,然而这使CNeo更适合于底层的系统级编程,并且仍然能保持对汇编API的良好兼容性。此外,CNeo将引入C++11标准中的类型推导特性以及Swift那种后置类型设计。CNeo中将引入关键字obj来声明一个对象,每个对象会有系统内置的属性可用于查询获取相关的地址、类型、类型名、占用存储大小、以多少字节对齐等。下面将举一些例子来说明一下。
obj a = 100; // 对象a的类型为int obj f: const = 10.5f; // 对象f的类型为const float obj d: double = -10; // 对象d的类型为double printf("size of a is: %ul\n", a.$bytesize); printf("address of f is: %ul", intptr(f.$adr)); printf("type of d is: %s\n", d.$typename); printf("alignment of d is: %ul", d.$align); obj c: f.$type = f + a; // 声明了一个对象c,其类型为float
因此,在CNeo所有对象均含有以下属性:$bytesize表示获取当前对象大小;$adr表示当前对象的地址(注意,obj.$adr的类型为ptr类型,不是一个intptr类型);$typename表示当前对象的类型名(类型为const char[N]);$type表示当前对象的类型;$align表示当前对象以多少字节对齐(类型为uint);$offset表示当前对象所在结构体中的偏移(类型为uint),如果该对象不在结构体中,则该属性值一直为0。所有这些属性都是只读的,不能对其写。
此外,还有其他类型对象所专用的属性:
指针类型对象含有$mem属性,表示访问该指针所指对象,即做解引用操作。
数组对象含有$count这个属性,表示当前数组含有多少元素(类型为uint)。该属性是只读的。
字符串字面量除了含有$count这个属性之外,还含有$length这个属性,表示该字符串字面量的长度。比如,"hello".$length 的值为5;u"你好".$length 的长度位2,u"你好".$count的值为3,因为除了前两个char16类型的字符之外,还有最后一个char16类型的0,u"你好".$bytesize的值为6。
函数与闭包含有$rettype这个属性,表示获取该函数或闭包的返回类型。该属性是只读的。此外,函数与闭包对象的$bytesize属性值等于一个指针对象的$bytesize属性值,而不是该函数或闭包代码指令的总字节数。
复数类型对象还有$real与$imag属性,分别表示获取该复数的实部值与虚部值。
在CNeo中,$符号只能被CNeo语言用于特定场合(比如用于对象属性的标识符前缀),而不能挪为他用。
下面将简单介绍一下CNeo其它一些主要特性:
1、真正的布尔类型:在C99标准中就引入了_Bool关键字,并引入了头文件,使得C语言也有了布尔类型。但其骨子里仍然没有真正的布尔类型。就好比说下面代码:
obj a = 10; // a不是一个布尔类型对象,但仍然能作为if、while等表达式来用 if(a) a--;
所以,我打算在CNeo中真正引入布尔类型——bool,并且它是一个原生类型,不需要这样的头文件。除此之外,在整个语法体系中只有布尔类型表达式才能作为if、while语句的表达式。比如以下代码:
obj a = 10; if (a) // 这里error!a不是一个布尔类型,不能作为if语句的表达式 a++; if (a > 0) // 这里OK! a++; obj b: bool = a > 10; if (b) // 这里OK!布尔对象可以作为if语句的表达式 a++;
2、更规范的整数与浮点类型:现在对整数类型的定义比较杂乱。由于早些时候C语言中使用int、short、char、long、long long等来表示一个整数对象的长度的。这里,对于不同系统环境,对这些类型的长度可能各不相同。比如在一个8位单片机中,一个int才1个字节;一个long类型为2个字节;而在现在32位系统中,一个int为4个字节;一个long也为4个字节;而在64位系统中又复杂了,在VC中,long仍然为4字节,但在GCC、Clang编译器中,long则为8个字节。正由于这种系统不确定性,所以在C99标准中引入了头文件,其中定义了int8_t、int16_t、int32_t、int64_t以及它们对应的无符号形式来指明当前整型对象的长度。
在CNeo中,去除了这种头文件,并仅引入这些整数类型——uint8、int8、uint16、int16、uint32、int32、uint64、int64、uint、int、ulong、long、intptr。这些类型分别表示无符号8位整数、带符号8位整数、无符号16位整数、带符号16位整数、无符号32位整数、带符号32位整数、无符号64位整数、带符号64位整数、能用于快速计算的最大无符号整数、能用于快速计算的最大带符号整数、当前环境可表示的最大无符号整数、当前环境可表示的最大带符号整数、能用于存放地址值的无符号整数。这样一来就可以把神马size_t、ptrdiff_t等杂七杂八的类型全都剔除了。使得整个基本整数类型变得干净、整洁。
这里,对于存在系统差异性的就两对类型,一对是uint和int,另一对是ulong和long,还有一个是intptr。正如上面所描述的,uint和int一般表示当前处理器的寄存器宽度,如果寄存器是32位的且能做快速的算术逻辑计算,那么它就是32位的;倘若在8位的单片机中,寄存器宽度是8位的,那么它们就是8位的。通常在32位系统下,uint和int为32位;在64位系统下,它们仍然为32位。而ulong和long用于存放当前系统的最大可表示的整数值,一般在32位系统下为32位,64位系统下为64位。intptr则用于存放当前系统的指针值和地址值,一般在32位系统下,地址值为32位,所以它们为32位;而在64位系统下,地址值长度为64位,所以它们为64位。
除此之外,字符类型分别为:char、char16与char32,分别表示UTF-8、UTF16与UTF-32编码格式。
浮点类型则分别为:half、float、double、quad。其中,float与double分别表示传统的单精度与双精度浮点。而half与quad分别表示IEEE754-2008标准中新引入的半精度与128位精度浮点。此外,quad也用于取代之前的long double,可用于存放x87 FPU的扩展双精度浮点数(80位)。
CNeo中整数、浮点数以及字符与字符串字面量也与C语言大致相同。没有任何后缀的整数表示int类型;u或U后缀的整数表示uint类型;l或L后缀的整数表示long类型;ul或UL后缀的整数表示ulong类型;t或T后缀表示intptr类型的整数,它表示一个地址值。f或F后缀的浮点数表示float类型;h或H后缀的浮点数表示half类型;没有后缀的浮点数表示double类型;q或Q后缀的浮点数表示quad类型。
此外,CNeo原生支持复数的字面量,以i为后缀的字面量表示一个实部为0,只含虚部的纯虚数。CNeo中复数的实部与虚部暂时只支持浮点类型。比如:(1.0f + 0.5fi)表示一个一个实部为1.0,虚部为0.5的复数,其中实部与虚部的类型为float。复数类型采用关键字complex,构造一个复数对象也可用complex(1.0f, 0.5f)进行,其中第一个参数表示实部,第二个参数表示虚部。一个复数含有两个额外的属性——$real和$imag,分别表示该复数对象的实部与虚部。比如以下代码:
obj comp1: complex = 1.0f + 0.5fi; obj comp2 = complex(2.0f, 0.5f); comp1 += comp2; // 相当于: comp1.$real += comp2.$real; comp1.$imag += comp2.$imag;
3、引入library机制。当有两个静态库或动态库存在相同符号的对象或函数时,我们可以指定使用哪个库的对象或函数。比如:
// 这里使用import用于提示默认导入libA与libB两个库中的所有符号
import libA; import libB;
#include "A.h"
#include "B.h" func main(argc: int, argv: array<array<const char>>) { // 假定funcA是libA库所独有的,这样可以直接调用 funcA(); // 假定funcB是libA和libB两个库都有的,那么我们可以用库名加点语法进行区分调用 libA.funcB(); // 调用libA的funcB libB.funcB(); // 调用libB的funcB }
这里要注意的是,import语句仅仅作为一种连接时的提示语,它与#include不同,无法将各种类型、对象以及函数声明引入进来,所以import libA;其实是对连接器的说明,优先使用libA中的符号。
4、更直观的指针形式。在C语言中用星号表示对指针对象的声明,在表达式中则用于表示间接操作符。而在CNeo中,我们直接用ptr关键字来表示指针对象。比如,C语言中的int *,在CNeo中可用ptr<int>来表示。空指针直接用null关键字来表示,null的类型为ptr<void>。intptr(null)的值为零。我们下面来例举一些例子来说明新指针形式的使用方式以及其特性。
obj a = 100; obj p: ptr<int> = a.$adr; // 这里,ptr<int>表示指向int的指针类型;a.$adr则表示a的地址值 p.$mem = 200; // 这里相当于C语言中的*p = 200; 此时a对象的值变为200
p[0] = 200; // 与上述语句等同
obj q = a.$adr; // 这里也可以用obj,如果指针所指类型能被推导出来的话 obj c: const int = 20; obj cp: ptr<const int> = c.$adr; // cp为指向一个const int对象的指针 obj co: const ptr<int> = a.$adr; // co为指向一个int对象的常量指针 obj cop: const ptr<const int> = c.$adr; // cop为指向一个const int对象的常量指针 // pp表示为一个指向ptr<int>对象的指针,即相当于C语言中的int **pp obj pp: ptr<ptr<int>> = p.$adr; // 一个指针对象可以使用intptr构造器来转换为一个整数值 obj addr: intptr = intptr(p);
// arr是一个数组对象
obj arr = int[]{ 1, 2, 3 };
// 如果一个指针对象指向的是一个数组的某个元素或可访问多个元素的存储空间,
// 那么也可以用关键字array来表示指针类型。CNeo中,array与ptr是完全等同的,
// 只不过引入array关键字用来指明该指针对象可访问多个元素,而ptr通常指明只访问一个单独的对象。
obj pArr: array<int> = arr[0].$adr;
5、构造型投射操作。在C语言中,投射操作用(类型名)来表示,而在CNeo中将以类型的构造方式来表示,因为在CNeo的语法体系下使用构造型投射操作会带来很多方便,比如以下代码:
obj f = 10.25f; // 这里是先将对象f的地址转为指向uint32的指针类型,然后查看该地址的内容。 // 这样就能观察到单精度浮点数10.25的十六进制表达了。 printf("The hex representation is: %.8X\n", ptr<uint32>(f.$adr).$mem); // 将一个intptr类型的整数转为指向int类型的指针,即将该整数值作为一个地址 obj p = ptr<int>(0x0100_0000T); // 将指针对象p所指地址的内容转为uint8类型 obj c: uint8 = uint8(p.$mem);
6、将数组类型类别完全独立出来。我们知道,在C语言中,一个数组对象在作为往往会被当作为一个指针,而且数组对象标识符可直接被隐式地转换为指针。这个特性据说是当时C编译器对数组类别实现有些难度而导致的。在CNeo中,一个数组对象就是一个数组对象,它不会被(也不能被)隐式或显式地转换为一个指针类型。所以,一个数组可以给另一个数组赋值以及初始化。我们先看下面这段例子。
obj a: int[3] = { 1, 2, 3 }; // 这在先用的C语言中是非法的,但在CNeo中是有效的,表示用数组a的元素对数组b进行初始化 // 这里a有三个元素,b有4个元素,所以对b的初始化结束后,b的内容为{ 1, 2, 3, 0 }; // 最后一个元素用0来初始化 obj b: int[4] = a; obj c: int[4]; // 这里是数组b对数组c进行赋值。 // 相当于:memcpy(c[0].$adr, b[0].$adr, b.$bytesize); c = b; // 在CNeo中,如果要用一个指针指向一个数组首个元素的地址,则不能用现在C语言的 // int *p = a; 这种形式,而是要用以下方式: obj p: array<int> = a[0].$adr; // 这里a的类型是int[3],p的类型是ptr<int>,a[0].$adr的类型也是ptr<int>,所以两者兼容 // 一个指针对象可以用加、减、递增、递减以及索引操作,其操作数必须是一个整数对象。 p++; // 相当于: p = ptr<int>(intptr(p) + 1 * p.$mem.$bytesize); p[1] = 100; // 相当于: (p + 1).$mem = 100; // 指向数组的指针与之前一样 // 声明一个指向int[4]数组的指针pArray,并用数组b的地址对其初始化 obj pArray: ptr<int[4]> = b.$adr; // 表示将pArray所指数组的元素内容全都修改为数组c的元素内容 // 相当于memcpy(pArray, c[0].$adr, c.$bytesize); pArray.$mem = c; // 除此之外,在声明一个数组时也能用一个数组字面量来对它初始化 // 这里用一个常量数组对array1进行初始化, // 并且array1的类型为int[3] obj array1 = const int[]{ 1, 2, 3 }; // 这里声明了一个数组对象array2,其类型为const int[5] obj array2: const int[5] = int[5]{ [0] = 1, [3] = 2 }; // 数组对象中还具有count属性用于获得该数组对象含有多少元素 printf("number of elements: %ul\n", array2.$count);
此外,CNeo不支持C99开始引入的变长数组类型以及所谓的可变修改类型(variably modified type),不过提供了一个内建函数可在栈上动态分配存储空间——stack_alloc。比如:
// stack_alloc返回array<void>类型,即ptr<void>类型 // 这里给arr指针对象在栈上分配了int[100]的空间 obj arr: array<int> = stack_alloc(arr[0].$bytesize * 100);
这样既能保持类型语法体系的干净,也能达到可变数组相同的功能。
7、结构体与联合体。CNeo中的结构体与联合体跟C语言中的类似,但也有一些方面不同。比如,在CNeo中,结构体与联合体的成员使用关键字obj进行声明。一个匿名结构体可充当一个对象的类型使用。下面就以结构体为例来举一些例子。
struct MyStruct { // 声明成员a,它是一个int类型对象 obj a: int; // 声明成员b,它是一个指向常量字符数组首地址的指针对象 obj b: array<const char>; // 声明一个成员c和d,它们具有uint16类型 obj c, d: uint16; // 以下声明一些位域成员 obj bf1: uint: 3; // 占用3比特 obj bf2: uint: 16; // 占用16比特 obj bf3: int: 10; // 占用10比特 }; // 声明一个MyStruct结构体对象,并用一个MyStruct的字面量对它初始化。 // CNeo中,一个命名结构体、联合体以及枚举类型的类型标识符无需给出, // 所以这里的struct关键字可省 obj s1 = MyStruct { 10, "hello", .d = -10, 1, 2, 3 }; union MyUnion { obj a: int; obj b: uint; }; // CNeo与传统C语言类似,即便是不同类型的相同类型名也不允许在同一名字空间中共存 // 这里声明一个MyStruct的对象un,并用一个MyUnion联合体字面量对它初始化 obj un = MyUnion { .a = 100 }; // 这里声明一个MyStruct的对象s2,并用初始化器对它初始化 obj s2: MyStruct = { 20, null, .bf1 = -1, -2 }; // 这里声明了一个匿名结构体的对象,该匿名结构体含有两个int成员a和b // 然后用初始化器对它初始化 obj s3: struct { obj a, b: int; } = { 10, 20 };
// 另外可以直接在声明变量的时候同时定义一个命名结构体
obj s4: struct MyStruct2 { // 这里,成员s是一个匿名结构体对象 obj s: struct{ obj a, b: int; }; } = MyStruct2{ { 1, 2 } }; // 这里已经能知道MyStruct2的完整类型,因此可直接用它的复合字面量
// 以下声明MyStruct2结构体对象并对它们初始化的方式均合法 obj s5 = MyStruct2 { { 10, 20 } }; obj s6: MyStruct2 = { .s = { 10, 20 } }; obj s7: MyStruct2 = { .s.a =10, .s.b = 20 };
上述代码例举了CNeo中结构体与联合体的各种特性。其中,结构体复合字面量的形式与定义一个结构体的方式很类似;而匿名结构体在使用时也非常方便。此外,CNeo与C还有一个很大的不同点是,具有结构体与联合体作用域。一个结构体可嵌套定义一个内部的命名结构体,内部嵌套的结构体类型可通过点语法进行访问。而CNeo中的匿名结构体类型不允许单独定义,而必须与对象、函数声明一同出现。比如:
struct OuterStruct { obj a: int; // 嵌套定义内部结构体 struct InnerStruct { obj i, j: int; }; // 用嵌套定义的内部结构体声明成员b obj b: InnerStruct; // 用匿名结构体类型声明成员c obj c: struct{ obj x, y: uint; }; }; obj a = OuterStruct { 10, { 1, 2 }, { 3, 4 } }; obj b = OuterStruct.InnerStruct { .i = 5, .j = 6 }; // 用结构体OuterStruct成员c的匿名结构体类型声明对象c obj c: a.c.$type = { 100, 200 };
一个结构体或联合体类型只能通过点语法去访问定义在它内部的类型,但不能访问其成员对象。只有通过一个结构体或联合体对象 才能去访问该对象的成员对象。
在CNeo中,联合体类型对象除了具备上述结构体类型对象初始化方式外,还有一个十分简便的初始化方式,即直接通过一个基本类型对其初始化,比如:
union Un { obj a: int; obj f: float; obj s: array<const char>; }; // 这是通过一个Un字面量对un1进行初始化 obj un1 = Un { .f = 10.05f }; // 这是通过初始化器对un2进行初始化 obj un2: Un = { .s = "hello" }; // 这里是CNeo高级的地方,直接通过 = 右边表达式的类型对联合体对象进行初始化。 // 编译器会自动做类型匹配,如果没有匹配到相应类型则会做编译失败处理。 obj un3: Un = 100; // 相当于obj un3: Un = { .a = 100 };
8、字符串。字符串在C语言中比较特殊,有不少语法体系就是为了字符串而做出例外的。比如字符串字面量可直接为一个字符数组进行初始化;一个字符串字面量仅管不是用const修饰的,但不能对它成员字符进行修改,这也是因为一个字符串字面量可给一个char*类型的对象进行赋值,倘若它是const的,那么反而违背了整个语法体系!
而在CNeo中,一个字符串字面量被完全设定为常量数组对象。比如,"hello"的类型为const char[6](最后有一个'\0'结束字符)。然而,在CNeo中,一个字符串字面量可被隐式地转换为一个array<const char>类型。在传统C语言中,我们用一个对象对字符串字面量引用之后,我们就不得不通过strlen函数在运行时获得该字符串的长度。而在CNeo中则完全没必要,我们可以直接访问字符串字面量的$length属性在编译时获得其长度。如果一个字符串字面量对一个对象进行初始化,该对象不显式注明类型,那么该对象默认为const char[N]类型,并且直接对该字符串字面量进行引用,即该对象首地址与字符串字面量的首地址完全一致。如果对象注明的是const char[N]类型,那么字符串字面量对该字符数组对象初始化时自动以strcpy的方式进行。我们下面举几个例子:
// s1直接对字符串"hello"进行引用,其类型为const char[6] // 其地址与"hello"所在的地址相同 obj s1 = "hello"; printf("s1 length is: %ul\n", s1.$count); // 这里输出:s1 length is: 6
// 由于这里s1即为对"hello"字符串字面量的别名,因此也完全可使用$length属性
printf("hello length is: %ul\n", s1.$length); // 这里输出:hello length is: 5 // 这里声明一个const char[6]类型的对象s2,并用"hello"进行初始化 // 由于这里s2已经标注了类型,所以它会被分配在当前函数所在的栈空间, // 并且在运行时是将"hello"的内容拷贝到s2中去。 obj s2: const char[6] = "hello"; // 声明一个指向const char类型的指针对像p, // 它直接指向字符串"hello"的起始地址 obj p: array<const char> = "hello"; // 相当于 obj q: array<const char> = "hello"[0].$adr;
这里大家要注意的是,在CNeo中,只有字符串字面量才能隐式转为指针类型,这也是考虑到实践中字符串用得比较频繁,并且也很有可能会比较长。
9、函数与闭包。CNeo中不仅仅拥有传统C语言的函数,而且还有闭包。函数的声明方式为:
func function-name ( parameter-list ) -> return-type
这里,如果返回类型为void,那么 -> return-type 可省。下面举一些简单的例子。
// foo1是参数列表为空,返回为void类型的函数 func foo1() { } // foo2是含有两个形参,分别为a和b的函数,其返回类型为int func foo2(a: int, b: ptr<const char>) -> int { return 0; } // foo3是含有一个形参a的函数,其返回类型是一个匿名结构体类型 func foo3(a: int) -> struct { obj a, b: uint } { // return语句后面直接跟{ }将会以当前函数返回类型 // 自动推导出 { } 中应该包含的元素对象 return { a + 1, a - 1 }; } // foo4是一个含有一个形参a的函数,其返回类型与形参a一样,都是int[8] func foo4(a: int[8]) -> a.$type { return { [0] = a[0], [3] = a[2], [7] = a[3] }; } // foo5是一个含有不定个数与类型的形参的函数,其返回类型为void func foo5( s... ) -> void { // 不定个数的形参具有属性$count,它就好比是一个ptr<void>[]变长数组对象 if(s.$count == 0) return;
// 在使用时需要进行类型转换。此外,如果传入实参的类型不是一个指针类型,则需要先转为指针类型再获取其内容。 obj arg0 = ptr<int>(s[0]).$mem; obj arg1 = ptr<float>(s[1]).$mem;
// 指针、函数以及闭包类型对象可直接使用
obj arg2 = array<const char>(s[2]);
// 在CNeo中,实参列表中最后一个实参后面可添加一个逗号,这样有助于不定参数宏扩展时,无实参传入的情况
foo2(arg0, arg0.$adr, );
// 这里的{1, 2, 3, 4}相当于int[8]{1, 2, 3, 4},
// 由于CNeo支持实参传递时的类型推导,实参通过{ }方式即可表达形参所要求的复合类型
foo4({1, 2, 3, 4});
return void(arg0); // 任何表达式都能转为void表达式 }
func test()
{
// 在调用带有不定参数个数的函数时,一些字面量或不具有地址的对象,
// 编译器会自动产生一个临时对象,然后将临时对象的地址传入。
// 函数与闭包类型不能被直接传入,因为ptr<void>不能被直接转为函数或闭包类型,但可以传指向它们的指针对象。
// 如果传入的一个对象是一个指针类型,那么它不会再次被取地址。
// 此外,不定参数列表不接受数组类型对象,需要程序员自己将数组对象转为其某个元素的地址或该数组对象的地址;
// 但能直接接受字符串字面量,它会被自动转为array<const char>类型。
// 所有基本类型、结构体、联合体、枚举类型对象可直接传入,编译器能自动将这些对象转为指向它们的指针。
foo5(10, 0.5f, "Hello");
}
在CNeo中不像传统的C语言使用指向函数的指针,CNeo里用的是函数引用,用func<>表示。下面声明与上述代码中5个foo函数相应的函数引用对象。
obj funRef1: func<() -> void> = foo1; obj funRef2: func<(int, array<const char>) -> int> = foo2; obj funRef3: func<(int) -> struct { obj a, b: uint }> = foo3; obj funRef4 = foo4; obj funRef5: func<( ... ) -> void> = foo5; // funPtr为指向函数对象funRef1的指针 obj funPtr: ptr<func<()->void>> = funRef1.$adr; // 一个函数对象引用可被转为intptr类型 obj ref: intptr = intptr(funRef1); // ref的值即为foo1函数的起始地址值
// 一个intptr类型可以被转换为函数类型,并直接调用
func<() -> void>(ref)();
闭包与函数类似,不过闭包可以定义在一个函数语句块内,但一个函数语句块内不能定义一个函数。下面举一些关于闭包使用的例子。
func foo() -> void { obj x = 100; // 这里声明了一个对闭包的引用对象c1, // 然后后面直接跟一个带有形参a,返回类型为void的闭包 obj c1: closure<(int) -> void> = closure(a: int) -> void { // 这里访问函数foo的临时对象x, // 这里,对象x只能读,不能写。 printf("The value is: %d\n", x + a); }; // 调用c1闭包 c1(); // 如果一个函数的临时对象允许在其闭包中访问, // 那么需要在它前面加上关键字capture,capture需要放在obj的前面 capture obj y = 200; // 这里定义了一个闭包,并且立即调用它 closure(a: int) -> int { // 这里可以对对象y进行修改 y++; return a + y; }(100); // 这里直接调用该闭包 // 这里声明了指向闭包引用对象c1的指针对象cPtr。
// 这里要注意的是,函数与闭包标识符本身不具有$adr属性,
// 所以必须定义一个对象去访问该对象的地址。 obj cPtr: ptr<closure<(int) -> void>> = c1.$adr;
// 一个函数与闭包除了$type属性之外,还有$rettype属性,用于直接获取其返回类型
obj lambda = closure() -> int { return 10; };
obj a: lambda.$rettype = x;
obj lam: lambda.$type = null; // 一个函数或闭包对象可指向空 } // 这里定义了一个静态函数foo2,其返回类型为一个闭包 static func foo2() -> closure<() -> void> { obj x = 100; capture obj y = 200; obj c = closure() { y += x; printf("y = %d\n", y); }; // 当我们要将一个闭包引用传递到函数外给其它函数使用, // 倘若在该闭包内不具有对外部函数临时对像的使用,那么可直接返回该闭包引用; // 否则,需要使用一次closure_copy这一内建函数,将当前闭包的上下文拷贝到 // 动态分配的存储空间,以至于在函数返回之后,该上下文能得到保留 obj result = closure_copy(c); return result; } obj lambda = foo2(); lambda(); // 在使用完拷贝之后的闭包,使用内建函数closure_release进行释放。 closure_release(lambda);
这里,CNeo对闭包引用引入了Objecive-C的引用计数机制。调用了一次clousure_copy之后,新得到的闭包引用计数为1。如果后面再次调用clousure_copy,那么参数地址与返回的闭包引用地址可以是相同的,这种情况下CNeo实现可直接使用该闭包引用计数加1的操作;而调用了closure_release,则将它引用计数减1,当计数减到0时则立即释放第一次拷贝时的动态分配的存储空间。
一个函数引用与一个闭包不能进行相互转换,因为两者构造有很大不同。闭包拥有存放引用外部对象的上下文空间,而函数没有这些特性。但是一个函数的API能与汇编过程(procedure或routine)接口完全兼容,但闭包则不行。
10、属性修饰符。在当前C语言标准中,有很多函数、结构体和联合体类型,以及对象的属性没有被标准化。在MSVC中一般用__declspec来指定属性;在GNU规范中则使用__attribute__来指定属性。CNeo引入了与C++11标准相同的方式来指定属性。比如以下代码:
// 这里指定了foo总是被内联,并且它是一个无副作用的函数 static func [[always_inline, pure]] foo(a: int) -> int { return a * a; } // 这里指定MyStruct结构体类型以最大4字节为基准做字节对齐与字节填充 struct [[packed(4)]] MyStruct { obj a: int; obj b: bool; obj d: double; }; // 指定对象a以int类型字节大小作为对齐要求 obj a: int[4] [[aligned(a[0].$bytesize)]]; // 指定函数foo2的起始地址以double类型的字节大小做字节对齐 func foo2(a: double) -> void [[aligned(a.$bytesize)]] { }
属性表达式可放在紧靠在对象、类型、函数标识符之前;也可放在函数、对象、类型声明符之后。
11、泛型。CNeo支持即支持类似C11中的generic selection,也支持C++11中的模板的一些简单功能。CNeo引入关键字generic来引出一个generic selection的表达式,一个generic switch-case语句块,或是用此关键字来声明一个结构体或联合体以及函数的泛型模板。这里对原有的C11的generic selection做了一个扩展,即引入了generic switch-case语句块。在generic switch-case语句块中,switch()语句中的表达式与generic selection中的第一个参数表达式一样,只获取其类型,而不对它进行计算,除非是可变修改类型。而每一条case后面都应该是一个完整的类型,而其它方面都与普通的switch-case语句类似。引入这个语法扩展是为了能更直观、方便地处理对于某些类型需要比较大幅度修改代码的情况。下面我们举一些简单例子:
// 定义一个泛型结构体MyStruct,它具有一个泛型类型T generic<T> struct MyStruct { // 这里声明成员对象a为泛型T类型 obj a: T; // 这里声明成员对象p为指向泛型T的指针类型 obj p: ptr<T>; }; // 这里定义一个泛型函数MyFunc,它具有一个泛型类型T // 其第一个形参a为T泛型类型,第二个形参s为泛型MyStruct<T>类型 generic<T> func MyFunc(a: T, s: MyStruct<T>) { } func foo() { obj a = -100; // 这里实例化泛型结构体MyStruct,它包含一个具体的类型int obj s = MyStruct<int>{ 10, a.$adr }; // 这里将泛型函数进行实例化 MyFunc<int>(20, s);
// 类似于C11的generic selection
a = generic(a, int: abs, float: fabsf, double: fabs, default: fabsl)(a);
// generic switch-case语句
generic switch(a)
{
case int {
printf("a = %d\n", a);
break;
}
case float ;
case double {
puts("This is a floating type!");
break;
}
default {
break ;
}
} }
CNeo对泛型函数的实例化后所产生的函数符号与C++的一致。另外,CNeo支持函数重载,只要给函数添加[[overloadable]]属性即可,重载后的函数符号也是与当前C++的一致。此外,CNeo不支持类似C++中对函数模板的部分特化特性,但是可以利用generic switch-case语法特性达到相同的目的。
我们这里还看到,switch语句块中的case和default标签后的语句与原来的C语言中的表达方式有些不同了。在CNeo中无论是generic switch-case还是普通的switch-case语句,switch语句块中只能出现case标签语句或default标签语句,不能出现其他语句表达式。此外,case和default标签语句后面要么是一条空语句,要么必须是用{ }包围起来的复合语句。这样可直接、有效地避免原来C语言中一直诟病的标签语句下对变量声明的作用域问题。
12、预处理器。CNeo的预处理器与传统C语言基本相通。不过像#define等指示符中,#与define之间不允许存在空白符,它们必须作为一个整体。此外,增加了_Defined()宏函数来判定指定的符号是否被定义,这个宏函数可以与条件预编译一起使用,当然现在也能单独地用在其它代码段中了。另外在CNeo中取消了#pragma预编译器,取而代之的仅仅使用_Pragma()的形式,由于编译选项往往需要在编译时根据当前上下文做特定处理,所以完全可以把它从预编译器中剔除。除此之外,在CNeo中增加了_Dup()宏函数,该宏函数的原型为:_Dup(count, expression)。表示将指定的表达式复制count份数。
此外,在调用宏函数过程中,实参可用# ... #括起来形成一个整体的token串,在此串中,除了#符号之外,可用其它任何符号。
最后,将原本宏定义时的token字符串化的#去掉,该用_Literal(token)宏函数;将##token拼接符替换为_Concat(token1, token2)宏函数。
#ifdef DEBUG #define MY_OPT #else #define MY_OPT _Pramga(O2, unroll_loop) #endif // 这里注意,在pragma与#之间有空白符, // 说明这里的#表示将pragma宏参数进行字符串化。 // 后面的#dup表示复制宏,由于#与dup之间没有任何空白符 #define FOO(token1, token2, dup) _Literal(_Concat(token1, token2)) ; _Dup(dup, # a--; #) MY_OPT func MyFunc() { obj a: int = _Defined(MY_OPT); // 这里MY_OPT被定义了,所以值为true,然后转换为整数1赋给对象a // 这里相当于复制了10条 a++; 语句 _Dup(10, # a++; #) printf("a = %d\n", a); // # ... #这种语法形式也可用于传递宏调用实参的场合。 // 这样实参还能带有逗号,还不会发生词法解析上的冲突。 obj s1 = FOO(#Hello, #, #world!#, 3); // 上述这个宏调用相当于: obj s2 = "Hello, world!"; a--; a--; a--; }
通过上述示例代码,各位应该能对CNeo强大灵活的预处理器机制感到格外嗨皮了吧~
另外,CNeo中的不定参数个数的宏定义与原来的C语言有些差异。下面例举一个实例来说明CNeo中不定参数宏定义:
// 定义了一个不定参数个数的宏定义 #define MY_VARIADIC_MACRO(s, args...) printf(s, args); // 调用宏MY_VARIADIC_MACRO MY_VARIADIC_MACRO("Hello, world"); // 这个调用后,最后一句printf(s, args);会被扩展为printf("Hello, world", ); // 不过在CNeo中由于函数调用时,其实参列表本身就允许最后带一个逗号,因此 // 这么扩展不会有任何问题。
因此,在CNeo中无需使用__VA_ARGS__固定的预定义的宏扩展符号。
13、取消goto语句并加入增强的continue与break跳转语句。在CNeo中将不会有goto关键字,取而代之的是允许在if、switch、do、while、for这些表示控制流的语句之前添加标签。在if和switch这两种选择语句中,可使用break后面加标签名以退出指定标签的条件语句。不过在if选择语句中不可单独使用break;,break后必须加标签名。而在do、while、for语句中,可使用break和continue加标签来跳出或继续执行指定标签的那个循环。这个机制借鉴了Swift编程语言中的跳转机制。下面举一些例子进行说明:
obj a = 10, b = 20; IF_LABEL1: if( a == 10) { if(b == 20) break IF_LABEL1; // 直接跳出当前的if条件的执行 // 以下这句将不会被执行 puts("Hello"); } IF_LABEL2: if (a == 5) b++; else { if(b == 20) break IF_LABEL2; // 直接跳出当前的if条件的执行 // 以下这句将不会被执行 puts("Hello"); } CASE_LABEL: switch(a) { case 1 { b++; break; } case 10 { if(b == 20) break CASE_LABEL; // 这里相当于break; puts("Hey!"); break; } default; } LOOP_FOR: for(obj i = 0; i < 10; i++) { LOOP_FOR: while(a > 0) { LOOP_DO: do { if(a == 5) continue LOOP_FOR; // 这里的continue直接跳转到对while(a > 0)的执行 a--; b--; if(b == 15) break LOOP_DO; // 这里就相当于break; } while(a > 5); if(b < 10) break LOOP_FOR; // 这里则退出整个大循环
}
}
14、改进并新增存储类类型:传统的C语言具有两种存储类(storage-class)类型,即extern和static,前者表示对象或函数具有外部连接,后者表示函数或对象具有内部连接。而在CNeo中新增了native存储类,表示函数或对象具有库连接。具有库连接的符号可以在生成静态库或动态库时就将该符号解决,而不需要暴露出来给其它库或程序进行使用。所以配合第3条新增语法特性,lib_name.symbol,后面的symbol肯定是具有外部连接的符号。在CNeo中,缺省存储类关键字,默认为native,而不是extern的。
15、CNeo可选地给出向量数据类型,可用于利用某些支持SIMD的CPU做数据级并行计算。当前支持的向量数据类型有:byte16,ubyte16,short8,ushort8,word4,uword4,long2,ulong2,half8,float4,double2。这里无论是哪种处理器,都需要严格遵循以下对类型字节长度的规定:byte作为一个字节;short作为两个字节;word作为四个字节;long作为八个字节;half作为半精度浮点(两个字节);float作为单精度浮点(四个字节);double作为双精度浮点(八个字节)。
向量数据对象可通过使用数组索引方式来访问其指定lane的数据。比如:
// 这里word4(0, 1, 2, 3)是一个word4类型的字面量 obj a = word4(0, 1, 2, 3); obj b = word4(5, 6, 7, 8); a += b; // 相当于 a[0] += b[0]; a[1] += b[1]; a[2] += b[2]; a[3] += b[3]; // 任一向量数据类型均可被转换为其他向量数据类型, // 这样对于初始化byte16这种很多lane的小数据向量类型对象就十分方便了 obj c = byte16(a);
以上例子介绍了向量数据类型的基本初始化以及使用方式。
16、这里先简单例举一下CNeo中针对C11而废弃使用的关键字:goto、inline、register、imaginary。由于在CNeo中,所有静态函数都默认是自动内联的,因此不需要inline关键字。此外,程序员可以通过显式地给函数增加[[always_inline]]属性迫使它总是内联。register关键字在C11标准中也明确指出将在下一个C语言版本中废弃。imaginary则是由于当前主流编译器均没有对它支持,所以必要性也不大,CNeo将仅支持complex。而对于联合体,由于CNeo全面支持泛型,因此一般使用泛型结构体就能解决很多需要联合体才能方便解决的问题。