C语言关键字的特性

volatile关键字

鲜为人知的关键字之一volatile,表示变量是’易变的’,之所以会有这个关键字,主要是消除编译优化带来的一些问题,看下面的代码

1
2
3
int a = 8;
int b = a;
int c = a;

编译器认为,上面的第2句代码与第三句代码之间,没有存在对a赋值的语句,所以编译出来的汇编代码在讲a的值赋给c的时候,不会再次到内存取这个变量的值,而是取cache中的值。这样虽然提高了效率,但也带来了一些问题,比如如果变量a被多个线程共享,且在a赋值给了b之后,a的值立马被另一个线程修改,则再赋值给c的就是过时的数据,有时希望c拿到的是实时的数据,这个时候volatile关键字就派上了用场

1
2
3
volatile int a = 8;
int b = a;
int c = a;

上面的关键字告诉编译器a的值是随时可能发生变化的值,要求每次使用都到内存中取值,这样就能保证c能获得实时数据。

sizeof关键字

很多人都认为sizeof 是函数,因为带括号嘛,还有返回值,不是函数是啥。其实sizeof 是关键字,不信你在测试变量的时候把括号去掉试试,当然,如果测试的是类型,则必须加括号,因为你如果sizeof 类型,不打扩号的话,编译器认为你在定义变量,而定义变量的时候前面显然是只能是修饰符如const,static和extern之类的,绝对不能是sizeof 所以会报错。

1
2
3
4
5
int a = 9; 
sizeof(a) ; // 合法
sizeof a ; // 合法
sizeof int ;// 非法
sizeof(int);// 合法

register关键字

register关键字定义的变量可能放在寄存器里面,可能放在寄存器里,也可能放在内存里,所以为了安全起见,不能对寄存器变量取地址,所以下面的代码编译会报错

1
2
register int a = 0;
printf("%d\n",&a);

const关键字

C语言中,const关键字定义了一个不可变的变量a ,注意a还是一个变量,没错是变量,不是常量,只是值不能变,是只读变量,编译的时候是不能确定值的。下面的代码可以说明问题

1
2
const int a = 4;
int arr[a];

上面的代码在VC6.0的ANSI标准下会报错,因为const定义的依然是变量,当然在GNU这种先进的编译器下会通过。

typedef关键字

大多人认为typedef是定义一个新的数据类型,其实不是,typedef关键字是给一个已经存在的数据类型取一个别名,很多人喜欢在定义类型的同时使用 typedef关键字,这就让自己慢慢的也误以为typedef是在定义一种新的数据类型

1
2
3
4
5
typedef struct s{
int a;
int b;
int c;
} NS;

其实换成像下面这样可能会更好

1
2
3
4
5
6
struct s{
int a;
int b;
int c;
};
typedef struct s NS;

另外看看下面的代码

先添加这样的声明

1
typedef struct s * PNS;

看下面的代码

1
2
3
4
5
6
7
8
NS ns;
const PNS pns1 = &ns;
pns1->a = 8;
NS ns2 ;
pns1 = &ns2; // 报错,pns1 只读
PNS const pns2 = &ns;
pns2->a = 8;
pns2 = &ns2; // 报错,pns2 只读

大家可能都能明白 const int * pint * const p的区别,但这里就有些模糊了,这个结果颠覆了大家的思维。

这是因为能把 (struct s *)重定义为一个整体,const遇到整体的类型定义会直接将这个整体忽略,也就是对于const int * pint * const p以及const int pint const p,编译器会把int忽略,得到 const * p* const p,以及const p

所以对于cosnt PNS pns1PNS const pns2,PNS会被忽略,就得到了const pns1const pns2,所以const修饰什么显而易见

数据类型篇

struct类型

相信让大家说structc++class的区别,99%的开发者都知道有,标准的C语言中struct中不能定义函数的

1
2
3
4
5
6
struct s{
int a;
int getA(){
return a;
}
};

上面的代码在C语言的环境下会报错。再就是structclass的默认访问属性不同。

除了上面的区别,struct还具备一些class不具备的一些属性

1
2
3
4
5
6
7
8
9
10
11
struct s{
int a;
int b;
int c;
};
// 直接初始化
struct s ele = {1,2};
// 全部成员初始化为0
struct s ele2 = {0};
// 指定初始化
struct s ele3 = {.a = 1};

还用空的结构体大小,在老版本的VC6.0 (应该是C89标准)不为0,而为1 ,因为最小的c语言类型为char,一个字节,struct的设计者要求struct至少能容纳一个字符,但是到了现在的C11标准,C语言中的空结构体大小为0,在C++中大小为1。

另外,结构体还有一个很神奇的东西–柔性数组,也就是结构体的最后一个成员可以定义为一个柔性数组–b变长数组。这个柔性数组的大小不会算在结构体的大小内,向下面这样

1
2
3
4
5
6
7
8
9
10
11
struct s{
int a;
int b;
int c;
int arr[];
};

typedef struct s NS;
typedef struct s * PNS;
// 实例化
PNS p = (PNS) malloc(sizeof(NS)+100*sizeof(int));

上面的代码就定义了一个结构体,并且分配了一个大小为100的柔性数组

多字符常量

1
int str = 'ABCD';

上面的代码会让四个字母分别占据int的四个字节,至于具体值,取决于存储的是大端模式还是小端模式

表达式和结构篇

switch语句

  • 奇葩写法1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    char ch = 'c';
    switch(ch){
    case 'a'...'z':
    printf("a-z");
    break;
    case 'A'...'Z':
    printf("A-Z");
    break;
    default:
    break;
    }
    //运行结果a-z

这种写法还算正常,GNU C扩充的,能够接受,下面这种。。

  • 奇葩写法2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int a = 3,b = 4,m;
    switch(a){
    case 1:
    printf("1");
    break;
    if(b == 4){
    case 2:
    printf("2");
    ;
    }else case 3:{
    printf("3");
    for(m = 1;m<3;m++){
    case 4:
    printf("4");
    ;
    }
    }
    default:
    break;
    }
    // 运行结果 344

第一次看到,我也惊呆了

scanf忽略输入

这个问题相比很多人都遇到过,scanf读取无用的换行符,下面的代码可以很好的解决这个问题

1
2
3
4
char c1,c2;
scanf("%c%*c%c",&c1,&c2);
putchar(c1);
putchar(c2);

这样,你换行输入单个字符才不会有问题,也有用下面这样的代码过滤换行符的

1
while((ch = getchar()) == '\n');

printf变量限定格式

1
2
3
int a=3;
float m = 3.1415926;
printf("%.*f\n",a,m); // 3.142

宏定义中的#号

1
2
3
4
5
6
7
#define SQR(x) printf("x^2 = %d\n",((x)*(x)));
#define SQR2(x) printf(""#x"^2 = %d\n",((x)*(x)));
#define SQR3(x) printf("%d^2 = %d\n",x,((x)*(x)));

SQR(3); // x^2 = 9
SQR2(3); // 3^2 = 9
SQR3(3); // 3^2 = 9

数组名

数组名是指针常量,定义完之后不能修改

1
2
3
4
5
int arr[3] = {1,2,3};
int a2[3];
int * p = a2;
arr = p;
arr = a2;

函数调用时不能传递数组,传递的只不过是一个指针

1
2
3
4
5
void fun(int arr[100]){
printf("%d\n",sizeof(arr));
}
int arr[3] = {1,2,3};
fun(arr); // 4

没错,那个参数列表中的100然并luan。关于向函数传递数组,后面还有讲解。

指针与函数篇

指针这部分如果学到比较好的这个应该都知道,算不得什么特性

直接对内存地址赋值

1
*(int*)0x12ff7c = 100;

取数组一行的最后一个值

1
2
int arr[5] = {1,2,3,4,5};
printf("%d\n",*(*(&arr+1)-1)); // 5

这个其实也很简单,arr是一级指针,列指针,再取一次地址后得到行指针,+1之后偏移一行,再解引用降级为列指针,再减1恰好指向arr[4],所以就是5。另外注意arr其实就是&arr[0]的值,也就是数组首元素的首地址。它与数组首地址其实有区别的,当arr为二维数组的时候,两者就存在区别。如果为二位数组,则arr==&arr[0]==&&arr[0][0]。

数组与指针参数

就像前面说到的,不能像函数传递一个数组,传递数组,编译器总是将它解析成一个指向数组首元素的指针,也就是说传递的使用个指针,指向数组的首元素,但不指向数组,也就是说传递arr与传递&arr[0]没有区别,这进一步说明了数组首地址与数组首元素的首地址是有却别的。

另外,指针传递也是数值传递看下面的代码

1
2
3
4
5
6
7
int f(int * p){
p = NULL;
}
int a = 3;
int *p = &a;
f(p);
printf("%d\n",*p);

在没有C++引用传递的情况下,想传递指针,就要传递指针的指针。像下面这样

1
2
3
4
5
6
int f2( int ** pp){
*pp = (int *) malloc(sizeof (int));
**pp = 9;
}
f2(&p);
printf("%d\n",*p); // 9

指针返回值

不要将局部变量的地址作为返回值返回,像下面这样的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int * getP(){
int a = 4;
return &a;
}
int * getP1(){
int * p = (int *) malloc(sizeof(int));
*p = 4;
return p;
}

int *p = getP();
int *p1 = getP1();
printf("%d\n",*p);
printf("%d\n",*p1);

虽然在我测试的时候都给出了正确的结果,但是这样做还是很危险的,因为局部变量在函数执行完毕后会被销毁,这个时候如果将局部变量的地址返回可能会得到野指针。

函数指针

下面来分析一个比较复杂的函数指针调用

1
(*(int** (*) (int **,int **))0)(int **,int **);

有点晕,其实分开来看,

int** (*) (int **,int **) 其实就是一个函数指针,函数的返回值是整形的二级指针,参数是两个整形的二级指针。

(int** (*) (int **,int **))0就是讲地址0指向的区域转换为函数指针

*(int** (*) (int **,int **))0就是对这个函数进行解引用

(*(int** (*) (int **,int **))0)(int **,int **)则是指行函数调用

先整理这么多吧,C语言博大精深,有着各种鲜为人知的高级特性,这里列出来的只是九牛一毛而已,权当复习而已。

文章目录
  1. 1. volatile关键字
  2. 2. sizeof关键字
  3. 3. register关键字
  4. 4. const关键字
  5. 5. typedef关键字
  6. 6. struct类型
  7. 7. 多字符常量
  8. 8. 表达式和结构篇
    1. 8.1. switch语句
    2. 8.2. scanf忽略输入
    3. 8.3. 数组名
  9. 9. 指针与函数篇
  10. 10. 数组与指针参数
  11. 11. 指针返回值
  12. 12. 函数指针