当前位置: 首页 > news >正文

C语言:指针从入门到精通(上)

一. 指针基础

1.1 取地址操作符 &

在C语言中,创建变量的本质就是是向内存申请一块空间

上面的代码中我们创建了一个整型变量a,内存会申请4个字节来存放整数10。我们在VS2022中进行调试,打开“内存”,输入“&a”回车,显示了图中右边的内容,因此这4个字节的地址分别为:

0x00EFF7D8  
0x00EFF7D9
0x00EFF7DA 
0x00EFF7DB 

如果我们想要得到 a 的地址就需要用到 & --- 取地址操作符,下面我们来看一下:

虽然a占用4个字节,但是我们发现在屏幕上只输出一个字节的地址。其实,&a取出的是a所占4个字节中地址较小的字节的地址,知道了第一个地址,我们便也能得出剩下3个字节的地址了

1.2 指针变量 p

#include <stdio.h>int main()
{int a = 10;//&取地址操作符(单目操作符)int* p = &a;//p是一个变量(指针变量),是一块空间//指针变量 --- 存放地址的变量//内存单元的编号 == 地址 == 指针return 0;
}

区分指针指针变量

那么如何来理解指针变量呢?

int a = 10;
int * p = &a;

上面语句中,a是int类型,它存放的是10,假设它的地址是0x0012ff40,那么p存放的就是0x0012ff40,而p的类型就是 int * 类型

我们再来看一个例子:

char ch = 'a';
char * p = &ch; // p 就是 char * 类型

1.3 解引用操作符 *

我们来看一段代码:

#include <stdio.h>int main()
{int a = 10;int * p = &a;*p = 0;//* - 解引用操作符(也叫间接访问操作符)printf("%d\n", a);//输出0return 0;
}

上面代码中,让 p 指向了 a 的地址,*p 就是 p 指向的地址中对应的内容,其实也就是变量 a 。那么 *p = 0; 其实也就是 a = 0; ,因此输出结果是0。这样我们对 a 的修改就多了一种途径,写代码就会更灵活

再来看一下:

p = &a; 

*p = a;

从中我们可以看出 & * 其实类似于一种抵消的关系,也就是 *&a = 0; 等价于 a = 0;

 a 和 p 是这样一种关系 ~~ : a 是幕后老大, p 是 a 的手下,当做某件事不方便 a 动手时,就会让 p 来实现这一事件,归根结底还是 a 的手笔


二. 指针变量

2.1 指针变量的大小

我们知道指针变量是用来存放地址的,那么地址是怎么产生的呢?它是在地址线上传输产生的。假设有32根地址线,那么地址就是32个0/1组成的二进制序列,则一个地址占32个bit位的空间,就需要4个字节才能存储;同理,如果是有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储它就需要8个字节的空间,指针变量的大小就是8个字节

下面我们来测试一下指针变量的大小:

#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;
}

由此我们可以得出:

1. 32位平台下地址是32个bit位,指针变量大小是4个字节

2. 64位平台下地址是64个bit位,指针变量大小是8个字节

3. 指针变量的大小和类型是无关的,只要指针的变量在相同平台下,大小都是相同的

bit --- 比特位 , Byte --- 字节

1Byte = 8bit

1KB = 1024Byte

1MB = 1024KB

1GB = 1024MB

1TB = 1024GB

1PB = 1024TB

2.2 指针变量类型的意义

下面,我们分别从三个方面来解释指针类型的意义:

2.2.1 指针的解引用

我们来对比一下下面这两张图:

我们发现,如果 pa 是 int * 类型,在修改时会将 a 的 4 个字节全部改为 0 ;而如果 pa 是 char * 类型,在修改时则只会把第 1 个字节改为 0 

指针类型决定了指针进行解引用操作符的时候访问几个字节,也就是决定指针解引用时的权限

2.2.2 指针 + - 整数

#include <stdio.h>int main()
{int a = 10;int* pa = &a;char* pc = &a;//pa和pc的大小一样//printf("%zd\n", sizeof(pa));//printf("%zd\n", sizeof(pc));printf("pa   = %p\n", pa);printf("pa+1 = %p\n", pa+1);printf("pc   = %p\n", pc);printf("pc+1 = %p\n", pc + 1);return 0;
}

运行结果:

pa 和 pa+1 之间相差 4 ,pc 和 pc+1 之间差 1 

也就是,char* 类型的指针变量 +1 跳过 1 个字节,int* 类型的指针变量 +1 跳过 4 个字节

我们可以得出,指针的类型决定了指针向前或者向后走一步有多大(距离)

2.2.3 void* 指针

根据前面的知识,我们了解到:char* --- 指向字符的指针,short* --- 指向短整型的指针,int* --- 指向整型的指针,float 8 --- 指向单精度浮点型的指针 ...... ,那么 void* 类型就可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接收任意类型地址,但也有其局限性:void* 类型的指针不能直接进行指针的 + - 整数和解引用的运算

下面来举个例子:

代码中,a 是 int 类型变量,当我们将这样一个变量的地址赋值给 char* 类型的指针变量时,编译器就会发出警告“类型不兼容”

现在再用 void* 类型的指针来试一下:

我们发现程序并没有报错,也就印证了 void* 类型的指针可以用来接收任意类型地址这一结论

当我们在写代码时,如果不确定传进去的指针类型时,就可以使用 void* 来解决这一问题

当然,刚才还提到 void* 不能直接进行指针运算,我们来看一下:

⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果

三. const修饰指针

3.1 const 修饰变量

int a = 10;
a = 20;
printf("%d\n", a);

这段代码的运行结果毫无疑问是 20,此时我们用 const 修饰一下 a ,再来看一下运行情况:

编译器报错了“表达式必须是可修改的左值”,也就是当 a 被 const 修饰后具有了常属性,使得它的值就不能被修改了,那 a 是不是就是常数了呢?

根据上面的内容,可以得出:被 const 修饰的 a 虽然值不能被修改,但本质上还是变量(常变量)

值得注意的是:不同于C语言,在C++里面,被 const 修饰后的变量就是常量

现在我们再来思考,被 const 修饰后的变量就真的不能被改变了吗?漏漏漏,被 const 限制后,我们只是不能直接去修改 a ,但不代表我们不能通过其他途径来修改 a 的值 ------指针!

我们发现通过这样的方式, a 的值被改变了,有没有感觉很棒呢?即使被 const 限制了,我们也依旧可以修改 a 的值,但是,再仔细想想,我们为什么要用 const 来修饰呢?不就是为了让它的值不能被改变吗?如果被 const 限制后,这个变量的值依然能被改变,那它还有什么用呢?

下面就引出了 const 修饰指针变量

3.2 const 修饰指针变量

我们先来区分一下下面这几个对应的内容:

int a = 10;
int* p = &a;

1. p 中存放 a 的地址 -> 0x0012ff40

2. p 的地址指向 a 的内容 *p --> a --> 10

3. p 本身有一个地址 &p --> 0x0012ff48

3.2.1 const 在 * 右边

#include <stdio.h>int main()
{int a = 10;int b = 20;int* const p = &a;//p = &b;//error*p = 15;printf("%d\n", a);return 0;
}
//运行结果:
//15

当 const 修饰指针变量时,将 const 放在 * 的右边,此时 const 限制的是指针变量本身,也就是指针变量 p 不能再指向其他变量了,但依然可以通过指针变量来修改其指向的内容

3.2.2 const 在 * 左边

#include <stdio.h>int main()
{int a = 10;int b = 20;int const* p = &a;p = &b;//*p = 15;//errorprintf("%d\n", *p);return 0;
}
//运行结果:
//20

注:int const* p = &a; 和 const int* p = &a; 是一样的

当 const 修饰指针变量时,将 const 放在 * 的左边,此时 const 限制的是指针指向的内容,也就是不能通过指针来修改指向的内容了,但是可以修改指针变量本身的值( p 可以指向其它变量)

3.2.3 const 加在 * 两边

#include <stdio.h>int main()
{int a = 10;int b = 20;int const* const p = &a;p = &b;//error*p = 15;//errorreturn 0;
}

当然了,这样的话就都改不了了

因此,需要将 const 放在 * 的什么位置,还要根据实际情况,看我们希望什么不要被改变来定


四. 指针运算

4.1 指针 + - 整数

数组在内存中是连续存放的,因此只要知道第一个元素的地址,就能找到后面所有的元素

#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = &arr[0];int i = 0;for (i = 0;i < sz;i++){printf("%d ", *p);p++;//让p移动}return 0;
}
//运行结果:
//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 sz = sizeof(arr) / sizeof(arr[0]);int* p = &arr[0];int i = 0;for (i = 0;i < sz;i++){printf("%d ", *(p + i));//让i移动}return 0;
}
//运行结果:
//1 2 3 4 5 6 7 8 9 10

上面两种代码的区别:一个是通过移动 p 来输出,一个是通过移动 i 来输出

4.2 指针 - 指针

根据前面的指针 + - 整数,我们知道“指针1+整数=指针2”,因此可以得出:指针1 - 指针2 = 整数,即两指针做差的绝对值为两个指针之间的元素个数

#include <stdio.h>int main()
{int arr[10] = { 0 };printf("%zd\n", &arr[9] - &arr[0]);//9printf("%zd\n", &arr[0] - &arr[9]);//-9return 0;
}

注:

指针 - 指针是有前提条件的 ------ 要求两个指针要指向同一块空间

#include <stdio.h>int main()
{int arr[10] = { 0 };char ch[10] = { 0 };printf("%zd\n", &ch[8] - &arr[3]);//错误return 0;
}

例:自定义函数计算字符串长度

#include <stdio.h>int my_strlen(char* str)
{char* start = str;while (*str != '\0'){str++;}return str - start;//指针-指针/*int count = 0;while (*str != '\0'){count++;str++;}return count;*/
}int main()
{//strlen - 求字符串长度,统计的是字符串中\0之前的字符个数char arr[] = "abcdef";//即a b c d e f \0int len = my_strlen(arr);//数组名arr是数组首元素的地址,即arr == &arr[0]printf("%d\n", len);return 0;
}

4.3 指针的关系运算

指针的关系运算其实就是:指针和指针比较大小,地址和地址比较大小

比如:

#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]);while (p < arr + sz)//指针的大小比较{printf("%d ", *p);p++;}return 0;
}

五. 野指针

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

5.1 为什么会出现野指针?

1. 指针未初始化

//正确
int a = 10;
int* p = &a;
*p = 20;//野指针
int* p;
*p = 20;
//p是一个局部变量,一个局部变量不初始化时默认存的是随机值

2. 指针越界访问

int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
int i = 0;//正确
for (i = 0; i < sz; i++)
{*(p++) = i;
}
//会越界
for (i = 0; i <= sz; i++)
{//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;
}

3. 指针指向的空间释放

#include <stdio.h>
//错误
int* test()
{int a = 10;return &a;
}
int main()
{int* p = test();//p为野指针//函数返回时,a的内存被释放,p指向已释放的内存 - 非法访问printf("%d\n", *p);return 0;
}
#include <stdio.h>
//正确
int* test()
{static int a = 10;//使用static修饰变量后,该变量变为静态局部变量,存储在静态存储区//它的生命周期贯穿整个程序运行期间,不会在函数结束时被销毁return &a;
}
int main()
{int* p = test();printf("%d\n", *p);return 0;
}

5.2 如何规避野指针?

1. 指针初始化

(1). 当明确知道指针应该指向哪里时,就给初始化一个明确的地址

(2). 如果还不知道应该指向哪里,那就初始化NULL

int a = 10;
int* p1 = &a;int* p2 = NULL;

2. 小心指针越界

3. 及时置NULL

当指针变量不再使用时,及时置NULL,指针使用之前要检查其有效性
//p越界时,将p置NULL
p = NULL;//再次使用时,判断p是否为NULL,不为NULL时使用
if (p != NULL)
{//...
}

六. assert 断言

assert ( ) 的使用要包含在头文件 assert.h 里,用于确保程序在运行时符合指定条件,如果不符合,就会报错终止运行。这个宏常被称为“断言

 assert ( ) 宏接受一个表达式作为参数,当表达式为真时,程序正常运行;当表达式为假时,程序就会报错,下面我们来看一下:

输入的 3 小于 6 ,程序直接报错提醒

貌似用 if 语句也能实现类似效果:

#include <stdio.h>
#include <assert.h>int main()
{int n = 0;scanf("%d", &n);//assert(n > 6);if (n > 6)printf("%d\n", n);return 0;
}

相对于用 if 语句,assert ( ) 还是有很多优点的:

1. assert 出现错误时,会直接报错,并指明在哪个文件,哪一行

2. 当确定程序没有问题,不需要再做断言时,直接在 #include <assert.h> 语句前定义一个宏  NDEBUG 即可

当然,assert ( ) 也有它的缺点:由于引入了额外的检查,增加了程序的运行时间

⼀般我们可以在 Debug 中使用,在 Release 版本中选择禁用 assert 就行,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在 debug 版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率

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

7.1 my_strlen 函数的完善

因为C语言中 strlen 函数(求字符串长度)的返回类型为 size_t ,因此我们也改为了 size_t 类型

size_t  strlen  ( const  char *   str )

#include <stdio.h>
#include<assert.h>size_t my_strlen(const char* str)
{//将const放在左边是为了防止修改str指向的内容://*str = 'b';//会修改arr数组的内容为b b c d e f,若加上const这句话就会报错,见下图size_t count = 0;assert(str != NULL);//assert断言检查指针有效性while (*str != '\0'){count++;str++;}return count;
}int main()
{char arr[] = "abcdef";size_t len = my_strlen(arr);printf("%zd\n", len);for (size_t i = 0;i < len;i++)printf("%c ", arr[i]);return 0;
}
//运行结果:
//6
//a b c d e f

有无 const 区别的:

修改了数组原内容

会报错提醒

7.2 传值调用

下面写一个函数,来交换两个整型变量的值:

#include <stdio.h>void Swap(int x, int y)
{int t = x;x = y;y = t;
}int main()
{int a = 6;int b = 8;printf("交换前:a = %d,b = %d\n", a, b);Swap(a, b);//传值调用printf("交换后:a = %d,b = %d\n", a, b);return 0;
}

运行结果:

交换前:a = 6,b = 8
交换后:a = 6,b = 8

我们发现 a,b 并没有交换,这是为什么呢?现在来调试一下:

我们可以看到 x, y 的值和 a, b 相等,但所占的地址却不同,相当于说 x, y 是独立的空间

在调用 Swap 函数时,交换了 x, y 的值,但却并不影响 a, b 的值

像 Swap 函数这种,在使用时把变量本身传递给函数的这种调用函数的方法,我们就称之为传值调用(当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参)

那我们要怎么实现交换的这个函数呢?这就引出了下面我们要讲的内容 ------ 传址调用

7.3 传址调用

#include <stdio.h>void Swap(int* pa, int* pb)
{int t = *pa;//*pa - a*pa = *pb;//*pb - b*pb = t;
}int main()
{int a = 6;int b = 8;printf("交换前:a = %d,b = %d\n", a, b);Swap(&a, &b);//传址调用printf("交换后:a = %d,b = %d\n", a, b);return 0;
}

运行结果:

交换前:a = 6,b = 8
交换后:a = 8,b = 6

在上面的代码中,我们将 main 函数中 a 和 b 的地址传给了 Swap 函数,使得 Swap 函数可以通过地址间接操作 main 函数中的 a 和 b,并达到了预期的效果

上面这种,将变量的地址传递给函数的函数调用方法,我们称之为传址调用

一定要注意两者不同的地方:

传址调用可以让主函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量

因此,未来函数中只需要主调函数中的变量值来实现计算,就可以采用传值调用;若函数内部要修改主调函数中的变量的值,就用传址调用


文章转载自:

http://1onRD3Fa.xqgtd.cn
http://0zuaNpTe.xqgtd.cn
http://XrCoWSQX.xqgtd.cn
http://UetJnifL.xqgtd.cn
http://dpQkdFbh.xqgtd.cn
http://rdvTYXpy.xqgtd.cn
http://UseTUNLO.xqgtd.cn
http://mY1ROSUG.xqgtd.cn
http://uX6e0MUd.xqgtd.cn
http://2Tk0zVwB.xqgtd.cn
http://3sewSdIa.xqgtd.cn
http://watcnbBZ.xqgtd.cn
http://EbSF53pY.xqgtd.cn
http://EvGYzALb.xqgtd.cn
http://sQok0Nn3.xqgtd.cn
http://41YHUP2D.xqgtd.cn
http://nItFaZY1.xqgtd.cn
http://BodfRaSd.xqgtd.cn
http://Swzh08Ge.xqgtd.cn
http://ESyCygNN.xqgtd.cn
http://CKL53OQD.xqgtd.cn
http://Ow6g8Iow.xqgtd.cn
http://eoEbManz.xqgtd.cn
http://ZslScgW3.xqgtd.cn
http://k8QwGZxy.xqgtd.cn
http://0BKhoFm7.xqgtd.cn
http://7LPeM9i9.xqgtd.cn
http://FPagfQoe.xqgtd.cn
http://zIPdjz31.xqgtd.cn
http://5Pry6vzr.xqgtd.cn
http://www.dtcms.com/a/382001.html

相关文章:

  • 【MySQL】--- 表的约束
  • SpringBoot 轻量级一站式日志可视化与JVM监控
  • Java零基础学习Day10——面向对象高级
  • JavaScript中ES模块语法详解与示例
  • 系统核心解析:深入操作系统内部机制——进程管理与控制指南(三)【进程优先级/切换/调度】
  • Roo Code:用自然语言编程的VS Code扩展
  • 第8.4节:awk的内置时间处理函数
  • leetcode算法刷题的第三十四天
  • 【技术博客分享】LLM推理过程中的不确定问题
  • Vue3基础知识-setup()、ref()和reactive()
  • 规则系统架构风格
  • 宋红康 JVM 笔记 Day17|垃圾回收器
  • vue表单弹窗最大化无法渲染复杂组件内容
  • 加餐加餐!烧烤斗破苍穹
  • SCSS 中的Mixins 和 Includes,%是什么意思
  • RFID基础了解 --- RC522
  • 第九篇 永磁同步电机控制-弱磁控制
  • 搭建langchain4j+SpringBoot的Ai项目
  • 一次 Linux 高负载 (Load) 异常问题排查实录
  • 扩散模型进化史
  • 学习Python是一个循序渐进的过程,结合系统学习、持续实践和项目驱动,
  • EKSPod 资源利用率配置修复:从占位符到完整资源分析系统
  • MySql基础:数据类型
  • 鸿蒙中的智能设备数据分析实战:从采集到建模的完整实现指南
  • Scikit-Learn 对糖尿病数据集(回归任务)进行全面分析
  • Scikit-learn 对加州房价数据集(回归任务)进行全面分析
  • Scintil在集成光子学技术方面筹集了5800万美元。
  • 通俗易懂地讲解JAVA的BIO、NIO、AIO
  • 数据结构与算法2:线性表补充
  • 内核实时监控策略针对海外vps容器性能的诊断方法