目录
1、什么是指针
指针就是地址,指针变量就是存放地址的变量。
指针变量的大小都是统一的,32位环境下为4个字节,64位环境下为8个字节。本文的指针类型都是占用8个字节。
#include <stdio.h>
int main()
{
int a = 10;
int *p = &a; // 指针的类型为 int*,指向int型元素;
printf("*p = %d\n",*p); // 指针解引用,打印p所指向的元素
printf("&p = %p\n",&p); // 打印指针p的地址
printf("sizeof(p) = %d\n",sizeof(p)); // 打印指针p的大小
return 0;
}
2、指针的性质
(1)指针的类型决定指针解引用时的访问的字节数量;
(2)指针的类型决定指针进行加减运算时指针偏移的字节数量(偏移的字节数量 = n * sizeof(所指向数据类型) )。
例子:利用指针的性质判断机器大小端问题
由于机器存储数据的方式分为大端存储和小端存储,为了判断我们的计算机的存储方式为哪种存储方式,我们可以利用指针的性质来知晓机器的存储方式。
这时,我们就可以设置一个整型指针ptr指向整型变量a,a赋值为1,然后再将这个整型指针ptr强制类型转换为字符型指针。由于字符型指针解引用后只可以访问1个字节的内存,所以我们只需要通判断ptr解引用后是否为0即可判断机器的大小端问题。
int a = 1;
int *ptr = &a;
if(*(char*)ptr == 0) // 将整型指针强转为字符型指针后再解引用
{
printf("大端存储\n");
}
else
{
printf("小端存储\n");
}
上述问题也可以通过联合的方法来判断,这里就不再说明。
3、指针的类型
指针的类型包括:内置数据类型指针,数组指针,函数指针,自定义数据类型指针和void*(空指针)。
(1)内置数据类型指针:即指向对应内置数据类型的指针,定义形式:内置数据类型名称 *p
#include <stdio.h>
int main()
{
int a = 10;
int *pa = &a; //整型指针, 指向一个整型元素
char c = 'c';
char *pc = &c; // 字符型指针,指向一个字符元素
return 0;
}
内置数据类型指针如:short int *,char *,int *,long *,float *,double * ......等等,只要是内置数据类型的都可以定义对应的指针类型。
小结:内置数据类型指针只能指向某一个具体的值(如int,char等数据类型);只能指向对应的数据类型数据,防止非法访问。
(2)数组指针:即指向数组的指针,定义形式为: 指向数组的类型 (*p)[所指向数组的数组长度];
如:
int arr[10]; // 一维整型数组
int (*pArr)[10] = &arr; // 数组指针,指向长度为10的整型数组的指针
char s[10]; // 一维字符型数组
char (*pS)[10] = &s; // 数组指针,指向长度为10的字符型数组的指针
int nums[3][4] = {0}; // 二维整型数组
int (*pArr)[4] = nums; // nums 等价于 &nums[0] 即首元素地址,指针类型为数组指针,指向长度为4的
// 整型数组
下面来解释一下与数组名相关的指针类型:
数组地址、数组名和首元素地址的区别:
分析:上面三行打印的都是同一个地址,都是首元素地址。下面三行则不同,因为arr与&arr[0]表示同一个意思,它们对应的指针类型是整型指针int *;所以arr + 1与&arr[0] + 1表示的是整型指针偏移一个整型大小,即4个字节,而&arr对应的指针类型为整型数组指针int *[10],表示一个数组的地址,所以&arr + 1表示偏移一个数组的大小,即 10 * sizeof(int) = 10 * 4 = 40,转为16进制为28,所以打印的结果不同。
不同之处:
1、指针类型不同,&arr 表示的是数组的地址,为整型数组指针;而arr 和&arr[0]表示的是首元素的地址,为整型指针。
2、数组的地址 + 1 表示跳过整个数组的大小;而 arr + 1 表示的是一下元素的地址 。如下图所示
小结:1、数组名等价于首元素地址; 2、多维数组数组名也是等价于首元素地址,这里的首元素不是某个具体的值,而是组成多维数组的数组看成多维数组中的元素。如,int nums[3][4] 二维数组中的元素分别是 nums[0],nums[1],nums[2] ,这三个元素又是一维数组。
(3)函数指针:即指向函数的指针,定义形式为:函数返回值类型 (*p)( 函数形参 );
#include <stdio.h>
int func(int a,int b)
{
return a + b;
}
int main()
{
int (*pf)(int,int) = func; // 指向func函数
int (*pf1)(int,int) = &func; // 与上一行一样,没区别
printf("%d\n",(*pf)(2,3)); // 通过函数指针调用func函数
printf("%d\n",pf(2,3)); // 与上一行一样,没区别
return 0;
}
小结:函数指针的返回值和函数形参必须与所指向的函数的返回值和函数形参完全一致。
(4)自定义数据类型指针:即指向自定义数据类型的指针,定义形式:类型名称 *p
#include <stdio.h>
struct Elempty
{
int _a;
double _b;
};
int main()
{
struct Elempty n; // 定义一个自定义数据类型
struct Elempty *p = &n; // 定义一个自定义数据类型指针,并指向对应的数据类型
return 0;
}
(4)空指针:即void * ;由于指针的大小都是固定的,所以都可以任何类型的指针都占用4/8个字节地址,所以C语言就提供了空指针这一种指针类型,可以指向(保存)任何地址。
#include <stdio.h>
int main()
{
int a = 0;
void *pa = &a;
double b = 0;
void *pb = &b;
return 0;
}
注意:1、空指针与其它类型指针不同,它虽然可以保存任何类型的地址(指针),但是它的性质与其它的指针是不一样的,即对它解引用或者+-整数操作都是未定义的,所以对它操作前必须先进行强制类型转换。 2、空指针强制类型转化为其它类型的指针应遵循“存什么就取什么”的原则!如传进去的是整型指针,那么取出来的时最好强转为整型指针,这是很重要的。如:
#include <stdio.h>
void Swap1(void *pa,void *pb)
{
// 错误操作
int t = *pa;
*pa = *pb;
*pb = t;
}
void Swap2(void *pa,void *pb)
{
// 正确操作
int *pa_ = (int *)pa;
int *pb_ = (int *)pb;
int t = *pa_;
*pa_ = *pb_;
*pb_ = t;
}
int main()
{
int a = 10,b = 20;
Swap1(&a,&b);
Swap2(&a,&b);
return 0;
}
总结:
1、注意分辨指针的类型,保证指针的类型与指向类型保持一致,防止非法访问。
2、注意数组名对应的指针类型
3、空指针应遵循“存什么就取什么”的原则,防止非法访问
在了解了指针的类型之后,指针的问题就迎刃而解了,下面来测试几道相关的题:
#include <stdio.h>
int main()
{
int arr[10] = {0};
int nums[10][10] = {0};
return 0;
}
问题1:arr 对应的指针类型是什么类型的指针?它还可以怎么表示?&arr 呢?
回答:arr 即首元素地址,还可以表示为 &arr[0],而arr的首元素类型是int,所以arr对应的指针类型为int *;&arr即取数组地址,所以它对应的指针类型为数组指针,为int (*p)[10]。
问题2:nums 对应的指针类型是什么类型的指针?它还可以怎么表示?&nums呢?
回答:nums即首元素地址,由于nums表示的是二维数组,所以它的每个元素都是一维数组,所以nums还可以表示为nums[0],对应的指针类型数组指针,为int (*p)[10]; &nums 表示取数组地址,所以它对应的指针类型为数组指针,为 int (*p)[10][10]。
4、 指针数组
指针数组即数组的元素的类型是指针类型。定义形式为:数据类型 *p[ 数组长度 ];
#include <stdio.h>
int main()
{
int *pInt[10] = {0}; // 定义一个整型指针数组,数组元素为整型指针
double *pDouble[10] = {0}; // 定义一个双精度浮点型指针数组,数组元素为双精度浮点型指针
return 0;
}
5、数组名与指针的不同之处
5.1、数组名两个特殊点
1、sizeof(数组名) 返回的整个数组所占的字节大小,这时候的数组名不应该被理解为首元素地址,而是整个数组;
2、&数组名 也表示的是整个数组,表示获取整个数组的地址。
5.2、自增,自减操作
由于数组名是一个常量,所以不能进行自增自减操作;而指针变量是变量,可以进行。
6、指针作为函数形参
6.1、指针作为函数形参的本质
指针传递地址的本质是值传递,这里的值是指地址。
6.2、数组传递
C语言里面数组的传递都是通过地址传递的,不存在值传递。
数组传递地址有三种形式:
(1)数组名[ ] // 注意:多维数组稍有不同
(2)数组指针
(3)普通指针
下面分别以传递一维数组和二维数组为例:
#include <stdio.h>
#define N 5
// 一维数组传参
// 数组[]作为函数形参
void output_1(int arr[],int length)
{
for(int i = 0;i < length;++i)
{
printf("%d ",arr[i]);
}
printf("\n");
}
// 指针数组作为函数形参
void output_2(int (*pArr)[N],int length)
{
for(int i = 0;i < length;++i)
{
printf("%d ",*(*pArr + i));
}
printf("\n");
}
void output_3(int *p,int length)
{
for(int i = 0;i < length;++i)
{
printf("%d ",p[i]);
}
printf("\n");
}
int main()
{
int arr[N] = {1,2,3,4,5};
output_1(arr,N);
output_2(&arr,N); // 注意这里要传的是数组的地址
output_3(arr,N);
return 0;
}
#include <stdio.h>
#define M 5
// 二维数组传参
// 数组[]
void visit_1(int arr[][M],int n,int m)
{
for(int i = 0;i < n;++i)
{
for(int j = 0;j < m;++j)
{
printf("%d ",*(*(arr + i) + j));
}
printf("\n");
}
printf("\n");
}
// 数组指针
void visit_2(int (*pNums)[M],int n,int m)
{
for(int i = 0;i < n;++i)
{
for(int j = 0;j < m;++j)
{
printf("%d ",*( *(pNums + i) + j) );
}
printf("\n");
}
printf("\n");
}
// 一级指针
void visit_3(int *p,int n,int m)
{
for(int i = 0;i < n;++i)
{
for(int j = 0;j < m;++j)
{
printf("%d ",*(p + i * m + j));
}
printf("\n");
}
printf("\n");
}
int main()
{
int nums[M][M] = {1,2,3,4,5,
6,7,8,9,0,
0,9,8,7,6,
5,4,3,2,1,
0,0,0,0,0};
visit_1(nums,M,M);
visit_2(nums,M,M);
visit_3(&nums[0][0],M,M);
return 0;
}
6.3、const 修饰指针
(1)const 类型 *p 型或 类型 const * p 型,称为常量指针
const修饰的是类型,特点: 不可以改指针所指向的内容,可以改指针的指向 。
(2)类型 * const p 型,称为指针常量
const修饰的是指针,特点:可以改指针所指向的内容,不可以改变指针的指向。
(3)const 类型 * const p 型
const既修饰类型,也修饰指针,特点:不可以改变指针所指向的内容,也不可以改变指针的指向。
6.4、void* 指针及其注意事项
1、空指针与其它类型指针不同,它虽然可以保存任何类型的地址(指针),但是它的性质与其它的指针是不一样的,即对它解引用或者+-整数操作都是未定义的,所以对它操作前必须先进行强制类型转换。 2、空指针强制类型转化为其它类型的指针应遵循“存什么就取什么”的原则!如传进去的是整型指针,那么取出来的时最好强转为整型指针,这是很重要的。
7、总结
1、要学会判断指针的类型
2、遇到复杂的指针时,可以逐一分解
3、画图解决问题
4、指针的传递一定要满足同级指针传递,即传什么类型的指针就用什么类型的指针接收