C1 深挖关键字

0 定义与声明

  • 定义:编译器为某变量创建一个对象并分配一片内存且给内存取一个名字,该名字就是所谓的变量名。示例如下:
int a;
  • 声明:虽然有一个名字但是却没有分配内存。示例如下:
extern int i; 
int add(int a, int b);	//函数形参,未分配内存

1 auto关键字

  • 在缺省情况下,编译器默认所有变量都是auto类型的

2 register关键字

2.1 register简介

  • 总所周知,寄存器是最靠近CPU的一级存储介质,其读写速度也是最快的
  • 如果某一个变量被冠以register的头衔,其意思就是让编译器尽可能地将该变量存储在CPU内部寄存器中。而不是按照常规那样通过去内存寻址去获得某一个变量值
  • 但是由于CPU内部寄存器是有限的(一般就十几个),假设我们在代码中定义了若干个register所修饰的变量,最终也只会有几个被真正存入CPU寄存器中

2.2 register作用

  • 由于CPU的读写速度超快,而现代存储器(如硬盘)的读写速度相较于CPU读写速度又显得十分慢,为了平衡二者之间的速度差异,因此诞生了寄存器
  • CPU相当于皇帝,寄存器相当于太监,存储介质(如硬盘)相当于大臣,数据相当于奏折。首先分析一下批阅奏折的场景,某大臣拿着一堆奏折来到大殿,然后太监将奏折从大臣手中拿过来转交给皇帝,皇帝批阅之后再交给太监,太监最后再交给大臣;同理在计算机的世界中,当需要修改硬盘中的数据时,寄存器将保存从硬盘读过来的数据,之后CPU再从寄存器那里取出数据处理,处理完成之后再将数据传给寄存器,最后再将寄存器中的数据写入硬盘。
  • 上述的例子中太监是主动接手转交奏折,而寄存器可没这么勤奋,寄存器从来不主动做事情,一般都是在CPU指挥下完成数据中转任务

2.3 register注意点

  • register虽快,但其并不是万能的,由于寄存器与CPU挂钩,因此被寄存器所修饰的变量必须能够被CPU所接受
  • register变量必须是一个单个的值,其长度应该小于或者等于整形的长度
  • register变量不能被存在内存中,因此也就不能使用&符号来获取变量地址

3 static关键字

static关键字是使用频率较高的关键字,虽说叫静态,实则一点都“不安静”

3.1 修饰变量

  • 静态全局变量:修饰全局变量的时候,该变量的作用域就被限制在了变量被定义的文件中了,在其他文件中无法使用extern对变量进行声明
  • 静态局部变量:该变量在函数内部定义,故而该变量只能在函数内部使用,同一个文件下的其他函数无法使用该静态局部变量

注意:静态局部变量只会被初始化一次

  • 例子,有如下一段代码
#include <stdio.h>

static int a;

void fun1(void)
{
    static int b = 0;
    b++;
    printf("b=%d,", b);
}

void fun2(void)
{
    a = 0;
    a++;
    printf("a=%d ", a);
}

int main(void)
{
    for (int i = 0; i < 10; i++)
    {
        fun1();
        fun2();
        //printf("a=%d,b=%d ", a, b);     //can not be access variable "b"
    }
    return 0;
}

代码执行结果为:b=1,a=1 b=2,a=1 b=3,a=1 b=4,a=1 b=5,a=1 b=6,a=1 b=7,a=1 b=8,a=1 b=9,a=1 b=10,a=1,可以看到静态局部变量只被初始化了一次

3.2 修饰函数

  • 在函数前面使用static修饰可使得该函数变成静态函数,静态函数的作用域仅局限在本文件中,因此当不同的人编写不同的函数时,不需要去担心函数名称重复的问题,在linux驱动中就存在大量静态函数

4 基本数据类型

  • 在32位系统上各数据类型大小如下表:
数据类型 大小(字节)
char 1
short 2
int,long,float 4
double 8

5 sizeof关键字

  • 可能大部分人认为sizeof是函数,实则它是一个关键字,此处可以使用编译器来确认,测试代码如下:
#include <stdio.h>

int main(void)
{
    int i = 0;
    printf("%ld ", sizeof(int));
    printf("%ld ", sizeof(i));
    printf("%ld ", sizeof int);
    printf("%ld ", sizeof i);
}
  • 编译结果如下,最后除了第三种表达方式报错之外,其他表达方式输出结果均为4。假设sizeof是一个函数,那么sizeof i将不合法。我们可以写为unsigned int但是不能写sizeof int
sizeof.c:8:27: error: expected expression before ‘int’
    8 |     printf("%ld ", sizeof int);
      |                           ^~~
  • 总之一句话,sizeof在计算变量所占空间大小的时候,括号可以省略;在计算数据类型(如intchar)的大小的时候,括号不能省略

5.1 奇怪的表达法

  • 介绍几种sizeof奇怪的表达法,如下例子所示,
#include <stdio.h>

int main(void)
{
    printf("%ld\n", sizeof(int)*p);		//4*p
    
    int *p = NULL;
    printf("%ld\n", sizeof(p));			//8,指针大小,32位系统指针大小都是8字节
    printf("%ld\n", sizeof(*p));		//4,指针解引用,即该指针指向的int变量大小,为4字节
    
    int a[100];
    printf("%ld\n", sizeof(a));			//400,数组a的大小,为100x4=400字节
    printf("%ld\n", sizeof(a[100]));	//4,求数组中第100个元素的大小,为4字节
    printf("%ld\n", sizeof(&a));		//8,求数组首地址的大小,为指针类型,8字节
    printf("%ld\n", sizeof(&a[0]));		//8,同上
}

6 signed、unsigned关键字

  • 众所周知,数据在计算机中最终都是按照二进制的方式进行存储,那么负数该如何存储呢?答案是做标记,即将基本数据类型的最高位腾出来,用来存放符号。如果最高位是1,那么表示这个数是负数;最高位是0,表示这个数是正数
  • 原码:即符号位加上真值的绝对值
  • 反码:正数的反码是其本身,负数的反码是在其原码基础上符号位不变,其余各位按位取反
  • 补码:正数的补码是其本身,负数的补码是在其原码基础上符号位不变,其余各位按位取反,最后加1
  • 按照上述约定,一个32位的signed int类型的整数,其表示范围是-231(2<sup>31</sup>-1);8位的`char`类型数组其表示的范围是-2<sup>7</sup>(27-1);同理一个32位的unsigned int类型的整数,其表示范围是0(2<sup>32</sup>-1);8位的`char`类型数组其表示的范围是0(28-1)
  • 需要注意的是,编译器在默认情况下数据类型都为signed类型
  • 上述规则看似简单,实际却需要十分小心,例如下述例子
#include <stdio.h>
#include <string.h>

int main(void)
{
    char a[1000];
    int i;
    for (i = 0; i < 1000; i++)
    {
        a[i] = -1 - i;
    }
    printf("%ld\n", strlen(a));
    return 0;
}
/*
	数字-0
原码:1000 0000
反码:1111 1111
补码:0000 0000(人为规定,与-128区分)
	数字-1
原码:1000 0001
反码:1111 1110
补码:1111 1111
	数字-2
原码:1000 0010
反码:1111 1101
补码:1111 1110
	数字-128
原码:1000 0000
反码:1111 1111
补码:1000 0000(人为规定)
	数字-129
原码:1 1000 0001
反码:1 0111 1110
补码:1 0111 1111
*/
1 0000 0000
1 1111 1111
1 0000 0000

在上述例子中,实际结果为255。首先当i的值为0的时候,a[0]的值为-1,其在计算机中是按其补码形式存放,即0xff,-2的补码是0xfe;以此类推,当i的值等于127的时候,a[127]的值为-128,其补码为0x80。当i的值为128的时候,a[128]的值为-129,明显超出了char所表示的范围,因此其最高位将被舍弃,最终保存为0x7f,当i增加到255的时候,a[255]为-256,其在8位的char类型下面将保存为0x00。由于strlen函数是以\0为结束符,该符号的asiic码刚好为0,对应了a[255]的值,因此最终strlen(a)为255,即a[0]~a[254],共计255个元素

  • 补充:+0和-0的补码是一样的,即0的补码只有一种表示,即0000 0000。而-128的补码是1000 0000
  • 再来一个例子,当输出显示为有符号的时候输出为-10,无符号的时候就会是很大一个数,因为-10的补码刚好对应4294967286
#include <stdio.h>
#include <string.h>

int main(void)
{
    int i = -20;
    unsigned j = 10;
    printf("%d\n", i + j);
    printf("%u\n", i + j);
    return 0;
}

//执行结果
-10                       	1000 0000 0000 0000 0000 0000 0000 1010
                            1111 1111 1111 1111 1111 1111 1111 0101
                            1111 1111 1111 1111 1111 1111 1111 0110
4294967286                  1111 1111 1111 1111 1111 1111 1111 0110
  • 最后一个例子,由于i是无符号类型,当i自减到0的时候又变成了65535,最终导致i永远大于等于0,进而会进入死循环。因为0减去1是-1,越界了。假如编译通过,编译时相当于隐式强制转换(unsigned)-1,根据它的补码形式,符号位变数字位,全1,即对应数据类型的最大值。
#include <stdio.h>

int main(void)
{
    unsigned i;
    for (i = 9; i >= 0; i--)
    {
        printf("%u\n", i);
    }
    return 0;
}

7 if、else

7.1 bool变量与“零值”进行比较

  • 有如下几种写法
bool test_flag = FALSE;

/*此种写法会让人误以为test_flag是整形变量*/
if (test_flag == 0);	if (test_flag == 1);

/*不同编译器会将TRUE与FALSE定义成不同的值,此种写法通用性不强*/
if (test_flag == TRUE);	if (test_flag == FALSE);

/*正确写法*/
if (test_flag);		    if (!test_flag);

7.2 float变量与“零值”进行比较

  • 有如下几种写法,因为floatdouble类型的数据都是有精度限制的,举个例子,假设某float类型变量在其精度之外为非0,但是在其精度范围之内将会被四舍五入为0
  • float的精度是保证至少7位有效数字是准确的
  • https://blog.csdn.net/albertsh/article/details/92385277
  • 不要在很大的浮点数与很小的浮点数之间做运算
float test_val = 0.0;

/*表达有误,假设test_val为0.0000 00001,它明显不为0,但在四舍五入后却为0*/
if (test_val == 0.0);	if (test_val != 0.0);

/*正确表达方式,此此写法会在[0.0-EPSINON, 0.0+EPSINON]闭区间内保证数据的准确性*/
if ((test_val >= -EPSINON) && (test_val <= EPSINON));	//EPSINON为定义好的精度

7.3 指针变量与“零值”比较

  • 有如下几种写法
int *p = NULL;

/*此种写法会让人误以为p是整形变量*/
if (p == 0);	if (p != 0);

/*此种写法会让人误以为p是bool变量*/
if (p);			if (!p);

/*正确写法,但是需要注意,此处将NULL写在左边是防止p = NULL等赋值语句的出现*/
if (NULL == p);	if (NULL != p);

7.4 其他注意事项

  • 先处理正确情况,再处理异常情况

8 switch、case

​ 编译时会对 switch 进行优化,根据 case 标签后面的常量值,生成跳转表,只经过少数次数的比较,就可以跳到对应标签下面。所以,标签也是不能重复的。如果允许变量,switch 只能退化成跟一连串的 if else, 对于一个数据也只能从头到尾地进行比较,也就失去了 switch 的意义。跳转表和逐个比较,这两种方式的复杂度差很多。

8.1 使用规则

  • if、else一般判断的是两个分支或者嵌套比较少的分支,当分支过多的时候需要使用switch、case组合
  • 每个case后不要忘了加break
  • 最后必须使用default分支
  • case后面只能是整形或者字符型的常量或者常量表达式
  • case语句排列规则
  • 在所有case语句没有明显重要性的情况下就按照字母或者数字的排列顺序排列各条case语句
  • 将正常情况放在前面,异常情况放在后面(别忘了注释)
  • 按执行频率排列case语句,将最常执行的情况放在前面
  • 如果你在switch中使用continue,continue生效是对于while循环;如果你在switch中使用break,break生效是对于switch;如果在switch外使用continue和break,生效都是对于while循环。
#include <stdio.h>
 
int main()
{
	int k;
	char c;
	for(k=1,c='A'; c < 'F'; k++)
	{
		switch(++c)
		{
			case'A': k++; printf("%c %d\n",c,k);break;
			case'B': k *= 2; printf("%c %d\n",c,k);break;   //跳出switch()执行其后的语句
			case'C': k--; printf("%c %d\n",c,k);	       //不论条件为何值,继续执行下一条case判断(case'D':)后面的语句                            
			case'D': k %= 3; printf("%c %d\n",c,k);continue; //不执行switch块后面的语句,跳出“本次”循环直接到外层循环
			case'E': k /= 2; printf("%c %d\n",c,k);
			case'F': k++; printf("%c %d\n",c,k);
			default: k += 2; printf("%c %d\n",c,k);			//所有条件不符合,执行default后面的语句
		}
		k++;
		printf("*********************\n");
	}
	printf("%d\n", k);
	return 0;
}

//
B, 2
**************
C, 3
C, 0
D, 1
E, 1
E, 2
E, 4
**************
F, 7
F, 9
**************
11

9 do、while、for

  • 在多重循环嵌套的情况下,应该将最长循环放在内层,短循环放在外层
for (col = 0; col < 5; col++)
{
    for (row = 0; row < 1000; row++)
    {
        ...;
    }
}
  • 在for循环中循环控制变量的取值采用半开半闭区间写法
for (n = 0; n < 10; n++);		//半开半闭区间
for (n = 0; n <= 9; n++);
  • 不能在for循环内修改循环变量,防止循环失控
  • 循环尽可能短,不超过20行
  • 循环嵌套控制在3层内

10 void

  • 凡是不加返回值类型限定的函数,就会被编译器作为返回整数处理
  • 如果函数的参数可以是任意类型指针,那么应该声明其参数为void *
void *memcpy(void *dest, const void *src, size_t len);
posted @ 2021-08-03 00:03  MHDSG  阅读(70)  评论(0)    收藏  举报