16、混淆指针与数组的问题

下面的程序输出什么?

#include <stdio.h>    //main.c

extern char* g_name;   //使用define.c定义的全局数组g_neme

int main()

{ define_print();

printf("main():%s\n",g_name);

return 0;

}

#include <stdio.h>  // define.c

char g_name[]="d.t.sofetere";

void define_print()

{ printf("define_print(): %s\n", g_name);}

是应该打印两行字符数组吗?

实例分析:指针==数组?

define_print(): d.t.sofetere

段错误

extern char g_name[];

这样就没错了。

再论指针和数组:

指针:

本质为变量,保存的目标值为内存地址。指针运算与*操作符配合使用能够模拟数组行为。

数组:

数组是一段连续的内存空间。数组名可看做指向数组第一个元素的常量指针。 

在c语言层面中的等价关系:

int a[3]={0}; 数组本质是一段连续内存空间的别名

int* p=a;

p[0]=1;  //a[0]=1

p[1]=2;  //a[1]=2

p[2]=3;  //a[2]=3

问题:在二进制层面,指针和数组是否等价?

#include <stdio.h>
int test()
{
int a[3]={0}; 
int* p=a;
p[0]=1;  //a[0]=1
p[1]=2;  //a[1]=2
a[2]=3;  //a[2]=3
}
int main()
{
test();
return 0;
}

objdump -S test.out > test.s

反汇编:

int a[3]={0}; 
movl   $0x0,-0x10(%ebp)
movl   $0x0,-0xc(%ebp)
movl   $0x0,-0x8(%ebp)
int* p=a; //p是数组中的一段内存保存数组的起始地址
lea    -0x10(%ebp),%eax

mov    %eax,-0x4(%ebp)

p[0]=1; 
mov    -0x4(%ebp),%eax //将地址放到eax中
movl   $0x1,(%eax)    //把1放到eax保存的内存地址中

p[1]=2;  
mov    -0x4(%ebp),%eax //将p指针里边的地址取出来放到eax寄存器中
add    $0x4,%eax    //在eax保存的地址上边加4

movl   $0x2,(%eax)   //将2放到eax存储的内存地址中

a[2]=3;  

movl   $0x3,-0x8(%ebp) //直接放过来了

二进制层面,指针和数组是截然不同的。

在二进制层面,把指针当作数组使用,会多一次寻址。先把指针里边保存的地址拿出来,拿出来之后在进行对应的地址操作。而数组直接操作地址,没有寻址的操作。

结论:

指针与数组在二进制层面的操作完全不同。

指针操作: 先寻址,在对地址单元进行操作。

数组操作:直接对地址单元进行操作。

C/C++编译器的天生缺陷:

由4个子部件组成(预处理器,编译器,汇编器,链接器)。

每个子部件独立工作,相互之间没有通信。缺陷

语法规范只在编译阶段有效(如:类型约束,保护成员)。

编译器部件对各个源文件进行独立编译(认为源文件相互独立)。(对每个源文件都会编译出一个汇编文件出来,因为认为独立的,这个假设可是说是上边段错误的直接原因)

汇编器,链接器没有类型概念,这就是nm命令查看标识符时,查看可执行程序中的符号时,是看不到类型信息的。

问题本质:

file.c--根据当前源码产生二进制代码-->file.o(目标文件中的符号无类型信息)

如果:当前源码中声明的“指针”,按照指针方式编译--->链接器最终确定各个符号的地址。

file.c编译之后产生汇编文件file.s,汇编文件被汇编器处理后得到目标文件。

如果指针是外边定义的数组,那么按指针的方式编译,那么就出错了。

图15.2

验证:

#include <stdio.h> 
extern char* g_name;   
int main()
{
define_print();
//printf("define_print():%s\n",g_name);
printf("define_print():%c\n",*(char*)&g_name);//打印这个指针第一个字节里边的数据
printf("define_print():%c\n",*(char*)&g_name+1);
printf("define_print():%c\n",*(char*)&g_name+2);
printf("define_print():%c\n",*(char*)&g_name+3);
return 0;

}

./test.out
define_print(): d.t.sofetere
define_print():d
define_print():.
define_print():t
define_print():.

打印了d.t.

define.c里边g_name 定义为字符数组,main.c里边g_name表示四个字节,表现为指针,把指针当作数组使用的时候会多一次寻址,将这四个字节当作了数组的首地址来使用,意味着g_name[1]指的是将这四个字节偏移一个单位在进行访问的,起始g_name[1]表示应该是第2个字节,这样就发生问题了。

main.c里边把g_name看成是前四个内存地址的别名,g_name是指针类型,进行数组操作时,把这四个字节取出来,做寻址,当成内存地址,在对内存地址里边的内容进行操作,然而d.t.四个字符组成的内存地址是个野地址,将g_name 看成指针之后这个指针就是野指针了。

main函数中g_name是个数组,四个字节,对应与define中的数组的前四个字节。

在c语言相互等价是因为编译器为了高效的编程,做了一些工作,这些工作就使得我们使用指针数组有一样效果了。

解决方案:

尽可能不使用跨文件的全局变量(非static全局变量)

当必须使用时,在统一固定的头文件中声明(global.h)

使用跨文件全局变量的源文件直接包含global.h。

定义跨文件全局变量的源文件也需要包含global.h。

#ifndef _GLOBAL_H_
#define _GLOBAL_H_
extern char g_name[];

#endif

小贴士:声明和定义不同

声明只是告诉编译器,目标存在,可使用。

定义实际为目标分配内存(变量)或确定执行流(函数)。

理论上,任何目标都需要先声明,在使用。

c/c++语言允许声明与定义的统一。

#include <stdio.h>
extern int* g_var;
//int g_var=0;
int test()
{
int a[3]={0}; 
int* p=a;
p[0]=1;  //a[0]=1
p[1]=2;  //a[1]=2
a[2]=3;  //a[2]=3
}
int main()
{
g_var=1;
test();
return 0;
}

int g_var=0;

编码规范:组织公司自己定义的规则。避免错误。

小结:

c/c++语言中的指针与数组在部分情况等价。

指针与数组在二进制层面的操作完全不同。

c/c++编译器忽略了源码间的依赖关系。

在统一固定的头文件中声明跨文件使用的全局变量(global.h)

使用跨文件全局变量的源文件直接包含global.h。

17、volatile,让我保持原样

嵌入式开发中的常见情形:图

main board(主控板):上电,启动外设

external device ,外设初始化

外设启动成功后,初始化外设的时候,主控板处于等待的状态。等待glag标记设置

设计方案:

1、循环读取状态值2根据状态值处理事件-->全局内存空间<--根据设备内部状态变化设置状态值(外设)

device.c:

#include <stdio.h>
#include <pthread.h>
extern const int g_ready;
int main()
{
launch_device();
while(g_ready==0)
{
sleep(1);
printf("main()-launching device:g_ready= %d\n",g_ready);
}
printf("main()-device status:g_ready= %d\n",g_ready);
return 0;
}

main.c:

#include <stdio.h>
#include <pthread.h>
extern const int g_ready;
int main()
{
launch_device();
while(g_ready==0)
{
sleep(1);
printf("main()-launching device:g_ready= %d\n",g_ready);
}
printf("main()-device status:g_ready= %d\n",g_ready);
return 0;

}

gcc main.c device.c -lpthread -o test.out  多线程库

调试板:

delphi@delphi-vm:~/make$ ./test.out
main()-launching device:g_ready= 0
main()-launching device:g_ready= 0
main()-launching device:g_ready= 0
main()-launching device:g_ready= 0
init_device()-device status:g_ready=1
main()-launching device:g_ready= 1

main()-device status:g_ready= 1

这是调试板,往往发布版会加上优化选项。

gcc -O3 main.c device.c -lpthread -o test.out  加上优化选项

加上优化程序不停的循环,

问题分析:

编译优化时,编译器根据当前文件进行优化。

编译器能够发现变量“不可能”被改变(const 全局变量)

为了效率上的提高,编译将变量值进行缓冲。

缓冲的方式为:把变量值从内存中读取进入寄存器。

每次访问变量时直接从寄存器读取对应值。寄存器的访问速度高于内存。

g_ready变量在main函数中没有被赋值,被编译器看成了常量。另一个线程改变了g_ready,然而这个改变不会反映到寄存器中,于是,main函数中认为g_ready没有被改变。

解决方案:

使用volatile修饰可能被“意外”修改的变量(内存):

volatile修饰的变量是一种“易变的”变量。

volatile可理解为“编译器警告指示字”

volatile告诉编译器必须每次去内存中取变量值。

改:

volatile int g_ready=0;

extern const volatile int g_ready;

在嵌入式开发中,要考虑是不是要用volatile来修饰全局变量

问题:

如何理解const(定义值不会改变的标识符)和volatile(修饰易变的变量)同时修饰变量?在语义上是否矛盾?

const和volatile:

const表示被修饰的变量不能出现在赋值符号左边。

volatile表示使用变量时直接从内存取值。

const和volatile同时修饰变量时互不影响其含义。

const(变量var不能出现在赋值符号左边,var是一个只读变量) volatile(使用变量var时直接从内存取值,不进行任何方式的优化) int var =1;

int main()
{
const int var=1;
//var=2;  // 不通过
int* p=(int*)&var; 
*p=2;  //能通过
printf("var=%d\n",var);
return 0;

}

不矛盾。

在main.c中  extern const volatile int g_ready; 有const

在debice.c中:volatile int g_ready=0;  没有const

声明时有,定义时没有

意味着这个全局变量仅仅在当前文件中有只读属性,仅仅在当前文件中不能出现在赋值符号的左边。

小结:

编译优化时,编译器只根据当前文件进行优化。

编译器的优化策略可能造成一些“意外”。

volatile强制编译器必须每次从内存中取变量值。

const和volatile同时修饰变量时互不影响其含义。

猜你喜欢

转载自blog.csdn.net/ws857707645/article/details/80923051
今日推荐