1 内存和地址
1.1 内存
首先为了理解指针的概念,我将以公寓楼举例。

我们可以把内存比作一个公寓楼,把内存中的数据比作其中的住户,而地址就是用来寻找住户的门牌号。但是,内存并不是所有数据都有它的地址,内存是被划分为一个个的空间的,而内存中的地址是一个最小单位的空间即一个字节的地址,但1个字节等于8个比特位(1 byte = 8 bit)。
而在C语言中,地址还有一个名字就是指针。
1.2 究竟如何理解编址
为了访问内存中的数据,我们就需要每个内存块的地址,也就需要给内存进行编址。而计算机中的编址不是把地址给记录下来,而是在设计时就已经规定好了的。这正如乐器的设计一样,设计者并没有把音调的位置标出来,但每个学习乐器的人都会知道音调的位置。
由于计算机中的各个硬件之间需要数据交流,但各个硬件之间又是独立的,所以我们就需要“线”来把它们连接起来。而今天我们只关注一根线,即地址总线。可以简单理解为,32位机器有32根地址总线,一根线有0和1两种状态,所以一共就有2^32种状态,而一种状态就是一个地址。而当地址信息下达给内存时,便能找到对应的信息。
2 指针变量和地址
2.1 取地址操作符(&)
当我们理解了地址与内存的关系后,就继续来研究C语言。
int main()
{
int a = 10;
return 0;
}
在这个代码中,我们创建一个变量a实际上就是向内存申请了一块空间来存放a的值。

因为一个整型是4个字节,所以我们为其开辟了4个字节的空间。即:

而当我们想要取出一个变量的地址时,我们就可以使用 & 这个符号。
#include<stdio.h>
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}

需注意:使用&所输出的地址是较低的地址,但因为整型的内存空间是在一起的,我们就可以顺藤摸瓜地找到其他地址。
2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
在C语言中,指针作为一种数据也是非常重要的。而为了储存这样的数据,便有了指针变量这种类型,所有存放到指针变量的值都会被理解为地址。
2.2.2 如何拆解指针类型
int a = 10;
int * pa = &a;
在上述代码中,我们把int类型的a的地址存到了类型为 int* 的指针变量中。
那么我们该如何去理解 int* 这个类型呢?
*说明了pa是一个指针变量,而int就代表pa中的地址所指向的数据类型是整型类型的。同理:对于char类型的变量,它的指针就是char *类型的。
2.2.3 解引用操作符
我们把地址储存了之后,肯定会去使用它,那么我们就需要通过解引用操作符来根据地址得到数据。
int a = 10;
int * pa = &a;
*pa = 0;
在上述代码中,我们所使用的解引用操作符就是通过pa找到了a并进行了修改。即:
*pa = 0 与 a = 0 等效。
2.3 指针变量的大小
上面我们说过,32位机器有32根地址总线,所以就有2^32个地址,也就需要32个比特位,即4个字节,所以地址大小就是4个字节。同理,64位机器的地址就是8个字节。因此,地址的大小只与环境有关,与类型无关。
3 指针变量类型的意义
既然指针变量的大小与类型无关,那么为什么指针要有不同的类型呢?
3.1 指针的解引用
请看下面两端代码:
1

2

我们很明显可以看到,不同类型的指针在使用时所访问的空间是不同的。
所以我们就可以知道,指针的类型决定了其在使用时所访问的空间大小。
3.2 指针±整数
我们先看一下这段代码

可以看出,int * 类型的指针加一可以跳过4个字节,而 char * 类型的指针可以跳过1个字节,由此可知,指针的类型可以决定指针向前或向后走一步的大小。
3.3 void*指针
在各种指针类型中有一种类型是void* 的类型,可以被理解为无具体类型的指针(也可以叫做泛型指针)
对于下一段代码
int main()
{
int a = 0;
char* pa = &a;
return 0;
}
在vs2022上运行时,会有以下警告:

这是因为类型不兼容,但如果我们使用void* 就不会有这个问题。
int main()
{
int a = 0;
void* pa = &a;
*pa = 10;
return 0;
}

虽然void *可以接受这些指针,但它却不能进行指针运算。
那么void *到底有什么用呢?
简单来说,就是它可以在函数传参时接受不同类型的指针以达到泛型编程的功能。
4 const修饰指针
4.1 const修饰变量
首先我们知道,一个变量是可以改变的。但如果出于某些原因我们不希望一些变量被改变,那么这时我们就可以使用const来达成我们的目的。
int main()
{
int a = 0;
a = 10;//可以改变
const int b = 0;
b = 10;//不可以改变
return 0;
}
如果我们运行了这段代码,就是这个结果。

这是因为我们使用const对语法进行了限制,如果我们去修改b的值,就会出现报错。
但这不代表我们一定无法对其进行修改。比如下面这段代码:
int main()
{
const int b = 0;
int* p = &b;
*p = 10;
return 0;
}
我们通过绕过b,使用b的地址就可以进行修改,虽然这样是在打破语法规则。
那如果我们想要让地址这条路也行不通该怎么办呢?
4.2 const修饰指针变量
下面有两端代码
1
int main()
{
int a = 0;
const int* pa = &a;
*pa = 10;//报错
}
2
int main()
{
int a = 0;
int b = 0;
int* const pa = &a;
pa = &b;//报错
}
这两段代码很好地反应了const在修饰指针变量的两种情况:const放在int*前和后。
对于第一种情况,const限制了pa所指向的值,使其不能被修改;
对于第二种情况,const限制了pa的值,使pa不能被修改,但pa指向的值仍能被修改。
5 指针运算
指针的基本运算有三种:
指针±整数
指针±指针
指针的关系运算
5.1 指针±整数
因为数组在内存中是连续存放的,所以我们可以通过这个方式来输出数组中的内容。

5.2 指针±指针
指针±指针比较简单,就是得到两个指针之间的差值。但值得注意的是,因为指针之间有大小关系,所以这个值是可正可负的。
5.3 指针的关系运算
同样,指针的大小也是可以直接比较的,这里直接跳过。
6 野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针成因
野指针有多个成因,如:
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
printf("%d", *test());
return 0;
}
比如这段代码就是因为a会被销毁,使得test返回的是一个野指针,导致编译器出现警告

6.2 如何规避野指针
6.2.1 指针初始化
如果我们知道希望指针指向的地址,就可以直接进行赋值;如果不知道,那么我们可以把NULL赋值给该指针,来避免其成为野指针。
6.2.2 避免越界访问
这点很简单,就是在使用指针时避免其超过我们预先开辟的内存。
6.2.3 当变量的地址不在使用时,及时置NULL,并在使用前检查指针的有效性
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int* pa = &arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(pa + i));
}//此时pa已越界
pa = NULL;
//下次使用时,要判断pa是否为NULL
pa = &arr[0];//重新给pa赋值
if (pa != NULL)
{
//...
}
return 0;
}
6.2.4 避免返回局部变量的地址
7 assert断言
头文件assert.h定义了宏assert(),这个宏也常常被称为断言。
assert断言是一个非常实用的工具,他可以确保程序在运行时满足指定条件,并在不满足条件时自动报错。也就是说,我们可以通过assert来避免使用野指针。
assert(pa != NULL)
assert()宏所接受的参数是一个判断表达式,如果表达式为真(非零),程序就可以正常运行,不受任何阻碍;如果表达式为假(零),程序就会停止运行并且报错。而且assert()宏的优势在于它可以把不满足的表达式的文件名和行号都表达出来。虽然assert()宏会增加程序的运行时间(因为需要判断表达式),但我们可以在debug版本下使用,在release版本下禁用就可以了(在vs中,release版本下assert()会被直接优化掉)。而如果程序已经没有了问题,无需使用assert()时,我们可以在头文件前面定义一个NDBUG
#define NDBUG
#include <assert.h>
这样就可以编译器就会禁用掉程序中所有的assert。
8 指针的使用和传址调用
我们先看这样一段代码:
#include<stdio.h>
void change(int a,int b)
{
int c = a;
a = b;
b = c;
}
int main()
{
int a = 1;
int b = 2;
printf("a = %d ,b = %d\n", a, b);
change(a,b);
printf("a = %d ,b = %d\n", a, b);
return 0;
}
结果如下:

我们可以看到,当我把a和b传给函数change()时,a和b并没有真的交换数值。这是因为当我们把a和b传给change()后,系统会创建两个临时变量,所以真正交换的是新创建的临时变量。这种方式也叫做:传值调用。
那么我们怎样才能通过函数交换两个变量的值呢?就是使用我们新学的指针。
#include<stdio.h>
void change(int* pa,int* pb)
{
int c = *pa;
*pa = *pb;
*pb = c;
}
int main()
{
int a = 1;
int b = 2;
printf("a = %d ,b = %d\n", a, b);
change(&a,&b);
printf("a = %d ,b = %d\n", a, b);
return 0;
}

在上述代码中,我们直接把a和b的地址传给了函数change(),因此当我们对pa和pb进行解引用时所得到的就是变量a和b,而不是一个临时变量。通过这种方法,我们便可以交换a和b的值,而这种方法叫做:传址调用。
通过传址调用,我们就可以在函数和主函数之间建立真正的联系,在函数中对主函数进行操作。
2157

被折叠的 条评论
为什么被折叠?



