- 首先,指针在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 指针变量
首先,我们要明确两点.
- 指针变量是什么?其实指针变量就是存储地址的变量.
- 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)吧!!!
真的是写的又有成就感,又要累死了!!!!