欢迎大家来到贝蒂大讲堂

养成好习惯,先赞后看哦~

所属专栏:C语言学习

贝蒂的主页:Betty‘s blog

1. 指针与地址

1.1 概念

我们都知道计算机的数据必须存储在内存里,为了正确地访问这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是唯一的,根据编号可以准确地找到某个数据。而这些编号我们就将其称为地址或者指针

1.2 指针变量

数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量

那我们如何使用指针变量呢?

  1. 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);
  1. 如果a的确大于0,assert判断为真,就会通过。

  2. 如果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语言指针,轻松解锁代码高效性与灵活性的更多相关文章

  1. C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻

    这是道哥的第014篇原创 目录 一.前言 二.变量与指针的本质 1. 内存地址 2. 32位与64位系统 3. 变量 4. 指针变量 5. 操作指针变量 5.1 指针变量自身的值 5.2 获取指针变量 ...

  2. (转载)c语言指针学习

    前言 近期俄罗斯的陨石.四月的血月.五月北京的飞雪以及天朝各种血腥和混乱,给人一种不详的预感.佛祖说的末法时期,五浊恶世 ,十恶之世,人再无心法约束,道德沦丧,和现在正好吻合.尤其是在天朝,空气,水, ...

  3. C语言指针【转】

    一.C语言指针的概念 在计算机中,所有的数据都是存放在存储器中的.一般把存储器中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不等,如整型量占2个单元,字符量占1个单元等,在前面已有详细 ...

  4. c语言指针学习【转】

    前言 近期俄罗斯的陨石.四月的血月.五月北京的飞雪以及天朝各种血腥和混乱,给人一种不详的预感.佛祖说的末法时期,五浊恶世 ,十恶之世,人再无心法约束,道德沦丧,和现在正好吻合.尤其是在天朝,空气,水, ...

  5. Tinyhttpd - 超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client(Qt也有很多第三方HTTP类)

    - 2. Tinyhttpd tinyhttpd是一个超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client,可以通过阅读这段代码理解一个 Htt ...

  6. 深入理解C语言 - 指针详解

    一.什么是指针 C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址.CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位.这里,数据对象是指存储在 ...

  7. C语言指针转换为intptr_t类型

    1.前言 今天在看代码时,发现将之一个指针赋值给一个intptr_t类型的变量.由于之前没有见过intptr_t这样数据类型,凭感觉认为intptr_t是int类型的指针.感觉很奇怪,为何要将一个指针 ...

  8. 不可或缺 Windows Native (7) - C 语言: 指针

    [源码下载] 不可或缺 Windows Native (7) - C 语言: 指针 作者:webabcd 介绍不可或缺 Windows Native 之 C 语言 指针 示例cPointer.h #i ...

  9. C语言指针学习

    C语言学过好久了,对于其中的指针却没有非常明确的认识,趁着有机会来好好学习一下,总结一下学过的知识,知识来自C语言指针详解一文 一:指针的概念 指针是一个特殊的变量,里面存储的数值是内存里的一个地址. ...

  10. 关于C语言指针的问题

    在学习关于C语言指针的时候,发现这样一个问题,代码如下: #include<stdio.h> #include<stdlib.h> #include<string.h&g ...

随机推荐

  1. [转帖]ramfs、tmpfs、rootfs、ramdisk介绍

    ramfs.tmpfs.rootfs.ramdisk介绍   bootleader--->kernel---->initrd(是xz.cpio.是ramfs的一种,主要是驱动和为了加载ro ...

  2. [转帖]学习如何编写 Shell 脚本(基础篇)

    https://juejin.cn/post/6930013333454061575 前言 如果仅仅会 Linux 一些命令,其实已经可以让你在平时的工作中游刃有余了.但如果你还会编写 Shell 脚 ...

  3. [转帖]tikv性能参数调优

    https://www.cnblogs.com/FengGeBlog/p/10278368.html#:~:text=max-%20bytes%20-for-level-%20base%20%3D%2 ...

  4. [转帖]shell脚本实现文本内容比较交互程序

    背景介绍 脚本基于Comm命令进行功能封装,考虑到命令执行前需要对文本进行排序,并且在多文件需要比较内容时可能会导致多个文本混乱,因此使用Shell封装成了一个交互式程序,快速对文件内容进行判断和输出 ...

  5. [转帖]Spring Cloud Alibaba Nacos 注册中心使用教程

    一. 什么是Nacos Nacos是一个更易于构建云原生应用的动态服务发现(Nacos Discovery ).服务配置(Nacos Config)和服务管理平台,集注册中心+配置中心+服务管理于一身 ...

  6. F5内核参数的简要学习

    前言 最近学习了很长时间的Linux内核参数 但是大部分是纸上谈兵. 也没有一个好的系统用于学习和参照 晚上搜索F5资料时发现F5有一些iso和ova文件 就想着下载学习一下. 看看F5系统默认的参数 ...

  7. Docker 运行 MongoDB的简单办法

    Docker 运行 MongoDB的简单办法 第一步拉取镜像 docker pull mongo 第二步创建自己的目录 地址 10.24.22.240 创建目录 mkdir /mongodb 第三步 ...

  8. 物联网浏览器(IoTBrowser)-Web串口自定义开发

    物联网浏览器(IoTBrowser)-Web串口自定义开发 工控系统中绝大部分硬件使用串口通讯,不论是原始串口通讯协议还是基于串口的Modbus-RTU协议,在代码成面都是使用System.IO.Po ...

  9. 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 ...

  10. 探索 GO 项目依赖包管理与Go Module常规操作

    探索 GO 项目依赖包管理与Go Module常规操作 目录 探索 GO 项目依赖包管理与Go Module常规操作 一.Go 构建模式的演变 1.1 GOPATH (初版) 1.1.1 go get ...