目录
一.指针定义
1.概念
如果把计算机内存看作一座楼房,那么地址就是门牌号,就像根据门牌号找到房间,同样根据地址能找到内存空间,地址便是指针。
2.指针变量及类型
存放地址的变量叫做指针变量,我们平时口中所说的指针通常也是指的指针变量。我们使用取地址操作符(&)拿到地址并存放在指针变量中,指针变量也具有不同的类型。
//例如此处对int类型变量a取地址,存放在int*类型的指针变量中
//可以这样理解指针类型:*表明p是指针变量,存放指针,int表面该指针指向对象为int类型
int a = 1;
int* p = &a;
//以下是常见指针类型,详细在后文介绍
void* p1;//指向对象未知类型,常用于函数
char* p2;//指向对象是char类型
int* p3;//指向对象是int类型
int** p4;//指向对象是int*类型
int(*p5)[5];//指向对象是int [5]类型
int* (*p6)[10];//指向对象是int* [10]类型
3.指针内存大小
指针大小取决于环境,32位环境(X86)下地址是32bit位(4字节),64位环境(X64)下地址是64bit位(8字节),并且值得注意的是指针变量的大小与类型无关,无论指向对象是什么类型,地址的大小在相同环境下都是相同的。
二.指针基础操作
1.解引用
如何使用指针变量?使用解引用操作符*对指针变量进行解引用便能得到指针指向的内容。
int a = 1;
int* p = &a;
*p = 10;//解引用p后便能得到a,可以进行赋值等运算
2.避免野指针
2.1野指针成因
//指针未初始化
int* p;
*p = 10;
//指针越界访问
char arr[5] = {0};
char* p = &arr[0];
for(int i = 0 ; i<=6 ; i++)
{
*(p+i) = i;
}
//指针指向的空间释放
int* test()
{
int a = 1;
int* p = &a;
return p;
}
2.2预防野指针
针对以上三种出现野指针的问题,最好是记住范围不要越界访问,也不要返回函数内局部变量的地址,初始化的问题可以先用NULL对指针进行初始化,要使用时再对指针进行检查,如果为NULL则停止程序进行即可,避免危险使用。
2.3 assert断言
以上要实现对NULL指针的检查,可以使用assert断言进行,使用时需要头文件assert.h,它的好处有很多,书写简便,能准确返回出错行数信息,并且在确保程序完好运行后只需要在 #include<assert.h>前定义一个宏NDEBUG,编译器就能自动禁用程序里所有的assert断言,再次想使用时去掉宏就行了,十分方便调试。
#define NDEBUG
#include<assert.h>
assert(p!=NULL);
3.指针运算
3.1指针 + - 整数
指针加减可以理解为指针向前或向后移动,指针+1可以看作指针向前走一步,而走一步有多大(距离)取决于指针的类型。
int a = 1;
char b = 'a';
int* p1 = &a;
char* p2 = &b;
printf("%p\n", p1);
printf("%p\n", p1+1);//指针是int* 类型,一步就是4个字节,地址+4
printf("%p\n", p2);
printf("%p\n", p2+1);//指针是char* 类型,一步就是1个字节,地址+1
3.2指针 - 指针
为什么没有指针+指针的运算?可以把两个指针看作两个日期,日期-日期得到相差的天数,同理指针-指针可以得到之间的元素个数(前提是两个指针指向同一片空间),那么日期+日期是什么呢?没有意义,故也不存在指针+指针的用法。
//例如自己实现strlen函数
int my_strlen(char* p)
{
char * p = s;
while(*P != '\0')
p++;
return p - s;//两指针相减得到字符串的字符个数
}
4.const修饰指针
4.1 const在*左边
以下两种情况属于同一种类型——const在*左边。可以这样理解:const修饰*p,*p是解引用后的值,意味着*p无法修改。即该情况修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本⾝的内容可变。
//例如:
int a = 1;
int b = 2;
const int *p = &a;//两种写法等效
int cosnt *p = &a;
*p = 3;//错误的
p = &b;//正确的
4.2 const在*右边
与以上情况相反,const在*右边,修饰p本身,p这个指针变量内装的地址无法改变。即该情况修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
//例如:
int a = 1;
int b = 2;
int* const p = &a;
*p = 3;//正确的
p = &b;//错误的
三.指针进阶用法
1.字符指针变量
字符指针有个特殊点,使用字符指针可以储存一个字符串的地址(字符串首元素的地址),这一点与字符数组区别开。若两个字符指针指向同一个常量字符串,那么它们的地址是相同的。
//p1的地址等于p2的地址
const char* p1 = "fresh hacker is so handsome!";
const char* p2 = "fresh hacker is so handsome!";
//p3的地址却不等于p4的地址
char p3[] = {"fresh hacker is so handsome!"};
char p4[] = {"fresh hacker is so handsome!"};
2.数组与指针搭配
2.1数组名的理解
一般来说,数组名都指的是数组首元素的地址。但有两个例外。第一个是sizeof(arr1)的时候,这时得到的是整个数组的大小,这种情况只能是sizeof(数组名),如sizeof(arr1+0)中arr1就代表数组首元素地址,结果就是地址的大小了。第二个就是&arr2的时候,这时候得到的整个数组的地址,需要使用数组指针储存。
2.2指针访问数组
在此需理解一个概念,arr[1]实际本质上就是*(arr+1),数组实际就是指针。因此在函数传参时需要传入数组时,通常使用指针传参。
void test(arr[10],int sz)//arr本质是指针,所以通常写成int *arr
{
for(int i = 0; i < sz; i++)
{
printf("%d ",*(arr+i));//这里换成arr[i]也是一样的
}
}
int arr[10] = {0};
int sz = sizeof(arr)/sizeof(arr[0]);
2.3指针数组
指针数组也是数组,不过数组里的每个元素是指针而已,写法为:int* arr[10];唯一与平常不同的是int换成了int*,也就是数组内每个元素的类型由int 变为了int*(指针)类型。可以使用指针数组来模拟二维数组。
//指针数组模拟二维数组
int arr1[3]={1,2,3};
int arr2[3]={2,3,4};
int arr3[3]={3,4,5};
int* parr[3] = {arr1,arr2,arr3};//指针数组,此处数组名表示数组首元素地址
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 3; j++)
{
printf("%d ",parr[i][j]);//parr[0]得到arr1,再arr1[j]访问元素
}
}
2.4数组指针
数组指针也是指针,不过指向的是数组。写法要与指针数组区分开,写法为:int (*p) [10];由于[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合,p与*结合便能表示这是个指针变量。int(*p)[10]表示该指针指向一个拥有10个元素的整形数组。取出数组的地址使用&数组名。
二维数组可以看作每个元素是一维数组的数组,因此根据数组名是首元素地址,二维数组的数组名可以看作一个一维数组的地址,这就需要使用数组指针存储该地址。
//二维数组传参的本质就是数组指针
int arr[2][3] = {{1,2,3},{4,5,6}};
test(arr,2,3);
void test(int(*p)[3] , int row ,int col)//传int p[2][3]是数组形式,但本质上是指针
{
for(int i = 0; i<row ; i++)
{
for(int j = 0; j<col ; j++)
{
printf("%d ",*(*(p+i)+j));
}
printf("\n");
}
}
3.函数指针
函数指针同样是指针,不够指针指向的对象是函数。没错,函数同样具有地址,函数名就是函数的地址。此处假设定义一个int test(int x,int y)的函数,则函数指针写法为:int (*p)(int , int);同样的*和变量名p要用括号括起来,这才能表达这是一个指针,而(*p)前面是指向函数的返回类型,后面是指向函数的参数类型。
int test(int x,int y)
{
return x + y;
}
int (*p)(int,int) = &test;
int (*p)(int,int) = test;//两种写法皆可
int ret = (*p)(2,3);
int ret = p(2,3);//使用时两种写法皆可
printf("%d\n",ret);
当然函数指针也可以和数组组合,函数指针数组就是应用之一。书写方法为:int (*p[4])(int ,int);根据[]的优先级要⾼于*号,这样的写法自然便是数组形式,这就是函数指针数组。如果要把几个函数的地址存在一个数组,前提是每个函数类型相同,即函数的参数和返回类型相同。
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 (*p[3])(int,int) = {Add,Sub,Mul};
int ret = p[0](1,2);//p[0]得到Add,再加上参数就能使用函数了
printf("%d\n", ret);