C语言总结十:指针全网最详细总结-从入门到精通

       C语言作为一种底层开发语言,是因为它可以直接访问内存,对内存单元进行操作,指针作为C语言的灵魂,提供了这种机会。C语言的各种语法其实从本质上都可以理解为通过指针对内存的操作,因此学好指针至关重要!本篇博客由浅入深详细的总结指针的所有内容,学完本篇博客,可以达到理解到运用的层次水平!

指针的本质:

        指针其实就是指针变量,它用来保存内存单元的编号,也就是地址,那为什么要叫指针呢?是因为一个编号/地址对应一块内存单元,可理解为编号/地址指向一块内存单元,因此形象的把它叫做指针,我们使用指针其实就是使用指针变量存放的地址编号,通过对地址解引用操作,便可以拿到这块内存单元存放的数据,可以对这块内存单元的数据访问或者修改!!!

目录

一、指针是什么

1.1 内存和地址

1.1.1 内存

1.1.2 如何理解编址

1.2  指针变量和地址

1.2.1 取地址操作符&

1.2.2 指针变量和解引用操作符*

1.2.3 指针变量的大小

二、指针类型的意义

2.1 对指针解引用的影响

​编辑

2.2 对指针+-整数(指针加1的能力)的影响

三、const与指针的结合

3.1 const 限制指针变量的指向

3.2 const 限制指针变量指向的数据

3.3 const既要限制指针变量又限制指针变量指向的数据(双重限定)

3.4 const与函数的形参(形参为指针)结合

四、野指针

4.1 造成野指针的原因

4.1.1 指针未初始化

4.1.2 指针越界访问

4.1.3 指针指向的空间被释放

4.2 如何规避野指针

4.2.1 指针初始化

4.2.2 小心指针越界

4.2.3 指针指向空间释放,及时置NULL(如动态内存章节)

4.2.4 避免返回局部变量的地址

4.2.5 指针使用之前检查有效性

五、指针运算

5.1 指针+- 整数

5.2 指针-指针

5.3指针的关系运算

六、指针和数组

6.1 数组名的理解

6.2 一维数组传参的本质

6.3 指针方式访问数组

6.4 加深理解 

七、二级指针

7.1 二级指针的运算

八、字符串指针(是指针)

8.1 字符串常量在栈区和常量区创建的区别

九、数组指针(是指针)

9.1 数组指针的概念理解

9.2 利用数组指针访问二维数组

十、指针数组(是数组!)

10.1 指针数组的概念理解

10.2 利用指针数组模拟二维数组

​编辑​编辑 ​​​​​​

番外篇:区别以下语句:

十一、数组传参和指针传参

11.1 一维数组传参

11.2 二维数组传参

11.3 一级指针传参

11.4 二级指针传参

十二、函数指针(是指针)

12.1 函数指针概念理解

12.2 函数指针的使用

12.3 适用场景

12.4 两个有趣的代码

十三、函数指针数组(是数组)

13.1 函数指针数组的概念

13.2 函数指针数组的应用场景

十四、指向函数指针数组的指针(是指针)

十五、回调函数

15.1 回调函数的基本概念

15.2 回调函数qsort库函数

15.2.1函数原型如下:

​编辑

15.2.2 参数说明:

15.2.3 使用方式:

15.2.4 使用回调函数,模拟实现qsort(采用冒泡的方式)

十六、指针和数组面试题的解析

16.1 一维数组

16.2 字符数组或者字符串

16.3 二维数组

十七、指针笔试题

17.1 笔试题1

17.2 笔试题2

17.3 笔试题3

17.4 笔试题4

17.5 笔试题5

17.6 笔试题6

17.7 笔试题7

17.8 笔试题8



一、指针是什么

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的地址
   //注:这里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,取出它的地址,可以使用&操作符。
    //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)拆解指针类型(认真理解指针的类型!!!非常重要

      指针的类型为该指针变量所指向的数据的类型!!!

      指针的定义方式为:指针类型  *指针变量名,因此想要准确的知道和区分指针的不同类型,必须要知道它所指向数据的类型!

     比如:整型指针,字符串指针,数组指针、函数指针,结构体指针,他们的侧重点都是后面的指针,即它们都是指针,只不过是指向不同数据类型的数据;

  1. 整型指针指的是指向整型数据的指针(保存整型变量的地址的指针);
  2. 字符串指针指的是指向字符串的指针(保存字符串首字符地址的指针);
  3. 数组指针指的是指向数组的指针(保存的是整个数组的地址);
  4. 函数指针指的是指向函数占用内存的首地址的指针(保存函数的入口地址)。
  5. 结构体指针指的是指向结构体的指针(保存的是结构体的地址)

注意区分:

  1.   数组名是数组首元素的地址,可以当作一个指针使用;
  2.   字符串名是字符串首字符的地址,可以当作是一个指针使用;
  3.   结构体名与数组名和字符串名不同,结构体名在任何表达式中他表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加取地址符&;

void*指针:

      在指针类型中有一种特殊的类型是void* 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。

      void*类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。

那么void*类型的指针到底有什么用呢?
        一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据。通过指针强转即可使用!

1.2.3 指针变量的大小

       前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变的大小就是8个字节。

  1. 在32位的机器上,地址是32个0或者1组成二进制序列,32个bit位,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
  2. 在64位机器上,如果有64个地址线,64个bit位,那一个指针变量的大小是8个字节,才能存放一个地址。
  3. 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。 

二、指针类型的意义

2.1 对指针解引用的影响

 通过调试得到如下结果:

结论:

      指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)!

比如: char* 的指针解引用就只能访问一个字节,而int* 的指针的解引用就能访问四个字节。

2.2 对指针+-整数(指针加1的能力)的影响

总结:

     指针的类型决定了指针向前或者向后走一步有多大(距离)!指针加1的能力!

可以看出,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。

三、const与指针的结合

const与指针配合使用的作用:

  1. 限制指针变量的指向
  2. 限制指针变量指向的数据
  3. 既要限制指针变量又限制指针变量指向的数据(双重限定)
  4. 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 小心指针越界

一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

4.2.3 指针指向空间释放,及时置NULL(如动态内存章节)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未来可期,静待花开~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值