1 指针和数组
深刻理解数组名
以前我们经常讲数组名就是首元素地址,真的就简单的是这样吗?
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//%p是专门打印地址的
printf("&arr[0]= %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
printf("arr = %p\n", arr);//数组名是首元素地址,三个结果一样
return 0;
}
从上面的代码中我们发现了,数组名和数组首地址一样,可以看出数组名是数组首元素地址,这里的&地址符号,&arr和arr和&arr[0]地址结果也一样
sizeof在数组上的使用
数组名是首元素地址的话,那sizeof(数组名)的结果是多少呢
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//sizeof返回的是size_t类型,%zd来打印
printf("%zd\n", sizeof(arr));
printf("%zd\n", sizeof(arr[0]));
printf("%d\n",sizeof(arr)/sizeof(arr[0]));//求出数组元素个数
return 0;
}
sizeof(数组名)取出的是整个数组,计算的是整个数组所占字节大小
这里的sizeof ( arr[0) 取出的是数组一个元素所占字节大小
sizeof(arr) / sizeof(arr[0]),求出整个数组所占字节大小 除上一个元素所占字节大小
可以求出数组元素个数
arr和&arr的区别
上面我们讲arr &arr[0] 和&arr,结果相同,但是&arr是取出的是整个数组的地址,他们还是有区别的,请看下面代码
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0]= %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
printf("arr = %p\n", arr);//数组名是首元素地址,三个结果一样
printf("&arr[0]+1= %p\n", &arr[0]+1);//跳过一个int 字节指向arr[1]的地址
printf("&arr+1 = %p\n", &arr+1);//跳过整个数组10个int字节所指向的地址
printf("arr+1 = %p\n", arr+1);//跳过一个int 字节指向arr[1]的地址
printf("%zd\n", sizeof(arr));//输出的是整个数组所占字节
return 0;
}
这里的&arr[0]+1和arr+1都是跳过一个int 类型的字节 指向的都是arr[1]的地址,然而&arr+1跳过40个字节跳过整个数组
sizeof求数组相关字节数
sizeof是操作符, sizeof计算操作数所占内存的⼤⼩,单位是字节,不关心存放什么类型的数据。
#include<stdio.h>
int main()
{
int a[] = { 1,2,3,4 };
printf("%zd\n", sizeof(a));//整个数组大小,16
printf("%zd\n", sizeof(a + 0));//指向数组首元素地址,地址都是4/8个字节
printf("%zd\n", sizeof(*a));//数组首元素类型所占的字节数4
printf("%zd\n", sizeof(a + 1));//arr[1]的地址,地址所占字节都是4/8
printf("%zd\n", sizeof(a[1]));//其下标为1元素的内容的类型所占4个字节
printf("%zd\n", sizeof(&a));//&a取出整个数组的地址,地址都是4/8个字节
printf("%zd\n", sizeof(*&a));//&先取出整个数组,在解引用获得整个数组,16
printf("%zd\n", sizeof(&a + 1));//跳过整个数组,指向数组后面的地址,地址都是4/8个字节
return 0;
}
只要是地址都是4/8个字节,无论你是整个数组的地址还是数组中某个元素的地址
元素的话我们要判断是整个数组的元素还是数组中某个元素
x64环境下(64位平台)
x86环境下(32位平台)
指针访问一维数组
有了上面对数组的深刻了解,我们可以进一步学习使用指针访问数组
我们已经知道数组名是首元素地址,那我们能不能使用指针来接受呢
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0;i < 10;i++)
{
scanf("%d", p+i);//输入时 p对应的是数组首元素地址,所以不需要&取地址符号,p+i==&arr[i]
printf("%d ", *(p + i));//*解引用取出所对应的数值,*(p+i)==p[i]==arr[i]
}
return 0;
}
使用int* p来存放数组首元素,我们知道数组是连续存储的,所以可以通过移动指针来访问数组
例如,上面的代码中 *(p+i)==p[i]==arr[i] ==(arr+i)都是等价的
一维数组传参
数组是可以传参给函数的,那数组传参的本质是什么呢
#include<stdio.h>
void test2(int* p)
{
printf("%zd\n", sizeof(p));//直接相当于指针所占字节数
}
void test1(int arr[])//既然传递的是数组名,那就是地址,地址需要用指针来接受,指针4/8个字节
{
printf("%zd\n", sizeof(arr));//这时候所测的是指针类型大小
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zd\n", sizeof(arr));
test1(arr);//传递的是数组名
test2(arr);//传递的是数组名
return 0;
}
x86环境下
x64环境下
我们可以看出数组传参其实是首元素地址,因此我们传给函数时候需要指针接收,那么在函数内部sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是整个数组的⼤⼩(单位字节)指针接收地址,所以结果是4/8取决于环境
所以我们在数组传参时候,函数形参可以是数组接收,也可以使用指针接收
指针数组
指针函数是指针呢,还是数组呢
我们可以类比一下,整形数组是一个存放整形的数组,字符数组是一个存放字符的数组
那么指针数组,是一个存放指针的数组
指针数组每个元素都是存放地址(指针)
使用指针数组模拟实现二维数组
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("%d ", *((parr[i]) + j));
printf("%d ", *(*(parr+i) + j));
}
printf("\n");
}
return 0;
}
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整个⼀维数组,parr[i][j]就是整型⼀维数组中的元素
并且这里的,我们只是模拟的二维数组,并非真正的二维数组,因为二维数组存储是连续的,我们这里的每一行都是非连续的。
这里的paar[i]表示的是每一行的首元素地址,通过指针的移动来访问三个数组
这里的parr[i][j]和* ((parr[i]) + j)和 *(*(parr+i) + j)都是等价的
运行结果如下
数组指针
之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
数组指针变量毋庸置疑肯定是指针变量
指针的话我们有字符float* 类型的指针,存放的是浮点型数据的地址
int* 类型的指针, 存放的是整形数据的地址
那数组指针是什么样呢
int* arr1[5];
int(*arr2)[5];
下标引用操作符[ ]的优先级要⾼于解引用操作符*号的
结合上面所学的指针数组,arr1是指针数组,数组元素类型为int*
arr2是数组指针,数组元素类型为int(*)[5]
那我们如何使用呢
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int (*p)[10] = &arr;
return 0;
}
我们之前所讲的&arr,是取出整个数组的地址,因此我们需要指针数组来接收数组的地址
从调试窗口我们可以看出p和&arr类型都是是 int(*)[10]
二维数组传参
有了数组指针,我们可以学习如何使用其接收二维数组,因为二维数组是按行排列的,每一行相当于一个一维数组
#include<stdio.h>
//函数指针接收,每行有5个元素
void test(int(*p)[5], int row, int col)
{
int i = 0;
for (i = 0;i < row; i++)
{
int j = 0;
for (j = 0;j < col;j++)
{
printf("%d ", *(*(p + i) + j));
//打印的和上面模拟实现二维数组的一样,有几种打印方式
}
printf("\n");
}
}
int main()
{
int arr[2][5] = { {1,2,3,4,5},{6,7,8,9,10} };
test(arr, 2, 5);//二维数组传参其实是第一行的地址
return 0;
}
我们函数在接收二维数组的时候,除了使用数组接收,我们可以使用数组指针来接收,因为我们要知道,二维数组的数组名是第一行的地址 所以上面的传参,传过去的第一行数组指针类型是int(*)[5]类型,传递的是第一行这个一维数组的地址。
二维数组在进行传参的时候可以使用二维数组接收,也可以使用数组指针接收
字符指针
我们知道的字符指针类型是char*
#include<stdio.h>
int main()
{
char ch = 's';
char* p = &ch;
printf("%c\n", *p);
return 0;
}
上面是使用指针接收一个字符
那我们可以使用字符指针来接收一个字符串吗
肯定是可以的
#include<stdio.h>
int main()
{
const char* p = "abcdef";//这里是把字符串的首地址传递给p,这样就可以直接顺藤摸瓜找到字符串中所有字符
printf("%s\n", p);//指针指向字符串的首地址,并非整个字符串
return 0;
}
我们要注意我们使用指针接收字符串的时候,接收的仅仅只是字符串的首字符的地址
上面的p接收的是字符h的地址,并非将整个字符串全部放进去
我们看下面这个例子,加强理解
#include <stdio.h>
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
//str1和str2是创建连个字符数组,虽然内容相同,但这里直接比较的是地址,地址不相同
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
//arr3和arr4为指针指向相同的字符串首地址,所以对应的地址相同
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
我们要知道我们if语句里面比较的是地址是否相同,结果是str1 != str2,str3==str4
因为str1和str2是我们创建的两个数组,会开辟不同的空间,地址不会相同,而str3和str4这两个字符指针,指向的是一个相同的字符串,指向相同的内存,所以str3和str4指向的地址相同。
2 指针与函数
毋庸置疑函数指针就是存放函数地址的指针
函数名的理解
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
//可以发现,函数名就是地址,这个&地址符号可以省略
printf("%p\n", add);
printf("%p\n", &add);
return 0;
}
从上面我们不难发现,函数名就是地址,因此这里的&取地址符号是可以省略的
函数指针变量创建和使用
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int x, int y) = &add;
int (*pff)(int, int) = add;
//在使用函数指针的时候,可以把函数指针的*解引用操作符省略
printf("%d\n", pf(2, 3));
printf("%d\n", (*pff)(3, 3));
return 0;
}
运行结果如下
int (*pf)(int x,int y) = &add;
这里的pf类型为int(*)(int x,int y)
(int x,int y)是参数类型和个数
最前面的是返回类型,这里的返回类型为int
我们在定义的时候,这里的参数和&取地址符号是可以省略的 ,但参数是不可以省略的
在使用的时候,这里的*解引用操作符也是可以省略的,但是要是有*解引用操作符必须和函数指针放在括号里
函数指针数组
我们把函数的地址存放在一个数组中,这样就被成为函数指针数组
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
parr1 先和 [ ] 结合,parr1是数组,数组的内容是函数指针类型 int(*)()
有了函数指针数组,我们可以把一些函数放在一个数组中
转移表
函数指针的应用:转移表
我们运用这完成计算器的+ - * / 运算
我们只是简单的使用函数指针完成功能,功能上可能会有一些细节疏忽
#include<stdio.h>
void menu()
{
printf("*******1.add 2.sub*******\n");
printf("*******3.mul 4.div*******\n");
printf("*******0.exit *******\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 x = 0;
int y = 0;
int input = 0;
int ret = 0;
do
{
//打印菜单选择加减乘除
menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个整数: ");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个整数: ");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个整数: ");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个整数: ");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("%d\n", ret);
break;
//0退出计算
case 0:
printf("退出计算\n");
default:
printf("请重新输入\n");
break;
}
} while (input);//输入0停止
return 0;
}
我们利用函数指针数组,将这些函数放在一个函数指针数组中
int (*p[5])(int x, int y) = { NULL,add,sub,mul,div };
创建一个函数指针数组,进行加减乘除运算选择
输入和输出
printf("请输入两个整数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x,y);//借助函数指针数组调用加减乘除运算函数
printf("%d\n", ret);
根据我们输入选择进入对应函数,在进行要进行的运算
#include<stdio.h>
void menu()
{
printf("*******1.add 2.sub*******\n");
printf("*******3.mul 4.div*******\n");
printf("*******0.exit *******\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 x = 0;
int y = 0;
int input = 0;
int ret = 0;
int (*p[5])(int x, int y) = { NULL,add,sub,mul,div };//创建一个函数指针数组,进行加减乘除运算选择
do
{
menu();
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个整数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x,y);//借助函数指针数组调用加减乘除运算函数
printf("%d\n", ret);
}
else if (input == 0)
{
printf("退出计算\n");
break;
}
else
{
printf("输入错误,请重新选择:\n");
}
} while (input);
return 0;
}
这样我们需要增加新的运算,只需加入函数指针数组中和新的函数定义。
3 两段有趣的代码
(*( void (*)() )0)();
( *( void (*)() )0 )();
这个代码可以看成
这里的 ( void (*)() ) 0-- 强制类型转换
将int类型的0转换成函数指针类型,意思是想让0做函数的地址
(*( void (*)() )0)();
1. 将0转化成函数的地址,但这个这个函数是没有参数的,返回类型是void空类型
2. 通过去调用0地址出的这个函数
void (*signal(int, void(*)(int) ))(int);
void (*signal ( int, void(*)(int) ) ) (int);
一次函数声明,函数名是singal
两个参数第一个int 第二个是函数指针void(*)(int)类型
参数是int 返回值是void空类型
signal函数返回值是函数指针类型void(*)(int),参数为int
通过上面这两段代码让我们知道如果我们没有规范的写代码,这样会导致别人很难看懂你的代码,所以我们写代码时候要写规范