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

news2024/11/30 14:48:17

指针的基本概念

指针类型的参数和返回值

指针与数组

指针与const限定符

指针与结构体

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

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

函数类型和函数指针类型

不完全类型和复杂声明


指针的基本概念

堆栈有栈顶指针,队列有头指针和尾指针,这些概念中的"指针"本质上是一个整数,是数组的索引,通过指针访问数组中的某个元素,经过学习我们在间接寻址那里看到了另一个指针的概念,把一个变量所在的内存单元的地址保存在另外一个内存单元中,保存地址的这个内存单元称为指针,通过指针和间接寻址访问变量,这种指针在C语言中可以用一个指针类型的变量表示,例如某程序中定义了以下全局变量:

int i;
int *pi = &i;
char c;
char *pc = &c;

 这几个变量的内存布局如下图所示,在初学阶段经常要借助于这样的图来理解指针。

 这里的&是取地址运算符,&i表示取变量i的地址,int *pi=&i;表示定义一个指向int型的指针变量pi,并用i的地址来初始化pi.我们讲过全局变量只能用常量表达式初始化,如果定义int p=i;就错了,因为i不是常量表达式,然而用i的地址来初始化一个指针却没有错,因为i的地址是在编译链接时能确定的,而不需要运行时才知道,&i是常量表达式。后面两行代码定义了一个字符型变量c和一个指向c的字符型指针pc,注意pi和pc虽然是不同类型的指针变量,但它们的内存单元都占4个字节,因为要保存32位的虚拟地址,同理,在64位平台上指针变量都占8个字节。

我们知道,在同一个语句中定义多个数组,每一个都要有[]号:int a[5],b[5];同样道理,在同一个语句中定义多个指针变量,每一个都要有*号,例如:

int *p, *q;

 如果写成int* p,q;就错了,这样是定义了一个整型指针p和一个整型变量q,定义数组的[]号写在变量后面,而定义指针的*号写在变量前面,更容易看错。定义指针的*号前后空格都可以省,写成int*p,*q;也算对,但*号通常和类型int之间留空格而和变量名写在一起,这样看int *p, q;就很明显是定义一个指针和一个整型变量,就不容易看错了。

如果要让pi指向另一个整型变量j,可以重新对pi赋值:

pi = &j;

 如果要改变pi所指向的整型变量的值,比如把变量j的值增加10,可以写:

*pi = *pi + 10;

这里的*号是指针间接寻址运算符,*pi表示取指针pi所指向的变量的值,也称为Dereference操作,指针有时称为变量的引用,所以根据指针找到变量称为Dereference.

&运算符的操作数必须是左值,因为只有左值才表示一个内存单元,才会有地址,运算结果是指针类型。*运算符的操作数必须是指针类型,运算结果可以做左值。所以,如果表达式E可以做左值,*&E和E等价,如果表达式E是指针类型,&*E和E等价。

指针之间可以相互赋值,也可以用一个指针初始化另一个指针,例如:

int *ptri=pi;

 或者:

int *ptri;
ptri=pi;

表示pi指向哪就让ptri也指向哪,本质上就是把变量pi所保存的地址值赋给变量ptri.用一个指针给另一个指针赋值时要注意,两个指针必须是同一类型的。在我们的例子中,pi是int*型的,pc是char*型的,pi=pc;这样赋值就是错误的。但是可以先强制类型转换然后赋值:

pi = (int *)pc;

 把char *指针的值赋给int *指针

 现在pi指向的地址和pc一样,但是通过*pc只能访问到一个字节,而通过*pi可以访问到4个字节,后3个字节已经不属于变量c了,除非你很确定变量c的一个字节和后面3个字节组合而成的int值是有意义的,否则就不应该给pi这么赋值。因此使用指针要特别小心,很容易将指针指向错误的地址,访问这样的地址可能导致段错误,可能读到无意义的值,也可能意外改写了某些数据,使得程序在随后的运行中出错。有一种情况需要特别注意,定义一个指针类型的局部变量而没有初始化:

int main(void)
{
 int *p;
 ...
 *p = 0;
 ...
}

 我们知道,在堆栈上分配的变量初始值是不确定的,也就是说指针p所指向的内存地址是不确定的,后面用*p访问不确定的地址就会导致不确定的后果,如果导致段错误还比较容易改正,如果意外改写了数据而导致随后的运行中出错,就很难找到错误原因了。像这种指向不确定地址的指针称为"野指针",为了避免野指针,在定义指针变量时就应该给它明确的初值,或者把它初始化位NULL:

int main(void)
{
 int *p = NULL;
 ...
 *p = 0;
 ...
}

 NULL在C标准库的头文件stddef.h中定义:

#define NULL ((void *)0)

 就是把地址0转换成指针类型,称为空指针,它的特殊之处在于,操作系统不会把任何数据保存在地址0及其附近,也不会把地址0-0xfff的页面映射到物理内存,所以任何对地址0的访问都会立刻导致段出错。*p=0;会导致段错误,就像放在眼前的炸弹一样很容易被找到,相比之下,野指针的错误就像埋下地雷一样,更难发现和排除,这次走过去没事,下次走过去就有十。

讲到这里就该讲一下void*类型了。在编程时经常需要一种通用指针,可以转换为任意其他类型的指针,任意其他类型的指针也可以转换为通用指针,最初C语言中没有void*类型,就把char*当通用指针,需要转换时就用类型转换运算符(),ANSI在将C语言标准化时引入了void*类型,void*指针与其他类型的指针之间可以隐式转换,而不必用类型转换运算符。注意,只能定义void*指针,而不能定义void型的变量,因为void*指针和别的指针一样都占4个字节,而如果定义void型变量,编译器不知道该分配几个字节给变量。同样道理,void*指针不能直接Dereference,而必须先转换成别的类型的指针再做Dereference.void*指针常用于函数接口。比如:

void func(void *pv)
{
 /* *pv = 'A' is illegal */
 char *pchar = pv;
 *pchar = 'A';
}
int main(void)
{
 char c;
 func(&c);
 printf("%c\n", c);
...

 下一章讲函数接口时再详细介绍void *指针的用处。

指针类型的参数和返回值

首先看以下程序:

指针参数和返回值

#include <stdio.h>
int *swap(int *px, int *py)
{
 int temp;
 temp = *px;
 *px = *py;
 *py = temp;
 return px;
}
int main(void)
{
 int i = 10, j = 20;
 int *p = swap(&i, &j);
 printf("now i=%d j=%d *p=%d\n", i, j, *p);
 return 0;
}

 我们知道,调用函数的传参过程相当于用实参定义并初始化形参,swap(&i,&j)这个调用相当于:

int *px = &i;
int *py = &j;

所以px和py分别指向main函数的局部变量i和j,在swap函数中读写*px和*py其实是读写main函数的i和j。尽管在swap函数的作用域中访问不到j和j这两个变量名,却可以通过地址访问它们,最终swap函数将i和j的值做了交换。

上面的例子还演示了函数返回值是指针的情况,return px;语句相当于定义了一个临时变量并用px初始化:
int *tmp = px;
然后临时变量 tmp 的值成为表达式 swap(&i, &j) 的值,然后在 main 函数中又把这个值赋给了 p ,相当
于:
int *p=tmp;

最后的结果是swap函数的px指向哪就让main函数的p指向哪。我们知道px指向i,所以p也指向i.

指针与数组

先看个例子,有如下语句:

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

 首先指针pa指向a[0]的地址,注意后缀运算符的优先级高于单目运算符,所以是取a[0]的地址,而不是取a的地址。然后pa++让pa指向下一个元素(也就是a[1]),由于pa是int *指针,一个int型元素占4个字节,所以pa++使pa所指向的地址加4,注意不是加1.

下面画图理解。从前面的例子我们发现,地址的具体数值其实无关紧要,关键是要说明地址之间的关系(a[1]位于a[0]之后4个字节处)以及指针与变量之间的关系(指针保存的是变量的地址),现在我们换一种画法,省略地址的具体数值,用方框表示存储空间,用箭头表示指针和变量之间的关系。

 既然指针可以用++运算符,当然也可以用+,-运算符,pa+2这个表达式也是有意义的,如上图所示,pa指向a[1],那么pa+2指向a[3]。事实上,E1[E2]这种写法和(*((E1)+(E2)))是等价的,*(pa+2)也可以写成pa[2],pa就像数组名一样,其实数组名也没有什么特殊的,a[2]之所以能取数组的第2个元素,是因为它等价于*(a+2),在数组那里讲过数组名做右值时自动转换成指向首元素的指针,所以a[2]和pa[2]本质上是一样的,都是通过指针间接寻址访问元素。由于(*((E1)+(E2)))显然可以写成(*((E2)+(E1))),所以E1[E2]也可以写成E2[E1],这意味着2[a],2[pa]这种写法也是对的,但一般不这么写。另外,由于a做右值使用时和&a[0]是一个意思,所以int *pa=&a[0];通常不这么写,而是写成更简单的形式int *pa=a;。

在数组那里讲过C语言允许数组下标是负数,现在你该明白为什么这样规定了。在上面的例子中,表达式pa[-1]是合法的,它和a[0]表示同一个元素。

现在猜一下,两个指针变量做比较运算表示什么意义?两个指针变量做减法运算又有什么什么意义?

你理解了指针和常数的加减的概念,再根据以往使用比较运算的经验,就应该猜到pa+2>pa,pa-1==a,所以指针指针的比较运算比的是地址,C语言正是这样规定的,不过C语言的规定更为严谨,只有指向同一个数组中元素的指针之间相互比较才有意义,否则没有意义。那么两个指针相减表示什么?pa-a等于几?因为pa-1==a,所以pa-a显然等于1,指针相减表示两个指针之间相差的元素的个数,同样只有指向同一个数组中的元素的指针之间相减才有意义。两个指针相加表示什么?想不出来它能有什么意义,因此C语言也规定两个指针不能相加。

在取数组元素时用数组名和用指针的语法一样,但如果把数组名做左值使用,和指针又有区别。例如pa++合法,但a++就不合法,pa=a+1是合法的,但是a=pa+1是不合法的。数组名做右值时转换成指向首元素的指针,但做左值仍然表示整个数组的存储空间,而不是首元素的存储空间,数组名做左值时还有一点特殊之处,不支持++,赋值这些运算符,但支持取地址运算符&,所以&a是合法的。

在函数原型中,如果参数是数组,则等价于参数是指针的形式,例如:

void func(int a[10])
{
 ...
}

 等价于:

void func(int *a)
{
 ...
}

第一种形式方括号中的数字可以不写,仍然是等价的:

void func(int a[])
{
 ...
}

参数写成指针形式还是数组形式对编译器来说没区别,都表示这个参数是指针,之所以规定两种形式是为了给读代码的人提供有用的信息,如果这个参数指向一个元素,通常写成指针的形式,如果这个参数指向一串元素的首元素,则经常写成数组的形式。 

指针与const限定符

const限定符和指针结合起来常见的情况有以下几种:

const int *a;
int const *a;

 这两种写法是一样的,a是一个指向const int型的指针,a所指向的内存单元不可改写,所以(*a)++是不允许的,但a可以改写,所以a++是允许的。

int * const a;

a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。 

int const* const a;

 a是一个指向const int型的const指针,因此*a和a都不允许改写。

指向非const变量的指针或者非const变量的地址可以传给指向const变量的指针,编译器可以做隐式类型转换,例如:

char c = 'a';
const char *pc = &c;

 但是,指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,以免投过后者意外改写了前者所指向的内存单元,例如对下面的代码编译器会报警告:

const char c = 'a';
char *pc = &c;

 即使不用const限定符也能写出正确的程序,但良好的编程习惯应该尽可能多的使用const,因为:

1.const给读代码的人传达非常有用的信息。比如一个函数的参数是const char *,你在调用这个函数时就可以放心地传给它char *或const char *指针,而不必担心指针所指的内存单元被改写。

2.尽可能多地使用const限定符,把不该变的都声明成只读,这样可以依靠编译器检查程序中的Bug,防止意外改写数据。

3.const对编译器优化是一个有用的提示,编译器也许会把const变量优化成常量。

字符串字面值通常分配在.rodata段,字符串字面值类似于数组名,做右值使用时自动转换成指向首元素的指针,这种指针应该是const char *型。我们知道printf函数原型的第一个参数是const char *型,可以把char *或const char *指针传给它,所以下面这些调用都是合法的:

const char *p = "abcd";
const char str1[5] = "abcd";
char str2[5] = "abcd";
printf(p);
printf(str1);
printf(str2);
printf("abcd");

 注意上面第一行,如果要定义一个指针指向字符串字面值,这个指针应该是const char*型,如果写成char *p="abcd";就不好了,有隐患,例如:

int main(void)
{
 char *p = "abcd";
...
 *p = 'A';
...
}

 *p指向.rodata段,不允许改写,但编译器不会报错,在运行时会出现段错误。

指针与结构体

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

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

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

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

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

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

 这样定义之后,表达式*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 *指针,指向一个命令行参数字符串。

打印命令行参数

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

 由于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];这个定义也可以拆成两句:

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]的首地址。

函数类型和函数指针类型

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

 相当于声明:

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;
通过函数指针调用函数和直接调用函数相比有什么好处呢?我们研究一个例子。由于结构体中多了一个类型字段,需要重新实现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 语言类型总结

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

struct s;
union u;
char str[];
具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组 str 声明两次:
char str[];
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 )。
链表

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类型:

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

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

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

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

typedef int T3[10];
typedef T3 *T2;
typedef T2 T1(void *);
T1 *fp;
显然, T3 是一个 int 数组,由 10 个元素组成。分解完毕。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/152257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 图进阶系列之纵横对比 Bellman-Ford 和 Dijkstra 最短路径求解算法

1. 前言 因无向、无加权图的任意顶点之间的最短路径由顶点之间的边数决定&#xff0c;可以直接使用原始定义的广度优先搜索算法查找。 但是&#xff0c;无论是有向、还是无向&#xff0c;只要是加权图&#xff0c;最短路径长度的定义是&#xff1a;起点到终点之间所有路径中权…

第五十讲:神州路由器IPv6隧道的配置

目前IPv6隧道技术有ISATAP隧道、6to4隧道、IPv6 over MPL隧道等。 任务一&#xff1a;配置ISATAP隧道 实验拓扑图如下所示 配置要求&#xff1a;两台路由器ROUTERA和ROUTERB通过IPv4网络连接&#xff0c;要求在两台路由器上分别配置ISATAP隧道&#xff0c;实现两端IPv6主机PC…

回收租赁商城系统功能拆解05讲-供货商

回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 可以快速帮助企业搭建类似闲鱼回收/爱回收/爱租机/人人租等回收租赁商城。 回收租赁系统支持智能评估回收价格&#xff0c;后台调整最终回收价&#xff0c;用户同意回收后系统即刻放款&#xff0c;用户微信零…

2022年学习和实习总结——收获颇多(未完待续...)

0 序言 时间已经进入了2023年&#xff0c;今年将是属于我们这一届秋招的一年。回顾2022年的学习和实习历程&#xff0c;我觉得今年的收获还是不少的&#xff0c;甚至可以说是整个高等教育生涯中&#xff0c;收获最多的一年。 1 学习情况总结 1.1 读书和学习总结 原来…

【Linux】项目构建自动化工具——make和makefile

make 和 makefile1.背景2. 利用make和makefile简单编译一个源文件3.使用方法解释4.执行原理5.项目清理1.背景 &#xff08;1&#xff09;一个工程中的源文件不计数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;makefile定义了一系列的规则来指定&#xff…

智能驾驶视觉传感器测试:自动驾驶车辆如何进行传感器标定?

为什么要进行标定&#xff1f;一辆自动驾驶车辆会安装多个传感器&#xff0c;需要通过传感器标定来确定相互之间的坐标关系&#xff0c;从而将多个传感器数据整合为“一个传感器”。因此&#xff0c;准确的传感器标定是实现智能车辆多传感器感知和定位系统的先决条件。传感器标…

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《6》

我们在看Faster R-CNN源码(MXNet版本)的时候&#xff0c;除了下面这些我们遇到的常见的参数解析 import argparse import ast parser argparse.ArgumentParser(descriptionDemonstrate a Faster R-CNN network,formatter_classargparse.ArgumentDefaultsHelpFormatter) parse…

【操作系统系统/Golang】实验5:磁盘调度(FCFS,SSTF,SCAN与循环SCAN)

1 实验问题描述设计程序模拟先来先服务FCFS&#xff0c;最短寻道时间优先SSTF&#xff0c;SCAN和循环SCAN算法&#xff08;对应其他参考书的LOOK和C-LOOK&#xff09;的工作过程。假设有trackNum个磁道号所组成的磁道访问序列&#xff0c;给定开始磁道号initTrackNum和磁头移动…

Java项目管理工具:Maven介绍

Maven 简介下载与环境配置Maven仓库仓库类型本地仓库配置简介 Maven ,一款跨平台的项目管理工具。它主要服务于基于 Java 平台的项目构建与依赖管理。即可以将项目的开发和管理过程抽象成一个项目对象模型(POM),开发人员只需要做一些简单的配置,Maven 便可自动完成项目构建…

HRNet源码阅读笔记(4),庞大的PoseHighResolutionNet模块-stage1

一、图和代码上一讲的图中&#xff0c;有stage1图例如下;关键是看pose_hrnet.py中PoseHighResolutionNet模块的forward函数相关部分如下&#xff1a;def forward(self, x):x self.conv1(x)x self.bn1(x)x self.relu(x)x self.conv2(x)x self.bn2(x)x self.relu(x)x self…

一辈子干好一件事,你就了不起

人生的道路和轨迹&#xff0c;就像是射出去的箭。假如左拐右拐&#xff0c;不但不能射中靶心&#xff0c;达不到目标&#xff0c;还有可能拐弯回到原点&#xff0c;原地踏步。日复一日&#xff0c;年复一年&#xff0c;没有成长和收获&#xff0c;这很可怕。 短暂的2022年&…

设计测试用例的万能公式 + 6大具体方法 = 面试就像聊天?

目录 一、设计测试用例的万能公式 二、设计测试用例的具体方法 2.1、等价类 2.2、边界值 2.3、判定表&#xff08;因果图的另一种形式&#xff09; 2.4、场景设计法 2.5、正交法&#xff08;用的少&#xff0c;基本不可见&#xff09; 2.4.1、使用allparis构建正交表 2.…

算法之动态规划理论

目录 前言 一个模型三个特征理论讲解 1.最优子结构 2.无后效性 3.重复子问题 一个模型三个特征实例剖析 两种动态规划解题思路总结 1.状态转移表法 2.状态转移方程法 四种算法思想比较分析 总结&#xff1a; 参考资料 前言 本篇博文主要讲解动态规划的理论&am…

行业分析| 交通综合执法对讲系统

随着社会的经济发展&#xff0c;人口的增加城市的不断壮大&#xff0c;城市交通情况越来越复杂&#xff0c;交警承担的执法任务越来越重&#xff0c;通信作为交警综合执法对讲调度的重要组成部分&#xff0c;也随之提出了更高的要求。综合执法对讲系统的出现使执法变得高效规范…

【Redis】高级进阶

&#x1f31f;个人博客&#xff1a;www.hellocode.top&#x1f31f; &#x1f31f;Java知识导航&#xff1a;Java-Navigate&#x1f31f; ⭐想获得更好的阅读体验请前往Java-Navigate &#x1f525;本文专栏&#xff1a;《流行框架》 &#x1f31e;如没有JavaWEB基础&#xff0…

【ZooKeeper】第三章 集群搭建

【ZooKeeper】第三章 集群搭建 文章目录【ZooKeeper】第三章 集群搭建一、ZooKeeper 集群介绍1.Leader 选举2.集群角色二、搭建 ZooKeeper 集群1.安装2.配置集群3.启动集群三、故障模拟一、ZooKeeper 集群介绍 1.Leader 选举 Serviceid&#xff1a;服务器 ID 比如有三台服务器…

分享84个NET源码,总有一款适合您

分享84个NET源码&#xff0c;总有一款适合您 链接&#xff1a;https://pan.baidu.com/s/1r7_yrTfQrg-5whL7AYJiLA?pwdeem6 提取码&#xff1a;eem6 import os from time import sleepimport requests from bs4 import BeautifulSoup from docx import Document from docx.sh…

iOS ReplayKit 屏幕共享,屏幕直播实现

使用replayKit iOS12 之后相关 api 完成系统/app 内 屏幕采集直播视频数据, 采用 socket进行进程间Broadcast Unload Extension 向 宿主 app 传输数据, 后台保活持续采集屏幕数据, 摄像头采集, 数据编码解码 编译环境 Xcode14.2, iOS12 系统屏幕数据采集app 内屏幕共享使用so…

FPGA与数字IC求职知识准备 - 数字电路知识总结

前言 本文整理了数字电路课程中的相关基本的知识点和较为重要的知识点&#xff0c;用于求职的数电部分的知识准备&#xff0c;差缺补漏。 二进制数的算术运算 无符号二进制数的算术运算 加法&#xff1a;同十进制加法&#xff0c;逢二进一&#xff0c;无符号二进制数的加法…

Redis源码篇(8)——集群模式

1、集群模式的启动和初始化 当开启了cluster-enabled&#xff0c;在初始化服务initServer方法中会调用clusterInit方法将redis带入cluster模式。 clusterInit void clusterInit(void) {int saveconf 0;//初始化clusterState结构 server.cluster zmalloc(sizeof(clusterSta…