C语言指针详解(1)

目录

一.内存和地址

二.指针变量和地址

2.1取地址操作符&

2.2指针变量和解引用操作符(*) 

2.2.1指针变量 

2.2.2指针变量的类型

​2.2.3 解引用操作符 ​​​​​​​

2.3 指针变量的大小

三. 指针变量类型的意义 

3.1 指针的解引⽤

3.2 指针+-整数 

3.3 void* 指针

 四.const修饰指针

4.1 const修饰变量 

 4.2 const修饰指针变量

1. const 在 * 左侧(const *)

2. const 在 * 右侧(* const)

五. 指针运算 

5.1 指针与整数的加减

5.2 指针之间的减法

注意

 六.野指针

6.1 指针未初始化 

6.2 指针越界访问

6.3 释放后的指针未置为NULL

如何避免野指针

七.传值调用和传址调用

7.1 传值调用(Call by Value)

7.2 传址调用(通过指针实现)

后言 


一.内存和地址

在讲内存和地址之前,我们先讲一个生活中的案例: 假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩, 如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。

如果把上⾯的例⼦对照到计算机中,又是怎么样呢? 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的 数据也会放回内存中,那我们买电脑的时候,电脑上内存是 8GB/16GB/32GB 等,那这些内存空间如 何高效的管理呢? 其实也是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节。 计算机中常见的单位(补充): ⼀个比特位可以存储⼀个2进制的位1或者0

其中,每个内存单元,相当于⼀个学生宿舍,一个字节空间里面能放8个比特位,就好比同学们住 的八人间,每个人是⼀个比特位。 每个内存单元也都有⼀个编号(这个编号就相当 于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。 生活中我们把门牌号也叫地址,在计算机中我们 把内存单元的编号也称为地址。C语言中给地址起了新的名字:指针

所以我们可以理解为: 内存单元的编号 == 地址 == 指针

二.指针变量和地址

2.1取地址操作符&

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:

比如,上述的代码就是创建了整型变量a,内存中 申请4个字节,用于存放整数10,其中每个字节都有地址,上图中4个字节的地址分别是: 

那我们如何能得到a的地址呢? 这里就得学习⼀个操作符(&)-取地址操作符 

变量在内存中的存储 

按照我画图的例子,会打印处理:006FFD70 &a取出的是a所占4个字节中地址较小的字节的地 址。 虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。 

2.2指针变量和解引用操作符(*) 

2.2.1指针变量 

那我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x006FFD70,这个数值有时候也是需要 存储起来,方便后期再使用的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中

比如:

指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。 

2.2.2指针变量的类型

整型指针(Integer Pointer)
指向整数的指针。在C语言中,int 类型的指针使用 int* 声明。

浮点型指针(Floating-Point Pointer)
指向浮点数的指针,包括 float 和 double 类型的指针。float 类型的指针使用 float* 声明,double 类型的指针使用 double* 声明。 

字符指针(Character Pointer)
通常用于指向字符串(字符数组的首地址)。在C语言中,字符串是以字符数组的形式存储的,并以空字符 \0 结尾。char 类型的指针使用 char* 声明。 

结构体指针(Structure Pointer)
指向结构体变量的指针。结构体是C语言中用户自定义的数据类型,可以包含多个不同类型的成员。结构体指针的声明方式是在结构体类型名后加上星号(*)。 

指针的指针(Pointer to Pointer)
也被称为二级指针,它是指向指针的指针。这种指针用于处理指针数组或者动态分配指针等场景。 

数组指针(Pointer to Array)
这种指针实际上是指向数组首元素的指针,但是在声明时指定了数组的大小。不过,在大多数情况下,我们讨论的是指向数组首元素的指针,即普通指针。  

函数指针(Function Pointer)
指向函数的指针。函数指针在C语言中非常有用,尤其是在回调函数、中断处理函数等场景中。

2.2.3 解引用操作符 

 我们将地址保存起来,未来是要使用的,那怎么使用呢? 在现实生活中,我们使⽤地址要找到⼀个房间,在房间里可以拿去或者存放物品。 C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这里必须学习⼀个操作符叫解引⽤操作符(*)。

上⾯代码中第7行就使用了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0 

2.3 指针变量的大小

32位平台下地址是32个bit位,指针变量大小是4个字节

64位平台下地址是64个bit位,指针变量大小是8个字节

注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。 

三. 指针变量类型的意义 

3.1 指针的解引⽤

对比,下⾯2段代码,主要在调试时观察内存的变化。 

调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。 结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。 比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。 

3.2 指针+-整数 

先看⼀段代码,调试观察地址的变化

 

我们可以看出,char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。

结论:指针的类型决定了指针向前或者向后走⼀步有多大(距离)。 

3.3 void* 指针

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

举例:

在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警 告(如下图),是因为类型不兼容。而使用void*类型就不会有这样的问题。 

那么void*类型的指针有哪些用处呢?

⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果,使得⼀个函数来处理多种类型的数据。

在C语言中,void* 指针因其能够指向任意类型的数据的特性,常被用于实现泛型编程(或称为泛型编程的原始形式,因为C语言本身并不直接支持现代意义上的泛型编程)。泛型编程允许编写与数据类型无关的代码,增加了代码的复用性和灵活性。下面通过一个简单的例子来说明 void* 指针在泛型编程中的应用。 

在这个例子中,bubble_sort 函数是一个通用的排序函数,它接受一个 void* 类型的数组、数组中的元素数量、每个元素的大小以及一个比较函数作为参数。这个函数通过 char* 指针和偏移量来访问和交换数组中的元素,这是因为它不知道元素的确切类型。compare_ints 函数是一个具体的比较函数,用于比较两个整数。 

 四.const修饰指针

4.1 const修饰变量 

在C语言中,const 关键字用来修饰变量,表示这个变量是一个常量,即它的值在初始化之后就不能被修改了。你可以把 const 看作是一个“保证书”,它告诉编译器和阅读代码的人:“这个变量一旦被赋予了一个值,就不要再改变了。” 

这里,MAX_SIZE 是一个常量,它被初始化为100。在程序的后续部分,你不能修改 MAX_SIZE 的值,比如你不能写 MAX_SIZE = 200;,因为编译器会报错,说你试图修改一个常量。

使用 const 有几个好处:

  1. 提高代码的可读性:读者可以一眼看出哪些变量是不应该被修改的。
  2. 提高代码的安全性:防止意外修改重要数据,减少错误。
  3. 有助于编译器优化:因为编译器知道 const 变量的值不会变,所以它可以更高效地处理这些变量。

需要注意的是,虽然 const 变量不能被修改,但指向 const 变量的指针是可以被修改的(即指针本身可以指向不同的地址),只是不能通过这个指针去修改它所指向的 const 变量的值。另外,还有一种指针类型,即指向 const 的指针(const *),这种指针指向的值不能通过它修改,但指针本身可以指向其他地址(如果它不是指向 const 的指针的指针)。 

 4.2 const修饰指针变量

⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。 

1. const 在 * 左侧(const *

当 const 放在 * 左侧时,它表示指针所指向的内容是常量,即不能通过这个指针来修改它所指向的数据。但是,指针本身是可以修改的,即它可以指向另一个地址。

 在这个例子中,ptr 是一个指向 const int 的指针,意味着你不能通过 ptr 来修改 a 的值,但是 ptr 可以重新指向 b

2. const 在 * 右侧(* const

当 const 放在 * 右侧时(注意这里通常需要先有一个指针类型声明,比如 int *,然后在整个声明后加 const,或者更常见的是通过类型别名来声明),它表示指针本身是常量,即指针的指向不能改变,但是它所指向的内容是可以修改的(当然,如果所指向的内容本身是常量,则不能修改)。

然而,直接写成 int * const ptr; 是最常见的形式,表示 ptr 是一个指向 int 的常量指针。

在这个例子中,ptr 是一个指向 int 的常量指针,意味着你不能改变 ptr 指向的地址(即它必须一直指向 a),但是你可以通过 ptr 来修改 a 的值。 

结论:const修饰指针变量的时候 

• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。

• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。 

五. 指针运算 

5.1 指针与整数的加减

当指针与整数进行加减运算时,实际上是在对指针所指向的内存地址进行偏移。偏移的字节数等于整数乘以指针所指向类型的大小。

在这个例子中,ptr 是一个指向 int 的指针。由于 int 类型的大小通常是 4 个字节(这取决于编译器和平台),所以 ptr++ 会使 ptr 指向下一个 int 类型的内存地址,即地址增加了 4 个字节。类似地,ptr += 2; 会使 ptr 指向 arr 数组中索引为 2 的元素之后的元素,即索引为 3 的元素。 

5.2 指针之间的减法

两个指向同一数组(或同一块连续内存区域)元素的指针可以进行减法运算,结果是一个整数,表示两个指针之间相隔的元素数量(而不是字节数)。

 在这个例子中,ptr2 - ptr1 的结果是 3,表示 ptr2 和 ptr1 之间相隔了 3 个 int 类型的元素。

注意

  • 指针的加减运算只适用于指向同一数组(或同一块连续内存区域)的指针。
  • 指针的加减运算结果不是简单的地址算术运算,而是基于指针所指向类型的大小进行的偏移。
  • 指针之间通常不进行乘法和除法运算,这些运算没有定义的意义。
  • 指针运算时需要注意不要越界访问内存,否则可能导致未定义行为,如程序崩溃。

 六.野指针

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

6.1 指针未初始化 

当指针变量被创建后,如果没有被初始化,它将包含一个随机的内存地址。这个地址很可能是无效的,指向了未知的内存区域。尝试通过这个指针进行读写操作将是不安全的。 

 

在这个例子中,p 是一个未初始化的指针,它可能指向任何随机的内存地址。尝试通过 *p = 10; 写入数据是非常危险的。 

6.2 指针越界访问

当指针指向数组时,如果指针的偏移超出了数组的实际范围,那么指针就变成了野指针。

 

在这个例子中,虽然 p 最初指向 arr 的有效内存,但在循环中,当 i 等于 4 时,p 已经超出了 arr 的范围,成为了野指针。 

6.3 释放后的指针未置为NULL

当使用 free 或 delete(在C++中)释放指针所指向的内存后,指针本身的值并不会自动变为 NULL。如果之后不将指针置为 NULL,而继续使用该指针,它将成为野指针。 

 

在这个例子中,虽然 p 所指向的内存已经被释放,但 p 的值仍然是之前分配的内存地址。如果后续代码不小心再次使用 p,就会尝试访问已经释放的内存,这是不安全的。 

如何避免野指针

  1. 初始化指针:在定义指针的同时初始化它,要么指向有效的内存地址,要么设置为 NULL
  2. 检查指针的有效性:在使用指针之前,检查它是否为 NULL
  3. 避免越界访问:确保指针的偏移不会超出它所指向的内存区域的范围。
  4. 释放内存后置为NULL:释放指针所指向的内存后,立即将指针置为 NULL

七.传值调用和传址调用

7.1 传值调用(Call by Value)

在传值调用中,函数的参数是调用者提供的值的副本。函数内部对参数所做的任何修改都不会影响到函数外部的原始数据。

 

在这个例子中,modifyValue 函数试图修改其参数 x 的值,但这个修改只影响到了 x 这个局部变量的副本,对 main 函数中的 a 没有任何影响。 

7.2 传址调用(通过指针实现)

在C语言中,没有直接的传址调用机制,但我们可以通过传递指针来实现类似的效果。当传递一个指针给函数时,函数接收的是指向原始数据的内存地址的副本。但通过这个地址,函数可以访问并修改原始数据。

 

在这个例子中,modifyAddress 函数接收一个指向整数的指针 p。通过解引用这个指针(*p),函数能够访问并修改指针所指向的原始数据(即 main 函数中的 a)。因此,当 modifyAddress 函数执行完毕后,main 函数中的 a 的值已经被修改为 10。

总结:

传值调用中,函数接收的是参数的副本

传址调用(在C语言中通过传递指针实现)中,函数接收的是指向原始数据的指针,可以通过这个指针访问并修改原始数据。

后言 

 觉得文章有帮助的小伙伴们点点赞,点点关注哦

 共勉!

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值