带你深入了解C语言指针(一)

目录

前言

一、内存和地址

1. 内存

2. 究竟该如何理解编址

二、指针变量和地址

1. 取地址操作符(&)

 2. 指针变量和解引用操作符(*)

2.1 指针变量

2.2 如何拆解指针类型 

 2.3 解引⽤操作符

3. 指针变量的大小

三、指针变量类型的意义

1. 指针的解引用

2. 指针+-整数 

3. void* 指针

​编辑

 四、const修饰指针

1. const修饰变量

 2. const修饰指针变量

 五、指针运算

1. 指针 +- 整数

2. 指针-指针 

3. 指针的关系运算

六、野指针 

1. 野指针成因

1.1 指针未初始化

1.2 指针越界访问 

1.3 指针指向的空间释放 

2. 如何规避野指针 

2.1 指针的初始化

2.2 小心指针越界

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

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

七、assert断言

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

1. strlen的模拟实验

2. 传值调⽤和传址调⽤

总结


前言

C语言是一种面向过程的计算机编程语言,C语言是比较偏底层的语言,为什么他比较偏底层,就是因为他的很多操作都是直接针对内存操作的。

这篇我们就来讲解C语言的一大特点,也是难点,指针和指针操作。

指针比较难以理解,但我会尽量会让大家都会理解,指针计划5~6篇结束,希望大家可以与我一起坚持下去;


一、内存和地址

1. 内存

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

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

⼀楼:101,102,103...
⼆楼:201,202,203....

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

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

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

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

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

可能有人还不清楚字节和比特的区别,这里我来简单提一下:

一个字节等于8比特(bit),⼀个⽐特位可以存储⼀个2进制的位1或者0。

bit - ⽐特位
byte - 字节
KB
MB
GB
TB
PB
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB

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

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

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

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

2. 究竟该如何理解编址

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

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

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

硬件编址也是如此 

⾸先,必须理解,计算机内是有很多的硬件单元,⽽硬件单元是要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。但是硬件与硬件之间是互相独⽴的,那么如何通信呢?答案很简单,⽤"线"连起来。⽽CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也⽤线连起来。

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

我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。

  • --------------------------0/1  00
  • --------------------------0/1  01
  • --------------------------0/1  10
  • --------------------------0/1  11

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

二、指针变量和地址

1. 取地址操作符(&)

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

int main()
{int a = 1;return 0;
}

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

那我们如何能得到a的地址呢?
这⾥就得学习⼀个操作符(&)-取地址操作符 

还是上面的那个例子,如果我们取a地址并打印,会得出什么?

int main()
{int a = 1;//&a -->& 取地址操作符printf("%p\n", &a);  //一般用%p用来取地址return 0;
}

我们可以调试看看: 

我们发现,打印出来的是a最小的那个地址;

我们不妨画个图理解一下:

按照我画图的例⼦,会打印处理:006FFD70,&a取出的是a所占4个字节中地址较⼩的字节的地
址。 

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

 2. 指针变量和解引用操作符(*)

2.1 指针变量

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

// p = &a;

但是我们定义一个变量要指定它的类型,那指针变量的类型是什么呢?答案是:int *,那我们就可以这样定义一个指针变量:

	int* p = &a;

2.2 如何拆解指针类型 

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

int a = 10;
int * pa = &a;

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

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

int main()
{char ch = 'w';char * pc = &ch;return 0;
}

 2.3 解引⽤操作符

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

int main()
{int a = 100;int* pa = &a;*pa = 0;printf("%d\n", a);return 0;
}

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

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

我们可以验证一下结果:

3. 指针变量的大小

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

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

 我们可以验证一下:

#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;
}

我们还可以试试32位环境(×86就是×32):

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

三、指针变量类型的意义

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

其实指针类型是有特殊意义的,我们接下来继续学习。

1. 指针的解引用

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

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

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

结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节

2. 指针+-整数 

我们来看看下面的代码:

#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个字节。
这就是指针变量的类型差异带来的变化。

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

3. void* 指针

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

举个例子:

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

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

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

int main()
{int a = 10;void* pa = &a;void* pc = &a;*pa = 10;*pc = 0;return 0;
}

 VS编译代码的结果:

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

那么 void* 类型的指针到底有什么⽤呢?
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,在《带你深⼊理解指针(4)》中我们会讲解。 

 四、const修饰指针

1. const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

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就能做到了,虽然这样做是在打破语法规则

int main()
{int m = 0;m = 20;  //m可以修改const int n = 0;printf("%d\n", n);int* p = &n;*p = 10;printf("%d\n", n);return 0;
}

输出结果:

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

 2. const修饰指针变量

我们看下⾯代码,来分析

const修饰*后面

int main()
{int a = 10;int* const p = &a;*p = 100;printf("%d\n", a);return 0;
}

我们看看是否可以通过指针变量,修改指针变量指向的内容;

通过结果我们知道const修饰指针变量的时候,放在*右边;const限制的是指针变量本身,指针变量不能再指向其他变量了,但是可以通过指针变量,修改指针变量的指向内容;

 const放*左边

int main()
{int a = 10;int const* p = &a;*p = 100;return 0;
}

这时候我们可以看到编译器报错: 

const修饰指针变量时,放在*左边,限制的是指针指向的内容,不能通过指针来修改;

但是可以修改指针变量本身的值(修改的指针变量的指向)

 我们可不可以两边都放呢:

int main()
{int a = 10;int n = 45;int const* const p = &a;p = &n;*p = 100;return 0;
}

这时候就将其限制死了:

 我们来总结一下:

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

即禁止:   *p = 10;
• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

即禁止:   p = &n;

• const如果放*两边,那即两种都禁止。

 五、指针运算

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

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

1. 指针 +- 整数

在上面我们已经提过了,这里我们给大家总结一下:

指针+n:

type* p:
p + 1 --->   跳过   1 * sizeof(type)字节p + n --->   跳过   n * sizeof(type)字节

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

int arr[10] = {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]);for (i = 0; i < sz; i++){printf("%d\n", arr[i]);}return 0;
}

 我们还可以利用指针:

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[1]);for (i = 0; i < sz; i++){printf("%d\n", *p);p++;}return 0;
}

 我们来验证一下:

2. 指针-指针 

我可以先说结果:

指针- 指针--->得到两个指针之间的元素个数;

指针+指针没有意义;

int main()
{int arr[10] = { 0 };printf("%d\n", &arr[9] - &arr[0]);return 0;
}

将数组的高地址减去低地址得出的是数组的元素个数?,我们来看看运行结果 :

为什么是9,不是10:

但是,指针减指针也不是随便的两个指针相减,有前提:两个指针指向同一块空间 。

int main()
{int arr[10] = { 0 };char ch[5] = { 0 };printf("%d\n", &ch[4] - &arr[5]);return 0;
}

 虽然有结果,但是我们的代码应该避免这样的代码出现,因为这种结果没有任何使用意义;

我们来用这个知识点实战一下,我们自己实现strlen函数;

int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}
int main()
{char arr[10] = "abcdef";printf("%d\n", my_strlen(arr)); //数组名arr是数组首元素的地址return 0;
}

运行结果:

3. 指针的关系运算

我们可以利用地址的关系运算来打印输出一个数组

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

 验证结果:

六、野指针 

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

1. 野指针成因

1.1 指针未初始化

我们开看看这两个代码:

//代码1int main()
{int a = 10;int* p = &a;*p = 20;return 0;
}//代码2int main()
{int* p;*p = 20;return 0
}

当然这样在VS2022中会报错:

1.2 指针越界访问 

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;
}

1.3 指针指向的空间释放 

我们来看看这样的一个代码,观察是否可以发现问题:

int* test()
{int a = 10;return &a;
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}

2. 如何规避野指针 

2.1 指针的初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.

NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。 

#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif

 我们一般在这种情况用NULL:

int main()
{int a = 10;int* p1 = &a;  //指针变量的定义与初始化int* p2 = NULL;  //如果我们现在要定义一个指针,但是现在不知道要将它指向哪?这时我们就可以将其指向NULLreturn 0;
}

2.2 小心指针越界

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

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

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

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。

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

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

七、assert断言

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

assert(p != NULL);

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

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

我们可以试试这个assert():

#include <stdio.h>
#include <assert.h>
int main()
{int* p = NULL;assert(p != NULL);return 0;
}

我们来看看运行结果:

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:

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

第一个好处已经在上面演示过了,我们来看看第二个好处:

如果我们现在想要代码中一个数大于3才可以继续运行,这时候我们可以使用assert():

#include <stdio.h>
#include <assert.h>int main()
{int n = 0;scanf("%d", &n);assert(n > 3);printf("%d\n", n);return 0;
}

 我们可以看到,只要输入大于3的数,程序就不会报错。

但是,我们如果调试完程序后,不再需要assert(). 我们只需要在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG:

#include <stdio.h>
#define NDEBUG
#include <assert.h>int main()
{int n = 0;scanf("%d", &n);assert(n > 3);printf("%d\n", n);return 0;
}

这时候,我们就算输入小于3的数也不会报错了

如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。 

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

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

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

1. strlen的模拟实验

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

函数原型如下:

size_t strlen ( const char * str );

 上其实对这点已经讲的很清楚了,我们再来给这个函数加点新东西:

之前我们的基本思路是:

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

那我们的mystrlen函数就可以这样写:

int mystrlen(char* str)
{int count = 0;while (*str != '\0'){count++;str++;}
}

这时候,我们需要用户传进去一个字符串的首地址,但是,考虑到有用户会粗心将NULL传进去,所以,我们利用assert()来进行限制,再防止有用户在函数里通过指针修改字符串,也为了让这个函数专一,我们还可以在其前加const来限制:

int mystrlen(const char* str)
{int count = 0;assert(str != NULL);while (*str != '\0'){count++;str++;}
}int main()
{char arr[] = "abcdef";int len = mystrlen(arr);printf("%d\n", len);return 0;
}

 验证一下结果是否为6:

其实大家如果了解strlen函数,他返回的不是int类型,而是一个叫size_t(无符号整型) 的类型,所以上面的代码还可以这样改:

size_t mystrlen(const char* str)
{size_t count = 0;assert(str != NULL);while (*str != '\0'){count++;str++;}return count;
}int main()
{char arr[] = "abcdef";size_t len = mystrlen(arr);printf("%d\n", len);return 0;
}

 好的代码是需要不断地打磨的,这就是我的打磨历程;

2. 传值调⽤和传址调⽤

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

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

很多人想到用函数这样写:

void Swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;printf("a=%d b=%d\n", a, b);Swap1(a, b);printf("a=%d b=%d\n", a, b);return 0;
}

行吗?哪有问题,我们先来运行一下看看结果换了没?

没换,why?

我们来调试试试:

我们调试到进入函数前,看看a,b的值:

 到这了,我们发现并没有什么异常,我们继续进入函数内部:

这时候我们发现,a,b的确传到了函数内部,但是他们的地址与x,y不一致,那他们在函数内部完成了交换,出函数有销毁了,并不能影响到a,b;这其实就是函数的传值调用;

这也就是前面函数所说的,形参的修改并不能影响到实参,此刻,你应该明白了;

C语言函数详解(中)【自定义函数】-CSDN博客文章浏览阅读1k次,点赞36次,收藏16次。了解了库函数,我们的关注度应该聚焦在⾃定义函数上,⾃定义函数其实更加重要,也能给程序员写代码更多的创造性。ret_type fun_name(形式参数)• ret_type 是函数返回类型• fun_name 是函数名• 括号中放的是形式参数• { }括起来的是函数体我们举一个实际的例子方便大家理解:我们可以把函数想象成⼩型的⼀个加⼯⼚,⼯⼚得输⼊原材料,经过⼯⼚加⼯才能⽣产出产品,那函数也是⼀样的,函数⼀般会输⼊⼀些值(可以是0个,也可以是多个),经过函数内的计算,得出结果。https://blog.csdn.net/2303_78558007/article/details/141438268?spm=1001.2014.3001.5501#:~:text=3.-,%E5%AE%9E%E5%8F%82%E5%92%8C%E5%BD%A2%E5%8F%82%E7%9A%84%E5%85%B3%E7%B3%BB,-%E8%99%BD%E7%84%B6%E6%88%91%E4%BB%AC%E6%8F%90%E5%88%B0

那怎么办? 这时候我们就要请出指针了:

我们将地址传给函数,通过解引用来“寻找”a ,b完成调换;

void Swap2(int* pa, int* pb)
{int tmp = 0;tmp = *pa;*pa = *pb;*pb = tmp;
}int main()
{int a = 10;int b = 20;printf("a=%d b=%d\n", a, b);Swap2(&a, &b);printf("a=%d b=%d\n", a, b);return 0;
}

  我们来看看是否完成交换:

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

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


总结

这篇文章比较长,但如果你可以耐心读到这,我相信你一定有收获,加油,少年,不仅是你,也是我,下期见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/52482.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript更改属性名称+增加字段+排序

JavaScript更改属性名称增加字段排序 背景 客户提供的接口里包含了一堆数据&#xff0c;其中分为多个模块&#xff0c;需要进行拆分&#xff0c;其中涉及到名称更改、字段增加、排序。处理过程 -需要的数据&#xff1a; data: {"四年级": [{ "class": &q…

LeetCode题练习与总结:矩形面积--223

一、题目描述 给你 二维 平面上两个 由直线构成且边与坐标轴平行/垂直 的矩形&#xff0c;请你计算并返回两个矩形覆盖的总面积。 每个矩形由其 左下 顶点和 右上 顶点坐标表示&#xff1a; 第一个矩形由其左下顶点 (ax1, ay1) 和右上顶点 (ax2, ay2) 定义。第二个矩形由其左…

jina-embeddings 的使用教程,怎么用它做embeddings和rerank的操作呢?

Jina-embeddings 使用教程 Jina-embeddings 是一个强大的工具&#xff0c;可以用来生成文本的嵌入向量&#xff08;embeddings&#xff09;&#xff0c;这些向量可用于相似度搜索、分类、重排序&#xff08;reranking&#xff09;等任务。在这个教程中&#xff0c;我将展示如何…

配置 MinGW 以及使用 g++ 编译 C++ 程序

如何在 Windows 上安装和配置 MinGW 以及使用 g 编译 C 程序 (C语言&#xff08;gcc&#xff09;类似 ) 在Windows环境下&#xff0c;使用C进行编程需要一个编译器&#xff0c;而MinGW (Minimalist GNU for Windows) 是一个常用的C/C编译器工具集。对于编程新手来说&#xff0c…

SOMEIP_ETS_101: SD_ClientServiceActivate_send_StopOfferService

测试目的&#xff1a; 验证当DUT在客户端模式下开始发送FindService消息时&#xff0c;测试器发送StopOfferService后&#xff0c;DUT能够理解其正在寻找的服务和实例ID不再可用&#xff0c;并停止为此服务和实例ID发送FindService消息。同时&#xff0c;DUT仍然可以发送Find-…

云曦2024秋季开学考

ezezssrf 第一关&#xff1a;md5弱比较 yunxi%5B%5D1&wlgf%5B%5D2 第二关&#xff1a; md5强比较 需要在bp中传参&#xff0c;在hackbar里不行 yunxiiM%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DC V%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_B…

【HarmonyOS NEXT】实现网络图片保存到手机相册

【问题描述】 给定一个网络图片的地址&#xff0c;实现将图片保存到手机相册 【API】 phAccessHelper.showAssetsCreationDialog【官方文档】 https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-photoaccesshelper-V5#showassetscreationdialog…

降维打击 华为赢麻了

文&#xff5c;琥珀食酒社 作者 | 积溪 真是赢麻了 华为估计都懵了 这辈子还能打这么富裕的仗&#xff1f; 其实在苹果和华为的发布会召开之前 我就知道华为肯定会赢 但我没想到 苹果会这么拉胯 华为这是妥妥的降维打击啊 就说这苹果iPhone 16吧 屏幕是变大了、颜色…

编译安装调试 scaLapack 和 openmpi 以及 lapack

编译安装调试 scaLapack /home/hipper/ex_scalapack/ mkdir ./lapack mkdir -p ./lapack/local/lib mkdir ./openmpi mkdir ./scalapack 1&#xff0c;编译安装 Lapack 下载代码&#xff1a; cd lapack wget https://github.com/Reference-LAPACK/lapack/archive/refs/tags/…

Python | Leetcode Python题解之第398题随机数索引

题目&#xff1a; 题解&#xff1a; class Solution:def __init__(self, nums: List[int]):self.nums numsdef pick(self, target: int) -> int:ans cnt 0for i, num in enumerate(self.nums):if num target:cnt 1 # 第 cnt 次遇到 targetif randrange(cnt) 0:ans …

逐行解析多头注意力机制

多头注意力机制是NLP算法岗常考的代码题&#xff0c;本篇文章将逐行梳理多头注意力机制的代码。 全部代码 import math import torch import torch.nn as nnclass MultiHeadAttention(nn.Module):def __init__(self, d_model, nums_head):super(MultiHeadAttention, self).__i…

QT 自定义组件 界面跳转

一、引用组件需要的类&#xff08;头文件&#xff09; 1、按钮类 QPushButton: 普通按钮; QToolButton: 工具按钮; QRadioButton: 单选按钮; QCheckBox: 复选按钮; QCommandLinkButton: 命令连接按钮; 2、布局类 QHBoxLayout水平 QVBoxLayout垂直 QGridLayout网格 QFormLayout…

存储芯片行业的封装类型

存储芯片行业的封装类型 存储芯片分类&#xff1a; 随机存储器&#xff08;RAM&#xff09;&#xff1a;这是易失性存储器&#xff0c;断电后存储的数据会丢失。它包括&#xff1a; 动态随机存储器&#xff08;DRAM&#xff09;&#xff1a;这是最常见的系统内存类型&#xf…

智能头盔语音识别声控芯片,AI离线语音识别ic方案,NRK3301

头盔是交通事故中保护电动车车主安全的最后一道屏障。为了增加骑行用户的安全保护&#xff0c;改善骑行用户的出行体验&#xff0c;让用户从被动使用头盔到主动佩戴头盔&#xff0c;头盔厂家与九芯电子合作&#xff0c;推出了语音智能头盔&#xff0c;它具备首家骑行专用的智能…

【网络安全】-xss跨站脚本攻击实战-xss-labs(1~10)

Level1: 检查页面源代码&#xff1a; function函数&#xff1a; (function(){try{let tn ;if(tn.includes(oem)){Object.defineProperty(document, referrer, {get: function(){return ;}});}else if(tn.includes(hao_pg)){if(!document.referrer.match(tn)){Object.definePro…

【python】python 安装和 pycharm 安装

1 python 安装 1.1 下载 下载地址&#xff1a;python 官网 1.2 安装 windows 安装为例。 双击.exe文件打开 安装界面 安装完成 1.3 检查安装是否成功 win/start 键r 键 运行窗口输入 cmd 回车 3 输入 python查看 显示版本信息&#xff0c;表示已经安装成功。 …

协议头,wireshark,http

目录 协议头 ip头 udp头 mac层 网络工具 telnet wireshark Http 一、HTTP 协议介绍 二、HTTP 协议的工作过程 三、使用抓包工具抓取报文 四、获取到http请求报文&#xff1a; 五、http请求&#xff08;request&#xff09; &#xff08;一&#xff09;、认识URL 项…

如果 Android 手机出现数据丢失,如何在Android上恢复丢失的数据

当您的 Android 手机发生数据丢失时&#xff0c;您可能需要检索丢失的文件。为了帮助您完成此过程&#xff0c;以下是执行 Android 数据恢复的一些有效方法&#xff1a; 如何在Android上检索数据 如果您的 Android 手机出现数据丢失&#xff0c;您可能需要检索丢失的文件。为了…

OpenWRT有三个地方设置DNS,究竟设置哪个地方会更好?

前言 刚上手OpenWRT软路由系统的小伙伴或许都会有这样的疑问&#xff1a;OpenWRT这个系统有三个地方是设置DNS的&#xff0c;究竟设置哪一个才是正确的&#xff1f; 这个还得从实际应用说起。 一般来说&#xff0c;咱们在使用路由器的时候&#xff0c;DNS都是默认运营商的DN…

前端框架大观:探索现代Web开发的基石

目录 引言 一、前端框架概述 二、主流前端框架介绍 2.1 React 2.1.1 简介 2.1.2 特点 2.1.3 代码示例 2.2 Vue.js 2.2.1 简介 2.2.2 特点 2.2.3 代码示例 2.3 Angular 2.3.1 简介 2.3.2 特点 2.3.3 代码示例 三、其他前端框架与库 四、前端框架的选择 五、结…