1.内存和地址
首先我们先来了解内存和地址的相关概念:
*内存(Memory)是计算机用于临时存储数据和指令的硬件设备,属于随机存取存储器(RAM)。其特点是读写速度快,但断电后数据会丢失。内存直接与CPU交互,存储当前运行的程序和所需数据,是计算机性能的关键部件之一。
*地址(Address)是内存中每个存储单元的编号,用于唯一标识数据的位置。通常以十六进制表示(如0x0000FFFF)。CPU通过地址访问内存中的数据,类似于通过门牌号找到具体房屋。
对于初学者或许会比较难以理解,我们可以把内存想象成一个个小房间,在管理内存空间时会将内存划分成内存单元,而这一个内存单元就相当于一个学生宿舍,一个字节空间里面能放八个比特位,就好比一个宿舍住八个,每个人都是一个比特位。
每个内存单元都有一个编号(想象成宿舍门牌号),这个编号就是地址。而在C语言中我们给这个地址取了一个新名字叫:指针。
所以我们可以理解为:内存单元的编号=地址=指针
字节及其衍生单位
计算机内存容量通常以字节及其衍生单位表示,常见换算关系如下:
1 Byte = 8 bits
1 Kilobyte (KB) = 1024 Bytes
1 Megabyte (MB) = 1024 KB
1 Gigabyte (GB) = 1024 MB
1 Terabyte (TB) = 1024 GB
2.指针变量和地址
2.1取地址操作符(&)
在C语言中创建变量的过程其实就在在向内存申请空间,比如:
// An highlighted block
int main()
{
int a=10;
return 0;
}
创建整形变量a时,向内存申请了4个字节,用于存放整数10,其中每个字节都有地址。
为了得到a的地址,我们可以使用取地址操作符:&。
// An highlighted block
#include <stdio.h>
int main()
{
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
虽然整形变量占四个字节,但我们只要知道了第一个字节的地址,就可以顺藤摸瓜访问到这四个字节的数据。
2.2指针变量
那我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要
存储起来,方便后期再使用的,我们把这样的地址值存放在指针变量中。
比如
// An highlighted block
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量是一种用于存放地址的辨别,存放在指针变量中的值都会理解为地址。
如何拆解指针类型呢
我们看到pa的类型是int*,*是在说明pa是指针变量,而前面的int是在说明pa指向的是整型(int)类型的对象。
那如果我们有一个char型的数据,需要存放它的地址该怎么做呢?
如下:
// An highlighted block
char c='t';
char *pc=&c;
2.3解引用操作符(*)
我们用指针变量存放了数据的地址,那该如何使用呢?这时候就要用到解引用操作符(*)
// An highlighted block
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
*pa的意思就是通过pa中存放的地址,找到指向的空间,pa其实就是a变量了;所以pa=0,这个操作符是把a改成了0。
有的同学可以会想,要改a的地址完全可以直接写a=0;但并不是所有情况都可以使用a=0,有时候我们会遇到只能用指针帮助的情况,在后面我们会讲到传值调用和传址调用。
2.4指针变量的大小
// An highlighted block
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环境的输出结果是:
4
4
4
4
在X64环境的输出结果是:
8
8
8
8
结论:
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3.指针变量类型的意义
3.1指针的解引用
对比下面俩段代码
//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
// 代码2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。
3.2指针±整数
// An highlighted block
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
运行结果如下:

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
3.3void*指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引用的运算。
举例:
// An highlighted block
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上面的代码中,将⼀个int类型的变量的地址赋值给⼀个char类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。而使用void类型就不会有这样的问题。

⼀般 void* 类型的指针是使⽤在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。
4.const修饰指针
4.1const修饰变量
当我们想要一个变量不能被修改时,就可以使用const来修饰这个变量。
// An highlighted block
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了。
// An highlighted block
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
4.2const修饰指针变量
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
• const如果放在的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
5.指针运算
5.1指针±整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,就能找到后⾯的所有元素。
// An highlighted block
#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;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
5.2指针-指针
// An highlighted block
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
5.3指针的关系运算
// An highlighted block
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
6.野指针
6.1野指针的三种情况
6.1.1指针未初始化
// An highlighted block
int *p;//局部指针未初始化默认为随机值
*p=20;
6.1.2指针越界访问
// An highlighted block
int arr[10]={0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
6.1.3指针指向的空间释放
// An highlighted block
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
当跳出test函数时,系统分配给n的内存已经返回给了系统,而指针p也就变成了野指针。
6.2如何规避野指针
初始化指针
声明指针时立即初始化为NULL或有效的内存地址。未初始化的指针可能指向任意内存地址,使用这样的指针会导致未定义行为
// An highlighted block
int *ptr=NULL;
释放后置空
// An highlighted block
free(p);
p=NULL;
避免指针访问越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
7.assert断言
7.1assert断言的基本概念
assert是C语言标准库中的一个宏,用于在程序运行时检查某个条件是否满足。若条件为真(非零),程序继续执行;若条件为假(零),assert会输出错误信息并终止程序。assert常用于调试阶段,帮助开发者快速定位逻辑错误。
7.2assert的语法
#include <assert.h>
void assert(int expression);
- expression:需要检查的条件表达式。若为假,触发断言失败。
7.3 assert的使用场景
- 检查函数参数合法性:确保传入函数的参数符合预期范围。
- 验证中间结果:在复杂计算中检查中间步骤的正确性。
- 调试辅助:快速捕捉程序运行时的逻辑错误。
7.4assert的示例代码
#include <stdio.h>
#include <assert.h>
int divide(int a, int b) {
// 检查除数是否为0
assert(b != 0);
return a / b;
}
int main() {
int x = 10, y = 2;
printf("10 / 2 = %d\n", divide(x, y));
// 触发断言失败
y = 0;
printf("10 / 0 = %d\n", divide(x, y));
return 0;
}
- divide函数:通过
assert(b != 0)确保除数不为零。若除数为零,程序终止并输出错误信息(包含文件名、行号及断言条件)。 - 正常调用:
divide(10, 2)不会触发断言,输出结果5。 - 异常调用:
divide(10, 0)触发断言失败,程序终止。
7.5禁用assert的方法
在发布版本中,可通过定义NDEBUG宏禁用assert:
#define NDEBUG
#include <assert.h>
此时,所有assert语句会被预处理器忽略,不会影响程序性能。
7.6 注意事项
- 仅用于调试:assert不应替代正常的错误处理逻辑(如输入验证)。
- 副作用问题:断言条件中的表达式应避免副作用(如修改变量值),否则禁用assert时可能引发问题。
- 错误信息:断言失败会输出标准错误流(stderr),便于日志记录。
通过合理使用assert,可以显著提升代码的健壮性和可维护性。### assert断言的基本概念
assert是C语言标准库中的一个宏,用于在程序运行时检查某个条件是否满足。若条件为真(非零),程序继续执行;若条件为假(零),assert会输出错误信息并终止程序。assert常用于调试阶段,帮助开发者快速定位逻辑错误。
8.指针使用和传址调用
8.1传值调用和传址调用
8.1.1传值调用(Call by Value)
传值调用是一种参数传递方式,函数调用时会将实际参数的值复制一份传递给形式参数。在函数内部对形式参数的修改不会影响实际参数的值。传值调用的特点是简单、安全,但可能因复制大量数据而影响性能。
特点:
- 实际参数和形式参数占用不同的内存空间。
- 函数内部对参数的修改不会影响调用者的变量。
- 适用于基本数据类型(如整数、浮点数)或小型结构体。
示例:
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 1, y = 2;
swap(x, y); // x和y的值不会被交换
return 0;
}
8.1.2传址调用(Call by Reference)
传址调用是一种参数传递方式,函数调用时会将实际参数的地址(指针)传递给形式参数。通过地址可以直接操作实际参数的值。传址调用的特点是高效,但可能因直接修改数据而带来风险。
特点:
- 实际参数和形式参数共享同一内存地址。
- 函数内部对参数的修改会影响调用者的变量。
- 适用于需要修改实际参数或传递大型数据的场景。
示例:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 1, y = 2;
swap(&x, &y); // x和y的值会被交换
return 0;
}
8.1.3传值与传址的对比
| 特性 | 传值调用 | 传址调用 |
|---|---|---|
| 参数传递方式 | 复制值 | 传递地址(指针) |
| 内存占用 | 额外内存存储副本 | 直接操作原数据 |
| 安全性 | 高(不影响原数据) | 低(可能误修改原数据) |
| 性能 | 可能较低(复制开销) | 较高(无复制开销) |
| 典型应用场景 | 基本数据类型、小型结构体 | 大型数据、需要修改原数据时 |
8.2传址调用与指针的联系
8.2.1 指针的作用
指针是存储内存地址的变量,通过指针可以间接访问或修改内存中的数据。指针的核心功能是提供对内存的直接操作能力,常用于动态内存分配、数组操作和函数参数传递等场景。
8.2.2传址调用与指针的联系
传址调用通常通过指针实现。在C/C++等语言中,将指针作为参数传递给函数,函数通过解引用指针(*操作)修改原始变量的值。例如:
void modifyValue(int *ptr) {
*ptr = 10; // 通过指针修改原始变量的值
}
int main() {
int a = 5;
modifyValue(&a); // 传递变量a的地址
return 0;
}
通过指针实现的传址调用是底层编程的核心机制,理解其原理有助于优化性能并避免内存错误。
本期有关指针的讲解,到此结束,下期再见👋

2208

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



