日志标签 ‘C编程’

exec族函数

2011年3月14日

通常我们使用了fork函数创建了一个子进程后,子进程会调用exec族函数执行另外一个程序。由于fork函数新建的子进程和父进程几乎一模一样(不过,后来引入了写时复制技术避免了这一点),因此需要使用exec函数使得子进程执行一个新的可执行程序。

我们之所以称exec为族函数是因为它有6种不同的形式,主要区别体现在参数上:

#include <unistd.h>;
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
             ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[],
              char *const envp[]);

总体来说,exec族函数的参数分为三类:文件路径,命令行参数,环境变量。不论那个exec函数,最终都是将上述三类参数传递给可执行程序中的main函数。这里有一个新的概念,即环境变量,它定义了用户的工作环境,包括用户的主目录,终端的类型,当前用户,当前目录等信息。事实上,main函数的完整形式应该是:

int main(int argc, char *argv[], char **envp);

通常我们并没有显示指定环境变量,这是因为main函数每次都默认使用系统自定义的全局变量environ。另外,通过在终端输入env命令,也可查看当前的所有环境变量信息。可以看到,main函数中的argv[]和envp与exec函数的参数有对应关系。

初次接触exec族函数,易被这6种不同的调用形式搞得混乱。下面我们将依次从上述三个参数的角度来区分这些函数的细微区别。

1.可执行文件的路径

以p结尾的exec函数可以直接传递可执行程序的文件名,此时这类函数会自动在PATH环境变量所指定的目录下查找这个可执行文件;非p结尾的exec函数则必须显示指定可执行程序的完整路径;

2.命令行参数

以execl开始的exec函数必须以列表的形式传递所有的命令行参数,并且最终以NULL作为命令行参数的结束;而以execv开始的exec函数则以字符串指针数组的形式传递所有命令行参数;

3.环境变量

以e结尾的exec函数必须显示指定环境变量;而非p结尾的exec函数则使用默认的环境变量,也就是main函数中的环境变量;

虽然exec函数有6种不同的调用形式,但是每个函数实现的功能都是一样的,即完成新的可执行程序的装入。下面的这个程序很好的演示了不同exec函数的使用方法:

#include < unistd.h >
#include < stdio.h >

int main()
{
	char *envp[]={
		"PATH = /tmp",
	        "USER = edsionte",
		NULL
	};

	char *argv_execv[] = {"echo", "excuted by execv", NULL};
	char *argv_execvp[] = {"echo", "executed by execvp", NULL};
	char *argv_execve[] = {"env", NULL};

	if (fork() == 0)
		if(execl("/bin/echo", "echo", "executed by execl", NULL) < 0)
			perror("Err on execl");
	if(fork() == 0)
		if(execlp("echo", "echo", "executed by execlp", NULL) < 0)
			perror("Err on execlp");
	if(fork() == 0)
		if(execle("/usr/bin/env", "env", NULL, envp) < 0)
			perror("Err on execle");
	if(fork() == 0)
		if(execv("/bin/echo", argv_execv) < 0)
			perror("Err on execv");
	if(fork() == 0)
		if(execvp("echo", argv_execvp) < 0)
			perror("Err on execvp");
	if(fork() == 0)
		if(execve("/usr/bin/env", argv_execve, envp) < 0)
			perror("Err on execve");

	printf("goodbye!\n");
	return 0;
}
//结果:
edsionte@edsionte-desktop:~/edsionte$ ./sys_process
goodbye!
edsionte@edsionte-desktop:~/edsionte$ executed by execl
excuted by execv
executed by execvp
executed by execlp
PATH = /tmp
USER = edsionte
PATH = /tmp
USER = edsionte

这个程序分别使用不同的exec函数调用了echo或env命令,并根据所传递的不同参数显示不同结果。该程序可能每次运行后的显示顺序都有所不同,这是进程的并发性所导致的。

值得注意的是,我们通常所说的命令行参数是以argv数组的第一个元素开始的,而第0号元素是可执行程序的名称。以第一个if语句为例,execl函数为/bin目录下的echo程序传递了两个参数:”echo”和”executed by execl”。由于我们通过execl函数已经装入了/bin/echo程序,因此在调用execl函数时,第一个参数”echo”可以替换成任意字符串,但是这并不代表就不需要传递该参数。

我们也可以让exec函数去执行我们自己编写的可执行程序。下面这个例子很好的演示了父进程和子进程的关系:

/*
 *Author: edsionte
 *Email:  edsionte@gmail.com
 *Time:   2011/03/14
*/

#include < stdio.h >
#include < unistd.h >
#include < stdlib.h >

int main()
{
	int pid;
	int val = 0;

	pid = fork();
	if (pid == 0) {
		//child proces;
		printf("[Child %d] I will be a new process!\n", getpid());
		if (execl("./sleeping","first arg", "20", NULL) == -1) {
			printf("execl error!\n");
			exit(1);
		}
		printf("[Child %d] I never go here!\n", getpid());
	} else if (pid > 0) {
		printf("[Parent %d] I am running!\n", getpid());
		/*
		wait(&val);
		printf("[Parent %d] All my child were leaving..\n", getpid());
		*/
	} else {
		printf("fork error!\n");
		exit(1);
	}

	printf("[process %d] I am leaving..\n", getpid());
	return 0;
}
//sleeping.c程序
#include < stdio.h >
#include < unistd.h >

int main(int argc, char** argv)
{
	int sec;

	sec = atoi(argv[1]);
	while (sec != 0) {
		sleep(1);
		printf("[New Child %d] I am running and my parent is %d\n", getpid(), getppid());
		sec--;
	}

	return 0;
}

该程序使用了与进程有关的四个最基本的系统调用函数:fork(),exec(),wait()和exit()。对于该程序的具体分析如下:

1.父进程通过fork函数创建子进程,然后父进程打印自己的pid;

2.子进程首先打印自己的pid,再通过execve函数装入可执行程序sleeping,并通过execve函数向sleeping传递了一个参数“20”;

3.在sleeping程序中,将主函数中传递的参数作为计数器,并依次循环睡眠20s。并且循环打印当前的sleeping进程的pid以及其父进程pid;

4.由于父进程先于子进程退出,则sleeping进程成为孤儿进程,因此在sleeping打印的父进程pid为1;如果去掉程序中的注释部分,则父进程等待子进程退出后再退出,那么sleeping所打印的父进程pid即为父进程的pid;

5.如果子进程执行exec函数成功,则主程序中最后一条语句就不会被子进程执行。说明子进程在执行了exec函数后就与父进程完全脱离。另外,在子进程执行过程中也可以通过ps命令查看当前的进程存在情况;

参考:

1.http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part3/

2.Linux C编程实战;童永清 著;人民邮电出版社;

多维数组那回事儿

2011年2月10日

前面几篇“那回事儿”的文章更强调一维组和指针之间的关系,本文关注的是多维数组,即“数组的数组”。

多维数组

我们可以将多维数组抽象的看作是具有某种类型的一维数组。当“某种类型”为基本的数据类型时,多维数组就退化成普通的一维数组;当“某种类型”仍然为数组时,那么就形成了多维数组。也就是说任何一个多维数组都可以分解成几个一维数组。

下面通过示例程序来深入了解多维数组ma[2][3]的构成。

#include < stdio.h >

int main()
{
	int ma[2][3];
	int (*r)[2][3];
	int (*p)[3];
	int *t;

	/*代码段1*/
	p = ma;
	printf("sizeof(ma[0])=%d\n",sizeof(ma[0]));
	printf("ma      =%p\tp   =%p\n",ma,p);
	printf("p+1 =%p\n",p+1);
	/*代码段2*/
	r = &ma;
	printf("sizeof(ma)=%d\n",sizeof(ma));
	printf("&ma     =%p\tr  =%p\n",&ma,r);
	printf("&ma+1   =%p\tr+1=%p\n",&ma+1,r+1);
	/*代码段3*/
	t = ma[0];
	printf("sizeof(ma[0][0])=%d\n",sizeof(ma[0][0]));
	printf("ma[0]   =%p\tt   =%p\n",ma[0],t);
	printf("ma[0]+1 =%p\tt+1 =%p\n",ma[0]+1,t+1);
	return 0;
}

由多维数组ma最左维的长度2可知,ma数组包含两个元素ma[0]和ma[1]。数组名ma在表达式中是数组ma首元素的首地址。在代码段1中将ma赋值给数组指针p,则p指向多维数组ma的首元素ma[0],则p+1指向第二个元素ma[1]。其中p是一个数组指针,它指向一个长度为3的数组,则指针p每次移动的偏移量为12。可参考下图:


在代码2中对ma取地址并将其赋值给指针r。r现在指向一个“第一维的大小为2,第二维的大小为3的数组”,则r+1将指向下一个这样的数组(尽管这样的数组并不存在)。由此也可得知r每次的偏移量为24。


ma[0]和ma[1]都是一个长度为3的整型数组,现在以ma[0]为例进行说明。ma[0]中包含三个元素ma[0][0],ma[0][1]和ma[0][2]。在代码段3中将ma[0]赋值给t,则t指向数组ma[0]的第一个元素a[0][0],则t+1和t+2则依次指向第二个元素和第三个元素。


对多维数组ma的结构有了一定了解后,现在再看上述程序的运行结果:

edsionte@edsionte-laptop:~/code/expertC$ gcc array.c -o array
edsionte@edsionte-laptop:~/code/expertC$ ./array
sizeof(ma[0])=12
ma   =0xbfdfaa6c	p=0xbfdfaa6c
p+1  =0xbfdfaa78
sizeof(ma)=24
&ma  =0xbfdfaa6c	r=0xbfdfaa6c
r+1  =0xbfdfaa84
sizeof(ma[0][0])=4
ma[0]=0xbfdfaa6c	t=0xbfdfaa6c
t+1  =0xbfdfaa70

注意在结果中,p,r和t的值均相同,但是所指向的数据却不同。更具体的说,这三个指针每次移动时的偏移量不同。

多维数组的初始化

数组的初始化只能在对数组进行声明(具体为定义型声明)时进行。一维数组的初始化很简单,只要将所有初始值放在一个大括号中即可。如果声明数组时未指定数组的长度,则编译器会根据初始值的个数来确定数组的长度。

#include < stdio.h >

int main()
{
	int m[] = {1,2,3};
	int n[] = {1,2,3,};

	printf("length(m)=%d\n",sizeof(m)/sizeof(m[0]));
	printf("length(n)=%d\n",sizeof(n)/sizeof(n[0]));
	return 0;
}

/* 编译并运行 */
edsionte@edsionte-laptop:~/code/expertC$ gcc init_array.c -o init_array
edsionte@edsionte-laptop:~/code/expertC$ ./init_array
length(m)=3
length(n)=3

注意,在最后一个初始值后面可以继续加一个逗号也可以省略,这并不影响数组的长度。

对于多维数组而言,通常使用嵌套的大括号进行多维数组的初始化。由于多维的数组其实是有若干个一维数组构成的,则每个大括号都代表一个一维数组。对于多维数组而言只能省略最左边 下标的长度。

#include < stdio.h >

int main()
{
	int b[][3] = {1,2,1,1};
	int c[][3] = {{1,2,1},{1,2,3},};

	printf("length(b)=%d\n",sizeof(b)/sizeof(b[0]));
	printf("length(c)=%d\n",sizeof(c)/sizeof(c[0]));
	return 0;
}

/* 编译并运行 */
edsionte@edsionte-laptop:~/code/expertC$ gcc init_array.c -o init_array
edsionte@edsionte-laptop:~/code/expertC$ ./init_array
length(b)=2
length(c)=2

可以看到,不使用大括号也可以对多维数组进行初始化,只不过代码可读性较差。

它总是迷惑你!

一旦涉及到多维数组,总有些让你迷惑的地方。比如:

	char ma[2][3][2]={
		{{1,2},{2,3},{3,4}},
		{{3,5},{4,5},{3,3}}
	};

sizeof(ma[0,1,1])=?

对于上面的代码,我们最后的迷惑点都可能落在ma[0,1,1]上。难道多维数组可以这样使用吗?如果ma[0,1,1]和ma[0][1][1]等价,那么sizeof(ma[0,1,1])的值就是1。很可惜这样的猜测是不正确的,正确答案为6。再比如下面的代码:

		char ma[3][2] = {
		(1,2),(3,4),(5,3)
	};

ma[0][0]=?

上述代码是为数组ma进行初始化,那么ma[0][0]的值是多少?恐怕很多人都会认为是1。不过正确答案是2。

这两个问题都涉及到了逗号表达式。如果你对逗号表达式有基本的了解,那么也就没有上述那种莫名其妙的迷惑了。根据逗号表达式的运算,对于举例1中的ma[0,1,1]实际上等价于ma[1];对于举例2中的初始化其实等价为char ma[3][2] = {2,4,3}。

参考:

《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;

 

指针和数组的可交换性

2011年2月7日

指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。《指针和数组不是一回事儿》系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于《指针和数组不是一回事儿》系列文章之三。

虽然前面两篇文章已经说明了数组和指针的不同,但不可否认的是,指针和数组某些可相互交换的用法仍然令人混淆。本文将给出指针和数组可交换的情景,并且分析可交换的原因。

“指针和数组可以交换!”

说出这句话并不是毫无根据的,因为在下面的两个举例中使用数组形式和指针形式都可以达到相同的结果。

举例1:

#include < stdio.h >

int main()
{
	char *p = "edsionte";
	char str[] = "edsionte";

	printf("p[1]=%c *(p+1)=%c\n",p[1],*(p+1));
	printf("str[1]=%c *(str+1)=%c\n",str[1],*(str+1));

	return 0;
}

/* 编译并运行程序 */
edsionte@edsionte-laptop:~/code/expertC$ gcc tmp.c -o tmp
edsionte@edsionte-laptop:~/code/expertC$ ./tmp
p[1]=d *(p+1)=d
str[1]=d *(str+1)=d

在举例1中,指针p指向一个匿名的字符串“edsionte”,这个匿名字符串的占用的内存空间为9个字节;与p指向一个匿名字符串不同,数组str内存储着字符串“edsionte”,占用了9个字节的空间。

现在分别要访问’d’,则方法如下。对于指针p,分别可以通过指针形式*(p+1)和数组形式p[1]来访问其所指的数据;对于数组str,分别可以通过指针形式*(str+1)和数组形式str[1]来访问数组内的元素。

我们已经知道指针和数组在内存构造和访问方式上都不同,但为什么它们都分别可以通过指针的方式和数组的方式进行访问?

举例2:

#include < stdio.h >

void getStr_pointer(char *str)
{
	printf("%s\n",str);
	printf("getStr_pointer(): sizeof(str)=%d\n",sizeof(str));
}

void getStr_array(char str[100])
{
	printf("%s\n",str);
	printf("getStr_array(): sizeof(str)=%d\n",sizeof(str));
}

int main()
{
	char str[] = "I am edsionte!";

	getStr_pointer(str);
	getStr_array(str);
	printf("main(): sizeof(str)=%d\n",sizeof(str));
}

/* 编译并运行程序 */
edsionte@edsionte-laptop:~/code/expertC$ gcc tmp2.c -o tmp2
edsionte@edsionte-laptop:~/code/expertC$ ./tmp2
I am edsionte!
getStr_pointer(): sizeof(str)=4
I am edsionte!
getStr_array(): sizeof(str)=4
main(): sizeof(str)=15

在举例2中,getStr_pointer函数和getStr_array函数的功能都是显示一条字符串。但不同的是,前者传入的参数是一个指针,后者传入的参数是一个数组。在主函数中分别调用这两个函数,传入的参数都是数组str。

既然数组和指针不同,但为什么作为函数的形参,char str[ ]和char *str相同?

上述举例所引出的这两个问题正是本文讨论的重点,它们分别对应着“指针和数组是相同”的两种情况。下面将分别进行讨论。

1.表达式中的数组名就是指针

表达式中的数组名其实就是数组首元素的首地址。对于编译器而言,a[i]其实就是*(a+i)的形式,因此以数组形式访问数组元素总是可以写成“数组首元素首地址加上偏移量”的形式。取下标符号[ ]其实可以看成一种运算规则,即指向T类型的指针和一个整数相加,最终产生的结果类型为T。这里的指针就为数组首元素首地址,而整数即为数组的偏移量。

这里必须说明一下偏移量,它是指针每次移动的步长。对于数组而言,偏移量即数组元素的大小;对于指针而言,它的偏移量即为指针所指类型的大小。在对指针进行移动时,编译器负责计算每次指针移动的步长。

因此,str[i]和*(str+i)两种形式其实是等价的。因为编译器总是将数组形式的访问自动转换成指针形式的访问。上面的分析都是针对数组而言,其实对指针以数组和指针形式访问的原理也是如此。只不过此时的访问是对指针所指向数据的访问。

结合数组和指针访问方式的不同,下面对举例1的代码做详细分析:

1.1.以指针的形式和以数组的形式访问数组

从符号表中得到符号str的地址即为数组首元素的首地址。

  • 以数组的形式:str[1]。从符号表中得到str符号的地址,即数组首元素的首地址;编译器将数组形式转化为*(str+1),在首元素首地址上加一个偏移量得到新地址;从这个新地址中读取数据,即为’d’;
  • 以指针的形式:*(str+1)。从符号表中得到str的地址,即数组首元素的首地址;在此地址上加一个偏移量得到新地址;从这个新地址中读取数据,即为’d’;

1.2.以指针的形式和以数组的形式访问指针

不管以何种方式访问,我们应该清楚p始终是一个指针。从编译器符号表中得到符号p的地址为指针p的地址。

  • 以指针的形式:*(p+1)。首先从符号表中得到p的地址;从该地址中得到指针p;对指针p加上1个偏移量得到新地址;从这个新地址中读取数据,即为’d’;
  • 以数组的形式:p[1]。首先从符号表中得到p的地址;从该地址中得到指针p;编译器将数组形式转化成*(p+1),对p加一个偏移量得到新地址;从这个新地址中读取新数据,即为’d’;

分析至此,你应该了解到以数组形式和以指针形式访问只是写法上的不同而已,其本质对内存的访问过程是一样的。

2.作为函数参数的数组名等同于指针

当作为函数形参时,编译器会将数组改成指向数组首元素的指针。此时的数组就等价于指针。之所以将传递给函数的数组形参转化为指针是处于效率的考虑。

在C语言中,所有非数组的实参数据都是以传值形式传递给函数的,即将实参的一份拷贝传递给调用函数中的形参,调用函数对这份拷贝(也就是形参)的修改不影响实参本身的值。如果按照这样的道理,传递数组时就必须拷贝整个数组空间,这样必然会产生很大的开销。并且,大部分时候并不会访问到数组中所有的元素而只是其中的几个。考虑到上述的原因,数组作为实参传递给调用函数时,只需将数组名传递给函数即可;而形参会被编译器该成指针的形式。因此,作为形参的数组既可以写成数组也可以写成指针。

现在再回到举例2中的代码,对于形参中的char str[]和char *str也就感到不再奇怪了。事实上,即便将形参写成char str[]或char str[100],编译器仍然会将它们改成char *str的形式。

既然任何数组作为形参时候都等价于一个指针,那么在函数内对“数组”的一切操作都等价于对指针的操作。验证这一点的很好例证就是举例2中对数组str求长度。在主函数中,sizeof(str)的值为15,这个结果毫无争议,它就是数组str的长度。而在getStr_pointer()和getStr_array()中,sizeof(str)的值都为4,也就验证了作为形参的数组str在调用函数中就是一个指针!在上述情况1中,虽然表达式中数组名也被认为是指针,但是数组仍然是数组(main函数中sizeof的结果就是很好的验证),而此部分数组就是指针。这也是数组等价于指针的唯一情况。

换句话说,虽然在将数组作为形参的函数中,你可以继续以数组的形式使用这个参数,但实际上你跟不可能找到数组的踪影!

总结

关于指针和数组之间的异同需要反复的思考和总结,才能搞清关系。下面对指针和数组之间的可交换性再作义简单的总结。

1.在表达式中以a[i]这样的形式对数组进行访问时,编译器总将其解释为*(a+i)的形式;

2.在数组作为函数的形参时,编译器将数组改写成指针,这个指针即为数组首元素的首地址。这也是数组等价指针的唯一情形;

3.由于2的原因,一个数组作为函数的形参时,既可以将数组定义成数组,也可以将数组定义成指针;

4.指针和数组永远是两码事,因此在不同文件中的声明和定义必须匹配,但却始终都能写成指针的形式和数组的形式(这完全是写法的不同)。

参考:

《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;

《C语言深度解剖》北京航空航天大学出版社;陈正冲 著;

指针和数组的访问方式

2011年2月5日

指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。《指针和数组不是一回事儿》系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于《指针和数组不是一回事儿》系列文章之二。

前文从内存结构的角度说明了指针和数组的不同,本文将以访问方式的角度再次说明指针和数组的不同。先看下面的代码:

char str[] = "edsionte";
char *p = "edsionte";

当编译完程序后,程序中的标示符都有一个地址,所有标示符的地址形成一个符号表。

数组的访问方式

以数组str为例,如果要访问str[1],即数组str的第二个元素,则它的访问步骤如下:

1.从编译器的符号表中得到str的地址,比如0x0000FE00。这个地址即为数组str首元素的首地址;

2.在这个地址上加一个偏移量得到新的地址0x0000FE01;

3.从这个新地址中读取数据;

通过下图可以加深对数组访问方式的理解:

指针的访问方式

以上述代码中的指针p为例,如果要访问*(p+1),即指针p所指向的匿名字符串的第二个字符,则它的访问步骤如下:

1.从编译器符号表中得到指针p的地址,比如0x0000EE20。这个地址即为&p,也就是指向指针p的指针;

2.从地址0x0000EE20中读取它内容即为指针p,比如0x00F0A000;

3.在0x00F0A000的基础上加一个偏移量,得到新地址0x00F0A001;

4.读取0x00F0A001中的内容,即为指针p所指的数据;

通过下图可以近一步理解指针的访问方式:


通过分析得知,在符号表中得到的是指针的地址而不是我们所要访问的指针;而在符号表中可以直接得到数组首元素的首地址。因此,访问指针时必须先通过符号表中指针的地址得到所要访问的指针,再接着进行指针所指内容的访问;而数组则直接可以通过符号表中的地址进行元素访问。也就是说指针的访问比数组的访问多了一次对内存地址的读取。

一不小心就引发的错误

现在看下面的两段代码:

/* 代码段1 */
file1:
char str[] = "edsionte";
file2:
extern char *str;
/* 代码段2 */
file1:
char *p == "edsionte";
file2:
extern char str[];

对于代码段1,在文件1中定义了数组str,而在文件2中将str声明为指针;对于代码段2,在文件1中定义了指针p,而在文件2中将p声明为数组。这里的声明指的是外部引用型声明,定义指的是定义型声明。

不管是上述那种情形,编译的时候都会出现错误。从上述对指针和数组访问方式的分析中可以得知,一个标示符被声明成指针还是数组对其访问方式影响巨大。下面我们对这两种错误作详细分析。

定义为数组,声明为指针

在文件2中,既然str被声明成指针,那么就应当按照指针的方式进行访问。首先从符号表中得到指针str的地址;从该地址中读取4个字节的数据即为指针str;接下来根据指针str访问其所指向的数据。这个过程好像很顺利,不过对于文件1中的数组str,其访问过程又是怎样的?文件1和文件2的访问结果是否一致?下图可帮助你理解。

从上图可以看到,str在文件2中被声明成指针,那么就符号表中str的地址0x0000FE00会被当作指针str的地址。根据指针的访问方式,必须从这个地址中取出指针p。虽然以0x0000FE00为首的四个字节中存储的是“edsi”,但是它们一律会被当成地址,按十六进制表示即为0x65647379。即便可以访问到这个地址,但是从这个地址中按照char型取出的数据并不是我们想要的。

由于编译器对每个文件进行单独编译,文件2并不知道str在文件1中被定义成什么类型。str在文件1中被定义成数组,那么就应该按照数组的方式访问数据;str在文件2中被声明成指针,那么就应该按照数组的方式访问数据。因此,在两个不同的文件中分别将str定义成数组而声明成指针会出现对str访问不一致的现象,所以编译器会产生错误。

定义为指针,声明为数组

此时,对于这种情况的理解也就简单多了。由于在文件而中p被声明成数组,因此就应该按照数组的方式对其进行访问。编译器会将原本指针str的地址当作str数组首元素的首地址,再对其加相应偏移量进行访问。这显然也是不合理的,因此编译器产生错误。具体可参见下图:

对上述的错误进行分析后,我们应该清楚将一个标示符声明成数组,编译器就会按照数组的访问方式去访问它;指针也是如此。因此,应该在多个文件中保持声明和定义相匹配。

指针和数组的其他区别

指针和数组除了在内存构造和访问方式上不同外,还有一些其他的区别。

1.指针通常用于指向一个动态的数据结构,而数组则用于存储固定大小和数据类型相同的数据;

2.指针所指向的数据通过malloc()分配,并且需要free()释放;而数组本身的内存空间则是隐士分配和释放,也就是在定义数组的时候进行;

3.指针所指向的数据通常是匿名的,而数组名则是数组所占内存空间的名字;

在本系列的最后一篇文章中,我们将分析指针和数组易被混淆的根源——也可将其称为指针和数组的可交换性。

参考:

《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;

《C语言深度解剖》北京航空航天大学出版社;陈正冲 著;

指针和数组的内存构造

2011年2月1日

指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。《指针和数组不是一回事儿》系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于《指针和数组不是一回事儿》系列文章之一。

指针和数组的本质是什么,这是本文讨论的重点。从内存结构的角度来说,两者是截然不同的两个概念。

数组的声明与定义

关于C语言中的声明,《声明那回事儿》一文中已详细叙述。这里再具体针对数组的定义和声明做以分析。以下面的声明代码为例:

char str[10];
extern char str[];

第一条声明语句定义了一个char型的数组,并为其分配了100字节大小的内存空间。而第二条声明语句则是为了告诉编译器这个数组的类型以及大小。由于在外部引用型声明中并不会数组分配内存空间,因此这种声明并不需要指定数组的大小。对于多维数组也并不需要指定第一维的大小。

指针的内存布局

指针本质上就是一个内存地址,为了方便使用这个内存地址将它和一个标示符绑定在一起。比如:

int i = 10;
int *p = &i; /* 假设变量i的内存地址为0x000F3E00 */

上述语句将地址0x000F3E00和p绑定在一起,并且一旦绑定就不能再修改,p此时也被称为指针变量,简称指针。“指针变量“中的“变量”并不是说明p可以再和其他地址绑定,而是强调与p绑定的这个地址中的内容可变,即i的值可以变化。

既然指针p是一个内存地址,那么在32位的系统中指针p所占的内存大小就始终为4字节。虽然整型变量i也占4字节,但是这两个同大小的内存空间却有着本质区别。指针p只能存放内存地址,并且这个地址只能是整型数据的首地址。即使在p内存放了其他数据,也会一律被当作内存地址来处理。

通过下图可以近一步了解指针和其所指数据的关系:


从图中可得知,不管指针所指数据占多大的内存空间,指针本身只占用4字节的大小。由于指针p本身占用4字节的内存空间,因此这部分内存空间也必然会有首地址。通过&p操作就可以得到指针p的首地址,也就是存储指针p的内存空间的首地址。从上图中可以看到指针p中即为整型变量i的首地址,因此我们也称p是一个指向整型变量i的指针。

数组的内存布局

数组是一块连续的内存空间,这块内存空间的名称即为数组名。比如:

int a[100];

当定义了一个具体的数组a时,编译器就根据数据类型和大小为其分配100*sizeof(int)大小的内存空间,并将这块连续的内存空间命名为a。虽然我们可以通过a[i]这种方式来访问元素i,但这并不代表a[i]就是这个元素的名称。因此每个数组元素实际上是没有名字的,编译器只为这块内存提供了唯一的名称a。同时数组名a也代表数组首元素的首地址。数组的内存结构如下:


通过对指针和数组内存布局的分析,我们可以得知这两者完全是不相同的。指针不管指向什么数据,它本身的大小就是4个字节(32位系统);而数组则是一块连续的内存空间。在下文中,将会从访问方式的角度分析指针和数组的不同。

参考:

《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;

《C语言深度解剖》北京航空航天大学出版社;陈正冲 著;

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key