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

相关文章

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目: 题解: static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num & MASK1) == 0) {return

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

C语言:柔性数组

数组定义 柔性数组 err int arr[0] = {0}; // ERROR 柔性数组 // 常见struct Test{int len;char arr[1024];} // 柔性数组struct Test{int len;char arr[0];}struct Test *t;t = malloc(sizeof(Test) + 11);strcpy(t->arr,

C语言指针入门 《C语言非常道》

C语言指针入门 《C语言非常道》 作为一个程序员,我接触 C 语言有十年了。有的朋友让我推荐 C 语言的参考书,我不敢乱推荐,尤其是国内作者写的书,往往七拼八凑,漏洞百出。 但是,李忠老师的《C语言非常道》值得一读。对了,李老师有个官网,网址是: 李忠老师官网 最棒的是,有配套的教学视频,可以试看。 试看点这里 接下来言归正传,讲解指针。以下内容很多都参考了李忠老师的《C语言非

C 语言基础之数组

文章目录 什么是数组数组变量的声明多维数组 什么是数组 数组,顾名思义,就是一组数。 假如班上有 30 个同学,让你编程统计每个人的分数,求最高分、最低分、平均分等。如果不知道数组,你只能这样写代码: int ZhangSan_score = 95;int LiSi_score = 90;......int LiuDong_score = 100;int Zhou

C 语言的基本数据类型

C 语言的基本数据类型 注:本文面向 C 语言初学者,如果你是熟手,那就不用看了。 有人问我,char、short、int、long、float、double 等这些关键字到底是什么意思,如果说他们是数据类型的话,那么为啥有这么多数据类型呢? 如果写了一句: int a; 那么执行的时候在内存中会有什么变化呢? 橡皮泥大家都玩过吧,一般你买橡皮泥的时候,店家会赠送一些模板。 上