【C进阶】第十三篇——指针详解(二)

简介: 【C进阶】第十三篇——指针详解

指针与结构体


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

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

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

指向指针的指针与指针数组


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

1. int i;
2. int *pi = &i;
3. 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的首元素:

1. int *a[10];
2. 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 *指针,指向一个命令行参数字符串。


打印命令行参数

#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如下图所示:

image.png

由于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]来区分不同的命令。

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


指针可以指向复合类型,上一节讲了指向指针的指针,这一节学习指向数组的指针,以下定义一个指向数组的指针,该数组有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];这个定义也可以拆成两句:

1. typedef int t[10];
2. t *a;

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

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

1. int a[10];
2. 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]的首地址。

函数类型和函数指针类型


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

函数指针

#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;

相当于声明:

1. int f(void);
2. int g(void);

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

F h(void);

因为函数可以返回void类型、标量类型、结构体、联合体,但不能返回函数类型,也不能返回数组

类型。而下面这个函数声明是正确的:

F *e(void);

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

F *((e))(void);

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

int (*fp)(void);

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

F *fp;

通过函数指针调用函数和直接调用函数相比有什么好处呢?我们研究一个例子。由于结构体中多了一个类型字段,需要重新实现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 又多了一种取值,只需要添加一组新的函数,修改函数指针数组,原有的函数仍然可

以不加改动地复用。

不完全类型和复杂声明


C语言类型总结

image.png

C语言的类型分为函数类型、对象类型和不完全类型三大类。对象类型又分为标量类型和非标量类 型。指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和if、for、while的控制表达式,NULL指针表示假,非NULL指针表示真。不完全类型是暂时没有完全定义好的类型,编译器 不知道这种类型该占几个字节的存储空间,例如:

struct s;
union u;
char str[];

具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组str声明两次:

1. char str[];
2. char str[10];

当编译器碰到第一个声明时,认为 str 是一个不完全类型,碰到第二个声明时 str 就组合成完全类型

了,如果编译器处理到程序文件的末尾仍然无法把组合成一个完全类型,就会报错。读者可能会想,这个语法有什么用呢?为何不在第一次声明时就把str 声明成完全类型?有些情况下这么做有一定的理由,比如第一个声明是写在头文件里的,第二个声明写在.c 文件里,这样如果要改数组长度,只改.c 文件就行了,头文件可以不用改。

不完全的结构体类型有重要作用:

struct s {
 struct t *pt;
};
struct t {
 struct s *ps;
};

struct s 和 struct t 各有一个指针成员指向另一种类型。编译器从前到后依次处理,当看到 struct

s { struct t* pt; }; 时,认为 struct t 是一个不完全类型, pt 是一个指向不完全类型的指针,尽管如此,这个指针却是完全类型,因为不管什么指针都占4 个字节存储空间,这一点很明确。然后编译器又看到struct t { struct s *ps; }; ,这时 struct t 有了完整的定义,就组合成一个完全类型了,pt 的类型就组合成一个指向完全类型的指针。由于 struct s 在前面有完整的定义,所以struct s *ps; 也定义了一个指向完全类型的指针。

这样的类型定义是错误的:

struct s {
 struct t ot;
};
struct t {
 struct s os;
};

编译器看到 struct s { struct t ot; }; 时,认为 struct t 是一个不完全类型,无法定义成员 ot, 因为不知道它该占几个字节。所以结构体中可以递归地定义指针成员,但不能递归地定义变量成员,你可以设想一下,假如允许递归地定义变量成员,struct s 中有一个 struct t , struct t 中又有一个struct s , struct s 又中有一个 struct t ,这就成了一个无穷递归的定义。以上是两个结构体构成的递归定义,一个结构体也可以递归定义:

struct s {
 char data[6];
 struct s* next;
};

当编译器处理到第一行 struct s { 时,认为 struct s 是一个不完全类型,当处理到第三行 struct s

*next; 时,认为 next 是一个指向不完全类型的指针,当处理到第四行 }; 时, struct s 成了一个完全

类型, next 也成了一个指向完全类型的指针。类似这样的结构体是很多种数据结构的基本组成单

元,如链表、二叉树等,我们将在后面详细介绍。下图示意了由几个 struct s 结构体组成的链表,

这些结构体称为链表的节点( Node )。

链表

image.png

head 指针是链表的头指针,指向第一个节点,每个节点的 next 指针域指向下一个节点,最后一个节

点的 next 指针域为 NULL ,在图中用 0 表示。

可以想像得到,如果把指针和数组、函数、结构体层层组合起来可以构成非常复杂的类型,下面看

几个复杂的声明。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

这个声明来自 signal(2) 。 sighandler_t 是一个函数指针,它所指向的函数带一个参数,返回值为void , signal 是一个函数,它带两个参数,一个 int 参数,一个 sighandler_t 参数,返回值也是sighandler_t 参数。如果把这两行合成一行写,就是:

void (*signal(int signum, void (*handler)(int)))(int);

在分析复杂声明时,要借助 typedef 把复杂声明分解成几种基本形式:

T *p; , p 是指向 T 类型的指针。

T a[]; , a 是由 T 类型的元素组成的数组,但有一个例外,如果 a 是函数的形参,则相当于 T

*a;

T1 f(T2, T3...); , f 是一个函数,参数类型是 T2 、 T3 等等,返回值类型是 T1 。

我们分解一下这个复杂声明:

int (*(*fp)(void *))[10];

1.fp*号括在一起,说明fp是一个指针,指向T1类型:

1. typedef int (*T1(void *))[10];
2. T1 *fp;

2.T1应该是一个函数类型,参数是void *,返回值是T2类型:

1. typedef int (*T2)[10];
2. typedef T2 T1(void *);
3. T1 *fp;

3.T2*号括在一起,应该也是个指针,指向T3类型:

typedef int T3[10];
typedef T3 *T2;
typedef T2 T1(void *);
T1 *fp;

显然, T3 是一个 int 数组,由 10 个元素组成。分解完毕。


相关文章
|
7月前
|
存储 编译器 C语言
C语言内功修炼--指针详讲(进阶)
C语言内功修炼--指针详讲(进阶)
|
存储 算法
学C的第二十六天【指针的进阶(二)】
6 . 函数指针数组 (1). 含义: 函数指针数组 是一个数组,是用于存放 函数指针 的数组。 例如:
学C的第二十七天【指针的进阶(三)】-1
复习巩固: 数组名: 数组名是数组首元素的地址, 但是有两个例外:
|
存储 C语言 C++
【进阶C语言】指针的进阶(万字图文详解)(二)
【进阶C语言】指针的进阶(万字图文详解)(二)
|
存储 C语言 C++
【进阶C语言】指针的进阶(万字图文详解)(一)
【进阶C语言】指针的进阶(万字图文详解)(一)
指针的进阶【中篇】
指针的进阶【中篇】
67 0
指针的进阶【下篇】
指针的进阶【下篇】
64 0
|
存储 安全 编译器
【C++系列P2】引用——背刺指针的神秘刺客(精讲一篇过!)
【C++系列P2】引用——背刺指针的神秘刺客(精讲一篇过!)