参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
视频教程:C语音深入剖析班(国嵌 唐老师主讲)
常量表达式
常量表达式是指该表达式在编译期间就能够被计算出来,而不需要生成相应运行时代码。常量表达式不应该含有赋值(=
)、递增(++
)、递减(--
)、函数调用(func()
)以及逗号操作符(,
),除非它们作为包含在一个不被计算的子表达式这中(比如包含在sizeof
操作数中),不过在sizeof
的操作数中也不允许有++
、--
、=
的操作,因为这些都是要CPU进行运算的,在编译器CPU是不会进行运算操作的,逗号运算符,函数调用都能写在sizeof
操作数中,并且函数调用写在sizeof
操作数中必须先进行声明,因为先声明,编译器就知道了所需要的内存大小,所以在编译的时候会直接编译成所需的大小,因此可以写在sizeof
操作数中。
例如
#include <stdio.h>
void test();
long long test2();
static int a = 100;
static const int c = sizeof(a,a,a); //编译通过,编译器编译时会当成sizeof(100,100,100);,最后编译成static const int c = 4;因为100在编译器中当成是int的内存大小
static const int e = sizeof(a,10,11,12); //这个也是可以的
static const int t = sizeof(test()); //因为声明中已经知道了函数的大小,所以可以通过编译,编译后是static const int t = 1;
static const int y = sizeof(test2()); //编译后成了static const int y = 8;
对于初始化器中的常量表达式,它可以是
- 一个算术常量表达式;如:
int a = 10 + 20;
或const int b = 30;
const int c = b + 20;
,这里可以用b去写常量表达式是因为b是一个常量 - 一个空指针常量;
NULL
- 一个地址常量;
int a[3];static const int *b = a;
,这里的a就是一个地址常量 - 一个完整对象类型的地址常量加上或减去一个整数常量所构成的表达式。如
int a[3];static const int *b = a + 1;
一个算术常量表达式应该具有算术类型,并且其操作数应该仅为整数常量、浮点常量、字符常量、不含有可变修改类型的sizeof
表达式,比如sizeof(a[v]);
里面是一个可变的数组,这样不行,因为可变数组是在运行时才确定值的,以及_Alignof
表达式,而且在一个算术常量表达式中的投射操作(强制类型转换)也应该只是将一种算法类型转换为另一个算术类型,除非作为sizeof
与_Alignof
的操作数。
要判定一个表达式是否为常量表达式其实比较简单,可以对一个全局对象进行初始化,如果能通过编译,那么为它初始化的表达式就基本是一个常量表达式,否则它就不是一个常量表达式,因为全局对象在编译的时候就已经要确定值的了,然后在运行的时候就只需要直接加载,而不需要运算。
#include <stdio.h>
#include <stdint.h>
//这里声明了一个静态变量对象a,并且这里的100是一个常量表达式
static int a = 100;
//由于静态文件作用域对象a不是一个常量,所以这里的a不是一个常量表达式。
//这条语句会引发编译错误——初始化器元素不是一个编译时常量
//static const int b = a;
//这里用const声明了一个常量对象c,并且这里的(200 - 100) / 2是一个常量表达式
static const int c = (200 - 100) / 2;
//由于对象c是一个常量,所以这里的(c + 2) << 1是一个常量表达式
static const int64_t d = (sizeof(c) + 2) << 1;
//整个(sizeof(d) > sizeof(c))? sizeof(d) + 1 : sizeof(c) - 1表达式是一个常量表达式
static int e = (sizeof(d) > sizeof(c))? sizeof(d) + 1 : sizeof(c) - 1;
//如果一个常量表达式作为一个初始化器,那么它可以是一个地址常量。
//这里,&e 这个表达式就是一个地址常量
int *p = &e;
//在GCC与clang中,被const修饰的一个常量对象也能作为初始化器的一个常量表达式,
//但在MSVC中却不被允许,C语言标准没有指定const修饰的对象是否能作为一个常量表达式,
//但C语言标准声明了,允许C语言实现接受其他形式的常量表达式
static int64_t maybeError = c + d;
int main(int argc,const char *argv[]) {
printf("a = %d,c = %d,d = %ld,e = %d\n",a,c,d,e);
return 0;
}
运行结果
a = 100,c = 50,d = 12,e = 9
泛型选择表达式
泛型选择表达式是C11标准新引入的一个语法特性。使得C语言使用轻量级的泛型机制
_Generic (赋值表达式,泛型关联列表)
泛型关联列表的形式是一组用逗号分隔的类型与表达式相联结的表达式
类型名 : 赋值表达式
整个泛型选择表达式的语义为:C语言实现先获取最左边的赋值表达式的类型,这里要注意的是,此获取类型的动作与sizeof
表达式一样,仅获取类型而不对表达式做计算,也不会生成相关的运行时代码,然后将获取到的类型与泛型关联列表中每一个“类型名”部分进行比较,如果两者兼容则选择该“类型名”所对应的赋值表达式作为整个泛型表达式的计算结果,否则跳过当前的泛型关联,尝试匹配下一条。其中,而类型名部分还可以用default
来表示当泛型关联列表中没找到与最左边的赋值表达式的类型相匹配的类型时所选出的表达式。
#include <stdio.h>
int main(int argc,const char *argv[]) {
//这里列举了一个简单的泛型选择表达式。
//该表达式中,对表达式100进行获取类型,然后对后面的泛型关联进行匹配。
//我们知道100属于int类型,因此最终整个泛型选择表达式的结果为表达式1.
//这条语句在编译完成后其实就相当于:int a = 1;
int a = _Generic(100,float : -1,int : 1,default:0);
printf("a = %d\n",a); //这里输出:a = 1
//在这条泛型选择表达式中,
//(++a,a + 1.5f)这一逗号表达式最终计算出的类型是float,
//由于泛型选择表达式中最左边用于类型匹配源的赋值表达式不被计算,
//所以这里的++a没有任何效果
a = _Generic((++a,a + 1.5f),int:a + 10,float:a + 100,double:a + 1000,default:a);
printf("second a = %d\n",a); //这里输出:a = 101
//尽管在一般赋值表达式中,float能隐式地转换为double类型,
//但是在泛型选择表达式中,类型匹配是相当严格的,a + 1.5f是float类型,
//那么由于在这里找不到float类型,
//C语言实现就会选择defalut中的表达式作为整个泛型表达式的结果。
//这条语句就相当于a = a;其实就如同一条空语句
a = _Generic(a + 1.5f,int:a + 10,long:a + 100,double:a + 1000,default:a);
printf("third a = %d\n",a); //这里仍然输出:a = 101
const char *output = "none";
//当我们的匹配源表达式是一个字符串字面量的时候必须注意,
//C语言标准中是将字符串字面量视作为char*类型,但有些C语言实现在匹配泛型关联的时候,
//可能仍然会将字符串类型设定为const char[N],N表示字符串中字符个数再加上一个'/0'结束符。
//因此使用时应当小心,尽量使用投射操作做显式的类型转换
output = _Generic("abc",const char *:"const char*",char*:"char*",const char[4]:"const char[4]",char[4]:"char[4]");
//我们尝试下面这条语句
//会看到当前编译器在做编译报错时仍然会把"abc"作为char[4]或const char[4]类型
//"abc" = 100;
printf("output is: %s\n",output);
struct Point {int x,y;};
struct Size {int width,height;};
struct Point p = {};
//结构体的类型匹配也同样如此
output = _Generic(p,struct Point:"Point",struct Size:"Size",default:"none");
printf("p is a %s\n",output);
int x,y;
//下面我们通过投射操作加逗号表达式作为泛型关联,分别给x、y两个对象进行初始化。
//我们在每个泛型关联中的表达式的前面加上(void),表示整个表达式为void表达式,不过最后还是都会转回成对应的类型,也就是加与不加区别不大。
//要注意,如果一个泛型关联中出现多个赋值表达式,需要用逗号分隔,不能使用分号。
//此外,需要给这些赋值表达式加上圆括号,以防止逗号作为泛型关联的分隔符
_Generic(x + y,int:(void)(x = 1,y = 2),float:(void)(x = 0.1f,y = 0.2f),default:(void)0);
printf("x = %d,y = %d\n",x,y);
return 0;
}
运行结果
a = 1
second a = 101
third a = 101
output is: char*
p is a Point
x = 1,y = 2
泛型选择表达式最常用的是用于做一些标准库。如:C语言标准库中不少数学函数都带有类型前缀与后缀,通过泛型表达式我们可以将这些前缀与后缀给应用开发者给抽象掉,不暴露出来,这样便于开发者更便捷地使用这些标准库API。
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
//这里定义了一个gen_abs宏函数,用于判定expr表达式的类型
//如果是int,则使用abs函数;如果是long,则使用labs;
//如果是float,则使用fabsf库函数,如果是double,则使用fabs库函数
//如果是long double,则使用fabsl库函数;默认使用fabs
#define gen_abs(expr) _Generic((expr), int:abs,long:labs,float:fabsf,\
double:fabs,long double:fabsl,default:fabs) \
(expr)
//这里定义了一个gen_pow宏函数,判定base + exponent这一表达式的类型
#define gen_pow(base,exponent) _Generic((base) + (exponent),float:powf,\
double:pow,long double:powl, \
default:pow) (base,exponent)
int main(int argc,const char * argv[]) {
//由于-100表达式是int类型,所以这里选择了abs另外这里再要提醒的是,
//一个函数标志用在表达式中则默认表示一个指向函数的指针类型,
//所以这里整个泛型选择表达式的结果为:abs(-100)
int a = gen_abs(-100);
//由于1000L表达式是long类型,所以这里选择了labs
long l = gen_abs(1000L);
//由于0.5f表达式是float类型,所以这里选择了fabsf
float f = gen_abs(0.5f);
printf("value = %f\n",a + l + f);
//这里base的类型为float,exponent的类型为6.0,两者相加的类型则取double
//因此,这里泛型选择的最终表达式为pow(2.0f,6.0)
double d = gen_pow(2.0f,6.0);
printf("d = %d\n",(int)d);
return 0;
}
运行结果
value = 1100.500000
d = 64
在使用泛型表达式的时候必须要注意,在泛型关联列表中不能出现两个一样的类型,也不能找不到与赋值表达式相匹配的类型,否则会引发编译报错,对于找不到与赋值表达式相匹配的类型的情况下,一定要加上default
泛型关联。另外,泛型关联中类型后面要跟的是一个表达式,而不是一条语句,所以不能出现分号,也不能出现{}
这种语句块。
除了基本类型外,我们自定义的枚举、结构体、联合体类型以及各种指针类型也都可以用泛型表达式
静态断言
静态断言是C11标志新引入的。其意图是让C语言程序在编译时就能投提示出可在编译时判定出的错误,并输出相应提示。静态断言是一条声明,即一条语句,而不是表达式。
_Static_assert (常量表达式,字符串字面量);
它表示:如果常量表达式的值为假(即计算结果为0或者是一个空指针),那么断言失败,C语言实现将编译时产生一条诊断信息,诊断信息中包含后面字符串字面量的信息,如果常量表达式的值计算结果为真,那么断言成功,该声明不起任何作用。这里的常量表达式必须是一个整数常量表达式,一个整型常量表达式应该具有整型类型,并且其操作数应该仅为整数常量、枚举常量、字符串常量、sizeof
表达式、_Alignof
表达式,以及警告整数类型投射操作(整数的类型的强制类型转换)的浮点常量。此外,这里的sizeof
与_Alignof
的操作数不应该是可变修改类型。而像一个地址常量等表达式在这里都不能作为一个整数常量表达式。
在使用静态断言的时候应该添加上标准库头文件<asser.h>
,然后直接使用预定义的宏函数static_assert
,而不是直接使用_Static_assert
#include <stdio.h>
#include <assert.h>
#include <stdbool.h>
int main(int argc,const char *argv[]) {
//这里常量表达式直接是真,所以断言成功,不产生任何效果
static_assert(true,"This is true");
//这里常量表达式直接是假,所以断言失败,编译器将产生诊断信息
static_assert(false,"This is false");
int a = 10;
//由于这里&a不是一个整数常量表达式,所以编译器直接报错
//静态断言表达式不是一个整数常量表达式
static_assert(&a,"&a is not null!");
//由于sizeof(a)的大小不为0,因此断言成功,这里不产生任何效果
static_assert(sizeof(a),"p is null!");
enum COLOR {RED,GREEN,BLUE};
//由于枚举常量RED是0,所以这里断言失败,编译器将产生诊断信息
static_assert(RED,"This is red color";)
}
运行结果
各种报错
static_assert.c:9:5: error: static_assert failed "This is false"
static_assert(false,"This is false");
^ ~~~~~
/usr/include/assert.h:143:24: note: expanded from macro 'static_assert'
# define static_assert _Static_assert
^
static_assert.c:14:19: error: static_assert expression is not an integral constant expression
static_assert(&a,"&a is not null!");
^~
static_assert.c:20:42: error: unexpected ';' before ')'
static_assert(RED,"This is red color";)
^
static_assert.c:20:44: error: expected ';' after static_assert
static_assert(RED,"This is red color";)
^
;
static_assert.c:20:5: error: static_assert failed due to requirement 'RED' "This is red color"
static_assert(RED,"This is red color";)
^ ~~~
/usr/include/assert.h:143:24: note: expanded from macro 'static_assert'
# define static_assert _Static_assert
^
5 errors generated.
使用静态断言的条件非常有限。我们一般用静态断言对当前编译环境进行判定,而决不能用于对运行时对象值的判定。
左值
简单的讲就是能放在赋值号(=
)左边的表达式就是左值,也就是赋值号左侧要求是一个可被修改多的左值。
C11标准明确定义:一个左值表达式能隐式地用来表示一个对象,如果左值在计算时无法用来表示一个对象,那么行为是未定义的。因为是规定到一个对象中,所以左值不能是一个数组类型,不能是一个不完整类型(不完整类型都没有实际的内存大小,放到赋值号左边进行赋值时都不知道应该给多大的内存空间进行存储好,所以也就规定了不完整类型不能作为左值),不能有const
限定符修饰(因为通过const
限定符修饰后的编译器默认为该变量是一个常量,那么常量就是不能被修改的,因此也不能作为左值),并且如果该左值是一个结构体或联合体类型的话,其任一成员也不能有const
限定符修饰。
当一个左值作为单目&
操作符、++
操作符、--
操作符的操作数,或成员访问操作符.
和赋值操作符=
的左操作数后,整个表达式就不具备左值特性了(如:(a = 10) = 100;
,这时(a = 10)
过后就不再是左值了,所以不能再次进行赋值为100;又或者(student.age = 18) = 22;
,道理和刚才是一样的,已经作为了一次左值,那么就会失去左值特性),这称为左值转换。
#include <stdio.h>
int main(int argc,const char *argv[]) {
int a = 10;
int *p;
//这里的a是一个左值
a += 5;
//这里的p是一个左值
//a原本是左值,但现在使用了&操作符,那么就不具备左值的特性了
p = &a;
//当a作为++操作符的操作数时,它就不再是左值了
//所以a++表达式不是左值,不能作为=操作符的左操作数。这条语句将引发编译错误
//a++ = 0;
//声明一个类型为array[5]的数组对象
int array[5] = {1,2,3};
//数组类型的对象不能作为左值,所以它也不能作为=操作符的左操作数,以下语句会引发编译错误
//因为数组类型对象实际上是一个常量地址,常量地址不能赋值
//array = (int[]){4,5,6};
//array[1]表达式是一个左值
array[1]++;
//main是int(int,const char**)的函数类型,不能作为左值
//main = NULL; //这条语句会引发编译错误
//声明一个函数指针,指向的类型是 int (int,const char**)
int (*pFunc)(int,const char**);
//pFunc是指向函数的指针类型,可以作为左值
pFunc = &main;
//这条语句将会引发编译错误。作为=操作符的左操作数之后,a就不再作为左值,
//因此整个(a = 10)表达式不是一个左值,它不能作为=操作符的左操作数
//(a = 10) = 100;
p = array;
a = 0;
//a++不是一个左值表达式,但p[a++]表达式是一个左值
p[a++] = 30;
//当左值p[a]作为&的操作数之后就不再是左值,因此以下语句将会引发编译错误
//&p[a] = NULL;
//而当&p[a]前面再加*进行解引用(解引用也就是通过对指针对象做间接操作以访问该指针所指向对象的值)之后,*&p[a]又再度成为左值
*&p[a] += 10;
return 0;
}
上面代码的指针的引用与解引用部分还是比较好理解的,取一个对象的地址时得到的是该对象的地址值,地址值是不能被修改的,因此对对象的引用自然就是一个常量,不能作为一个左值了。而取一个指针的内容时,由于前面没有const
修饰,自然就能访问相应地址的数据内容,因此解引用表达式自然能作为左值。