掌握C语言指针,轻松解锁代码高效性与灵活性
欢迎大家来到贝蒂大讲堂
养成好习惯,先赞后看哦~
所属专栏:C语言学习
贝蒂的主页:Betty‘s blog
1. 指针与地址
1.1 概念
我们都知道计算机的数据必须存储在内存里,为了正确地访问这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是唯一的,根据编号可以准确地找到某个数据。而这些编号我们就将其称为地址或者指针
1.2 指针变量
数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量。
那我们如何使用指针变量呢?
- datatype *name;
*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型
例如:
int* p1;//指向一个整型的指针
char* p2;//指向一个字符的指针
float* p3;//指向一个单精度浮点数的指针
double* p4;//指向一个双精度浮点数的指针
1.3 &和*
我们早在学习scanf时候就用过取地址符&,它是将某个变量的地址取出来,而解引用*的意思就是通过某个地址找到该地址存储的变量。可能解释起来比较抽象,我们可以通过一个不恰当的例子形象说明一下。

首先我们可以得到如下几个关系:
int a = 1;//第一个客户,&a为0x00000001
int b = 2;//第二个客户,&b为0x00000002
int c = 3;//第三个客户,&c为0x00000003
然后我们可以通过指针变量把他们地址存储进去
int* pa = &a;//把a的地址存进去
int* pb = &b;//把b的地址存进去
int* pc = &c;//把c的地址存进去
在酒店中,我们可以通过门牌号准确找到每个客户。同理,我们也可以通过每个地址准确找到每个变量。
printf("a=%db=%dc=%d", *pa, *pb, *pc);//通过*解引用地址找到对应的值
输出结果 a=1 b=2 c=3
并且我们可以通过指针变量进行赋值。
*pa = 4;
*pb = 5;
*pc = 6;
printf("a=%d b=%d c=%d\n", *pa, *pb, *pc);
输出结果:a=4 b=5 c=6
1.4 void*指针和NULL
(1)void*是一种特殊的指针类型,它可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。
void*p1;
int*p2;
p1=p2;//这是被允许的
- 但是却不能把void*指针赋值给任意指针类型,也不能直接对其解引用
void*p1;
int *p2;
p2=p1;//不能这样赋值
*p1//不能直接对void*解引用
(2)NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
int*p=NULL;//初始化指针
1.5 指针变量的大小
我们知道,现在常见的计算机分为32位机器和64位机器。32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。同理,64位机器需要8个字节才能存储。
我们可以通过以下代码来验证一下。
int main()
{
printf("%zd ", sizeof(char*));
printf("%zd ", sizeof(short*));
printf("%zd ", sizeof(int*));
printf("%zd ", sizeof(double*));
return 0;
}
输出结果:
32位机器:4 4 4 4
64位机器:8 8 8 8
2. 指针的基本运算
2.1 指针+-整数
我们先观察一下如下代码的地址变化
#include <stdio.h>
int main()
{
int n = 10;
char* p1 = (char*)&n;//将int*强转为char*
int* p2 = &n;
printf("%p\n", &n);
printf("%p\n", p1);
printf("%p\n", p1 + 1);//p1向后移动一位
printf("%p\n", p2);
printf("%p\n", p2 + 1);//p2向后移动一位
return 0;
}
输出 :
&n=005DF8D4
p1=005DF8D4
p1+1=005DF8D5
p2=005DF8D4
p2+1=005DF8D8
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。由此我们得出结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
- 因为每次代码运行时,系统都会重新分配内存,所以输出结果每次都不会一样,但是规律是一样的
我们知道数组在内存中是连续存储的(地址由低到高),所以我们只需要只要首元素的地址就能找到数组所有元素的地址,而一维数组的数组名恰恰就是我们首元素的地址。
假设有数组int arr[10]={1,2,3,4,5,6,7,8,9,10}
| arr | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
那我们如何通过指针访问每个元素呢?
代码参考如下:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];//&arr[0]=arr
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//因为数组元素连续存储,所以可以通过+-整数找到之后元素
}
return 0;
}
输出结果:1 2 3 4 5 6 7 8 9 10
2.2 指针-指针
指针-指针其实是指在同一空间内,两个指针之间的元素个数。
知道这点之后,我们可不可以自己实现一个字符串库函数strlen()呢?
思路如下:
思路:首先定义两个指针p1,p2,让两个指针指向首元素,然后让一个指针p2循环++,直到指向‘\0’就停止,最后返回p2-p1,就能得到字符串的长度

代码如下:
int my_strlen(char* p1)
{
char* p2 = p1;//使两个指针都指向首元素
while (*p2)
{
p2++;
}
return p2 - p1;//返回两指针直接的元素的个数就是其长度
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);//计算arr字符串的长度
printf("%d\n", len);
return 0;
}
2.3 指针的关系运算
我们知道了指针变量本质是存放的地址,而地址本质就是十六进制的整数,所以指针变量也是可以比较大小的。
代码示例:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);//打印数组每个元素
p++;
}
return 0;
}
3. const修饰
我们知道变量是可以改变的,但是在有些场景下,我们不希望变量改变,那我们该怎么办呢?这就是我们接下来要讲的const的作用啦。
3.1 const修饰变量
简单来说,经过const修饰的变量,可以当做一个常量,而常量是不能改变的。
int a = 1;//a可修改的
const int b = 2;
b=3;//b不可修改的
但是可以通过指针间接修改.
代码如下:
int main()
{
const int b = 2;
int* p = &b;
*p = 3;//通过指针间接修改
return 0;
}
3.2 const修饰指针
我们知道const的作用后,就可以看看下面几段代码。
int a = 10;
const int* p = &a;
*p = 20;//是否可以
p = p + 1;//是否可以
通过测试我们发现,*p无法改变成20,但是p可以改变成p+1.
那如果把const调换一下位置,又会出现什么情况呢~
int a = 10;
int* const p = &a;
*p = 20;//是否可以
p = p + 1;//是否可以
再次测试之后我们发现,*p可以被赋值为20,但是p不能赋值为p+1了
通过上述测试,我们大致可以总结出两个结论。
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
4. assert断言
assert是一个宏,它的头文件为<assert.h>,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
举一个简单的例子:
assert(a>0);
如果a的确大于0,assert判断为真,就会通过。
如果a不大于0,assert判断为假,就会报错。
所以assert常常用于检查空指针问题,以防止程序因为空指针的问题而出错。
int *p=NULL;
assert(p);//空指针是0,0为假,就会报错
5. 传值调用与传址调用
5.1 传值调用
我们前面学习函数时候,遇到过这样一段代码。
#include<stdio.h>
void swap(int x, int y)//返回类型为void表示不返回值
{
int temp = 0;//定义一个临时变量
temp = x;//把x的值赋给temp
x = y;//把y的值赋给x
y = temp;//把temp的值赋给y,完成交换操作
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(a, b);//交换函数
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
输入:3 5
输出:交换后a=3 ,b=5
为什么两个值并没有交换呢,这是因为形参只是实参的一份临时拷贝,对形参改变,根本不会改变实参。如果忘记的同学可以再去温习一下贝蒂的函数小课堂
5.2 传址调用
那我们想在函数中改变实参的值,那又该如何改变呢?
其实很简单,我们学了指针,知道可以通过地址间接访问该变量的值,所以我们只需要把地址传给函数,在函数中通过地址访问实参,并进行交换。
代码如下:
#include<stdio.h>
void swap(int*x, int*y)//通过指针变量接受地址
{
int temp = 0;//定义一个临时变量
temp = *x;//把*x的值赋给temp
*x = *y;//把*y的值赋给*x
*y = temp;//把temp的值赋给*y,完成交换操作
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);//将地址传给函数
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}

6. 野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针成因
(1)指针未初始化
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
因为p是随机值,所以对p解引用,系统无法通过p的地址找到对应的空间,所以出错造成野指针
(2)数组越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < 11; i++)
{
//数组下标是0到9
printf("%d ", *(arr + i));
}
return 0;
}

- 一般出现这种较大的随机值,一般都是数组越界访问
(3)指针指向空间释放
#include <stdio.h>
int* test()
{
int n = 10;
return &n;//返回n的地址
}
int main()
{
int* p = test();//用p接受n的地址
printf("%d\n", *p);//打印出n的值
return 0;
}
这段代码乍一看,好像并没有什么问题,但是大家在学习函数的时候知道,在函数中定义的变量是临时变量,一旦出了作用域就会销毁。
一旦销毁,系统就无法访问该空间,而通过指针我们还可以访问该空间,这就造成了冲突,所以出错,造成野指针。
6.2 解决方法
(1) 初始化
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。如下是NULL在编译器中的定义:
ifdef __cplusplus
define NULL 0
else
define NULL ((void *)0)
endif
#include <stdio.h>
int main()
{
int* p=NULL;//用空指针初始化,让其有指向位置
//*p = 20;NULL地址不能读写
return 0;
}
(2) 小心越界访问
我们在使用数组时候,一定要对数组的元素个数有一个清晰的把控,不然就很容易出现越界访问的情况。
(3) 不能返回临时变量的地址
临时变量出了作用域就会销毁,系统会回收该空间,所以我们要尽量避免指针指向已经销毁的空间,尤其在函数中,不能返回临时变量的地址。
掌握C语言指针,轻松解锁代码高效性与灵活性的更多相关文章
- C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻
这是道哥的第014篇原创 目录 一.前言 二.变量与指针的本质 1. 内存地址 2. 32位与64位系统 3. 变量 4. 指针变量 5. 操作指针变量 5.1 指针变量自身的值 5.2 获取指针变量 ...
- (转载)c语言指针学习
前言 近期俄罗斯的陨石.四月的血月.五月北京的飞雪以及天朝各种血腥和混乱,给人一种不详的预感.佛祖说的末法时期,五浊恶世 ,十恶之世,人再无心法约束,道德沦丧,和现在正好吻合.尤其是在天朝,空气,水, ...
- C语言指针【转】
一.C语言指针的概念 在计算机中,所有的数据都是存放在存储器中的.一般把存储器中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不等,如整型量占2个单元,字符量占1个单元等,在前面已有详细 ...
- c语言指针学习【转】
前言 近期俄罗斯的陨石.四月的血月.五月北京的飞雪以及天朝各种血腥和混乱,给人一种不详的预感.佛祖说的末法时期,五浊恶世 ,十恶之世,人再无心法约束,道德沦丧,和现在正好吻合.尤其是在天朝,空气,水, ...
- Tinyhttpd - 超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client(Qt也有很多第三方HTTP类)
- 2. Tinyhttpd tinyhttpd是一个超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client,可以通过阅读这段代码理解一个 Htt ...
- 深入理解C语言 - 指针详解
一.什么是指针 C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址.CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位.这里,数据对象是指存储在 ...
- C语言指针转换为intptr_t类型
1.前言 今天在看代码时,发现将之一个指针赋值给一个intptr_t类型的变量.由于之前没有见过intptr_t这样数据类型,凭感觉认为intptr_t是int类型的指针.感觉很奇怪,为何要将一个指针 ...
- 不可或缺 Windows Native (7) - C 语言: 指针
[源码下载] 不可或缺 Windows Native (7) - C 语言: 指针 作者:webabcd 介绍不可或缺 Windows Native 之 C 语言 指针 示例cPointer.h #i ...
- C语言指针学习
C语言学过好久了,对于其中的指针却没有非常明确的认识,趁着有机会来好好学习一下,总结一下学过的知识,知识来自C语言指针详解一文 一:指针的概念 指针是一个特殊的变量,里面存储的数值是内存里的一个地址. ...
- 关于C语言指针的问题
在学习关于C语言指针的时候,发现这样一个问题,代码如下: #include<stdio.h> #include<stdlib.h> #include<string.h&g ...
随机推荐
- [转帖]ramfs、tmpfs、rootfs、ramdisk介绍
ramfs.tmpfs.rootfs.ramdisk介绍 bootleader--->kernel---->initrd(是xz.cpio.是ramfs的一种,主要是驱动和为了加载ro ...
- [转帖]学习如何编写 Shell 脚本(基础篇)
https://juejin.cn/post/6930013333454061575 前言 如果仅仅会 Linux 一些命令,其实已经可以让你在平时的工作中游刃有余了.但如果你还会编写 Shell 脚 ...
- [转帖]tikv性能参数调优
https://www.cnblogs.com/FengGeBlog/p/10278368.html#:~:text=max-%20bytes%20-for-level-%20base%20%3D%2 ...
- [转帖]shell脚本实现文本内容比较交互程序
背景介绍 脚本基于Comm命令进行功能封装,考虑到命令执行前需要对文本进行排序,并且在多文件需要比较内容时可能会导致多个文本混乱,因此使用Shell封装成了一个交互式程序,快速对文件内容进行判断和输出 ...
- [转帖]Spring Cloud Alibaba Nacos 注册中心使用教程
一. 什么是Nacos Nacos是一个更易于构建云原生应用的动态服务发现(Nacos Discovery ).服务配置(Nacos Config)和服务管理平台,集注册中心+配置中心+服务管理于一身 ...
- F5内核参数的简要学习
前言 最近学习了很长时间的Linux内核参数 但是大部分是纸上谈兵. 也没有一个好的系统用于学习和参照 晚上搜索F5资料时发现F5有一些iso和ova文件 就想着下载学习一下. 看看F5系统默认的参数 ...
- Docker 运行 MongoDB的简单办法
Docker 运行 MongoDB的简单办法 第一步拉取镜像 docker pull mongo 第二步创建自己的目录 地址 10.24.22.240 创建目录 mkdir /mongodb 第三步 ...
- 物联网浏览器(IoTBrowser)-Web串口自定义开发
物联网浏览器(IoTBrowser)-Web串口自定义开发 工控系统中绝大部分硬件使用串口通讯,不论是原始串口通讯协议还是基于串口的Modbus-RTU协议,在代码成面都是使用System.IO.Po ...
- It is currently in use by another Gradle instance
FAILURE: Build failed with an exception. * What went wrong: Could not create service of type TaskHis ...
- 探索 GO 项目依赖包管理与Go Module常规操作
探索 GO 项目依赖包管理与Go Module常规操作 目录 探索 GO 项目依赖包管理与Go Module常规操作 一.Go 构建模式的演变 1.1 GOPATH (初版) 1.1.1 go get ...
