C语言作为一种底层开发语言,是因为它可以直接访问内存,对内存单元进行操作,指针作为C语言的灵魂,提供了这种机会。C语言的各种语法其实从本质上都可以理解为通过指针对内存的操作,因此学好指针至关重要!本篇博客由浅入深详细的总结指针的所有内容,学完本篇博客,可以达到理解到运用的层次水平!
指针的本质:
指针其实就是指针变量,它用来保存内存单元的编号,也就是地址,那为什么要叫指针呢?是因为一个编号/地址对应一块内存单元,可理解为编号/地址指向一块内存单元,因此形象的把它叫做指针,我们使用指针其实就是使用指针变量存放的地址编号,通过对地址解引用操作,便可以拿到这块内存单元存放的数据,可以对这块内存单元的数据访问或者修改!!!
目录
3.3 const既要限制指针变量又限制指针变量指向的数据(双重限定)
4.2.3 指针指向空间释放,及时置NULL(如动态内存章节)
15.2.4 使用回调函数,模拟实现qsort(采用冒泡的方式)
一、指针是什么
1.1 内存和地址
1.1.1 内存
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。其实内存的使用和现实生活中的空间使用本质上是一样的,设想有个生活中的案例:假设有一栋宿舍楼,把你放在楼里,楼上有100个房间,但是房间没有编号,你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
生活中,每个房间有了房间号,就能提高效率,能快速的找到房间。如果把上面的例子对照到计算机中,又是怎么样呢?
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何高效的管理呢? 那么我们就需要明白两个问题:
1、内存是怎么编号的?
2、一个内存单元是多大的空间?
目前微软的操作系统有32位(x86)和64位的,这里以32位为例,进行说明,32位代表有32根地址总线,本质上是物理线,由通电和断电进行表示(0/1),因此电信号便可以转化成数字信号(信息),即32位0或者1组成的二进制序列(占据4个字节),因此便会有2的32次方个组合,这也代表有2的32次方个内存单元!
总结:
内存划分为一个个的内存单元,每个内存单元的大小取1个字节。其中,每个内存单元,相当于一个学生宿舍,一个人字节空间里面能放8个比特位,就好比学生们住的八人间,每个人是一个比特位。每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。指针是一个变量,它是用来存放的地址的!使用指针,其实是使用指针存放的地址,因此指针与地址等价!另外对于32位操作系统的4G内存单元又划分为不同的区域,分别用来存储不同类型的数据,相关知识在动态内存管理章节已经详细总结过。
⭐所以我们可以理解为: 内存单元的编号==地址==指针
1.1.2 如何理解编址
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)。计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
钢琴、吉他上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识!
硬件编址也是如此首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递。但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。不过,我们今天关心一组线,叫做地址总线。
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
1.2 指针变量和地址
1.2.1 取地址操作符&
#include <stdio.h>
int main()
{
int num = 10; //num占用4个字节
#//取出num的地址
//注:这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)
printf("%p\n", &num);//打印地址,%p是以地址的形式打印
return 0;
}
按照上面的图展示,会打印处理:0012ff44,&num取出的是num所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
注意地址是编号,它是一个数!!而不是变量!!取地址操作符&只能针对的是变量!
1.2.2 指针变量和解引用操作符*
(1)指针变量和解引用操作符*
我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?
答案是:指针变量中
如何使用呢?在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品C语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址 (指针)找到地址()指向的对象,这里必须学习一个操作符叫解引用操作符*
#include <stdio.h>
int main()
{
int num = 10;//在内存中开辟一块空间
int *p = #//这里我们对变量num,取出它的地址,可以使用&操作符。
//num变量占用4个字节的空间,这里是将num的4个字节的第一个字节的地址存放在p变量
中,p就是一个指针变量。
*p=20; //相当于num=20;
//通过解引用操作符,修改num的值
return 0;
}
上面代码中就使用了解引用操作符,*p的意思就是通过p中存放的地址,找到指向的空间,*p其实就是a变量了;所以*p=20,这个操作符是把num改成了20。
有同学肯定在想,这里如果目的就是把a改成0的话,写成a = 0;不就完了,为啥非要使用指针呢?其实这里是把a的修改交给了pa来操作,这样对a的修改,就多了一种的途径,写代码就会更加灵活,后期慢慢就能理解了。
番外总结:
(2)拆解指针类型(认真理解指针的类型!!!非常重要)
指针的类型为该指针变量所指向的数据的类型!!!
指针的定义方式为:指针类型 *指针变量名,因此想要准确的知道和区分指针的不同类型,必须要知道它所指向数据的类型!
比如:整型指针,字符串指针,数组指针、函数指针,结构体指针,他们的侧重点都是后面的指针,即它们都是指针,只不过是指向不同数据类型的数据;
- 整型指针指的是指向整型数据的指针(保存整型变量的地址的指针);
- 字符串指针指的是指向字符串的指针(保存字符串首字符地址的指针);
- 数组指针指的是指向数组的指针(保存的是整个数组的地址);
- 函数指针指的是指向函数占用内存的首地址的指针(保存函数的入口地址)。
- 结构体指针指的是指向结构体的指针(保存的是结构体的地址)
注意区分:
- 数组名是数组首元素的地址,可以当作一个指针使用;
- 字符串名是字符串首字符的地址,可以当作是一个指针使用;
- 结构体名与数组名和字符串名不同,结构体名在任何表达式中他表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加取地址符&;
void*指针:
在指针类型中有一种特殊的类型是void* 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。
void*类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
那么void*类型的指针到底有什么用呢?
一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据。通过指针强转即可使用!
1.2.3 指针变量的大小
前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变的大小就是8个字节。
- 在32位的机器上,地址是32个0或者1组成二进制序列,32个bit位,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
- 在64位机器上,如果有64个地址线,64个bit位,那一个指针变量的大小是8个字节,才能存放一个地址。
- 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
二、指针类型的意义
2.1 对指针解引用的影响
通过调试得到如下结果:
结论:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)!
比如: char* 的指针解引用就只能访问一个字节,而int* 的指针的解引用就能访问四个字节。
2.2 对指针+-整数(指针加1的能力)的影响
总结:
指针的类型决定了指针向前或者向后走一步有多大(距离)!指针加1的能力!
可以看出,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。
三、const与指针的结合
const与指针配合使用的作用:
- 限制指针变量的指向
- 限制指针变量指向的数据
- 既要限制指针变量又限制指针变量指向的数据(双重限定)
- const与函数的形参(形参为指针)结合
3.1 const 限制指针变量的指向
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针解引用来改变,但是指针变量本身的内容可变(指针的指向可以发生变化,可以保存其他变量的地址)。
如:const int *ip; int const *ip;
上面两种写法都可以,一般使用第二种,限制指针变量指向的数据的意思就是指针可
以指向不同的变量(指针本身的值可以修改),但是不能用指针修改指针指向的数据的值,
3.2 const 限制指针变量指向的数据
const如果放在*的右边,修饰的是指针变量本身,指针变量的内容不能修改,指针的指向不可以发生变化),但是指针指向的数据,可以通过指针解引用改变。所以 被 const 修饰的指针变量指针只能在定义时初始化,不能定义之后再赋值
3.3 const既要限制指针变量又限制指针变量指向的数据(双重限定)
const既有放在*左边的也有放在右边的,此时,指针变量和指针变量指向数据的值都不能修改
3.4 const与函数的形参(形参为指针)结合
如果函数的形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用const修饰!
用指针实现mystrlen()
#include<stdio.h>
#include <cassert>
int my_strlen1(const char* str)
{
assert(str != NULL);
int count = 0;
while (*str != '\0') //*str++ != '\0'
{
count++;
str++;
}
return count;
}
指针实现my_strcat()
#include <cassert>
#include <stdio.h>
#include <string.h>
void my_strcat(char* str, const char* src)
{
assert(str != NULL && src != NULL);
while (*str++); //str 指针定位到\0,str走到\0停止
str--;
while (*str++ = *src++); //str 跟 src 指针同时向后遍历
}
指针实现my_strcpy()
#include <cassert>
void mystrcpy(char* str, const char* src)
{
assert(str != NULL && src != NULL);
while (*str)
{
*str++ = *src++;
}
*str = '\0';
}
指针实现my_strcmp()
#include <cassert>
int my_strcmp(const char* str, const char* src)
{
assert(str != NULL && src != NULL);
while (*str++ == *src++)
{
if (*str == '\0')
return 0;
}
return *str > *src ? 1 : -1;
}
四、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
4.1 造成野指针的原因
4.1.1 指针未初始化
#include<stdio.h>
int main()
{
int* p;//指针未初始化
*p = 20;
return 0;
}
上述代码就犯了一个严重的错误,即使用了未初始化的指针,此时指针p指向一片我们为止的空间,我们进行了指针操作使得指针p修改了所指向地址的四个字节的数据,这有可能造成严重的后果,在写代码时我们一定要避免使用未初始化的指针。
4.1.2 指针越界访问
#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;
for(i=0;i<=10;i++)
{
*p = 0;
p++;
//arr数组中只有十个变量,循环有十一次,当我们进行到第十一次时已经越界访问了,此时的指针p为野指针
}
return 0;
}
arr数组中只有十个变量,循环有十一次,当我们进行到第十一次时已经越界访问了,此时的指针p为野指针
4.1.3 指针指向的空间被释放
#include<stdio.h>
int *test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d", *p);
return 0;
}
上述代码看似好像没问题,实际上已经出现了野指针的问题,变量n定义在函数test中,所以它是一个局部变量,我们知道局部变量会在函数调用结束时被销毁即结束生命周期,此时我们想要打印的*p其实已经被销毁了,此时的指针p指向一个被释放的空间所以指针p是野指针。
4.2 如何规避野指针
4.2.1 指针初始化
#include<stdio.h>
int main()
{
int n = 100;
int* p = &n; //1.如果知道指针指向哪里就赋值地址
int* q = NULL; //2.不知道指针指向哪里就让它指向NULL
return 0;
}
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL,NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
4.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。