C语言内功修炼---指针详讲(初阶)

2023-11-11 06:30

本文主要是介绍C语言内功修炼---指针详讲(初阶),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:

都说会用一门语言几个礼拜就可以了。这句话我不敢苟同,至少在我学习C语言指针之后就不这么觉得了。

不信?来上才艺:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

这两行代码出自《C陷阱和缺陷》

我相信大部分人在第一次看这俩行代码都是一脸懵逼。

是不是头皮发麻?这是啥东西?

如果你是这样,那么请收起你的骄傲,再也不要觉得C语言很“简单”,谦虚一点,好好学习!

如果不是,能一眼看出来是这俩行代码是什么意思的评论区告诉我,我给你点赞 (大佬抱抱)。

好了,其实无论你能否一眼看出来以上代码所表示的意思,我觉得都不应该轻视任何一门语言,编程世界,浩瀚无边,人外有天,天外有人。只有对知识怀着敬畏之心,知识才会源源不断的涌入你的脑袋,挤走水分。

打住!以下开始c语言指针的学习。

1.指针是什么?

要知道,我们的数据存放在计算机的内存里面,这些数据是非常多的,而要在这么多的数据里面找到我们所需的数据就需要对内存里面的每个单元编号,这样一来,每个内存单元都有了自己独立的编号,我们在存放数据以及查找数据时就只需要找到对应的编号在进行操作就可以了。

就像是在一栋二十楼的大厦里面找到张三的住处,如果不知道他的住房编号,那么就只能一个房间一个房间的查了,而如果有张三的住房编号,知道他住几楼几号,那找到他的房子就很简单了。

这样类似于房间编号的编码,就是地址,也就是指针。

既然每一个最小的内存单元都有一个地址,那么这个内存单元多大呢?

首先定义一个一维数组,因为一维数组的元素在内存中是连续存放的,每个元素的空间大小除以每个元素所占连续地址的数量,就是每一个地址所占得空间大小。

cfc9c40e5a584519ae930619a384e921.png

eb6ca9a9f2144aa3a395cdf2a19e071b.png

这里我们可以看到,a[0]的地址与a[1]的地址相差4,又因为int占四个字节,所以这四个字节都有一个地址。

计算机存储信息的最小单位,称之为位(bit,又称比特)存储器中所包含存储单元的数量称为存储容量,其计量基本单位是字节(Byte。简称B),8个二进制位称为1个字节,此外还有KB、MB、GB、TB等,它们之间的换算关系是1Byte=8bit,1KB=1024B,1MB=1024KB,1GB=1024MB,1TB=1024GB。

所以理解指针有两个要点:

1.指针就是最小内存单元的编号(地址),每个内存单元为一字节。

2.我们口头上表述的指针其实是指针变量,是一个用来存放地址的变量。

指针变量:

是一个存放地址的变量,返回值是指针类型,可以用取地址符&把地址取出来。(上面的代码有用到)。

e00953a84bbd45179028a5ea8fb91cf2.png

好了,现在我们知道了指针变量就是存放地址值以及每一个地址都是一个字节的编号。

如何编址:

还有一个问题,就是这个地址是怎么来的呢?是如何编址的呢?

大概就是在计算机里面有一些地址线,如果是32位机器那就是32根线,每一根线在寻址的时候都会产生高电压或者低电压,也就对应着二进制的1和0,也就是说,这32根线可以组成多少个不同的01序列呢?2的32次方。

这些不同的01序列也就一一对应着一个地址,也就是有2的32次方个字节去编址。

0101001010101001010101001010………………

32个0/1位要用多大的空间去储存?1个字节8个比特位,那就是4个字节存放这么一串32位序列。

所以,一个用来存放地址的指针变量也就占4个字节的大小咯!(32位机器)。

那么2的32次方个字节是多大呢?

1GB=1024*1MB=1024*1024*KB=1024*1024*1024*bit=2^30bit.

所以2^32bit=4GB.

那么64位机器有64根地址线,能编多少个地址呢?

好大好大………

总结:

1.指针是用来存放地址的,地址是唯一的一块空间标识。

2.指针变量的大小跟机器操作位数有关,32位的话就是4个字节,64位的话就是8个字节。

 

2.指针类型:

指针有什么类型呢?给出以下类型:

char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;

指针变量的定义:类型 + *.

char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址等等。

那么问题来了,既然每个指针变量存放的都是地址,也就是一个编码,其本质上来说都是一样的,

2.1为什么还要给指针区分类型呢?

指针类型的意义主要体现在以下几个方面:

  1. 内存管理:指针类型允许我们动态地分配和释放内存。通过指针,我们可以在运行时分配内存块,并在不需要时释放它们,这样可以更有效地利用内存资源。例如,在C语言中,可以使用malloc()函数来动态分配内存,并使用free()函数来释放内存。

  2. 数据结构:在C语言中,指针类型非常适合用来构建复杂的数据结构,如链表、树和图等。通过指针,可以连接不同的数据节点,并通过指针进行遍历、插入和删除等操作,从而实现高效的数据操作。

  3. 函数传参:在C语言中,函数的参数传递通常是通过值传递的方式,也就是将实参的值复制给形参。但是对于大型的数据结构或者需要修改实参的情况,通过指针传递参数可以避免数据的复制,提高函数的执行效率,并且可以直接修改实参的值。

  4. 数组操作:数组在C语言中是通过指针进行访问的。数组名实际上是指向数组首元素的指针。通过指针可以对数组进行遍历、访问和修改等操作,使得数组操作更加灵活高效。

2.2指针加减一个整数:

char ch = 'a';
char* pc = &ch;
int num = 11;
int* pi = #printf("ch地址 %p\n", pc);//输出char类型变量ch的地址
printf("ch地址+1 %p\n", pc + 1);//输出pc+1的地址printf("num地址 %p\n", pi);
printf("num地址+1 %p\n", pi + 1);

2d2d6266ca6647a1bf18ea83a5462278.png

指针的类型还在结构上决定了指针向前或者向后走一步有多大(距离)。

2.3指针的解引用

定义:指针的解引用是指通过指针访问或修改指针所指向的内存中存储的数据。当我们通过一个指针变量来间接访问它所指向的值时,就称为指针的解引用。

int x = 10;
int* p = &x;  // p指向变量x的地址
printf("%d\n", *p);  // 输出变量x的值,输出:10
*p = 20;  // 修改变量x的值
printf("%d\n", x);  // 输出修改后的变量x的值,输出:20

在上述代码中,通过使用"*p"来解引用指针p,我们实际上是在访问或修改p所指向的内存中的值,也就是变量x的值。

需要注意的是,当解引用一个指针时,要确保该指针已经被正确地初始化,且指向有效的内存位置,否则会导致未定义的行为。因此,在对指针进行解引用之前,经常需要对指针进行空指针判断或者有效性检查。

不同的指针类型解引用有什么区别呢?

fb5b1d48fbfa4971b525c5a9968f8a00.png

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节

 

3.野指针

3.1什么叫野指针?

野指针(Wild Pointer)是指指向非法内存地址的指针变量。这种指针没有被正确初始化,或者指向的内存已被释放,因此不能安全地访问或修改其所指向的数据。

3.2什么情况会引起野指针问题呢?

1.指针没有初始化

在定义指针变量的时候没有给其赋予初值,也没有使其指向有效的空间地址,这样一个没有被定义的指针在被解引用的时候就会因为找不到其指向地址而产生不确定的后果,甚至会导致系统崩溃。

2.指针指向的空间被释放

在一片空间被释放的时候,如果没有将其指针置为NULL或者指向其他的地址,如果继续使用已释放的指针进行解引用操作,可能会导致访问无效的内存,造成程序错误或崩溃。

3.指针越界

当访问或者修改一个指针指向的内存块范围外的空间位置时,该位置也许是一个无效的内存,也有可能已经存放了其他的变量的数据,所以这样的访问可能会导致程序崩溃,数据损坏等错误。

3.3如何避免野指针的出现呢?

1、定义指针时初始化。

2、在使用指针的时候检查有没有越界。

3、在释放内存后,将对应的指针置为NULL。

4、在指针超出其作用域后,将其置为NULL,以免被误用。

5、使用前检查其有效性。

总的来说,为了避免野指针问题,应该养成良好的指针使用习惯。

 

4.指针运算

4.1指针加减整数

指针+整数:

int arr[5] = { 1,2,3,4,5 };
int* p1 = &arr[0];
for (int i = 0; i < 5; i++) {printf("%p %d\n", p1 + i, *(p1 + i));
}

d2831b9d72424d9fa8d2543bbfbd8ab6.png

表示指针向地址增高的方向移动了若干个元素的距离,如果元素是整数,那么就移动4个字节的距离。

减法也是同理:

4f9486b25fce4976998df49d5b17fb1a.png

4.2指针减指针

int arr[5] = { 1,2,3,4,5 };int* p1 = &arr[4];int* p2 = &arr[0];printf("%d\n", p1 - p2);char str[] = "abcdefghij";char pc1 = &str[0];char pc2 = &str[9];printf("%d\n", pc2 - pc1);

aa4ee5e891824ac1a2ef2364a5b849b0.png

我们可以发现,两个指针相减得到的结果是中间的元素个数。

4.3指针的关系运算

指针是怎么进行比较的呢?

int arr[] = { 1, 2, 3 };
int* p0 = &arr[0];
int* p1 = &arr [1];
int* p2 = &arr[2];
//分别输出三个指针
printf("p0=%d\n", p0);
printf("p1=%d\n", p1);
printf("p2=%d\n", p2);
//比较三个指针,并输出表达式的值
printf("p0>p1=%d\n", p0 > p1);
printf("p0<p1=%d\n", p0 < p1);
printf("p0>p2=%d\n", p0 > p2);
printf("p0<p2=%d\n", p0 < p2);
printf("p2>p1=%d\n", p2 > p1);
printf("p2<p1=%d\n", p2 < p1);

eb34f836582747eba1c920d78956e00a.png

我们发现,其实指针比较的就是地址大小,返回0表示假,1表示真。

我们可以利用这一点来比较数组元素的相对顺序。

值得注意的一点是,标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。

5.指针和数组

大多数情况下,数组名和数组首元素的地址是一样的。

int arr[3] = { 1,2,3 };
printf("%p %p\n", arr, arr[0]);

035556c21f964dcf83feb9a6a92c1917.png

所以数组名表示的就是数组首元素的地址。

只有两种情况例外:

1.sizeof数组名

int arr[3] = { 1,2,3 };
//printf("%p %p\n", arr, &arr[0]);
printf("%d\n", sizeof arr);//输出arr表示的字节大小
printf("%d\n", sizeof arr[0]);//输出首元素的字节的大小

de94055363ad4e59aae44149487a24f0.png

我们可以看到,这个时候arr和arr[0]表示的意思不一样了,此时的arr表示的是整个数组,所以sizeof(arr) 也就是得到整个数组的字节大小。

2.&数组名


void test(int* p) {printf("%d\n", sizeof p);
}int main() {int arr[5] = { 1,2,3,4,5 };test(&arr);return 0;
}

e338e8e05f024b12a3fe4fd67453c8b1.png

为什么此时的sizeof p=4呢?

其实这里的4表示是的是指针变量的空间大小,你换成char* 类型同样也是4。

当用数组名作为参数传参的时候,形参实际上上就是一个指针变量,sizeof 指针=4(32位机器)。

通过指针访问数组

既然可以把数组名当成一个地址存放在指针中,那我们就可以利用这个指针来访问这个数组。

int arr[5] = { 1,2,3,4,5 };
int* p = arr;
int sz = sizeof arr / sizeof arr[0];//得到数组大小
for (int i = 0; i < sz; i++) {printf(" & arr[%d] =%p   <====> p + %d = %p\n",i, &arr[i],i, p + i);printf(" arr[%d] =%d   <====> *(p + %d) = %d\n", i, arr[i], i, *(p + i));
}

100aa65febea4d20be68a82d9487e4a1.png

所以 p+i 其实计算的是数组 arr 下标为i的地址,也就是说 *(p+i)=arr[i]。 那我们就可以直接通过指针来访问数组。

6.二级指针

看完以上内容相信我们已经初步知道了指针变量的由来以及用法。那么,问题又来了,既然指针变量也是一个变量,那指针变量又是存放在那里呢?指针变量的地址存在那里呢?

一级指针变量的地址存放在二级指针里。

来看以下代码:

int a = 10;
int* p1 = &a;//将变量a的地址赋给指针p1
printf("%p\n", p1);
int** p2 = &p1;//将指针p1的地址赋给p2
printf("%p\n", p2);
//分别进行解引用
printf("%d\n", *p1);
printf("%p\n", *p2);
printf("%p\n", p2);

d30491f7fc564b94a734cdd20c75dc67.png

我们可以看到,我们把变量a的地址赋给了指针p1,再把指针p1的地址赋给指针p2,这个时候解引用p1得到的是a的值,而解引用p2得到的是p1的值,也就是a的地址。

bcf7745fdfd44873b90a71f8265da907.png

又因为p2存的是p1的地址,*p2得到的是变量a的地址,也就是说再对*p2解引用得到的就是变量a的值了。

int a = 10;
int* p1 = &a;//将变量a的地址赋给指针p1
int** p2 = &p1;//将指针p1的地址赋给p2
printf("%d\n", **p2);

输出10.

学习是一个循序渐进的过程,只有把这些指针的基本知识先了解了才能更好的深入了解指针,下一篇博客我将和大家更加深入的了解指针,感谢大家的支持!

 

 

 

 

 

这篇关于C语言内功修炼---指针详讲(初阶)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/388165

相关文章

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

使用SQL语言查询多个Excel表格的操作方法

《使用SQL语言查询多个Excel表格的操作方法》本文介绍了如何使用SQL语言查询多个Excel表格,通过将所有Excel表格放入一个.xlsx文件中,并使用pandas和pandasql库进行读取和... 目录如何用SQL语言查询多个Excel表格如何使用sql查询excel内容1. 简介2. 实现思路3

Go语言实现将中文转化为拼音功能

《Go语言实现将中文转化为拼音功能》这篇文章主要为大家详细介绍了Go语言中如何实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 有这么一个需求:新用户入职 创建一系列账号比较麻烦,打算通过接口传入姓名进行初始化。想把姓名转化成拼音。因为有些账号即需要中文也需要英

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ