【C语言】深入理解指针(1)

文章目录

  • 前言
  • 一、内存和地址
    • [1.1 内存](#1.1 内存)
    • [1.2 究竟该如何理解编址](#1.2 究竟该如何理解编址)
  • 二、指针变量和地址
    • [2.1 取地址操作符(&)](#2.1 取地址操作符(&))
    • [2.2 指针变量和解引⽤操作符(*)](#2.2 指针变量和解引⽤操作符(*))
      • [2.2.1 指针变量](#2.2.1 指针变量)
      • [2.2.2 如何拆解指针类型](#2.2.2 如何拆解指针类型)
      • [2.2.3 解引用操作符](#2.2.3 解引用操作符)
    • [2.3 指针变量的大小](#2.3 指针变量的大小)
  • 三、指针变量类型的意义
    • [3.1 指针的解引用](#3.1 指针的解引用)
    • [3.2 指针+-整数](#3.2 指针+-整数)
    • [3.3 void* 指针](#3.3 void* 指针)
  • [四、const 修饰指针](#四、const 修饰指针)
    • [4.1 const修饰变量](#4.1 const修饰变量)
    • [4.2 const修饰指针变量](#4.2 const修饰指针变量)
  • 五、指针运算
    • [5.1 指针+-整数](#5.1 指针+-整数)
    • [5.2 指针-指针](#5.2 指针-指针)
    • [5.3 指针的关系运算](#5.3 指针的关系运算)
  • 六、野指针
    • [6.1 野指针成因](#6.1 野指针成因)
      • [6.1.1 指针未初始化](#6.1.1 指针未初始化)
      • [6.1.2 指针越界访问](#6.1.2 指针越界访问)
      • [6.1.3 指针指向的空间释放](#6.1.3 指针指向的空间释放)
    • [6.2 如何规避野指针](#6.2 如何规避野指针)
      • [6.2.1 指针初始化](#6.2.1 指针初始化)
      • [6.2.2 小心指针越界](#6.2.2 小心指针越界)
      • [6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性](#6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性)
      • [6.2.4 避免返回局部变量的地址](#6.2.4 避免返回局部变量的地址)
  • [七、assert 断⾔](#七、assert 断⾔)
  • 八、指针的使⽤和传址调⽤
    • [8.1 strlen的模拟实现](#8.1 strlen的模拟实现)
    • [8.2 传值调用和传址调用](#8.2 传值调用和传址调用)
  • 总结

前言

本篇文章先对指针有个大概的了解。指针与地址,指针运算,野指针等。


一、内存和地址

1.1 内存

在讲内存和地址之前,我们想有个⽣活中的案例:

假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:

c 复制代码
⼀楼:101,102,103...
⼆楼:201,202,203...

有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。

⽣活中,每个房间有了房间号,就能提⾼效率,能快速的找到房间。

如果把上⾯的例⼦对照到计算机中,⼜是怎么样呢?

我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?

其实也是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。

计算机中常⻅的单位(补充):⼀个⽐特位可以存储⼀个2进制的位1或者0

c 复制代码
1Byte(字节) = 8bit(比特位)
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB

其中,每个内存单元,相当于⼀个学⽣宿舍,⼀个字节空间⾥⾯能放8个⽐特位,就好⽐同学们住的⼋⼈间,每个⼈是⼀个⽐特位。

每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的⻔牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。

⽣活中我们把⻔牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起

了新的名字叫:指针。

所以我们可以理解为:内存单元的编号 = = 地址 = = 指针

1.2 究竟该如何理解编址

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,⽽因为内存中字节很多,所以需要给内存进⾏编址(就如同宿舍很多,需要给宿舍编号⼀样)。

计算机中的编址,并不是把每个字节的地址记录下来,⽽是通过硬件设计完成的。

钢琴、吉他上⾯没有写上"剁、来、咪、发、唆、拉、西"这样的信息,但演奏者照样能够准确找到每⼀个琴弦的每⼀个位置,这是为何?因为制造商已经在乐器硬件层⾯上设计好了,并且所有的演奏者都知道。本质是⼀种约定出来的共识!

⾸先,必须理解,计算机内是有很多的硬件单元,⽽硬件单元是要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。

但是硬件与硬件之间是互相独⽴的,那么如何通信呢?答案很简单,⽤"线"连起来。

⽽CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也⽤线连起来。

不过,我们今天关⼼⼀组线,叫做地址总线

硬件编址也是如此

我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含义,每⼀种含义都代表⼀个地址。

地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。


二、指针变量和地址

2.1 取地址操作符(&)

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	return 0;
}

⽐如,上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都有地址,上图中4个字节的地址分别是:

c 复制代码
0x008FFB70
0x008FFB71
0x008FFB72
0x008FFB73

那我们如何能得到a的地址呢?

这⾥就得学习⼀个操作符(&)-取地址操作符

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	&a;//取出a的地址
	printf("%p\n", &a);
	return 0;
}

会打印处理:008FFB70

&a取出的是a所占4个字节中地址较⼩的字节的地址。

虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可⾏的。

2.2 指针变量和解引⽤操作符(*)

2.2.1 指针变量

那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x008FFB70,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量 中。

⽐如:

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;//取出a的地址并存储到指针变量pa中
	return 0;
}

指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址

2.2.2 如何拆解指针类型

我们看到pa的类型是int* ,我们该如何理解指针的类型呢?

c 复制代码
int a = 10;
int * pa = &a;

这⾥pa左边写的是int*,*是在说明pa是指针变量,⽽前⾯的int是在说明pa指向的是整型(int)类型的对象。

那如果有⼀个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?

c 复制代码
char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?

2.2.3 解引用操作符

我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?

在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。

c 复制代码
#include <stdio.h>

int main()
{
	int a = 100;
	int* pa = &a;
	*pa = 0;
	return 0;
}

上⾯代码中第7⾏就使⽤了解引⽤操作符,*pa的意思就是通过pa中存放的地址,找到指向的空间,pa其实就是a变量了;所以 pa=0,这个操作符是把a改成了0.

这⾥如果⽬的就是把a改成0的话,写成a = 0;不就完了,为啥⾮要使⽤指针呢?其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活,后期慢慢就能理解了。

2.3 指针变量的大小

前⾯的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么**⼀个地址就是32个bit位,需要4个字节才能存储** 。

如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节 的空间才可以。

同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。

c 复制代码
#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环境输出结果:

X64环境输出结果:

结论:

  • 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
  • 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
  • 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的

三、指针变量类型的意义

指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?

其实指针类型是有特殊意义的。

3.1 指针的解引用

对⽐,下⾯2段代码,主要在调试时观察内存的变化。

c 复制代码
//代码1
#include <stdio.h>
int main()
{
	int n = 0x11223344;
	int* pi = &n;
	*pi = 0;
	return 0;
}
c 复制代码
//代码2
#include <stdio.h>
int main()
{
	int n = 0x11223344;
	int* pc = (char*)&n;
	*pc = 0;
	return 0;
}

调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。

结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。

⽐如:char* 的指针解引⽤就只能访问⼀个字节,⽽int* 的指针的解引⽤就能访问四个字节。

3.2 指针±整数

先看⼀段代码,调试观察地址的变化。

c 复制代码
#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.3 void* 指针

在指针类型中有⼀种特殊的类型是void * 类型的,可以理解为**⽆具体类型的指针** (或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进⾏指针的±整数和解引⽤的运算。

举例:

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	return 0;
}

在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。⽽使⽤void 类型就不会有这样的问题。

使⽤void*类型的指针接收地址:

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	void* pa = &a;
	void* pc = &a;
	*pa = 10;
	*pc = 0;
	return 0;
}

VS编译代码的结果:

这⾥我们可以看到,void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。

那么void* 类型的指针到底有什么⽤呢?

⼀般void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。


四、const 修饰指针

4.1 const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。

但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

c 复制代码
#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就能做到了,虽然这样做是在打破语法规则。

c 复制代码
#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;
}

输出结果:

我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?

4.2 const修饰指针变量

⼀般来讲const修饰指针变量,可以放在的左边,也可以放在的右边,意义是不⼀样的。

c 复制代码
int* p;//没有const修饰?
int const* p;//const 放在*的左边做修饰
int* const p;//const 放在*的右边做修饰

我们看下⾯代码,来分析具体分析⼀下:

c 复制代码
#include <stdio.h>
//代码1 -测试⽆const修饰的情况
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}

//代码2 -测试const放在*的左边情况
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}

//代码3 -测试const放在*的右边情况
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; //ok?
	p = &m;  //ok?
}

//代码4 -测试*的左右两边都有const
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; //ok?
	p = &m;  //ok?
}

int main()
{
	//测试⽆const修饰的情况
    test1();
	//测试const放在*的左边情况
    test2();
	//测试const放在*的右边情况
    test3();
	//测试*的左右两边都有const
	test4();
	return 0;
}

结论:const修饰指针变量的时候

  • const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
  • const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

五、指针运算

指针的基本运算有三种,分别是:

  • 指针±整数
  • 指针-指针
  • 指针的关系运算

5.1 指针±整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

c 复制代码
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
c 复制代码
#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 指针-指针

c 复制代码
#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 指针的关系运算

c 复制代码
#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.1 野指针成因

6.1.1 指针未初始化

c 复制代码
#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	* p = 20;
	return 0;
}

6.1.2 指针越界访问

c 复制代码
#include <stdio.h>
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;
}

6.1.3 指针指向的空间释放

c 复制代码
#include <stdio.h>
int* test()
{
	int n = 100;
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

6.2 如何规避野指针

6.2.1 指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。NULL是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

c 复制代码
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

初始化如下:

c 复制代码
#include <stdio.h>
int main()
{
	int num = 10;
	int* p1 = &num;
	int* p2 = NULL;
	return 0;
}

6.2.2 小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。

不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。

c 复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤

	//...
	p = &arr[0];//重新让p获得地址

	if (p != NULL) //判断
    {
	    //...
	}
	return 0;
}

6.2.4 避免返回局部变量的地址

如造成野指针的第3个例⼦,不要返回局部变量的地址。


七、assert 断⾔

assert.h头⽂件定义了宏assert(),⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为"断⾔"。

c 复制代码
assert(p != NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。

assert()宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零),assert()不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

assert()的使⽤对程序员是⾮常友好的,使⽤assert()有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断⾔,就在#include <assert.h> 语句的前⾯,定义⼀个宏NDEBUG。

c 复制代码
 #define NDEBUG
 #include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的assert()语句。如果程序⼜出现问题,可以移除这条#define NDEBUG指令(或者把它注释掉),再次编译,这样就重新启⽤了assert()语句。

assert()的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。

⼀般我们可以在Debug中使⽤,在Release版本中选择禁⽤assert就⾏,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在Release版本不影响⽤⼾使⽤时程序的效率。


八、指针的使⽤和传址调⽤

8.1 strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中\0之前的字符的个数。

函数原型如下:

c 复制代码
size_t strlen ( const char * str );

**参数str接收⼀个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回⻓度。**如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0字符,计数器就+1,这样直到\0就停⽌。

参考代码如下:

c 复制代码
int my_strlen(const char * str)
 {
 int count = 0;
 assert(str);
 while(*str)
 {
 count++;
 str++;
 }
 return count;
 }
 int main()
 {
 int len = my_strlen("abcdef");
 printf("%d\n", len);
 return 0;
 }

8.2 传值调用和传址调用

学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?

例如:写⼀个函数,交换两个整型变量的值

⼀番思考后,我们可能写出这样的代码:

c 复制代码
#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a = % d b = % d\n", a, b);
	Swap1(a, b);
	printf("交换后:a = % d b = % d\n", a, b);
	return 0;
}

当我们运⾏代码,结果如下:

我们发现其实没产⽣交换的效果,这是为什么呢?

调试⼀下,试试呢?

我们发现在main函数内部,创建了a和b,a的地址是0x0067fa64,b的地址是0x0067fa58,在调⽤Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x0067f980,y的地址是0x0067f984,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这种叫传值调⽤

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

所以Swap1是失败的了。

那怎么办呢?

我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

c 复制代码
#include <stdio.h>
void Swap2(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);
	Swap2(&a, &b);
	printf("交换前:a = % d b = % d\n", a, b);
	printf("交换后:a = % d b = % d\n", a, b);
    return 0;
}

调试一下:

输出结果:

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。


总结

深刻理解指针与地址的关系。还有指针类型相关的内容,指针是怎么计算的等。知道assert断言的用法。区分传值和传址调用。

相关推荐
BoomHe5 小时前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农12 小时前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少12 小时前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker13 小时前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋13 小时前
Android 协程时代,Handler 应该退休了吗?
android
火柴就是我1 天前
让我们实现一个更好看的内部阴影按钮
android·flutter
RuoZoe1 天前
重塑WPF辉煌?基于DirectX 12的现代.NET UI框架Jalium
c语言
砖厂小工1 天前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心1 天前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心1 天前
Android 17 来了!新特性介绍与适配建议
android·前端