指针与结构体知识小结

    技术2025-05-31  11

    1.基本概念:

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

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

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

     

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

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

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

    这样定义之后,表达式*ppi取pi的值,表达式**ppi取i的值。请读者自己画图理解i、pi、ppi这三个变量之间的关系。

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

    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[4]是NULL,我们也可以这样循环遍历argv:

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

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

    在这个例子中我们还看到,如果给程序建立符号链接,然后通过符号链接运行这个程序,就可以得到不同的argv[0]。通常,程序会根据不同的命令行参数做不同的事情,例如ls -l和ls -R打印不同的文件列表,而有些程序会根据不同的argv[0]做不同的事情,例如专门针对嵌入式系统的开源项目Busybox,将各种Linux命令裁剪后集于一身,编译成一个可执行文件busybox,安装时将busybox程序拷到嵌入式系统的/bin目录下,同时在/bin、/sbin、/usr/bin、/usr/sbin等目录下创建很多指向/bin/busybox的符号链接,命名为cp、ls、mv、ifconfig等等,不管执行哪个命令其实最终都是在执行/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]取的也是同一个元素,而且pa比a用起来更灵活,数组名不支持赋值、自增等运算,而指针可以支持,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。那么可以这样声明f和g:

    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_part、img_part、magnitude、angle这些函数,你当时是怎么实现的?大概是这样吧:

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

    现在类型字段有两种取值,RECTANGULAR和POLAR,每个函数都要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_part和pol_real_part各做各的,互相独立,而不必把它们的代码都耦合到一个函数中。“低耦合,高内聚”(Low Coupling, High Cohesion)是程序设计的一条基本原则,这样可以更好地复用现有代码,使代码更容易维护。如果类型字段z.t又多了一种取值,只需要添加一组新的函数,修改函数指针数组,原有的函数仍然可以不加改动地复用。

    最新回复(0)