C语言 指针
创始人
2024-02-10 19:46:11
0

C语言 指针

  • 引言
    • 1. 什么是指针
    • 2. 简单认识指针
    • 3. 取地址符 & 和解引用 * 符
  • 一、指针与内存
  • 二、指针类型的存在意义
    • 1. 指针变量的大小
    • 2. 指针移动
    • 3. 不同指针类型的解引用
  • 三、指针运算
    • 1. 指针加减整数
      • 程序清单1
      • 程序清单2
    • 2. 指针 - 指针
    • 3. 指针关系运算
  • 四、二级指针
  • 五、野指针
    • 程序清单1
    • 程序清单2
    • 如何避免野指针问题
  • 六、字符指针
    • 程序清单1
    • 程序清单2

引言

1. 什么是指针

1. 指针是内存中一个最小单元的编号,通俗的说,指针也就是地址。

2. 平时口语中说的指针,通常指的是指针变量,即存储一块内存地址的一个变量。

3. 在 32位 的机器上,地址是 32个 0或1 组成二进制序列,此时地址就得用 4 个字节的空间来存储,所以此时一个指针变量的大小就应该是 4 个字节。同样地,在 64位 的机器上,地址是 64个 0或1 组成二进制序列,此时地址就得用 8 个字节的空间来存储,所以此时一个指针变量的大小就应该是 8 个字节。

综上所述,指针即指针变量,它占用内存大小要么为 4,要么为 8.

2. 简单认识指针

经过上面的介绍,我们就来简单地认识下图的指针。

下面的两行代码,我画了一幅图来解释它。我们可以说,指针变量 pa 指向 整型变量 a. 也可以说,指针变量 pa 存储了变量 a 的地址。

备注: 0x11332244 是 a 的十六进制地址,0x00001111 是 pa 的十六进制地址,这两者不要混淆了。因为指针本质上也是一个变量,既然是变量,那么它在创建的时候,底层就会为其开辟内存。

1-1

3. 取地址符 & 和解引用 * 符

int a = 10;
int* pa = &a; // 将 a 的地址赋给 pa
*pa = 20; // 将 a 的值改为 20

① int* 表示 pa 是一个整型指针变量。
② *pa 表示解引用 指针变量 pa,*pa 就等价于 a.
③ 通俗的来说,解引用符和取地址符是可以充当 " 抵消的作用 " 。

*pa <==> *(&pa) <==> a

一、指针与内存

指针就是地址,有了地址,就能帮助我们快速地找到一块内存空间。

程序清单:

#include int main() 
{int a = 10;int* pa = &a; // 取出 a 的地址赋值给指针变量 pa*pa = 20; // *pa == aprintf("%d\n", a);return 0;
}// 输出结果:20

在上面的程序中,&a 表示取出 int变量 a 的地址 (取出的是 变量a 的第一个字节地址);*pa 表示解引用 pa,*pa 就等价于 a.

如下图所示:(假设虚拟地址空间为 32位)

1-2

注意事项:

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。为了能够有效访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。在 C语言中,每创建一个变量就会在底层开辟地址。

① 内存会被划分为小的内存单元,一个内存单元的大小是1个字节。
② 每个内存单元都有编号,这个编号也被称为:地址 / 指针。
③ 地址 / 指针可以存放在一个变量中, 这个变量称为指针变量,指针变量也是一个变量,它也有自己的地址。
④ 通过指针变量中存储的地址,就能找到指针指向的空间。

二、指针类型的存在意义

1. 指针变量的大小

程序清单:

#include int main() 
{int a = 10;char ch = 'a';double d = 3.14;int* pa = &a;char* pc = &ch;double* pd = &d;printf("%d\n", sizeof(pa)); //4printf("%d\n", sizeof(pc)); //4printf("%d\n", sizeof(pd)); //4return 0;
}

结论:

指针变量是用来存放地址的。所以,地址的存放需要多大空间,指针变量的大小就应该是多大。

① 32位 机器,支持 32位 虚拟地址空间,其产生的地址就是 32位,所以此时指针变量就需要 32位 的空间存储,即 4字节。
② 64位 机器,支持 64位 虚拟地址空间,其产生的地址就是 64位,所以此时指针变量就需要 64位 的空间存储,即 8字节。

2. 指针移动

程序清单:

#include int main() {int a = 3;char ch = 'a';int* pa = &a;char* pc = &ch;printf("%p\n", pa);printf("%p\n", pa + 1);printf("%p\n", pc);printf("%p\n", pc + 1);return 0;
}

输出结果:

1-3

总结:

从输出结果来看,指针类型决定了指针向前或者向后走一步有多大距离。当一个整型指针进行挪动的时候,移动 4 个字节;当一个字符指针进行挪动的时候,移动 1 个字节。这是一个很重要的知识点,因为这决定了一个指针一次性访问多少个字节。

1-4

3. 不同指针类型的解引用

① 对一个整型指针变量解引用后,并为之赋值。

1-5

② 对一个字符指针变量解引用后,并为之赋值。

1-6

总结:

从输出结果来看,指针类型也决定了指针进行解引用时能操作几个字节。当对一个整型指针变量解引用后,能操作 4 个字节;当对一个字符指针变量解引用后,能操作 1 个字节。

三、指针运算

1. 指针加减整数

程序清单1

#include int main() {int a = 3;int* pa = &a;printf("%p\n", pa);printf("%p\n", pa + 1);printf("%p\n", pa - 1);return 0;
}

输出结果:

1-7

程序清单2

#include void print(int arr[]) {for (int i = 0; i < 10; i++) {printf("%d ", arr[i]);}printf("\n");
}int main() {int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];print(arr);for (int i = 0; i < 10; i++) {*p = 0; // 将数组的每一个元素都设置成 0p++; 	// 将指针往后挪动一个元素}print(arr);return 0;
}

输出结果:

1-8

2. 指针 - 指针

程序清单:

#include int main() {int arr[10] = { 0 };int* p = NULL;printf("%d\n", &arr[8] - &arr[1]);printf("%d\n", &arr[1] - &arr[8]);printf("%d\n", &arr[8] - p);return 0;
}

输出结果:

1-9

注意事项:

① 从上面的输出结果来看," 指针 - 指针 " 运算适用于两个指针指向同一块空间才有意义。由于数组的内存地址是连续的,且由低到高变化,所以 " 指针 - 指针 " 运算就相当于数组下标之差。

② " 指针 - 指针 " 也可以理解为两个指针之间隔了多少个元素,其差值结果是一个数值,而不是字节。 这一点不能单纯的与指针变量 " 所占用内存的大小之差 " 的概念弄混淆了。

3. 指针关系运算

程序清单:

#include int main() {int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };if (arr[1] >= arr[3]) {printf("haha\n");}else {printf("hehe\n");}// 指针关系运算// 随着数组下标增长,数组的地址由低到高变化if (&arr[1] >= &arr[3]) {printf("haha\n");}else {printf("hehe\n");}return 0;
}// 输出结果:
// haha
// hehe

四、二级指针

二级指针即指针的指针,它存放的是指针变量的地址。一级指针的取地址、解引用等操作,也可以类比到此处的二级指针。

程序清单:

#include int main() {int a = 10;int* pa = &a;int** ppa = &pa;**ppa = 20;printf("%d\n", a);return 0;
}// 输出结果:20

五、野指针

野指针:指针指向的位置是不可知的、随机的、不正确的、没有明确限制的。

程序清单1

#include int main()
{int* p;	 //局部变量指针未初始化,默认为随机值*p = 20;return 0;
}

程序清单2

指针访问数组越界。

#include int main()
{int arr[10] = { 0 };int* p = arr;int i = 0;for (i = 0; i < 20; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}return 0;
}

如何避免野指针问题

1. 当指针在定义时,不知道指向谁,初始化为 NULL.
2. 预防指针越界。
3. 使用指针前,进行 assert 断言。

#include 
#include int main() {int a = 10;int* pa = &a;int* p = NULL;assert(pa != NULL);assert(p != NULL); // 编译器直接提示报错信息}

注意事项:

① assert 在使用时需要引入头文件
② 如果 assert 括号内的条件为真,则程序正常执行;如果它括号内的条件为假,则会直接报错,并提示错误信息,精确到行。

六、字符指针

字符指针通常与字符串相关联,这里需要明确的是,字符指针通常存储的是字符串中的首个字符的地址,而不是整个字符串的地址。

程序清单1

#include int main() {char* p = "abcdef"; // p 指向字符串的第一个字符printf("%c\n", *p);printf("%s\n", p);return 0;
}// 输出结果:
// a
// abcdef

注意事项:

需要明确: 指针 p 指向 " abcdef " 的第一个字符 ’ a ’ 的地址,而不是整个字符串的地址。或者说,指针 p 中存放的字符 ’ a ’ 的地址。

② 针对上面的第二个输出结果,为什么对一个字符指针变量打印就能够输出整个字符串呢?原因在于:指针 p 指向第一个字符,就能够找到整个字符串后面的所有字符。这和顺藤摸瓜是一个道理。

③ 我们日常所说的字符串其实是一个常量字符串,放在常量区,不可被修改。所以当我们创建一个字符指针,用于指向一个字符串时,就可以将这个指针变量添加 const 修饰符,这样更加规范。

const char* p = "abcdef";

程序清单2

#include int main() {char* p1 = "abcdef";char* p2 = "abcdef";char arr1[] = "abcdef";char arr2[] = "abcdef";if (p1 == p2) {printf("p1 == p2\n");}else {printf("p1 != p2\n");}if (arr1 == arr2) {printf("arr1 == arr2\n");}else {printf("arr1 != arr2\n");}return 0;
}// 输出结果:
// p1 == p2
// arr1 != arr2

注意事项:

① 分析第一个输出结果,当我们创建两个字符指针时,它们指向的都是字符串的首字符地址,而字符串又是常量字符串,不可被更改,所以,p1 和 p2 都指向同一份 ’ a ’ 的地址。

② 分析第二个输出结果,当我们创建两个字符数组时,同样的常量字符串中的字符被放入了不同的数组,数组在栈区开辟了新的内存,所以两个数组首元素的地址是不同的。

相关内容

热门资讯

京尹资讯:暖冬相聚,情满京尹—... 岁末暖冬,温情相聚。在平安夜与圣诞节的温馨氛围中,北京京尹律师事务所将节日庆典与十二月生日会巧妙融合...
空客因坠机事故被遇难者家属起诉 中国航空新闻网讯:据外媒12月25日报道,两名在2023年12月一起直升机坠毁事故中遇难机组人员的家...
保险消费陷阱解析:销售误导、理... 在本期《年度消费避坑图鉴》中,我们聚焦保险领域。尽管投诉占比为2.05%,但其条款复杂、信息不对称,...
面对不确定的世界经济,中国如何... 2025年,关税与贸易壁垒再度成为全球经济辩论的核心。联合国贸易和发展会议发布的《2025年贸易与发...
征求意见丨关于公开征求《辽宁省... 为进一步加强知识产权保护,激发创新创造活力,推动知识产权强省建设,我局起草形成了《辽宁省知识产权保护...
《反有组织犯罪法》宣传进校园 ... 筑牢校园安全防线,守护青少年健康成长,是人民法院肩负的重要职责。为进一步增强青少年法治意识,营造安全...
尹锡悦、金建希夫妇同日被起诉 当地时间12月26日,韩国金建希特检组以违反《公职选举法》为由对前总统尹锡悦提出起诉。 同日,韩国...
宣威市工商联联合“三方四家”成... 为进一步强化新时代民营企业家学法、守法、依法经营的法治意识,引导民营经济人士增强对中国特色社会主义的...