原文网址:http://bbs.elecfans.com/jishu_354666_1_1.html
再过1个月又是一年应届毕业生应聘的高峰期了,为了方便应届毕业生应聘,笔者将大学四年C语言知识及去年本人C语言笔试难点进行梳理,希望能对今年应届毕业生的应聘有所帮助。
2013年10月18日更新--> 攻破C语言这个帖子更新到这里,我不仅仅是为了补充大学学生遗漏的知识,我更重要的是希望通过我的经验,你们实际项目中的C语言写得漂亮,写出属于你的风格。“朱兆祺STM32手记”(http://bbs.elecfans.com/jishu_385613_1_1.html)一帖中我就多次强调过编程的模块化,要考虑到可移植性和别人的可阅读性。我在某公司实习的时候,拿到一个程序,我当时就想吐槽,我想除了这个程序的当时写作者能够看懂之外,其他人有谁还能看懂,程序构架、可移植性性就更是一塌糊涂。结果我全部推到重写。因此明志电子科技工作室从承接第一个项目做开始,我便和搭档说,我们必须制定我们工作室的编程规范和编程风格,这样就算给谁,换成谁,拿到项目都能马上接下去做。
朱兆祺在这个帖子将会不断更新,明志电子工作室的项目经验也将在电子发烧友论坛不断贴出,我希望能用我们仅有的力量,将我们的经验毫不保留传授给大家。我只希望在未来某一天,有那么几个人会竖着大拇指说:明志电子科技工作室的经验受益匪浅就够了。我相信,在深圳,明志电子科技工作室的影响力会日益增长,因为我们已经规划好了未来脚步。
C语言是一门技术,但更是一门艺术。写一个C语言代码不难,写一个高水平、漂亮的代码很难。朱兆祺将在此帖不断为大家攻破C语言。
<--朱兆祺于2013年10月18日
-->朱兆祺更新于2014年2月21日:
深圳市馒头科技有限公司于2014年2月注册成立,当初命名为馒头科技,不管是吴坚鸿所说亲切还是谢贤斌所说草根。馒头科技永远以用户满意为我们的追求,馒头科技不追求锦上添花,但是我们很愿意为每一位客户雪中送炭。因为锦上添花每一个人都会做,但是雪中送炭却不是每一个人愿意去做。
馒头科技往后将为每一位读者送上更为精美的学习资料。敬请期待。
第一节 C语言编程中的几个基本概念
http://bbs.elecfans.com/jishu_354666_1_1.html
第二节 数据存储与变量
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088253&fromuid=222350
第三节 数学算法解决C语言问题
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088283&fromuid=222350
第四节 关键字、运算符与语句
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088352&fromuid=222350
第五节 C语言中的细节
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088375&fromuid=222350
第六节 数组与指针
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088417&fromuid=222350
第七节 结构体与联合体
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088582&fromuid=222350
第八节 内存分配与内存释放
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088596&fromuid=222350
第九节 笔试中的几个常考题
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2088606&fromuid=222350
第十节 数据结构之冒泡排序、选择排序
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2092632&fromuid=222350
第十一节 机试题之数据编码
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2096393&fromuid=222350
第十二节 机试题之十进制1~N的所有整数中出现“1”的个数
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2097513&fromuid=222350
第十三节 机试题之 遍历单链表一次,找出链表中间元素
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2103563&fromuid=222350
第十四节 机试题之全排序
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2105648&fromuid=222350
第十五节 机试题之大整数运算
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2109737&fromuid=222350
第十六节 机试题之大整数减法与乘法
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2115675&fromuid=222350
第十七节 算法之二分查找
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2128027&fromuid=222350
第十八节 数据结构之单向链表(颠覆你手中数据结构的三观)
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2139670&fromuid=222350
第十九节 数据结构之双向链表(接着颠覆你手中的数据结构三观)
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2156978&fromuid=222350
第二十节 数据结构之栈
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2193337&fromuid=222350
C语言技术公开课第一讲——编译环境给C语言带来的困扰,网络课程讲义
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2230571&fromuid=222350
第二十一节 通过加减法高效的求出两个无符号整数的商和余数
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2252461&fromuid=222350
第二十二节 表达式计算器(1)
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2280837&fromuid=222350
第二十三节 表达式计算器(2)
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2325485&fromuid=222350
第二十四节 表达式计算器(3)
http://bbs.elecfans.com/forum.ph ... 4547&fromuid=222350
第二十五节 表达式计算器(4)
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2395966&fromuid=222350
C语言技术公开课第三讲 const问题
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2434706&fromuid=222350
第二十六节 序列差最小
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2466868&fromuid=222350
第二十七节 sizeof与strlen的深入
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2676569&fromuid=222350
第二十八节 C与C++中的const
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2678981&fromuid=222350
第二十九节 do、while
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2681907&fromuid=222350
第三十节 变量的生命周期
http://bbs.elecfans.com/forum.php?mod=redirect&goto=findpost&ptid=354666&pid=2690469&fromuid=222350
第一节 C语言编程中的几个基本概念
1.1 #include< >与#include" "
1. #include< >和#include" "有什么区别?
这个题目考查大家的基础能力,#include< >用来包含开发环境提供的库,
#include" "用来包含.c/.cpp文件所在目录下的头文件。注意:有些开发环境可以在当前目录下面自动收索(包含子目录),有些开发环境需要指定明确的文件路径名。
1.2 switch()
1. switch(c) 语句中 c 可以是 int, long, char, float, unsigned int 类型?
其实这个题目很基础,c应该是整型或者可以隐式转换为整型的数据,很明显不能是实型(float、double)。所以这个命题是错误的。
1.3 const
1. const有什么用途?
虽然const很常用,但是我相信有很多人仍然答不上来。
(1) 欲阻止一个变量被改变,可以使用const 关键字。在定义该 const 变量时,通常需要对它进行初 始化,因为以后就没有机会再去改变它了;
(2) 对指针来说,可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为 const;
(3) 在一个函数声明中,const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4) 对于类的成员函数,若指定其为 const 类型,则表明其是一个常函数,不能修改类的成员变量;
(5) 对于类的成员函数,有时候必须指定其返回值为 const 类型,以使得其返回值不为“左值”。
1.4 #ifndef/#define/#endif
1. 头文件中的 #ifndef/#define/#endif 干什么用?
其实#ifndef、#define、#endif这些在u-boot、linux内核文件中经常见到,在这么大型的程序中大量使用,可见它的作用不可小觑。
这些条件预编译多用于对代码的编译控制,增加代码的可裁剪性,通过宏定义可以轻松的对代码进行裁剪。
#ifndef/#define/#endif最主要的作用是防止头文件被重复定义。
1.5 全局变量和局部变量
1. 全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
全局变量储存在静态数据库,局部变量在堆栈。 其实,由于计算机没有通用数据寄存器,则函数的参数、局部变量和返回值只能保存在堆栈中。提示:局部变量太大可能导致栈溢出,所以建议把较大数组放在main函数外,防止产生栈溢出。
思考:如程序清单1. 1所示。会出现怎样的情况?
程序清单1. 1 大数组放在main函数中导致堆栈溢出
int main(int argc, char *argv[])
{
int iArray[1024 * 1024];
return 0;
}
第二节 数据存储与变量
2.1 变量的声明与定义
1. 如程序清单2. 1所示会不会报错?为什么?如果不会报错,又是输出什么结果?
程序清单2. 1 变量的声明与定义
#include<stdio.h>
static int a ;
static int b[] ;
int main( int argc , char *argv[] )
{
printf( "%d %d \n" , a , b[0] ) ;
return 0 ;
}
static int a = 8 ;
static int b[4] ;
这个程序是不会报错的,并且连警告都不会出现。输出的结果是:8 0
static int a ,这句程序是声明全局变量a;static int b[],这句程序是声明全局数组变量b,并且是不完全声明,也就是可以省略数组下标。static int a = 8,这里才是定义全局变量a,static int b[4],这里是定义全局变量b。
2.2 局部变量与全局变量的较量
1. 请问如程序清单2. 2所示输出什么?
程序清单2. 2 局部变量与全局变量
#include<stdio.h>
static int a = 8 ;
int main( int argc , char *argv[] )
{
int a = 4 ;
printf( "%d \n" , a ) ;
return 0 ;
}
C语言规定,局部变量在自己的可见范围内会“挡住”同名的全局变量,让同名的全局变量临时不可见。即在局部变量的可见范围内不能访问同名的全局变量。因此本程序输出为:4。
2.3 char、int、float、double的数据存储
1. 请问如程序清单2. 3所示,i和j输出什么?
程序清单2. 3 数据存储
float i = 3 ;
int j = *(int*)(&i) ;
printf( "i = %f \n" , i ) ;
printf( "j = %#x \n" , j ) ;
i是毋庸置疑是:3.000000。但是j呢?3.000000?答案是否定的,j是输出:0x4040 0000。有人会问了,难道j是随机输出?瞎说,j输出0x4040 0000是有依据,是一个定值!
由于i是float数据类型,而j是int数据类型。理论上说,j是取了i的地址然后再去地址,应该得到的就是i的值:3。但是问题的关键就是float数据类型的存储方式和int数据类型不一样,float是占用4个字节(32位),但是float存储是使用科学计数法存储,最高位是存储数符(负数的数符是0,正数的数符是1);接下来8位是存储阶码;剩下的23位是存储尾数。上面i=3.000000,那么3.000000(10进制) = 11(2进制) = <v:shape id=_x0000_i1027 style="WIDTH: 40.5pt; HEIGHT: 21.75pt" equationxml=‘ 121.1 脳<21‘ type="#_x0000_t75"> (二进制)。数据在电脑中存储都是二进制,这个应该都没有疑问。那么这里的数符为:0 ,阶码为:E – 127 = 1 ,那么阶码为:E = 128 即为:1000 0000 (2进制) ,尾数为:100 0000 0000 0000 0000 0000 。那么存储形式就是:0100 0000 0100 0000 0000 0000 0000 0000。这个数据转换成16进制就是0x4040 0000。
图2. 1 数据存储方式 |
char、int、float、double的存储方式如图2. 1所示。
提问:如果i = -3.5 的话,请问j输出多少?
i = -3.500000
j = 0xc0600000
这个希望读者自行分析。
再问:如果如程序清单2. 4所示。
程序清单2. 4 数据存储
double i = 3 ;
int j = *(int*)(&i) ;
printf( "i = %lf \n" , i ) ;
printf( "j = %#x \n" , j ) ;
这样的话,j又输出多少呢?
提示:double( 8个字节(64位) )的存储方式是:最高位存储数符,接下来11位存储阶码,剩下52位存储尾数。
是不是得不到你想要的结果?double是8个字节,int是4个字节。一定别忘记了这个。用这个方法也同时可以验证大小端模式!
2.4 容易忽略char的范围
1. 如程序清单2. 5所示,假设&b=0x12ff54,请问三个输出分别为多少?
程序清单2. 5 char的范围
unsigned int b = 0x12ff60 ;
printf("( (int)(&b)+1 ) = %#x \n" , ( (int)(&b)+1 ) ) ;
printf("*( (int*)( (int)(&b)+1 ) ) = %#x \n" , *( (int*)( (int)(&b)+1 ) ) ) ;
printf("*( (char*)( (int)(&b)+1 ) ) = %#x \n" , *( (char*)( (int)(&b)+1 ) ) ) ;
很显然,&b是取无符号整型b变量的地址,那么(int)(&b)是强制转换为整型变量,那么加1即为0x12ff54+1=0x12ff55。所以( (int)(&b)+1 )是0x12ff55。
图2. 3 指针加1取字符型数据 |
由于( (int)(&b)+1 )是整型数据类型,通过(int *)( (int)(&b)+1 )转化为了整型指针类型,说明要占4个字节,即为:0x12ff55、0x12ff56、0x12ff57、0x12ff58,再去地址*( (int *)( (int) (&b)+1 ) )得到存储在这4个字节中的数据。但是很遗憾,0x12ff58我们并不知道存储的是什么,所以我们只能写出0x**0012ff。**表示存储在0x12ff58中的数据。如图2. 2所示。
图2. 2 指针加1取整型数据 |
以此类推,*( (char *)( (int) (&b)+1 ) ) = 0xff。如图2. 3所示。
但是,*( (char *)( (int) (&b)+1 ) )输出的却是:0xff ff ff ff !
问题出现了,为什么*( (char *)( (int) (&b)+1 ) )不是0xff,而是0xff ff ff ff?char型数据应该占用1个字节,为什么会输出0xff ff ff ff?
使用%d输出,
printf("*( (char*)( (int)(&b)+1 ) ) = %d \n" , *( (char*)( (int)(&b)+1 ) ) ) ;
结果为-1???
问题出在signed char 的范围是:-128~127,这样肯定无法储存0xff,出现溢出。所以将
printf("*( (char*)( (int)(&b)+1 ) ) = %#x \n" , *( (char*)( (int)(&b)+1 ) ) ) ;
改成
printf("*( (unsigned char*)( (int)(&b)+1 ) ) = %#x \n" ,
*( (unsigned char*)( (int)(&b)+1 ) ) ) ;
就可以输出0xff,因为unsigned char 的范围是:0~255(0xff)。
第三节 数学算法解决C语言问题
3.1 N!结果中0的个数
1. 99!结果中后面有多少个0?
谁跟你说过高数没用?数学是C语言的支撑,没有数学建模的支撑就没有那么经典的C语言算法!
如果你能一个一个乘出来我算你狠!我也佩服你!
0也就意味着乘积是10的倍数,有10的地方就有5.有5就不担心没2.10以内能被5整除的只有5,但是能被2整除多的去了。所以就简化成了这个数只要有一个因子5就一定对应一个0.
所以可以得出下面结论:
当0 < n < 5时,f(n!) = 0;
当n >= 5时,f(n!) = k + f(k!), 其中 k = n / 5(取整)。
如程序清单3. 1所示。
程序清单3. 1 求N!中0的个数
#include<stdio.h>
int fun(int iValue)
{
int iSum = 0;
while(iValue / 5 != 0)
{
iSum += (iValue / 5 );
iValue /= 5;
}
return iSum;
}
int main( int argc , char *argv[] )
{
int iNumber, iZoreNumber;
scanf( "%d", &iNumber);
iZoreNumber = fun(iNumber);
printf( "%d\n", iZoreNumber);
return 0;
}
所以99!后面有多少个0的答案是:99 / 5 = 19 , 19 / 5 = 3 ; 3 / 5 = 0 .也就是:19 + 3 + 0 = 22.
这里延伸为N!呢,一样的,万变不离其宗!
3.2 N!结果中的位数
1. 请问N!结果中有几位数?
数学!还是数学,数学真的博大精深,如果大学没有好好学数学,会成为你一辈子的遗憾。
我们先假设:N! = 10 ^A,我们知道 10^1~10^2(不含10^2)之间是2位数,10^2~10^3(不含10^3)之间是3位数,以此类推,10^A~10^(A+1)(不含10^(A+1))则是(A+1)位数,那就是说,我们只要求出A,即可知道N!有几位数。A=log10(N!),
N!= 1*2*3……*N,那么A= log10(1)+log10(2)+……+log10(N).
这样好计算了吧!程序如程序清单6. 2所示。
程序清单6. 2 求N!结果中的位数
#include <stdio.h>
#include <math.h>
int main(int argc, char *argv[])
{
int iNumber , i = 0 ;
double sum = 0 ;
printf("请输入iNumber :");
scanf( "%d" , &iNumber );
for( i = 1 ; i < ( iNumber + 1 ) ; i++) {
sum += log10(i) ;
}
printf(" N!有%d位 \n" , (int)sum + 1 ) ;
return 0;
}
我们看下调试结果:
请输入iNumber :10
N!有7位
请按任意键继续. . .
网友可以自行验证。
第四节 关键字、运算符与语句 1.1 static 1. 如程序清单4. 1所示,请问输出i、j的结果? 程序清单4. 1 static #include <stdio.h> static int j ; void fun1(void) { static int i = 0 ; i++ ; printf("i = %d " , i ); } void fun2(void) { j = 0 ; j++ ; printf("j = %d \n" , j ); } int main(int argc, char *argv[]) { int k = 0 ; for( k = 0 ; k < 10 ; k++ ){ fun1() ; fun2() ; printf("\n"); } return 0; } 答案: i = 1 j = 1 i = 2 j = 1 i = 3 j = 1 i = 4 j = 1 i = 5 j = 1 i = 6 j = 1 i = 7 j = 1 i = 8 j = 1 i = 9 j = 1 i = 10 j = 1 请按任意键继续. . . 很多人傻了,为什么呢?是啊,为什么呢?! 由于被static修饰的变量总存在内存静态区,所以运行这个函数结束,这个静态变量的值也不会被销毁,函数下次使用的时候仍然能使用这个值。 有人就问啊,为什么j一直是1啊。因为每次调用fun2()这个函数,j都被强行置0了。 static的作用: (1) 函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次, 因此其值在下次调用时仍维持上次的值; (2) 在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问; (3) 在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明 它的模块内; (4) 在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝; (5) 在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的static 成员变量。 1.2 for循环的深究 1. 如程序清单4. 2所示,输出结果是什么? 程序清单4. 2 for循环 #include <stdio.h> int main(int argc, char *argv[]) { int i = 0 ; for( i = 0 ,printf("First = %d " , i ) ; printf("Second = %d " , i ) , i < 10 ; i++ , printf("Third = %d " , i )) { printf("Fourth = %d \n" , i) ; } return 0; } 这个题目主要考察对for循环的理解。我们先来看看运行程序会输出什么? First = 0 Second = 0 Fourth = 0 Third = 1 Second = 1 Fourth = 1 Third = 2 Second = 2 Fourth = 2 Third = 3 Second = 3 Fourth = 3 Third = 4 Second = 4 Fourth = 4 Third = 5 Second = 5 Fourth = 5 Third = 6 Second = 6 Fourth = 6 Third = 7 Second = 7 Fourth = 7 Third = 8 Second = 8 Fourth = 8 Third = 9 Second = 9 Fourth = 9 Third = 10 Second = 10 请按任意键继续. . . 从输出我们就可以知道程序到底是什么运行: 首先i = 0 , 所以输出:First = 0 ; 接着输出:Second = 0 ; i < 10 成立,则输出:Fourth = 0 。就此完成第一个循环。接着 i ++ , 此时i = 1 ,输出:Third = 1 ;接着输出:Second = 1 ;i < 10 成立,则输出:Fourth = 1 ······以此类推。 1.3 尺子——sizeof 1. 如程序清单4. 3所示,sizeof(a),sizeof(b)分别是多少? 程序清单4. 3 sizeof #include <stdio.h> int main(int argc, char *argv[]) { char a[2][3] ; short b[2][3] ; printf( "sizeof(a) = %d \n" , sizeof( a ) ) ; printf( "sizeof(b) = %d \n" , sizeof( b ) ) ; return 0; } 这个题目比较简单,由于char 是1个字节、short是2个字节,所以本题答案是: sizeof(a) = 6 sizeof(b) = 12 请按任意键继续. . . 好的,再来看看如程序清单4. 4所示,sizeof(a),sizeof(b)分别是多少? 程序清单4. 4 sizeof #include <stdio.h> int main(int argc, char *argv[]) { char *a[2][3] ; short *b[2][3] ; printf( "sizeof(a) = %d \n" , sizeof( a ) ) ; printf( "sizeof(b) = %d \n" , sizeof( b ) ) ; return 0; } 是数组指针呢,还是指针数组呢?这里涉及*和[]和优先级的问题。我告诉大家的是这两个数组存放的都是指针变量,至于为什么,在后续章节会详细解释,然而指针变量所占的字节数为4字节,所以答案: sizeof(a) = 24 sizeof(b) = 24 请按任意键继续. . . 1.4 ++i和i++ 1. 或许大家都知道,++i是先执行i自加再赋值,但是i++是先赋值再自加,但是还有隐藏在后面的东西呢? int i = 0 ; int iNumber = 0 ; iNumber = (++i) + (++i) + (++i) ; C-Free编译输出是:7,有的编译器输出是:9。这两个答案都是对的,编译器不同所不同。7 = 2+2+3;9=3+3+3。区别在于答案是7的先执行(++i)+(++i)再执行+(++i),但是答案是9的是一起执行。 这只是前奏,先看几个让你目瞪口呆的例子。编译环境是VS2010。 int i = 0 ; int iNumber = 0 ; iNumber = (i++) + (++i) + (++i) ; printf( "iNumber = %d \n" , iNumber ) ; 这里输出是:4!!!4 = 1+1+2。 int i = 0 ; int iNumber = 0 ; iNumber = (++i) + (i++) + (++i) ; printf( "iNumber = %d \n" , iNumber ) ; 这里输出是:4!!!4=1+1+2。 int i = 0 ; int iNumber = 0 ; iNumber = (++i) + (++i) + (i++) ; printf( "iNumber = %d \n" , iNumber ) ; 这里输出是:6!!!6=2+2+2。 这里至少能说明两个问题,其一,先执行前面两个,再执行第三个;其二,(i++)这个i的自加是最后执行! int i = 0 ; int iNumber = 0 ; iNumber = (++i) + (i++) + (++i) + (++i) + (i++) ; printf( "iNumber = %d \n" , iNumber ) ; 这个会是多少?!答案是:10!!!10=1+1+2+3+3! 不同的编译器或许会存在不同的答案,希望读者自行进行验证。 1.5 scanf()函数的输入 1. 如程序清单4. 5所示,运行程序,当显示Enter Dividend: , 输入的是a,按下Enter之后程序会怎么运行? 程序清单4. 5 scanf()函数的输入 #include<stdio.h> int main(void) { float fDividend,fDivisor,fResult; printf("Enter Dividend:"); scanf("%f",&fDividend); printf("Enter Divisor:"); scanf("%f",&fDivisor); fResult=fDividend/fDivisor; printf("Result is: %f\n",fResult); return 0; } 这个问题有人会说,肯定是显示Enter Divisor:要我输入除数咯。是这样吗? 答案是:如果你在Enter Dividend:之后输入非数字,按下Enter之后显示的不是Enter Divisor: 要你输入除数,而是程序到此就运行结束,显示一个不确定答案,这个答案每一次都会变。如果你在Enter Divisor:之后输入非数字,按下Enter之后显示的不是Reslut的结果, 而是程序到此就运行结束,显示一个不确定答案,这个答案每一次都会变。 由于scanf()使用了%f,当输入数字的时候,scanf()将缓冲区中的数字读入fDividend,并清空缓冲区。由于我们输入的并非数字,因此scanf()在读入失败的同时并不会清空缓冲区。最后的的结果是,我们不需要再输入其他字符,scanf()每次都会去读取缓冲区,每次都失败,每次都不会清空缓冲区。当执行下一条scanf()函数读取除数时,由于缓冲区中有数据,因此它不等待用户输入,而是直接从缓冲区中取走数据。 那么防止输入非数字的程序应该怎样呢? #include<stdio.h> int main( int argc , char *argv[] ) { float fDividend , fDivisor , fResult ; int iRet ; char cTmp1[ 256 ] ; printf( "Enter Dividend \n" ) ; iRet = scanf( "%f" , &fDividend ) ; if ( 1 == iRet ) { printf( "Enter Divisor \n" ) ; iRet = scanf( "%f" , &fDivisor ) ; if ( 1== iRet ) { fResult = fDividend / fDivisor ; printf( "Result is %f \n" , fResult ) ; } else { printf( "Input error ,not a number! \n" ) ; gets(cTmp1) ; return 1 ; } } else { printf( "Input error , not a number! \n" ) ; gets(cTmp1) ; return 1 ; } return 0 ; } 1.6 scanf()函数的返回值 1. 如程序清单4. 6所示,请问输出会是什么? 程序清单4. 6 scanf()函数的返回值 int a , b ; printf( "%d \n" , scanf("%d%d" , &a , &b) ) ; 输出输入这个函数的返回值?!答案是:2。只要你合法输入,不管你输入什么,输出都是2。那么我们就要深入解析scanf这个函数。scanf()的返回值是成功赋值的变量数量。 1.7 const作用下的变量 1. 阅读如程序清单4. 7所示,想想会输出什么?为什么? 程序清单4. 7 const作用下的变量 const int iNumber = 10 ; printf(" iNumber = %d \n" , iNumber) ; int *ptr = (int *)(&iNumber) ; *ptr = 100 ; printf(" iNumber = %d \n" , iNumber) ; const的作用在第四章已经详细讲了,这里就不再累赘,答案是:10,10。这里补充一个知识点: const int *p 指针变量p可变,而p指向的数据元素不能变 int* const p 指针变量p不可变,而p指向的数据元素可变 const int* const p 指针变量p不可变,而p指向的数据元素亦不能变 1.8 *ptr++、*(ptr++),*++ptr、*(++ptr),++(*ptr)、(*ptr)++的纠缠不清 1. 如程序清单4. 8所示程序,输出什么? 程序清单4. 8 *ptr++ int iArray[3] = { 1 , 11 , 22} ; int *ptr = iArray ; printf( "*ptr++ = %d \n" , *ptr++ ) ; printf( "*ptr = %d \n" , *ptr ) ; 纠结啊,是先算*ptr还是ptr++;还是纠结啊,ptr是地址加1还是偏移一个数组元素! 这里考查了两个知识点,其一:*与++的优先级问题;其二,数组i++和++i的问题。*和++都是优先级为2,且都是单目运算符,自右向左结合。所以这里的*ptr++和*(ptr++)是等效的。 首先ptr是数组首元素的地址,所以ptr++是偏移一个数组元素的地址。那么ptr++运算完成之后,此时的ptr是指向iArray[1],所以第二个输出*ptr = 11 。如图4. 1所示。那么倒回来看第一个输出,ptr++是在执行完成*ptr++之后再执行的,所以,*ptr++ = 1 。
如程序清单4. 9所示程序,输出会是什么? 程序清单4. 9 *++ptr int iArray[3] = { 1 , 11 , 22} ; int *ptr = iArray ; printf( "*++ptr = %d \n" , *++ptr ) ; printf( "*ptr = %d \n" , *ptr ) ; 这个解释和上面解释差不多,就是++ptr和ptr++的差别,所以这里的两个输出都是:11。同样的道理,*++ptr和*(++ptr)是等效。 再如程序清单4. 10所示,输出又会是什么? 程序清单4. 10 (*ptr)++ int iArray[3] = { 1 , 11 , 22} ; int *ptr = iArray ; printf( "(*ptr)++ = %d \n" , (*ptr)++ ) ; printf( "*ptr = %d \n" , *ptr ) ; 这个的输出是:1,2。原因请读者分析。 |
<ignore_js_op>
-
4.jpg (94.96 KB, 下载次数: 21)图4.1 ptr++
第五节 C语言中的细节
1.1 “零值”比较
1. 写出float x 与“零值”比较的if语句。
首先要知道float是有精度的,不能直接与0相比较或者两数相减与0相比较。float能保留几位小数?答案是6位。既然如此,那么就应该这么写:
if((x > 0.000001) && (x < -0.000001)) 。
1.2 宏定义
1. 定义一个宏,返回X、Y中的较大值。
这个题目其实很简单,但是在很多笔试中都会拿出来考试,并且出错率很高,原因只有一个,忽略细节(优先级的问题,实在搞不明白就加括号吧,你好理解,别人一看也懂)。终究还是细节决定成败。
#define MAX( (X) , (Y) ) ((X) >= (Y) ? (X) : (Y))
2. 宏定义两个数相加
请问如程序清单5. 1输出什么?
程序清单5. 1 宏定义两数相加
#define DOUBLE(x) x+x
int main(int argc, char* argv[])
{
int iNumber = 0 ;
printf("%d\n" , 10*DOUBLE(10));
return 0;
}
其实这个程序非常简单,学习C语言一周就应该理解是什么意思,但是一般会出错的的地方都在细节。其实这个程序输出是110。
可能有人会问,不是10先DOUBLE嘛,然后乘以10,不是200嘛。是啊,想法是好的,我想这个程序的“原意”也应该是这样,但是就是由于优先级的问题,打破了我们的愿望。如果要得到200,那么就应该是这样宏定义:#define DOUBLE(x) ((x)+(x))。我想,无论我加多少层括号都不算违法吧。
1.3 递归运算
1. 如程序清单5. 2所示,输出什么?
程序清单5. 2 递归运算
#include <stdio.h>
int func(int a)
{
if (a==0)
{
return a;
}
printf("%d\n",func(a++/2));
return a;
}
int main(int argc, char *argv[])
{
printf("%d",func(7));
return 0;
}
答案:0,2,4,8
这里把7送进去,那么func(a++/2),先执行7/2=3,然后a++ = 8,此时返回3;接着把3送进去,func(a++/2),先执行3/2=1,然后a++ = 4,此时返回1;接着把1送进去,func(a++/2),先执行1/2=0,然后a++ = 2,此时返回0;接着把0送进去,此时直接返回0,递归结束。
递归最容易忽略的细节是,由于递归次数过多,容易导致堆栈溢出。
1.4 让人忽略的贪心法
1. 如程序清单5. 3所示,程序输出什么?
程序清单5. 3 贪心法
int k = 8 ;
int i = 10 ;
int j = 10 ;
k *= i+++j ;
printf("%d \n" , k) ;
贪心法,就是一次性能尽可能多得吃运算符,那么这里k *= i+++j ,加上括号之后就是这样:k = k * ((i++) + j) ;这样的话就很简单可以得出答案为:160。
1.5 性能优化
1. 对如程序清单5. 4所示进行性能优化,使得效率提高。
程序清单5. 4 性能优化
int iValue1;
int iValue2;
iValue1 = 1234/16;
iValue2 = 1234%32;
对于嵌入式进行除法是很消耗效率的,能使用移位完成最好使用移位完成。
iValue1 = 1234 >> 4;
iValue2 = 1234 – ((1234 >> 5) << 5);
1234 / 16 = 77; 1234 % 32 = 18。而十进制:1234转化成二进制:0100 1101 0010。1234 >> 4 = 0000 0100 1101,转化为十进制即为:77;1234 >> 5 = 0000 0010 0110,((1234 >> 5) << 5)即为0100 1100 0000,转化为十进制即为:1120,1234 – 1216 = 18。
第六节 数组与指针
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下结果:
[email protected]:/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。
|
u.c和u.s的存储方式如图7. 5所示。
第八节 内存分配与内存释放
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 ++;
}
这样写,招聘人员一定会眼前一亮。
第十六节 机试题之大整数减法与乘法
考虑到有的初学者对第十五节的减法和乘法的疑惑,本节特此将减法和乘法的程序补上。
/******************************************************************************
** 函数名称:BigNumberSub
** 函数功能:大整数的减法运算
** 入口参数:str1:第一个减数
str2:第二个减数
ptr:容纳两数之差的空间首地址
ptrSize:此空间大小
** 出口参数:
******************************************************************************/
int BigNumberSub(const char *str1, const char *str2, char *ptr, int ptrSize)
{
/*
** iStr1Len:存储第一个字符串
** iStr2Len:存储第二个字符串
** iMaxLen :两个字符串中最长的长度
** i、j :循环
** iTemp :当前位暂存位
** iBorrow :进位标志位
*/
int iStr1Len , iStr2Len , iMaxLen , i , j , iTemp = 0 , iBorrow = 0 ;
char character1 , character2 ;
/* 测量两个字符串的长度 */
iStr1Len = strlen(str1) ;
iStr2Len = strlen(str2) ;
/* 将ptr存储区域的数据全部清零 */
memset(ptr, 0, ptrSize) ;
/* 如果被减数小于减数 */
if ( iStr1Len < iStr2Len ) {
return 0 ;
}
/* 从高位到低位逐位相减 */
for ( i = 0 ; i < iStr1Len ; i++ ) {
character1 = str1[iStr1Len - 1 - i] ;
character2 = (iStr2Len - 1 - i) < 0 ? ‘0‘ : str2[iStr2Len - 1 - i] ;
/* 如果有哪一位不是数字,则退出运算 */
if ( (!isdigit(character1)) || (!isdigit(character2)) ) {
return 0 ;
}
/* 当前位相减之后的暂存值 */
iTemp = (character1 - ‘0‘) + 10 - iBorrow - (character2 - ‘0‘) ;
assert(i < ptrSize) ;
/* 将当前相减位的值转换为字符存入结果 */
ptr = iTemp % 10 + ‘0‘ ;
/* 借位位的值 */
iBorrow = 1 - iTemp / 10 ;
}
assert(i < ptrSize) ;
ptr = ‘\0‘ ;
/* 将字符串从高位依次输出 */
for ( j = 0 ; j < i-- ; j++) {
char cTemp = ptr[j] ;
ptr[j] = ptr ;
ptr = cTemp ;
}
return 1 ;
}
/******************************************************************************
** 函数名称:BigNumberMul
** 函数功能:大整数的乘法运算
** 入口参数:str1:第一个乘数
str2:第二个乘数
ptr:容纳两数之积的空间首地址
ptrSize:此空间大小
** 出口参数:
******************************************************************************/
int BigNumberMul(const char *str1, const char *str2, char *ptr, int ptrSize)
{
/*
** iStr1Len:存储第一个字符串
** iStr2Len:存储第二个字符串
** iMaxLen :两个字符串中最长的长度
** i、j :循环
** iCarry :进位标志位
*/
int iStr1Len , iStr2Len , iMaxLen , i , j , iCarry = 0 ;
/* 测量两个字符串长度 */
iStr1Len = strlen(str1) ;
iStr2Len = strlen(str2) ;
/* 将ptr存储区域的数据全部清零 */
memset(ptr, 0, ptrSize) ;
/* 用数据一的低位到高位逐位和数据二相乘 */
for ( i = 0 ; i < iStr1Len ; i++ ) {
/* 如果字符串1中有不是数字,则退出 */
if ( (!isdigit(str1[iStr1Len - 1 - i])) ) {
return 0 ;
}
/* 从低位向高位逐位相乘 */
for ( j = 0 ; j < iStr2Len ; j++ ) {
/* 如果字符串2中有不是数字,则退出 */
if ( (!isdigit(str2[iStr2Len - 1 - i])) ) {
return 0 ;
}
/* 确保相乘之积能存储下 */
assert((i+j) < ptrSize) ;
/* 逐位相乘 */
iCarry += (str1[iStr1Len-1-i]-‘0‘)*(str2[iStr2Len-1-i]-‘0‘)+ptr[i+j] ;
ptr[i+j] = iCarry % 10 ;
iCarry /= 10 ;
}
/* 处理进位 */
for ( ; iCarry != 0 ; j++ ) {
assert((i+j) < ptrSize) ;
iCarry += ptr[i+j] ;
ptr[i+j] = iCarry % 10 ;
iCarry /= 10 ;
}
}
i = ptr[iStr1Len+iStr2Len-1] ? (iStr1Len+iStr2Len) : (iStr1Len+iStr2Len-1) ;
for ( j = 0 ; j < i ; j++ ) {
ptr[j] += ‘0‘ ;
}
/* 将结果从高位到低位输出 */
for ( j = 0 ; j < --i ; j++ ) {
char cTemp = ptr[j] ;
ptr[j] = ptr ;
ptr = cTemp ;
}
return 1 ;
}
其实大整数的加减乘除法很多公司都喜欢用来作为机试题目,作为今年找工作的亲们得好好看一下。其实笔试和机试都只是一种考核大家能力的形式,或许或多或少可能不能完全衡量一个人,但是企业要在几天内决定是否录用你,考试还是最直接的办法。你要是能通过你的三寸不烂之舌把HR搞定,我朱兆祺也佩服你。或许哪天我会邀请你来做我的首席销售员。呵呵。
第十七节 算法之二分查找
C语言程序的经典与否很大在于算法是否经典,这一节开始朱兆祺带领大家学习C语言算法篇。
就拿二分查找下手。
// text.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
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 ;
}
}
}
//测试程序
int main(int argc, char* argv[])
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
printf("%d\n" , BinarySeach(a,6,10));
return 0;
}
递归调用就是函数对自身的调用,但是一定要慎重使用,递归涉及到栈是否会溢出等问题,还有就是算法是否合适,并非说任何时候都是适合的。
我记得谭浩强的《C……》中用递归算法来解决阶乘的问题,我就疑惑了,使用循环解决岂不是更简单明了。
第十八节 数据结构之单向链表
链表在数据结构中讲得太多,但是都是炒来炒去没有一个是具有高移植性、易理解性等,以至于很多学习C语言的学生看到C语言都是畏惧三分,
朱兆祺这节带着你且看如何写出高效率、通俗易懂的单向链表,看完这节,你要是没有撕掉你手中的数据结构书本,那就是我写得还不够好,那么
请你联系我,我再改,直到你有撕掉你手中数据结构书本的感觉为止。
typedef unsigned int ElementType ;
/* 数据类型定义 */
typedef struct _SingleListNode {
ElementType data ;
struct _SingleListNode *next ;
}SingleListNode;
typedef SingleListNode* SingleList ;
typedef SingleListNode* Position ;
首先要创建一个头节点,并在内存中为头节点动态分配存储空间。
/* 申请存储空间,建立头节点 */
SingleListNode *pHead = (SingleListNode*)malloc( sizeof(SingleListNode) ) ;
将pHead定义为(SingleListNode *)类型,malloc函数的原型为void *malloc( size_t size ); 函数指向一个大小为size的空间,存储空间的指针必须为堆,不能是栈。因为不知道开辟的空间是什么类型,所以malloc函数返回的是(void*)类型,再通过强制转换为我们想要的类型。由于data占4个字节,next占4个字节,所以sizeof(SingleListNode) = 8。所以*pHead所指向的数据类型(即为链表节点)占用8个字节。
#if 1
printf("sizeof(SingleListNode) = %d \n" , sizeof(SingleListNode) ) ;
printf("sizeof(*pHead) = %d \n" , sizeof(*pHead) ) ;
printf("sizeof(pHead) = %d \n" , sizeof(pHead) ) ;
#endif
输出结果为:8,8,4。
创建链表即要将data和next置空。
pHead->next = NULL ; //将头节点指针域置空
pHead->data = NULL ; //将头节点数据域置空
在将节点插入到链表之前,首先得为这个待插入的节点申请存储空间,并且建立该节点的指针ptr。
SingleListNode *ptr = (SingleListNode *)malloc( sizeof(SingleListNode) ) ;
这里ptr为待插入节点的指针,ptr的值即为待插入链表的节点的地址。*ptr即为节点中存储的数据:data和next,共占8个字节。
待插入节点插入链表第一步,将数据x保存到ptr->data。
ptr->data = x ;
待插入节点插入链表第二步,将pos->next赋给ptr->next。
ptr->next = pos->next ;
待插入节点插入链表第三步,将pos->next指向待插入节点。
pos->next = ptr ;
这样我们就完成了将数据x的节点插入到链表中。
#if 1
printf(" x = %c \n" , x ) ;
printf("&x = %#x \n" , &x ) ;
printf(" ptr = %#x \n" , ptr ) ;
printf("&ptr = %#x \n" , &ptr ) ;
printf("(pos->next) = %#x \n" , (pos->next) ) ;
printf("(ptr->next) = %#x \n\n" , (ptr->next) ) ;
#endif
在插入节点函数中加入调试代码,运行程序,输入abc,调试结果。
请输入链表节点数据: abc
x = a
&x = 0x12fe3c
ptr = 0x392920
&ptr = 0x12fe24
(pos->next) = 0x392920
(ptr->next) = 0
x = b
&x = 0x12fe3c
ptr = 0x392958
&ptr = 0x12fe24
(pos->next) = 0x392958
(ptr->next) = 0
x = c
&x = 0x12fe3c
ptr = 0x392990
&ptr = 0x12fe24
(pos->next) = 0x392990
(ptr->next) = 0
ListData: abc
ListAddress: 0x394b48 0x392920 0x392958 0x392990
通过上面的调试,我们将数据和地址放进节点,便于我们明白链表的储存。
可以看到pos->next是和ptr相等的,因为两者都是指向待插入节点,而ptr->next始终是为0,因为ptr->next是指向NULL。
从中我们还可以看出,链表的存储空间是间断的,而不是像数组元素存储空间是连续的。
/*************************************************************************
** 函数名称 :sll_FindPrevious
** 函数功能 :单向链表按值搜索位置的前驱算法函数
** 入口参数 :pList ,
** 出口参数 : pCur
*************************************************************************/
Position sll_FindPrevious(SingleList pList , ElementType x)
上面这个函数得明白什么叫做函数指针,什么叫做指针函数。
void *Func () :指针类型的函数
void (*Func)() :一个指向void型函数的指针
定义一个结构体指针pCur,使其总是指向当前搜索节点的前一个节点。初始状态下pCur是指向头结点。
Position pCur ;
pCur = pList ; //pCur的初值指向头节点
从第一个节点开始比较pCur指向下一个节点的数据域和数据x的值,如果两者相等,则返回pCur;否则pCur重新指向下一个节点。
while ( (pCur->next) && (pCur->next->data) != x) {
pCur = pCur->next ; //pCur重新指向下一个节点
}
return pCur ; //返回已找到元素x所在节点的地址
删除节点和插入节点是相对应的,在删除节点之前,我们先要找到需要删除节点的位置,此时,我们就可以使用按值搜索节点位置的前去算法找到需要删除的节点位置。
/*************************************************************************
** 函数名称 :sll_delete
** 函数功能 :删除节点函数
** 入口参数 :pList ,x
** 出口参数 :void
*************************************************************************/
void sll_delete(SingleList pList , ElementType x)
Position pCur , temp ; //pCur指向要删除节点的上一个节点,temp指向删除的节点
pCur = sll_FindPrevious(pList , x) ; //返回需要删除的节点的上一个节点地址
如果sll_FindPrevious能返回删除节点的位置(即pCur不为NULL),那么执行删除该节点的操作。
if ( pCur && (pCur->next) )
删除操作第一步,让temp指向待删除的节点。
temp = pCur->next ;
删除操作第二步,让pCur->next指向待删除节点的下一个节点,这样就把待删除节点*temp删除。
pCur->next = temp->next ;
最后将已经删除了的节点temp的存储空间释放掉。
free(temp) ;
在删除节点函数中添加调试代码,在删除节点前和删除节点后都加入,进行前后对比。
#if 1
printf("pCur = %#x \n" , pCur) ;
printf("pCur->next = %#x \n" , pCur->next) ;
sll_printAddress(pList) ;
printf("\n\n") ;
#endif
if ( pCur && (pCur->next) ) {
temp = pCur->next ;
pCur->next = temp->next ;
free(temp) ;
}
#if 1
printf("pCur = %#x \n" , pCur) ;
printf("pCur->next = %#x \n" , pCur->next) ;
sll_printAddress(pList) ;
printf("\n\n") ;
#endif
调试代码的输出。
请输入链表节点数据: abc
ListData: abc
ListAddress: 0x394b48 0x392920 0x392958 0x392990
输入要删除的字符: b
pCur = 0x392920
pCur->next = 0x392958
0x394b48 0x392920 0x392958 0x392990
pCur = 0x392920
pCur->next = 0x392990
0x394b48 0x392920 0x392990
ListData: ac
请按任意键继续. . .
通过前后比较可以知道,我们删除了0x392958这个地址的节点,即为储存b的节点。
如果这个链表不再需要使用,则要把这个链表销毁掉。销毁办法是一个一个节点释放掉。
销毁链表第一步,让待销毁的节点指针(ptr)指向头节点(初始时,pList是指向头结点)。
SingleListNode *ptr = pList ;
销毁链表第二步,让现在pList所指的节点的下一个节点成为头节点,因为现节点将要被删除,直到所指节点为空为止。
pList = pList->next ;
销毁链表第三步,释放掉待销毁节点。
free(ptr) ;
销毁链表第四步,让下一个节点(即为pList现在所指节点)成为待删除节点。
ptr = pList ;
这样往复循环,直到ptr指向NULL为止。这样就成功把一个链表销毁了。
在销毁链表函数中加入调试代码,运行。
#if 1
printf("ptr = %#x \n" , ptr) ;
sll_printAddress(pList) ;
printf("\n\n") ;
#endif
运行结果,
ptr = 0x394b48
0x392920 0x392990 0x3929c8
ptr = 0x392920
0x392990 0x3929c8
ptr = 0x392990
0x3929c8
ptr = 0x3929c8
请按任意键继续. . .
我们可以看到当前删除的节点地址ptr,和删除节点之后各节点的地址。
第十九节 数据结构之双向链表
在单向链表中,获取当前节点的下一个节点,即可使用ptr = ptr->next,但是如果要获取当前节点的上一个节点,则需要重新遍历一次链表。但是如果有指向上一个节点的指针,ptr = ptr->befor,这样即可获取当前节点的上一个节点。虽然增加了空间上的执行,但是时间效率却提高了,这也算是以空间换时间的一种方法吧。其实老子在《道德经》中就多次提到时间和空间的置换,两者在一定条件下是要互相变通。
每一个节点应该保存的数据,有数据指针、指向下一个节点的指针、指针上一个节点的指针。
/* 双向链表节点数据结构 */
typedef struct _DoubleListNode {
void *pData ; //数据指针,用于保存链表元素的个数
struct _DoubleListNode *pNext ; //指向下一个节点的指针
struct _DoubleListNode *pPrev ; //指向前一个节点的指针
} DoubleListNode , *POSITION;
相对于总个链表,肯定需要一个头节点,让头节点的pNext指向第一个节点,而头节点的pPrev指向最后一个节点,而第一个节点的pPrev和最后一个节点的pNext都是指向头节点,这样就形成了循环链表。如果将头节点的的数据域空间用来存储链表节点的个数,这样我们就能明了链表的长度。
1.1 创建链表
/******************************************************************************
** 函数名称: ListCreate
** 函数功能: 创建链表函数
该函数虽然有一个参数,为函数指针,但是在创建函数中并不调用它,
而仅仅是保存它。这种保存而不调用回调函数的方法也叫注册回调函数。
如果不需要销毁元素,则只需要传入NULL即可。
** 入口参数: DestroyFunc:
** 出口参数: 如果创建失败,则返回NULL;成功则返回一个双向链表结构体的指针
******************************************************************************/
LIST *ListCreate(DESTROYFUNC DestroyFunc)
先开辟一个链表结构体空间,同时为头节点的所有成员初始化,初始状态下,pNext和pPrev都是指向本身,头节点的数据域用来保存节点个数。
LIST *pList ;
pList = (LIST *)malloc(sizeof(LIST)) ; //申请存储空间,建立头结点
if (pList) {
/* 初始时,头结点两个指针都指向头结点本身 */
pList->nodeHead.pNext = &(pList->nodeHead) ;
pList->nodeHead.pPrev = &(pList->nodeHead) ;
pList->nodeHead.pData = 0 ; //初始时节点个数为0
pList->DestroyFunc = DestroyFunc ; //注册销毁函数
}
return pList ; //返回链表结构体的指针
1.1.1 获取链表第一个节点位置
/******************************************************************************
** 函数名称: ListGetBegin
** 函数功能: 获取链表第一个节点的位置
** 入口参数: pList:要操作的双向链表指针
** 出口参数: pList->nodeHead.pNext:返回头结点指针
******************************************************************************/
POSITION ListGetBegin( PLIST pList )
{
assert(pList) ;
return (pList->nodeHead.pNext) ;
}
1.1.2 获取链表最后一个节点的下一个节点位置
/******************************************************************************
** 函数名称: ListGetEnd
** 函数功能: 获取链表最后一个节点的下一个节点的位置
** 入口参数: pList:要操作的双向链表指针
** 出口参数: &(pList->nodeHead):返回最后一个节点的下一个节点地址
******************************************************************************/
POSITION ListGetEnd( PLIST pList )
{
assert(pList) ;
return (&(pList->nodeHead)) ;
}
1.1.3 获取链表的元素个数
/******************************************************************************
** 函数名称: ListGetSize
** 函数功能: 获取链表元素的个数
** 入口参数: pList:要操作的双向链表指针
** 出口参数: (unsigned int)(pList->nodeHead.pData):返回链表个数
******************************************************************************/
unsigned int ListGetSize( PLIST pList )
{
assert(pList) ;
return ( (unsigned int)(pList->nodeHead.pData) ) ;
}
1.1.4 由位置获取数据的值
/******************************************************************************
** 函数名称: ListGetData
** 函数功能: 由位置获取数据的值
** 入口参数: pList:要操作的双向链表指针
pos :待插入节点的位置指针
** 出口参数:
******************************************************************************/
DATATYPE ListGetData( PLIST pList , POSITION pos )
{
assert(pList) ;
assert(pos) ;
return (pos->pData) ;
}
1.1 插入节点1.1.1 将数据插入到指定位置
要想将数据data插入到双向链表中,则必须先为待插入数据data的节点申请存储空间,并且建立指向该节点的指针pNewNode,用于 存储新节点的地址。
/******************************************************************************
** 函数名称: ListInsert
** 函数功能: 将数据插入到链表的指定位置函数
** 入口参数: pList:要操作的双向链表指针
pos :为待插入节点的位置指针
data :为待插入节点的数据指针
** 出口参数: 如果失败,则返回FALSE;如果成功则返回TRUE
******************************************************************************/
BOOL ListInsert( PLIST pList , POSITION pos , DATATYPE data )
插入节点第一步,为待插入数据data的节点申请存储空间;并将data数据存储在新建节点的数据域。
DoubleListNode *pNewNode ;
unsigned int uiTemp = 0 ;
assert(pList) ;
assert(pos) ;
pNewNode = (DoubleListNode *)malloc(sizeof(DoubleListNode)) ;
pNewNode->pData = data ;
插入节点第二步,将待插入节点的前向指针指向pos所指向的节点。
pNewNode->pPrev = pos ;
插入节点第三步,将待插入节点的后向指针指向pos指向的下一个节点。
pNewNode->pNext = pos->pNext ;
插入节点第四步,pos指向节点的下一节点的前向指针指向pNewNode。
pos->pNext->pPrev = pNewNode ;
插入节点第五步,pos指向节点的后向指针pNewNode。
pos->pNext = pNewNode ;
uiTemp = ((unsigned int)(pList->nodeHead.pData)) ;
uiTemp++ ;
(pList->nodeHead.pData) = ( void * ) uiTemp ;
return TRUE ;
成功插入节点后,链表的节点数要加1,返回成功插入的标志。
1.1.1 将数据插入到链表头成为第一个节点
也就是说吃插入在头结点后面,成为第一个节点。那么同样要知道头节点的地址,即为&pList->nodeHead。
/******************************************************************************
** 函数名称: ListPushFront
** 函数功能: 将数据插入到链表头函数
** 入口参数: pList:要操作的双向链表指针
data :为待插入节点的数据指针
** 出口参数: 如果失败,则返回FALSE;如果成功则返回TRUE
******************************************************************************/
BOOL ListPushFront( PLIST pList , DATATYPE data )
{
return (ListInsert(pList , &(pList->nodeHead) ,data)) ;
}
1.1.2 将数据插入到链表尾成为最后一个节点
这里我们可以调用上面函数实现,只要我们知道链表尾的地址和待插入的数据,我们就能将数据插入到链表尾。链表最后一个节点的地址即为头结点的前向指针pList->nodeHead.pPrev。由于上面返回值只有TRUE和FALSE。则函数类型可以定义为BOOL型。
/******************************************************************************
** 函数名称: ListPushBack
** 函数功能: 将数据插入到链表尾函数
** 入口参数: pList:要操作的双向链表指针
data :为待插入节点的数据指针
** 出口参数: 如果失败,则返回FALSE;如果成功则返回TRUE
******************************************************************************/
BOOL ListPushBack( PLIST pList , DATATYPE data )
{
return (ListInsert(pList , (pList->nodeHead.pPrev) , data)) ;
}
1.2 删除节点1.2.1 删除指定位置的节点
/******************************************************************************
** 函数名称: ListErase
** 函数功能: 删除指定位置节点函数
** 入口参数: pList:要操作的双向链表指针
data :为待插入节点的数据指针
** 出口参数: 如果失败,则返回FALSE;如果成功则返回TRUE
******************************************************************************/
BOOL ListDelete( PLIST pList , POSITION pos )
将指向待删除节点的两个指针重新指向即可,先判断待删除节点是否为头节点。
unsigned int uiTemp = 0 ;
assert(pList) ;
/* 不允许删除头节点 */
assert( pos && (pos != &(pList->nodeHead)) ) ;
让(pos指向节点的前一个节点的后向指针)指向(pos指向节点的下一个节点)。
pos->pPrev->pNext = pos->pNext ;
让(pos指向节点的后一个节点的前向指针)指向(pos指向节点的前一个节点)。
pos->pNext->pPrev = pos->pPrev ;
如果用户定义了销毁函数,则执行调用销毁函数。然后释放待删除节点所占的空间。
if (pList->DestroyFunc) {
pList->DestroyFunc(pos->pData) ;
}
图3. 11 free(pos) |
free(pos) ;
成功删除节点后,链表节点数要减1,并且返回TRUE。
uiTemp = ((unsigned int)(pList->nodeHead.pData)) ;
uiTemp++ ;
(pList->nodeHead.pData) = (void *)uiTemp ;
return TRUE ;
1.1.1 删除链表头节点后的节点(即第一个节点)
只要知道第一个节点的地址即可调用上面函数删除第一个节点。而第一个节点的地址为:pList->nodeHead.pNext。
/******************************************************************************
** 函数名称: ListPopFront
** 函数功能: 删除链表头节点函数
** 入口参数: pList:要操作的双向链表指针
** 出口参数: 如果失败,则返回FALSE;如果成功则返回TRUE
******************************************************************************/
BOOL ListPopFront( PLIST pList )
{
return ( ListDelete(pList , (pList->nodeHead.pNext)) ) ;
}
1.1.2 删除链表的尾节点
尾节点地址为:pList->nodeHead.pPrev。
/******************************************************************************
** 函数名称: ListPopBack
** 函数功能: 删除链表尾节点函数
** 入口参数: pList:要操作的双向链表指针
** 出口参数: 如果失败,则返回FALSE;如果成功则返回TRUE
******************************************************************************/
BOOL ListPopBack( PLIST pList )
{
return ( ListDelete(pList , (pList->nodeHead.pPrev)) ) ;
}
1.2 按值搜索位置算法
从第一个节点开始搜索,直到搜索到数据域为data的节点为止;如果从第一个节点搜索到最后一个节点都没有搜索到数据域为data的节点,则返回NULL。
/******************************************************************************
** 函数名称: ListFind
** 函数功能: 搜索算法函数
** 入口参数: pList:要操作的双向链表指针
data :要查找的匹配数据
CompareFunc:数据匹配比较函数
** 出口参数:
******************************************************************************/
POSITION ListFind( PLIST pList , DATATYPE data , COMPAREFUNC CompareFunc )
{
DoubleListNode *pNode , *pEndNode ;
assert(pList) ;
pNode = ListGetBegin(pList) ; //获取第一个节点位置
pEndNode = ListGetEnd(pList) ; //获取最后一个节点的下一个节点位置
/* 从头到尾开始找 */
while (pNode != pEndNode) {
/* 如果找到则返回 */
if ( 0 == CompareFunc((pNode->pData),data) ) {
return pNode ;
}
pNode = pNode->pNext ;
}
return (NULL) ;
}
1.1 遍历算法
这个函数主要是在遍历双向链表的同时要对每一个节点进行怎么样的操作,这个操作由用户自己决定,入口参数只是给定了一个函数指针TRAVERSEFUNC,由用户自己调用。
/******************************************************************************
** 函数名称: ListTraverse
** 函数功能: 遍历算法函数
** 入口参数: pList:要操作的双向链表指针
TraverseFunc:节点数据的遍历操作函数
** 出口参数: 如果失败,则返回 0 ;如果成功则返回 1
******************************************************************************/
void ListTraverse( PLIST pList , TRAVERSEFUNC TraverseFunc )
{
DoubleListNode *pNode , *pEndNode ;
assert(pList) ;
pNode = ListGetBegin(pList) ; //获取第一个节点的位置
pEndNode = ListGetEnd(pList) ; //获取最后一个节点的下一个节点位置
/* 从头到尾开始遍历 */
while (pNode != pEndNode) {
TraverseFunc(pNode->pData) ; //访问数据
}
}
1.2 销毁链表
从最后一个节点开始逐个删除节点,一直到头结点为止。最后再释放掉链表所占空间。
/******************************************************************************
** 函数名称: ListDestroy
** 函数功能: 销毁链表函数
** 入口参数: pList:要释放的双链表指针
** 出口参数: void
******************************************************************************/
void ListDestroy( PLIST pList )
{
assert(pList) ;
while( 0 != ListGetSize(pList) ) {
ListPopBack(pList) ; //删除链表尾节点
}
free(pList) ; //释放节点所占的空间
}
第二十节 数据结构之栈
栈是只允许从栈顶压入数据,从栈顶弹出数据,所以栈是先进后出(FILO)。栈的操作有:栈的初始化、压栈、出栈、判断是否栈空、判断是否栈满、取栈顶元素。
建立一个栈,然后将栈初始化为空。
/*************************************************************************
** 函数名称 : Stack_SetNull
** 函数功能 : 构造一个空栈函数
** 入口参数 : stk
** 出口参数 : void
**************************************************************************/
void Stack_SetNull( stack *stk )
{
stk->uiTop = 0 ;
}
判断栈是否为空,只需要判断uiTop是否为0。
/*************************************************************************
** 函数名称 : StackIsEmpty
** 函数功能 : 判断栈是否为空函数
** 入口参数 : stk
** 出口参数 : ( (bool)(stk->uiTop == 0))
**************************************************************************/
bool StackIsEmpty( stack *stk)
{
return ( (bool)(stk->uiTop == 0)) ;
}
判断栈是否为满,只需要判断uiTop是否大于等于MaxSize。
/*************************************************************************
** 函数名称 : StackIsFull
** 函数功能 : 判断栈是否满函数
** 入口参数 : stk
** 出口参数 : ((bool)(stk->uiTop >= MaxSize))
**************************************************************************/
bool StackIsFull( const stack *stk )
{
return ((bool)(stk->uiTop >= MaxSize)) ;
}
首先要判断没有栈满,先将待入栈元素压入栈顶,栈顶值加1。
/*************************************************************************
** 函数名称 : PushStack
** 函数功能 : 入栈操作函数
** 入口参数 : stk , x
** 出口参数 :
**************************************************************************/
void PushStack( stack *stk , stackElementT x )
{
assert(!StackIsFull(stk)) ; //确保栈没有满
stk->elements[(stk->uiTop)++] = x ; //先将新元素x入栈,栈顶值加1
}
如果栈没有空,执行出栈操作。栈顶值减1,弹出栈最上面元素的数据。/*************************************************************************
** 函数名称 : PopStack
** 函数功能 : 出栈操作函数
** 入口参数 : stk
** 出口参数 : stk->elements[--(stk->uiTop)]
**************************************************************************/
stackElementT PopStack( stack *stk )
{
assert (!StackIsEmpty(stk)) ; //确保栈不为空?
return stk->elements[--(stk->uiTop)] ; //将栈顶值减1,相当于删除栈顶节点
//返回该节点的值
}
设定一个搜索值index,如果index = 0 ,则是读取站最上面元素的值;如果index = uiTop – 1,则是读取栈最底元素的值。
/*************************************************************************
** 函数名称 : GetStackElement
** 函数功能 : 读栈任意位置元素值函数
** 入口参数 : stk , index
** 出口参数 : (stk->elements[stk->uiTop - index -1])
**************************************************************************/
stackElementT GetStackElement( stack *stk , int index )
{
assert ( (index >= 0) && (index < (stk->uiTop))) ;
return (stk->elements[stk->uiTop - index -1]) ;
}
栈中元素个数即为uiTop的值。
/*************************************************************************
** 函数名称 : StackDepth
** 函数功能 : 返回栈中元素个数函数
** 入口参数 : stk , index
** 出口参数 : (stk->uiTop)
**************************************************************************/
int StackDepth( stack *stk )
{
return (stk->uiTop) ;
}
第二十一节 通过加减法高效的求出两个无符号整数的商和余数
假设处理器不能实现除法运算,通过加减法高效地求出两个无符号整数的商和余数。 事实上,在微处理器上,所有的四则运算都会转化为加法,但是除法运算会消耗掉微处理器大量的时间,因此我们设计一个算法,只通过加减法求出两个无符号整数的商和余数。这个题目广州致远电子有限公司曾经用来作为招聘软件工程师的机试题目。说起周立功,首先确实得感谢他,从大一暑假开始就给我提供夏令营机会在广州致远电子有限公司实习,将软件功底一次次提升,去年7月份开始更和致远签下合同,开始正式实习之路。但是11月份,由于个人想法,依然离开了广州致远。扯远了,拉回来。
算法一:用被除数(iDividend)循环去减除数(iDivisor),直到被除数减去除数的差(记录为iRemainder)小于除数为止。做减法的次数(记录为iCompany)即为商,iRemainder即为余数。但是当被除数比除数大很多时(iDividend=10000 ,iDivisor=3),相减的次数达3333次,所以此方法非常低效。
算法二:使用递归算法实现。除数(iDivisor) =除数(iDivisor)*2。因此,商(iCompany)=商(iCompany)*2。如果余数大于除数,则商(iCompany)= 商(iCompany)+1,余数(iRemainder)=余数(iRemainder)- 除数(iDivisor)。假设被除数(iDividend) =100,除数(iDivisor)=3,则递归算法求商和余数过程。
很显然,算法一效率太低,不可取,因此选择算法二。
采用算法二,就涉及到返回值的传递,方法有四种:
作为返回值的商和余数,如何传回给主函数?这是一个很有讲究的问题,方法很多,可以是用过指针传回,可以通过全局变量,可以通过结构体,可以使指针函数等。
方法一:全局变量,一个程序中出现全局变量是很难让人接受的,这里使用全局变量返回时不可取的。
图 2.1 程序流程图 |
方法二:结构体,结构体确实可以保存很多变量,并且传回来也是没有问题的。但是结构体所占内存较大,不提倡使用。
方法三:指针,在递归函数中,使用指针变量将值返回。
方法四:指针函数,在递归函数中将商和余数的值返回。
对比上面四种方法,从程序健壮性、可移植性、安全性等诸多方面考虑采取方法三或方法四。
递归函数的声明,采用指针变量返回商和余数。
/*******************************************************************************
** 函数名称:pass_to_return
** 函数功能:递归算法
** 入口参数:unsigned int pDividend , unsigned int pDivisor
** unsigned int *iCompany , unsigned int *iRemainder
** 出口参数:
*******************************************************************************/
extern unsigned int
pass_to_return( unsigned int iDividend , unsigned int iDivisor,
unsigned int *iCompany , unsigned int *iRemainder) ;
递归函数的详细设计,采用指针变量返回商和余数。
/*******************************************************************************
** 函数名称:pass_to_return
** 函数功能:递归算法
** 入口参数:unsigned int pDividend , unsigned int pDivisor
** unsigned int *iCompany , unsigned int *iRemainder
** 出口参数:
*******************************************************************************/
unsigned int pass_to_return( unsigned int iDividend , unsigned int iDivisor,
unsigned int *iCompany , unsigned int *iRemainder)
{
/*
* 如果被除数(iDividend)小于除数(iDivisor)
* 则商(iCompany)为0,余数(iRemainder)为被除数(iDividend)
*/
if( (iDividend) < (iDivisor) ){
*iCompany = 0 ;
*iRemainder = iDividend ;
}
/*
* 如果被除数(iDividend)等于除数(iDivisor)
* 则商(iCompany)为1,余数(iRemainder)为0
*/
else if( (iDividend) == (iDivisor) ){
*iCompany = 1 ;
*iRemainder = 0 ;
} else{
/*
* 如果被除数(iDividend)大于除数(iDivisor)
* 进行递归调用
*/
/*
* 返回递归函数
*/
pass_to_return( iDividend , (iDivisor+iDivisor) ,
iCompany , iRemainder ) ;
/*
* 商 = 2*商
*/
*iCompany += *iCompany ;
/*
* 如果余数 (*iRemainder) 大于 除数(iDivisor)
*/
if( (*iRemainder) >= iDivisor ){
/*
* 则商=商+1
*/
*iCompany += 1 ;
/*
* 余数=余数-除数
*/
*iRemainder = *iRemainder - iDivisor ;
}
}
return 0 ;
}
第二十二节 表达式计算器(1)
攻破C语言到现在是不是该结束了呢,非也,前面二十一节只是给大家填充下你们在大学遗漏的C语言基本知识。对,没错,是基本知识,或许你看前面的程序或者有难度,但是我可以告诉你,这些本应都是在大学应该完成的知识体系,只是因为大学期间的种种原因,没有去完成。
好了,基本知识体系差不多补充完整了,从这节开始,我们上实战了。
要求:写一个控制台程序,该程序实现输入一个表达式能够根据表达式计算出相应的结果。
例如:例如:输入字符串"1+2*(3-4)/5=",输出结果:0.6
要求:
基本要求:能够对+ - * / 进行运算,能够对优先级进行运算和括号匹配运算。
扩展要求:
1. 支持多数据类型输入,如: 0x12F(十六进制)、 12FH(十六进制)、 234O(八进制) 、‘F’(字符) 、0000 1111B(二进制)
2. 支持未知数赋值运算 如: x=3 则 3X+5=14
3. 支持常用函数运算 如: log、 ln、 sin、 sqrt等;
4. 支持大数据计算;
5. 支持自定义运算符。
这个题目涉及的东西很多很多,在往后待我慢慢给大家分析。暂且先将这个帖子闲置几天,看看有没有网友对此有想法,如果你想学习下,OK、联系我,我会给你提供思路和帮助。
《朱兆祺教你如何攻破C语言学习、笔试与机试的难点》后续更经常,朱兆祺用4年C语言经验、2年项目经验告诉你如何写出一个漂亮的算法写出一手漂亮的代码,敬请关注。
第二十三节 表达式计算器(2)
在二十二节我就说了,表达式计算器所涉及的知识点非常多,我先给大家罗列下:
1.动态双向链表(这是我的算法,或许你可以试试数组,但是这两者的优劣在何处?我希望你能明白),表达式的校对
2.中缀表达式转化为后缀表达式
3.后缀表达式的计算
4.文件读写
5.不同进制的转换
6.大整数算法
我从大方向大致列举出这六点,现在我们就一项一项进行搞定。
我们首先建立一个动态双向链表:
/* 建立一个头结点*/
Formula_head = new node;
init_node(Formula_head);
Formula_head->ch = getchar();
Formula_follow = Formula_head;
/* 从用户读取输入*/
for(;;){
newnode = new node; /* 新增加一个节点来存储输入*/
init_node(newnode); /* 初始化节点数据 */
while(1){ /* 获取输入数据,并且删除空格*/
newnode->ch = getchar();
//cin >> newnode->ch ;
if(‘ ‘ != newnode->ch ){
//print_link_data(Formula_follow);
break;
}
}
/* 结束输入*/
if(‘\n‘ == newnode->ch){
break;
}
/* 加入节点*/
Formula_follow = add_node(Formula_follow, newnode);
}
读取表达式之后,需要对表达式进行校对,处理未知数和防止输入不规则表达式(错误表达式):
/* 检查是否是未知数算式*/
if( check_unknow(Formula_head) ){
x_head = Formula_head;
Formula_head = NULL;
X_flag = 1;
continue;
}
/*如果有未知数的处理*/
if(X_flag){
Formula_head = add_x_in_link(Formula_head,x_head);
}
/*检查错误*/
if(check(Formula_head)){
continue;
}
/***************************************************
** 函数名称:check
** 函数功能:扫描链表,检查表达式是否错误
** 入口参数:node *head
** 出口参数:
***************************************************/
int check(node *head)
{
int brackets = 0;
for(; head; head=head->next){
/*连续出现2个运算符,报错*/
if((‘+‘==head->ch) || (‘-‘==head->ch) ||
(‘*‘==head->ch) || (‘/‘==head->ch)){
if((‘+‘==head->next->ch) || (‘-‘==head->next->ch) ||
(‘*‘==head->next->ch) || (‘/‘==head->next->ch)){
cout<<"erro: Consecutive two operators!"<<endl;
return 1;
}
}
/* 括号不匹配,报错*/
if(‘(‘ == head->ch){
brackets++;
}
if(‘)‘ == head->ch){
brackets--;
if(brackets<0){
cout<<"erro: brackets is not right,please check it out!"<<endl;
return 1;
}
}
}
/* 括号不匹配*/
if(0 != brackets){
cout<<"erro: brackets is not right,please check it out!"<<endl;
return 1;
}
/*没错返回0*/
return 0 ;
}
/***************************************************
** 函数名称:check_unknow
** 函数功能:检查是否为未知数算式
** 入口参数:node *head
** 出口参数:
***************************************************/
int check_unknow(node *head)
{
if((‘x‘==head->ch) && (‘=‘==head->next->ch) && (NULL!=head->next->next)){
return 1 ;
}
return 0;
}
这里开始,我们就不再是小试牛刀了,而是将数据结构真正用于实际项目中,达到算法最优、代码最优的效果。
第二十四节 表达式计算器(3)
最近事情比较多,有几天没有更新了,朱兆祺今天抽点时间,无论如何也得更新下。
承接上一节所讲,这一节我们应该到了中缀表达式转化为后缀表达式。这个数据结构在计算器表达式的项目中是非常经典的。这里我就再啰嗦几句,中缀表达式也叫做中缀记法,如1+2*3便是,因为操作符在中间嘛,所以就叫做中缀表达式。那么什么叫做后缀表达式,举一反三,即为操作数在后面嘛,那么即是:123*+。后缀表达式也叫做逆波兰表达式。
中缀表达式转化为后缀表达式,栈的用武之地来了,说到栈,第一个词语就是FILO,先进后出。中缀表达式转化为后缀表达式也正是用了这一个特点。
/***************************************************
** 函数名称:*mid_to_bk
** 函数功能:中缀转换成后缀表达式
** 入口参数:node *head
** 出口参数:
***************************************************/
node *mid_to_bk(node *head)
{
node *mid = Separate_str(head); /* 处理后的中缀表达式链表*/
node *bk = NULL; /* 后缀表达式头指针*/
node *ptr = bk;
node *oper = NULL; /* 符号堆栈 */
node *newnode = NULL;
node *code_node = NULL; /* 用来记录,保存地址 */
/* 开始转换*/
for(; mid; mid=mid->next){
/* 将节点数据拷贝到新节点去*/
newnode = new node;
init_node(newnode);
copy_node_data(newnode,mid);
/* 如果遇到数字就直接入栈*/
if(NUM == newnode->type){
/*加入链表*/
if(NULL == bk){
ptr = bk = add_node(ptr,newnode);
} else{
ptr = add_node(ptr,newnode);
}
}
/* 如果遇到特殊表达式*/
else if(SP_OPERATOR == newnode->type){
/* 如果前一个运算符也是特殊运算符就要出栈 */
while(oper){
if(SP_OPERATOR == oper->type){
code_node = oper->befor; /* 将这个节点换到后缀表达式里面*/
ptr = add_node(ptr,oper);
oper = code_node;
} else break;
}
oper = add_node(oper,newnode);
}
/*如果遇到普通运算符*/
else if(OPERATOR == newnode->type){
/*‘(‘直接进栈*/
if(‘(‘ == newnode->ch){
oper = add_node(oper,newnode);
}
/*‘)‘直接退栈到‘)‘*/
else if(‘)‘ == newnode->ch){
while(oper){
if(‘(‘ == oper->ch)break;/* 遇到‘(‘退栈结束*/
code_node = oper->befor; /* 将这个节点换到后缀表达式里面*/
ptr = add_node(ptr,oper);
oper = code_node;
}
oper = del_node(oper); /* 删除掉‘(‘符号*/
}
/* 遇到+-全部退栈,直到遇到‘(‘,或者退完为止*/
else if((‘+‘ == newnode->ch) || (‘-‘ == newnode->ch)){
while(oper){
if(‘(‘ == oper->ch)break; /* 遇到‘(‘退栈结束*/
code_node = oper->befor; /* 将这个节点换到后缀表达式里面*/
ptr = add_node(ptr,oper);
oper = code_node;
}
oper = add_node(oper,newnode);
}
/* 遇到/* 把特殊运算符全部退栈处理*/
else if((‘*‘ == newnode->ch) || (‘/‘ == newnode->ch)){
while(oper){
if(‘(‘ == oper->ch)break; /* 遇到‘(‘和特殊运算符截止*/
if((‘+‘ == oper->ch) || (‘-‘ == oper->ch))break;
code_node = oper->befor; /* 将这个节点换到后缀表达式里面*/
ptr = add_node(ptr,oper);
oper = code_node;
}
oper = add_node(oper,newnode);
}
/*
* 下面这段程序是调试使用
*/
#if 0
cout<<"mid: ";
print_link_data(test);
getchar();
#endif
}
}
/* 把最后留在堆栈里面的东西退出来*/
while(oper){
code_node = oper->befor; /* 将这个节点换到后缀表达式里面*/
ptr = add_node(ptr,oper);
oper = code_node;
}
ptr->next = NULL;
/*
* 下面这段程序是调试使用
*/
#if 0
cout<<"mid: ";
print_link_data(bk);
#endif
return bk;
}
C语言并不难学,数据结构也并不难。关键在于会不会花时间去学,会不会坚持下去。最近很多网友问我,学了一段时间就想放弃,我只想说4个字:剩者为王。
第二十五节 表达式计算器(4)
承接上一节,我们实现了中缀表达式转化为逆波兰表达式,这节,我们要做的肯定就是怎么样将逆波兰表达式计算出来。
其实这个计算过程是很简单了,如下:
/***************************************************
** 函数名称:calculate
** 函数功能:对后缀表达式进行计算处理
** 入口参数:node *formula
** 出口参数:result[0]
***************************************************/
double calculate(node *formula)
{
double result[200];
int i=0;
for(; formula; formula = formula->next){
/*数据再次进堆栈*/
if(NUM == formula->type){
result = formula->num;
i++;
}
//由于前面进行了中缀表达式转化为逆波兰表达式,因此为这里的计算奠定了十足的运算基础。
/*遇到普通符号就直接进行判断并且计算*/
else if(OPERATOR == formula->type){
/*加法计算*/
if(‘+‘ == formula->ch){
i--;
result[i-1] = result[i-1]+result;
}
/*减法计算*/
if(‘-‘ == formula->ch){
i--;
result[i-1] = result[i-1]-result;
}
/*乘法计算*/
if(‘*‘ == formula->ch){
i--;
result[i-1] = result[i-1]*result;
}
/*除法计算*/
if(‘/‘ == formula->ch){
i--;
/* 如果除数是0的话提示错误*/
if(0 == result){
cout<<"erro: the divvisor equal zero!Please check is out!"<<endl;
exit(1);
} else{
result[i-1] = result[i-1]/result;
}
}
}
/*遇到特殊符号*/
else{
/*sin*/
if(!strcmp(formula->str,"sin")){ //进行符号匹配,如果是sin运算符,进行项目的运算。这里读者其实可以进行改进
result[i-1] = sin(result[i-1]);
}
/* cos*/
else if(!strcmp(formula->str,"cos")){
result[i-1] = cos(result[i-1]);
}
/* tan*/
else if(!strcmp(formula->str,"tan")){
result[i-1] = tan(result[i-1]);
}
/* log*/
else if(!strcmp(formula->str,"log")){
result[i-1] = log(result[i-1]);
}
/*sqrt*/
else if(!strcmp(formula->str,"sqrt")){
result[i-1] = sqrt(result[i-1]);
}
/*help*/
else if(!strcmp(formula->str,"help")){
r_file() ;
} else{
cout<<"have no this OPERATOR,if you want add it,please add it upon, thank you!\n"<<endl;
exit(1);
}
}
}
return result[0];
}
有些东西,看似很难,那是因为你没有突破心理的障碍,当你全身心投入去实践的时候,你会发现,一切都是那么简单并且有趣。在嵌入式道路上,朱兆祺与你们同行。朱兆祺在最近两年内,一定会完成从C语言到单片机到ARM到嵌入式的整套完整的学习资料,并且全部开源,为你们在嵌入式道路杀出一条光明大道。
第二十六节 序列差最小
最近总有网友问我,C语言要怎么样去学,才能学好。很简单,实践实践再实践。这个程序,我提出几个问题,我大家能够举一反三。
// text.cpp : 定义控制台应用程序的入口点。
/***********************************************************************
** 程序名称 : 交换数据
** 程序描述 : 通过交换a,b中的元素,使[序列a元素的和]与[序列b无素的和]之间的差最小
************************************************************************/
#include "stdafx.h"
/*******************************************************************
** 函数名称 : void speed(int *iNum , int max)
** 函数功能 : 应用快速排序法排列数组顺序(从小到大)
** 入口参数 : int *iNum , int max //想想这两个参数
** 出口参数 : 无
********************************************************************/
void speed(int *iNum , int max)
{
int j , k ;
/* 这两层循环什么意思,还有什么和这个是相似的 */
for ( j = 0 ; j < max ; j++ )
{
for ( k = j + 1 ; k < max ; k++ )
{
if ( iNum[j] > iNum[k] )
{
/* 想想还有什么办法 */
iNum[j] ^= iNum[k] ^= iNum[j] ^= iNum[k] ;
}
}
}
}
/*******************************************************************
** 函数名称 : int sumall( int *all , int max )
** 函数功能 : 对序列求和
** 入口参数 : int *all , int max
** 出口参数 : sum
*******************************************************************/
int sumall( int *all , int max )
{
int i , sum = 0 ;
for( i=0 ; i < max ; i++ )
{
sum += all ;
}
return sum ;
//printf("和为:%d\n" , sum );
}
/*******************************************************************
** 函数名称 : void change( int *aNum , int *bNum , int max )
** 函数功能 : 对AB序列进行交换数据
** 入口参数 : int *aNum , int *bNum , int max
** 出口参数 : 无
*******************************************************************/
void change( int *aNum , int *bNum , int max )
{
int i , j , n , m , k ;
for ( i = 0 ; i < max ; i++ )
{
/* 认真看下这个if语句 */
if( ( (sumall(aNum,max) < sumall(bNum,max))&&(aNum<bNum) )||
( (sumall(aNum,max) > sumall(bNum,max))&&(aNum>bNum) ) )
{
aNum ^= bNum ^= aNum ^= bNum ;
speed(aNum,max) ;
speed(bNum,max) ;
}
}
printf( "*************************************\n" );
printf( "A序列调整之后:" ) ;
for ( n = 0 ; n < max ; n++ )
{
printf( "%d, " ,aNum[n]) ;
}
printf( "\n" );
printf( "B序列调整之后:" ) ;
for ( m = 0 ; m < max ; m++ )
{
printf( "%d, " ,bNum[m]) ;
}
printf( "\n" );
printf( "*************************************\n" ) ;
printf( "A序列调整之后的和:%d\n" , sumall(aNum,max) ) ;
printf( "B序列调整之后的和:%d\n" , sumall(bNum,max) ) ;
}
/***********************************************************************
** 函数名称 : 主函数
** 函数功能 : 输入和输出数据
** 入口参数 : int argc, char* argv[]
** 出口参数 : return
************************************************************************/
int main(int argc, char* argv[])
{
int i , j , n , m ;
int A[100]={} ;
int B[100]={} ;
int max ;
printf( "请输入你要输入的最大个数:Max=" ) ;
scanf( "%d" , &max );
printf( "请输入A序列数据 :\n" ) ;
for ( i = 0 ; i < max ; i++ )
{
scanf( "%d",&A ) ;
}
printf( "请输入B序列数据 :\n" ) ;
for ( j = 0 ; j < max ; j++ )
{
scanf( "%d",&B[j] ) ;
}
speed( A , max );
speed( B , max );
printf( "*************************************\n" );
printf( "排序之后的A序列:" ) ;
for ( n = 0 ; n < max ; n++ )
{
printf( "%d, " ,A[n] ) ;
}
printf( "\n" );
printf( "排序之后的B序列:" ) ;
for ( m = 0 ; m < max ; m++ )
{
printf( "%d, " ,B[m] ) ;
}
printf( "\n" );
printf( "*************************************\n" ) ;
change( A , B , max ) ;
return 0;
}
第二十七节 sizeof与strlen的深入
学校安排电气工程与自动化学院学生去校医院进行体检,远远就能听见测量身高那位医生一直在喊:“杨杰,170cm”,“范毅,160cm”……每个学生往测量身高的仪器一站,每个人身高都将会测量出来。
在C语言的数据类型中也一样,只要往sizeof中一放,每个数据类型占据了几个字节的内存也都一目了然。
1. 如程序清单7. 1所示,sizeof(a),sizeof(b)分别是多少?
程序清单7. 1 sizeof
#include <stdio.h>
int main(int argc, char *argv[])
{
char a[2][3] ;
short b[2][3] ;
printf( "sizeof(a) = %d \n" , sizeof( a ) ) ;
printf( "sizeof(b) = %d \n" , sizeof( b ) ) ;
return 0;
}
由于char 是1个字节、short是2个字节,所以本题答案是:
sizeof(a) = 6
sizeof(b) = 12
请按任意键继续. . .
为什么char是1个字节、short是2个字节呢?
并非说char占2个字节、short占2个字节、int占4个字节这些天生就是这样的,这与编译器有关。也就是说这些数据类型所占的字节数是会随着编译器的不一样而改变。由于本书使用的都是32位的编译器,那么其数据类型所占字节数的关系如下:
char :1个字节
short int :2个字节
int : 4个字节
unsigned int :4个字节
float : 4个字节
double : 8个字节
好的,再来看看如程序清单7. 2所示,sizeof(a),sizeof(b)分别是多少?
程序清单7. 2 sizeof
#include <stdio.h>
int main(int argc, char *argv[])
{
char *a[2][3] ;
short *b[2][3] ;
printf( "sizeof(a) = %d \n" , sizeof( a ) ) ;
printf( "sizeof(b) = %d \n" , sizeof( b ) ) ;
return 0;
}
是数组指针呢,还是指针数组呢?这里涉及*和[]和优先级的问题。我告诉大家的是这两个数组存放的都是指针变量,至于为什么,在后续章节会详细解释,然而指针变量所占的字节数为4字节,所以答案:
sizeof(a) = 24
sizeof(b) = 24
请按任意键继续. . .
为什么指针变量所占字节数为4个字节呢?
我相信在看这本书的读者应该都知道指针,指针记录的是一个对象的地址。既然指针是来存放地址的,那么它当然等于计算机内部地址总线的宽度。所以在32位计算机中,一个指针变量的返回值必定是4个字节。这就是为什么指针变量所占字节数为4的原因。
留给读者一个思考题:
int *ip;
char *cp;
double *dp;
void *vp;
printf("sizeof(ip) = %d \n", sizeof(ip));
printf("sizeof(cp) = %d \n", sizeof(cp));
printf("sizeof(dp) = %d \n", sizeof(dp));
printf("sizeof(vp) = %d \n", sizeof(vp));
在这里读者要特别注意的是sizeof使用的时候后面会加括号,但是一个测量数据类型长度的关键字,而并非是一个函数,因此字节数的计算在程序编译时进行,而不是在程序执行的过程中计算出来。与sizeof具有类似功能的一个函数:strlen()。strlen()所作的仅仅是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符‘\0‘为止,然后返回计数器值。
sizeof和strlen的区别:
1. sizeof是运算符关键字;strlen是函数。
2. sizeof在程序编译时进行字节数的计算;strlen的结果在程序执行的过程中进行计算。
3. sizeof用于计算类型所占内存的大小;strlen用于计算字符串的长度。
4. sizeof可以用类型和函数做参数;strlen只能用char*做参数,且必须是以“\0”结尾。
5. 数组做sizeof的参数时不退化;传递给strlen时会退化为指针。
第二十八节 C与C++中的const
其实在C++中一般不会使用#define定义常量,而是使用const定义常量。这是因为:
1) const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误(边际效应)。
2) 有部分调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
实际上常量的引进是在早期的C++版本中,当时标准C规范正在制订。那时,常量被看做一个好的思想而被包含。在C中。但是,C中的const的意思是“一个不能被改变的普通变量”。在C中,它总是占用内存,而且它的名字是全局符。C编译器不能把const看成一个编译期间的常量。在C中,如果写:
const iSize = 100;
char cArray[iSize];
尽管看起来好像做了一件合理的事,但这将得到一个错误的结果。因为iSize占用内存的某个地方,所以C编译器不知道它在编译时的值。在C语言中可以选择这样书写:
const iSize;
这样写在C++中是不对的,而C编译器则把它作为一个声明,这个声明指明在别的地方有内存分配。因为C默认const是外部连接的,C++默认const是内部连接的,这样,如果在C++中想完成与C中同样的事情,必须用extern把内部连接改成外部连接:
extern const iSize;
这种方法也可用在C语言中。在C语言中使用限定符const不是很有用,即使是在常数表达式里(必须在编译期间被求出)想使用一个已命名的值,使用const也不是很有用的。C迫使程序员在预处理器里使用#define。
第二十九节 do、while
看过《亮剑》的人都知道,楚云飞部下准备投敌时,李云龙和赵刚之间有这么一段对话(大意):
赵刚:“是不是该请示下总部首长。”
李云龙:“等请示下来,黄花菜都凉了,步兵连快速前往,一营二营迂回包抄,一个小时解决战斗。”
将在外,军令有所不受。如果说while()是先奏后斩,那么do、while语句这是先斩后奏。
do{……}while()语句和while()语句除了循环控制以外,do{……}while()语句还有一种特殊用法,如下:
#define w_tr(x,y)
do {
parport_write_data(lp_table[(x)].dev->port, (y));
parport_write_control(lp_table[(x)].dev->port, (y));
} while (0)
这段宏定义取自于linux内核源码中,放眼一看,感觉怪怪的,因为do{……}while(0)语句里面的程序只循环了一次,貌似do{……}while(0)语句是多余的。事实上并非如此,我们去掉do{……}while(0)语句看看会出现什么情况。
#define w_tr(x,y)
parport_write_data(lp_table[(x)].dev->port, (y));
parport_write_control(lp_table[(x)].dev->port, (y));
如果有一天你不听劝,if语句忘了加大括号的时候,当执行如下程序时:
if (x > 10)
w_tr(x,y)
那么悲剧了,因为这段程序使用宏替换就变成了:
if (x > 10)
parport_write_data(lp_table[(x)].dev->port, (y));
parport_write_control(lp_table[(x)].dev->port, (y));
这就意味着不管x是否大于10,第二条语句都将执行。读者可以自行参考linux内核源码,里面宏定义的语句中,基本上都是使用do{……}while()格式。
第三十节 变量的生命周期
《道德经》第七章有云:天地所以能长且久者,以其不自生,故能长生。是以圣人後其身而身先,外其身而身存。非以其无私耶?故能成其私。老子告诉我们,万事万物都有生命周期,所以C语言中的变量是有生命周期,有生有死。
我们先来看下这个程序:
int main(int argc, char* argv[])
{
for (int i = 0; i < 10; i++)
{
;
}
for (int i = 0; i < 10; i++)
{
;
}
return 0;
}
在main函数中出现了两次i,但是这两者互不影响,原因在于当每个for循环结束之后,i的生命周期也就完结。但是笔者不提倡这种代码风格,因为变量i隐含在了执行程序中,无论是代码的阅读和维护都有较大的困难,因此工程项目中不建议使用该语法。如果程序代码,不是为了方便别人阅读而写的,而是仅仅自己能够阅读,那么再漂亮的代码也终将石沉大海。读者千万要牢记:只有自己才能读得懂的代码,是没有使用价值的。
因此笔者建议将上述代码写成:
int main(int argc, char* argv[])
{
int i = 0;
int j = 0;
for (i = 0; i < 10; i++)
{
;
}
for (j = 0; j < 10; j++)
{
;
}
return 0;
}
有效区域、生命周期、作用域,这三者或多或少是相互依赖,对于变量的作用域,笔者给大家举一个列子,相信你马上就会明白。
int i = 1;
int main(int argc, char* argv[])
{
printf("i = %d \n", i);
for (i = 0; i < 3; i ++)
{
printf("i = %d \n", i);
int i = 10;
printf("i = %d \n", i);
}
printf("i = %d \n", i);
return 0;
}
我想很多读者看到这个程序,第一反应应该是难道不会报错。是的,不会报错。我们先来见证下奇迹的时刻,输出为:
i = 1
i = 0
i = 10
i = 1
i = 10
i = 2
i = 10
i = 3
请按任意键继续. . .
我们来分析一下,程序定义了一个全局变量i,并且赋值为1,进入main()之后,输出,即为此刻为1的i。OK,紧接着进入for循环,此刻重新对全局变量赋值为0,此刻输出为0的全局变量i的值;接着重定义一个局部变量i,并且赋值为10,此刻输出即为局部变量为10的i的值。循环3次之后,此刻全局变量为3,最后将其输出。从这个程序可以看出,全局变量i的作用域是整个程序,而在for循环内局部变量i的作用域仅仅是在for循环内。