C语言知识点总结

最近读了一些关于C语言的书,受益良多,也确实认识到自己的C语言水平还很差,离真正的熟练掌握C语言还有很遥远的距离,感谢这些大师们,我会坚持阅读它们直到完全掌握。

这些书也是比较经典的C语言学习和参考资料,分别是:《C专家编程》、《C和指针》、《C陷阱与缺陷》、《C语言深度剖析》及FAQ系列的《你必须知道的495个C语言问题》。

我使用的编译环境是:Keil uVision4.72,使用的MCU是STM32F103VET6。

目前我只是将个人在阅读和实际调试过程遇到的问题总结一下以免忘记,因此错漏之处肯定会有的,还会有继续的更新,希望可以和大家共同学习和进步。

1、例如a是一个数组,则a和&a之间的区别问题

有的编译器对于&a的定义认为是错误或是无效的,但在我的编译环境中,它是有效的,表示整个数组的首地址,可以定义一个数组指针来指向它,看下面的例子:

int a[5]={10,20,30,40,50};
int (*p)[5]=&a;//定义一个数组指针,执行完这句,在调试时发现p的值为0x20002B10
p++;//执行完这一句后,p的值变为了0x20002B24

上面的例子,p初始值为指向内存0x20002B10处的地址,当执行完p++后,其值为0x20002B24,实际就是0x20002B10+sizeof(a),证明了&a确实是有效的,只是在执行了p++后所指向的内存位置是下一个数组的首地址,超出了当前数组的范围,对此处的数据进行修改可能会导致程序不可预知的错误或异常。

2、关于char, signed char以及unsigned char的问题以及加不加signed的问题

对于有符号和无符号的问题以及负数在计算机中存储方式的问题我一直比较纠结,现在总算弄明白些了。

数据都是以补码形式存储,正数的补码与其原码一致,负数的补码,符号位为1,其余位为该数的绝对值的原码按位取反,然后加1。

另外,char,signed char以及unsigned char的区别在网上有很多文章,这里就不多说了,我这里主要计论当使用这几种数据类型时,发生溢出时会出现什么情况。说明一下,有的编译环境中char的值范围和signed char一样,下面举例说明下。

例如有如下的代码:

char c1=-600;;//编译器警告,会将该值从-600改为-88
signed char c2=-600;//编译器警告,会将该值从-600改为-88
unsigned char c3=500;//编译器警告,会将该值从500改为244
int i1=c2;//调试时其值为0xFFFFFFA8
unsigned int i2=c3;//调试时其值为0x000000F4

很明显,c1, c2, c3的值都不在其数据类型的范围内,也就是发生了溢出,调试时发现c1, c2的值为0xA8,也就是保存的是-88的补码,c3是244。

这个-88是怎么来的呢?先不看符号位,600的二进制表示为1001011000,由于signed char是8位的,因此高于8位的高位部分舍去只留下低8位变成了01011000,最高位为0,表示是正数,也就是88,再加上前面的负号就是-88,而0xA8是它的补码值。当然c1是char类型,值范围是0-127,不可能有0xA8这么大的值,0xA8的二进制是10101000,如果转换成char类型,则最高位1表示符号位,只取剩余的低7位,结果为十进制的40,若将c1改为unsigned char,则最高位就不是符号位了,结果就是0xA8。

再举一个例子,赋值-15698给c2,则计算如下:先不考虑符号位,15698的二进制为:11110101010010,取低8位为:01010010,高位是0表示正数,结果为82,加上前面的负号,就是-82。

再举一个正数的例子,赋值123456给c2,123456的二进制表示为:11110001001000000,取低8位是:01000000,最高位是0表示正数,结果为64。

又例如赋值500给c2,500的二进制为:111110100,500本身是一个正数,取低8位得到:11110100,若将它赋值给signed char类型,则证明最高位为符号位,这个低8位保存的是它的补码,那么现在还原补码到原码,先减1,然后除了符号位外全部取反,结果为:10001100,也就是-12。始终记得最高位是符号位,只作标识,不参与计算。

而 i1的值为0xFFFFFFA8则是因为c2发生了溢出,由于signed int数据类型长于signed char,在将短数据类型的值赋给长数据类型时,短数据类型的符号位会填充到长数据类型的高位字节,由于-88是负数,因此signed char的符号位为1,所以 i1的高位字节都是1了,那i1也就是-88了,只不过这里保存的也是它的补码。

这里以signed char为例,当给一个signed char变量赋一个超过其范围的值时,处理步骤如下:

1. 先不管该值的符号,即正负号,取该数的低8位,这个低8位所保存的值实际是最终所需值的补码(这一点非常重要);

2. 如果该8位二进制的最高位是0,则这个8位数是正数,直接返回这个8位二进制数,因为正数的补码与原码相同;

3. 如果该8位二进制的最高位是1,则这个8位数是负数,在此将补码还原成原码,先减1,然后将除最高位以外的其他位都取反,获取这个除最高位外的值;

4. 将上述获得的值与步骤1中的正负号结合,就是signed char变量所获取的最终结果,这个变量在调试时保存的是补码,实际进行算术运算时是实际的值。

3、关于枚举在内存中占用的空间问题

这个问题以前我也不知道,经过自己调试后才知道结果为1,测试代码如下:

enum Color{
		GREEN=1,
		RED,
		BLUE,
		YELLOW=10,
		WHITE
	}ColorVal;
	
	int i=sizeof(ColorVal);

4、哪些值可以作为case关键字后面的值的问题

case后面的值只能是整型或字符型的常量或者是一个常量表达式,例如以下代码都是可以通过编译的。

switch(m)
	{
		case (1+2):
			break;
		case (3/2):
			break;
                case -1:
                        break;
		case 'A':
			break;
		default:
			break;
	}

5、整除和取余运算中有负数的处理情况

整除的结果是:如果两个数的操作符相同,则结果为正,否则为负;

取余的结果是:余数的符号与被除数相同,例如有以下例子:

int i=3/(-2);//结果为-1
int j=(-3)/2;//结果为-1
int x=(-3)/-2;//结果为1
int k=3%(-2);//结果为1
int m=(-3)%2;//结果为-1
int y=(-3)%(-2);//结果为-1

另外还有关于整除时除数是浮点数的情况:

int x=2/(0.3);//结果为6
int y=2/(-0.3);//结果为-6
float z=2/(-0.3);//结果为-6.666667

但是像下面的取余的话,则除数不能为浮点数:

float x=2%(-0.3);//会出现编译错误

6、结构体中使用了#pragma pack(n)后内存对齐的情况

在结构体中,当不使用#pragma pack这个预处理的情况就不多说了,使用编译器默认的对齐方式,也就是分别按照各个元素类型的大小进行边界对齐,不足的补空白字节,并且结构体的大小必须为结构体中最大类型大小的整数倍。

当使用了#pragma pack(n)预处理后,情况有些不同了,这里以32位系统为例说明如下:

例1:

#pragma pack(2)
	struct test {
		char a; 
		double b; 
	};
#pragma pack()

int i=sizeof(struct test);//i的值为10

这里指定按两字节对齐,结构体成员a默认为按1个字节对齐,它与指定的两字节对齐大小比较,取小的,于是按1字节对齐,成员b本身按8字节对齐,但是指定了按两字节对齐,8字节与指定两字节对齐比较,取小的,因此成员b按两字节对齐,既然成员b按两字节对齐,则需要在成员a后面补充1个字节,则目前所需要的字节数为:1+1+8=10,而且10满足是结构体中最大对齐参数2的整数倍,因此整个sizeof的结果就是10。
 

例2:

#pragma pack(4)
	struct test {
		char a; 
		double b; 
	};
#pragma pack()

int i=sizeof(struct test);//i的值为12

例2和例1比较,主要是对齐系数大小不同,这里指定按4字节对齐,成员a还是按1字节对齐,成员b本身占用8个字节,和指定对齐系数4比较,取小的,则按4字节对齐,所以成员a需要补充3个空白字节,则目前所需要的字节数为:1+3+8=12,而12也满足是最大对齐系数整数倍的要求,所以sizeof最后的结果是12。

最后,当没有使用#pragma pack(n)预处理而使用编译器的默认对齐方式时,sizeof的结果就是16了。

关于在结构体中存在数组的对齐问题,实际是按数据类型大小来的对齐的,例如:结构体中有个成员是char c[3],则其对齐方式和分别写3个char是一样的,也就是说它还是按1字节对齐的,并不是按数组的长度来对齐的。

7、关于C语言中数组和指针复杂类型定义的解读问题

先看下以下定义,这些定义是在《C和指针》一书中的练习题,实际应用中我们可能很少会用到其中一些较复杂的定义,这里的目的是学会方法,以后遇到类似问题就迎刃而解。我在读了《C专家编程》之后,测试了自己,结果全部答对了。这本书中确实教了我这方面的技巧,要不然我会看得头晕眼花都不知道是什么意思。答案在后面,有兴趣的朋友可以看下。

a. int abc();
b. int abc[3];
c. int **abc();
d. int (*abc)();
e. int (*abc)[6];
f. int *abc();
g. int **(*abc[6])();
h. int **abc[6];
i. int *(*abc)[6];
j. int *(*abc())();
k. int (**(*abc)())();
l. int (*(*abc)())[6];
m. int *(*(*(*abc)())[6])();

答案如下(有些读起来太拗口了):

a. 返回值为int的函数;

b. int型数组;

c. 返回值为“int型指针的指针”的函数;

d. 返回值为int的函数指针;

e. 指向“int型数组”的指针;

f. 返回值为“int型指针”的函数;

g. 指向“返回值为int型指针的指针的函数”的指针的数组;

h. int型的指针的指针数组;

i. 指向“int型指针数组”的指针;

j. 返回值为“返回值为int型指针的函数指针”的函数;

k. 返回值为“返回值为int的函数指针的指针”的函数指针;

l. 返回值为“指向int型数组的指针”的函数指针;

m. 返回值为“指向‘返回值为int型指针的函数指针’的数组的指针”的函数指针;


下面来分析下如何解读这类较复杂类型的定义:

以L项为例来分析。

int (*(*abc)())[6];

1. 先看变量名abc和它的第一层括号,括号内明显表示是一个指针的定义,先不管它指向什么,移除掉*abc,剩下int (*())[6];

2. 按照优先级原则,接下来是处理上一步中最里层的圆括号,()代表的是函数,证明步骤1的指针指向的是一个函数,也就是函数指针了,同时也可以移除这个表示函数的()了,现在剩下int (*)[6];

3. 很明显,这是一个我们熟悉的类型,圆括号内有一个*号,这表明步骤2中函数指针指向的函数的返回值是一个指针,先不管这个指针指向什么,现在也移除括号和里面的内容,剩下int [6];

4. 很明显这是一个数组的定义,证明步骤3中的指针指向了一个包含6个元素的数组,去掉这个表示数组的中括号,剩下int;

5. 证明步骤4中的数组的元素类型为int;

所以从里到外一层一层的解析,到最后就很简单了,最关键第一步要确定变量的类型,这里要运用到C语言各类运算符的优先级了。






已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页