第六节 数组与指针
1.1 数组、数组元素、指针的大小
1. 如程序清单6. 1所示,程序输出什么?
程序清单6. 1 数组、数组元素、指针的大小
#include <stdio.h>
int main(int argc, char *argv[])
{
int *p ;
printf( "sizeof(p) = %d \n" , sizeof(p) ) ;
printf( "sizeof(*p) = %d \n" , sizeof(*p) ) ;
int a[100];
printf( "sizeof(a) = %d \n" , sizeof(a) ) ;
printf( "sizeof(a[100]) = %d \n" , sizeof(a[100]) ) ;
printf( "sizeof(&a) = %d \n" , sizeof(&a) ) ;
printf( "sizeof(&a[0]) = %d \n" , sizeof(&a[0]) ) ;
return 0;
}
p是指针,任何数据类型的指针都是占4字节;
*p是一个指针指向的int数据,int型数据占用4字节;
a是数组,除了sizeof(a)和&a之外,当a出现在表达式中时,编译器会将a生成一个指向a[0]的指针,而这里测量的是整个数组的大小。
答案:
sizeof(p) = 4
sizeof(*p) = 4
sizeof(a) = 400
sizeof(a[100]) = 4
sizeof(&a) = 4
sizeof(&a[0]) = 4
请按任意键继续. . .
1.2 广东省的省政府和广州市的市政府都在广州
1. 如程序清单6. 2所示,如果ptr = 0x1000 0000 ,那么剩下三个输出是多少?
程序清单6. 2 数组首地址与数组首元素地址
int iArray[3] = { 1 , 2 , 3 } ;
int *ptr = iArray ;
printf("ptr = %#x\n" , ptr ) ;
printf("iArray = %#x\n" , iArray ) ;
printf("&iArray = %#x\n" , &iArray ) ;
printf("&iArray[0] = %#x\n" , &iArray[0] ) ;
iArray是数组名,由6.1节对a的说明,可iArray知同时也是数组首元素的地址,为0x1000 0000;&iArray是数组的首地址,这是毫无疑问的。&iArray[0]是数组首元素的地址,也是0x1000 0000。也就是说数组的首地址和数组首元素的地址是相等的。因为广东省的省政府和广东省的首号城市广州市的市政府都在广州,两者的地址是相等的。如图6. 1所示。
如程序清单6. 3所示,ptr = 0x1000 0000 ,那么这三个输出会是多少?
程序清单9. 3 数组首地址加1、数组首元素地址加1
int iArray[3] = { 1 , 2 , 3 } ;
int *ptr = iArray ;
printf("&iArray+1 = %#x\n" , &iArray+1 ) ;
printf("iArray+1 = %#x\n" , iArray+1 ) ;
printf("&iArray[0]+1 = %#x\n" , &iArray[0]+1 ) ;
答案是,第一个输出:0x1000 000C;第二个、第三个输出:0x1000 0004。
&iArray是数组的首地址,那么&iArray+1是偏移一个数组单元,因为站在全国的角度报全国各省政府的天气预报,报完广东省省政府之后就为湖南省省政府;如图6. 1所示。&iArray[0]是数组首元素的地址,&iArray[0]+1是偏移一个数组元素单元,好比站在广东的角度报广东各城市的天气预报,报完广东省首号城市广州的天气预报之后就是为广东省第二号城市的天气预报。
1.3 数组作为函数参数,是传什么进去了
1. 如程序清单6. 4所示,程序输出什么?
程序清单6. 4 数组作为函数参数
void text(char cArray[])
{
printf( "sizeof(cArray) = %d \n" , sizeof(cArray) ) ;
}
int main(int argc, char* argv[])
{
char cArray[] = "aBcDeF" ;
printf( "sizeof(cArray) = %d \n" , sizeof(cArray) ) ;
text(cArray) ;
return 0;
}
这里考查两个知识点,其一,sizeof和strlen();其二text(char cArray[])形参到底是什么?
答案是7,4。看到答案我想大家就应该明白上面两个问题了吧。到底是传值还是传址一定要搞明白哦。
1.4 指针相减
1. 如程序清单6. 5程序,输出会是什么?
程序清单6. 5 指针相减
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[2] = { 3 , 6 } ;
int *p = a ;
int *q = p + 1 ;
printf( "q - p = %d \n" , q-p ) ;
printf( "(int)q - (int)p = %d \n" , (int)q-(int)p ) ;
return 0;
}
用数学方法到可以做出q-p = 1这个答案,但是(int)q - (int)p 的答案。指针,指针的强大。由于指针加1,内存地址是加sizeof(int),但是int(q)和int(p)就不再是指针,而是一个整形数据。所以(int)q - (int)p = 4 。
1.5 指针加1到底是加什么
1. 如程序清单6. 6所示,请问p1+5=__;p2+5=__;
程序清单6. 6 指针加1
#include <stdio.h>
int main(int argc, char *argv[])
{
unsigned char *p1 ;
unsigned long *p2 ;
p1 = (unsigned char *)0x801000 ;
p2 = (unsigned long *)0x810000 ;
printf( "p1+5 = %#x \n" , p1 + 5 ) ;
printf( "p2+5 = %#x \n" , p2 + 5 ) ;
return 0;
}
由于p + n = p + n * sizeof(p的数据类型) ,所以答案为:
p1+5 = 0x801005
p2+5 = 0x810014
请按任意键继续. . .
1.6 数组与指针的概念
1. 如程序清单6. 7所示,解释程序。
程序清单6. 7 数组与指针的概念
a) int a;
b) int *a;
c) int **a;
d) int a[10];
e) int *a[10];
f) int (*a)[10];
g) int (*a)(int);
h) int (*a[10])(int);
答案:
a) 一个整型数 ;
b) 一个指向整型数的指针;
c) 一个指向指针的指针,它指向的指针是指向一个整型数;
d) 一个有10个整型数的数组;
e) 一个有10个指针的数组,该指针是指向一个整型数的;
f) 一个指向有10个整型数数组的指针;
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数;
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数。
这个题目很经典,很多公司的笔试题目都会截取上面部分出来考试。特别是e和f,哪一个是数组指针,哪一个又是指针数组;g和h哪一个是函数指针,哪一个又是指针函数。
1.7 数组与指针的糅合
1. 如程序清单6. 8所示,输出会是什么?
程序清单6. 8 数组与指针的糅合应用1
int arr[] ={6,7,8,9,10};
int *ptr =arr;
*(ptr++) +=123;
printf("%d,%d",*ptr,*(++ptr));
这个题目来源于华为的一道C语言笔试题,答案是:8,8。*ptr = arr ,这里ptr取得是数组arr[]的首元素地址,*(ptr++) +=123 ,这里是ptr++,此时*(ptr)即为:6,那么*(prt++)+123=129,执行完*(ptr++)+=123之后,*(ptr) = 7。跟*(++ptr)之后为8这个值是没有半点关系,由于执行了*(++ptr),所以此刻的*(ptr)为8,所以输出为:8,8。
2. 如程序清单6. 9所示,输出会是什么?
程序清单6. 9 数组与指针的糅合应用2
int main( int argc , char *argv[] )
{
int a[5] = { 1 , 2 , 3 , 4 , 5 };
int *ptr = (int *)( &a + 1 );
printf( "%d,%d" , *(a+1) , *(ptr-1) );
}
这个题目要求对指针的理解要比较透彻。由于*(a+1)和a[1]是等效的,则*(a+1)=a[1] = 2 。&a指的是指向整个数组变量的指针,&a+1不是首地址+1,系统会认为加了一个a数组的偏移量,即偏移了一个数组的大小,因此ptr指向的是a[5],即是*(ptr+5),既然如此,那么*(ptr-1)当然就是a[4] = 5咯。所以这个题目的答案是: 2 , 5 。
其实这个题目还有一个延伸,int *ptr = (int *)( (int) a + 1 ), *ptr是多少。答案是:2 00 00 00。
假设 &a[0] = 0x1000 0000,由于存储方式是小端模式,那么a[0] = 1和a[1] = 2的存储方式如图6. 2所示。
因为a = 0x1000 0000,而(int)a将这个地址强制转化为了int型数据,((int)a + 1) = 0x1000 0001,经过(int *)((int)a + 1)成了地址,ptr = (int *)((int)a + 1),由于ptr是指向int型的指针,*ptr占4个字节,*ptr所占字节即为:0x00,0x00,0x00,0x02,那么*ptr即为0x02000000。
3. 如程序清单6. 10所示,如果ptr1为0x1000 0000,那么三个输出分别为多少?
程序清单9. 10 数组与指针的糅合应用3
int iArray[7] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 } ;
int *ptr1 = iArray ;
int *ptr2 = &iArray[5] ;
printf( " ptr2 = %#x \n" , ptr2 ) ;
printf( " ptr1 = %#x \n" , ptr1 ) ;
printf( "ptr2 - ptr1 = %d \n" , ptr2 - ptr1 ) ;
很明显iArray是整型数据类型数组,那么ptr2 = ptr1 + 5*sizeof(int) = 0x1000 0014。很多同学立马就会脱口而出ptr2 – ptr1 = 20嘛!真的是这样吗?其实答案是:5!
解释之前,我们先来看这个程序:
int iArray[7] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 } ;
char *p1 = (char *) iArray ;
char *p2 = (char *) &iArray[5] ;
printf( "p2 - p1 = %d \n" , p2 - p1 ) ;
这个程序的输出是:20。因为指针类型是char*,char是1个字节;而上面*ptr1和*ptr2是int*,所以答案是:5。
如果是:
short *p1 = (short *) iArray ;
short *p2 = (short *) &iArray[5] ;
则p2 – p1 就是为:10。
这里还有一个延伸:
int iArray[7] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 } ;
int *ptr1 = iArray ;
int *ptr2 = &iArray[5] ;
printf( " ptr2 = %#x \n" , ptr2 ) ;
printf( " ptr1 = %#x \n" , ptr1 ) ;
printf( "ptr2 - ptr1 = %d \n" , (int)ptr2 – (int)ptr1 ) ;
这样输出答案是多少呢?20!
1.8 指针数组
1. 阅读程序,输出什么?
char *cArray[3] = { "abcdef" , "123456" , "jxlgdx" } ;
printf( "sizeof(cArray[0]) = %d \n" , sizeof(cArray[0]) );
我相信有较多的人的答案是:6或者7。原因是忽略了*cArray[3]这是一个指针数组,也就是说cArray[3]数组中存放的是指针,而不是字符串常量。在C语言笔试陷阱与难点第一阶段讲过,只要是指针变量,其大小就是:4。所以这里毋庸置疑,输出应该是:4。
你要是存在怀疑,可以输出cArray[3]数组的各个元素看看是不是指针。
printf( "cArray[0] = %#x \n" , cArray[0] ) ;
printf( "cArray[1] = %#x \n" , cArray[1] ) ;
printf( "cArray[2] = %#x \n" , cArray[2] ) ;
运行程序输出为:
sizeof(cArray[0]) = 4
cArray[0] = 0x415840
cArray[1] = 0x415770
cArray[2] = 0x415768
请按任意键继续. . .
读者亦可输出指针所指向的字符串:
printf( "cArray[0] = %s \n" , cArray[0] ) ;
printf( "cArray[1] = %s \n" , cArray[1] ) ;
printf( "cArray[2] = %s \n" , cArray[2] ) ;
运行输出为:
cArray[0] = abcdef
cArray[1] = 123456
cArray[2] = jxlgdx
请按任意键继续. . .
2. 阅读下列程序,输出什么?
typedef int (init_fnc_t) (void);
extern int arch_cpu_init(void);
extern int board_early_init_f(void);
init_fnc_t *init_sequence[] = {
arch_cpu_init,
board_early_init_f,
NULL,
};
int arch_cpu_init(void)
{
printf("This is arch_cpu_init \n");
return 0;
}
int board_early_init_f(void)
{
printf("This is board_early_init_f \n");
return 0;
}
void hang (void)
{
printf("Error! \n");
while (1) ;
}
int main(int argc, char* argv[])
{
init_fnc_t **init_fnc_ptr;
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr)
{
if ( (*init_fnc_ptr)() != 0 )
{
hang ();
}
}
return 0;
}
这个题目是我在阅读u-boot-2012.10源码的时候稍作修改从中提取出来的。这个题目将指针数组、函数指针等知识点融为一体。
This is arch_cpu_init
This is board_early_init_f
请按任意键继续. . .
1.9 数组指针
1. 如程序清单6. 11所示,程序输出什么?
程序清单6. 11 数组指针
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[][4]={ 1,3,5,7,9,11,13,15,17,19,21,23};
int (*p)[4] , i=2 , j=1 ;
p=a;
printf( "%d\n", *(*(p+i)+j));
return 0;
}
答案是:19。
不能理解?好吧,如果我告诉你**(p+1) = 9, *((*p)+1) = 3!到这里能理解了吗?如果还是不能理解,ok,p是指向一个含有4个整型数据的数组,那么p如果加1,地址是不是得偏移4个整形数据的地址,而p等于数组a的首元素地址,a是二维数组,也就意味着p是双重指针了。
1.10 再论数组指针与数组首地址
1. 如程序清单6. 12所示,已知&a[0][0] = 0x22fe70,想想会是输出什么?
程序清单6. 12 数组指针与数组首地址
int main(int argc, char *argv[])
{
int a[8][8] = {1,2,3,4};
int (*ptr1)[8] = a ;
int (*ptr2)[8][8] = &a;
int *ptr3 = &a[0][0];
printf(" ptr1 = %#x\n" , ptr1);
printf(" &a[0] = %#x\n" , &a[0]);
printf(" ptr1+1 = %#x\n" , ptr1+1);
printf(" &a[0]+1 = %#x\n\n" , &a[0]+1);
printf(" ptr2 = %#x\n" , ptr2);
printf(" &a = %#x\n" , &a);
printf(" ptr2+1 = %#x\n" , ptr2+1);
printf(" &a+1 = %#x\n\n" , &a+1);
printf(" ptr3 = %#x\n" , ptr3);
printf(" &a[0][0] = %#x\n" , &a[0][0]);
printf(" ptr3+1 = %#x\n" , ptr3+1);
printf(" &a[0][0]+1 = %#x\n\n" , &a[0][0]+1);
return 0;
}
这个题目涉及到两个知识点,其一,讲烂了的数组首元素地址和数组的首地址;其二,数组指针和指针数组的区别。
先看第三个指针,int *ptr3 = &a[0][0];这个毫无疑问,是将数组a的首元素地址赋给指针ptr3,由于是int型数组,那么ptr3+1则是偏移一个int型大小,即偏移4个字节,那么ptr3这一组的输出即为:
ptr3 = 0x22fe70
&a[0][0] = 0x22fe70
ptr3+1 = 0x22fe74
&a[0][0]+1 = 0x22fe74
我们再看第二个指针,int (*ptr2)[8][8] = &a;根据之前我们讲过的,这个是取数组a的首地址,ptr2的解释就是:一个指向二维数组[8][8]的指针,那么ptr2+1则是偏移了一个二维数组[8][8]的地址,即为4*8*8=256(0x100)个字节的偏移。那么ptr2这一组的输出即为:
ptr2 = 0x22fe70
&a = 0x22fe70
ptr2+1 = 0x22ff70
&a+1 = 0x22ff70
剩下第一个指针,这个和6.9节差不多,int (*ptr1)[8] = a ;其实它是等价于int (*ptr1)[8] = &a[8] ;那么ptr1则是一个指向一维数组[8]的指针,如果我们这么理解a[8][8] = {a1[8],a2[8],…a8[8]}(当然这个理解是错误的),那么ptr1就是指向a1[8],那么当ptr1+1就是指向a2[8],也就是偏移了一个含有8个int型数据的数组,即4*8=32(0x20)个字节。那么ptr1这一组的输出即为:
ptr1 = 0x22fe70
&a[0] = 0x22fe70
ptr1+1 = 0x22fe90
&a[0]+1 = 0x22fe90
这里再一次重复讲一下数组指针和指针数组。
int (*p)[8] p是指向一个含有8个整型数据的数组的指针(数组指针)
int *p[8] p是一个含有8个指针型变量的数组(指针数组)
第七节 结构体与联合体
1.1 结构体内存对齐问题
1. 这个程序本是我写来验证结构体内存对齐问题,但是我在linux系统和windows系统下的答案让我有点意外,我便将其加进本书。如程序清单7. 1所示,程序输出会是什么?
程序清单7. 1 结构体的内存对齐问题
#include<stdio.h>
struct Date{
int year ;
int month ;
int day ;
} ;
struct DateType{
int year ;
int month ;
int day ;
}birthday ;
struct student{
int iNum ;
char cName[30] ;
float fScore ;
char cSex ;
double menber ;
} people ;
int main( int argc , char *argv[] )
{
printf( "sizeof(struct Date) = %d \n\n",
sizeof( struct Date) ) ;
printf( "sizeof(struct DateType) = %d \n" ,
sizeof( struct DateType) ) ;
printf( "sizeof(birthday) = %d \n\n", sizeof( birthday ) ) ;
printf( "&birthday.year = %d \n" , &birthday.year ) ;
printf( "&sizeof.month = %d \n" , &birthday.month ) ;
printf( "&sizeof.day = %d \n\n", &birthday.day ) ;
printf( "sizeof(struct student) = %d \n" ,
sizeof( struct student) ) ;
printf( "sizeof(people) = %d \n\n", sizeof( people ) ) ;
printf( "&people.iNum = %d \n" , &people.iNum ) ;
printf( "&people.cName = %d \n" , &people.cName ) ;
printf( "&people.fScore = %d \n" , &people.fScore ) ;
printf( "&people.cSex = %d \n" ,
&people.cSex ) ;
printf( "&people.menber = %d \n\n", &people.menber ) ;
printf( "sizeof(people.menber) = %d \n\n", sizeof( people.menber ) ) ;
return 0 ;
}
传统在windows下,结果大家都应该知道,我现在就直接把window下和linux下结果直接贴出来,大家看看。(如果大家对结果有质疑,大可上机试试,毕竟眼见为实。)
sizeof(struct Date) = 12
sizeof(struct DateType) = 12
sizeof(birthday) = 12
&birthday.year = 4210832
&sizeof.month = 4210836
&sizeof.day = 4210840
sizeof(struct student) = 56
sizeof(people) = 56
&people.iNum = 4210848
&people.cName = 4210852
&people.fScore = 4210884
&people.cSex = 4210888
&people.menber = 4210896
sizeof(people.menber) = 8
请按任意键继续. . .
上面是C-Free中运行的结果,你可以试试VC等,答案依然如此。
我们再来看看linux下结果:
root@zhuzhaoqi-desktop:/home/zhuzhaoqi/C/prog1.34# ./prog
sizeof(struct Date) = 12
sizeof(struct DateType) = 12
sizeof(birthday) = 12
&birthday.year = 134520948
&sizeof.month = 134520952
&sizeof.day = 134520956
sizeof(struct student) = 52
sizeof(people) = 52
&people.iNum = 134520896
&people.cName = 134520900
&people.fScore = 134520932
&people.cSex = 134520936
&people.menber = 134520940
sizeof(people.menber) = 8
这是linux下编译的结果。
加粗标注区域够让你吃惊吧!
说实话,看到第一眼,我也傻了。为什么,我们再看下划线标注区域,people.cSex 在windows下联系上下确实应该占用8个字节,可是,可是为什么在linux下只占用4个字节!!
原来,在linux中以4个字节为开辟单元,即不足4个开辟4个,多于4个的继续开辟4个,多出的部分 放进另一个4个字节中。
struct student{
int iNum ; /* 开辟4个字节 */
char cName[30] ; /* 开辟32个字节 */
float fScore ; /* 开辟4个字节 */
/*开辟4个字节,自己用1个字节,剩下3个,不足以存储menber */
char cSex ;
double menber ; /* 所以这里重新开辟4+4个字节 */
} people ;
所以我们得出的答案是:4+32+4+4+8=52。
但是,我们一直使用的windows下,以最大单元为开辟单位,即系统先检查结构中最大单位 为double 8个字节,所以以8个字节为单位。
student 在Linux和windows下内存开辟如图7. 1和图7. 2所示。
1.1 结构体在STM32的应用
1. 如程序清单7. 2所示程序是截取STM32固件库中的一段代码,请问输出是什么?
程序清单7. 2 结构体在STM32的应用
#include <stdio.h>
typedef volatile unsigned int vui32;
typedef struct {
vui32 CRL;
vui32 CRH;
vui32 IDR;
vui32 ODR;
vui32 BSRR;
vui32 BRR;
vui32 LCKR;
} GPIO_TypeDef;
#define GPIOA (GPIO_TypeDef *)0x10000000
#define GPIOLED (GPIO_TypeDef *)GPIOA
void func (GPIO_TypeDef *GPIO)
{
printf("GPIO->CRL = %#x\n" , &(GPIO->CRL));
printf("GPIO->CRH = %#x\n" , &(GPIO->CRH));
printf("GPIO->LCKR = %#x\n" , &(GPIO->LCKR));
}
int main(int argc, char *argv[])
{
printf("sizeof(GPIO_TypeDef) = %d\n" , sizeof(GPIO_TypeDef)) ;
printf("GPIOLED=%#x\n" , GPIOLED);
func(GPIOLED);
return 0;
}
如果使用过STM32的固件函数库的话,应该对这个结构体不会陌生,STM32固件函数库就是这样,通过“大行其肆”的结构体和指针实现对一大堆寄存器的配置,在_map.h这个头文件中,定义很多这样的结构体。这样做有什么好处呢,将共同点给抽象出来了,上面7个寄存器就是每个GPIO口寄存器的共有特性,那么只要给定某一个GPIO口的映射地址,很快就可以通过这个结构体得到每个寄存器的地址。能这么做很巧的是ARM的MCU每个寄存器的偏移量都是4个字节或者2个字节,所以能使用结构体完成,如果有一天出现了3个字节的偏移量,我想此时结构体也就没辙了。
答案是:
sizeof(GPIO_TypeDef) = 28
GPIOLED =0x10000000
GPIO->CRL = 0x10000000
GPIO->CRH = 0x10000004
GPIO->LCKR = 0x10000018
请按任意键继续. . .
确实很巧妙,方便!
1.2 结构体与指针
1. 已知如下所示条件。
struct student{
long int num ;
char *name ;
short int date ;
char sex ;
short int da[5] ;
}*p;
p = (student*)0x1000000 ;
那么请问,以下输出什么?
printf( "sizeof(*p) = %d\n" , sizeof(*p) ) ;
printf( "sizeof(student) = %d\n" , sizeof(student) ) ;
printf( "p = %#x\n" , p ) ;
printf( "p + 0x200 = %#x\n" , p + 0x200 ) ;
printf( "(char*)p + 0x200 = %#x\n" , (char*)p + 0x200 ) ;
printf( "(int*)p + 0x200 = %#x\n" , (int*)p + 0x200 ) ;
第一个输出不解释,内存对齐问题,结构体指针,答案为:24。
第二个输出答案为:24。
第三个输出,为已知,答案为:0x1000000。
第四个输出,由于p此时是结构体类型指针,那么
p+0x200 = p + 0x200*sizeof(student)。
所以 p + 0x200 = p + 0x200 * 24 = 0x1000000 + 0x3000 = 0x1003000。
第五个输出,由于p被强制转换成了字符型指针,那么p + 0x200 = 0x1000200。
第六个输出同理为:p + 0x200 = 0x1000800。
1.3 联合体的存储
1. 如程序清单7. 3所示,程序输出什么?
程序清单7. 3 联合体的存储
union {
int i ;
struct {
char L;
char H;
}Bity;
}N;
int main(int argc, char* argv[])
{
N.i = 0x1234;
printf("N.Bity.L = %#x\n",N.Bity.L);
printf("N.Bity.H = %#x\n",N.Bity.H);
return 0;
}
结构体的成员是共用一块内存,也就是说N.i和N.Bity是在同一个地址空间中。那么好办了,但是要注意,CPU是小段存储模式,所以低字节存储在低地址中,高字节存储在高地址中。那么N.Bity.L是取了低地址,也就是得到低字节,即为0x34,N.Bity.H是取了高字节,即为0x12。在电脑中,int是占4字节,所以存储方式如图10. 3所示。
其实这里有一个很巧妙的用法可以用于C51单片机中,为了与上面不重复,假设C51的存储模式是大端模式。在C51的定时器,给定时器赋初值的时候,要将高八位和低八位分别赋给模式1定时器的高位预置值和低位预置值,有这么个式子:
THx = (65536-10000)/256;
TLx = (65536-10000)%256.
那么我们就可以让这样写这个程序
union {
unsigned int i ;
struct {
unsigned char H;
unsigned char L;
}Bity;
}N;
int main(int argc, char* argv[])
{
……
N.i = 65536 - 10000;
THx = N.Bity.H ;
TLx = N.Bity.L ;
……
return 0;
}
这样很方便并且高效地将高低位置好,其实单片机是一个很好学习C语言的载体,麻雀虽小,但是五脏俱全。
65536 – 10000 = 55536 = 0xD8F0。
由于在单片机中,int是占2字节,那么存储方式如图7. 4所示。
1.1 结构体在联合体中的声明
1. 如程序清单7. 4所示,请问:printf( "%d\n" , sizeof(T.N) ) ;
printf( "%d\n" , sizeof(T) ) ;输出什么?
程序清单7. 4 结构体在联合体中的声明
union T {
int i ;
struct N {
int j ;
float k ;
double m ;
};
};
第一个结构体嘛,4+4+8=16;第二个嘛,最大元素所占内存开辟,则为:16!真的是这样吗?正确答案是:16,4!
union T {
int i ;
struct N {
int j ;
float k ;
double m ;
}A;
};
如果这个程序是这样,输出又是多少呢?正确答案是:16,16!不知道你是否知道其中的原因了呢?
1.2 结构体与联合体的内存
1. 如程序清单7. 5所示,语句printf("%d",sizeof(struct date)+sizeof(max));的执行结果是?
程序清单7. 5 结构体与联合的内存
typedef union {
long i;
int k[5];
char c;
} DATE;
struct data {
int cat;
DATE cow;
double dog;
}too;
DATE max;
很明显,这里考查的是联合体和结构体的内存对齐问题。联合体的内存大小是以最大类型存储内存作为依据,而结构体则是内存对齐相加。
上面例子的联合体最大是:20,所以sizeof(max) = 20 ;在结构体中,sizeof(cat) = 4 ,sizeof(cow) = 20 ,sizeof(dog) = 8 ,由于内存都是对齐的,所以siezof(struct date) = 32 .所以最终答案是:52.
#include <stdio.h>
typedef union {
long int i ;
int k[5] ;
char c ;
}DATE ;
struct data {
int cat ;
DATE cow ;
double dog ;
}too ;
int main(int argc, char *argv[])
{
DATE max;
printf("sizeof(cat)=%d\n", sizeof(too.cat));
printf("sizeof(cow)=%d\n", sizeof(too.cow));
printf("sizeof(dog)=%d\n\n", sizeof(too.dog));
printf("sizeof(struct data)=%d\n",sizeof(struct data));
printf("sizeof(max)=%d\n", sizeof(max));
printf("sizeof(struct data)+ sizeof(max)=%d\n",
sizeof(struct data)+ sizeof(max));
return 0;
}
运行结果是:
sizeof(cat)=4
sizeof(cow)=20
sizeof(dog)=8
sizeof(struct data)=32
sizeof(max)=20
sizeof(struct data)+ sizeof(max)=52
请按任意键继续. . .
内存对齐问题是一个比较难以理解的内存问题,因为摸不着、难以猜透。
1.3 再论联合体与结构体
1. 如程序清单7. 6所示,程序输出什么?
程序清单7. 6 再论联合体与结构体
union {
struct {
unsigned char c1:3;
unsigned char c2:3;
unsigned char c3:2;
}s;
unsigned char c;
}u;
int main(int argc, char *argv[])
{
u.c = 100;
printf("u.s.c1 = %d\n", u.s.c1);
printf("u.s.c2 = %d\n", u.s.c2);
printf("u.s.c3 = %d\n", u.s.c3);
return 0;
}
这个程序考查对结构体和联合体的理解。
首先我们应该知道,u.c和u.s是在同一个地址空间中,那么u.s中存储的数据即为100。100转化为二进制为:0110 0100。由于是小端模式存储方式,那么u.s.c1取最低三位100,即为十进制的4;u.s.c2取中间三位100,即为十进制的4;而u.s.c3取最高两位01,即为十进制的1。所以输出即为:4,4,1。
第八节 内存分配与内存释放
1.1 malloc
1. 某32 位系统下, C++程序,请计算 sizeof 的值 。
char str[] = “http://www.ibegroup.com/” ;
char *p = str ;
int n = 10;
请计算
sizeof (str ) = ?(1)
sizeof ( p ) = ?(2)
sizeof ( n ) = ?(3)
void Foo ( char str[100]){
}
请计算
sizeof( str ) = ?(4)
void *p = malloc( 100 );
请计算
sizeof ( p ) = ?(5)
答案是:
(1).25 (2).4 (3).4 (4).4 (5).4
不管是int *还是char *指针,指针长度都是4.有了这点sizeof(p) = 4应该就没有任何问题了。sizeof(n) = 4 , 因为整型长度为4。剩下sizeof(str)了,我们把char str[100]变下形你可能就知道了,其实char str[100]和*(str+100)是等效的,也就是说传进去的是指针,而不是数组,那么sizeof(str) = 4就应该可以理解了。
2. 如程序清单8. 1所示,请问运行Test函数会有什么样的结果?
程序清单8. 1 malloc()的应用1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
我敢说很多人看到这里会冒出来的答案是: hello world 。实际上答案是:NULL 。是不是傻眼了。程序意图很简单,想通过GetMenory这个函数改变str的值。事实上,GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入实参的值,执行完 char *str = NULL; GetMemory( str );这2条程序后的str仍然为NULL 。
3. 如程序清单8. 2所示,请问运行Test函数会有什么样的结果?
程序清单8. 2 malloc()的应用2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这个是hello world 了吧 !这个还真不是输出hello world 。有同学就要问了,str = GetMemory(),而Getmemory()函数返回的是p , 而p[] = "hello world " ,怎么可能不是hello world ! 实际上,p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。所以输出什么我也不知道,很可能是乱码。所以要理解变量的生存周期,否则就死翘翘了。
4. 如程序清单8. 3所示,请问运行Test函数会有什么样的结果?
程序清单8. 3 malloc()的应用3
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
有些人到这里不敢吭声了,这个会是输出什么?答案是:hello。这个题目我不分析,结合上面2题请读者自己分析下。
5. 如程序清单8. 4所示,请问这个会是输出什么?
程序清单8. 4 malloc()的应用4
#include <stdio.h>
char *str()
{
char *p = "abcdef";
return p;
}
int main(int argc, char *argv[])
{
printf("%s", str());
return 0;
}
乍眼一看,在哪里见过?是的,确实似曾相识。有记忆了吧,会不会有人立马说出答案:输出乱码?
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
上面这个题目的答案确实是:乱码!
但是char *p = "abcdef";和char p[] = “abcdef”是有区别的。char *p = "abcdef"这个程序的正确答案是输出:abcdef。
为什么?是的,有人会说,你不是说数组是可以退化成指针吗?但是,你要知道,数组是存储在栈中,p[] = “abcdef”实在执行这条语句时,abcdef才会赋给p[],并且栈在执行完成之后是会自动销毁;但是指针是保存在堆中,当开辟了*p这个地址的时候,abcdef就已经存储在p中,然而堆只有在程序结束之后才会销毁,所以是可以输出abcdef这个字符串。
1.2 malloc(0)
1. 把这个独立开来是因为很少这样使用,但是又会使用。如程序清单8. 5所示,程序会输出什么?
程序清单8. 5 malloc(0)
int main(int argc, char *argv[])
{
char *ptr = NULL;
if ((ptr = (char *)malloc(0)) == NULL)
{
printf("Null pointer\n");
printf("ptr = %#x\n", ptr);
}
else
{
printf("Valid pointer\n");
printf("ptr = %#x\n", ptr);
}
return 0;
}
我想很多人的第一个感知是输出:Null pointer!
但是很遗憾,是输出Valid pointer!虽然ptr所开辟的内存空间为0,但是ptr是不会等于NULL的。
第九节 笔试中的几个常考题
1.1 strcpy
1. 已知strcpy函数的原型是char *strcpy(char *strDest, const char *strSrc); 其中strDest是目的字符串,strSrc是源字符串。不调用C++/C的字符串库函数,请编写函数 strcpy。
char *strcpy(char *strDest, const char *strSrc)
{
assert((strDest!=NULL) && (strSrc !=NULL));
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ )
NULL ;
return address ;
}
这个题目创维和华为都曾用来做为考题。
在程序开头我们肯定要断言strDest和strSrc不等于NULL,assert()的作用是:断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true。如果表达式计算为 false,那么系统会报告一个 Assertionerror。
我们注意到返回值是char *类型!这里是为了实现链式表达式。
将这个题目再引申下,已知strncpy的函数原型是char *strncpy( char *to, const char *from, size_t count ); 其中to是目的字符串,from是源字符串。不调用C++/C的字符串库函数,请编写函数 strncpy。
提示:将字符串from 中至多count个字符复制到字符串to中。如果字符串from 的长度小于count,其余部分用‘\0‘填补。返回处理完成的字符串。
char *strncpy(char *to, const char *from , size_t count )
{
assert((to!=NULL) && (from !=NULL));
char *address = to;
while( count != 0 )
{
*address++ = *from++ ;
if ( *from == ‘\0‘)
{
*address++ = ‘\0‘ ;
}
count-- ;
}
return address ;
}
1.2 CPU的使用率
1. 写一个程序,让你的电脑CPU使用率一直运行在50%。
#include <iostream>
#include <stdlib.h>
using namespace std;
/* 让CPU的使用率在50% */
int main(int argc, char *argv[])
{
for( ; ; )
{
for( int i = 0 ; i < 800000000 ; i++ ) ;
_sleep(10) ;
}
return 0;
}
这里的800000000是根据我自己电脑算出来的,因为我的电脑主频是2.0GHz。留一个问题给读者,怎样让自己的电脑CPU以正弦曲线运行?
注意:对于新代处理器由于优化,可能做不到。
1.3 二进制数据中1的个数
1. 写一个程序,随意输入x,输出x的二进制数据中1的个数。
这个程序的算法很多,可以一位一位右移进行测试,也有其他办法。右移法我就不再累赘,这个方法简单,但是时间复杂度会比较大。看看下面这个方法:
int number( unsigned int x )
{
unsigned int countx = 0;
while (x) {
countx ++ ;
x = x&(x-1) ;
}
return countx ;
}
如果x大于0,那么x一定有一位为1,所以进入while之后countx先加1。如果x=100,那么经过x=x&(x-1),x为0,countx为1,此时结束程序。
1.4 二进制高位到低位实现逆变
1. 编写一个函数,实现将一个32位int型数据的二进制高位到低位实现逆变,例如:1101 0101变成1010 1011。
这个题目的解决方法很多,代表性的有两种。
int func(unsigned int uiData , int length)
{
unsigned int uiValue = 0 ;
int i = 0 ;
for ( i = 0 ; i < length ; i++ )
{
uiValue = (uiValue << 1) + (uiData & 0x01) ;
uiData = uiData >> 1 ;
}
return uiValue ;
}
这个方法比较常见,通过移位的方法将高位到低位实现逆序。但是这个方法存在唯一的不足之处是效率低,要进行32次移位和运算。
int func (unsigned int uiData)
{
unsigned int uiValue = 0 ;
/* 分而治之的思想 */
/* 高16位和低16互换 */
uiValue = ((uiData >> 16)&0x0000ffff) |
((uiData << 16)&0xffff0000);
/*高低16位中的高低8位互换*/
uiValue = ((uiValue >> 8)&0x00ff00ff) |
((uiValue << 8)&0xff00ff00);
/*8位中的高低4位互换*/
uiValue = ((uiValue >> 4)&0x0f0f0f0f) |
((uiValue << 4)&0xf0f0f0f0);
/*4位中的高低2位互换*/
uiValue = ((uiValue >> 2)&0x33333333) |
((uiValue << 2)&0xcccccccc);
/*2位中的高低位互换*/
uiValue = ((uiValue >> 1)&0x55555555) |
((uiValue << 1)&0xaaaaaaaa);
return uiValue ;
}
这个程序只需要位操作5次,就能实现高位到低位的逆序。我们逐句程序分析一下。假设uiData = 1100 0101 0101 1100 1100 0101 0101 1111。执行完成下面这句程序,
/* 高16位和低16互换 */
uiValue = ((uiData >> 16)&0x0000ffff) | ((uiData << 16)&0xffff0000);
得到1100 0101 0101 1111 1100 0101 0101 1100,也就是高16位和低16位互换。
执行完成:
/*高低16位中的高低8位互换*/
uiValue = ((uiValue >> 8)&0x00ff00ff) | ((uiValue << 8)&0xff00ff00);
得到0101 1111 1100 0101 0101 1100 1100 0101,也就是高低16位中高8位和低8位互换。
执行完成:
/*8位中的高低4位互换*/
uiValue = ((uiValue >> 4)&0x0f0f0f0f) | ((uiValue << 4)&0xf0f0f0f0);
得到1111 0101 0101 1100 1100 0101 0101 1100,也就是从高位起,每8位段的高4位和低4位完成互换。
执行完成:
/*4位中的高低2位互换*/
uiValue = ((uiValue >> 2)&0x33333333) | ((uiValue << 2)&0xcccccccc);
得到1111 0101 0101 0011 0011 0101 0101 0011,也就是从高位起,每4位段的高2位和低2位完成互换。
执行完成:
/*2位中的高低位互换*/
uiValue = ((uiValue >> 1)&0x55555555) | ((uiValue << 1)&0xaaaaaaaa);
得到1111 1010 1010 0011 0011 1010 1010 0011。也就是从高位起,每2位段的高1位和低1位完成互换。和原始数据1100 0101 0101 1100 1100 0101 0101 1111进行对比,逆序。
1.5 大小端测试
1. 编写一个函数,测试MCU是大端模式存储还是小端模式存储。
/****************************************************************
** 函数名称:LBEndian
** 函数功能:大小端测试函数
** 入口参数:None
** 出口参数:1 or 0
****************************************************************/
int LBEndian (void)
{
unsigned int uiNumber = 0x12345678 ;
unsigned char *ucptr = (void *)0 ;
/* 将最低位1一个字节赋给ucptr */
ucptr = (unsigned char *)(&uiNumber) ;
/* 如果是小段模式,则返回1*/
if ( 0x78 == (*ucptr) )
{
return 1 ;
}
/* 如果是大端模式,则返回0 */
else
{
return 0 ;
}
}
ucptr = (void *)0 ,这样做是为了防止野指针的危患。通过ucptr = (unsigned char *)(&uiNumber) (好好理解这句程序);截取低地址的存储字节数据,如果低地址存储的是低字节,那么就是小端模式,如果低字节存储的是高字节,那么就是大端模式。
1.6 二分查找
1. 写一个函数实现二分查找
int BinarySeach(int *iArray, int key, int n)
{
int iLow = 0 ;
int iHigh = n - 1;
int iMid;
while (iLow <= iHigh)
{
iMid = (iHigh + iLow)/2;
if (iArray[iMid] > key)
{
iHigh = iMid - 1 ;
}
else if (iArray[iMid] < key)
{
iLow = iMid + 1 ;
}
else
{
return iMid ;
}
}
}
数据结构中的各种查找算法在笔试中是无处不在,在工程应用中也是“无孔不入”。所以作为一个软件工程师,必须牢牢掌握各种查找算法。
1.7 int (*p[10])(int)
1. int (*p[10])(int) 表示的是什么?
函数指针数组,int(*p)(int),我们知道这是一个函数指针,一个指向int Fun(int)函数的指针,那么int (*p[10])(int)即为函数指针数组。
1.8 对绝对地址赋值的问题
涉及到内存的问题,都让很多人望而却步,因为内存确实是地雷阵,稍不小心就会引爆。
1. 要对绝对地址0x10 0000赋值,我们该怎么做?
*(unsigned int *)0x10 0000 = 1234 ;
通过这个程序我们把常量1234存储在地址为0x10 0000。
2. 如果想让程序跳转到绝对地址为0x10 0000去执行,应该怎么做?
*( (void (*)( ))0x100000 ) ( );
首先要将0x10 0000转换成函数指针:
(void (*)( ))0x100000
然后再调用他:
*( (void (*)( ))0x100000 ) () ;
因为内存是摸不着,猜不透的,所以很像地雷阵,随时都可能挂掉。
第十节 数据结构之冒泡排序、选择排序
我相信很多人曾经写冒泡排序和选择排序都是一个算法一个代码,并且还一个一个
写得不亦乐乎。zzq宁静致远今天就告诉你如何写出一手漂亮的C语言代码,当你看完
今天的帖子,你就会恍然顿悟曾经自己写的代码如此不堪。
1. 冒泡排序
1.1 底层冒泡排序的头文件
为了增强代码的可移植性,健壮性。我们将冒泡排序的算法封装在库中,我们只需要调用库函数即可。冒泡排序头文件程序如程序清单1. 1所示。
程序清单1. 1 冒泡排序头文件
/*
* 声明比较函数,升序还是降序
*/
typedef int (*COMPAREFUN)(const void *pvData1, const void *pvData2);
/*******************************************************************************
**函数名称: bubbleSort
**函数功能: 冒泡排序
**入口参数: *pvData: 需要进行排序的数组
stAmount: 数组中包含的元素个数
stSize: 元素内存大小
CompareFun: 需要排序的数据类型、升序还是降序
**出口参数:
*******************************************************************************/
extern void bubbleSort (void *pvData, size_t stAmount, size_t stSize , COMPAREFUN CompareFun);
为了各种数据的兼容性,所有不确定情况的数据类型都使用void *。
1.2 底层数据交换函数实现
通过函数指针类型数据,向swap函数传入需要交换的两个数据的地址和数组元素内存大小,实现数据的交换。swap函数代码如程序清单1. 2所示。
程序清单1. 2 swap函数
/*******************************************************************************
**函数名称: __swap
**函数功能: 数据交换
**入口参数: *pvData1: 需要交换的数据一
*pvData2: 需要交换的数据二
stDataSize:元素内存大小
**出口参数:
*******************************************************************************/
static void __swap (void *pvData1, void *pvData2, size_t stDataSize)
{
unsigned int *p1=(unsigned int)pvData1;
unsigned int *p2=(unsigned int)pvData2;
unsigned int uiTemp;
while ( stDataSize >= sizeof(unsigned int) ) //一次交换sizeof(unsigned int)个字节
{
(*p1) ^=(*p2);
(*p2) ^=(*p1);
(*p1++)^=(*p2++);
stDataSize -= sizeof(unsigned int);
}
if (stDataSize>0)
{
/*
* void *memmove( void *to, const void *from, size_t count );
* 函数从from中复制count 个字符到to中,并返回to指针。
*/
memmove( &uiTemp,p1,stDataSize);
memmove( p1,p2,stDataSize);
memmove( p2,&uiTemp,stDataSize);
}
}
这里传进去的是三个参数分别是:pvData1,为需要交换的两个数据中的第一个数据的地址;pvData2,为需要交换的两个数据中的第二个数据的地址;stDataSize:数组中元素内存的大小。
传进去之后,先将两个数据的地址强制转化为(int*)型地址。数据的交换分成两个部分:如果元素内存大小大于一个sizeof(unsigned int)个字节大小,则一次性交换4位;当stDataSize大于0且小于一个sizeof(unsigned int)个字节大小时,再通过memmove交换剩下的部分。
1.3 冒泡排序算法实现
冒泡排序的基本思想是通过相邻元素之间的比较和交换使得排序码较小的元素上移或下移。冒泡排序代码如程序清单1. 3所示。
程序清单1. 3 冒泡排序
/*******************************************************************************
**函数名称: bubbleSort
**函数功能: 冒泡排序
**入口参数: *pvData: 需要进行排序的数组
stAmount: 数组中包含的元素个数
stSize: 元素内存大小
CompareFun: 需要排序的数据类型、升序还是降序
**出口参数:
*******************************************************************************/
void bubbleSort (void *pvData, size_t stAmount, size_t stSize , COMPAREFUN CompareFun)
{
int i, j;
int iNoSwapFlg = 0;
void *pvThis = NULL;
void *pvNext = NULL;
/*
* 冒泡排序
*/
i = stAmount - 1;
do {
iNoSwapFlg = 0;
pvThis = pvData;
pvNext = (char *)pvData + stSize;
j = i;
do {
if (CompareFun(pvThis, pvNext) > 0) {
__swap(pvThis, pvNext, stSize);
iNoSwapFlg = 1;
}
pvThis = pvNext;
pvNext = (char *)pvNext + stSize;
} while (--j != 0);
if (iNoSwapFlg == 0) {
break;
}
} while ( --i != 0);
}
bubbleSort函数传入的有四个参数:pvData:需要进行排序的数组的首元素地址,但是这个地址也就是需要进行排序的数组的地址。这个区别就好像是广东的省政府在广州,而广东省首号城市广州市的市政府也在广州,虽然地址相同,但是意义不同。为了证明这个观点,我定义了两个数组进行测试。
static int iArray[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};
static char *strArray[] ={"forARM","mzdzkj","c language","shenzhen","china"};
printf("&iArray = %#x \n" , &iArray ) ;
printf("&iArray[0] = %#x \n" , &iArray[0] ) ;
printf("strArray = %#x \n" , strArray ) ;
printf("&strArray = %#x \n" , &strArray ) ;
编译之后运行的结果为:
&iArray = 0x402000
&iArray[0] = 0x402000
strArray = 0x402024
&strArray = 0x402024
所以在这个函数中,无论传入的是数组的首元素地址,还是数组的地址,都是可以的,因为有这么一句程序:
pvNext = (char *)pvData + stSize;
所以无论如何,pvNext都是下一元素的地址。
测试程序:
printf("(char*)&iArray[0] + sizeof(iArray[0]) = %#x \n" , (char*)&iArray[0] + sizeof(iArray[0]) ) ;
printf("&iArray[1] = %#x \n\n" , &iArray[1] ) ;
printf("(char*)&strArray[0] + sizeof(strArray[0]) = %#x \n" , (char*)&strArray[0] + sizeof(strArray[0]) ) ;
printf("&strArray[1] = %#x \n" , &strArray[1] ) ;
结果:
(char*)&iArray[0] + sizeof(iArray[0]) = 0x402004
&iArray[1] = 0x402004
(char*)&strArray[0] + sizeof(strArray[0]) = 0x402028
&strArray[1] = 0x402028
stAmount:数组中包含的元素个数,我们通常使用:sizeof(strArray) / sizeof(strArray[0],即为数组总长度除以元素内存大小,这个结果就是数组元素的个数。
stSize:元素内存大小,sizeof(strArray[0],因为数组内每一个元素的类型相同,所以每个元素的内存大小也就相同。
CompareFun:需要排序的数据类型、升序还是降序。这个函数的原型是:
typedef int (*COMPAREFUN)(const void *pvData1, const void *pvData2);
如果是整型数组需要升序排列,则函数为如程序清单1. 4所示:
程序清单1. 4 整型数组升序
/*******************************************************************************
**函数名称: int_Rise_cmp
**函数功能: 对int进行升序排列
**入口参数: *x:
*y:
**出口参数: ( *(int *)x - *(int *)y )
确定升序
*******************************************************************************/
int int_Rise_cmp(void *x , void *y)
{
return ( *(int *)x - *(int *)y );
}
我们就综合上述对其进行一个整体的分析。假设需排序的数组为:static int iArray[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};pvData是需排序数组的首元素地址,由:
pvThis = pvData;
pvNext = (char *)pvData + stSize;
那么pvThis即为数组首元素的地址,也就是&iArray[0],pvNext为下一个元素的地址,也就是&iArray[1]。接着通过CompareFun(pvThis, pvNext)比较两个元素的大小,进入CompareFun,也就是int_Rise_cmp函数,x即为pvThis,y即为pvNext。这样x即为数组首元素的地址,这里还是void*,我们通过强制转化,将x指向整型,即为(int*)x,再去地址,也就是( *(int *)x,数组首元素,y以此类推。如果( *(int *)x - *(int *)y ) >0,也就是CompareFun(pvThis, pvNext)>0,则交换这两个数据,从而达到从小到大排列的目的。交换完成之后,
pvThis = pvNext;
pvNext = (char *)pvNext + stSize;
这样以此类推。
static int iArray[] = {39, 33, 18, 64, 73, 30, 49, 51, 81};
static char *strArray[] ={"forARM","mzdzkj","c language","shenzhen","china"};
第二个数组值得一提,这是一个指针数组,即为数组中存储的是指针变量。不相信的话可以测试一下。
printf("strArray[0] = %#x \n\n" , strArray[0] ) ;
结果是:
strArray[0] = 0x403000
很显然是指针。上述两个数组经过排序之后的测试结果如程序清单1. 5所示。
程序清单1. 5 测试结果
*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
整型数组数据排序之前:
39 33 18 64 73 30 49 51 81
字符串数组排序之前:
‘forARM‘ ‘mzdzkj‘ ‘c language‘ ‘shenzhen‘ ‘china‘
整型数组数据升序之后:
18 30 33 39 49 51 64 73 81
整型数组数据降序之后:
81 73 64 51 49 39 33 30 18
字符串数组数据升序之后:
‘c language‘ ‘china‘ ‘forARM‘ ‘mzdzkj‘ ‘shenzhen‘
字符串数组数据降序之后:
‘shenzhen‘ ‘mzdzkj‘ ‘forARM‘ ‘china‘ ‘c language‘
2.选择排序
2.1 选择排序算法
一个好的迭代器,只需要修改排序算法,其他的程序都无需修改。其实这里只需要把冒泡排序算法修改为选择排序算法即可。
选择排序算法程序如程序清单2. 1所示。
程序清单2. 1 选择排序函数
/*******************************************************************************
**函数名称: selectSort
**函数功能: 选择排序
**入口参数: *pvData: 需要进行排序的数组
stAmount: 数组中包含的元素个数
stSize: 元素内存大小
CompareFun: 需要排序的数据类型、升序还是降序
**出口参数:
*******************************************************************************/
void selectSort (void *pvData , size_t stAmount, size_t stSize , COMPAREFUN CompareFun)
{
int i , j , k ;
void *pvThis = NULL;
void *pvThat = NULL;
/*
* 冒泡排序
*/
#if 0
printf("pvData = %#x\n" ,pvData ) ;
printf("pvThis = %#x\n" ,pvBegin ) ;
printf("pvThat = %#x\n" ,pvEnd ) ;
#endif
for ( i = 0 ; i < stAmount ; i++ ) {
k = i ;
for ( j = i + 1 ; j < stAmount ; j++) {
pvThis = (char *)pvData + j*stSize;
pvThat = (char *)pvData + k*stSize;
if (CompareFun(pvThis , pvThat ) > 0) {
k = j ;
}
if( k != i ) {
pvThis = (char *)pvData + i*stSize;
pvThat = (char *)pvData + k*stSize;
__swap(pvThis , pvThat , stSize);
}
}
}
}
其实这个选择排序函数和冒泡排序函数只是改动了内部程序,其他地方都没有修改。道理是一样,我就不加说明。
触类旁通的思想真的很重要,当你庖丁解牛对待一个冒泡排序的时候,你会发现其他排序方法也就自然而然会了。
我们看看测试结果:
*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
先测试一些数据,便于我们理解
第一组数据:
sizeof(iArray) = 36
sizeof(iArray[0]) = 4
&iArray = 0x402000
&iArray[0] = 0x402000
(char*)&iArray[0] + sizeof(iArray[0]) = 0x402004
&iArray[1] = 0x402004
&iArray[8] = 0x402020
第二组数据:
sizeof(strArray) = 20
sizeof(strArray[0]) = 4
strArray = 0x402024
&strArray = 0x402024
&strArray[0] = 0x402024
(char*)&strArray[0] + sizeof(strArray[0]) = 0x402028
&strArray[1] = 0x402028
strArray[0] = 0x403000
*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
整型数组数据排序之前:
39 33 18 64 73 30 49 51 81
字符串数组排序之前:
‘forARM‘ ‘mzdzkj‘ ‘c language‘ ‘shenzhen‘ ‘china‘
整型数组数据升序之后:
18 30 33 39 49 51 64 73 81
整型数组数据降序之后:
81 73 64 51 49 39 33 30 18
字符串数组数据升序之后:
‘c language‘ ‘china‘ ‘forARM‘ ‘mzdzkj‘ ‘shenzhen‘
字符串数组数据降序之后:
‘shenzhen‘ ‘mzdzkj‘ ‘forARM‘ ‘china‘ ‘c language‘
请按任意键继续. . .
测试通过!
第十一节 机试题之数据编码
某部队为了防止消息泄密从而对原始数据进行编码,编码规则如下。
1) 所有信息都为ASCII 编码;
2) 在收到原始密文后将字符进行二进制逆转,如字符‘A‘(0x41,0100 0001B)将数据逆转后为(0x82,1000 0010B);
3) 将逆转后的数据按照16 进制打印输出(原始数据允许空格),如字符串"ABCD EFGH"加密后的输出结果为:"8242C22208A262E212"。
为了加快编码解码速度现在需要你编写一个程序实现该密文的编码。
这个题目说到底就是将一个字符转化成二进制,再将这个二进制的高低位逆转,之后输出逆变后对应数据的ASCII。
二进制高低位逆转在12.4有详细讲解,为了算法不重复,这里采用逐位逆转方法进行解答。
// text.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <string.h>
/****************************************************************
** 函数名称: Printb
** 函数功能: 输入一个数据,将其二进制反位输出
** 入口参数: uiValue:待转换的值
** 出口参数: None
** 返回值 : uiSum: 转换之后的值
****************************************************************/
unsigned int Printb( unsigned int uiValue)
{
unsigned int uiSum = 0 ;
/* 将数据的二进制逆位 */
for ( int i = 0 ; i < 8 ; i++ )
{
uiSum = ( uiSum << 1 ) + ( uiValue & 0x01 ) ;
uiValue = ( uiValue >> 1 ) ;
}
return uiSum ;
}
/****************************************************************
** 函数名称: main
** 函数功能: 主函数
** 入口参数: argc,* argv[]
** 出口参数: none
** 返回值 : 0
****************************************************************/
int main(int argc, char* argv[])
{
char s[100] ;
unsigned int iArray[100] ;
scanf( "%s" , s ) ;
/* 将字符串转化为整型数据 */
for ( int i = 0 ; i < strlen(s) ; i++ )
{
iArray = s ;
}
/* 以十六进制输出字符串数据 */
for ( int j = 0 ; j < strlen(s) ; j++ )
{
printf( "%x" , iArray[j] ) ;
}
printf( "\n" ) ;
/* 以十六进制输出译码后的字符串数据 */
for ( int k = 0 ; k < strlen(s) ; k++ )
{
printf( "%x" , Printb( iArray[k] ) );
}
printf("\n") ;
return 0;
}
编译结果:
ABCDEFGH
4142434445464748
8242c222a262e212
请按任意键继续. . .
第十二节 机试题目之十进制1~N的所有整数中出现“1”的个数
给定一个十进制数N,写下从1开始到N的所有整数,然后数一下其中出现的所有“1”的个数,比如:
1) N = 2 ,写下1、2,这样只出现1个“1”;
2) N= 12 ,我们会写下1、2、3、4、5、6、7、8、9、10、11、12,这样1的个数为5。
问题是:写一个函数f(N),返回1~N之间出现“1”的个数,比如:f(12) = 5。
这个题目带有几分找规律性质。本题解法可能较多,这里提供两种。
方法一:
// text.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
/****************************************************************
** Function name: oneNumber
** Descriptions: 计算1的个数
** input parameters: uliNumber:输入的数据,即为要计算1的个数的数据
** output parameters: void
** Returned value: uliCount:1的个数
****************************************************************/
unsigned long int oneNumber ( unsigned long int uliNumber )
{
/* 将uliNumber传进来的值赋给uliTally */
unsigned long int uliTally = uliNumber ;
/* 记录1的个数 */
unsigned long int uliCount = 0 ;
/* 提取位的权值 */
unsigned long int uliFlag = 1 ;
/* 提取位 */
unsigned int uiFlag = 0 ;
/* 提取位的幂次方 */
unsigned int uiLog = 0 ;
/*
*对数据逐位取提取位
*/
while ( ( uliTally / uliFlag ) != 0 )
{
/* 从左开始计算,依次取出uiFlag*10^n */
uiFlag = ( uliTally / uliFlag ) % 10 ;
/* 如果是 1 * 10^n ,则按1*10^n公式进行计算 */
if ( 1 == uiFlag )
{
uliCount += uiLog * ((unsigned long int)( uliFlag / 10)) + 1 ;
/* 加上10^n后面数据 */
uliCount += ( uliNumber % uliFlag ) ;
}
/*
*如果是 uiFlag * 10^n ,则按uiFlag*10^n公式进行计算
*/
else
{
/*( uliFlag ) * ( 1&&uiFlag )是为了uliFlag是0,则是加0 */
uliCount += ( uliFlag ) * ( 1&&uiFlag ) +
uiFlag * uiLog * ( (unsigned long int)( uliFlag / 10) ) ;
}
/* 依次向左取 */
uliFlag *= 10 ;
/* uiLog = log10(uliFlag) */
uiLog += 1 ;
}
/* 返回1的个数 */
return uliCount ;
}
/****************************************************************
** Function name: main
** Descriptions: 输入输出
** input parameters: argc , argv[]
** output parameters: void
** Returned value: 0
****************************************************************/
int main(int argc, char* argv[])
{
unsigned long int iNumber = 0 ;
unsigned long int uliCountNumber = 0 ;
printf("请输入iNumber: ") ;
scanf("%ld" , &iNumber) ;
uliCountNumber = oneNumber(iNumber) ;
printf( "1~%d中1的个数为: %d\n" , iNumber , uliCountNumber ) ;
return 0;
}
函数头文件这里使用的是英语,但是格式是不变的。
编译结果:
请输入iNumber: 100
1~100中1的个数为: 21
请按任意键继续. . .
方法二:
// text.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
/****************************************************************
** Function name: oneNumber
** Descriptions: 计算1的个数
** input parameters: uliNumber:输入的数据,即为要计算1的个数的数据
** output parameters: void
** Returned value: uliCount:1的个数
****************************************************************/
unsigned long int oneNumber ( unsigned long int uliNumber )
{
/*
** 记录1的个数
*/
unsigned long int uliCount = 0 ;
/*
** 提取位左边的数
*/
unsigned long int uliLeft = 0 ;
/*
** 提取位右边的数
*/
unsigned long int uliRight = 0 ;
/*
** 提取位,此位对1进行计数
*/
unsigned long int uliFlag = 0 ;
/*
** 提取位的权值
*/
unsigned int uiFlag = 1 ;
/*
** 对数据逐位取提取位
*/
while ( (uliNumber / uiFlag)!=0 )
{
/*
** 提取位右边的数
*/
uliRight = uliNumber % uiFlag ;
/*
** 提取位
*/
uliFlag = ( uliNumber / uiFlag ) % 10 ;
/*
** 提取位左边的数
*/
uliLeft = ( uliNumber / uiFlag ) / 10 ;
/*
** 判断提取位,分成0、1和大于等于2这三种情况
*/
switch ( uliFlag )
{
/*
** 如果提取位为0,那么1的个数等于提取位左边数乘以取提取位的数据
*/
case 0 :
{
uliCount += uliLeft * uiFlag ;
} break ;
/*
** 如果提取位为1,那么1的个数等于提取位左边数乘以取提取位的数据
** 再加上(0到提取位右边数)+1
*/
case 1 :
{
uliCount += uliLeft * uiFlag + uliRight + 1 ;
} break;
/*
** 如果提取位大于1,那么1的个数等于(提取位左边数+1)乘以
** 取提取位的数据
*/
default :
{
uliCount += ( uliLeft + 1 ) * uiFlag ;
} break;
}
/*
** 提取位向左移动一位
*/
uiFlag *= 10 ;
}
/*
** 返回1的个数
*/
return uliCount ;
}
/****************************************************************
** Function name: main
** Descriptions: 输入输出
** input parameters: argc , argv[]
** output parameters: void
** Returned value: 0
****************************************************************/
int main(int argc, char* argv[])
{
unsigned long int iNumber = 0 ;
unsigned long int uliCountNumber = 0 ;
printf("请输入iNumber: ") ;
scanf("%ld" , &iNumber) ;
uliCountNumber = oneNumber(iNumber) ;
printf( "1~%d中1的个数为: %d\n" , iNumber , uliCountNumber ) ;
return 0;
}
第十三节 机试题之 遍历单链表一次,找出链表中间元素
单链表最大的特点就是“不走回头路”,不能实现随机存取。如果我们想要找一个数组a的中间元素,直接a[len/2]就可以了,但是链表不行,因为只有a[len/2 - 1] 知道a[len/2],其节点不知道。因此,如果按照数组的做法依样画葫芦,要找到链表的中点,我们需要做两步(1)知道链表有多长(2)从头结点开始顺序遍历到链表长度的一半的位置。这就需要1.5n(n为链表的长度)的时间复杂度了。有没有更好的办法呢?答案是有的。想法很简单:两个人赛跑,如果A的速度是B的两倍的话,当A到终点的时候,B应该刚到中点。这只需要遍历一遍链表就行了,还不用计算链表的长度。
下面这个程序算法就是只遍历单链表一次,即能找出链表中间元素。
typedef struct _list_node {
int iData;
_list_node *next;
}ListNode;
ListNode *FindList(ListNode *head)
{
ListNode *p1, *p2;
if ( (NULL == head) || (NULL == head->next) )
{
return head;
}
p1 = head;
p2 = head;
while (1)
{
if ( (NULL != p2->next) && (NULL != p2->next->next) )
{
p2 = p2->next->next;
p1 = p1->next;
}
else
{
break;
}
}
return p1;
}
第十四节 机试题之全排序
写一个程序,对任意一串字符串进行全排序。如123的全排序为:123,132,213,231,312,321.
这个题目使用数学很简单,因为高中的排列组合一个式子就把这个题目给KO了,C语言其实也很简单,无非是列举出所有排列顺序罢了。
// text.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
//读者在这里思考两个问题:
//1.这个函数的作用是什么?
//2.inline是做什么用的?
inline void Swap(char *a, char *b)
{
/* 交换a和b */
char temp = *a;
*a = *b;
*b = temp;
}
//读者想想如何列举出所有的排列顺序
void Perm(char list[], int k, int m)
{
/* 生成list [k:的所有排列方式 */
int i = 0;
/* 输出一个排列方式 */
if (k == m)
{
for (i = 0; i <= m; i++)
{
putchar(list);
}
putchar(‘\n‘);
}
else
{
/* list[k:有多个排列方式 */
/* 递归地产生这些排列方式 */
for (i = k; i <= m; i++)
{
Swap (&list[k], &list);
Perm (list, k+1, m);
Swap (&list [k], &list );
}
}
}
//测试
int main(int argc, char* argv[])
{
char s[] = "1234";
Perm(s, 0, 3);
return 0;
}
编译结果:
1234
1243
1324
1342
1432
1423
2134
2143
2314
2341
2431
2413
3214
3241
3124
3142
3412
3421
4231
4213
4321
4312
4132
4123
请按任意键继续. . .
第十五节 机试题之大整数加法运算
所接触的数据类型中,int数据类型、float数据类型等都有范围,如果超出这个范围,则无法表达,更不能进行数学中的运算。但是字符串的长度却不受限制,但是要让字符串进行数学运算也就解决了大整数的运算。
这样的话,可以联想到小学的竖式加减乘除的方法,逐位相加减。
大整数的加法算法:
/****************************************************************
** 函数名称:BigNumberAdd
** 函数功能:大整数的加法运算
** 入口参数:str1:第一个加数
str2:第二个加数
ptr:容纳两数之和的空间首地址
ptrSize:此空间大小
** 出口参数:
****************************************************************/
int BigNumberAdd(const char *str1, const char *str2,
char *ptr, int ptrSize)
{
/*
** iStr1Len:存储第一个字符串
** iStr2Len:存储第二个字符串
** iMaxLen : 两个字符串中最长的长度
** i、j : 循环
** iCarry : 进位标志位
*/
int iStr1Len , iStr2Len , iMaxLen , i , j , iCarry = 0 ;
char character1 , character2 ;
/* 测量两个字符串长度¨¨ */
iStr1Len = strlen(str1) ;
iStr2Len = strlen(str2) ;
/* 将ptr存储区域的数据全部清零 */
memset(ptr, 0, ptrSize) ;
/* 得到两个加数中最大的长度¨¨ */
iMaxLen = iStr1Len > iStr2Len ? iStr1Len : iStr2Len ;
/* 从低位向高位逐位相加 */
for ( i = 0 ; i < iMaxLen ; i++ ) {
character1 = \
(iStr1Len - 1 - i) < 0 ? ‘0‘ : str1[iStr1Len - 1 - i] ;
character2 = \
(iStr2Len - 1 - i) < 0 ? ‘0‘ : str2[iStr2Len - 1 - i] ;
/* 如果character1和character2不是数字,则退出 */
if ( (!isdigit(character1)) || (!isdigit(character2)) ) {
return 0 ;
}
/* 模仿竖式逐位相加 */
iCarry += (character1 - ‘0‘) + (character2 - ‘0‘) ;
assert(i < ptrSize) ;
/* 保存当前位数据 */
ptr = iCarry % 10 + ‘0‘ ;
/* 保存进位数据 */
iCarry /= 10 ;
}
/* 如果最高位出现进位,则增加一位 */
if (0 != iCarry) {
assert(i < ptrSize) ;
ptr[i++] = iCarry + ‘0‘ ;
}
assert(i < ptrSize) ;
ptr = ‘\0‘ ;
/* 将数字逆序输出 */
for ( j = 0 ; j < --i ; j++) {
char cTemp = ptr[j] ;
ptr[j] = ptr ;
ptr = cTemp ;
}
return 1 ;
}
大整数的减法、乘法、除法运算都可依据小学的竖式运算方法。测试结果:
数据一:987654321123456789
数据二:123456789987654321
两数相加之和:1111111111111111110
两数相加之差:864197531135802468
两数相加之积:120408474453741807546258192212924859
请按任意键继续. . .
或许很多学生在大学所学习的C语言只是一个初概念,没有深入理解。今年5月份回校办理毕业手续,给几位学弟交流了下,C语言学了2年还是一团雾水。但是C语言是嵌入式的灵魂(我是这么理解的),如果C语言没有学好,很难写出一个精美的程序。
或许很多学生知道:
int iArray[10];
int i;
iArray[i]是什么,但是我问他i[iArray]是什么?就不知道了。为什么,因为对数组和指针理解不够深入。
iArray[i] = *(iArray+i) = *(i+iArray) = i[iArray],说白了这里就是小学的加法交换律。但是就是因为不理解指针。
马上就要进行14届应届毕业生的招聘会了,笔者就尽自己所能,每天为《攻破C语言笔试与机试难点》写一点吧。
if (iNumber == 10) {
i ++;
}
这个语句没问题,当iNumber与10相等,i则++。
但是,问题的关键在于你是不是每次都会记得是:
iNumber == 10,你会不会有那么一次写成了:
iNumber = 10呢?该死的编译器这个时候不会告诉你这是一个错误。
所以有那么一次,程序是不是就铁定挂了。
为了避免不必要的麻烦:
if (10 == iNumber) {
i ++;
}
这样写,招聘人员一定会眼前一亮。