【维生素C语言】第六章 - 指针_i++

前言:

本章是指针部分的开始,将对C语言中非常重要的指针进行讲解。本章结束后有能力的读者可对应指针进阶部分进行进一步学习。指针专题配备了一些笔试题,建议尝试。



一、指针的定义

【维生素C语言】第六章 - 指针_i++_02


0x00 何为指针

❓ 我们先来看看定义:

指针是编程语言中的一个对象,利用地址,他的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

📚 简单地说:指针就是地址,地址就是指针;

📌 注意事项:

      ① 指针就是变量,用来存放地址的变量(存放在之阵中的值都被当成地址处理);

      ② 一个小的内存单元大小为 1 个字节;

      ③ 指针是用来存放地址的,地址是唯一标识一块内存空间的;

      ④ 指针的大小在 32 位平台上是 4 个字节,在 64 位平台上是 8 个字节;

💬 指针的创建:


int main()
{
int a = 10; // 在内存中开辟一块空间
int* pa = &a; // 使用解引用操作符&,取出变量a的地址
// 👆 将a的地址存放在pa变量中,此时pa就是一个指针变量
return 0;
}


❓ 什么是指针变量

💡 指针变量就是存放指针的变量,这里的 int* pa

0x02 指针的大小

💻 32位平台:4 bit , 64位平台:8 bit ;

💬 验证当前系统的指针大小:


int main()
{
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(double*));

return 0;
}


二、指针的指针类型

0x00 指针类型

📚  int 型指针和 char


int main()
{
int a = 0x11223344;
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);
printf("%p\n", pc);

return 0;
}


🚩 运行结果:他们的运行结果是一样的

0x01 指针类型的意义

📚 指针类型决定了指针进行解引用时,能够访问的内存大小是多少;

【维生素C语言】第六章 - 指针_i++_03


💬 不同的指针类型,访问的大小不同:


int main()
{
int a = 0x11223344;
int* pa = &a; // 44 33 22 11 (至于为什么是倒着的,后面会讲。)
*pa = 0;// 00 00 00 00

char* pc = &a; // 44 33 22 11
*pc = 0; // 00 33 22 11
// 在内存中仅仅改变了一个字节

// 解引用操作时就不一样了
// 整型指针操作了4个字节,让四个字节变为0
// 字符指针能把地址交到内存中,
// 但是解引用操作时,只敢动1个字节

return 0;
}


0x02 指针加减整数

📚 定理:指针类型决定指针步长(指针走一步走多远);

【维生素C语言】第六章 - 指针_指针_04


💬 代码验证:指针类型决定指针步长;


int main()
{
int a = 0x11223344;
int* pa = &a;
char* pc = &a;

printf("%p\n", pa); // 0095FB58
printf("%p\n", pa+1); // 0095FB5C +4

printf("%p\n", pc); // 0095FB58
printf("%p\n", pc+1); // 0095FB59 +1

return 0;
}


0x03 指针修改数组元素

💬 把数组里的元素都改成1

1. 使用整型指针:


int main()
{
int arr[10] = {0};
int* p = arr; //数组名 - 首元素地址

/* 修改 */
int i = 0;
for(i=0; i<10; i++) {
*(p+i) = 1; //成功,arr里的元素都变为了1
}

/* 打印 */
for(i=0; i<10; i++) {
printf("%d ", arr[i]);
}

return 0;
}


🚩  1 1 1 1 1 1 1 1 1 1

2. 使用字符指针:


int main()
{
int arr[10] = {0};
char* p = arr; //数组名 - 首元素地址

/* 修改 */
int i = 0;
for(i=0; i<10; i++)
{
*(p+i) = 1; // 一个一个字节改,只改了十个字节
}

return 0;
}


💡 解析:

【维生素C语言】第六章 - 指针_i++_05


🔺 总结:

     ① 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节);

char* 的指针解引用只能访问1个字节,而 int* 的指针解引用就能够访问4个字节

三、野指针(Wild pointer)

0x00 野指针的概念

【维生素C语言】第六章 - 指针_c语言_06


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

野指针指向了一块随机的内存空间,不受程序控制;

0x01 野指针的成因

📚 原因:

     ① 指针未初始化;

     ② 指针越界访问;

     ③ 指针指向的空间已释放;

💬 指针未初始化

∵ 局部变量不初始化默认为随机值:


int main()
{
int a;//局部变量不初始化默认为随机值
printf("%d", a);

return 0;
}


∴ 同理,局部的指针变量,如果不初始化,默认为随机值:


int main()
{
int *p; //局部的指针变量,就被初始化随机值
*p = 20; //内存中随便找个地址存进去

return 0;
}


💬 指针越界访问

指针越界,越出arr管理范围时会产生野指针:


int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<12; i++)
{
//当指针越出arr管理的范围时,p就称为野指针
p++;
}

return 0;
}


💬 指针指向的空间已释放


int* test()
{
int a = 10;

return &a;
}

int main()
{
int *pa = test();
*pa = 20;

return 0;
}


🔑 解析:

test 函数内时就创建一个临时变量 a(10 - 0x0012ff44),这个a是局部变量,进入范围时创建,一旦出去就销毁,销毁就意味着这个内存空间还给了操作系统,这块空间(0x0012ff44)就不再是 a 的了;

a,有了地址,ruturn &a 把地址返回去了,但是这个函数一结束,这块空间就不属于自己了,当你使用时,这块空间已经释放了,指针指向的空间被指放了,这种情况就会导致野指针的问题;

     ③ 只要是返回临时变量的地址,都会存在问题(除非这个变量出了这个范围不销毁);

0x02 如何规避野指针

【维生素C语言】第六章 - 指针_c语言_07


💬 指针初始化


int main()
{
int a = 10;
int* pa = &a; // 初始化
int* p = NULL; // 当你不知道给什么值的时候用NULL

return 0;
}


💬 指针指向空间释放及时置 NULL


int main()
{
int a = 10;
int *pa = &a;
*pa = 20;

//假设已经把a操作好了,pa指针已经不打算用它了
pa = NULL; //置成空指针

return 0;
}


💬 指针使用之前检查有效性


int main()
{
int a = 10;
int *pa = &a;
*pa = 20;

pa = NULL;
//*pa = 10; 崩溃,访问发生错误,指针为空时不能访问
if(pa != NULL) { // 检查 如果指针不是空指针
*pa = 10; // 检查通过才执行
}

return 0;
}


四、指针运算

0x00 指针加整数

💬 指针加整数:打印 1 2 3 4 5 6 7 8 9 10


int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr; // 指向数组的首元素 - 1

for(i=0; i<sz; i++) {
printf("%d ", *p);
p = p + 1; //p++ 第一次循环+1之后指向2
}

return 0;
}


🚩  1 2 3 4 5 6 7 8 9 10

0x01 指针减整数

💬 指针减整数:打印 10 8 6 4 2


int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[9]; // 取出数组最后一个元素的地址

for(i=0; i<sz/2; i++) {
printf("%d ", *p);
p = p - 2;
}
return 0;
}


0x02 指针后置++

#include <stdio.h>
#define N_VALUES 5

int main() {
float value[N_VALUES];
float* vp;

for (vp = &value[0]; vp < &value[N_VALUES];) {
*vp++ = 0;
printf("%d ", *vp);
}

return 0;
}

0x03 指针减指针

📚 说明:指针减指针得到的是元素之间元素的个数;

📌 注意事项:当指针减指针时,他们必须指向同一空间(比如同一个数组的空间);

💬 指针减指针:


int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d\n", &arr[9] - &arr[0]); // 得到指针和指针之间元素的个数

return 0;
}


🚩  9

❌ 错误演示:不在同一内存空间


int ch[5] = {0};
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d\n", &arr[9] - &ch[0]); // 没有意义,结果是不可预知的


💬 手写 strlen


int my_strlen(char* str)
{
char* start = str;
char* end = str;
while(*end != '\0') {
end++;
}
return end - start; //return
}
int main()
{
//strlen - 求字符串长度
//递归 - 模拟实现了strlen - 计数器方式1, 递归的方式2

char arr[] = "abcdef";
int len = my_strlen(arr); //arr是首元素的地址
printf("%d\n", len);

return 0;
}


⚡ 简化(库函数的写法):


int my_strlen(const char* str)
{
const char* end = str;
while(*end++);
return (end - str - 1);
}

int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);

return 0;
}


0x04 指针的关系运算(比较大小)

💬 指针减减指针:


#define N_VALUES 5

int main()
{
float values[N_VALUES];
float *vp;

for(vp=&values[N_VALUES]; vp> &values[0]; ) {
*--vp = 0; //前置--
}

return 0;
}


⚡ 简化(这么写更容易理解,上面代码 *--vp在最大索引后的位置开始访问的):


int main()
{
float values[5];
float *vp;

for(vp=&values[N_VALUES]; vp> &values[0]; vp--) {
*vp = 0;
}

return 0;
}


❗  实际在绝大部分编译器上是可行的,但是我们应该避免这么写,因为标准并不保证它可执行;、

🔑  解析:标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的拿个内存位置的指针比较,但是不允许与指向第一个元素之前的拿个内存位置的指针进行比较;

【维生素C语言】第六章 - 指针_指针_08


五、指针和数组

0x00 数组名

📚 数组名在 绝大部分情况下都是首元素地址;

💬 大多数情况下数组名是首元素地址:arr 等同于 &arr[0]


int main()
{
int arr[10] = {0};
printf("%p\n", arr); // 数组名是地址,首元素地址
printf("%p\n", &arr[0]); // 结果同上
printf("%p\n", &arr); // 看下面的 “例外”

return 0;
}


🚩  00EFF8E0  00EFF8E0  00EFF8E0(这是整个元素的地址)

🌂 例外:

1.  &数组名( &arr

💬 &数组名 - 数组名表示的是整个数组:


int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", arr+1);

printf("%p\n", &arr[0]);
printf("%p\n", &arr[0]+1);

printf("%p\n", &arr);
printf("%p\n", &arr + 1); // +1,以整个元素为单位

return 0;
}


🚩  运行结果如下:

【维生素C语言】第六章 - 指针_c语言_09


2.  sizeof(数组名):计算的是整个数组的大小,单位是字节

💬 sizeof(数组名):数组名表示的是整个数组:


int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz);

return 0;
}


🚩  10

🔺 总结:数组名是首元素地址( 除&数组名  和 sizeof 数组名

0x01 使用指针访问数组

💬 p+i 计算的是数组 arr 下标为 i 的地址:


int main()
{
int arr[10] = {0};
int* p = arr; //这时arr数组就可以通过指针进行访问了
int i = 0;

for(i=0; i<10; i++) {
printf("%p == %p\n", p+i, &arr[i]);
}

return 0;
}


🚩  运行结果如下:

【维生素C语言】第六章 - 指针_数组名_10


💬 生成 0 1 2 3 4 5 6 7 8 9


int main()
{
int arr[10] = {0};
int* p = arr; // 这时arr数组就可以通过指针进行访问了
int i = 0;

printf("生成前:\n");
for(i=0; i<10; i++) {
printf("%d ", arr[i]);
}

for(i=0; i<10; i++) {
*(p+i) = i; // p+1=1, p+2=2, p+3=3...
// 👆 arr[i] = i; 等价于
}

printf("\n生成后:\n");
for(i=0; i<10; i++) {
printf("%d ", arr[i]);
// 👆 printf("%d ", *(p + i)); 等价于
}

return 0;
}


🚩  运行结果如下:

【维生素C语言】第六章 - 指针_i++_11


💬 生成 6 6 6 6 6 6 6 6 6 6


int main()
{
int arr[10] = {0};
int* p = arr; // 这时arr数组就可以通过指针进行访问了
int i = 0;


for(i=0; i<10; i++) {
*(p+i) = 8; // p+1=1, p+2=2, p+3=3...
}
for(i=0; i<10; i++) {
printf("%d ", *(p + i));
}

return 0;
}


🚩  6 6 6 6 6 6 6 6 6 6

六、二级指针(Second Rank Pointer)

0x00 二级指针的概念

📚 概念:指针变量也是变量,是变量就有地址,指针的地址存放在二级指针;

【维生素C语言】第六章 - 指针_指针_12


💬 二级指针:


int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa; // ppa就是二级指针
int*** pppa = &ppa; // pppa就是三级指针
...

**ppa = 20;
printf("%d\n", *ppa); // 20
printf("%d\n", a); // 20

return 0;
}


🔑 对于二级指针的运算:

     ① *ppa 通过对 ppa 中的地址进行解引用,找到了的是 pa,*ppa 其实访问的就是pa;

     ② **ppa 先通过 *ppa 找到 pa,然后对 pa 进行解引用操作,*pa 找到的就是 a;

七、指针数组(Pointer to Array)

0x00 指针数组的概念

📚 概念:指针数组本质上是数组,存放指针的数组;

📌 注意:不要和数组指针混淆,数组指针本质上是指针;

【维生素C语言】第六章 - 指针_指针_13


❓ 分析下面的数组:


int arr1[5];
char arr2[6];
int* arr3[5];


🔑 解析:

整型;

char 型;

整型指针;


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

比特科技. C语言基础[EB/OL]. 2021[2021.8.31]. .

📌 本文作者: Foxny

📃 更新记录: 2021.6.9

勘误记录:

📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。