[C语言]深入理解指针(1)------这么详细,这么通俗易懂,还不速速码住!!!

  • 首先,指针在C语言中是非常重要的,但是指针也是对我们来说是比较难的.这篇博客对指针的讲解我会尽量做到通俗易懂,让大家都能看懂,对指针能够有一定的了解. 所以这篇博客对无论时对初学者还是复习指针的同学都是很友好的.
  • 大家可以按顺序看,也可以根据目录找到自己所需要的内容去看.
  • 有关指针内容的博客我会持续更新五篇,觉得内容还不错的小伙伴们可以持续关注一下哦!

请添加图片描述

1. 内存和地址

1.1 内存

  • 什么是内存呢?
    内存是计算机系统中用于存储数据和程序的硬件组件.
  • 我们假设这个长方形是我们的内存:
    在这里插入图片描述
  • 内存是由一系列连续的存储单元组成,每个单元都有一个唯一的地址,称为内存地址。这些地址用于定位和访问特定的内存位置。
    在这里插入图片描述
  • C语言的变量都是放在内存中的,当你声明一个变量时,编译器会在内存中为该变量分配一定的存储空间, 这个存储空间的大小(占有的字节)取决于变量的类型(例如,int、char、float等)。变量的值就存储在这个分配的内存空间中,而内存中的每个字节都有一个地址的编号.其中第一个字节在内存中的地址就称为变量的地址.
    在这里插入图片描述

1.2 究竟该如何理解编址

  • 编址:存储器是由一个个存储单元构成的,为了对存储器进行有效的管理,就需要对各个存储单元编上号,即给每个单元赋予一个地址码,这叫编址。经编址后,存储器在逻辑上便形成一个线性地址空间。
  • 画的很丑,大家见谅一下,实在是在这方面没什么天赋,也不愿努力。请添加图片描述

在这里插入图片描述

  • 地址总线是用于传输要访问的内存地址。它确定了CPU可以寻址的范围,我们可以这样理解,在32位机器有32根地址线,在传输要访问的内存地址时,每根地址线可能发出两种信号(0或1),我们有32根地址线,可以在一次操作中传输32个bit位(4个字节)的地址。同样地,64位的可以传输64个比特位(8个字节)的地址。

2. 指针变量和地址

2.1 取地址操作符(&)

  • 在C语言中,取地址操作符(&)用于获取变量的内存地址。这个操作符放在变量名前面,返回该变量在内存中的地址。这个地址通常是一个指向该变量类型的指针。
  • 下面我们利用代码来理解一下这个取地址操作符,如图:
  • 这里我定义了一个变量a,并将其初始化为0,我们打印一下(&a),
    从这个图中,可以很直观的看到(&a)就是一个地址.

在这里插入图片描述

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

2.2.1 指针变量

首先,我们要明确两点.

    1. 指针变量是什么?其实指针变量就是存储地址的变量.
      在这里插入图片描述
  • 2.指针是什么? 指针就是地址.
    在这里插入图片描述
    从这里我们可以直观的了解到,指针就是地址.

2.2.2 如何拆解指针类型

公主们,王子们,请看图:
在这里插入图片描述

  • 这里我们定义了一个整型变量a,并将其初始化为10,然后我们定义了一个指向整型变量的指针变量pa,它指向a的地址.
  • 当我们有了整型变量a在内存中的地址,那么我们就可以很顺理成章地找到它,我们可以通过地址来访问整型变量a(间接访问).我们也可以通过这种方式来对整型变量a的值进行修改.
    在这里插入图片描述

2.2.3 解引用操作符(*)

  • 在C语言中,解引用操作符是*(星号)。它用于获取指针所指向的值。解引用操作符可以与指针变量一起使用,以访问指针所指向的内存位置中存储的实际值。(*pa)在这里就叫做解引用操作.
    在这里插入图片描述
  • 在这里我们是定义了一个整型变量a,并且将其初始化为10,然后我们定义了一个指向整型变量的指针pa,并将其 指向整型变量a的地址.通过打印&a、pa和pa的值,我们可以观察到它们之间的关系。其中,a的值为10,pa的值为a的地址,而pa的值为指针pa所指向的值,即a的值10。

2.3 指针变量的大小

  • 我们知道,在定义一个变量时,我们也会定义这个变量的类型(short int long char double等),不同类型的变量所占的字节数不同。
    我们在定义一个指针变量时也会定义指针变量的类型,那么指针变量也会不会像上述的一样,不同的类型所占的字节数不同呢?

  • 接下来让我们用代码来感受一下。首先看一下在x64的平台下是怎么样的?

在这里插入图片描述
接下来再看再x86平台下的情况:
在这里插入图片描述

  • 我们可以很直观的看到不管定义的指针变量是什么类型的,只要是在相同的平台下,它所占的字节数都是相同的。即指针变量的大小与类型是无关的,只与所处的平台有关,这是为什么呢?
  • 这里就和我们前面所说的编址有关系了。在x64平台下有64根地址线,它要传输64个bit位,需要8个字节才能存储。同理,在x86平台下有32根地址线,它要传输32个bit位,需要4个字节来存储.

3. 指针变量类型的意义

  • 看到这里,大家可能会疑惑,既然不管是什么类型在相同平台下所占的字节数都是相同的,我们为什么还要有不同的指针变量类型呢?直接定义一个统一的指针类型不就行了吗?不要着急,我们后面会给大家解惑的。

3.1 指针的解引用

  • 接下来,我们通过对两个代码调试来观察一下不同:
    第一个代码:
  • 首先说明,我们为了方便后续的观察和对比,这里定义的整型变量a的值是一个十六进制的数字。
    在这里插入图片描述
    在这里插入图片描述

第二个代码:
在这里插入图片描述
在这里插入图片描述

  • 这两个代码不同的就是我们在定义指针变量时,所定义的指针变量的类型不同,当我们定义指针变量为int类型时,我们通过指针间接访问a时,我们修改a的值时,可以改变4个字节。
  • 而当我们定义指针变量为char*类型时,我们通过指针间接访问a时,我们修改a的值时,只能修改1个字节。
  • 所以虽然说在相同的平台下,任何类型的指针变量所占的字节数都是一样的,但是不同类型的指针变量在访问内存空间时,权限不一样。

3.2 指针±整数

这个问题我们来用代码感受一下:
在这里插入图片描述

  • 从这段代码和代码运行的结果我们可以直观地看到,(char*)类型的指针,+1一次跳过的是1个字节,(int*)类型的指针,+1一次跳过的是4个字节。
  • 在C语言中,指针加减整数表示指针的移动。具体来说,当你给一个指针加上一个整数,指针会向前移动(指向更高的内存地址);当你给一个指针减去一个整数,指针会向后移动(指向更低的内存地址)。
  • 这种移动的大小取决于指针所指向的数据类型的大小。例如,如果你有一个指向int的指针,并且在一个32位系统上,int通常是4字节(32位),那么当你给这个指针加1时,它会向前移动4个字节。同样地,如果你有一个指向char的指针,并且char通常是1字节,那么给这个指针加1会使它向前移动1个字节。

3.3 void* 指针

  • 在C语言中,void* 类型的指针是一个特殊的指针类型,它可以指向任何类型的数据,但在解引用之前,必须将其转换为具体的类型。由于 void* 指针不携带类型信息,它不能直接被解引用,因为它不知道应该解释多少个字节的数据作为它所指向的类型。
  • void* 类型指针可以作为函数参数传递,这样可以实现对任何类型数据的操作。
    其实在我们前面的有些代码是有一点错误的:
    在这里插入图片描述
    但是如果我们用(void*)来接收就不会出现这种情况,如图:
    在这里插入图片描述
    但是,当我们用(void*)来进行进行解引用的时候编译器会报错,所以我们在使用前应该将其转化成具体的类型。
    在这里插入图片描述

4. const 修饰指针

  • 大家知道,指针其实就是地址,当我们拿到这个变量的地址时,我们就可以通过间接访问来对其值进行修改,然而有些时候我们是希望只读不改的 ,这样也增强了安全性,这个时候我们就可以使用const来对指针变量进行修饰。接下来我们对这种情况来进行详细的分析。

4.1 const 修饰变量

首先我们先来回忆一下const修饰变量,上代码:
在这里插入图片描述
这样大家就应该可以清晰明了的感受到const的作用了。
那么是不是这样a 的值就没有办法被修改了呢?答案是不!请看下一节:

4.2 const 修饰指针变量

在这里插入图片描述

  • 我们还可以通过指针来间接访问a,并对其成功地修改。那么我们这个const还有什么用呢?我们加const的目的就是不想让a的值改变。那么怎样才能最终达到这个目的呢?
  • 这个时候我们就想到用const修饰指针会有什么情况呢?能不能达到这个目的呢?
/*
const修饰指针有两种方式:
1.放在*的前面
const int* p = &a;

2.放在*的后面
int* const p = &a;

3.在*前后都放上const
const int* const p = &a;

那么这三种方式又有什么不同呢?
*/




//1.放在*前
int main()
{
	int a = 10;
	int b = 20;
	const int* p = &a;
	*p = 20;//err
	p = &b;//ok
	return 0;
}
/*
即我们这个时候不能通过指针来修改变量a的值了。
但是我们可以将这个指针指向其他变量的地址。
总结来说,就是当const放在*前时,
我们只是限制了指针变量p所指向的地址的存储空间的内容不能被修改,
但指针p可以指向其他变量的地址。
*/



//2.放在*后
int main()
{
    int a = 10;
	int b = 20;
	int* const p = &a;
	*p = 20;//ok
	p = &b;//err
	return 0;
}
/*
这个时候,我们可以通过指针修改变量a的值,
但是,我们不可以让这个指针变量指向其他变量的地址了。
总结来说,当const放在*后时,我们限制的指针变量p所指向的地址,
但是这个地址的存储空间里所存储的内容它限制不了。
*/


//3.*前后都用const进行修饰
int main()
{
	int a = 10;
	int b = 20;
	const int* const p = &a;
	*p = 20;//err
	p = &b;//err
	return 0;
}
/*
这个时候,无论是地址还是存储空间的内容都被限制了,都不能进行修改。
*/

5. 指针运算

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

5.1 指针±整数

我们已经在前面学过数组,大家都知道数组在内存里是连续存放的,
在这里插入图片描述

  • 我们可以使用指针来访问数组,数组在内存中是连续存放的,我们定义的数组是int类型的(4个字节),那么一个元素就占4个字节.我们定义了一个int*类型的指针变量p,(p±1)就是向后或者向前跳过4个字节来访问.
    在这里插入图片描述

5.2 指针-指针

在这里插入图片描述
从上述代码中可以看出,指针-指针的结果是指针之间的元素个数.

5.3 指针的关系运算

#include<stdio.h>

int main() {
    int arr[5] = { 1, 2, 3, 4, 5 };
    int* p1 = &arr[2]; // 指向数组中的第三个元素  
    int* p2 = &arr[3]; // 指向数组中的第四个元素  
    int* p3 = p1;        // 指向和p1相同的地址  

    // 比较指针是否相等  
    if (p1 == p3) {
        printf("相等\n");
    }

    // 比较指针是否不相等  
    if (p1 != p2) {
        printf("不相等\n");
    }

    // 比较一个指针是否在另一个指针之前  
    if (p1 < p2) {
        printf("yes\n");
    }

    // 比较一个指针是否在另一个指针之后  
    if (p2 > p1) {
        printf("yes\n");
    }

    // 比较一个指针是否不大于另一个指针  
    if (p1 <= p2) {
        printf("yes\n");
    }

    // 比较一个指针是否不小于另一个指针  
    if (p2 >= p1) {
        printf("yes\n");
    }

    return 0;
}

6. 野指针

6.1 野指针的成因

  • 1.指针变量没有被初始化:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。如果没有初始化,编译器可能会报错,提示指针可能未初始化。在Release模式下,编译器可能会将指针赋随机值。而在某些环境下,如linux的g++环境,会将野指针置0,使其指向NULL。因此,在指针变量创建时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  • 2.指针被释放后未置为NULL:当指针p被free或者delete之后,它指向的内存被释放了,但指针本身没有被设置为NULL。此时的指针依然指向原来的位置,只不过这个位置的内存数据已经被销毁,因此,此时的指针指向的内存就是一个垃圾内存。所以在指针指向的内存被释放后,应将指针置为NULL。

  • 3.指针操作超越了变量的作用范围:即在变量的作用范围之外使用了指向变量地址的指针。这通常发生在局部指针变量在函数返回后仍然被使用,或者在某个对象被销毁后,其成员变量的指针仍然被使用。

6.2 如何规避野指针

6.2.1 指针初始化

//我们在定义一指针变量时,要么让指针指向一个有效的地址,要么就给指针置空(NULL).
#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;//指向一个有效的地址
	int* p1 = NULL;//置空
	return 0;
}

6.2.2 小心指针越界

在这里插入图片描述

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

  • 在C语言中,当指针变量不再使用时,及时将其置为NULL是一个非常重要的编程实践。这样做有助于防止悬挂指针(dangling pointer)的问题,即指针指向的内存已经被释放,但指针本身没有被重新赋值,导致后续可能错误地访问已释放的内存。
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = &arr[0];
	printf("%d\n", *(p + 10));
	//刚刚已经验证了此时指针已经越界了,这时我们及时置空
	p = NULL;

	//当我们需要再次使用时,再让其指向有效地址
	p = &arr[2];
	return 0;
}
  • 同样,在使用指针之前检查其有效性也是确保程序健壮性的关键步骤。
#include<stdio.h>

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = &arr[0];
	printf("%d\n", *(p + 10));
	//刚刚已经验证了此时指针已经越界了,这时我们及时置空
	p = NULL;

	//当我们需要再次使用时,再让其指向有效地址
	p = &arr[2];

	//使用前及时检查
	if (p != NULL)
	{
		//......
	}
	return 0;
}

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

  • 在C语言中,返回局部变量的地址通常是不安全的,因为局部变量在函数返回后其存储空间可能会被释放或覆盖,导致返回的指针指向了一个无效的内存地址。
#include<stdio.h>

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

7. assert断言

  • assert宏是标准库<assert.h>中定义的一个用于诊断错误的工具。它用于在代码中插入检查点,这些检查点会在运行时验证某个条件是否为真。如果条件不满足(即为假),则assert会输出一条错误消息并终止程序执行。
    我们来看一下报错的情况:
    在这里插入图片描述

接下来我们来看一下正常运行的情况:
在这里插入图片描述

8. 指针的使用和传址调用

8.1 strlen的模拟实现

  • 在C语言中,strlen函数用于计算一个字符串的长度(不包括结尾的空字符’\0’)。
  • 下面我们来模拟实现一下,在模拟实现之前我们先了解一下strlen这个函数。
    在这里插入图片描述
#include<stdio.h>

size_t my_strlen(const char* str)
{
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	printf("%d\n", my_strlen("abcdef"));
	return 0;
}

8.2 传值调用和传址调用

比如我们写一个函数来交换两个变量的值,使用传值调用。
在这里插入图片描述

  • 我们发现并没有实现交换啊,这是因为传值调用是指在函数调用时,将实际参数的值复制一份,然后将复制的值传递给形式参数,形式参数是实际参数的一份临时拷贝。这意味着在函数体内部,对形式参数的操作并不会影响到实际参数的值。这是因为形式参数和实际参数分别占用不同的内存空间,对形式参数的修改不会影响到实际参数。

  • 那么传址调用也是怎么样的呢?能够实现我们交换两个变量的值的目的吗?
    话不多说,上代码:
    在这里插入图片描述
    哇塞!!!!!!!!!!我们成功了哈哈哈哈哈哈
    请添加图片描述

  • 这是因为传址调用它是在函数调用时传递的是变量的地址,而不是变量的值。这意味着在函数体内部,对形式参数的操作实际上是对实际参数的操作,因为它们指向的是同一块内存地址。因此,对形式参数的修改会影响到实际参数的值。


好了,深入理解指针(1)的内容就到此为止了,期待深入理解指针(2)吧!!!
真的是写的又有成就感,又要累死了!!!!
请添加图片描述

请添加图片描述
请添加图片描述
请添加图片描述

  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

论迹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值