指针与结构体知识小结

1.基本概念:

首先定义一个结构体类型,然后定义这种类型的变量和指针:

struct unit {
	char c;
	int num;
};
struct unit u;
struct unit *p = &u;

要通过指针p访问结构体成员可以写成(*p).c(*p).num,为了书写方便,C语言提供了->运算符,也可以写成p->cp->num

 

2.指向指针的指针与指针数组

指针可以指向基本类型,也可以指向复合类型,因此也可以指向另外一个指针变量,称为指向指针的指针。

int i;
int *pi = &i;
int **ppi = π

这样定义之后,表达式*ppipi的值,表达式**ppii的值。请读者自己画图理解ipippi这三个变量之间的关系。

很自然地,也可以定义指向“指向指针的指针”的指针,但是很少用到:

int ***p;

数组中的每个元素可以是基本类型,也可以复合类型,因此也可以是指针类型。例如定义一个数组a由10个元素组成,每个元素都是int *指针:

int *a[10];

这称为指针数组。int *a[10];int **pa;之间的关系类似于int a[10];int *pa;之间的关系:a是由一种元素组成的数组,pa则是指向这种元素的指针。所以,如果pa指向a的首元素:

int *a[10];
int **pa = &a[0];

pa[0]a[0]取的是同一个元素,唯一比原来复杂的地方在于这个元素是一个int *指针,而不是基本类型。

我们知道main函数的标准原型应该是int main(int argc, char *argv[]);argc是命令行参数的个数。而argv是一个指向指针的指针,为什么不是指针数组呢?因为前面讲过,函数原型中的[]表示指针而不表示数组,等价于char **argv。那为什么要写成char *argv[]而不写成char **argv呢?这样写给读代码的人提供了有用信息,argv不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *指针,指向一个命令行参数字符串。

例 23.2. 打印命令行参数

#include <stdio.h>

int main(int argc, char *argv[])
{
	int i;
	for(i = 0; i < argc; i++)
		printf("argv[%d]=%s/n", i, argv[i]);
	return 0;
}

编译执行:

$ gcc main.c
$ ./a.out a b c
argv[0]=./a.out
argv[1]=a
argv[2]=b
argv[3]=c
$ ln -s a.out printargv
$ ./printargv d e 
argv[0]=./printargv
argv[1]=d
argv[2]=e

注意程序名也算一个命令行参数,所以执行./a.out a b c这个命令时,argc是4,argv如下图所示:

图 23.4. argv指针数组

argv指针数组

由于argv[4]NULL,我们也可以这样循环遍历argv

for(i=0; argv[i] != NULL; i++)

NULL标识着argv的结尾,这个循环碰到NULL就结束,因而不会访问越界,这种用法很形象地称为SentinelNULL就像一个哨兵守卫着数组的边界。

在这个例子中我们还看到,如果给程序建立符号链接,然后通过符号链接运行这个程序,就可以得到不同的argv[0]。通常,程序会根据不同的命令行参数做不同的事情,例如ls -lls -R打印不同的文件列表,而有些程序会根据不同的argv[0]做不同的事情,例如专门针对嵌入式系统的开源项目Busybox,将各种Linux命令裁剪后集于一身,编译成一个可执行文件busybox,安装时将busybox程序拷到嵌入式系统的/bin目录下,同时在/bin/sbin/usr/bin/usr/sbin等目录下创建很多指向/bin/busybox的符号链接,命名为cplsmvifconfig等等,不管执行哪个命令其实最终都是在执行/bin/busybox,它会根据argv[0]来区分不同的命令。

 

3.指向数组的指针与多维数组

指针可以指向复合类型,上一节讲了指向指针的指针,这一节学习指向数组的指针。以下定义一个指向数组的指针,该数组有10个int元素:

int (*a)[10];

和上一节指针数组的定义int *a[10];相比,仅仅多了一个()括号。如何记住和区分这两种定义呢?我们可以认为[]*有更高的优先级,如果a先和*结合则表示a是一个指针,如果a先和[]结合则表示a是一个数组。int *a[10];这个定义可以拆成两句:

typedef int *t;
t a[10];

t代表int *类型,a则是由这种类型的元素组成的数组。int (*a)[10];这个定义也可以拆成两句:

typedef int t[10];
t *a;

t代表由10个int组成的数组类型,a则是指向这种类型的指针。

现在看指向数组的指针如何使用:

int a[10];
int (*pa)[10] = &a;

a是一个数组,在&a这个表达式中,数组名做左值,取整个数组的首地址赋给指针pa。注意,&a[0]表示数组a的首元素的首地址,而&a表示数组a的首地址,显然这两个地址的数值相同,但这两个表达式的类型是两种不同的指针类型,前者的类型是int *,而后者的类型是int (*)[10]*pa就表示pa所指向的数组a,所以取数组的a[0]元素可以用表达式(*pa)[0]。注意到*pa可以写成pa[0],所以(*pa)[0]这个表达式也可以改写成pa[0][0]pa就像一个二维数组的名字,它表示什么含义呢?下面把pa和二维数组放在一起做个分析。

int a[5][10];int (*pa)[10];之间的关系同样类似于int a[10];int *pa;之间的关系:a是由一种元素组成的数组,pa则是指向这种元素的指针。所以,如果pa指向a的首元素:

int a[5][10];
int (*pa)[10] = &a[0];

pa[0]a[0]取的是同一个元素,唯一比原来复杂的地方在于这个元素是由10个int组成的数组,而不是基本类型。这样,我们可以把pa当成二维数组名来使用,pa[1][2]a[1][2]取的也是同一个元素,而且paa用起来更灵活,数组名不支持赋值、自增等运算,而指针可以支持,pa++使pa跳过二维数组的一行(40个字节),指向a[1]的首地址。

 

4.函数类型和函数指针类型

在C语言中,函数也是一种类型,可以定义指向函数的指针。我们知道,指针变量的内存单元存放一个地址值,而函数指针存放的就是函数的入口地址(位于.text段)。下面看一个简单的例子:

例 23.3. 函数指针

#include <stdio.h>

void say_hello(const char *str)
{
	printf("Hello %s/n", str);
}

int main(void)
{
	void (*f)(const char *) = say_hello;
	f("Guys");
	return 0;
}

分析一下变量f的类型声明void (*f)(const char *)f首先跟*号结合在一起,因此是一个指针。(*f)外面是一个函数原型的格式,参数是const char *,返回值是void,所以f是指向这种函数的指针。而say_hello的参数是const char *,返回值是void,正好是这种函数,因此f可以指向say_hello。注意,say_hello是一种函数类型,而函数类型和数组类型类似,做右值使用时自动转换成函数指针类型,所以可以直接赋给f,当然也可以写成void (*f)(const char *) = &say_hello;,把函数say_hello先取地址再赋给f,就不需要自动类型转换了。

可以直接通过函数指针调用函数,如上面的f("Guys"),也可以先用*f取出它所指的函数类型,再调用函数,即(*f)("Guys")。可以这么理解:函数调用运算符()要求操作数是函数指针,所以f("Guys")是最直接的写法,而say_hello("Guys")(*f)("Guys")则是把函数类型自动转换成函数指针然后做函数调用。

下面再举几个例子区分函数类型和函数指针类型。首先定义函数类型F:

typedef int F(void);

这种类型的函数不带参数,返回值是int。那么可以这样声明fg

F f, g;

相当于声明:

int f(void);
int g(void);

下面这个函数声明是错误的:

F h(void);

因为函数可以返回void类型、标量类型、结构体、联合体,但不能返回函数类型,也不能返回数组类型。而下面这个函数声明是正确的:

F *e(void);

函数e返回一个F *类型的函数指针。如果给e多套几层括号仍然表示同样的意思:

F *((e))(void);

但如果把*号也套在括号里就不一样了:

int (*fp)(void);

这样声明了一个函数指针,而不是声明一个函数。fp也可以这样声明:

F *fp;

通过函数指针调用函数和直接调用函数相比有什么好处呢?我们研究一个例子。回顾第 3 节 “数据类型标志”的习题1,由于结构体中多了一个类型字段,需要重新实现real_partimg_partmagnitudeangle这些函数,你当时是怎么实现的?大概是这样吧:

double real_part(struct complex_struct z)
{
	if (z.t == RECTANGULAR)
		return z.a;
	else
		return z.a * cos(z.b);
}

现在类型字段有两种取值,RECTANGULARPOLAR,每个函数都要if ... else ...,如果类型字段有三种取值呢?每个函数都要if ... else if ... else,或者switch ... case ...。这样维护代码是不够理想的,现在我用函数指针给出一种实现:

double rect_real_part(struct complex_struct z)
{
	return z.a;
}

double rect_img_part(struct complex_struct z)
{
	return z.b;
}

double rect_magnitude(struct complex_struct z)
{
	return sqrt(z.a * z.a + z.b * z.b);
}

double rect_angle(struct complex_struct z)
{
	double PI = acos(-1.0);

	if (z.a > 0)
		return atan(z.b / z.a);
	else
		return atan(z.b / z.a) + PI;
}

double pol_real_part(struct complex_struct z)
{
	return z.a * cos(z.b);
}

double pol_img_part(struct complex_struct z)
{
	return z.a * sin(z.b);
}

double pol_magnitude(struct complex_struct z)
{
	return z.a;
}

double pol_angle(struct complex_struct z)
{
	return z.b;
}

double (*real_part_tbl[])(struct complex_struct) = { rect_real_part, pol_real_part };
double (*img_part_tbl[])(struct complex_struct) = { rect_img_part, pol_img_part };
double (*magnitude_tbl[])(struct complex_struct) = { rect_magnitude, pol_magnitude };
double (*angle_tbl[])(struct complex_struct) = { rect_angle, pol_angle };

#define real_part(z) real_part_tbl[z.t](z)
#define img_part(z) img_part_tbl[z.t](z)
#define magnitude(z) magnitude_tbl[z.t](z)
#define angle(z) angle_tbl[z.t](z)

当调用real_part(z)时,用类型字段z.t做索引,从指针数组real_part_tbl中取出相应的函数指针来调用,也可以达到if ... else ...的效果,但相比之下这种实现更好,每个函数都只做一件事情,而不必用if ... else ...兼顾好几件事情,比如rect_real_partpol_real_part各做各的,互相独立,而不必把它们的代码都耦合到一个函数中。“低耦合,高内聚”(Low Coupling, High Cohesion)是程序设计的一条基本原则,这样可以更好地复用现有代码,使代码更容易维护。如果类型字段z.t又多了一种取值,只需要添加一组新的函数,修改函数指针数组,原有的函数仍然可以不加改动地复用。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值