Contents
数组,指针,字符串(中)
指针,其实没什么难的,特别是对指针的理解,好学生,背背,考试100分。难的在于书写习惯和设计方法。原理理解了,对于指针的使用,养成良好习惯,等你能力提升后,自然可以使用的游刃有余。例如刚开始打键盘,确定好十个手指的位置,或许一开始你很郁闷,每个字母都要去回忆在哪个位置,应该用那个手指,但熟练后呢?
那么对于指针的良好习惯究竟有哪些?其实很多,而且和功夫一样,分门派的,没个标准答案,无非看你是否坚持。这里我说说起步点的就我这个风格的一些建议。论我是哪派?自认为逍遥派吧。哈。所以注意,以下的建议都是鬼话,而不是军规,而且是针对那些新手,总会出指针跑飞的问题。你听不听随便。 挺烦“军规”的,如同战场上踢正步,不死也是因为对手按照军规只会打死靶,不会打活人。真的在战场上活下来的,都算兵油子,而且具备高素养的,近乎本能的生存习惯。因此包括我下面的建议,谈不上高素养,但也只是生存方法,要想提高自己,还需要多锻炼,多提升自己的高素养的本能(设计方法)。
鬼话1:全局指针存储空间,仅用于传递,而不用于操作(逻辑,或计算),落实到每个函数内,应该用局部存储指针空间来复制使用。例如model_done()函数里 static void model_done(void){ char *p = g_pmodel; 这样的目的是防止你内部修改了指针存储空间的内容。
鬼话2:任何一个指针,你要明确它指向哪?逻辑上的,不是实际地址。同时,指向的空间是什么?逻辑上的,随后,根据这个空间的逻辑含义,理解其空间大小。
鬼话3:对使用指针,来进行空间寻找(寻找到诸如 p[i]的存储空间),那么其寻找范围,记得用宏方式实现,所以,model_done()函数里的方法就是错误的。
for (i = 0 ; i < 100 ;i++){
p[i] = i;
}
正确的方法是,你将
#define S_PMODEL_3 6
#define S_PMODEL_2 7
#define S_PMODEL_1 180
替换到value.h。而上述的代码应该是 for (i = 0 ; i < S_PMODEL_1 ; i++)
鬼话4:虽然任意类型的指针的存储空间,都是个指针类型的存储空间,但是从逻辑理解上,一定要非常明确究竟是什么意思,而能用少级数的指针类型就别用高级数的。诸如 char ****pppp;是个毛病的做法。
鬼话5:对指针所指向空间的操作,以及指针自身的操作,尽可能的用宏。一个比较极端的例子为
#define NEXT_P(p) do {p++;}while(0)
#define SET_P(p,d) do {p[0] = d;} while(0)
for (i = 0 ;i < S_PMODE_1 ; i++){
SET_P(p,i);
NEXT_P(p);
}
鬼话6:别总想念malloc ,free 。要么一次申请个够,要么就压根别用。
鬼话7:模块之间的交互,肯定是外部函数啦,就是哪些没有static的函数,要确保有指针检测逻辑。所以模块和模块之间,对一个任务,不要频繁交互。而对于static类的函数,则不需要做指针检测,因为逻辑你可以显式的用代码来约束。例如 i < S_PMODEL_1
鬼话8:指针被一个函数修改后,尽可能用函数返回来传递。实在不行用C文件内部局部变量来传递,谁用参数来传递,谁傻瓜。为什么说这个鬼话,不解释,看看我全身沾满的粪迹,就可以理解,你完全可以不听,无非最终你和我拥有一样的外套。但这个外套是摔出来的,我借你穿也没用。
鬼话9:时刻搞清楚,你是在改变指针存储空间内的值,还是在改变指针存储空间内的值所指向的空间内的值。
上面的鬼话,其实没什么含量。但从带过的小朋友的错误的代码来看,经常是因为不能坚持上面的原则。
下面介绍一个特殊的指针,函数。 为什么说他特殊,因为这个指针你不可改变。为什么说函数就是指针,此处的函数不是函数实现本身。而是函数在程序中的位置。
严重警告一句鬼话:函数是个指针,这是一句鬼话,不是一个可以在考卷上写的答案。此处仅是从函数的存储,和利用来说明。
我们修改model.c下半部分如下:
#include <stdio.h>
#include <setjmp.h>
#include "value.h"
#define TEST_FUNC_P
#ifdef TEST_FUNC_P
typedef void F_V_V(void);
#endif
jmp_buf context_buf;
static int test = 0;
static void model_done(void){
int i;
printf("param_done func !\n");
for (i = 0 ; i < 100; i++){
printf("touch the dog! \n");
if (test >= 10){
printf("the officers come ! run away!\n");
longjmp(context_buf,test);
}
test += 1;
}
return;
}
static void model_exit(void){
printf("param_exit func !\n");
return;
}
void model(int status){
int t;
int flag = 0;
#ifdef TEST_FUNC_P
F_V_V *tt;
#endif
do {
switch (status){
case 0:
if ( (t = setjmp(context_buf)) >= 10){
printf("my god ,i escape! %d\n",t);
status = 2; flag = 1;
}
break;
case 1:
model_done();
break;
case 2:
#ifdef TEST_FUNC_P
tt = model_exit;
tt();
#else
model_exit();
#endif
flag = 0;
break;
default:
printf("error ! the status value is illegal!\n");
status = 2;
flag = 1;
}
}while (flag);
return;
}
编译,链接,运行。 你会发现,model_exit();被调用了。注意#define TEST_FUNC_P的用法。
你看看,是不是个指针?你可能会说,F_V_V和 model_exit()这个函数有什么关系。不妨先说下typedef,在参考文献 1 6.7.8里有所介绍,不过一个原理性的内容6.7.1.5摘抄如下:
The typedef specifer is called a "storage-class specifer" for syntactic convenience only.
主要注意,storage-class。简单的理解可以是一种存储类型的标记,或新定义名,就是 typedef。你可以看做是一个类型的新名,而不能看做是一个新类型。
而对于函数的存储类型的标记就是在 typedef 后面跟上函数的接口声明。此时,函数名,就不再是函数名了,而是这个接口类型一个函数类型名。
鬼话: 我们区分函数之间的差异,仅是函数的接口,包括参数,和返回值。如果这两个相同,那么就是同一类函数。这如同char 类型里面存了两个不同的数值,都是8位,我们认为是带符号的,那么他们都是char 类型。因此,函数里面怎么实现的,和几个函数接口是否属于同一个类型函数没关系。
你不信?想证明 F_V_V 和 model_exit有点关系,那你在 typedef void F_V_V(void); 下面加上一个 F_V_V model_exit; 你编译一下,怎么样,错了吧。不是我逻辑有问题,错了应该没关系,怎么能证明有关系,你尝试如下写 static F_V_V model_exit; 你看是否有价值。 这个和 static void model_exit(void); 实际是一个含义。
但需要注意,这个的F_V_V model_exit和 char c;等申请一个存储空间并不一样。 F_V_V准确说是个接口描述。而F_V_V *tt;表示,tt是一个存储空间,里面存了一个指针,这个指针指向一个具备F_V_V接口描述的地方,这是啥地方?函数入口。也就是函数。
其实上面的说法不准确,并非说函数是个指针,但因为大家的常用口头语,导致,函数确实也就是个指针。准确说,应该是存放函数的入口地址的空间是个指针类型。
而我们通常调用一个函数,这就是取出这个函数入口地址,并跳转过去(当然还有其他动作不是简单goto),。记得在程序里,函数之间的调用,实际是,函数入口地址的利用。
由此,我们可以说,存在函数入口地址的空间是个指针空间类型,(这很废话),但被大家叫来叫去,call function,由此我只好说,函数也就是个指针。
理解清楚这个,你就可以理解,为什么F_V_V tt; 这就是函数指针,函数指针是在有太多用途了。不经常使用函数指针,只能说你的代码逻辑不够复杂。一个典型的应用就是用一个数组,数组的每个存储单元的类型就是一个函数指针类型。结合 F_V_V tt;看看下面的代码是否能理解
#include <stdio.h>
#include <setjmp.h>
#include "value.h"
jmp_buf context_buf;
typedef void F_V_V(void);
static int test = 0;
static void model_init(void);
static void model_done(void);
static void model_exit(void);
static F_V_V *model_entry[4] = {model_init,model_done,model_exit,model_exit};
static void model_init(void){
int t;
if ( (t = setjmp(context_buf)) >= 10){
printf("my god ,i escape! %d\n",t);
model_exit();
}
return;
}
static void model_done(void){
int i;
printf("param_done func !\n");
for (i = 0 ; i < 100; i++){
printf("touch the dog! \n");
if (test >= 10){
printf("the officers come ! run away!\n");
longjmp(context_buf,test);
}
test += 1;
}
return;
}
static void model_exit(void){
printf("param_exit func !\n");
return;
}
void model(int status){
status = (status > 3 ) ? 3: status;
model_entry[status]();
}
这里说说上面代码的一个问题。 1. static F_V_V *model_entry[4],我们从右边向左读,,首先是个有4个存储单元的空间,也即数组。 数组名叫做model_entry,每个存储单元是一个函数指针。并做了数组的初始化。记得由于每个单元都是指针,所以你完全可以让两个单元指向同样的地方,例如第2,3都是指向model_exit,对应第0指向model_init。 * >鬼话:随便你,反正我一直把函数(利用时不是设计书写时),看作指针(指向一个函数接口类型的地址),包括我写DSP汇编时。突出DSP不是证明我行,而是证明我在诸如ARM上不行。非并发指令集,C语言的编译器和 C语言本身已经做的足够好了。大多数时,没有必要由你设计汇编。 2. status = (status > 3 ) ? 3: status;这类代码逻辑必写,你可以有如下写法 if (status > 3){ error();// 直接报警退出 //or status = 3; } * 这样做是防止程序跳转异常。 除非model_entryX;这类的函数指针数组取内容再调用的方式,你是在一个static函数内,也即你本C文件内的调用逻辑可以严格保证status的取值范围。 * >鬼话:相信我,上面这第2条不遵守,一旦出事,几乎一定是事故,而不会是故事。 3. 这样做的好处是什么?模块化设计,你甚至可以动态的修改model_entry[]的内容,实现所谓的动态加载。 * >鬼话:别迷信其他更高级的语言的智能性,其他高级语言能实现的,C语言都能实现。 4. 注意 model_init(void) 内部的代码,还没到工程级别。这里只是简单的进行exit的退出动作。实际应当返回状态标记,由模块整体来进行判断组织。 5. 回顾一下,#define,如果我们再增加个模块内部的函数,你是不是又要重新修改代码?因此我们需要用#define来进行重新设定。 6. 在进行#define重新设定前,我们注意一个小细节。 model_exit 这个函数的打印点的内容是 printf("param_exit func !\n"); 唉。。历史遗留问题,其实从介绍打印点时,我就不想这样写,因为函数名一改,你得跟这改。通常我更喜欢使用func;(注意前后都是两个下划线),这是什么?预定义宏。这可是好东西。但实话实说,很多教科书上都忽略了。怎么用?参考下面整体的代码变化。 func可参考文献1 6.4.2.2.1
#include <stdio.h>
#include <setjmp.h>
#include "value.h"
jmp_buf context_buf;
typedef void F_V_V(void);
static int test = 0;
static void model_init(void);
static void model_done(void);
static void model_exit(void);
#define MODEL_ENTRY_NUM 4
static F_V_V *model_entry[MODEL_ENTRY_NUM] = {model_init,model_done,model_exit,model_exit};
#define __PRINT_FUNC() do {printf("%s func!\n",__func__);}while(0)
static void model_init(void){
int t;
__PRINT_FUNC();
if ( (t = setjmp(context_buf)) >= 10){
printf("my god ,i escape! %d\n",t);
model_exit();
}
return;
}
static void model_done(void){
int i;
__PRINT_FUNC();
for (i = 0 ; i < 100; i++){
printf("touch the dog! \n");
if (test >= 10){
printf("the officers come ! run away!\n");
longjmp(context_buf,test);
}
test += 1;
}
return;
}
static void model_exit(void){
__PRINT_FUNC();
return;
}
void model(int status){
status = (status >= MODEL_ENTRY_NUM ) ? MODEL_ENTRY_NUM-1: status;
model_entry[status]();
}
现在是否觉得,#define很好用了。func在宏__PRINT_FUNC()中。养成个习惯,如果是一个动作或操作,或者代码片,即便没有替换内容,那么也使用()方式,以提醒自己这是个操作。不是简单的名称或标示替换。
__PRINT_FUNC(),为什么前面加两个下划线,其实你不加也行,问我为什么?
鬼话:就是唬人,唬谁?你和其他需要看这段代码的人。以表示这个和本设计逻辑没有关系。而是一个基础的利用C语言的逻辑代码。也防止和你的逻辑设计中存在类似PRINT_FUNC的定义名有冲突。
其实这样还不够好。这里介绍一个连续定义enum,通常叫做枚举。方法如下
enum {
A,
B,
C
};
为什么叫连续定义,其等同于
#define A 0
#define B 1
#define C 2
但这个表示连续的对A,B,C进行定义,并不表示,对A,B,C的定义值连续。你完全可以如下
enum {
A = 1,
B = 10,
C = 0
};
这等同于
#define A 1
#define B 10
#define C 0
enum通常用于连续定义,包括对定义内容的连续,这方便啊。
很多教科书喜欢用
enum {
Monday,February,Wednesday
};
恨不得再对上述做个类型,并声明个空间符合此类型,由此认为,这样我们就可以对这个空间的存储值进行约束,只是上面的内容。但我这里仍然不介绍enum的类型定义方式,和空间声明方式。
鬼话:我尽可能介绍那些有用,有价值的东西,会给你带来歧义的东西,我另可丢到烟灰缸里,还用N个烟头,烫它,拷问他“老实交代,你是不是鬼子派来的”
为什么这么说,是因为,C语言里,强调类型是针对位宽和某些位宽下的有符号,无符号。如果指望一个类型,采用枚举,这是胡扯蛋的事情,你的代码很容易对内容进行修改,而不属于你的枚举类型。这种做法应当杜绝。也即,你认为一个类型,有5种情况,实际存储最小还是8位,而这个存储空间的操作,完全可以超出5,由此你的逻辑会有混乱。这和char c; c == -128到,c ==127不同。这个范围内,都可以看做char类型。
讨论完毕,我们修改一下部分代码,如下
enum{
MODEL_INIT,
MODEL_DONE,
MODEL_EXIT,
MODEL_ENTRY_NUM
}
static F_V_V *model_entry[MODEL_ENTRY_NUM] = {model_init,model_done,model_exit};
...
void model(int status){
status = (status >= MODEL_ENTRY_NUM ) ? MODEL_EXIT : status;
model_entry[status]();
}
此时,如果你想增加一个新函数,则可以修改如下:
static void model_new_done(void);//新增
enum{
MODEL_INIT,
MODEL_DONE,
MODEL_EXIT,
MODEL_NEW_DONE,
MODEL_ENTRY_NUM
}
static F_V_V *model_entry[MODEL_ENTRY_NUM] =
{ model_init,model_done,model_exit,model_new_done };
再加上,model_new_done的实现部分。 你不需要调整MODEL_ENTRY_NUM和 status = (status >= MODEL_ENTRY_NUM ) ? MODEL_EXIT : status; 这是模块化,结构化设计的一种思想体现。
鬼话:模块化设计思想可以使用各种方式实现,我给出的,只是我的经验,不存在抄袭谁,也不存在一定是权威,无非乐意用,就用,如果你喜欢麻烦我也没资格约束你。
下面讨论数组,前面已经有很多数组的例子了,再不讨论,要晕呼了。不过补上enum的资料。在参考文献1 的6.7.2.2,其实不用记得enum枚举出来的东西都是常量。理解一下连续定义这四个字,就OK了。当然不能重定义。而如果#define A 0 这种是可以重新对A进行定义的。或许你可以补充一下,常量化的连续定义。
数组,数组。说数组在说指针之后,是因为,指针你能搞清楚了。数组就简单了。数组和指针的根本区别如下: 数组,是编译器帮你申请好空间的。指针所指向的空间本身和指针自己的空间没有关系。 数组,是编译器帮你申请好空间的,所以,任何数组中的两个单元之间的关系,是固定的。同时数组可以如多级指针那样,使用p[1][2][3]的方式来实现。为了说明数组和指针的区别,我们回顾一下三级指针的空间申请。
char ***ppp;
int i,j;
ppp = (char ***)malloc(sizeof(void *)*5);
for (i = 0 ; i < 5 ; i++){
ppp[i] = (char **)malloc(sizeof(void*)*6);
}
for (i = 0 ; i< 5; i++){
for (j = 0 ; j < 6 ; j++){
ppp[i][j] = (char *)malloc(sizeof(char)*7);
}
}
那么我们看下如下代码
char a[5*6*7];
char *aa[5*6];
char ** aaa[5];
int i,j;
for (i = 0 ; i < 5 ; i++){
aaa[i] = &(aa[i * 6]);
}
for (i = 0 ; i < 5 ; i++){
for (j = 0 ; j < 6 ; j++){
aaa[i][j] = a + 7 * (i * 6 + j);
}
}
上面两个程序有相似和不同的地方。不同的地方在于,对于malloc(sizeof(char)7);后者,是使用a[56*7]这个空间来利用。一个是编译器帮你申请到的空间 a。一个是malloc调用操作系统的模块帮你动态获取的空间。
同时,我们注意,aaa,他只有5个存储空间,而实际要如ppp一样,实现3级的存储空间都可存在。由此需要将aaa的五个存储空间里,存放 5个不同的地址,而这些地址指向的空间,也是编译器帮你分配的,如同
for (i = 0 ; i < 5 ; i++){
ppp[i] = (char **)malloc(sizeof(void*)*6);
}
代码使用malloc一样,无非我不至于无聊到,让编译器申请5个,每个包含6个存储单元,每个存储单元是一个char 类型的指针。
鬼话:有人要骂我了,怎么说数组,又有指针了,嘿,你能把上面的东西,分析清楚,你就能理解指针和数组的差异了。 先说个
for (i = 0 ; i < 5 ; i++){
ppp[i] = (char **)malloc(sizeof(void*)*6);
}
和 aaa[i] = &(aa[i * 6]);
的不同。
哪不同? aaa[i] 和aaa[i+1]内存放的数值差异可是固定的,始终差了 6个指针类型的存储空间对应的地址宽度(不是位宽)。 那么我们再琢磨琢磨另一个例子
char b[5*6*7];
char *bbb[5][6];
for (i = 0 ; i < 5 ; i++){
for (j = 0 ; j < 6 ; j++){
bbb[i][j] = b + 7 * (i * 6 + j);
}
}
哦。少了 char aa[56];和
for (i = 0 ; i < 5 ; i++){
aaa[i] = &(aa[i * 6]);
}
那么原先 char aa[56];存储空间在哪? 其实就在 bbb[5][6]里面。那么是不是上面的空间逻辑不一样,废话,显然。如果一样,怎么会少代码呢。但对bbb的使用,逻辑上并没有差异性。
这是为什么?为什么会少了存储空间,逻辑还没有差异性。这是因为, char *bbb[5][6]; 已经包含了一个固定的逻辑,对于bbb[0] 和 bbb[1],编译器可以明确知道,他们的差异是 6个char 类型的指针所需要的存储空间的地址宽度。我们没有必要,再用诸如char ** XXX[5];来进行记录。
同样,我们看一下下面的代码 char ccc[5][6][7]; 是不是我漏泄了什么?对于aaa的逻辑正确应用?没有。这里已经包含了567个char 类型的空间。同时你可以通过 ccc[i],ccc[i][j] ,ccc[0][1][3]等来获取对应的内容。特别是 ccc[0][1][3]和前面的任何例子,所访问的空间是一致的。包括ppp[0][1][3]。
但现在 ccc[i][j] 和 bbb[i][j]是一致的?逻辑上是一致的。但是实际不一致,为啥?bbb[i][j]可是有户口的。ccc[i][j]最多是个标签。为什么这么说?
char *bbb[5][6]; 可不是白写的。 那么ccc[i][j]的标签指向哪?当然是ccc[i][j][0]这个位置的地址,一切从零0开始嘛,自然我们照顾0了。 而bbb[i][j]的标签指向哪?是b[i][j][0]这个位置的地址,为什么这么说?自己看代码。 因此,ccc[i][j] 对应bbb[i][j]的一致是逻辑上,不一致是是否有自己的存储空间。
由于ccc[i][j]指向了ccc[i][j][0]的地址,所以虽然是个标签,我们也可以认为他是指向一个char 类型存储空间地址的类型。但切记切记,这家伙可不是存储空间。那么既然不是存储空间,怎么能确定ccc[i][j]的具体值呢。毕竟bbb[i][j]可是可以取到的。
这就得复读机一下了,数组内的存储空间之间的位置是固定的。也就是指向数组内不同存储空间的地址的差异是固定的。无非bbb[i][j]是保存了这些不同存储空间的地址,而ccc[i][j]只是个标签。标签不要紧。我们只要知道ccc的整体存储空间是哪,那么随后不一样可以计算出来吗?
还不清楚?抬出ARM汇编分析法。修改model.c文件如下:
#include <stdio.h>
#include <setjmp.h>
#include "value.h"
#if 1
char *bbb[5][6];
char ccc[5][6][7];
void f1(void){
bbb[1][2][3] = ccc[1][2][3];
}
#else
。。
#endif //本C文件尾部
反汇编,记得是只对model.c进行-c -o 形成model.o的操作,而objdump仅针对model.o ,则如下:
00000000 <f1>:
0: b480 push {r7}
2: af00 add r7, sp, #0
4: f240 0300 movw r3, #0
8: f2c0 0300 movt r3, #0
c: 6a1b ldr r3, [r3, #32]
e: f103 0203 add.w r2, r3, #3
12: f240 0300 movw r3, #0
16: f2c0 0300 movt r3, #0
1a: f893 303b ldrb.w r3, [r3, #59] ; 0x3b
1e: 7013 strb r3, [r2, #0]
20: 46bd mov sp, r7
22: bc80 pop {r7}
24: 4770 bx lr
26: bf00 nop
鬼话:我真不想arm反汇编,出来啥结果,不用想都知道。因为完全可以从我们的C代码的分析中获得。
注意: c: 6a1b ldr r3, [r3, #32] e: f103 0203 add.w r2, r3, #3 这里有一个从r3开始,对应32个偏移量位置读取数据的动作。读取上来是什么?数值,char类型,还是一个指针? 你可以在strb r3, [r2, #0]找到答案。 哦,r2里面存的是个地址,唉。当初读取上来的是个指针。因为r2 = r3 + 3,为什么?很简单 r3里面存放的是 bbb[1][2],对应bbb[1][2][3]的位置,自然是 bbb[1][2]里面的值 + 3,一个char 存储空间在这里,就是一个byte,一个地址差异,就是一个byte,所以bbb[1][2][0] 和bbb[1][2][3]的地址差了3嘛。而bbb[1][2]里面存放的地址指向的空间自然是bbb[1][2][0]
ldr r3, [r3, #32]的意思是说 ,从r3指向的存储空间 + 32个byte,再读取出来,读取出来的是个地址。那么r3就应该是 bbb这个数组所对应的空间的起始位置。为什么?
ARM这里是32位系统,一个指针类型的空间是4个byte。而我们bbb的数组,是[5][6],那么bbb[1][2]对应bbb[0][0]之间差了多少个存储空间? 6 * 1 + 2 = 8,所以实际地址变差是 8 * 4。
由于bbb是个数组,但存放的是char 类型的空间的地址,也就是char *这个指针。所以要访问bbb[1][2][3]必然有个二次读取的动作。这和bbb[1][2]有自己的空间是对应的。因此我们可以称 char * bbb[5][6];等同于二级指针类型。
注意读取ccc[1][2][3]的动作, 12: f240 0300 movw r3, #0 16: f2c0 0300 movt r3, #0 1a: f893 303b ldrb.w r3, [r3, #59] ; 0x3b 先不要关注12,和16这两个指令地址的内容。仅就 ldrb.w r3, [r3, #59] 从strb r3, [r2, #0] 可以看出,r3里面的值需要保存在 bbb[1][2][3],那么你可以理解,r3里面存放的是个char 的值,而不是什么地址,指针。但是, ldrb.w r3, [r3, #59] ,[]里的,r3可是货真价实的地址,此时[]里的r3的值是什么?没错,就是ccc[0][0][0]的地址。也就是ccc这个存储空间里存放的值。为什么是 r3 = [r3 + 59]? 太简单了。 ccc[1][2][3]和ccc[0][0][0]两个空间的地址偏差是多少?
sizeof(char) * (1 * 6 * 7 + 2 * 7 + 3),sizeof(char)前面说过,就是1,那么后面的计算,想都不用想,一定是59.
我们把上面model.c的#else注释掉。保证model.c所有代码可用。如下处理 arm-linux-gnueabi-gcc attr.c value.c model.c control.c view.c -o attr arm-linux-gnueabi-objdump -d attr >> debug 在debug文件中,找到f1这个函数的反汇编,如下
00008990 <f1>:
8990: b480 push {r7}
8992: af00 add r7, sp, #0
8994: f641 2344 movw r3, #6724 ; 0x1a44
8998: f2c0 0301 movt r3, #1
899c: 6a1b ldr r3, [r3, #32]
899e: f103 0203 add.w r2, r3, #3
89a2: f641 1370 movw r3, #6512 ; 0x1970
89a6: f2c0 0301 movt r3, #1
89aa: f893 303b ldrb.w r3, [r3, #59] ; 0x3b
89ae: 7013 strb r3, [r2, #0]
89b0: 46bd mov sp, r7
89b2: bc80 pop {r7}
89b4: 4770 bx lr
89b6: bf00 nop
你不要介意 8990等字样,也不要介意#6512这个字样,倒是你要介意 89a2: f641 1370 movw r3, #6512 ; 0x1970 89a6: f2c0 0301 movt r3, #1 89aa: f893 303b ldrb.w r3, [r3, #59] ; 0x3b 和上一个例子中 12: f240 0300 movw r3, #0 16: f2c0 0300 movt r3, #0 1a: f893 303b ldrb.w r3, [r3, #59] ; 0x3b 的差异,怎么同样的逻辑,甚至汇编语句相同 ,而值不同?重复强调一边,这就是编译和链接的差异。由于ccc,bbb都是外部存储空间,编译器,即便针对同一个C文件里的内容,但不会对全局存储空间这类外部存储空间和函数内部的访问这些存储空间的内容进行绑定。地址绑定的事情是链接的工作。
鬼话:之所以,又强调一次编译和链接,是应为,竟然见过写C代码几年的研究生应届毕业生,不理解编译和链接的差异。但如果你还没有被公司扔到垃圾篓里,即便你本科刚毕业一年,实实在在的参与了C的开发,你也会知道编译和链接的差异。这就是应试的悲哀。考试过了,老师轻松了,回头落到公司里,都要如我野鬼这样的货色才帮他们擦屁股。而这些本应该是在学校里学习到的。
回到正题,正题,现在是否理解,bbb,ccc的相似处,和差异处了? 那么我们现在要注意 一下,虽然,bbb[1][2],和ccc[1][2]逻辑上是相同的。而且我们说了,ccc[0],ccc[1]也是标签,但是否bbb[1]和ccc[1]逻辑上相同?
当然不同。一个表示二级指针,bbb[1]是个标签,指向,bbb[1][0]的空间地址。而该空间地址指向的一个char 类型的空间的地址。 ccc[1]仅是个一级指针。ccc[1]是个标签,指向的是ccc[1][0][0]的空间地址。
哇塞,估计你要跳起来了。你发现了一个重大的特性。 ccc[1]和ccc[1][0]都是个一级指针,他们是完全一样的 。我呸!,你试试?看看编译器是否给你warning。
鬼话:确实,用我野鬼的教育概念,ccc[1]和ccc[1][0]都是一级指针,而且是标签型,不需要有存储空间存放地址,但是,ccc[1]和ccc[1][0]所指向的空间类型不同。
ccc[1]指向的空间对应的是一个包含了[6][7]的空间,而ccc[1][0]指向的空间对应的是一个包含了[7]的空间。且不谈空间的内部逻辑问题。他们的空间宽度就不一样。也即 ccc[1] 的地址,是针对 ccc[0],差异了,6 7sizeof(char) 而ccc[1][0]的地址,是针对了 ccc[0][5],差异了, 7 *sizeof(char),或许 ,ccc[1][1]和ccc[1][0]的差异你更好理解。
为什么我强调,ccc[1],ccc[1][0],我们可以看一下另一个指针类型,如下 char (p)[6][7]; 这是什么?记得有()所以要先读它。p是一个指针存储空间,他指向哪?指向一个存储空间,而这个存储空间的一个[6][7]的二维数组,每个单元是char类型。此时,p[x]就等同于ccc[x],对应的, char (p1)[7]; 那么 p1[x] 就等同于ccc[y][x];y未定。 可是, char (pp)[7]; 这可和 char (p1)[7]; 不同。明显的嘛,这是个二级指针,最终指向的,和ccc[y][x]一样。但是,如果你认为pp[y][x]和ccc[y][x]一样,除非你认为 char ppp; 和 char ccc[5][6][7]; 一样。显然不对。
我们,总结一下:
数组和指针的差异,在于数组存在空间申请。并且单元之间的位置是明确的。 由此引发几个差异: 1. 多级指针,每级指针的存储空间内指向的地址,需要你另行获取。而数组,不存在这个问题,始终只会有个一级的指针标签的表述形式。 2. char *pp ,和char p是不一样的,一个二级,一个一级,但实际pp和p的空间类型是一样的,都是指针类型空间,这是有户口的,实实在在有存储区域的。c[0]和c[0][0]虽然也是不一样,虽然他们都是一级指针标签,这可是没户口的,同时指向的空间类型不一样。
由于c[0]指向了一个 [6][7]的空间,那么实际对这个空间的具体字符类型的数据存储单元的或许,需要用c[0][i][j]来索引。由此导致,c[0][i][j]和ppp[0][j][j]的书写方式相同。但完全是两会事。
也真是 c[0] ,c[1][2],c[1][2][3]不是同个东西,虽然前两者都是一级指针标记。因此我们对数组,有了维度对概念。因为c[0]指向的是一个[6][7],c[1][2]指向的是个[7]的空间嘛。因此,char c[5][6][7];我们可以非常教科书的称,此事三维。
鬼话:现在大家知道我为啥自创,指针是分级的概念了吧,X级 。当然不代表你到18岁后才能理解这些。
OK。数组将完了。为了检验你的理解能力,理解我的鬼话,下面给出一个鬼例子,尝试自己判断每个*这个取地址所指向空间的内容操作,究竟做了,什么?
char ***ppp;
char ccc[5][6][7];
*(*(*(ppp + 1) + 2) + 3) = 1;
*(*(*(ccc + 1) + 2) + 3) = 1;
他们的操作步骤都是一样的吗?回答很简单。答不上来,把这个加如代码,反汇编嘛。
那么对应的数组的初始化怎么操作?与其我重复,不如看参考文献1 ,6.7.9,有大量的介绍。不过关注一下6.7.9.32。例子如下:
char s[]="abc";
char t[3] = "abc";
"abc"是什么?参考文献1 6.4.5 特别是6.4.5.3摘抄如下:
A character string literal is a sequence of zero or more multibyte character enclosed in double-quotes,as in "xyz"。
这里说了两个内容,其实听对应上面两个例子。你可以尝试 sizeof一下。例如这样的代码
char a[] = "abc";
char b[3] = "abc";
printf("%d,%d,%d\n",sizeof(char),sizeof(a),sizeof(b));
sizeof很好用。此处使用了类型,也使用了申请的空间名。两种方式。你甚至可以 char c[3][4][5]; sizeof(c[1]); 如果不出岔子,这应该等于20.意思是,c[1]这个标记的地址对应的空间类型,是一个[4][5]的char类型。这也是为什么我说c[1]等等是个一级指针的标记。因为本身并不存在一个指针空间存放这个地址。在编译阶段就会被确认。这也是重复,数组和指针的一个差异。
现在a,b不同。a比b多了一个char .存什么? of zero。a实际存放的是 a[0] == 'a' a[1] == 'b' a[2] == 'c' a[3] == 0 ‘a'表示a这个键盘字符。实际存如a[0]的仍然是个整数。具体值多少,你可以查看ASCII表。无非ASCII表里的很多字符都很直观,因此你可以使用 ‘’来显示的描述出来。例如空格 ' ',当然对应的ASCII的值是32(10进制),同时 '0'对应的也有个数值,不过不是0. 0对应什么?有其含义,你可以参考ASCII表。而在语言里,如果你是个字符串,则用于标记这个字符串的末尾。
鬼话:可以这么说,字符串的描述为,存放0这个值的空间,及其前向连续且不包含存储0的整体空间。
注意,这里字符串说的是一个包含逻辑关系的连续空间。这个逻辑关系是,有且仅有一个0,且其前向连续个非0的空间。这和指针,以及数组差异都很大。指针,强调的是,我这个存储空间内的值是用于指向一个地址的。数组强调的是一个整体空间,无非里面有固定大小的单元。
上面两个谈来弹去,都会设计到地址。而字符串,相反,并不设计地址,却涉及结尾,必须是个0。那么我们以下集中情况。 char a[5] = {1,2,3,4,0}; 那么我们从a[0]这个存储空间来看,直到a[4]这个存储空间,符合上面的条件,那么就是个字符串。你别怀疑我排版有问题,难道 char a[5] = {'1','2','3','4',0};才是字符串?无非一种是可以显示的,一种是无法显示的。 那么我们从a[4]开始,到a[4]这个存储空间本身结束,这个是不是字符串?没错也是。无非是没有实际内容,只有个0的字符串。我们可以称位空串。这和 "",描述一致。就是两个 double-quote 之间没有任何东西。
这里还需要强调一个事实,即便 a[0]开始,到a[4],和从a[1]开始到a[4](都包含a[4]),他们在同一个区域,但我们仍然称他们是两个字符串。差异在哪?废话,当然在开头。
鬼话:不是我刻意不按照教科书的方式讨论。而是实际开发中,很多字符串操作的代码,是使用上面的思维来进行的,而不是框定死你,每个不同的字符串有不同的存储空间。
那么我们确定一个字符串,则需要两头入手。一个是屁股,0嘛。一个是头,从屁股开始向前连续不为0的空间有很多。这些空间里,哪个被指引了,则对应空间的地址,注意是地址不是空间本身,则用于标记这个字符串。所以通常字符串的引用使用char *的指针来描述。例如
char a[5] = {1,2,3,4,0};
int strcmp(const char *s1, const char *s2);
你可以从参考文献7.24.4.2找到。我们可以如下实现
int strcmp(const char *s1,const char *s2){
while (*s1 == *s2){
if (p1[0]){
}else{
return 0;
}
s1++;
s2++;
}
return *(unsigned char *)(s1) - *(unsigned char *)(s2);
}
上面的strcmp完全可以这样调用,当然返回肯定不会都是0。 strcmp(&(a[1]),&(a[2])); 从字符串的C标准库的定义,你可以参考听下一个极端的鬼话
鬼话:字符串,就是个常量性的char 类型指针。上面const表示常量约束,意味着你在代码中,不能书写 s1[0] = xx;这种语句,const表示随后的存储空间禁止修改。当然你非要写,编译器只有找你麻烦。为什么说是常量性的,一旦你修改,意味着,这个字符串变了,不单单这个存储空间的内容变了,甚至当你对一个非0的空间改为0 ,存0的空间改为非0,这个字符串的存储空间都变了。
那么它的长度,是动态确认的。就是需要对这个指针所指向的空间,开始判断。由此C标准库给出了一个函数strlen,用于判断一个字符串就的函数。参考文献1 7.24.6.3 size_t strlen(const char *s); 那么我们可以如下实现
size_t strlen(const char *s){
size_t re = 0;
while (*s++){
re++;
}
return re;
}
鬼话:别和我纠结效率,这里只是我随手写的说明strlen原理的代码。只考虑逻辑不考虑效率,如上面的strcmp。
那么strlen(&(a[0])) 和strlen(&(a[1]));显然不一样,也确实不一样。但看下逻辑,如果s一开始所指向的空间里面存的就是0,虽然s被++了,但是re并没有,返回是0,因此 strlen(&(a[0]));实际返回的是3.含义是,这个字符串包含3个有效字符。而对应实际这个字符串占用了4个char类型的存储空间,还有个屁股嘛。
看一下另一个定义, char t[3] = "abc"; 如果你尝试 strlen(&(t[0]));会是什么结果?不好意思,我不知道,我只知道肯定大于2就是了。编译器也不知道。如果你认为你运行可以得出3这个数值,我只能说,你运气好,而且优势这种运气会一直好,直到你改变了编译选项或者换了另一个机器,或者和其他模块联合起来运行。
为什么这么说,因为,t的存储空间只有3个char类型的单元。而随后的空间是否存放0,和t没有关系,和 char t[3] = "abc";也没有关系。运气好,t[2]后面的空间里是0,那么你返回3,如果不是,则会继续向后查找。编译器看到t[3]也只是给t分配3个char类型空间的单元,而不会在意后面是什么。这是和 a[]分配的区别。a[]里面没有内容,表示空间大小由后面指定。默认,"abc",我们都看作字符串。自然有 0这个屁股咯。
注意一点,我始终用了&(a[0]),的写法,你完全可以按照教科书的内容写成 &a[0],这是没错的。但,好记性不如好习惯。养成用()分割逻辑顺序的习惯,将有助于你降低代码逻辑错误。
鬼话:见过太多,理论一说,就一套一套的应届毕业生,且不谈整天拿C语言做研究的学院派。但凡真落到实际工作中,思想进入业务,也就是设计逻辑本身,理论的东西,一走神就忘掉了。于是,诸如strlen(&(t[0]));这种连我也不知道答案的错误,实实在在的就摆那了。而且通常都会在本地测试环境中通过。废话,不通过自己就去查了。而这些随机性错误(绝大多数是由于对未知空间的利用,由于未知空间内存放的值不确定导致的随机错误)在debug时,查找的难度是很大的。因此,愿意听学院派的,证明自己考试满分的,我也不介意,摔坑里时,记得别喊我就行。
这里,非常建议你,将参考文献1的 7.24全部看完。里面涉及字符串操作的很多内容。他们对应的头文件都是<string.h>。不过可能对于新手,会有个郁闷的事情,为什么有这个函数? int memcmp(const void s1, const void s2, size_t n); 这不是内存空间的比较吗?和字符串有什么联系?
当然有联系,不妨我说差异,唯一差异就是,字符串对 0作为判断比较结束点。而memcmp对n,作为判断比较结束点。我随手写出memcmp的函数内容,你可以对比下和strlen的差异。(随手的意思,不代表实际你用到的对应函数的内部代码如此设计)
int memcmp(const void *s1, const void *s2, size_t n){
const unsigned char *p1 = (const unsigned char *)s1;
const unsigned char *p2 = (const unsigned char *)s2;
while (*p1 == *p2){
if (--n){
}else{
return 0;
}
p1++;p2++;
}
return *p1 - *p2;
}
注意,这里使用了--n,前缀--,意思是我先减了,在比较是否位0。由于memcmp仅正对指针,所以不失一般性的使用void ,毕竟比较内存空间嘛,我们不在乎实际指向空间的类型。但话又回来,实际比较,还是要取值的呀,你不告诉编译器,指针指向的空间地址是什么类型,它怎么知道该怎么取值。由此我们用p1 ,p2来替换s1,s2。(const unsigned char )是强制类型转换的意思。现在知道了吧,字符串究竟是什么?无非是一个
这里扩展讨论一下为什只知道地址,不知道类型,无法实际对地址的内容取值。说个例子。 int a = -3; char b = -3; int t; 假设,int 是 32位。对于 t = a ; 这个很简单那。取个32位,从a中,放到t中。 t = b; 这个就不简单。通常机器指令会有以下动作。 ld r0,[b] //意思是load 读取, b这个存储空间里值,到r0中。 exp {b,w} r0l,r0l exp {w,dw} r0,r0l exp的意思是扩展,将最高位,扩展到更高的位中,那么对于 exp {b,w} r0l,r0l 意思是,把r0l (通常32位寄存器,我们用r0l表示低16位,r0h表示高16位)第8位中的最高位,也就是第7位的值,扩展到高8位,也就是
r0中,从第8位到第15位,如果 b里存放的是个负数,那么根据补码的规则,第7位是1,则高8位都是1.反之都是0.
exp {w,dw} r0,r0l的意思是,把r0l这个16位的最高位,扩展到r0的高16位中,就是第16位到第31位。同理,此时 r0 ,从第31位到第7位都是同样的值。这样有啥好处?
对于int类型, 129是存在,而对于char 类型,129实际补码表示 -127,为了让-127能保存到32位中,按照补码规则,自然需要高位扩展。
注意上述是我随手写对伪汇编,不对应任何平台指令集。有些硬件平台,一条指令帮你都做完了。可能在 ld动作中,就有。但是实际硬件电路设计是,上述动作,还是都存在的。
那么如下三个语句就有不同的概念。
t = (unsigned char)a;
t = a;
t = (int)a;
后两者结果相同,但和第一个情况不同。你可以自己写代码验证一下。通过这个例子,我们不使用标准规范的内容来说明,你光知道指针指向的空间,但不知道这个空间的类型,也就不知道这个空间里的存放数据的使用方法(读和写)。
扩展说一下,强制转换。我始终强调,写C代码,要有空间概念。空间的分布在脑子里存在,那么我们就可以有效利用硬件进行逻辑设计。例如memcpy完全可以如下写
int memcmp(const void *s1, const void *s2, size_t n){
const unsigned char *p1 = (const unsigned char *)s1;
const unsigned char *p2 = (const unsigned char *)s2;
const int *pi1 = (const int *)s1;
const int *pi2 = (const int *)p2;
while (n >= 4){
if (*pi1 != *pi2){
p1 = (const unsigned char*)pi1;
p2 = (const unsigned char*)pi2;
break;
}
n -= 4;pi1++;pi2++;
}
if (n == 0){
return 0;
}
while (*p1 == *p2){
if (--n){
}else{
return 0;
}
p1++;p2++;
}
return *p1 - *p2;
}
上述做法的好处是,我们可以一次比较4个byte。如果n是大于4的情况。如果我们要比较的长度是1027个。即便最终会有3个依次比较(假设前面1024个都是相等的),但还是比按照char类型位宽的方式进行比较快很多。
强制转换也叫类型转换,别怕别怕,你知道空间里的逻辑就不怕了。
鬼话:通常,见到胆大的,是不知道空间逻辑也转换。更大胆的是,连转换都没有,报出一堆warning。或许学院派会整天教育你,操作优先级,我到希望你忘记操作优先级,用()来描述,而更多琢磨琢磨空间逻辑,和类型转换的问题。你就这一个脑子,就那么点时间,学点可以少抓BUG,提早下班的事情不好吗?
OK。字符串基本说完,余下我们需要折腾MVC了。记得我们还没有读取参数文件,并对参数文件进行识别处理呢。
上一篇:数组,指针,字符串(上)
下一篇:数组,指针,字符串(下)