下面的程序输出什么?
#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同时修饰变量时互不影响其含义。