在学习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 = #
//这里定义中有两个颗星,分别有不同的意义
//第二颗星星先与ppi结合,告诉我们,ppi是一个指针
//而第一颗星与int结合,告诉我们ppi指针指向数据的类型是一个整型指针的类型
int** ppi = π
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).
关于指针本文就介绍到这里,后期会更新相关配套练习,大家可以通过练习提升对指针的了解,最后感谢大家的支持,看到这给个免费的关注呗,十分感谢。