?欢迎关注 轻松拿捏C语言系列,来和 小哇 一起进步!✊
?感谢大家的阅读、点赞、收藏和关注?
目录?
一、指针与内存?
二、指针变量 、取地址操作符&和解引用操作符* ?
三、指针变量类型的意义?
1.指针变量类型不同决定了在解引用的时候能访问的字节数不同?
2.指针变量类型决定了指针向前或向后的步长?
3.void*指针?
四、const修饰指针☀️
五、指针运算?
1、指针加减运算?
2、指针相减?
3、指针关系运算?
六、野指针?️
1.指针变量没有初始化
2.指针越界访问
3.指针指向的空间被释放掉了
七、传值调用与传址调用?
八、一维数组与指针?
1.数组名的理解
2.用指针访问数组
3.一维数组传参本质
九、指针数组?
1.定义
2.用指针数组模拟二维数组
十、字符指针变量 ?
十一、数组指针变量?
1.定义
2.数组指针变量的初始化:
3、二维数组传参的本质:
十二、函数指针变量?
1.定义
2.使用
3.两段代码
十三、函数指针数组?
1.定义
2.区分两个数组,两个指针变量
3.函数指针数组用途——转移表
一、指针与内存
有一栋楼,里有200个房间,假如我们要去某个房间找某个人,然后他说他在C304,我们就能通过门牌号C304快速找到他所在房间。
在计算机中内存划分为一个个内存单元,每个内存单元也有编号,每个内存单元占1字节的空间大小,1字节又等于8个比特位
这相当于,内存就是一栋楼,每个内存单元就是一个房间,内存单元编号就是房间门牌号,房间里有8个床位。
内存单元编号==地址==指针
二、指针变量 、取地址操作符&和解引用操作符*
int a = 10;
这里创建了一个整型变量a,占四个字节,所以就会向内存申请四个字节大小的连续空间,每个字节的内存单元都有编号。
通过取地址符& 我们可以得到a所占四个字节中 地址最小的内存单元 的地址,该地址就是变量a的地址
因为这四个字节的空间连续,我们得到了这一个地址,就能挨着访问另外的地址
拿到地址有什么用?
我们可以将地址存储在一个变量中,用来存储地址的这个变量就叫做指针变量
int a = 10;
int* b = &a;
这里b就是一个指针变量,它的类型是int*类型。int*中*说明b是指针变量,int说明b指向的对象是整型(即b中存储的这个地址对应的变量a是整型)
通过解引用操作符*,我们可以改变指针变量指向的内容
int a = 10;
int* b = &a;
*b = 5;这样a中的值就从10变成了5。b中存放的是a的地址,*b就是找到b中存放的地址对应的空间,所以其实*b就是a了,*b=5就是把a变成了5。
通过解引用操作符没有直接修改变量a,而是通过地址来间接修改
另外指针变量的大小与它的类型没有关系,在32位平台下(32个比特位),指针变量大小是4个字节;在64位平台下指针变量是8个字节。
总结一下关于指针p的三个值:
int a = 1;
int* p = &a;
①p p中放着一个地址,这里是a的地址
②*p p指向的对象,这里为a
③&p 表示变量p的地址
二级指针:存放一级指针变量地址的变量
int a = 10;
int* p =&a;
int** m = &p;
对*m = p,**m = *p = a。
三、指针变量类型的意义
1.指针变量类型不同决定了在解引用的时候能访问的字节数不同
例如,char*类型的指针解引用时只能访问一个字节,而int*类型的指针解引用能访问四个字节
int n = 0x11223344;
int *pi = &n;
*pi = 0;这里将变量n的四个字节空间的内容都改成0
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;这里只将变量n四个字节中第一个字节的内容改为0
2.指针变量类型决定了指针向前或向后的步长
char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。
指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可 以-1。
3.void*指针
void*是一种特殊的指针类型,也叫泛型指针(或无具体类型的指针)
优点:可以接收任何类型的指针
缺点:不能进行 指针+-整数的运算,不能进行 解引用操作
四、const修饰指针
const修饰变量时,变量不能被修改
#include <stdio.h>int main(){ int m = 0; m = 20;//m是可以修改的 const int n = 0; n = 20;//n是不能被修改的 return 0;}
但是这里我们可以不直接修改变量n,可以通过它的地址来间接修改
但我们给n加上const的目的就是为了使它不能被修改,所以我们应该让p拿到n的地址后也不能间接修改n
我们可以在*p前面加上const const int *p = &n; 或者 int const *p 这样就不能通过指针变量p来间接修改n的值了
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变(就是他存储的地址可以改变)。
const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改(它存储的地址不能改变),但是指针指向的内容,可以通过指针改变。
五、指针运算
1、指针加减运算
数组在内存中连续存放,找到第一个元素地址就能顺藤摸瓜找到所有元素
#include <stdio.h>int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i<sz; i++) { printf("%d ", *(p+i));// p+i 这⾥就是指针+整数 } return 0;}
2、指针相减
前提:两个指针指向同一块空间
指针 - 指针的绝对值是指针间的元素个数
#include <stdio.h>int my_strlen(char *s)//s为字符串常量abc中a的地址{ char *p = s; while(*p != '\0' ) p++;当p指向\0,不再++ return p-s;指向\0的地址p减指向a的地址s,所以p-s为3}int main(){ printf("%d\n", my_strlen("abc"));//打印3 return 0;}
3、指针关系运算
地址大小比较
#include <stdio.h>int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int sz = sizeof(arr)/sizeof(arr[0]); while(p<arr+sz) //指针的⼤⼩⽐较 { printf("%d ", *p); p++; } return 0;}
六、野指针
指针指向的位置是未知的、不正确的、随机的,那么这个指针就是野指针。
野指针成因:
1.指针变量没有初始化
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
规避方法,将指针初始化
当不知道指针变量该指向哪里时,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
2.指针越界访问
#include <stdio.h>int main(){ int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0;}
3.指针指向的空间被释放掉了
#include <stdio.h>int* test(){ int n = 100;//局部变量n return &n;//该函数结束后,创建的变量n会被销毁}int main(){ int*p = test(); printf("%d\n", *p); return 0;}
七、传值调用与传址调用
通过一个题来感受一下什么是传值调用,什么是传址调用
写一个函数,交换整型变量的值
#include <stdio.h>void Swap(int x, int y){ int tmp = x; x = y; y = tmp;}int main(){ int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap(a, b); printf("交换后:a=%d b=%d\n", a, b); return 0;}
但是我们却发现a和b的值却没有交换
调试看一下:
通过调试我们发现,虽然a确实把值传给了x,b把值传给了y,但是a的地址和x的地址不是同一个地址,b的地址和y的地址也不是同一个地址。
这是因为变量x和y是在Swap函数内部创建的,变量x和变量y是两个独立的空间,因此x和y交换值对变量a和b是没有影响的。
像这样把变量的值传给函数,这就是传值调用。
把实际参数传递给形式参数时,形参会单独创建一个空间来接收实参,因此形参的改变对实参没有影响。
所以我们可以将a和b的地址传过去,通过地址将a和b的值交换。
#include <stdio.h>void Swap(int* x, int* y){ int tmp = *x; *x = *y; *y = tmp;}int main(){ int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0;}
交换成功。
像这样把变量的地址传递给函数,这就是传址调用。
所以在函数中需要改变主调函数中变量的值,我们可以采用传址调用;如果仅需要在函数内利用变量的值来计算,就采用传值调用。
八、一维数组与指针
1.数组名的理解
数组名是数组首元素的地址
#include <stdio.h>int main(){ int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0;}
数组名和数组首元素打出的地址一样。
但是有两个例外:
1、 sizeof(数组名),sizeof中单独放数组名,这的数组名表示整个数组,计算的是整个数组的大小, 单位是字节
2、 &数组名,这的数组名表示整个数组,取出的是整个数组的地址
这里讲一下&arr和arr的区别:
可以看出它们三个打印出的一模一样,没区别呀?
这时就发现,&arr[0]和arr加1,它们地址都只加了 4,而&arr加1后,它的地址加了40。
这时因为&arr[0]和arr都是首元素的地址,它们加1,就是跳过一个元素
而&arr是整个数组的地址,它加1就是跳过整个数组
2.用指针访问数组
#include <stdio.h>int main(){ int arr[10] = {0}; //输⼊ int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输⼊ int* p = arr; for(i=0; i<sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i=0; i<sz; i++) { printf("%d ", *(p+i)); } return 0;}
将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)。
同理arr[i] 应该等价于 *(arr+i) 。
3.一维数组传参本质
之前我们都是在主函数里计算数组元素的个数,那能在函数里计算吗?
#include <stdio.h>void test(int arr[]){ int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n", sz2);}int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n", sz1); test(arr); return 0;}
这个代码看上去感觉sz1和sz2算出来是一样的,但并不是这样。
上面讲过,arr表示数组首元素的地址,因此在形参中我们应该用一个int* 类型的指针变量来接受实参,所以形参中int arr[]只是写成了数组的形式,本质上还是一个指针变量。
所以在函数内部sizeof(arr)计算的是数组首元素的地址的大小,并不是整个数组的大小
(这里提一个点,在32位的环境下 指针变量占4字节,64位环境下 指针变量占8字节,所以不同环境下sz2可能算出来一个是1,一个是2)。
九、指针数组
1.定义
指针数组是一个存放指针的数组,是数组。
类比,整型数组是存放整型的,字符数组是存放字符的数组。
所以指针数组的每个元素存储的都是地址,类型都为指针类型,每个元素又能通过指针指向一块空间。
一个指针数组arr,长度为5,元素类型为int*类型 即元素都是 整型指针变量 的地址。
int* arr[5] = {&a1,&a2,&a3,&a4,&a5};
那么这个数组arr的类型 为 int* [5]
2.用指针数组模拟二维数组
#include <stdio.h>int main(){ int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 int* parr[3] = {arr1, arr2, arr3}; int i = 0; int j = 0; for(i=0; i<3; i++) { for(j=0; j<5; j++) { printf("%d ", parr[i][j]); } printf("\n"); }
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。
但这并非是二维数组,二维数组是连续的一块空间,但指针数组模拟的并不是连续的。
十、字符指针变量
有一种指针类型是 char* 类型
它是把一个字符的地址放进指针变量中
#include<stdio.h>{ char a = 'w'; char* p = &a; printf("%c\n",*P); return 0;}
请问下面这个代码是把⼀个字符串放到pstr指针变量里了吗
int main(){ const char* pstr = "hello Bao Gengxiaowa."; printf("%s\n", pstr); return 0;}
并不是!
它是将字符串hello Bao Gengxiaowa.的首元素地址即h的地址放进指针变量中。
用%s打印字符串,只需要传首元素的地址。
现在来看一段代码:
#include <stdio.h>int main(){ char str1[] = "hello Bao Gengxiaowa."; char str2[] = "hello Bao Gengxiaowa."; const char *str3 = "hello Bao Gengxiaowa."; const char *str4 = "hello Bao Gengxiaowa."; if(str1 ==str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if(str3 ==str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0;}
❓ 你觉得程序允许后屏幕会打印哪些语句?
你对了吗?
这是因为str1和str2是两个数组首元素的地址,是两个不同的地址。
所以str1、str2 not same
而str3、str4都是字符指针变量,都存放的是字符串 hello Bao Gengxiaowa.的首元素h的地址。
所以str3、str4 same
十一、数组指针变量
1.定义
数组指针变量是一个指向数组的指针,存储的是数组的地址,它不是数组
类比一下:
整型指针变量 int* p;存储的是一个int型变量的地址,指针类型是 int*。
字符指针变量 char* p;存储的是一个char类型变量的地址,指针类型是 char*。
看看这两个分别是什么:
int *p1[10];
int (*p2)[10];
第一个是 一个数组长度为10,数组元素类型为 int* 的 指针数组,存储的是指针(地址)。
第二个是 一个指向的 数组长度为10 数组元素类型为 int 的 数组指针,这个数组指针变量中存储的是数组的地址。 这个指针变量的类型为 int (*)[10]
注意:[]的优先级要高于*号的,所以在数组指针变量中,必须加上()来保证p先和*结合,否则p和[]先结合,那就是一个指针数组了。
2.数组指针变量的初始化:
int arr[10] = {0};
int (*p)[10] = &arr;//数组的地址
p和&arr的类型一致,都是int (*)[10]类型。
p是这个数组指针变量的变量名,10表示p指向的数组元素个数,int为数组元素的类型。
3、二维数组传参的本质:
#include <stdio.h>void test(int a[3][5], int r, int c){ ………}int main(){ int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}}; test(arr, 3, 5); return 0;}
这里实参是二维数组,形参也是二维数组的形式 ,知道了数组指针后 参数还能有别的写法吗?
二维数组可以看成是一个 一维数组 的数组,每一行就是一个一维数组,那么二维数组首元素的地址就是第一行的地址
第一行数组元素类型为 int [5],所以第一行元素的地址的类型为 int (*)[5]
所以二维数组传参的本质是 传递了地址,传递的是第一行这个一维数组的地址。
所以形参也可以写成指针形式:
#include <stdio.h>void test(int (*p)[5], int r, int c){ int i = 0; int j = 0; for(i=0; i<r; i++) { for(j=0; j<c; j++) { printf("%d ", *(*(p+i)+j)); } printf("\n"); }}int main(){ int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}}; test(arr, 3, 5); return 0;}
十二、函数指针变量
1.定义
函数指针变量是存放函数地址的变量,能通过这个变量或地址来调用函数
函数名 就是函数的地址,也可以在函数名的前面加上&来获取地址(加不加&都一样)
写法:
void test(){ printf("hehe\n");}void (*pf1)() = &test;void (*pf2)()= test;int Add(int x, int y){ return x+y;}int(*pf3)(int, int) = Add;int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
那int(*pf3)(int, int)举例,pf3是函数指针变量的变量名,
函数指针变量pf3的类型(就是函数地址的类型)是int(*)(int,int)
它指向的函数的参数有两个,类型都为int,函数的返回值类型为int。
2.使用
#include <stdio.h>int Add(int x, int y){ return x+y;} int main(){ int(*pf3)(int, int) = Add; printf("%d\n", (*pf3)(2, 3)); printf("%d\n", pf3(3, 5)); return 0;}
这里printf中的*pf3和pf3都是函数Add地址,所以(*pf3)(2,3)和pf3(3,5)都是在调用函数Add。
3.两段代码
来看两段代码
(*(void (*)())0)();
这个代码中,void (*)()是一个函数指针类型,它指向的函数没有形参,返回值类型为void。
void (*)()放在整数0的前面表示强制类型转换,将整型的数字0转换成void (*)()类型的 地址0。
所以这是一次函数调用,调用0地址处放的那个函数,0地址处放的函数没有参数,返回值也是void
void (*signal(int , void(*)(int)))(int);
这整个代码表示的是一个 函数的声明。
函数名字是signal,函数参数有两个,一个是int类型,一个是 void(*)(int)函数指针类型。
函数的返回值类型也是 void(*)(int)函数指针类型,也就是说函数signal的返回值是一个函数的地址。
但是 我们并没有写成 void(*)(int) signal(int , void(*)(int)),而是把函数名和参数放进返回值类型里面,所以就是void (*signal(int , void(*)(int)))(int);
十三、函数指针数组
1.定义
是一个用来存放函数指针的数组
定义:int (*p[3])();
p先和[3]结合表示数组,数组中存放的是int (*)()类型的函数地址。
我们可以这样使用
int func1() { return 1; } int func2() { return 2; } int func3() { return 3; } int (*p[3])() = {func1, func2, func3};
2.区分两个数组,两个指针变量
我们来区分一下这几个是什么:
1、int (*p[3])(); //函数指针数组
2、int*(p[3]); //指针数组
3、int (*p)(); //函数指针变量
4、int (*p)[3]; //数组指针变量
第1个中p是一个数组,它包含 3 个元素,每个元素都是int (*)()类型
第2个中,p是一个数组,有3个元素,每个元素都是int*类型
第3个中,p是一个指针变量,它存储的是一个函数的地址,这个函数返回值为int型,没有形参
第4个中,p也是一个指针变量,存储的是一个数组的地址,数组有3个int型的元素。
3.函数指针数组用途——转移表
使用转移表比使用switch语句更加灵活,因为你可以动态地改变转移表的内容,而不需要修改调用转移表的代码。
举例:分别用switch和转移表来实现一个计算器功能
用switch:
#include<stdio.h>void menu(){printf("====================================\n");printf("********* 1.add 2.sub **********\n");printf("********* 3.mul 4.div **********\n");printf("********* 0.exit 退出 **********\n");printf("====================================\n");}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;}int main(){int input = 0;int x = 0;int y = 0;int ret = 0;do{menu();printf("请选择:"); scanf("%d", &input); switch (input) { case 1: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("ret = %d\n", ret); break; case 2: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break;} while (input);return 0;}
用函数指针数组(转移表):
#include<stdio.h>void menu(){printf("====================================\n");printf("********* 1.add 2.sub **********\n");printf("********* 3.mul 4.div **********\n");printf("********* 0.exit 退出 **********\n");printf("====================================\n");}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;}int main(){int input = 0;int x = 0;int y = 0;int ret = 0; //函数指针数组int (*arr[5])(int, int) = {0,add,sub,mul,div };do{menu();printf("请选择:");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请输入两个操作数");scanf("%d %d", &x, &y);ret = arr[input](x, y);printf("%d\n", ret);}else if (input == 0)printf("退出计算器\n");elseprintf("输出错误,请选择0-4\n");} while (input);return 0;}
用转移表代码量大大减少,能提高程序效率。
???本文内容结束啦,希望各位大佬多多指教!
??感谢大家三连支持
?敬请期待下篇文章吧~