一.什么是指针

我么知道数据是在内存中存放的,当我们想要访问内存中的某一特定区域时就需要用到了指针,例如我创建了一个局部变量int a=4而我如果想直接操作这个数字所在的内存空间将其更改。而指针又是如何实现的呢?

我们知道内存中是存在地址这个概念的,什么是地址呢,地址呢就是地址。 image.png 当然这里并不是说一些废话,而是地址我们可以简单的理解为字面意思。假设内存是一个正在建设的小镇,这个小镇里面按一个字节的大小整齐的划分成了一个一个的区域,我们给这些区域按顺序编上了号,那么这些编号就是对应区域的地址。而地址就是指针 image.png

当我们需要一片空间来盖房子或者干其他事就需要向内存申请一片空间。

image.png

既然有了指针我们又该怎么去用呢,这个时候我们便需要一个指针变量去存储我们的指针。

二.什么是指针变量

int main()
{
 int a=8;
 int* p=&a;
 return 0;
}

这个p就是我们用来存放指针a的指针变量。 当我们想要修改a的时候只要使用我们的解引用操作符*对p进行解引用就可以直接操作其中的内容了

int main()
{
 int a=8;
 int* p=&a;
 *p=2;
 return 0;
}

此时我们的a中存放的数值就改成了2。

简单的来说就是我们假设a的地址是0x175864BF,我们在内存中开辟的这片内存空间就是一个宝箱

而地址就是这个宝箱的编号 image.png (宝箱是我自己随手画的,很丑,还请见谅)

其中指针p里边就是存放的就是这个编号,通过这个编号我们就找到了这个宝箱。

当我们在p前面加上写成p就可以找到这个宝箱,拿走或者放入东西了。

三.二级指针

我们既然提到了指针变量本身也是存放的一组数据(地址),那么指针变量它本身也是有地址的。

那么这个地址又能不能存放到一个指针变量之中呢,当然是可以的。

那么这个存放指针变量的的指针也就是二级指针。

以上面那个宝箱为例,既然宝箱有编号,那么我们就可以把这张编号写在纸上,将这张纸放入另一个宝箱,那么存放这张纸的宝箱肯定也有自己的编号(地址),这个存放宝箱编号的宝箱的编号(地址)就是二级指针。

当然这里只是为了更形象的说明才举了这样一个例子,实际存储中并没有纸这个介质,数据是直接存在内存空间中的。

int main()
{
 int a=8;
 int* b=&a;
 int* c=&b;
 **c=2;
 printf("%d",a);
 return 0;
}

image.png 当我们解引用一次的时候,找到c宝箱的位置里边放的是一个地址,也就是另一个宝箱的编号。在次解引用找到名为a的宝箱,将里边放的2换成了8。

当然这些描述对于比较熟悉C语言的人来说可能会感到相当糟糕,因为实际上指针也就是地址实际上是一片空间的首位(或者说低地址处)的地址,根据指针的类型来决定它的访问权限的,这时候就要提到我们的指针类型的意义了。

四.指针类型的意义

1.指针的访问权限

我们已经知道了一个int开辟的是一个4个字节的空间,而我们每一个字节都是有自己的地址的,那么当我们使用一个int*的指针变量时它其中又是怎么存放的地址呢

image.png

假设我们如图中所示开辟了一片空间用来存放数据a,如果此时我们给了一个p的指针变量去存放a的地址,那么p中存放的就是4个字节中3这个位置的地址。

而类型就决定了我们会从3开始往后访问几个字节,如int我们会从3的最左端开始往后访问4个字节,而使用char的话只会从左端开始访问一个字节也就是3这个字节。

2.指针的运算

1.指针类型也决定了我们指针的运算 以下面这段代码为例

int main()
{
	char ch = 5;
	int a = 5;
	int* pa = &a;
	char* pc = &ch;
	printf("%p\n", pa);
	printf("%p\n", pa+1);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
}

image.png 我们可以看到pa+1跳过了一个int的大小4个字节,而pa+1跳过了一个char 类型的大小1个字节。

3.如果指针减指针会发生什么

首先指针减指针的话,两个指针必须指向的是一片连续的空间例如arr[6],里面arr[0]到arr[6]就是一片连续的空间

关于指针减指针我们可以通过下面一段代码进行分析

#include<stdio.h> int main() { int arr[10] = { 0,1,2,3,4,5,6,7,8,9 }; printf("%d\n", &arr[0] - &arr[9]); printf("%d\n", &arr[9] - &arr[0]); }

image.png

五.指针变量的大小

既然p是存放我们地址的,那么p本身也就是一组数据,那么自然也就会在内存中开辟一片空间。

而这片空间的大小在32位平台和64位平台环境下是有所差别的

这是因为32位系统的最大寻址空间是2的32次方=4294967296(bit)= 4(GB)左右,64位系统的最大寻址空间为2的64次方方。所以对指针的大小也就有所改变,在32位x86环境下指针变量的大小是4个字节(32个比特位),而在x64环境下就是8个字节。

六.野指针

1.什么是野指针

野指针就是在内存中没有明确指向的指针。 它出现的原因可能是指针未初始化

int main()
{
 int *p;
 return 0;
}

数据存放的空间已销毁,比如函数返回了局部变量的地址

int* test()
{
int a=0;
return &a;
}
int main()
{
int * ptr=test();
}

又或者使用动态内存分配时指针指向的空间已经释放等各种原因。 在实际编写代码中如果意外使用了野指针可能会导致难以发现的bug,比如我们读了野指针指向的区域又刚好没有报错同时又恰好意外得到了一个符合我们预期的数,恐怕要迷惑很久一阵子。

2.如何规避野指针

1.首先注意指针的初始化

避免出现未完全初始化的指针,以及在使用时对已经释放过的空间的指针置为空指针NULL

2.避免指针越界

int main() { int arr[5]={0,1,2,3,4}; int* p=arr; *(arr+5)=1; return 0; } 注意此时就出现了指针越界。

3.指针指向的空间释放时及时的将其置为NULL空指针

#include<stdlib.h>
int main()
{
 int* p=malloc(40);
 free(p);
 p=NULL;
}

malloc是动态内存分配,当我们给他一个40时他就会开辟一块40字节的空间给我们,返回值是指向这片空间开头的指针,当然我们在这里就不详细讲解malloc了;

free的作用释放内存,当我们free(p)时这个块的内存空间就释放了,此时指针就没有了明确指向,所以我们使用p=NULL;将其置为一个空指针避免其成为野指针。

4.避免返回局部变量的地址

int* fun()
{
 int a=8;
 return &a;
}
int main()
{
 int* p=fun();
}

像这种里面的指针p也是一个野指针, 我们知道a是一个局部变量,当离开它的作用域之后,它的生命周期也就结束了,此时指向a的空间已经释放。

七.字符指针

在指针类型中我们知道有一种指针类型char*

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

还有一种写法我们可以将一个字符串放入其中

#include<stdio.h>
int main()
{
const char* arr="hello word";
printf("%s",arr);
return 0;
}

这种const修饰的字符串我们称其为常量字符串,不能修改.

当然不能修改并不意味着不能观察,就像银行柜台的钱只能看不能动,你甚至可以数一下柜台里有多少人,几台验钞机但是你不能去拿它。像下面这样一段代码就没有问题。不过要注意你可以把arr的内容放到arr2中,但不能将arr2放到arr中。更改常量字符串的内容是初学者很容易出现的错误

#include<stdio.h>
#include<string.h>
int main()
{
const char* arr="hello word";
char arr2[]="################";
strcpy(arr2,arr);
printf("%s",arr2);
return 0;
}

image.png 而且注意arr并没有将他们全部存入进去,只是将字符串的首地址存放在了其中 image.png 下面我们可以看一道曾经出现过的面试题来排除一下认知上可能出现的错误

#include <stdio.h>
int main()
{
    char str1[] = "hello word.";
    char str2[] = "hello word.";
    const char *str3 = "hello word.";
    const char *str4 = "hello word.";
    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;
}

image.png 这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当 几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化 不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。实际上对比的是存储他们的内存空间是不是同一个地方。

八.注意区分指针数组和数组指针

1.指针数组

指针数组就是用来存放指针的数组,它本质上是一个数组,只不过里面存放的内容是相应类型的指针。

int* arr1[6]; //整形指针的数组
char *arr2[6]; //一级字符指针的数组
char **arr3[6];//二级字符指针的数组

2.数组指针

1.什么是数组指针

数组指针就是是指向数组的指针,它本质上是一个指针。

不妨我们来判断一下下面这两个谁是数组指针把

1.int *p1[6];
2.int (*p2)[6];

这就牵涉到int和p谁先和结合了,像1之中的就先和int结合在了一起所以他是一个存放int*类型(整型指针)的数组。

而2里面我们加上括号让它先跟p2结合在了一起,说明p2是一个指针类型的变量,指向一个大小为6个整型的数组。

因为[ ]的优先级是高于*的所以如果我们不用()去限定它,它会有优先往数组的方向去判定定,而不是指针。

2.arr与&arr

int arr[10]; 我们学习过数组名表示的是首元素地址,在各种使用和传递的过程中传递的就是首元素的地址。

那么数组名arr和&arr有区别吗?

这就牵扯到我们上篇内容所讲的指针类型的问题了

不妨看一下下面这段代码

#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0;
}

image.png 可以看到他们之中存放的都是首元素地址,但是当加1是arr跳过了4个字节也就是1个int的类型的大小,而&arr加上一个1则跳过了整个数组的大小.所以这个取地址arr实际上取出的是整个数组的地址它的实际指针类型是数组指针int(*)[10];

九.指针参数的传递

我们在使用指针的过程中经常会用到指针来传递或者接受参数,那么函数的参数该怎么设计呢

1.一维数组传参

int main()
{
    int arr[10] = {0};
    test(arr);
    return 0;
}

像这样我们创建一个一维数组arr,将它作为参数传递给我们创建的test函数,那么test又该用什么类型来接受它呢。

void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}

作为一个一维数组,我们当然可以用一个一维数组数组类型来接受了。同时我们也知道数组名其实就是首元素的地址,因此用一个指针类型来接受也是可以的。

2.二维数组传参

int main()
{
 int arr[3][5] = {0};
 test(arr);
}

我们已经知道一维数组怎么传参了,那么一个二维数组呢。

void test(int arr[3][5])
{}
void test(int arr[][])//×
{}
void test(int arr[][5])
{}

如果我们已经学过数组了,那么应该也知道当我们在将二维数组传参给函数时接收它的数组行是可以省略的,但列不能省略。如果想用一个指针来接收该怎么实现呢

void test(int *arr)//1×
{}
void test(int* arr[5])//2×
{}
void test(int (*arr)[5])//3✓
{}
void test(int **arr)//4×
{}

如果我们用int *类型去接受它,只能得到第一行的地址,所以1是不对的。

int* arr[5]是一个五个元素大小的指针数组自然也不能用来接受我们的二维数组。

而int(*arr)[5]则是一个指向指向数组的指针,5说明它指向的数组每行有多少个元素,所以正确。

int**arr是一个二级指针,二级指针是用来存放指针变量的自然不能用来接收一个二维数组。

十.函数指针

#include<stdio.h>
void test()
{
	printf("hello");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

image.png test和&test在使用时是没有区别的像下面这段代码,test和&test都是一样的。

#include<stdio.h>
void test(int a)
{
	printf("%d\n",a);
}
int main()
{
	test(5);
	void (*pt)(int) = test;
	pt(6);
	return 0;
}

image.png 可以看到当我们想要创建一个函数指针时,首先要先告诉它函数返回值类型。然后写出指针变量名,再写出函数的接收值的类型。注意(pt)的括号不能去掉如果去掉的话会先跟void的结合表示一个返回值为void的函数。 image.png 同时也要注意我们在使用时直接使用pt跟pt都是一样的都能顺利调用函数

你已经了解了函数指针那么来看一下这段代码解释一下它把。

(*(void (*)())0)();

其实这段代码并没有看起来那么“阴间”,我们一点一点的拆开来看,首先

void(*)( )我们可以很明白的看出它是一个函数指针,而在一个数前面加()的意思是强制类型转换

(void()())0也就是把0强制转换成void()( )类型

( * (void (*)()) 0 ) ()则是说明将0强类型转换之后再去调用0地址处的函数

当然我们在日常中一般是用不到这么“诡异”的代码的,而且在一些平台0的地址处是不能随便访问的。

void (*signal(int , void(*)(int)))(int);

我们在看这些代码时一步一步拆开来看即可首先void(* )(int)这样来看它就是一个普通的函数指针了,返回值类型为void接收一个int类型的形参。它说明了函数signal的返回类型是一个函数指针signal(int , void(*)(int)) 函数名为signal,参数为int类型和函数指针类型的函数。 这一段其实跟int test(int a);这样的代码没有本质上的区别都是一个函数声明,只不过返回类型和参数看起来比较复杂罢了。

我们的int test(int a,char ch);声明了一个返回类型为int 参数为int和char类型的函数。

而上面这段代码则是声明一个返回类型为函数指针,参数为int和函数指针类型的函数。

如果你暂时理解不了也没有关系,在后面见到更多特殊的返回类型之后一般会慢慢理解。

十一.函数指针的使用之回调函数*

讲了这么多,函数指针在实际的使用中又有什么作用呢。这里就牵扯到一个重要的内容回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的

假设我们需要一个能够加减乘除的计算器那么我们可以写下面一段代码

#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    do
    {
        printf("*************************\n");
        printf(" 1:add           2:sub \n");
        printf(" 3:mul           4:div \n");
        printf("*************************\n");
        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;
}

但是这段代码中却存在一些问题,我们在每一段都写有

printf("输入操作数:");

scanf("%d %d", &x, &y);

ret = add(x, y);

这种相似但重复的内容。

并且这些函数的返回值和参数类型都相同,我们可以写下面一段代码

void counter(int(*con)(int , int))
{
    int  x = 0, y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("ret = %d\n", con(x,y));
}

上面add,sub,mul,div这几个函数,它们的返回值类型都是int,参数类型都是(int,int)所以我们可以使用这样一个函数指针去接收他们,然后调用

int(*con)(int , int) 上面的计算器经过改造之后就变成了下边这样

#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
void counter(int(*con)(int , int))
{
    int  x = 0, y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("ret = %d\n", con(x,y));
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    do
    {
        printf("*************************\n");
        printf(" 1:add           2:sub \n");
        printf(" 3:mul           4:div \n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            counter(add);
            break;
        case 2:
            counter(sub);
            break;
        case 3:
            counter(mul);
            break;
        case 4:
            counter(div);
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }
    } while (input);
    return 0;

是不是简洁了许多,当然回掉函数的用处并不仅止于此。

我们有一种类型为void*类型的指针它可以接收任意类型的指针,当然在使用的时候要先转换为其它类型才能使用因为void是无类型的。

#include<stdio.h>
#include<string.h>
int test(void*x, void*y,const char* ch)
{
	return *(int*)x - *(int*)y;
}
int main()
{
	int a = 1, b = 2;
	printf("%d\n", ret);
}

上面这段代码充满了随意感,不过不用在意我只是为了给你演示一下void类型的使用 image.png 这段代码编译器没有报错也没有警告当然写这样一段代码只是为了说明我们可以用void接受任意类型并在使用的时候再强制类型转换成为我们所需要的类型。

而利用这样一个特性我们可以以较少的代码量完成一些功能比如写一个能够排序任意类型的排序函数。

当然这些只是回调函数的一些基本用法。 十二.函数指针数组 函数指针数组,数组是一类相同元素的集合。而函数指针作为一种元素当然也能够放入一个数组之中。 例如上面我们写过的计算器代码add,sub,mul,div他们的返回类型,跟参数都相同也可以视为一类相同元素

int (*arr[5])(int x,int y)={0,add,sub,mul,div};

既然是一个数组自然可以通过下标去调用。

#include <stdio.h>
int add(int a, int b)
{
           return a + b;
}
int sub(int a, int b)
{
           return a - b;
}
int mul(int a, int b)
{
           return a*b;
}
int div(int a, int b)
{
           return a / b;
}
int main()
{ 
     int x, y;
     int input = 1;
     int ret = 0;
     int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
     while (input)
     {
          printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
          scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
         {
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);
         }
          else
               printf( "输入有误\n" );
          printf( "ret = %d\n", ret);
     }
      return 0;
}

小结

指针的基本语法与使用在这里基本已经介绍完毕,当然也只是基本内容,更加深入的内容依赖于后续的使用需求和学习。