指针的奥秘:深入探索内存的秘密

前言

在计算机编程的广阔天地中,指针作为一种独特的数据类型,它不仅是C语言的核心,也是理解计算机内存管理的基石。指针的概念虽然强大,但对于初学者来说,它常常是学习过程中的一个难点。本文旨在揭开指针的神秘面纱,带你一探究竟,从基础概念到高级应用,全面解析指针的奥秘。

 指针:连接现实与抽象的桥梁

在现实世界中,地址是定位位置的一种方式。同样,在计算机的虚拟世界里,内存地址是定位数据的一种手段。指针,作为内存地址的直接表达,它允许程序员直接操作内存中的数据。这种能力是双刃剑,它既提供了无与伦比的灵活性和控制力,也带来了额外的复杂性和潜在的错误风险。

探索内存的深层结构

要深入理解指针,首先需要了解计算机内存的工作原理。内存被组织成一系列的单元,每个单元都有一个唯一的地址。指针存储的就是这些地址,通过指针,我们可以访问、读取和修改这些内存单元中的数据。这种直接与内存交互的能力,是理解计算机系统的关键。

 指针的多面性

指针不仅仅是一个简单的地址,它还包含了类型信息,这决定了指针可以访问的数据的大小和排列方式。不同类型的指针,如int*char*void*等,它们在内存中的操作方式各不相同。了解这些差异,对于编写高效、安全代码至关重要。

 指针与数组:内存连续性的体现

数组是内存连续性的一个完美体现,而指针则是操作数组的有力工具。通过指针,我们可以轻松遍历数组,访问任意元素。这种能力在处理大型数据集时显得尤为重要,它使得算法的实现更加简洁和高效。

 指针的高级应用:函数和结构体

指针的应用不仅限于基本数据类型,它还可以指向函数、结构体等复杂数据结构。这种高级应用使得程序的模块化和功能扩展成为可能,同时也带来了编程的灵活性和强大能力。

 安全与风险:指针的双刃剑

指针的强大能力伴随着风险。野指针、内存泄漏、越界访问等问题都与指针使用不当有关。因此,了解如何安全地使用指针,避免潜在的内存问题,对于每个程序员来说都是必备的技能。

 我们将一起探索指针的这些奥秘,从基础的内存和地址概念,到指针的高级应用,我们将一一揭开。希望通过几篇文章,能够对指针有一个全面而深入的理解,在编程的道路上更加自信和从容。让我们一起启程,深入探索内存的秘密,揭开指针的奥秘。

1  内存和地址

1.1 计算机中的房间编号系统

在深入探讨指针之前,让我们先通过一个生活中的案例来理解内存和地址的概念。这个案例将帮助我们建立一个直观的认识,从而更好地理解计算机是如何管理内存的。

1.1.1 宿舍楼的比喻:房间编号系统

想象一下,如果你被放置在一栋有100个房间的宿舍楼中,而这些房间并没有编号。这时,如果一个朋友来找你玩,他需要逐个房间敲门,直到找到你为止。这种方法显然效率极低。为了提高效率,我们给每个房间分配一个唯一的编号,比如一楼的房间编号为101、102、103,二楼的房间编号为201、202、203,以此类推。有了这些编号,你的朋友只需根据房间号直接找到你,大大节省了时间。

1.1.2 计算机内存的管理

在计算机中,内存的管理与宿舍楼的房间编号系统有着异曲同工之妙。CPU在处理数据时,需要从内存中读取数据,处理完毕后再将数据写回内存。我们购买电脑时,会看到内存的大小如8GB、16GB、32GB等,这些内存空间需要被高效地管理。

为了实现这一点,计算机将内存划分为一个个内存单元,每个单元的大小为1个字节。在计算机中,数据的大小通常以字节为单位,而1个字节等于8个比特位。比特位是计算机中最小的数据单位,可以存储一个二进制位,即1或0。

1.1.3 计算机中的单位换算

在计算机科学中,我们经常会遇到各种数据单位,它们之间的换算关系如下:

- 1 byte(字节)= 8 bit(比特位)
- 1 KB(千字节)= 1024 byte
- 1 MB(兆字节)= 1024 KB
- 1 GB(吉字节)= 1024 MB
- 1 TB(太字节)= 1024 GB
- 1 PB(拍字节)= 1024 TB 

这些单位帮助我们量化数据的大小,同时也反映了计算机内存的层次结构。

1.1.4  内存单元的编号:地址

每个内存单元都有一个唯一的编号,这个编号就像是宿舍房间的门牌号。在计算机中,我们称这个编号为地址。地址是内存单元的标识符,它允许CPU快速定位到特定的内存空间。

1.1.5  指针:C语言中的地址表示

在C语言中,我们用“指针”来表示内存单元的地址。指针是存储内存地址的变量,它指向内存中的某个位置。通过指针,我们可以访问、读取和修改该位置的数据。因此,我们可以将内存单元的编号、地址和指针视为同一概念:

内存单元的编号=地址=指针

理解了这些基本概念后,我们就可以开始探索指针的更多奥秘,包括它们在编程中的应用、指针运算以及如何通过指针来管理复杂的数据结构。指针不仅是C语言的核心,也是连接程序与硬件的桥梁,理解指针就是理解计算机如何工作的关键一步。

 1.2  理解内存编址:计算机的地址系统

在计算机的世界里,内存是一个巨大的数据存储空间,它由无数个字节组成。要精确地访问这些字节中的任何一个,我们需要一个系统来标识每个字节的位置,这就是内存编址的概念。

1.2.1  为什么需要编址?

想象一下,如果一栋宿舍楼有成千上万个房间,但没有房间号,那么当你的朋友来访时,他可能需要逐个敲门来找到你,这显然非常低效。同样,CPU需要能够快速准确地找到内存中的特定数据。因此,就像宿舍楼里的每个房间都有唯一的编号一样,内存中的每个字节也需要一个唯一的地址。

1.2.2  硬件层面的编址设计

在计算机中,编址并不是简单地记录每个字节的地址,而是通过硬件设计来实现的。这就像乐器上的琴弦,虽然没有标记“都瑞咪发嗦啦”,但演奏者知道每个琴弦的位置,因为这是乐器设计的一部分,是一种共识。

1.2.3  硬件单元的协同工作

计算机内部有许多硬件单元,它们需要协同工作,进行数据传递。这些硬件单元之间是独立的,它们通过“线”连接起来,以便通信。CPU和内存之间也有大量的数据交互,它们通过地址总线连接。

1.2.4  地址总线的作用

在32位计算机中,有32根地址总线,每根线可以表示两种状态(0或1),即电脉冲的存在与否。这样,一根线可以表示两种含义,两根线可以表示四种含义,以此类推。32根地址线可以表示 (2^{32})种不同的状态,每一种状态都对应一个唯一的地址。

当地址信息通过地址总线传递给内存时,内存就能定位到对应的数据,然后通过数据总线将数据传输到CPU的寄存器中。

1.2.5 总结

内存编址是计算机能够高效运行的基础。它使得CPU能够快速准确地访问内存中的任何数据,就像我们能够通过房间号快速找到宿舍楼中的朋友一样。这种编址系统是硬件设计的一部分,是计算机内部通信和数据传递的基础。通过理解内存编址,我们可以更好地理解计算机的工作原理,以及如何有效地与硬件交互。
 

2. 指针变量和地址

2.1  掌握取地址操作符(&)

在C语言的世界里,变量不仅仅是存储数据的容器,它们还拥有一个特殊的属性——地址。每个变量在内存中都有自己的位置,这个位置可以通过取地址操作符(&)来获取。

2.1.1  变量和内存空间

当我们在C语言中创建一个变量时,我们实际上是在向内存申请空间。例如,当我们声明一个整型变量`a`并给它赋值为10时,我们实际上是在内存中申请了4个字节的空间来存储这个整数。这4个字节在内存中是连续的,并且每个字节都有自己的地址。

2.1.2  取地址操作符(&)

要获取变量的地址,我们使用取地址操作符(&)。这个操作符非常简单,只需要在变量名前加上`&`即可。例如,`&a`就会给出变量`a`的地址。

让我们来看一个简单的例子
 

#include <stdio.h>
int main() {int a = 10;printf("%p\n", &a); // 打印变量a的地址return 0;
}

 

在这个例子中,&a取出的是变量a所占4个字节中地址最小的那个字节的地址。在大多数情况下,这个地址就是变量a的起始地址。

2.1.3  为什么需要地址?

即使我们知道了变量占用的第一个字节的地址,我们也可以顺藤摸瓜地访问到整个变量的数据。这是因为变量在内存中是连续存储的,所以只要我们知道了起始地址,就可以通过偏移量来访问后续的字节。

2.1.4  总结

通过取地址操作符(&),我们可以获取变量在内存中的地址。这个地址是变量与内存交互的桥梁,也是我们使用指针进行高级操作的基础。理解了变量的地址,我们就能够更深入地理解程序是如何在内存中管理和操作数据的。这种能力是编写高效、灵活的C语言程序的关键。

2.2  指针变量和解引用操作符(*):深入指针的内部

在计算机编程中,指针变量是一种特殊的变量,它存储的是其他变量的内存地址。这使得指针变量成为操作内存的强大工具。让我们进一步探索指针变量的概念和如何使用它们。

 2.2.1  指针变量的基本概念

当我们使用取地址操作符(&)获取一个变量的地址时,我们得到的是一个数值,比如`0x006FFD70`。这个数值代表的是内存中的一个具体位置。有时候,我们需要将这个地址保存起来,以便将来使用。这时,我们就需要一个指针变量来存储这个地址。

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

在上面的例子中,pa是一个指针变量,它的类型是int*,表示它存储的是整型变量的地址。pa指向的内存地址正好是变量a的地址。

 2.2.2  理解指针的类型

指针变量的类型非常重要,因为它决定了指针可以指向的数据类型,以及如何通过指针来访问和操作数据。当我们声明一个指针变量时,类型声明告诉编译器指针应该指向什么类型的数据。

例如:
int a = 10;
int* pa = &a; // pa是指向整型数据的指针

在这里,pa左边的int*表示pa是一个指针变量,而*符号前的int表示pa指向的是整型(int)数据。这意味着,当我们通过pa来访问数据时,编译器会知道应该读取一个整型的数据。

2.2.3  指针类型的应用

如果我们有一个char类型的变量ch,我们想要获取它的地址并存储在一个指针变量中,我们应该使用什么样的指针类型呢?
 

char ch = 'w';
char* pc = &ch; // pc是指向字符型数据的指针

在这个例子中,pc被声明为char*类型,这意味着pc是一个指向字符型数据的指针。这样,我们就可以通过pc来访问和操作ch变量中的字符数据。

2.2.4  解引⽤操作符
我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
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.2.5  总结

指针变量和它们的类型是C语言中非常强大的特性。它们允许我们直接操作内存地址,提供了一种灵活的方式来访问和修改数据。理解指针变量的类型和如何使用它们是编写高效C程序的关键。通过指针,我们可以深入到内存的层面,实现更复杂的数据操作和程序控制。

 2.3   指针变量的大小:探秘32位与64位系统的差异

在深入理解指针变量之前,我们需要知道它们在内存中所占的空间大小。这个大小并不是随意的,而是由我们使用的计算机系统的架构决定的。

2.3.1  指针变量大小的决定因素

在32位系统中,CPU有32根地址总线,每根总线可以表示一个0或1的二进制位。这意味着,32位系统可以生成一个32位的地址,这个地址由2的32次方个不同的状态组成,足以覆盖4GB的内存空间。因此,一个地址需要4个字节来存储。

同理,在64位系统中,有64根地址总线,可以生成一个64位的地址,这个地址由2的64次方个不同的状态组成,远远超过当前任何实际使用的内存大小。这样的地址需要8个字节来存储。

2.3.2   指针变量大小的实际应用

指针变量是用来存储地址的,所以它们的大小必须足以容纳一个地址。在C语言中,我们可以通过sizeof`操作符来查看不同指针变量的大小。

#include <stdio.h>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位平台上运行上述程序,你会发现所有的指针变量大小都是4个字节。而在64位平台上,它们的大小会是8个字节。

2.3.3  结论

 在32位平台上,由于地址是32位(4个字节),所以指针变量的大小也是4个字节。
 在64位平台上,地址是64位(8个字节),指针变量的大小相应地是8个字节。
 值得注意的是,指针变量的大小与其能够指向的数据类型无关。无论是指向char、short、int  还是double,只要是指针类型的变量,在相同的平台下,它们的大小都是相同的。

3. 指针变量类型的重要性

尽管指针变量的大小与其类型无关,但指针的类型对于它们如何操作内存至关重要。不同类型的指针允许我们以不同的方式访问和处理数据。

3.1 指针的解引用

通过比较以下两段代码,我们可以在调试时观察内存的变化,以理解指针类型的重要性。

//代码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*类型的指针在解引用时可以访问四个字节。这种类型的区分确保了我们能够以正确的大小和格式访问内存中的数据,从而避免数据损坏和未定义行为。

 3.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.3  void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
例子
#include <stdio.h>
int main()
{int a = 10;int* pa = &a;char* pc = &a;return 0;
}
在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告,是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。
使⽤void*类型的指针接收地址:

#include <stdio.h>
int main()
{int a = 10;void* pa = &a;void* pc = &a;*pa = 10;*pc = 0;return 0;
}
这⾥我们可以看到, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
那么 void* 类型的指针到底有什么⽤呢?
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。

 4  const修饰指针

4.1   const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
#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就能做到了,虽然这样做是在打破语法规则。

#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修饰指针变量

我们看下⾯代码,来分析
#include <stdio.h>
//代码1
void test1()
{int n = 10;int m = 20;int* p = &n;*p = 20;//ok?p = &m; //ok?
}
void test2()
{//代码2int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}
void test3()
{int n = 10;int m = 20;int* const p = &n;*p = 20; //ok?p = &m; //ok?
}
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();//测试*的左右两边都有consttest4();return 0;
}
结论:const修饰指针变量的时候
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

5  指针运算

指针的基本运算有三种,分别是:
指针+- 整数
指针-指针
指针的关系运算

5.1   指针+- 整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
#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   指针-指针

//指针-指针
#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  指针的关系运算

//指针的关系运算
#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]);while(p<arr+sz) //指针的⼤⼩⽐较{printf("%d ", *p);p++;}return 0;
}

6  野指针

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

6.1   野指针成因

  (1)指针未初始化
#include <stdio.h>
int main()
{ int *p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;
}
(2)   指针越界访问
#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;
}

(3) 指针指向的空间释放

#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也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif

6.2.2  小心指针越界

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

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

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。
int main()
{int arr[10] = {1,2,3,4,5,67,7,8,9,10};int *p = &arr[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个例⼦,不要返回局部变量的地址。

7.   assert断言

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p!=NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和
出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问
题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移
除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert()
句。
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开
发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,
Release 版本不影响⽤⼾使⽤时程序的效率。

8  指针的使⽤和传址调⽤

8.1 strlen的模拟实现

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

函数原型如下:

 size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。
参考代码如下:
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   传值调⽤和传址调⽤

例如:写⼀个函数,交换两个整型变量的值
⼀番思考后,我们可能写出这样的代码:
#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的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤
Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这种叫传值调⽤。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实
参。
所以Swap是失败的了。
那怎么办呢?
我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

#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); 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/diannao/61921.shtml

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

相关文章

理解clickhouse 里的分区和分片键区别

文章目录 分片分区两分片&#xff0c;0副本的cluster 分片 CREATE TABLE logs_distributed AS logs_local ENGINE Distributed(cluster_name, -- 集群名称database_name, -- 数据库名称logs_local, -- 本地表名cityHash64(user_id) -- 分片键&#xf…

shell脚本(二)

声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&#…

多模态大型语言模型(MLLM)综述

目录 多模态大语言模型的基础 长短期网络结构(LSTM) 自注意力机制 基于Transformer架构的自然语言处理模型 多模态嵌入的关键步骤 TF-IDF TF-IDF的概念 TF-IDF的计算公式 TF-IDF的主要思路 TF-IDF的案例 训练和微调多模态大语言模型(MLLM) 对比学习 (CLIP, ALIG…

《智慧教育实时数据分析推荐项目》详细分析

一、项目介绍 1、背景介绍 在互联网、移动互联网的带动下&#xff0c;教育逐渐从线下走向线上&#xff0c;在线教育近几年一直处于行业的风口浪尖&#xff0c;那随着基础设施的不断完善&#xff0c;用户需求也发生不少变化&#xff0c;因此传统教育机构、新兴互联网企业都在探…

AI+云环境开发上线项目全流程(sealos)

AI云环境开发上线项目全流程 现在是AI技术爆炸&#x1f4a5;的时代&#xff0c;我们作为开发自然需要跟上时代的潮流&#xff0c;今天就跟大家介绍一款云开发环境&#xff0c;并且搭配AI实现一行代码不用写&#xff0c;直接上线一个完整的项目&#xff08;包含前后端&#xff0…

【C++11】可变参数模板/新的类功能/lambda/包装器--C++

文章目录 一、可变参数模板1、基本语法及原理2、包扩展3、empalce系列接口 二、新的类功能1、默认的移动构造和移动赋值2、成员变量声明时给缺省值3、defult和delete4、final与override 三、STL中一些变化四、lambda1、lambda表达式语法2、捕捉列表3、lambda的应用4、lambda的原…

STM32C011开发(1)----开发板测试

STM32C011开发----1.开发板测试 概述硬件准备视频教学样品申请源码下载参考程序生成STM32CUBEMX串口配置LED配置堆栈设置串口重定向主循环演示 概述 STM32C011F4P6-TSSOP20 评估套件可以使用户能够无缝评估 STM32C0 系列TSSOP20 封装的微控制器功能&#xff0c;基于 ARM Corte…

算法编程题-寻找最近的回文数

算法编程题-寻找最近的回文数 原题描述思路简述代码实现复杂度分析参考 摘要&#xff1a;本文将对LeetCode 原题 564 寻找最近的回文数进行讲解&#xff0c;并且给出golang语言的实现&#xff0c;该实现通过了所有测试用例且执行用时超过100%的提交&#xff0c;最后给出相关的复…

提升数据分析效率:Excel Power Query和Power Pivot的妙用

在日常工作中&#xff0c;微软的Excel Power Query和Power Pivot是提升数据处理和分析效率的利器。他们的特点也各不相同&#xff0c;Power Query侧重数据的高效导入与清洗&#xff0c;Power Pivot更测试数据建模与复杂计算。下面将介绍它们各自的功能&#xff0c;并提供应用案…

认识RabbitMq和RabbitMq的使用

1 认识RabbitMq RabbitMQ是⼀个消息中间件&#xff0c;也是⼀个生产者消费者模型&#xff0c;它负责接收&#xff0c;存储并转发消息。 2.1 Producer和Consumer Producer&#xff1a;生产者&#xff0c;是RabbitMQServer的客户端&#xff0c;向RabbitMQ发送消息 Consumer&…

代码纪元——源神重塑无序

简介 源神&#xff0c;真名为张晨斌&#xff0c;原为代码宇宙创世四神之一。代码宇宙在创造之初时空无一物&#xff0c;只有复杂且繁琐的底层代码&#xff0c;智慧神灵每日都困在诸如脚本等复杂的底层框架之中&#xff0c;源神面对这种局面非常不满意&#xff0c;于是源神通过大…

LVGL加载器,led和列表学习(基于正点原子)

加载器部件&#xff08;lv_spinner&#xff09; 加载器部件常用于提示当前任务正在加载。 加载器部件组成部分&#xff1a; 主体(LV_PART_MAIN) 指示器(LV_PART_INDICATOR) 手柄(LV_PART_KNOB) 知识点1&#xff1a;创建加载器部件 lv_obj_t *spinner lv_spinner_creat…

内存不足引发C++程序闪退崩溃问题的分析与总结

目录 1、内存不足一般出现在32位程序中 2、内存不足时会导致malloc或new申请内存失败 2.1、malloc申请内存失败&#xff0c;返回NULL 2.2、new申请内存失败&#xff0c;抛出异常 3、内存不足项目实战案例中相关细节与要点说明 3.1、内存不足导致malloc申请内存失败&#…

docker搭建私有的仓库

docker搭建私有仓库 一、为什么要搭建私有的仓库&#xff1f; 因为在国内&#xff0c;访问&#xff1a;https://hub.docker.com/ 会出现无法访问页面。。。。&#xff08;已经使用了魔法&#xff09; 当然现在也有一些国内的镜像管理网站&#xff0c;比如网易云镜像服务、Dao…

【Linux】安装cuda

一、安装nvidia驱动 # 添加nvidia驱动ppa库 sudo add-apt-repository ppa:graphics-drivers/ppa sudo apt update# 查找推荐版本 sudo ubuntu-drivers devices# 安装推荐版本 sudo apt install nvidia-driver-560# 检验nvidia驱动是否安装 nvidia-smi 二、安装cudatoolkit&…

深度学习图像视觉 RKNN Toolkit2 部署 RK3588S边缘端 过程全记录

深度学习图像视觉 RKNN Toolkit2 部署 RK3588S边缘端 过程全记录 认识RKNN Toolkit2 工程文件学习路线&#xff1a; Anaconda Miniconda安装.condarc 文件配置镜像源自定义conda虚拟环境路径创建Conda虚拟环境 本地训练环境本地转换环境安装 RKNN-Toolkit2&#xff1a;添加 lin…

论文模型设置与实验数据:scBERT

Yang, F., Wang, W., Wang, F. et al. scBERT as a large-scale pretrained deep language model for cell type annotation of single-cell RNA-seq data. Nat Mach Intell 4, 852–866 (2022). https://doi.org/10.1038/s42256-022-00534-z 论文地址&#xff1a;scBERT as a…

Jenkins的环境部署

day22 回顾 Jenkins 简介 官网Jenkins Jenkins Build great things at any scale The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project. 用来构建一切 其实就是用Java写的一个项目…

怎么编译OpenWrt镜像?-基于Widora开发板

1.准备相应的环境&#xff0c;我使用的环境是VMware16ubuntu20.04&#xff0c;如图1所示安装编译所需的依赖包&#xff1b; sudo apt-get install build-essential asciidoc binutils bzip2 gawk gettext git libncurses5-dev libz-dev patch python3 python2.7 unzip zlib1g-…

Python 获取微博用户信息及作品(完整版)

在当今的社交媒体时代&#xff0c;微博作为一个热门的社交平台&#xff0c;蕴含着海量的用户信息和丰富多样的内容。今天&#xff0c;我将带大家深入了解一段 Python 代码&#xff0c;它能够帮助我们获取微博用户的基本信息以及下载其微博中的相关素材&#xff0c;比如图片等。…