1.指针
1. 指针是什么
我们口头说的指针就是地址,
指针变量是变量,是用来储存地址的。
2. 指针和指针类型
#include <stdio.h> int main() { int a = 0; int* p = &a; *p = 20; printf("%d\n",a); printf("%d\n",*p); return 0; }
对于上面的代码
这里的p是指针变量——是用来存放地址的变量;
可以这样理解从*可以看出p是个指针变量,p指向的内容是int类型的。
*p = 20,此处的 * 是解引用操作符。
&为取地址操作符
指针的类型是根据原来值的类型来确定用什么类型的指针。如:char类型,那就用char* 。
去掉指针变量名剩下的就是指针的类型
关于指针类型的声明有人可能会问用不同的类型声明可以吗?当然可以,但是会出现一些问题。
不同的指针类型决定了指针解引用所移动的步长
如:char类型的,那么它+1就指向下一个字节(向后走一步)
int 类型的,那么它+1就指向到第4个字节处。(向后走4步)
指针的类型代表它所能访问几个字节大小的空间
看下面这个代码
int main() { int a = 0x11223344; char* p = (char*)&a; *p=0; printf("%x", a); return 0; }
此处的&a
应该是int*
类型的,但强转成char*
类型的指针之后,对p进行解引用赋值成0只能改变一个字节,所以输出的结果是11223300。
看计算机输出的结果
指针的大小由电脑的平台所决定的而不是由指针类型决定的。
如果平台上是32位的,那就是4个字节的大小;64位的平台就是8个字节的大小。
看下面这个代码可以看出来:
#include <stdio.h> int main() { printf("%d\n", sizeof(int*)); printf("%d\n", sizeof(char*)); printf("%d\n", sizeof(long*)); printf("%d\n", sizeof(float*)); return 0; }
在我的32位平台上上面的输出结果都是4。
3. 野指针
野指针就是指针访问了位置不可知的地方,造成非法访问
🥇1. 指针没有初始化
比如这样:
int* i; 这样写是不对的,指针i指向的空间不知道 可以这样写: int* i=NULL;
🥈2. 指针越界访问
看下面这个代码:
int arr[5]={1,2,3,4,5}; int* p=arr; int i=0; for(i=0;i<=5;i++) { *(p++)=i; }
该数组里面有5个元素,当i为5时,p
访问的空间超过了数组的范围
造成越界访问,这时p就是野指针.
🥉3. 指针指向的空间被释放
int* f(int* a,int* b) { int c = 0; c = *a + *b; return &c; } int main() { int a = 10, b = 20; int* p=f(&a, &b); printf("%d\n", *p); return 0; }
在调用函数的过程中开辟的空间,出了这个了函数,则开辟的空间返还给操作系统,导致p
指针接受的地址被释放,次数p
为野指针。
虽然在vs 上面依然可以输出,但是确实是错误的。需要注意。
还有这种,和上面的差不多:
int main() { int* p = (int*)malloc(4 * sizeof(int)); int i = 0; if (p != NULL) { for (i = 0; i < 4; i++) { *(p + i) = i; } } free(p); for (i = 0; i < 4; i++) { printf("%d ", *(p + i)); } return 0; }
在p指向的空间被释放后,它所指向的空间被返还给操作系统
再次对它解引用进行访问就造成非法访问。
通常在释放空间后加上
p=NULL
4. 指针运算
- 🚗 指针加减整数
指针加一个整数表示该指针向后走几个该指针所指类型字节的个数。
比如:
int main() { int arr[] = { 1,2,3,4,5,6,7,8 }; int p=*(arr + 4); return 0; }
p就是5,因为数组的每个元素都是int类型的,所以arr+4表示arr向后走4个int类型的字节数,即访问数组第5个元素。
指针减整数也是同理,注意不要越界访问
- 🚓指针相减
指针相减得到的是两个指针之间的元素个数,不要认为是地址相减的结果,不要问为什么,这是这么规定的。
看下面的代码帮助你理解:
int main() { int arr[] = { 1,2,3,4,5,6,7,8 }; int* p1 = arr; int* p2 = arr + 4; printf("%d\n", p2 - p1); return 0; }
输出的结果是4哦😁
- 🚕指针做比较
指针也是有大小的,就比如有高地址与低地址这么一说
c语言标准规定
允许指针与指针指向数组的最后一个元素后面的那个地址进行比较,不允许和指针指向数组第一个元素前面的那个地址进行比较。
再次说明不要问为什么,规定就是规定
例子:
int main() { int arr[] = { 1,2,3,4,5,6,7,8 }; int* p; for (p=arr;p<=&arr[7];p++) { ; } 上面这个可以,下面这个不可以,原因上面说的非常清楚 for (p = &arr[7]; p >= arr; p--) { ; } return 0; }
5. 二级指针
指针的地址怎么储存呢?——用二级指针变量进行储存
int i=0; int* p=&i; int** p1=&p;
指针的指针就是二级指针,用于储存一级指针的地址。要改变i
的值p1
要解引用2次才可以改变
2. 字符指针
能够指向字符数据的指针
形如这样的char* p
这里就讲比较难的地方吧!看下面这个代码
void pd(char* s1,char* s2) { if (s1 == s2) printf("相等\n"); else printf("不相等\n"); } int main() { char arr1[] = "abcef"; char arr2[] = "abcef"; pd(arr1,arr2); char* p1 = "abcef"; char* p2 = "abcef"; pd(p1,p2); return 0; }
这里的arr1和arr2,p1和p2相等吗?看结果
arr1与arr2不相等,p1和p2相等。在数组储存数据时,即使储存的字符串相等也开辟不同的空间。但是p1,p2可不能储存"abcef"这样的常量字符串,它们只是储存了首字符的地址,所以p1和p2相等.并且p1,p2所指向内存的数据不能更改,因为初始化的常量字符串是不能更改的,可以这样写
const char* p1 = "abcef"; const char* p2 = "abcef";
3. 数组指针
数组指针本质上是指针,例如:
int (*p)[5] *说明p是一个指针,指针指向一个数组, 数组中有5个元素,每个元素是int类型
这里的p
的类型为 int (*)[5] 类型。
这样使用,例子:
void f(int (*p)[5],int n) { int i, j; for (i=0;i<5;i++) { for (j=0;j<5;j++) { *(*(p + i) + j) = 1; } } } int main() { int arr[5][5]={1,2,3,4,5,6,7}; f(arr,5); return 0; }
- 这里的二维数组名表示第一行数组的地址,数组的地址要用数组指针进行接收。
下面这个对应着内存布局:
4. 指针数组
指针数组本质上是数组。例如:
int *p[5] 根据运算符的结合性可知, p先与[]结合,说明p是一个数组,数组里面有5个元素, 每个元素是int*类型,也就是指针指向int类型
给个简单的例子:
看代码:简单
int main() { int arr1[] = { 1,2,3 }; int arr2[] = { 4,5,6 }; int arr3[] = { 7,8,9 }; int* p[] = { arr1,arr2,arr3 }; int i = 0, j = 0; for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { printf("%d ", p[i][j]); } printf("\n"); } return 0; }
5.数组传参和指针传参
🥳1️⃣数组名表示的含义
- 一维数组名
一维数组名表示数组首元素地址- 二维数组名
二维数组名表示首行数组的地址
每一行的数组都有自己的名字,对与下面的代码arr2[0]
为第一行数组的名字
sizeof(数组名)
求的是整个数组的大小。
&数组名
取出的是整个数组的地址
例子:看代码
int main() { int arr1[] = { 1,2,3,4,5,6 }; int arr2[][3] = { 1,2,3,4,5,6 }; printf("%d\n",sizeof(arr1));求的整个数组的大小,单位字节24 printf("%d\n", sizeof(arr2));求的整个数组的大小24 printf("%d\n\n", sizeof(arr2[0]));求的是第一行数组的大小12 printf("%p\n", arr1);首元素的地址 printf("%p\n", arr1+1);第2个元素的大小 printf("%p\n", &arr1);这个元素的大小 printf("%p\n\n", &arr1+1);越过整个数组后的地址 printf("%p\n", arr2);首行的地址 printf("%p\n", &arr2 + 1);越过这个数组后的地址 printf("%p\n", arr2[0]);首行首元素的地址 printf("%p\n", &arr2[0] + 1);第二行数组的首元素的地址 return 0; }
- 运行的结果
🥳2️⃣数组传参
- 一维数组传参
f(int arr[]) {} f(int arr[3]) {} f(int* arr) {} 上面这三种都可以,但是最后一种比较好,它能反应出数组名字为数组首元素地址, 像第1与第2种里面[]里面有没有数值的没有问题。 f1(int* arr1[]) {} f1(int* arr1[3]) {} 上面这两种大家应该可以理解 f1(int** arr1) {} arr是数组名,表示首元素地址,每个元素是指针,即指针的地址要用二级指针接受 int main() { int arr[3] = { 1,2,3 }; int* arr1[3]; f(arr); f1(arr1); return 0; } f(int
对于一维数组传参,上面的几种都正确。
- 二维数组传参
f(int p[2][2] ) {} f(int p[][2] ) {} 上面这两种都可以,也都可以理解,前一个数值可以省略, 但是第二个[]的数值不能去掉. f(int(*p)[2] ) {} 二维数组名表示首行数组的地址,要用数组指针进行接收 int main() { int arr[2][2]; f(arr); return 0; }
🥳3️⃣指针传参
- 一级指针传参
f(int* p) {} int main() { int a = 0; int* p = &a; f(p); return 0; }
一级指针传参用一级指针进行接收
思考:形参为一级指针的时候,实参可以为什么样的类型
1️⃣实参可以为一维数组的数组名
2️⃣可以为一级指针
- 二级指针传参
f(int** p1) {} int main() { int a = 0; int* p = &a; int** p1 = &p; f(p1); return 0; }
二级指针传参用二级指针进行接收
思考:形参为一级指针的时候,实参可以为什么样的类型
1️⃣实参可以为指针数组
2️⃣可以为二级指针
3️⃣可以为一级指针的地址
6. 函数指针
引例:
int add(int x,int y) { return x + y; } int main() { &add; return 0; }
对于函数的地址应该怎么储存呢?那就需要用函数指针类型进行接收。
对于上面的代码,我们可以这样写
int (*p)(int,int)
对于上面这个可以这么理解:
* 告诉我们P是个指针,从后面的小括号可以看出是个函数,函数的参数有两个,都是int 类型的,返回类型是int
下面这样进行使用:
int(*p)(int,int)=&add; (*p)(1, 2); (**p)(1, 2); p(1, 2); add(1, 2);
我们发现上面这3种都正确,说明p变量前面的星号没有任何用处,p就相当与add。
看下面的这个代码可以更好的理解:我们发现打印的结果相同
printf("%p\n",add); printf("%p\n", &add);
- 看着两个表示的是什么:
(*(void (*)())0)()
void (*signal(int , void(*)(int)))(int)
我们一点一点拆分看
(*(void (*)())0)(); 1.void (*)()这是一个函数指针类型,无参数,返回类型为空(void) 2.(void (*)())0这个是强制类型转换,把0强转为函数指针类型 3.*解引用变成函数,调用函数,函数无参数 void (*signal(int , void(*)(int)))(int); 1.void(*)(int)这是一个函数指针类型,返回类型为空,参数为int类型的 2.signal(int , void(*)(int))这是一个函数,函数名为signal, 参数有俩个,分别为int类型和函数指针类型。 3.函数要有返回类型,去掉signal(int , void(*)(int)),就是该函数的返回类型 该函数的类型为void (*)(int),函数指针类型。 对这个函数可以进行一下好看的改变 typedef void (*v_i)(int) 然后这样写:v_i signal(int,v_i);这样看着是不是非常舒服
7. 函数指针数组
函数指针数组
是个数组,里面的元素是是函数指针
给个例子:
void f1() {} void f2() {} int main() { void (*p[2])()={f1,f2}; 这个就是函数指针数组, 这样理解p先和[]结合说明p是一个数组,数组有2个元素, 每个元素的个数是void (*)()函数指针类型 return 0; }
8. 指向函数指针数组的指针
指向函数指针数组的指针
它是一个指针,指向一个数组,数组的每个元素是函数指针类型
看下面的这个代码:
void f1() {} void f2() {} int main() { void (*p[2])()={f1,f2}; void(*(*p1)[2])()=&p; p1就是一个函数指针数组的指针 return 0; }
9. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
下面有个例子:
void f1() {} f(int a,void (*p)()) { p(); } int main() { int a; f(a, f1); 把f1函数作为参数传给f函数,通过函数指针来调用f1函数。 return 0; }
10. 小型计算器项目(对7,8,9等知识点的运用)
#include <stdio.h> int add(int x, int y) { return x + y; } int sub(int x, int y) { return x - y; } int mul(int x, int y) { return x * y; } int div(int x, int y) { return x / y; } void menu() { printf("**** 请选择 ****\n"); printf("**** 1.加 2.减 **********\n"); printf("**** 3.乘 4.除 **********\n"); printf("************* 0退出 ***************\n"); } f(int (*p)(int, int)) { int a, b; printf("请输入两个整数:>"); scanf("%d%d", &a, &b); printf("结果为%d\n", p(a, b)); } int main() { int n; int (*gather[5])(int, int) = { 0,add,sub,mul,div }; 该函数指针数组里面存的是各个函数的地址,首个元素设置为0, 方便用下标访问 do { menu(); 该函数打印菜单,供用户选择 scanf("%d", &n); if (n == 0) { printf("退出程序\n"); } else if (n > 0 && n < 5) { f(gather[n]); 这就是一个回调函数 } else { printf("选择错误请从新选择\n"); } } while (n); return 0; }
运行的基本情况如下: