文章目录
- 一、内存和地址
- 1.1 内存的基本概念
- 1.2 编址的原理
- 二、指针变量和地址
- 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修饰指针变量
- 五、指针运算
- 5.1 指针±整数
- 5.2 指针-指针
- 5.3 指针的关系运算
- 六、野指针
- 6.1 野指针成因
- 6.2 如何规避野指针
- 七、assert断言
- 八、指针的使用和传址调用
- 8.1 strlen的模拟实现
- 8.2 传值调用和传址调用
一、内存和地址
1.1 内存的基本概念
在计算机中,CPU处理数据时需要从内存中读取数据,处理后的数据也会放回内存。为了高效管理内存,内存被划分为一个个大小为1字节的内存单元,每个内存单元都有唯一的编号,这个编号就是我们所说的地址,在C语言中也被称为指针。
举个例子:一栋宿舍楼有100个房间,给每个房间编上号(如101、102等),根据房间号就能快速找到房间。内存就像这栋宿舍楼,每个内存单元就像一个房间,地址则是房间号,有了地址,CPU就能快速找到对应的内存单元。
计算机中常见的存储单位及换算关系如下:
- 1byte(字节)= 8bit(比特位)
- 1KB = 1024byte
- 1MB = 1024KB
- 1GB = 1024MB
- 1TB = 1024GB
- 1PB = 1024TB
1.2 编址的原理
CPU访问内存中的某个字节空间,必须知道该字节空间的地址。计算机的编址是通过硬件设计实现的,就像钢琴、吉他等乐器,制造商在硬件层面设计好,演奏者就能准确找到相应位置。
CPU和内存之间通过大量的线连接,其中一组重要的线是地址总线。32位机器有32根地址总线,每根线有0和1两种状态(表示电脉冲有无),32根地址线能表示2^32种不同的地址。地址信息通过地址总线传给内存,内存根据地址找到对应数据,再通过数据总线传入CPU寄存器。
二、指针变量和地址
2.1 取地址操作符(&)
在C语言中,创建变量的本质是向内存申请空间。
#include <stdio.h>
int main()
{int a = 10;return 0;
}
变量a
占用4个字节的内存空间,每个字节都有自己的地址。我们可以使用取地址操作符&
来获取变量的地址,如&a
得到的是a
所占4个字节中地址较小的那个字节的地址。
2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
通过取地址操作符&
得到的地址是一个数值,我们可以将其存储在指针变量中。指针变量就是专门用来存放地址的变量。
#include <stdio.h>
int main()
{int a = 10;int* pa = &a; // 取出a的地址并存储到指针变量pa中return 0;
}
2.2.2 指针类型的解读
指针变量的类型由*
和前面的类型组成,如int*
表示该指针变量指向的是整型(int
)类型的对象。对于char
类型的变量ch
,其地址应存放在char*
类型的指针变量中。
2.2.3 解引用操作符
有了指针变量存储地址后,我们可以使用解引用操作符*
通过地址找到对应的变量并进行操作。
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0; // 通过pa中存放的地址找到a,并将a的值改为0return 0;
}
这里*pa
就相当于变量a
,通过*pa
可以对a
进行修改,这为操作变量提供了另一种途径。
2.3 指针变量的大小
指针变量的大小取决于地址的大小:
- 在32位平台下,地址是32个比特位,指针变量大小为4个字节。
- 在64位平台下,地址是64个比特位,指针变量大小为8个字节。
需要注意的是,指针变量的大小和其类型无关,在相同平台下,所有指针类型的变量大小都是相同的。例如:
#include <stdio.h>
int main()
{printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(double*));return 0;
}
在32位环境下输出结果均为4,在64位环境下输出结果均为8。
三、指针变量类型的意义
虽然指针变量的大小和类型无关,但指针类型有着重要的意义。
3.1 影响解引用的权限
指针的类型决定了对指针解引用时的操作权限,即一次能操作的字节数。例如:
char*
类型的指针解引用只能访问1个字节。int*
类型的指针解引用能访问4个字节。
看下面两段代码:
// 代码1
int main() {int a = 0x11223344;int* p = &a;*p = 0;return 0;
}
逐语句调试,代码运行到15行时。int*
可以访问四个字节,将四个字节都改为0
// 代码2
int main() {int a = 0x11223344;char* p = &a;*p = 0;return 0;
}
逐语句调试,代码运行到22行时。char*
只访问访问一个字节,将第一个字节改为0
3.2 影响指针 ± 整数的步长
指针的类型决定了指针向前或者向后走一步的距离。例如:
int* + 1
跳过四个字节(int大小),char* + 1
跳过一个字节(char大小)
3.3 void* 指针
void*
类型的指针可以接受任意类型的地址,可以理解为无具体类型指针(或者叫泛型指针)但它不能直接进行指针的±整数和解引用运算。通常用于函数参数部分,实现泛型编程的效果,以处理多种类型的数据。
四、const修饰指针
4.1 const修饰变量
被const
修饰的变量不能直接被修改。
#include <stdio.h>
int main()
{const int n = 0;n = 20; // 报错,n不能被直接修改return 0;
}
此时变量具有常属性,称为常变量,但本质依旧是变量而不是常量。
在C++中被const
修饰则为常量。
但如果通过指针获取其地址,还是可以修改该变量的值,这显然打破了const
的限制,所以需要用const
修饰指针变量。
4.2 const修饰指针变量
const
放在*
的右边:修饰的是指针变量本身,指针变量不可以再指向其他变量。但可以通过指针修改指向的内容。
int main()
{int a = 10;int b = 20;int * const p = &a;p = &b;//err*p = 100;//可以通过编译return 0;
}
const
放在*
的左边:限制指向的内容,不可以通过指针来修改,但可以修改指针指向的变量。
int main()
{int a = 10;int b = 20;int const* p = &a;p = &b;//可以通过编译*p = 100;//errreturn 0;
}
五、指针运算
指针的基本运算有三种:
5.1 指针±整数
原理同本文3.2部分。
由于数组在内存中是连续存放的,知道第一个元素的地址后,通过指针±整数可以访问数组中的其他元素。
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)); // 通过指针+整数访问数组元素}return 0;
}
注意:
*(p+1)
不要写成*p+1
,前者表示指针变量+1,后者表示p指向的内容+1- 在
sizeof()
中,输入数组名arr
,计算整个数组的大小。
5.2 指针-指针
通过上述,我们可以明确:
指针1 + 整数 = 指针2
以此推理出
整数 = 指针2 - 指针1
类比“日期 - 日期”,得到之间的天数。两个指针相减的结果是它们之间的元素个数,常用于计算字符串长度等场景。
strlen()
求字符串长度,统计字符串\0
之前字符个数- 数组名arr是数组首元素的地址。
arr
等价于&arr[0]
模拟实现strlen()
函数:
- 方法1 计数器
int my_strlen(char* s)
{int cnt = 0; //计数器while(*s != '\0'){cnt++;str++}return cnt; // 计算两个指针之间的元素个数,即字符串长度
}
int main()
{printf("%d\n", my_strlen("abc")); //输出3return 0;
}
- 方法2 指针 - 指针
int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s; // 计算两个指针之间的元素个数,即字符串长度
}
int main()
{printf("%d\n", my_strlen("abc")); //输出3return 0;
}
注意:
- 指针 - 指针 的前提时两个指针指向同一块空间!
例如:
int main()
{int arr[10] = {0};char ch[10] = {'0'};printf("%d\n",&ch[0] - &arr[0]);//errreturn 0;
}
- "日期 + 日期"没有意义,同样的,“指针 + 指针”也没有任何意义。
5.3 指针的关系运算
指针与指针比较大小,其实就是地址与地质比较大小。
数组随下标变大,地址由低变高。
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) // 指针的大小比较,当p指向的地址小于arr+sz(相当于数组最后一个元素的地址)进入循环{printf("%d ", *p);p++;}//输出 1 2 3 4 5 6 7 8 9 10return 0;
}
六、野指针
野指针就是指针指向内容是不可知的(不正确、随机、没有明确限制)
6.1 野指针成因
- 指针未初始化:局部变量指针未初始化时,其值是随机的。
int main()
{ int *p;//此时p是局部变量,指针未初始化,默认为随机值 *p = 20;return 0;
}
- 指针越界访问:指针指向的范围超出数组等申请的内存空间。
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;
}
- 指针指向的空间释放:返回局部变量的地址,该局部变量的空间在函数调用结束后会被释放。
int* test()
{int n = 100;return &n;
}int main()
{int*p = test();printf("%d\n", *p);//此时test()调用完成,栈帧被销毁, 内存被释放return 0;
}
6.2 如何规避野指针
- 指针初始化:明确指向时直接赋值地址,否则赋值
NULL
(NULL
是值为0的标识符常量,该地址无法使用,读写地址也会报错)。
int main()
{int* p = NULL;*p = 20//err
}
- 小心指针越界:不访问超出申请内存范围的空间。
- 及时置
NULL
并检查:指针变量不再使用时置为NULL
,使用前判断是否为NULL
。 - 避免返回局部变量的地址。
七、assert断言
assert.h
头文件中的assert()
宏用于在运行时确保程序符合指定条件,如果不符合就报错终止运行。其表达式为真时程序继续运行,为假时报错并显示相关信息。
assert(p != NULL);//确保 p为有效指针
assert()
断言相对if
语句的优点:- 出现错误会直接报错,指明在什么文件,哪一行
- 无需修改代码就可以禁用
assert()
.可以通过在#include <assert.h>
前定义NDEBUG
宏来禁用assert()
语句.
- 缺点:
- 引入了额外的检查,增加了程序运行时间
通常在Debug版本中使用,Release版本中禁用,以不影响程序效率。VS2022中release版会直接禁用assert
八、指针的使用和传址调用
8.1 strlen的模拟实现
strlen
函数用于求字符串长度,统计的是字符串中\0
之前的字符个数。模拟实现如下:
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)//限制内容,不能被修改
{int count = 0;assert(str); // 确保 str不为NULLwhile (*str){count++;str++;}return count;
}
int main()
{int len = my_strlen("abcdef");printf("%zd\n", len);return 0;
}
求出的长度不可能是负数,因此返回值类型使用size_t
(无符号整型)更合适,打印应使用zd%
作为占位符。
8.2 传值调用和传址调用
-
传值调用:实参传递给形参时,形参创建临时空间接收实参。形参和实参是独立的两个空间。形参只是实参的一份临时拷贝,对形参的修改不影响实参。
-
传址调用:将变量的地址传递给函数,函数内部通过地址间接操作主调函数中的变量,可实现对变量的修改。
例如交换两个整型变量的值,使用传址调用:
#include <stdio.h>
void Swap(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);Swap(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}
运行结果:
指针是C语言的精华,掌握好指针能让我们在编程中更加得心应手。希望本文能帮助大家更好地理解指针的相关知识,后续还会有更深入的探讨。