Loading... # 指针 - 什么是指针? - 指针就是存放变量地址的,**指针**就是**地址**,**地址**就是**指针** - 指针有什么用? - 访问变量地址 - 能直接访问硬件 - 能方便的处理字符串 - 指针在c语言学习中的地位是什么? - 指针是c语言的灵魂 - 指针学习重点 - 指针运算符 - 指向运算符 - 变量指针和指针变量 - 数组指针和指针数组 - 函数指针和指针函数 - 指针与字符串 - 指向结构体的指针和链式存储结构 - 考试要求 1. 了解指针与地址的概念。 2. 理解指针变量与指向变量的指针之间区别与联系。 3. 掌握指针数组和数组指针的区别及指针在数组中的应⽤。 4. 掌握指针与字符串的应⽤。 5. 掌握指针函数和函数指针的区别及指针在函数中的应⽤。 6. 掌握指向结构体指针的应⽤。 7. 了解链式存储结构的概念。 ## 指针运算符 ### 指针运算符(* and &) #### 取地址运算符& **格式**:&变量名 **含义**:取出存放变量的地址 **用途**:跨函数传递变量值 > **举例**: &a //表示变量a的存放地址 b = &a //表示把变量a的地址赋值给变量b #### 间接运算符* **格式**:*指针名/地址名 **含义**:取出存储在地址中的对应值 > **举例**: a = 3; //将a复制为3 c = &a; //把a的地址赋值给c d = *c; //取出c存放a地址中的值,并赋值给d printf("d = %d",d); //打印d 输出为: d = 3 ## 指向运算符:-> ### 指针设声明 声明格式 - int * pi - char * p **解释**:**"->"**代表一个箭头,称为指向运算符。 **使用场景**:当在结构体变量中使用指针时用到->。 **使用方法**:C语言允许把`(*p).name`用`p->name`代替,都表示p所指向的结构体变量中的name成员。 **常见错误**:1、*`p.name (x)`,“.”运算符的优先级别高,所以*p一定要用括号。2、*`p->name (x)`,*p是指针p指向的对象,指向运算符配合的是指针。 ## 变量指针和指针变量 ### 变量指针 变量的**指针**就是变量的**地址**。存放变量**地址**的变量是**指针变量**。即在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个变量的地址或称为某变量的指针。 为了表示指针变量和它所指向的变量之间的关系,在程序中用“*”符号表示“指向” ### 指针变量(重点) #### 1.**定义指针变量** 格式:类型说明符 *变量名; *表示这是一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。 > 例如: > > ```c > int *p1; > ``` 表示 p1 是一个指针变量, 它的值是某个整型变量的地址。 或者说 p1 指向一个整型变量。至于 p1 究竟指向哪一个整型变量,应由向 p1 赋予的地址来决定。 #### 2.**指针变量的引用** **注意**: 1、指针不仅要先定义说明,而且必须赋予具体的值 。 如果现在还不是很清楚这句话等下下面有案例。 2、指针变量的赋值只能赋予地址,决不能赋予任何其它数据。 3、不允许把一个数赋予指针变量 eg:int *p; p=1000;//**错误** 4、被赋值的指针变量前不能再加“*”说明符 eg: *p=&a//**错误 正确:p=&a** **正确的初始化赋值方式**: ```c int a;int *p=&a; int a;int *p; p=&a; ``` ## 数组指针和指针数组 ### **数组指针(也称行指针)** #### 定义 int (*p)[n]; ()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。 如要将二维数组赋给一指针,应这样赋值: ```c int a[3][4]; int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。 p=a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0] p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][] ``` 所以数组指针也称指向一维数组的指针,亦称行指针。 ### **指针数组** #### 定义 int *p[n]; []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。 如要将二维数组赋给一指针数组: ```c int *p[3]; int a[3][4]; p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针 for(i=0;i<3;i++) p[i]=a[i] ``` 这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2] 所以要分别赋值。 这样两者的区别就豁然开朗了,数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。 还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。 比如要表示数组中i行j列一个元素: *(p[i]+j)、*(*(p+i)+j)、(*(p+i))[j]、p[i][j] **优先级**:() > [] > * ## 函数指针和指针函数 “指针函数”与“函数指针”容易搞错,最简单的辨别方式就是看函数名前面的指针*号有没有被括号()包含,如果被包含就是函数指针,反之则是指针函数。 ### 指针函数 #### 定义 指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。 声明格式为:*类型标识符 函数名(参数表) ```c int *fun(int x,int y); ``` 所谓的指针函数和普通函数对比不过就是其返回了一个 **指针** (即地址值)而已。 #### 格式: ```c ret *func(args, ...); ``` 其中,`func`是一个函数,`args`是形参列表,`ret *`作为一个**整体**,是 `func`函数的**返回值**,是一个指针的形式。 > 实例: ```c # include <stdio.h> # include <stdlib.h> int * func_sum(int n) { if (n < 0) { printf("error:n must be > 0\n"); exit(-1); } static int sum = 0; int *p = ∑ for (int i = 0; i < n; i++) { sum += i; } return p; } int main(void) { int num = 0; printf("please input one number:"); scanf("%d", &num); int *p = func_sum(num); printf("sum:%d\n", *p); return 0; } ``` 上例就是一个指针函数的例子,其中,`int * func_sum(int n)`就是一个指针函数, 其功能十分简单,是根据传入的参数n,来计算从0到n的所有自然数的和,其结果通过指针的形式返回给调用方。 以上代码的运行结果如下所示: > 运行结果:4950 如果上述代码使用普通的局部变量来实现,也是可以的,如下所示: ```c # include <stdio.h> # include <stdlib.h> int func_sum2(int n) { if (n < 0) { printf("error:n must be > 0\n"); exit(-1); } int sum = 0; int i = 0; for (i = 0; i < n; i++) { sum += i; } return sum; } int main(void) { int num = 0; printf("please input one number:"); scanf("%d", &num); int ret = func_sum2(num); printf("sum2:%d\n", ret); return 0; } ``` ### 函数指针 #### 定义 函数指针,其本质是一个**指针变量**,该指针指向这个函数。总结来说,**函数指针就是指向函数的指针**。 #### 格式 声明格式:类型说明符 (*函数名) (参数) ```c ret (*p)(args, ...); ``` 其中,ret为返回值,*p作为一个整体,代表的是指向该函数的指针,args为形参列表。其中p被称为函数指针变量 。 关于函数指针的初始化 与数组类似,在数组中,数组名即代表着该数组的首地址,函数也是一样,函数名即是该数组的入口地址,因此,函数名就是该函数的函数指针。 因此,我们可以采用如下的初始化方式: ```c 函数指针变量 = 函数名; ``` > 实例: ```c #include <stdio.h> int max(int a, int b) { return a > b ? a : b; } int main(void) { int (*p)(int, int); //函数指针的定义 //int (*p)(); //函数指针的另一种定义方式,不过不建议使用 //int (*p)(int a, int b); //也可以使用这种方式定义函数指针 p = max; //函数指针初始化 int ret = p(10, 15); //函数指针的调用 //int ret = (*max)(10,15); //int ret = (*p)(10,15); //以上两种写法与第一种写法是等价的,不过建议使用第一种方式 printf("max = %d \n", ret); return 0; } ``` ### 二者区别 总结下二者的区别: #### 定义不同 指针函数本质是一个**函数**,其返回值为指针。 函数指针本质是一个**指针**,其指向一个函数。 #### 写法不同 > 指针函数: ```c int* fun(int x,int y); ``` > 函数指针: ```c int (*fun)(int x,int y); ``` 可以简单粗暴的理解为,指针函数的* 是属于数据类型的,而函数指针的星号是属于函数名的。 再简单一点,可以这样辨别两者:函数名带括号的就是函数指针,否则就是指针函数。 #### 用法不同 1、指针函数:int* func(int a,int b); 普通函数就是int func(int a,int b);,返回值是int,而指针函数就是返回值是指针的函数,即返回值是int*。 2、函数指针:int (*func)(int x,int y); 这里定义了一个函数指针,只不过这个指针的格式有点特殊,正常定义的指针是把指针变量放到最后的,即int* p ;,p是指针变量,但是函数指针是把指针变量放到了中间,即func是指针变量,而且这是一个指向函数的指针变量,而具体指向哪个函数是自己定的。 #### 小结: > 使用函数指针时一定要小心,因为C不会检查参数传递是否正确。 ```c int (*f1)(double); //传入 double,返回 int void (*f2)(char*); //传入 char 指针,没有返回值 double* (*f3)(int,int); //传递两个整数,返回 double 指针 ``` > 不要把返回指针的函数和函数指针搞混。 ```c int *f4(); //f4是一个函数,返回一个整数指针 int (*f5)(); //f5是一个返回整数的函数指针 int* (*f6)(); //f6是一个返回整数指针的函数指针 ``` ## 指针与字符串 ### **1.字符指针可以指向一个字符串**。 我们可以用字符串常量对字符指针进行初始化。例如,有说明语句: ```c char *str = "This is a string."; ``` 是对字符指针进行初始化。此时,字符指针指向的是一个字符串常量的首地址,即指向字符串的首地址。 **这里要注意字符指针与字符数组之间的区别**。例如,有说明语句: ```c char string[ ]="This is a string."; ``` 此时,string是字符数组,它存放了一个字符串。 **字符指针str与字符数组string的区别是**:str是一个变量,可以改变str使它指向不同的字符串,但不能改变str所指的字符串常量。string是一个数组,可以改变数组中保存的内容。 ### **2.实例**: ```c char *str, *str1="This is another string."; char string[100]="This is a string."; ``` **则在程序中,可以使用如下语句**: ```c str++; /* 指针str加1 */ str = "This is a NEW string."; /* 使指针指向新的字符串常量 */ str = str1; /* 改变指针str的指向 */ strcpy( string, "This is a NEW string.") /* 改变字符串的的内容 */ strcat( string, str) /* 进行串连接操作 */ ``` **在程序中,不能进行如下操作**: ```c string++; /* 不能对数组名进行++运算 */✘ string = "This is a NEW string."; /* 错误的串操作 */✘ string = str1; /* 对数组名不能进行赋值 */✘ strcat(str, "This is a NEW string.") /* 不能在str的后面进行串连接 */✘ strcpy(str, string) /* 不能向str进行串复制 */✘ ``` ### **3.其它说明:** \1) 以字符串形式出现的,编译器都会为该字符串自动添加一个0作为结束符,如在代码中写:"abc",那么编译器帮你存储的是"abc\0" \2) "abc"是常量吗?答案是有时是,有时不是。 #### **不是常量的情况**: "abc"作为字符数组初始值的时候就不是,如 ```c char str[] = "abc"; ``` 因为定义的是一个字符数组,所以就相当于定义了一些空间来存放"abc",而又因为字符数组就是把字符一个一个地存放的,所以编译器把这个语句解析为 char str[3] = {'a','b','c'};又根据上面的总结1,所以char str[] = "abc";的最终结果是 char str[4] = {'a','b','c','\0'}; 做一下扩展,如果char str[] = "abc";是在函数内部写的话,那么这里的"abc\0"因为不是常量,所以应该被放在栈上。 #### **是常量的情况:** 把"abc"赋给一个字符指针变量时,如 ```c char *ptr = "abc"; ``` 因为定义的是一个普通指针,并没有定义空间来存放"abc",所以编译器得帮我们找地方来放"abc",显然,把这里的"abc"当成常量并把它放到程序的常量区是编译器最合适的选择。所以尽管ptr的类型不是const char*,并且ptr[0] = 'x';也能编译通过,但是执行ptr[0] = 'x';就会发生运行时异常,因为这个语句试图去修改程序常量区中的东西。 **但是建议的写法应该是const char\* ptr = "abc";这样如果后面写ptr[0] = 'x'的话编译器就不会让它编译通过,也就避免了上面说的运行时异常。** 又扩展一下,如果char* ptr = "abc";写在函数体内,那么虽然这里的"abc\0"被 放在常量区中,但是ptr本身只是一个普通的指针变量,所以ptr是被放在栈上的, 只不过是它所指向的东西被放在常量区罢了。 3) 数组的类型是由该数组所存放的东西的类型以及数组本身的大小决定的。如char s1[3]和char s2[4],s1的类型就是char[3],s2的类型就是char[4], 也就是说尽管s1和s2都是字符数组,但两者的类型却是不同的。 4) 字符串常量的类型可以理解为相应字符常量数组的类型, 如"abcdef"的类型就可以看成是const char[7] 5) `sizeof`是用来求类型的字节数的。如int a;那么无论sizeof(int)或者是sizeof(a)都是等于4,因为sizeof(a)其实就是sizeof(type of a) 6) 对于函数参数列表中的以数组类型书写的形式参数,编译器把其解释为普通的指针类型,如对于void func(char sa[100],int ia[20],char *p) 则sa的类型为char*,ia的类型为int*,p的类型为char* #### 根据上面的总结,例: 对于char str[] = "abcdef";就有sizeof(str) == 7,因为str的类型是char[7], 也有sizeof("abcdef") == 7,因为"abcdef"的类型是const char[7]。 对于char *ptr = "abcdef";就有sizeof(ptr) == 4,因为ptr的类型是char*。 对于char str2[10] = "abcdef";就有sizeof(str2) == 10,因为str2的类型是char[10]。 对于void func(char sa[100],int ia[20],char *p); 就有sizeof(sa) == sizeof(ia) == sizeof(p) == 4, 因为sa的类型是char*,ia的类型是int*,p的类型是char*。 ### **4.区别:** #### **(1)字符数组由若干个元素组成** 每个元素中存放字符串的一个字符,而字符指针变量中存放的是字符串的首地址。 #### **(2)初始化方式不同。** 对字符数组初始化要用static存储类别,在编译时进行。而对字符指针变量初始化不必加static,在实际执行时进行。 #### **(3)赋值方式不同。** 对字符数组不能整体赋值,只能转化成份量,对单个元素进行。而字符指针变量赋值可整体进行。 ## 指向结构体的指针 ### 1. 结构体指针变量声明的一般形式 与一般变量一样,可以使一个指针变量指向结构体,从而形成结构体指针变量。其值是所指向的结构体变量的首地址。通过结构体指针即可访问该结构体变量,这与数组指针和函数指针的情况是相同的。结构体指针变量声明的一般形式为: struct 结构名 *结构指针变量名; 对前面定义的结构体hero,可以使一个指针`phero`指向hero类型的某个变量: ```c struct hero *phero; ``` 既然结构体指针变量也是一种结构体变量,当然可以使用其他的两种定义结构体变量的方式定义结构体指针变量。具体定义方式此处不再叙述。 ### 2.为何需要结构体指针变量 之所以引入结构体指针变量,出于以下原因: l 更易于操作 类似于数组指针比数组更易于操作一样(如排序问题),结构体指针比结构体本身更加的易于操作。 l 更强的通用性 一些早期的C语言实现不支持将结构体变量作为参数传递给函数,但是结构体指针变量却可以。 l 丰富的数据表示 许多奇妙的数据表示,如文件指针,都包含指向结构体的指针。 ### 3.指针变量到底指向什么? 与前面讨论的各类指针变量相同,结构指针变量也必须要先赋值后才能使用。赋值是把结构变量的首地址赋予该指针变量,不能把结构名赋予该指针变量。 例如,下面对结构体指针变量phero1的赋值是正确的: ```c *phero1 = &wusong;//将变量wusong的首地址赋值给指针变量phero ``` 但是下面的赋值是错误的: ```c *phero2 = &hero; //错误!✘ ``` 结构体名和结构体变量是两个不同的概念,不能混淆。结构体名只能表示一个结构形式,编译系统并不为其分配内存空间。只有当某变量被定义为这种类型的结构体时,才为该变量分配存储空间。所以`&wusong`的形式是正确的,表示结构体变量`wusong`的首地址,而`&hero`的形式是错误的,因为系统没有为hero分配内存,也不存在hero的首地址。 ### 4.访问成员 与其他结构体变量一样,可以使用“.”运算符访问结构体指针变量的成员,其一般形式为: (*结构指针变量).成员名 例如,对前面定义的变量`phero`的成员`number`进行访问如下: ```c (*phero).number; ``` 因为成员运算符“.”和指针运算符“*”是同一优先级的运算符,但其结合顺序是从右到左的,所以括号运算符()必不可少,即下面的形式是错误的: ```c *phero.number; ``` 为了防止此类错误,C语言还提供了一种访问结构体成员的方法,就是使用成员指针运算符“->”,使用->运算符访问结构体成员的一般形式如下: 结构指针变量->成员名 例如,下面用这种方式访问结构体指针变量`phero`的成员name: ```c *phero = &linchong; phero->name; ``` 其等价于: ```c (*phero).name; linchong.name; ``` 最后修改:2022 年 01 月 30 日 © 允许规范转载 打赏 赞赏作者 赞 如果觉得我的文章对你有用,请随意赞赏