为了更好地理解本篇文章的知识内容,读者可以将以下文章作为补充知识进行阅读 :
C语言————原码 补码 反码 (超绝详细解释)-优快云博客
Hi!冒险者😎,欢迎闯入 C 语言的奇幻异世界🌌!
我是 Anklelss🧑💻,和你一样的闯荡者~ 这是我的冒险笔记📖,里面有踩过的坑🕳️、攒的技能🌟、遇的惊喜🌈,希望能帮你少走弯路✨。
愿我们在代码山峦⛰️各自攀登,顶峰碰拳👊,共赏风景呀!🥳
目录
1. 内存和指针(地址)
1.1 内存的介绍
在计算机中,有各种各样的数据,他们的存储需要在内存中划分空间,计算机中的内存空间大小是有限的。如果把数据比作水,内存就是用以承载水的容器,而我们知道在生活中容器的大小都是有限的。因此我们可以 更好地理解内存之于数据的意义。
1.1.1内存的划分
一个整型变量a= 10存储在程序中需要占据4个字节的内存空间大小,而数据的单位是多种多样的,那我们在内存中应该按照何种单位进行空间划分呢?
为了内存空间的高效管理,内存被划分为一个个的内存单元,而每个内存单元的大小为1字节。
其中,一个bit位可以存储一个二进制的0或1,一个字节可以存储8个bit位,即一个内存单元能存储8个bit位。
在内存中存储的数据需要通过CPU的处理,那么CPU又是如何读取这些数据的呢?
1.2 指针和地址
我们打个比方,当我们在入住一个酒店时,服务员会给我们对应的 房号和房卡,这样我们就能快速找到对应的房间。CPU和内存中的数据传输也是同样的道理,他们之间通过很多的地址总线进行连接,每根线只有两态,表示0或1(联想到二进制),那么通过地址总线不同的脉冲组合形成的这样一个二进制数字,就是对应数据的地址编码,即地址。

在C语言中,我们将这样的地址起名为指针。
所以我们可以理解为:
内存单元的编号 == 地址 == 指针
2. 指针变量和地址
2.1 取地址操作符&
我们在学习scanf函数时知道,scanf函数除格式化字符串以外的参数代表的都是地址。
当我们在创建变量的时候,他会向内存申请空间,我们想知道他具体的地址编号时就需要用到操作符&,示例如下:

如图创建的整型变量a,通过查看内存,我们知道他的地址即指针为0x00000099588FFB14-0x00000099588FFB17(x64环境下),共四个字节;但如果我们对a的地址进行打印的话(x86环境下,更加便于查看),结果又是怎样的呢?

我们会发现,他只打印了一个地址编号,这是因为一个数据进行存储时,他的内存空间都是连续的,打印的往往是最低的那个地址编号,进而根据数据的内存大小,从低往高访问对应数据。

经过多次尝试我们会发现,每一次变量的地址都是在发生变化的,这是因为在每次运行程序时,操作系统的内存分配情况存在差异,所以分配给变量的具体内存地址是不同的。
2.2 指针变量
2.2.1 指针变量的定义
那么通过取地址操作符&得到的地址我们又该将他存储在哪呢?为了方便提取这些指针的数据,C语言中用指针变量作为他的容器。如:
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
此时的pa就是一个指针变量,而他的类型为int *;
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
在C语言中,地址就是指针,指针就是地址。
2.2.2 指针变量的类型
由上我们知道的一种指针变量类型为int *,我们应该怎么去理解他呢?
我们单独看int * pa = &a这段语句可以知道,a为整型变量,pa存储的是a的地址。由此知道:
int 代表pa存储的指针所指向的数据a的类型(整型),* 表明pa为指针变量。
a和pa分别都在内存中划分了属于他们自己的空间。

那么字符类型的变量a,他的地址又该放在上面类型的指针变量中呢?
我们可以进一步推导如下:
int main()
{
char a = '2';
char* pc = &a;//字符指针pc,类型为char *
return 0;
}
2.2.3 指针变量的大小
在介绍内存中,我们知道地址的编号是由地址总线输出的信号转换得到的,32位机器假设有32根地址总线,他们产生的二进制序列作为一个地址,那么一个地址就是32个bit位,需要4个字节的存储空间,指针变量的大小就是 4个字节。
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要 8个字节的空间,指针变量的大小就是8个字节。
我们可以输出如下的代码进行测试:
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
在x86环境下,即32位操作系统

在x64环境下,即64位操作系统

结论:
1. 32位平台下地址是32个bit位,指针变量大小是4个字节
2. 64位平台下地址是64个bit位,指针变量大小是8个字节
注:指针变量的大小和类型是无关的,同样指针类型的变量,在相同平台下,大小都是相同的
2.3 解引用操作符*
那么对于指针变量,他们应该如何使用呢?这里我们将介绍一个关键的操作符——解引用操作符*
他相当于是一把钥匙,指针变量是对应的地址,指针变量指向的数据相当于被存储在对应的地址,但我们无法直接操作他,因此需要通过钥匙打开这道壁垒,这样我们在不直接使用数据变量时,也能对数据进行相应的操作。示例如下:
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;//找到变量a,并通过*打开操作他的权限
return 0;
}
我们会发现,通过解引用,*pa就相当于变量a,我们能够对他进行重新赋值

2.4 指针变量类型的意义
由2.1中我们知道,指针变量会存储数据空间中最小的地址编号,整型指针变量解引用时,他会向上访问四个字节的内存空间,我们思考一下,如果我们使用字符指针变量对&a进行访问,能得到正确的数据么?

在int *整型指针下,我们打印读取的整型变量数据是正确的(十六进制11223344转为十进制为287454020)

当我们使用char *字符指针对整型变量n进行读取时,我们发现,他仅读取了n内存空间中的一个内存单元,数据为十六进制的44,转为十进制为68。
在这里我们可以发现,不同的指针变量所访问的内存空间大小也是不一样的,因此学习指针变量的类型也是十分关键的。
注:在进行地址存储时,指针变量的类型应该和地址的类型相对应,在代码中我们可以看到(char*)&n,由于&n的类型为int *,我们用char *的指针变量接收他,两者类型不同,为了使指针变量pc能够顺利存储n的地址,我们需要对&n进行强制转换,如果不进行强制转换,编译器会发出警告。(编译器会进行隐式转换类型)

结论:指针的类型决定了对指针解引⽤的时候有多大的权限(⼀次能操作几个字节)。
如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引用就能访问四个字节。
3. 指针计算
3.1 指针+-整数
我们观察如下代码的运行结果:

我们可以发现,整型指针变量+1,他的地址跳过了4个字节;字符指针变量+1,他的地址跳过了1个字节。
指针+1,就是跳过1个指针指向的元素。指针可以+1,那也可以-1。
跳过一个指针的空间大小就取决于指针的类型
3.2 指针-指针
此运算的前提条件:
- 参与减法运算的两个指针必须指向同一数组中的元素(或数组最后一个元素的下一个位置)
- 两个指针必须指向相同类型的数据(即指针变量类型一致)
运算结果:
结果是一个ptrdiff_t类型(在<stddef.h>中定义的有符号整数类型),表示两个指针所指向元素之间的元素个数差,而不是字节数差。

我们观察如下代码:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int* p1 = &arr[9];
printf("%d\n", (int)(p1 - p));//p1 - p为ptrdiff_t类型,%d无法读取,需要强制转换
return 0;
}
指针求差他们的结果就是两个指针之间内存的元素个数,如下图,两个箭头之间的元素有1,2,3,4,5,6,7,8,9。共9个整型元素,故输出9。

3.3 指针的关系运算
我们知道内存的地址编码是从低到高依次排布的,因为指针是可以用来比较大小的。
常见的关系运算符包括:== 、!= 、< 、> 、<= 、>= 。
这些关系符构建的指针关系运算可以作为语句的判断条件;
指针的关系运算常用于数组遍历或内存区间判断。
int arr[5] = {1,2,3,4,5};
int *p;
// 遍历数组:当p未超过数组最后一个元素时继续循环
for (p = &arr[0]; p < &arr[5]; p++) {
printf("%d ", *p);
}
4.野指针
- 可能立即触发程序崩溃(如段错误)
- 可能暂时正常运行,但在后续操作中引发错误
- 可能修改无关内存区域,导致数据损坏或程序逻辑错误
- 可能触发安全漏洞,被用于缓冲区溢出等攻击
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] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
当指针指向的范围超出数组arr的范围时,p就是野指针 。
4.1.3 指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
当程序运行函数test()结束后,由于n为局部变量,他在内存中所占的空间就会被销毁,导致指针p无法指向具体的变量,成为了野指针。
解决方法:
1. 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性,置NULL的指针不指向任何有效内存;
2. 如造成野指针的第3个例⼦,不要返回局部变量的地址。
———————————————————————————————————————————
持续更新中
5. void指针和assert断言
5.1 void指针
在指针类型中有一种类型为void *空类型指针(void在英文中的意思是空的),也可以理解为无类型的指针或者叫做泛型指针。
void*空类型指针难道是任何类型的数据地址都不能存放的指针么?恰恰相反,他可以接收任意类型地址。
void*空类型指针也有他的局限性:
1. 不能直接进行解引用;
2. 不能直接进行指针运算。
原因:空类型指针无法确定访问的内存字节数目。
void* 空类型指针的核心作用就是作为“通用地址容器”,必须通过类型转换才能进行解引用或者指针运算。
5.2 assert断言
在C语言中,assert()是一个用于调试的宏(不是函数),主要作用是在程序运行时检查某个条件是否为真。
基本用法:
1. 在使用assert()时,需要包含标准库头文件<assert.h>;
2. 他的语法形式为: assert(表达式);
3. 运行时判断“表达式”是否为真(非0),若为真,assert()不做任何操作,程序继续运行;若为假,assert()会打印错误信息(包含文件名、行号、表达式),并调用abort()函数终止程序。
如下所示:
abort()函数是用于异常终止程序的标准库函数,他的作用是在程序发生严重错误时强制结束运行,不执行正常的清理操作:
#include <stdlib.h> //使用前需包含头文件 void abort(void);//此为函数原型,无参数,无返回值 作用:立即终止当前程序,并返回一个非零状态码给操作系统,表示程序异常退出 //可以联想main()函数中的return 0;0 表示程序正常结束
如果我们给上述报错代码加上 #define NDEBUG ,就可以关闭assert()宏对表达式的判断。
#define NDEBUG//解除断言
#include <assert.h>
int main()
{
int a = 10;
int* p = &a;
p = NULL;
assert(p != NULL);
return 0;
}
assert()他的作用和if语句十分相近,他们的对比如下:

assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间。
在Release版本中,程序会将assert()优化掉,从而不影响用户使用程序时的效率。
6. 指针的应用
6.1 strlen的模拟实现
我们可以思考试着解决这样一个题目:
模拟实现库函数strlen
注:strlen是用来测量字符串长度的一个标准库函数,他的头文件是<string.h>。他的工作原理是从字符串的起始地址开始遍历,当遇到 '\0' 时就会停止,他计算的就是第一个字符到 '\0' 之间的字符个数(不包括\0)。
递归实现方法
int mystrlen(char* str)//构建自有函数mystrlen
{
int count;
if (*str == '\0')
{
count = 0;//趋近条件
return count;
}
else
{
count = 1 + mystrlen(str + 1);//递归法
return count;
}
}
int main()
{
char str[] = "sadas";
printf("%d", mystrlen(str));
return 0;
}
迭代法
int mystrlen_2(char* str)
{
int count = 0;
while (*str)//当*str == '\0'时,判断为0,循环结束
{
count++;
str++;
}
return count;
}
int main()
{
char str[] = "sadas";
printf("%d", mystrlen_2(str));
return 0;
}
在学习字符函数后,我们会继续更新第三种方法
6.2 传值调用和传址调用
指针最常见的使用,就是利用地址解引用,对相关数据进行一个修改,但是我们知道更直接的一种方法就是变量赋值,那什么情况下非指针不可呢?
我们观察下面的代码:
#include <stdio.h>
void Swap1(int x, int y)//内部交换x,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;
}
在自建的函数中,我们对x和y的值进行了交换,那么他会影响main函数中变量a和b的值么? 
我们发现,a和b的值并没有发生交换,这是为什么呢?
我们尝试对这段代码进行调试:

实参a和b同形参x和y他们的地址是不一样的,指针指向数据存储的区域,那我们能够知道,x和y的数值交换,并不影响实参a和b的值,因为他们的地址不同。swap1函数实际上,只是把实参a和b的数值传递给了形参x和y,这种就叫做传值调用。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。
那我们要怎么通过函数实现a和b的数值交换呢?我们前面提到了指针,那是不是我们可以把a和b的指针传递过去呢?基于这个想法,我们对代码进行修改:
#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);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

成功实现了a和b的数值交换,我们使用指针作为参数传递的方式,叫做传址调用。
7. 指针和数组
7.1 数组名的理解
给数组分配内存大小的过程为数组的定义
我们知道在使用scanf函数对数组arr[]首元素和其他位元素输入数据时情况如下:
int main()
{
int arr[10] ;//声明数组
scanf("%d", arr);//数组首地址元素
scanf("%d", &arr[2]);//下标为2的数组元素
printf("%d %d", arr[0], arr[2]);
return 0;
}
scanf函数除第一个参数外的参数都需要传入对应的地址信息(即指针),那么我们知道数组名在这种情况下,其实就是数组首元素的地址,而对于其他位置的数组元素则需要使用取地址符&。
我们试看下面的情况:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
这里代码的输出结果为40,即是arr【10】数组占用的空间大小,按照之前的理解arr是数组首元素的地址,那么输出的结果应该是4/8才对(取决于计算机环境),难道数组名并不代表数组首元素地址?
产生这种情况的原因是由于两个例外:
1. sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除了上述特殊情况外,在任何地方使用数组名时,数组名都表示首元素的定制
思考如下代码,是否与所吸收的知识内容一致,(打印的都是元素的地址,地址的跨度与元素类型相关)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);//0
printf("&arr[0]+1 = %p\n", &arr[0] + 1);//4
printf("arr = %p\n", arr);//0
printf("arr+1 = %p\n", arr + 1);//4
printf("&arr = %p\n", &arr);//0
printf("&arr+1 = %p\n", &arr + 1);//40 为0x28
return 0;
}
7.2 指针访问数组
int main()
{
int arr[3] = {0};
scanf("%d", arr + 1);//两种不同的方式表示元素地址
scanf("%d", &arr[1]);
printf("%d", arr[1]);//两种不同的方式表示元素
printf("%d", *(arr + 1));
}
结论 : *(arr + 1) == arr[1] ;arr + 1 == &arr[1]
根据这个特性,在很多情况中,arr[i]都可以转换成*(arr + i),这样的思路可以便捷指针的求值运算。
7.3 数组传参
// 以下三种声明方式等价
void func1(int arr[]) {}
void func2(int arr[10]) {} // 方括号中的数字可省略,仅作提示
void func3(int *arr) {}
7.4 数组指针
持续更新中
打怪升级中.........................................................................................................................................

1613





