• C语言 指针


    引言

    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
    
    • 1
    • 2
    • 3

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

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

    一、指针与内存

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

    程序清单:

    #include 
    
    int main() 
    {
    	int a = 10;
    	int* pa = &a; // 取出 a 的地址赋值给指针变量 pa
    	*pa = 20; // *pa == a
    	printf("%d\n", a);
    	return 0;
    }
    
    // 输出结果:20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在上面的程序中,&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)); //4
    	printf("%d\n", sizeof(pc)); //4
    	printf("%d\n", sizeof(pd)); //4
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    结论:

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

    ① 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    输出结果:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    输出结果:

    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; // 将数组的每一个元素都设置成 0
    		p++; 	// 将指针往后挪动一个元素
    	}
    	print(arr);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    输出结果:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    输出结果:

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    四、二级指针

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

    程序清单:

    #include 
    
    int main() {
    
    	int a = 10;
    	int* pa = &a;
    	int** ppa = &pa;
    	
    	**ppa = 20;
    	printf("%d\n", a);
    
    	return 0;
    }
    
    // 输出结果:20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    五、野指针

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

    程序清单1

    指针 p 没有指向任何地址。所以 p 中存放的可能是一个随机地址,或者说, 指针 p 随意指向了内存中的一块区域,如果我们再次对指针 p 解引用,就会造成非法访问。

    #include 
    
    int main()
    {
    	int* p;	 //局部变量指针未初始化,默认为随机值
    	*p = 20;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    程序清单2

    指针访问数组越界。这就好像我们给指针 p 只规定了一块限定区域,超出这个区域,它就访问到了 " 无名区 "。

    #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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如何避免野指针问题

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

    #include 
    #include 
    
    int main() {
    
    	int a = 10;
    	int* pa = &a;
    	int* p = NULL;
    
    	assert(pa != NULL);
    	assert(p != NULL); // 编译器直接提示报错信息
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意事项:

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

    六、字符指针

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

    程序清单1

    #include 
    
    int main() {
    
    	char* p = "abcdef"; 
    
    	// p 指向字符串的第一个字符
    	printf("%c\n", *p);
    	printf("%s\n", p);
    
    	return 0;
    }
    
    // 输出结果:
    // a
    // abcdef
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意事项:

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

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

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

    const char* p = "abcdef";
    
    • 1

    程序清单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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注意事项:

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

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

  • 相关阅读:
    正则表达式提取豆瓣T250中的部分信息
    如何写一个中间件的springboot的starter?
    B2B电子商务策略[在2022年发展您的业务]
    2020年09月 C/C++(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
    人工智能知识全面讲解:回归分析
    想开发DAYU200,我教你
    云原生之容器编排实践-阿里云私有容器镜像仓库
    iOS微信发布8.0.29版本,苹果14用户快来
    X86(32位)汇编指令与机器码转换原理
    五个维度着手MySQL的优化,我和面试官都聊嗨了
  • 原文地址:https://blog.csdn.net/lfm1010123/article/details/127824637