目录
一、初阶了解指针
在学习指针前,我们先了解一下计算机中的内存。
1.内存和地址
在计算机中为了方便管理内存,内存会被划分为以字节为单位的内存空间,也就是说一个内存单元的大小是一个字节,为了方便找到这个内存单元,我们会给每个内存单元一个编号,就像生活中每个房间都有门牌号。当我们有了内存单元的编号,就可以快速的找到内存单元。
在计算机中我们把内存单元的编号也称为地址。 C语言中给地址起 了新的名字叫:指针。
大家可以理解内存单元的编号 == 地址 == 指针
![](https://i-blog.csdnimg.cn/blog_migrate/e3489ba1d5dbc8fae1453153f663087d.png)
2. 指针变量和地址
2.1 取地址操作符(&)
我们了解了内存和地址,回到C语言中。当我们创建一个变量时,变量被存放在哪儿呢?
答案是内存单元中。
因此变量创建的本质:在内存上开辟空间。
例如下面的代码
int main()
{
int a = 10;
return 0;
}
我们创建了一个变量a,那么内存中就会开辟一块空间用来存放10。按下F10进入调试模式,看看a被放到了哪里,它究竟有没有编号呢?
如下图,我们看到整型变量a有四个地址,依次存放着0a 00 00 00。第一个字节内容是0a(16进制),表示10。里面确实存放着10,并且前面的一长串16进制表示对应字节的地址。四个字节是连续存放。
![](https://i-blog.csdnimg.cn/blog_migrate/95eec6232931fff9411097e6bd5d4f4f.png)
那我们在代码中怎么拿到a的地址呢?这里就要用到取地址操作符&,&的作用是取地址。但是需要注意的是,我们拿到的是a所占四个字节的第一个字节的地址(地址较小的那个字节的地址)
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
这里打印出来的是四个字节里的低位地址。
注:每次打印结果都不一样,因为每次创建地址的位置都不一样。
2.2 指针变量和解引⽤操作符(*)
那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?
答案是:指针变量中。
int main()
{
int a = 10;
int* p = &a;//取出a的地址并存储到指针变量p中
return 0;
}
这⾥p左边写的是 int*,int代表p指向对象的类型是int,*是在说明p是指针变量。
指针变量就是用来存放地址的。
存放在指针变量中的值,都会被当成地址使用。
我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
这里我们就需要使用解引⽤操作符*,看下面代码。
int main()
{
int a = 10;
int* p = &a;
*p = 0;//*解引用操作符
//*p的意思就是通过p中存放的地址,找到地址指向的空间,*p就是a,我们再给a赋值0,打印看看结果
printf("%d\n", a);
return 0;
}
可以看到打印结果为0,这里我们通过*P其实就得到了a的值,然后我们再给值赋为0,那么a里面的。就变成0了。大家可以仔细理解一下。
2.3 指针变量的大小
这里直接总结,不再代码举例。
•32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3. 指针变量类型的意义
3.1 指针的解引用
上面学会了如何将一个变量的地址进行存放。那当我们拿到地址又该使用地址所指向的内容呢?
这里便需要指针的解引用。
我们看下面代码
int main()
{
int n=0x11223344;
int* p = &n;//取n的地址存放到指针变量p中,p为int类型。
*p =0;//*p表示对p解引用,*p表示存储在p中的地址所指向的内容,等同于0x11223344,
//然后我们将0赋给*p,修改p所指向的内容,因此0x11223344变为0x00000000。
return 0;
}
这里我们可以看到,四个字节的内容全部变成了0。取n的地址存放到指针变量p中,p为int类型。
*p表示对p解引用,*p表示存储在p中的地址所指向的内容,等同于0x11223344,
然后我们将0赋给*p,修改p所指向的内容,因此0x11223344变为0x00000000。
那如果我们将代码改一改
int main()
{
int n=0x11223344;
char* p = &n;
*p =0;
return 0;
}
我们可以看到只改变了一个字节。这是因为int类型地址存放时是四个字节,char是一个字节,&n地址时只拿到了四个字节里面的低位地址,就是最小的地址。所以对*p修改只修改一个字节所指向的内容,其余三个字节的地址没有收到影响。
因此我们可以得到结论, 指针类型决定了指针在解引用操作时的权限,也就是一次解引用访问几个字节,char*类型的指针解引用访问一个字节,int*类型的指针解引用访问四个字节。
3.2 指针+-整数
直接看代码
int main()
{
int n=0x11223344;
char* pc = &n;
int * p = &n;
printf("p =%p\n", p);//打印p所存放的地址
printf("p+1 =%p\n", p+1);//打印地址p+1后的地址
printf("pc =%p\n", pc);
printf("pc+1 =%p\n", pc+1);
return 0;
}
我们可以看到p+1的地址跳过了四个字节,pc+1的地址跳过了一个字节。
这是为什么呢?
不妨可以大胆猜一下,int类型占用内存四个字节,因此地址+1跳过一个整形,所以地址向后加上4个字节,cha类型占用内存一个字节,地址+1时向后跳过一个char,地址加一个字节。
这里我们可以得出结论,指针类型决定了,指针进行+1/-1操作的时候,一次跳过几个字节。
下面我们可以用上面所学的知识,用指针代码打印数组。大家可以自己去尝试。不懂可以注释。
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];//p存放数组首元素的地址
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//因为数组在内存中存放时,
//每一个元素的地址都是连续的,
//因此拿到第一个元素的地址进行+i,便可得到对应i下标的元素的地址
}
return 0;
}
4. const修饰指针
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
int n=100;
n=20;
这里我们有一个变量n,我们可以轻易的修改n的值。
当我们加上const时,此时再进行修改,代码会报错无法修改。
const int n=100;
n=10;//这里会报错,不能修改
我们再看一段代码
const int n=100;
int *p=&n;
*p=20;
这里我们通过指针变量p取地址n,然后解引用修改n的值,发现可以修改成功。
这里const从语法上限制直接更改n无法更改,但是我们将n的地址给p,通过p可以更改。
显然这里有点不合理,既然想要n不能更改,那就应该无论什么方法都不可以更改。
所以这里我们有一种方法,那就是const修饰指针。
int main()
{
int m = 100;
int n = 10;
const int* p = &n;
// *p = 0; 这句报错,const 在左边限制*p,p所指向的内容不可以变,可以用p = &m;
int* const p = &n;
// p = &m; 这句报错 const在*右边,限制的是指针变量本身,不能修改指针变量的指向,可以用*p = 0;
return 0;
}
总结:
• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
5. 指针运算
指针的基本运算可以分为三种
指针+-整数
指针-指针
指针的关系运算
5.1 指针+- 整数
指针加减整数,与指针类型有关系。比如int类型,加1就是跳过四个字节,char类型加1,跳过一个字节。比如,数组在内存中存放的地址是连续的,我们只需要知道数组首元素地址,便可得到每一个元素的地址。这里可以通过指针+-整数实现。
代码举例
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
下面还有一个代码应用,大家可以自行理解。
int main()
{
char arr[] = "abcedef";
printf("%s\n", arr);
char* pc = &arr[0];//取字符串首元素的地址
while (*pc != '\0')//字符串的结尾是\0,如果指针所指向的元素不是\0,那么循环继续。
{
printf("%c", *pc);
pc++;
}
return 0;
}
同样都是打印字符串,用了两种不同的方法,有助于理解指针的+-。
5.2 指针-指针
这里先看代码。
int main()
{
int arr[10] = { 0 };
int ret = &arr[9] - &arr[0];//取数组中下标为9元素的地址减去下标为0元素的地址
printf("%d\n", ret);
return 0;
}
大家想一想,连个指针相减得到的是什么?
结果ret=9. 解释:指针减指针得到的是之前的元素个数。两个指针相减前提的条件是,两个指针指向通一块空间,得到的值得绝对值是指针和指针之间的元素个数。
5.3 指针的关系运算
指针的关系运算就是比较指针大小。
//指针的关系运算
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];//取数组首元素的地址
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);//这里是计算数组有多少个元素
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
我们打印数组这里便可用到指针的关系运算。
创建一个10个元素的地址,将首元素的地址给p,然后只要p的地址小于第十个元素后面的地址就进行打印,然后p+1.使用这种方法也可以完成数组的打印,涉及到了指针的关系运算,需要比较p的地址和第11个元素的地址。
6. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针有以下几个原因。
1. 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
在内存中一块空间,你想要使用,你得申请拿到这块空间才能使用!
2. 指针越界访问
假如一个数组arr,里面有10个元素,1,2,3,4,5,6,7,8,9,10。
那我们for循环打印数组元素,如果我们打印完十个元素后继续循环打印,这里就是越界访问了。程序就会出现问题。第十个后面元素的地址就是野指针。
3. 指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
这里p得到的是n地址,但是n在函数结束时已经不存在了,但是p里面还存在n那块空间的地址,这就属于野指针。
那么如何避免野指针?
1、指针初始化
最有效的就是指针初始化。如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。本质是0。
int *a=NULL;
这里就是空指针,防止出现野指针。空指针不可以直接访问。
2、小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
3、指针变量不再使⽤时,及时置NULL,指针使用之前检查有效性
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
内存是一块区域,里面划分为栈区、堆区、静态区。局部变量一般存放在栈区。
7. assert断言
我们使用指针的过程中,经常能遇到assert。
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);
这样如果p是空指针程序就会报错,直接告诉你哪里出问题了。相比if语句更好。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
8. 指针的使⽤和传址调用
8.1 传址调⽤
写⼀个函数,交换两个整型变量的值。
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们发现其实没产⽣交换的效果,这是为什么呢?a和b传参时调用的是a和b的本身,叫做传值调用,函数实参传递给形参后,形参是实参的一份临时拷贝,所以对形参的修改不影响实参。形参有自己的独立空间。
总结:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。所以Swap是失败的了。
修改代码
#include <stdio.h>
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调⽤。
大家可以用上面的知识写一个函数,模拟实现strlen函数。这里就不在演示了。
二、深⼊理解指针
1. 数组名的理解
直接上结论,数组名就是首元素的地址
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
我们可以看到打印结果一样。
数组名是数组首元素的地址,但是有两个例外:
1、sizeof(数组名),这里的数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节
2、&数组名,这里的数组名表示整个数组,&数组名:取出的是整个数组的地址
除此之外,遇到的所有的数组名都是数组首元素的地址。
2. 一维数组传参的本质
数组名是数组⾸元素的地址,那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。
⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
3.二级指针
int a=10;
int *p=&a;
a是整形变量,占用4个字节的空间,a是有自己的地址,&a拿到的就是a所占4个字节的第一个字节的地址。
p是指针变量,占用4/8个字节的空间,p也是有自己的地址,&p就拿到了p的地址。
那如果我们想存放p的地址呢?
int * * pp=&p;
pp也是指针变量,这里*pp表示是指针,int*表示的是p的类型。因此前面是int**。
一颗*叫做一级指针,**表示二级指针。
int ***ppp=&pp;//ppp是三级指针
4. 指针数组
什么是指针数组呢?我们可以类比一下之前学过的。
整形数组-存放整形的数组 int arr[10];
字符数组-存放字符的数组 char ch[5];
指针数组-存放指针的数组 int * parr[5]; char *pc[10]; ......
用来存放指针的数组叫做指针数组
如图,每一个元素都是int*类型的,因此数组的类型为int*。叫做指针数组。
5. 指针数组模拟二维数组
#include <stdio.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
这里用三个一维数组,用指针数组将三个数组联系起来,模拟实现二维数组。大家可以自行实现加深理解。
这里如果不理解parr [ i ] [ j ],可以这样理解*(parr[i]+j),得到的值是一样的。[ ]就想当于解引用了。
6. 字符指针变量
int main()
{
char* ch = 'a';
char* pc = &ch;//pc就是字符指针变量
//字符指针变量时用来存放地址的
char* p = "abcdefe";
//不是将abcdefe\0 字符串存放到p中,而是将首字符a的地址存储到p中
//"abcdefe"这是一个常量字符串,是不能被修改的
return 0;
}
如何理解字符串存放到p中,是将a的地址存储到p中呢?因为字符串的地址是连续存放的,所以得到首字符地址也就能得到整个字符串元素。注意的是"abcdefe"这是一个常量字符串,是不能被修改的。
7. 数组指针变量
我们之前血虚指针数组,指针数组是数组,是存放指针的数组。那数组指针是什么呢?
我们依然类比来看:
字符指针-指向字符的指针,存放的是字符的地址 char ch='w'; char* pc=&ch;
正定型指针-指向整形的指针,存放的是整形的地址 int n=100; int *p=&n;
数组指针-指向数组的地址,存放的是数组的地址 int arr[10]; int (*p)[10]=&arr;
int main()
{
int arr[6];
int *p=arr;
int (*ptr)[6]=&arr;//数组的地址
// ptr 是数组指针
return 0;
}
8. ⼆维数组传参的本质
如下图,二维数组的每一行是一维数组,这个一维数组可以看做是二维数组的一个元素,所以二维数组也可以认为是一维数组的数组。那么二维数组的数组名表示首元素的地址,就是第一行的地址!也就是一个一维数组的地址。
直接上代码,结合数组指针打印二维数组,理解二维数组传参本质。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
test(int(*p)[5], int x, int y)//这里得到的是一维数组的地址,因此形参定义数组指针接收。
{
int i = 0;
for (i = 0; i < x; i++)
{
int j = 0;
for (j = 0; j < y; j++)
{
printf("%d ", *(*(p + i) + j));//arr[i][j]这样写更好理解
}
//这里p存放的是二维数组第一行元素的地址,理解成一个一维数组,
//我们将二维数组看成是一维数组构成的,每一行相当于一维数组,
// 那么一维数组地址p+i,就得到第i行的一维数组的地址
// *(p+i)得到第i行首元素的地址,然后再+j,整体解引用得到的是第i行的第j个元素。
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);//二维数组,数组名传参,
//表示第一行一维数组的地址,因此我们定义形参时要用数组指针接收。
return 0;
}
这里注释讲解的很详细了,不理解可以看看注释。
9. 函数指针变量
数组指针-是指针-是指向数组的指针,是存放数组地址的指针
函数指针-是指针-是指向函数的指针-是存放函数地址的指针!!
那么函数是否有地址呢? 我们做个测试:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
输出结果如下:
test: 005913CA
&test: 005913CA
可以看到,函数也是有地址的,并且两个打印结果是一样的。因此对于函数,&函数名和函数名都是函数的地址。
函数既然有地址,那么怎么存放呢?
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;// pf3函数指针变量,形参的xy可以不写,只需要交代类型
printf("%d\n", (*pf3)(2, 3)); // 函数指针变量的使用,输出结果5
printf("%d\n", pf3(3, 5)); // 这里两种写法都可以,直接pf3和(*pf3)都可以。输出结果8
return 0;
}
到此,C语言中指针的知识已经讲解完成了。感谢大家的观看,喜欢的可以点赞收藏多多支持一下。