C语言 | 指针详解

        在学习C语言的时候,很多人都因为指针而被劝退,其实,当我们仔细一点一点的啃下这块硬骨头时,回过头来看,其实指针也并没有我们想象的那样难,这篇博客小编就带着大家有由入门到进阶,一起细细体会指针的奥妙之处。

前言

总有人要赢,为什么不能是我?     ----   科比

一.指针的定义 

什么是指针?

1. 指针是内存中一个最小单元的编号,也就是地址

2.平常口语中的指针通常是指针变量

1.那么什么是地址呢?地址又是怎么能来的呢?

前面我们讲过,地址就是一个编号,那么这个编号怎么来的呢?实际上,在32位的机器上,有32根地址线,每根地址线由高低电频表示1和0,而由这32根地址线可以表示2^32个编号,每个编号就是我们所说的地址了,可按照下图理解

         每个地址都能找到对应的一块空间,每块空间大小都是1字节,我们也可以将地址编号理解成为我们生活中酒店的门牌号,而每个门牌号都对应一个房间,每个地址也有对应的一块内存空间。

2.指针变量

所谓指针变量就是储存指针的变量,我们可以通过&来取出变量的内存地址,如下

int a = 5;

int* pa = &a;

此时pa中储存的便是a的地址。我们将pa称作指针变量。

        简而言之,指针是地址,指针变量是储存地址(指针)的变量。我们有时口语称指针变量为指针。 

3.指针变量的大小

        我们了解指针变量的概念以后,我们必须知道指针的大小,前面我们也有说过,在32位机器下,指针是由32根地址线产生的01信号组成的编号,每一个信号都要由一个比特位储存,因此,在32位机器下,指针的大小是4字节(32比特位),在64位机器下,指针的大小是8字节(64比特位)。

小结:指针的大小与指针储存的数据类型无关,只与操作系统的机器数有关。

4.指针和指针的类型 

        我们都知道普通变量都有不同的类型,由整型,字符型,浮点型等等,而指针变量也有不同的类型,即  "type" + *

int a = 5;

假设有以上a变量,那么我们可以将a的地址储存进以下指针变量中

char* pc = &a;

short* ps = &a;

int* pi = &a;

long* pl = &a;

float* pf = &a;

double* pd = &a;

        既然一个整型类型的数据可以被别的不同类型数据的指针储存,那么指针类型的意义又是什么呢?这需要引出指针的另外一个概念了,以下将会讲解指针类型的意义究竟是什么。

5.指针加减整数 

观察以下代码,揣测代码输出结果的原因。

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

 

         我们将n的地址分别储存进pc和pi两种不同类型的指针变量中,我们发现他们会指向该变量的同一起始地址,而分别对他们加一时,字符型指针变量的地址加了1,而整型指针变量的地址加了4,我们发现指针变量的类型决定了指针向前或向后移动的距离。

总结:指针的类型决定了指针向前或向后移动的步长。

 6.指针的解引用

        既然我们可以通过将变量的地址储存进指针中,那我们是否可以通过该指针来引用该变量呢?答案当然时肯定的,这时,我们引入另一个符号,解引用符号(*)。

#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 int *pi = &n;
 *pc = 0;
 *pi = 0;
 return 0;
}

        我们在内存监视窗口中,确实发现了pc和pi指向同一块内存区域

 此时我们的代码还未执行第23行,接下来我们执行第23行后的代码如下图

       我们发现pc指针确实修改了数据,但他仅仅只是将第一个字节修改成了0,再一次验证了指针类型的作用,char类型的指针一次只能访问一个字节。接下来我们执行第24行代码。

        这一次我们发现pi指针修改了四个字节,因为pi指针是整型指针,一次可以访问四个字节 ,指针的类型也会影响一次访问字节的个数。

补充一下:

         很多新手在学习指针的时候可能会有以下疑惑,定义指针和解引用中的星号的差异。

虽然这两个星号是同一符号,但是其意义有本质区别。看以下代码

 二.野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

 1.野指针的成因

(1)指针未初始化

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
 return 0;
}

(2)指针越界访问

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}

三.指针的基本运算

指针的基本运算一般分为以下三种 :

  • 指针加减整数
  • 指针减指针
  • 指针的关系运算

1.指针的加减整数 

 指针加减的步长与指针的类型相关:

#include <stdio.h>
int main()
{
	int a[10];
	int* pi = a;
	for (int i = 0; i < 10; i++)
	{
		*pi = i;
		pi += 1;
	}
	return 0;
}

2.指针-指针

指针减指针得到的是两指针间的元素个数

#include <stdio.h>
int main()
{
	int a[10];
	for (int i = 0; i < 10; i++)
	{
		a[i] = i;
	}
	int* p1 = &a[0];
	int* p2 = &a[5];
	printf("%d\n", p2 - p1);
	return 0;
}

 3.指针的比较运算

 指针与指针之间也可以进行比较,高地址处的指针大于低地址处指针

#include <stdio.h>
#define Len 5
int main()
{
	int values[Len];
	int* vp;
	for (vp = &values[Len - 1]; vp >= &values[0]; vp--)
	{
		*vp = 0;
	}
	return 0;
}

四.二级指针 

        学习了上面的内容,我们知道每个变量都有自己的地址,那么指针变量的地址应该存放在哪里呢?这时候引出一个新概念----二级指针

 1.二级指针的定义

什么叫二级指针呢,简单来说,一级指针变量的地址便是二级指针,看以下代码;

#include <stdio.h>
int main()
{
	int num = 10;
	int* pi = &num;
	//这里定义中有两个颗星,分别有不同的意义
	//第二颗星星先与ppi结合,告诉我们,ppi是一个指针
	//而第一颗星与int结合,告诉我们ppi指针指向数据的类型是一个整型指针的类型
	int** ppi = &pi;
	return 0;
}

        理解定义中星星的作用是理解二级指针的关键,依次类推,依次由三级指针,四级指针等等,这些指针统称为多阶指针,实际应用中,多阶指针的使用并不常见,这里仅仅作为了解即可。

2.二级指针的解引用

观察以下代码,体会二级指针的解引用;

#include <stdio.h>
int main()
{
	int a = 3;
	int* pa = &a;
	int** ppa = &pa;
	//当对二级指针ppi进行一次解引用时,我们得到的是pa变量中储存的值
	//也就是a变量的地址
	printf("%p\n", *ppa);
	//当我们对二级指针两次解引用时,我们得到的是a变量的值
	printf("%d\n", **ppa);
	return 0;

 

 我们也可以通过下图来理解二级指针;

        变量a中储存的是数据3,变量pa中储存的是a的地址,对pa一次解引用,也就是通过a的地址访问到变量a,变量ppi中储存的是pa的地址,我们对ppi一次解引用,也就是通过pa的地址访问到变量pa,对ppi第二次解引用也就是对pa解引用,访问到变量a。

五.字符指针 

 字符指针是用char*来定义,一般使用方式为:

char ch = 'a';

char* pc = &ch;

 但是除了以上的用法,字符指针还有另外一种用法,如下代码;

#include <stdio.h>
int main() 
{
	char* str = "hello world!";
	printf("%s\n", str);
	return 0;
}

与上面代码,还有一种类似的写法,很多萌新都搞不清这两种的区别,如下;

#include <stdio.h>
int main()
{
	char str[] = "hello world!";
	printf("%s\n", str);
	return 0;
}

两种写法都可打印出字符串 "hello world!",但是两者之间却有很大的差距: 

第一种写法中字符指针只是储存了字符串常量首字符 'h' 的地址,对应关系如下图;

 而第二种写法中,数组str将字符串中的每个字符都存进了str数组中,具体关系如下图;

在了解以上知识后,对于下面面试题应该可以很轻松解决了

以下代码的输出是什么?

#include <stdio.h>
int main()
{
    char str1[] = "hello world.";
    char str2[] = "hello world.";
    const char* str3 = "hello world.";
    const char* str4 = "hello world.";
    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是两个字符数组,每个字符数组会在内存中开辟自己的空间,而str3和str4是字符指针,他们储存的是字符串常量中字符h的地址,通过以上分析,str1和str2不相同,因为数组名代表首元素地址,而这两个数组在内存中分别有着自己的空间,故str1和str2不同,而str3和str4是字符指针,他们储存的都是字符串常量中字符h的地址,故str3和str4相同。

六.指针数组和数组指针

1.指针数组

指针数组是指针还是数组呢?

指针数组的本质是数组

以下是指针数组的定义:

int* arr[4]; 

该数组在内存中以如下方式储存:

数组中的每个元素都是一个指针 

2.数组指针 

(1)指针数组的定义

数组指针的本质是指针;

以下为数组指针的定义方式:

int (*p)[5]; 

数组指针的定义仅仅只是在指针数组定义上加上了一个括号;

int *p[5];          -----   指针数组

int (*p)[5];        -----   数组指针

        我们可以这么理解,当没有小括号时,因为方括号[] 优先级高于星号,故p先与方括号结合,形成数组,拿下方括号,剩下的便是数组的类型;而第二组,由于小括号,p先于*组合,形成指针,拿去星号,便是该指针指向的数据类型。

 (2)数组名与&数组名

 我们之前学习过,数组名代表首元素地址。观察下列代码。

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

        输出如上所示,我们发现数组名和&数组名最后输出的地址相同。那么两者又有怎么样的区别呢?看如下代码

#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;
}

        通过计算我们不难看出,数组名加1跳过了4个字节,而&数组名加1跳过了40个字节,虽然数组名和&数组名的值是相同的,而他们加1的值却有很大的差异,原因是&数组名的本质其实是一个数组指针,结合前面的知识,指针加1的步长由指针的类型决定,而上面的代码指针指向的类型是一个十个元素的整型数组,因此加1跳过40字节,而数组名仅仅代表首元素地址,即数组名在上面的代码可以理解为是一个整型指针,加1移动4个字节。

(3)数组指针的使用

 二维数组的数组名可以理解成为数组指针

#include <stdio.h>
//此处的第一个参数还可以写成arr[][5]或arr[3][5]
void print(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
    print(arr, 3, 5);
    return 0;
}

 可根据下图来理解为什么可以用数组指针来接收;

        上图是一个3行5列的二维数组,我们arr数组名代表首元素的地址,而这个二维数组的首元素仍然是一个数组,我们将其数组名看作arr[1],取其地址,接收该地址的类型便是数组指针。

(4)小试牛刀 

分析以下变量

int arr[5];

int *parr1[10];

int (*parr2)[10];

int (*parr3[10])[5];

        arr是一个整型数组,数组中一共有5个元素,每个元素都是整型。

        parr1是一个指针数组,数组一共有10个元素,每个元素都是整型指针类型。

        parr2是一个数组指针,指针指向的数组有十个元素,每个元素都是整型。

        parr3是一个数组指针数组,给数组一共有十个元素,每个元素都是一个指向一个有五个整形的数组的指针。

七.数组参数和指针参数 

1.一维数组传参 

观察以下代码,形参是否正确书写?

#include <stdio.h>
void test1(int arr[])//ok?
{}
void test1(int arr[10])//ok?
{}
void test1(int *arr)//ok?
{}
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int main()
{
 int arr1[10] = {0};
 int *arr2[20] = {0};
 test1(arr1);
 test2(arr2);
}

        在test1中,第一种和第二种形参为一维数组,其中一维数组作为形参时,方括号中的数组大小可省略,形参也为一维数组,故都正确。第三种形参为一级指针,在传实参中,实参数组名代表首元素地址,而首元素地址类型也是int*,故第三种也正确。

        在test2中,第一种形参为数组,且为指针数组,而实参也是指针数组,故第一种正确;第二种形参为二级指针,实参是指针数组,且数组中每个元素为int*类型,而传过去的数组名,数组名单独出现是表示首元素地址,首元素为int*,故int*取地址为int**,故第二种也正确。

2.二维数组传参

 观察以下代码,形参是否正确书写?

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

        通过观察,我们发现以上有7种传参方式,而123是形参为数组的形式,4567为形参为指针的形式 ;

        当实参为二维数组名,形参也为二维数组时,形参中数组行下标可以省略,列下标不能省略,故1 3正确,2错误。

        当实参为二维数组名,形参为指针时,实参传过去的是二维数组的首元素地址,而二维数组首元素是一个元素为5的整型数组,也就是实际传的是这个数组的地址,而第6种用的正是一个数组指针,且该指针指向一个5个整型的数组,符合实参,而第4种形参用的是一个一级指针,明显不行,第5种用的是一个指针数组,也不符合,第7种用的是一个二级指针,二级指针是用来接收一级指针的地址,而实参传过来的是一个数组的地址,也不符合,所以4567中,只有6符合。

3.一级指针传参 

以下为一级指针传参的实例;

#include <stdio.h>
void print(int *p, int sz)
{
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0;
}

那么假设给个有一个函数的形参为一级指针,那么它的实参实参传参方式有哪些呢?


void test1(int *p)
{

}

分别为以下三种:

1. &a

2.传一级指针

3.传一维数组名

4.二级指针传参

以下为二级指针传参实例;

#include <stdio.h>
void test(int** ptr)
{
 printf("num = %d\n", **ptr); 
}
int main()
{
 int n = 10;
 int*p = &n;
 int **pp = &p;
 test(pp);
 test(&p);
 return 0;
}

 反过来思考,那么一个二级指针的形参,我们可以传入哪些实参呢?

void test(char **p)
{
 
}

int main()
{
    
    char ch = 'c';
    char* pc = &ch;
    char** ppc = &ch;
    char* arr[5];
    //传一级指针取地址
    test(&pc);
    //传二级指针
    test(ppc);
    //传指针数组的数组名
    test(arr);
    return 0;
}

以上三种都可。

八.函数指针 

1.函数指针的概念与定义 

所谓函数指针,便是指向函数的指针。 观察以下代码

#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, int) = Add;
	int (*pf)(int x, int y) = Add;
	return 0;
}

       在定义函数指针时,我们可以回忆数组指针的定义,函数指针也是类似,首先我们需要将星号和变量名用小括号圈起来,这使变量名首先与星号结合,形成指针,随后我们在后面写上函数的参数,前面写上函数的返回值类型,其中函数参数的形参名可以省略。

2.函数指针的调用 

观察以下代码,揣摩函数指针的调用

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = Add;
	//以下两种写法均可
	//int sum = (*pf)(2, 5);
	int sum = pf(2, 5);
	printf("%d\n", sum);
	return 0;
}

 在利用函数指针调用函数时,可以不需要解引用;

 在学习了以上的代码后来阅读下面两段代码;(来自《C陷阱与缺陷》)

//代码1
(*(void (*)())0)();

         以上代码是将整型0看作一个地址,并强制类型转换为void (*) ()类型,并对其解引用,调用该函数。

//代码2
void (*signal(int , void(*)(int)))(int);

提示:突破点signal;

该段代码是一个函数定义;

函数名是signal;

signal函数的第一个参数是int;

signal函数的第二个参数是一个函数指针 ---  void(*)(int),该函数指针的参数是int,返回值是void 

signal函数的返回值是一个函数指针  ---  void(*)(int),该函数指针的参数是int,返回值是void。

 代码2还可以简化为以下代码

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

 九.函数指针数组

 函数指针数组可以理解为将函数指针储存进数组中,这个数组便是函数指针数组。

1.函数指针数组的定义

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}

int main()
{
	//函数指针数组的定义
	int (*Farr[2])(int x, int y) = { Add, Sub };
	//调用
	int ret = Farr[1](2, 5);
	printf("%d\n", ret);
	return 0;
}

        函数指针数组的定义与数组指针数组的定义类似,我们定义的Farr先与方括号结合,形成数组,拿去数组后,剩下的便是该数组的类型,也就是函数指针。

2.函数指针数组的应用

我们可以利用函数指针数组完成计算器功能(转移表)

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int main()
{
	int input = 0;
    //我们将函数添加进函数指针数组中
	int (*Farr[])(int, int) = {0, Add, Sub };
	do
	{
		printf("请选择算法(1.Add 2.Sub 0.exit):>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
			
		if (input >= 1 && input <= 2)
		{
			int x = 0;
			int y = 0;
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
            //通过调用函数指针数组使用对应的函数
			int ret = Farr[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("输入有误,请重新输入\n");
		}

	} while (input);
	return 0;
}

         使用函数指针数组后,我们可以避免switch语句的使用,是代码看起来更整洁。

十.回调函数 

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

 观察以下代码,学习如何使用回调函数;

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}

void calc(int (*p)(int, int))
{
	int x = 0, y = 0;
	printf("请输入两个操作数:>");
	scanf("%d%d", &x, &y);
	int ret = p(x, y);
	printf("%d\n", ret);
}
int main()
{
	int input = 0;
	do
	{
		printf("请选择算法(1.Add 2.Sub 0.exit):>");
		scanf("%d", &input);
		switch (input)
		{
		
			case 1:
				calc(Add);
				break;
			case 2:
				calc(Sub);
				break;
			case 0:
				printf("退出计算器\n");
				break;
			default:
				printf("输入有误,请重新输入!\n");
				break;
		}
	} while (input);
	return 0;
}

        在每个switch语句中,我们都使用了calc函数,并传递我们需要进行的运算函数的地址,我们在calc函数中又调用了传递过来的参数,这个被调用的函数便被称为回调函数。

        在库函数中,有一个排序函数,我们需要自己写排序的方式的函数,然后将排序方法给这个给排序函数作为参数,本质上也是一种回调函数,本文就不做详细介绍,感兴趣可以翻阅官网,查阅函数使用方法(cplusplus.com - The C++ Resources Network).

        关于指针本文就介绍到这里,后期会更新相关配套练习,大家可以通过练习提升对指针的了解,最后感谢大家的支持,看到这给个免费的关注呗,十分感谢。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值