在C语言编程中,有许多高级技巧可以帮助我们更好地解决问题,提高代码的效率和可维护性。在本文中, 本人根据工作多年的开发经验总结出平时容易出错的知识点和一些常用的技巧方法, 使初学者少走弯路,避开之前走过的坑路,来提高开发的效率,总结出问题排查的一些方法。
1. 关于变量占位地址大小的问题:
---------------------------例子1-----------------------
(1)以下为Linux下的32位C程序,请计算sizeof的值。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Func (char str2[100], int size)
{
printf (" sizeof (str2):%d.\n", sizeof(str2));
}
int main (int argc, char **argv)
{
char str1[] = "hello";
char *p = str1;
int n = 10;
void *p2 = malloc(100);
char str2[100] = {0};
printf ("\n sizeof(str)=%d. \n", sizeof(str1));
printf (" sizeof(p)=%d.\n", sizeof(p));
printf (" sizeof(n)=%d.\n", sizeof(n));
printf (" sizeof(p2)=%d.\n", sizeof(p2));
Func (str2, 100);
return 0;
}
32位机器输出结果如下:
1) sizeof(str1)= 6 ##字符串5个字符+一个结束符'\0' = 6个字节;
2) sizeof(p)= 4 ##指针变量,大小固定为4个字节;
3) sizeof(n)= 4 ##int类型, 大小固定为4个字节;
4) sizeof(p2)= 4 ##p2也是指针类型,大小为4个字节,指向分配的100个字节首地址;
5) sizeof(str2)= 4 ##str2为形参,也是指针类型,大小固定为4个字节;
64位机器输出结果如下:
1) sizeof(str1)= 6 ##字符串5个字符+一个结束符'\0' = 6个字节;
2) sizeof(p)= 8 ##指针变量,大小固定为8个字节;
3) sizeof(n)= 4 ##int类型, 大小固定为4个字节;
4) sizeof(str2)= 8 ##p2也是指针类型,大小为4个字节
5) sizeof(p2)= 8 ##p2也是指针类型,大小为4个字节
---------------------------例子2-----------------------
(2).Linux下的64位C程序打印值是多少?
typedef union
{
long i;
int k[5];
char c;
}Item1;
typedef struct __data{
int cat;
Item1 cow;
double dog;
} Item2;
DATE max;
printf("sizeof item1: %d, sizeof item2: %d.\n", sizeof(Item1), sizeof(Item2));
执行结果:sizeof item1: 24, sizeof item2: 40.
解析:
32位系统一般为4字节对齐,64位系统一般为8字对齐,所以在64位ubuntu系统下结果为:
--Item1是union类型,所有成员变量共用地址空间,要看最大变量所占地址,由于8字节对齐,
k[5]占20个字节,8字节对齐,所以为24个字节;
--Item2是struct 类型,所有成员地址之和,并考虑字节对齐,所以结构体大小为:
4+4(int8字节对齐) + 24(union类型) + 8(double类型为8个字节) = 40
2. 字节对齐问题:
1)结构体对齐规则
<1> 每个数据成员的起始位置必须是自身大小的整数倍;
<2> 结构体总大小必须是结构体成员中最大的对齐模数的整数倍;
<3> 结构体包含数组时,按单个类型对齐方式;
<4> 共用体union取成员的最大内存,但包含在结构体内时,按union内部最大类型字节数的整数倍开始存储; 以下均在32位系统默认4字节对齐的情况下测试:
-----------------------------------例子1-----------------------------------
struct item_data1
{
char a;
int b;
short c;
};
结构体大小(32位系统):12个字节
解析:char占一个字节,int占四个字节,由于int的起始地址要在4的倍数上,char后边补齐3个字节,
shor占两个字节,但是整个结构体大小要是最大的对齐模数的整数倍,即4的倍数,所以补两个字节,一共12个字节。
-----------------------------------例子2-----------------------------------
struct item_data2
{
char a;
short c;
int b;
};
结构体大小(32位系统):8个字节;
解析:char占一个字节,short占两个字节,由于short的起始地址要在2的倍数上,char后边补齐1个字节;
int占四个字节,刚好在4的倍数上,所以总共8个字节;
-----------------------------------例子3:结构体嵌套结构体-----------------------------------
结构体包含另一个结构体成员,则被包含的结构体成员要从其原始结构体内部的最大对齐模数的整数倍地址开始存储(比如struct a里含有struct b,b中有char、double 、int 元素,那么b应该从8(double)的整数倍开始存储)
struct item_data3
{
char a;
struct item_data1 b[2];
short c;
};
结构体大小(32位系统):32个字节;
解析: char占一个字节,char后边补齐3个字节;结构体item_data1占12个字节,2个成员占24个字节;
成员c 占2个字节,后边补齐2个字节,所以总大小为:4+12*2+4 = 32;
-----------------------------------例子4:结构体嵌套union-----------------------------------
结构体包含共用体成员,则该共用体成员要从其原始共用体内部成员中的最大对齐模数的整数倍地址开始存储
struct item_data4
{
char a;
union x{
char m;
int n;
} b;
short c;
};
结构体大小(32位系统):12个字节;
解析: char占一个字节,char后边补齐3个字节;共用体最大为4个字节,成员c为short类型,需要补齐2个字节; 所以总大小为:4+4+4 = 12;
-----------------------------------例子5:结构体包含0数组-----------------------------------
struct byte2
{
char a;
short c;
int b;
double d[0];
};
结构体大小(32位系统):8个字节;
解析:
长度为0的数组的作用是为了满足需要可变长度的结构体,具体用法是在一个结构体的最后,申明一个长度为0的数组,就可以使得这个结构体是可变长的。
对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量。
2) 共同体union对齐规则
共同体的内存除了取最大成员内存外,还要保证字节对齐。
union byte3
{
char a[6];
short c[5];
int b;
};
共同体byte3中最大成员就是short c[5],占10个字节,由于还要保证字节对齐,所以是10+2=12;
3) 位域字节对齐规则
a. “位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。使用位域的主要目的是压缩存储。 位域列表的形式为: 类型说明符 位域名:位域长度(单位:位 bite)
如:struct bs
{
int a:8;
int b:2;
int c:6;
} data;
其中位域a占8位,位域b占2位,位域c占6位。
b. 位域说明:
<1>. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。
<2>. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节(8位)的长度,也就是说不能超过8位二进位。
<3>. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如: int :2
c.位域对齐规则:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,从新的存储单元开始,偏移量为其类型大小的整数倍,即不压缩;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
-----------------------------------例子1----------------------------------
例题:
(1)typedef struct AA
{
unsigned int b1:5;
unsigned int b2:5;
unsigned int b3:5;
unsigned int b4:5;
unsigned int b5:5;
}AA;
sizeof(AA)= 4
【解析】参考规则 1)。
由于相邻成员类型相同,unsigned int为4个字节,b1占5位,b2加上b1的位数之和为10位,不超过4字节,因此b2接着b1继续存储;
同理b3、b4、b5的类型相同,位数之和不超过4字节,因此接着b2继续存储,总位数为25位。
由于结构体的大小是最宽类型成员的整数倍,因此25位之后的补0,直到补满4字节。
-----------------------------------例子2----------------------------------
(2)typedef struct BB
{
unsigned int b1:5;
unsigned int b2:5;
unsigned int b3:5;
unsigned int b4:5;
unsigned int b5:5;
unsigned int b6:5;
unsigned int b7:5;
}BB;
sizeof(BB)= 8
【解析】参考规则 1) 和规则 2) 。由于相邻成员类型相同,unsigned int为 4 个字节(32位),当存储到 b7 时,b7和b6之前的位数相加超过4字节,因此b7从新的存储单元开始存储。即b1~b6 存储在 第0~29位,第30、31位补0,b7从下一个4字节存储单元 开始存储5位,剩下的补0。
-----------------------------------例子3----------------------------------
(3)
struct test1
{
char a:1;
char :2;
long b:3;
char c:2;
};
sizeof(test1)= 12
【解析】
char a:1; //用一个字节去存储
char :2; //空域。因为与前面的a的类型相同,而两个位域的位宽相加仍然少于8位,所以依然用1个字节表示
long b:3; //long类型的位宽是4个字节,与前面的char类型不同,所以b与a之间偏移4个字节,它们之间自动补充3个字节
char c:2; //因为c与b又不同型,以test1中的最长的long类型的位宽进行偏移,所以虽然char只用1个字节就够了,但依然要占4个字节。
结构体总长以最长的类型位宽做为偏移量,最长的是long型,占4位,所以不同类型之间应该是4个字节的偏移,即test1应该是4字节的整数倍。 总共是12字节。
3. 指针变量的理解和定义:
1) 一个整型数
int a;
例子:
int a = 100; //定义变量a,其值为100;
2)一个指向整型数的指针
int *a;
例子:
int val = 100;
a = &val; //a为一个地址变量,其值为变量val的内存地址;内存地址的值为100;
3)一个指向指针的的指针,它指向的指针是指向一个整型数
int **a;
例子:
int val = 100;
int *p = &val;
a = &p; //a为地址变量,其值为指针变量p的地址,p的值为变量val的地址;**a = *(*a) = 100;
4)一个有10个整型数的数组
int a[10];
例子:
int a[10] = {1,2,3,4,5,6,7,8,9, 0}; //有10个int类型成员的数组;
5) 一个有10个指针的数组,该指针是指向一个整型数的。
int *a[10];
例子:
int *p1, *p2, *p3...., *p10; //定义了10个整形指针变量;a为10个指针变量的数组(相当于二维数组);
a[0] = p1;
a[1] = p2;
......
a[9] = p10;
6) 一个指向有10个整型数数组的指针
int (*a)[10];
例子:
int array1[10], array2[10], array3[10];
a = array1; //a为一个指针变量,指向有10个成员的一维数组;(一维数组指针)
7)一个返回值为指针的函数:
int *a(int );
例子:
int *a(int value){ ///a本质是一个函数,返回值为指针的函数;
int *p = (int *)malloc(4);
*p = value;
return p;
}
8) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
int (*a)(int a);
例子:
int function(int val)
{
....
}
a = function; //a为函数指针变量,类型与function函数一致;
int ret = (*a)(100); //函数运行;
9) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
int (*a[10])(int);
定义了10个指针函数;
例子:
int func1(int val)
{
....
}
int func2(int val)
{
....
}
a[0] = func1; //a为函数指针数组,可以管理多个同类型(如func1一样)的函数;
a[1] = func2;
for(int i=0; i<10; i++)
ret = (*a[i])(100); //可以使用for循环进行管理,非常方便;
4. 字符串变量相关的地址操作:
(1)分析下列程序的结果:
--------------------例子1-----------------------------------
char *s1 = "AAA";
char s2[] = "BBB";
s1[0] = 'B'; //程序崩溃;
s2[0] = 'C';
printf ("s1:%s, s2:%s.\n", s1, s2);
【解析】
(1)s1是一个指针变量,指向一个常量字符串,即“AAA”字符串在代码段,是无法进行修改的;
所以s1没有可写的内存,当进行赋值时,会出现段错误:Segmentation fault (core dumped);
(2)s2是一个数组,初始化为“BBB”,默认已经分配了4个字节的大小,所以可以进行修改操作:s2[0]='C',没问题;
--------------------例子2-----------------------------------
void getmemory(char *p)
{
p=(char *)malloc(100):
strcpy(p, “hello world”);
}
int main()
{
char*str=NULL;
getmemory(str);
printf(“str:%s/n”,str);
free(str);
return 0;
}
【解析】
1) 形参中值传递,并不会改变原值的数据。因为原址只是拷贝一份数据传进去,修改的值是拷贝的数据。
2) 指针作为参数,可以传值,但无法传地址;所以指针作为形参,传递的是变量的地址,可以看成是指针变量地址的一份拷贝,函数返回后并没有重新指向新的内存;仍然为空;进入函数后,指针重新指向了一块新分配的内存,但地址已经改变;会产生内存泄露;
3)如果采用双重指针对实现上述功能:
void getmemory(char **pptr)
{ char *p = NULL;
p=(char *)malloc(100):
strcpy(p, “hello world”);
*pptr = p; //把p对应的地址作为pptr的值传出
}
int main()
{
char*str=NULL;
getmemory(&str); 、
printf(“str:%s/n”,str);
free(str);
return 0;
}
--------------------例子3-----------------------------------
char szstr[10] = {0};
strcpy (szstr,"0123456789");
printf ("str:%s.--\n", szstr);
解析:字符串必须要有结尾符号'\0',不然无法识别出来,所以此程序可能会因为地址越界出现打印乱码;
应该定义更大一些的数组szstr[11],另外说明下,strcpy会自动把'\0'一起copy到目的地址;
--------------------例子4----------------------------------
char aa[10] = {0xFF, 0x0, 'a', 'b','c','d'};
printf ("size: %d, strlen:%d. \n", sizeof(aa), strlen(aa));
【解析】
1)sizeof 计算变量的实际大小,所以sizeof(aa) = 10;
2) strlen 原理是遇到‘\0’或0马上结束,所以上述strlen(aa) = 1;
--------------------例子5-----------------------------------
void swap_data( int *p1, *p2)
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
【解析】
运行结果程序崩溃,原因是定义的p变量为指针类型,但未有分配地址空间,所以*p = *p1,直接赋值会出错;
--------------------例子6-----------------------------------
说明以下变量对应的地址和值是多少?
#include <stdio.h>
int main()
{
int array[5] = {1, 2, 3, 4, 5};
printf("array = %p, value:%d.\n", array, *(array));
printf("array + 1 = %p, value:%d.\n", array + 1, *(array+1)));
printf("array + 2 = %p, value:%d.\n", array + 2, *(array+2)));
printf("array + 3 = %p, value:%d.\n", array + 3, *(array+3)));
printf("&array = %p\n", &array);
printf("&array + 1 = %p\n", &array + 1);
printf ("sizeof(array):%d, sizeof(&array):%d.\n", sizeof(array), sizeof(&array));
int *ptr = (int *)(&array + 1);
printf ("*(array+1)=%d, *(ptr-1)=%d \n", *(array+1), *(ptr-1));
return 0;
}
【解析】(输出结果):
array = 0x7ffe96cce190, value:1. //以为e190结尾的地址;
array+1 = 0x7ffe96cce194, value:2. //以为e190+4结尾的地址;
array+2 = 0x7ffe96cce198, value:3. //以为e190+8结尾的地址;
array+3 = 0x7ffe96cce19c, value:4. //以为e190+12结尾的地址;
&array = 0x7ffe96cce190 //以为e190结尾的地址
&array + 1 = 0x7ffe96cce1a4
//!!重点:以为e190+20结尾的地址,其中20整个数组的长度大小。
所以,&array + 1 表示:以整个数组大小为单位进行计算操作的;
sizeof(array):20, sizeof(&array):8.
//array是一个数组变量,大小为数组长度;&array是一个指针变量,大小为8个字节(64位系统)
*(array+1)=2, *(ptr-1)=5
//这里注意ptr地址是array+sizeof(array), 所以ptr-1就是指向最后一个数据地址,址为5;
5. 指针与地址转换问题:
--------------------例子1 ---------------------------
unsigned char *p1 = NULL;
unsigned long *p2 = NULL;
unsigned int address = 0x0801000;
p1 = (unsigned char *)address;
p2 = (unsigned long *)address;
printf ("p1+1: %p, p2+1:%p.\n", p1+1, p2+1);
printf ("p1+5: %p, p2+5:%p.\n", p1+5, p2+5);
请说明下打印结果的。
【结果】
p1+1: 0x801001, p2+1:0x801008. //char类型占一个字节, long类型占8个字节;
p1+5: 0x801005, p2+5:0x801028.
【解析】
定义不能类型的指针,在进行加减运算时会按指针类型的字节数进行操作。
- -------------------例子2---------------------------
int array [5] = {1,2,3,4,5};
int *ptr = (int *)(&array + 1);
printf ("%d, %d \n", *(array +1), *(ptr-1));
【结果】 2, 5
【解析】 &array + 1 表示:以整个数组大小为单位进行计算操作的;
所以ptr指向地址为:array +sizeof(array ) = array +5,那么 *(ptr-1)即为array [4]的值 ;
-------------------例子3-----------------------------
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
【结果】这个问题测试你是否懂得C语言中的整数自动转换原则。这无符号整型问题的答案是输出是 ">6"。
【解析】 是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。
因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。
这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。
---------------------例子4 -------------------------------
1)如何把地址为 0x100000的内存,赋值为0x1234;
2)如何跳转到地址为 0x100000的函数,并运行;
【解析】
1)(unsigned int*)0x100000 = 0x1234;
2)步骤如下:
---首先要将0x100000强制转换成函数指针即: (void(*)())0x100000
---然后再调用它: *( (void(*)())0x100000) ();
6. 变量自加的问题:
--------------------例子1 ---------------------------
#include <stdio.h>
int main
{
int a,b,c,d;
a=10;
b=a++; //1. a先把值赋给b,然后自加;所以:b=10,a=11;
c=++a; //2. a先自加后,再把值赋给C; 所以:a=12,c=12;
d=10*a++; //3. a先乘以10赋给d, 然后a自加;所以:a=13, d=120;
printf("a, b,c,d: %a, %d,%d,%d", b, c,d);
return 0;
}
【结果】a=13, b=10,c=12,d=120
--------------------例子2 ---------------------------
int a = 5, b = 7, c, d;c = a+++b;
分析:重点是理解运算的优先级,++ 优先级高于+,所以先进行a++运算,再进行+运算;
可看成:c = a++ + b;
【答案】这段代码持行后a = 6, b = 7, c = 12
---------------------例子3 -------------------------------
代码如下:
int b=10;
printf("1-----(b++, b++): (%d, %d)\n", b++,b++);
b=10;
printf("2-----(++b, ++b): (%d, %d)\n", ++b,++b);
b=10;
printf("3-----(b++, ++b): (%d, %d)\n", b++,++b);
b=10;
printf("4-----(++b, b++): (%d, %d)\n", ++b,b++);
【结果】
1-----(b++, b++): (11, 10)
2-----(++b, ++b): (12, 12)
3-----(b++, ++b): (11, 12)
4-----(++b, b++): (12, 10)
【解析】printf函数参数中的变量是经过压栈和出栈操作的,参数中括号内的表达式是从右向左执行的,输出结果是从左向右的:
1. 先执行右边的b++,因为b++是先赋值保存,后++,所以cpu会记住b的值(即右边打印值已经确定即为10),之后再执行左边的b++,此时 b为11(执行前面b++得到的结果),因为右值++,因此就输出为11,所以结果为11, 10;
2. 先执行右边的++b,因为++b先自加,再调用,而此时cpu并没有到调用它的时候,printf第二个格式化的值对应的数字现在还不确定, 再然后执行++b,同样先自加,再调用的左值运算,即输出12,接着再输出第二个,因为cpu没有记住对第二的调用,他会找当前b的值,当前b的值是12,所以输出12, 12;
3. 先执行右边的++b,之后b=11,同样,第二个打印参数未确定,再执行b++,因为b++是先调用,再自加,所以先输出b,此时b的值是11,再自加,b=12,此时再输出b,因此输出的结果是11,12;
4. 同理分析,右边的b++先执行,由于是右值运算,此时第二个打印值 已经确定,保存下来了,即为10, 再进行左边的++b运算,打印值为12,即得为:12, 10。