有一栋楼,里有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。
例如,char*类型的指针解引用时只能访问一个字节,而int*类型的指针解引用能访问四个字节
int n = 0x11223344;
int *pi = &n;
*pi = 0;这里将变量n的四个字节空间的内容都改成0
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;这里只将变量n四个字节中第一个字节的内容改为0
char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。
指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可 以-1。
void*是一种特殊的指针类型,也叫泛型指针(或无具体类型的指针)
优点:可以接收任何类型的指针
缺点:不能进行 指针+-整数的运算,不能进行 解引用操作
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如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改(它存储的地址不能改变),但是指针指向的内容,可以通过指针改变。
数组在内存中连续存放,找到第一个元素地址就能顺藤摸瓜找到所有元素
#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;
}
前提:两个指针指向同一块空间
指针 - 指针的绝对值是指针间的元素个数
#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;
}
地址大小比较
#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;
}
指针指向的位置是未知的、不正确的、随机的,那么这个指针就是野指针。
野指针成因:
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
规避方法,将指针初始化
当不知道指针变量该指向哪里时,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
#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;
}
#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;
}
交换成功。
像这样把变量的地址传递给函数,这就是传址调用。
所以在函数中需要改变主调函数中变量的值,我们可以采用传址调用;如果仅需要在函数内利用变量的值来计算,就采用传值调用。
数组名是数组首元素的地址
#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就是跳过整个数组
#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) 。
之前我们都是在主函数里计算数组元素的个数,那能在函数里计算吗?
#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)。
指针数组是一个存放指针的数组,是数组。
类比,整型数组是存放整型的,字符数组是存放字符的数组。
所以指针数组的每个元素存储的都是地址,类型都为指针类型,每个元素又能通过指针指向一块空间。
一个指针数组arr,长度为5,元素类型为int*类型 即元素都是 整型指针变量 的地址。 int* arr[5] = {&a1,&a2,&a3,&a4,&a5}; 那么这个数组arr的类型 为 int* [5]
#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
数组指针变量是一个指向数组的指针,存储的是数组的地址,它不是数组
类比一下:
整型指针变量 int* p;存储的是一个int型变量的地址,指针类型是 int*。
字符指针变量 char* p;存储的是一个char类型变量的地址,指针类型是 char*。
看看这两个分别是什么:
int *p1[10]; int (*p2)[10];
第一个是 一个数组长度为10,数组元素类型为 int* 的 指针数组,存储的是指针(地址)。
第二个是 一个指向的 数组长度为10 数组元素类型为 int 的 数组指针,这个数组指针变量中存储的是数组的地址。 这个指针变量的类型为 int (*)[10]
注意:[]的优先级要高于*号的,所以在数组指针变量中,必须加上()来保证p先和*结合,否则p和[]先结合,那就是一个指针数组了。
int arr[10] = {0}; int (*p)[10] = &arr;//数组的地址
p和&arr的类型一致,都是int (*)[10]类型。
p是这个数组指针变量的变量名,10表示p指向的数组元素个数,int为数组元素的类型。
#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;
}
函数指针变量是存放函数地址的变量,能通过这个变量或地址来调用函数
函数名 就是函数的地址,也可以在函数名的前面加上&来获取地址(加不加&都一样)
写法:
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。
#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。
来看两段代码
(*(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);
是一个用来存放函数指针的数组
定义:int (*p[3])();
p先和[3]结合表示数组,数组中存放的是int (*)()类型的函数地址。
我们可以这样使用
int func1() { return 1; }
int func2() { return 2; }
int func3() { return 3; }
int (*p[3])() = {func1, func2, func3};
我们来区分一下这几个是什么:
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型的元素。
使用转移表比使用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");
else
printf("输出错误,请选择0-4\n");
} while (input);
return 0;
}
用转移表代码量大大减少,能提高程序效率。
🎉🎉🎉本文内容结束啦,希望各位大佬多多指教!
🌹🌹感谢大家三连支持
💕敬请期待下篇文章吧~
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有