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

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

目录

一、内存和指针

1.1 指针的使用场景

二、指针变量和地址

2.1 取地址符(&)

2.2指针变量和解引用操作符(*)

2.2.1 指针变量

2.3 指针变量的大小

三、指针变量类型的意义

3.2 指针+-整数

 ​编辑

 四、指针计算

 五、const修饰指针

5.1 const修饰变量

1.2 const修饰指针变量 

六、野指针

6.1 野指针成因

1 指针未初始化

2.指针越界访问

3.指针指向的空间释放

6.2 如何规避野指针

6.2.1 指针初始化

6.2.2 小心指针越界

6.2.3 指针变量不再使用时,即使设置NULL

 七、assert断言

7.1 assert是什么

7.2 assert的优缺点 


一、内存和指针

再将内存和指针之前,我想引入一个生活中的例子: 

  •   内存可以类比为一个巨大的仓库,里面有许多存储单元,每个单元都有一个唯一的地址。这些存储单元就像仓库中的货架,每个货架都有一个编号,方便我们找到存放的物品。
  •   指针则类似于仓库的管理员,他手里有一张清单,清单上记录了每个货架的编号以及货架上存放的物品。管理员通过这张清单可以快速找到某个物品的位置,而不需要逐个货架去查找。

1.1 指针的使用场景

假设仓库中有多个货架,每个货架上存放着不同的物品。管理员(指针)可以通过清单(指针变量)找到某个货架(内存地址),并查看或修改货架上的物品(数据)。 

#include <stdio.h>
int main
{int a = 10;  // 在某个货架上存放了数字10int *p = &a; // 管理员记录下这个货架的编号*p = 20;     // 管理员找到这个货架,并将上面的数字改为20retrun 0;
}

二、指针变量和地址

2.1 取地址符(&)

 c语言中创建变量其实就是想内存申请空间,比如:

#include <stdio.h>
int main()
{int a = 10;//向内存申请4个字节的空间,存放10进去。return 0;
}

而我们应该如何得到地址呢?

#include <stdio.h>
int main()
{int a = 10;&a;//使用“&”,取出a的地址printf("%p\n", a);//%p 地址return 0;
}

2.2指针变量和解引用操作符(*)

2.2.1 指针变量

当我们通过&符号来拿到的地址其实是一个数值,比如0000000A,这个值如果我们想存储起来,那我们应该该如何把这个值给存储到一个合适的地方呢,该存在哪?

c语言中,像这么一个问题,我们一个存入指针变量中

#include <stdio.h>
int main()
{int a = 10;int *pa = &a;//取出a的地址并存储到指针变量pa中  &a指向*pareturn 0;
}

 这种变量(指针变量)是用来存放指针的,存放在指针变量中的值都会理解为地址。

2.2.2 如何拆解指针类型

  我们看到上面的代码:

int *pa = &a;

  pa的类型是int *,*是在说明pa是指针变量

  int说明pa只想的是整形(int)类型的对象

  • 指针就是地址
  • 指针变量就是变量,是专门用来存放地址的变量
  • 存放在指针变量的值,就是地址

那我们现在写一个char类型ch变量,我们应该放在什么类型当中呢?

char ch = 'w';
pc = &ch;char * pc = &ch;//pc的类型

2.2.3解应用操作符 

  • 我们现在所讲的pc = &ch;,只是讲ch这个地址起来,但是我们存起来之后该怎么使用他呢?
  • 在现实生活中,我们得知一个柜子编号要去取出或者存放物品。而c语言也一样,我们得知了这个数的地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫做解引用操作符(*)
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0;//pa就是a,把a改成0return 0;
}

上面代码中第六行的 int* pa = &a; 就是用了"*"解引用操作符,*pa的意思就是通过pa存放的地址,找到指向的空间,*pa其实就是a变量了;所以 *pa = 0,这个操作符就是把a改成了0.

  那么我们可以通过代码看的更直观一点:

#include <stdio.h>
int main()
{int a = 100;int* pa = &a;printf("%d\n",*pa);*pa = 0;//pa就是a,把a改成0printf("%d\n",a);return 0;
}

2.3 指针变量的大小

前面内容我们了解到在不同输出环境中,指针变量的大小是不同的,例如X86和X64就有所不同

代码如下 :

int main()
{printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(double*));return 0;
}

那么我们在不同环境下有什么差别呢??? 

X64

X86

结论:

  • 32位平台下地址就是32bit位,指针变量大小是4个字节
  • 64位平台下地址就是64bit位,指针变量大小是8个字节

三、指针变量类型的意义

指针变量的大小和类型无关,只要是指针变量,在一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?

3.1 指针的解引用

对比下面两个代码,主要是观察内存的变化:

int main()
{int n = 0x11223344;int* pi = &n;*pi = 0;return 0;
}int main()
{int n = 0x11223344;char* pc = &n;*pc = 0;return 0;
}

大家看到这里可以自己在内存窗口看看他们两个的差别

int: 

 char:

调试我们可以看到代码1的4个字节全部改为0,而代码2只有第一个字节改为0。

结论:

  • 指针的类型决定了对指针解引用的时候有多大权限(例如int是是四个字节)。

3.2 指针+-整数

int main()
{int n = 10;char* pc = (char*)&n;int* pi = &n;printf("%p\n", &n);printf("%p\n", &pc);printf("%p\n", &pc+1);printf("%p\n", &pi);printf("%p\n", &pi+1);printf("%p\n", &n);
}

运行结果如下: 

 

我们可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过的是4个字节。

这就是+1带来的变化,同理-1页是一样。

3.3 void指针

在指针类型中有一个特殊的类型就是void类型,可以理解为无具体类型的指针,这种类型的指针可以接受所有类型的地址。但是也有局限性:他不能+-整数和解引用计算

int main()
{int a = 10;int* pa = &a;double* pc = &a;return 0;}

使用 void*类型的指针接受地址:

#include <stdio.h>int main(){int a = 10;void* pa = &a;void* pc = &a;*pa = 10; *pc = 0;return 0;}

 四、指针计算

  • 指针+-整数
  • 指针-指针
  • 指针的关系运算

4.1指针+-整数

因为数组在内存中是连续存放的,只要知道第一个元素的地址,就可以慢慢找到所有元素。

int arr[10] = {1,2,3,4,5,6,7,8,9,10};

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]);for (i = 0;i <sz;i++){printf("%d ", *(p + i));//p+i就是指针+整数}
}

4.2 指针-指针 

int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}int main()
{printf("%d\n", my_strlen("abc"));return 0;
}

4.3 指针的关系运算

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

 五、const修饰指针

5.1 const修饰变量

变量是可以修改的,如果吧变量的地址交给一个指针变量,通过指针我们也是可以修改这个便来那个的,但是我们如果不想让这个变量被修改,那我们该如何做呢,这时候就引出了const。

int main()
{int a = 1;a = 10;printf("%d", a);const int b = 1;b = 10;printf("%d", b);}

大家可以发现运行时是会报错的,无法修改b的值,这就是const的作用。

但是在这中情况下,我们用另一种方法——使用地址,去修改就可以成功啦

int main()
{const int a = 0;printf("n = %d\n", a);int* p = &a;*p = 10;printf("n = %d\n", a);return 0;
}

我们看到我们a的值确实被修改了,但是我们要想一下,我们既然要使用const来修饰a,那就是不想让a可以被修改,所以上面这个方法对于实践应用来说没有什么意义,那我们该如何让p拿到n的地址也不被修改呢?

1.2 const修饰指针变量 

int *p ;

int const * p;     const放在*左边修饰

int * const P;     const放在*左边修饰

int* const ptr;//ptr 是一个 const 指针,指向 int 类型的数据。
//ptr 的指向不可更改,但可以通过 ptr 修改所指向的数据
const int* const ptr;//ptr 是一个 const 指针,指向 const int 类型的数据。
//既不能通过 ptr 修改所指向的数据,也不能修改 ptr 的指向。

见如下代码:

void test1()
{int n = 10;int m = 20;int* p = &n;*p = 20;p = &m;
}//测试const放在*左边的情况
void test2()
{int n = 10;int m = 20;const int* p = &n;//只限制*p,指针指向的内容不能修改,但不限制p*p = 20;p = &m;
}//测试const放在*的右边情况
void test3()
{int n = 10;int m = 20;int* const p = &n; //只能约束p,指针指向的内容可以通过p来改变,但是指针变量不能被改变*p = 20;p = &m;
}
//测试*的左右两边都有const
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20;//两个都约束p = &m;
}int main()
{test1();test2();test3();test4();return 0;
}

 结论:

  • const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
  • const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

六、野指针

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

6.1 野指针成因

1 指针未初始化

int main()
{int* p;//局部变量指针未初始化,默认为随机值*p = 20;//p就是一个野指针return 0;
}

2.指针越界访问

int main()
{int arr[10] = { 0 };int* p = &arr[0];int i = 0;for (i = 0;i <= 11;i++)//0开始,循环12次{//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}
}

3.指针指向的空间释放

int *test()
{ int n = 100;return &n;
}//结束后n还给操作系统了int main()
{int* p = test();printf("%d\n", *p);return 0;
}

return &n的时候,n的生命周期结束了,那么n就会还给操作系统 

但结合我们之前的博客,在第二行前加上“static”,那么就没问题啦

int *test()
{ static int n = 100;return &n;
}//结束后n还给操作系统了int main()
{int* p = test();printf("%d\n", *p);return 0;
}

6.2 如何规避野指针

6.2.1 指针初始化

  • 如果明确知道指针指向哪里就直接赋值地址(例如int i = 0; int* pi = &i;)
  • 不知道的话可以给指针赋值NULL
  • NULL是c语言中定义的一个标识符常量,值是0(赋一个空值),但这个地址是无法使用的,读写改地址会报错

初始化如下:

int main()
{int num = 10;int* p1 = &num;int* p2 = NULL;return 0;
}

6.2.2 小心指针越界

一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。所以我们申请多少内存就使用多少内存。 

6.2.3 指针变量不再使用时,即使设置NULL

当一块区域不再访问的时候,我们及时将该指针设置为NULL。

只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL

int main()
{int arr[10] = { 0 };int* p = &arr[0];int i = 0;for(i =0;i<10;i++){*(p++) = i;}
//此时p已经越界了,可以把p设置为NULLp = NULL;//下次使用p的时候,判断p不为NULL的时候再使用p = &arr[0];//再次定义,重新获得地址if (p != NULL){//表达式}return 0;
}

 七、assert断言

7.1 assert是什么

在编程中,assert 是一种用于调试的语句,用于验证某个条件是否为真。如果条件为假,assert 会抛出异常(通常是 AssertionError),帮助开发者快速发现逻辑错误。

  assert.h头文件定义了assert(),这个宏通常被称为“断言”

assert(p != NULL);
//验证 p!=NULL 真假——为假不运行,给出报错
#include<assert.h>
int main()
{int* p = NULL;assert(p != NULL);return 0;
}

 

7.2 assert的优缺点 

优点:

  • 它不仅能自动标识文件和出问题的行号,还有一种无需更换代码就能开启或关闭assert()的机制

缺点:

  • 因为引入了额外的检查,会增加了程序的运行时间

http://www.xdnf.cn/news/10272.html

相关文章:

  • c++第四课(基础c)——布尔变量
  • 需求分析文档(PRD)编写指南——结构化定义与标准化写作方法
  • 使用Python绘制节日祝福——以端午节和儿童节为例
  • IPD流程体系-TR3评审要素表
  • Excel如何分开查看工作表方便数据撰写
  • DeepSeek模型微调实战:从数据准备到生产部署全流程指南
  • CRISPR-Cas系统的小型化研究进展-文献精读137
  • 关于镜像如何装进虚拟机
  • [SC]SystemC在CPU/GPU验证中的应用(一)
  • (8)-Fiddler抓包-Fiddler如何设置捕获会话
  • C51单片机
  • hot100 -- 1.哈希系列
  • LeetCode hot100-9
  • 让大模型看得见自己的推理 — KnowTrace结构化知识追踪
  • 时间的基本概念与相关技术三
  • 【六. Java面向对象编程入门指南】
  • HackMyVM-Ephemeral3
  • js数据类型有哪些?它们有什么区别?
  • 吴恩达MCP课程(3):mcp_chatbot
  • NW994NX734美光固态闪存NX737NX740
  • SpringBoot如何实现一个自定义Starter?
  • python创建args命令行分析
  • Halcon
  • 从gitee仓库中恢复IDEA项目某一版本
  • Java基础 Day26
  • NumPy 数组计算:广播机制
  • langchain学习 01
  • enumiax:IAX 协议用户名枚举器!全参数详细教程!Kali Linux教程!
  • Vue 核心技术与实战day06
  • Java并发编程实战 Day 2:线程安全与synchronized关键字