🌈个人主页:是店小二呀
🌈C语言笔记专栏:C语言笔记
🌈C++笔记专栏: C++笔记
🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅
文章目录
**内存和地址(知识铺垫(了解即可))**如何理解编址**正文****指针变量****指针的初始化和赋值运算****指针运算符(& 、*)****指针变量的大小****指针变量类型的意义****指针+-整数**const修饰指针const修饰变量const修饰指针变量**指针运算****野指针****assert断言(assert.h头文件定义了宏assert())****传值调用和传址调用**
内存和地址(知识铺垫(了解即可))
内存(Memory)是计算机的重要部件,也称内存储器和[主存储器]它用于暂时存放CPU中的运算数据,以及与硬盘等[外部存储器]交换的数据。
当CPU(中央处理器)在处理数据的时,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
问题:那么内存空间如何高效的管理的呢?
是将内存划分为一个个的内存单元,每个内存单元的大小取一个字节(一个字节等于八个比特位,比特bit是二级制位(Binary digit)的简称,一个二进制包含的信息量成为一比特bit。)每个内存单元都有一个编号(相当于宿舍房间的门牌号),有这个内存单元的编号,CPU就可以通过这个编号快速找到一个内存空间在计算机中我们把内存单元的编号也称为地址。C语言给地址起了个新名字位指针,这里解释了计算机中内存是按照字节编址的,也是每个字节都有唯一的地址,而对于比特是没有地址可以理解为:内存单元的编号==地址 ==指针;如何理解编址
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,因为内存中有很多字节,所以需要给内存进行编制,计算机中的编址不是把每个字节的地址都记录下,而是通过硬件设计完成。
钢琴、吉他上面没有写上“都瑞咪发嗦啦”这样 的信息,但演奏者照样能够准确找到每⼀个琴弦的每⼀个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是⼀种约定出来的共识!
首先,计算机内是有很多硬件单元的,而硬件单元是要相互协同工作,既然是协同工作,至少之间要能够进行数据传递,但是硬件和硬件之间是互相独立的,那么如何通信呢?那么需要通过"线"连起来,那么CPU和内存之间也是有大量的数据交互,所以这两者必须也用线连起来。
这里只关注一组线:地址总线
可以简单理解为32位机器有32根地址总线,那么每根线只有两态,表示0,1【电脉冲有无】,一根线能代表两种含义,两根线就能表示四种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。
地址信息被下达给内存,在内存上,就可以找到地址对应的数据,将数据在通过数据总线传入CPU内寄存器中
正文
指针变量
了解内存和地址的关系,可知创建变量需要向内存申请一定大小的空间
指针变量时用于存放其他变量的地址(其他变量在内存中存储的位置),简称指针。指针本身是一种变量,需要占用一定大小的空间的,用来存放指针值(指针变量本身的地址)。
指针定义说明的一般形式:
类型说明符(void) *指针变量名(name);*表示pa是一个指针变量类型说明符表示指针变量指向什么类型的对象
比如:
int p=1;char q='a';int *pa=&p;说明指针变量pa是指向int类型变量的指针char *qa=&q;说明指针变量qa是指向char类型变量的指针
注意:
指针变量值表达的是某个数据对象的地址,只允许取正的整数值的。但是他不等同于整形类型变量,如果指针变量取0值,即为NULL(空),则表示指针指向对象不存在,为空指针指针的初始化和赋值运算
类型说明符 指针变量名=初始化地址值
注意:
初始化中也是将初始地址赋值给指针变量在赋值语句中,变量的地址也只能赋值给指针变量,这种赋值运算操作限制在同类之间指针运算符(& 、*)
取地址操作符 &:返回存放其他变量的内存地址(只限于一个具体的变量或数值元素,不可用于表达式)
int p=1;int *pa=&p;//这里pa存放p的地址
*解引用操作符 :返回地址(指针标量指向的地址)中的变量值
int p=1;int *pa=&p;//这里pa存放p的地址int ret=*pa//ret==1
这里样对于变量的修改,多了一种途径,写代码就会更加灵活
指针变量的大小
通过前面“内存和地址”,32位机器有32根地址总线,每根地址线出来的电信号转换成数字信号后(1或0),将32根地址线产生的二级制序列当做一个地址,那么一个地址需要32个bit位,也是4个字节存储。那么在64位机器,有64根地址总线,一个的地址就是64个二级制位组成的二级制序列,那么一个需要地址需要64个bit位,也是8个字节存储。
小总结:
指针变量是用来存放地址的,那么在不同机器下,地址的大小也会影响指针变量的大小
32位平台下地址是32个bit位,指针变量大小是4个字节64位平台下地址是64位bit位,指针变量大小是8个字节指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的指针变量类型的意义
既然指针变量的大小与指向的类型无关,那么为什么要区分各种指针类型呢?
那当然是因为指针类型在其他方面有特殊的意义
#include <stdio.h>int main(){ int n=0x11223344; int *p1=&n; *p=0; int m=0x11223344; char *p2=(char *)&m;//原本&m 应该由int *类型存储 *pc=0; return 0;}这里涉及到了大小端的知识,理解指针类型对解引用的影响
如图可得的,n的四个字节全部改为0,而m只是第一个字节改为0
结论:指针的类型决定,对指针解引用能修改几个字节的权利
拿上面的例子:
n是int 类型(占四个字节,其中两个数字算一个字节),int *指针类型指向n的地址,*当解引用的时候,int 指针只能访问四个字节
m是char 类型(占一个字节,其中两个数字算一个字节),char *指针类型指向m的地址,*当解引用的时候,char 指针只能访问一个字节(修改44)
指针±整数
代码:
#include <stdio.h>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);return 0;}
结论:指针类型决定了指针向前或者向后走一步有多大(距离)。char *类型的指针变量+1跳过1个字节,int *类型的指针变量+1跳过了4个
const修饰指针
const修饰变量
如果不希望这个变量被修改,可以使用const进行限制。
int main(){ const int m=0; m=1;//ok?}
上边的代码:使用const修饰(语法上加了限制),只要对n变量进行修改,就不符合语法规则,就会报错,对于const修饰后的变量是不能进行直接修改的.
通过上面刚学的一种修改变量的方法,通过n的地址取修改n值(打破语法规则)
int main(){ const int m=0; int *p=&m; *p=20; return 0;}
这样子n变量还能被修改,const使用的就没有多大意义,这样子不是自己打自己脸吗?那么有什么办法p拿到了n的地址也不能修改n值(对指针下手)。
const修饰指针变量
int main(){ int m=0; int n=1; 第一种: const int *pa=&m; pa=&n; *pa=10;//右边为不可修改值 第二种: int * const pa=&m; pa=&n;//左边为不可修改值 *pa=10;}
总结:当const修饰指针变量的时候
const放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变const放在*的右边,修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变指针运算
指针的基本运算有三种
1.指针±整数(在连续存放的数据,只要知道第一个元素的地址,就可以知道后面所有的地址),这里整数也称为偏移量
数组 int main(){ int nums[]={1,2,3,4,5}; int *p=nums;//首元素的地址 printf("%d",*(p+2));//那么p的偏移量为+2,打印结果是3 return 0;}
2.指针-指针
int main() { char *p="abcd";//不要忘记还有'\0' char *s=p; while(*p!='\0') { p++; } int ret=p-s;//4 printf("%d",ret); return 0;}
结论:指针减指针表示指向两个指针之间的元素个数,求首元素到某个元素之间相错个数(在连续存储的情况下)
3.指针的关系运算
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;
野指针
野指针;指针指向位置是不可知的(具有不确定因素)
下列有几种野指针的成因:
1.指针未初始化int *p;
2.指针越界访问(数组越界访问)
int nums[10]={0};int *p=nums;for(int i=0;i<11;i<++){ *(p++)=i;}
3.指针指向的空间释放
int* test(){ int m=0; return &m}int mian(){ int *p=test(); printf("%d\n",*p); return 0;}m是一个被销毁的局部变量,这里指针指向m所在的位置空间是不明确的,原本属于m的空间,可能被给了其他变量占用,这样子就导致程序可能不能达到预期效果
规避野指针
1.确定指针指向一片有效的空间,如果指针目前没有指向,可以为指针赋值为NULL,NULL是一个定义的标识符常量,值为0,0也是地址,这个地址是无法使用的
2.当指针变量不再使用,设置为NULL,指针使用之前检查有效性(判断语句或者断言)
3.规定:只要指针为NULL就不去访问,但是给野指针赋值为NULL,将野指针暂时管理起来,还是存在危险的。
assert断言(assert.h头文件定义了宏assert())
作用:当符合程序符合指定条件,没有啥影响,如何不符合条件,就会报错终止运行。而这个宏常称为断言。
具体使用细节:
#inlclude <assert.h>int p=0assert(p!=0);
assert()宏接受一个表达式作为参数。如果表达式为真(返回值为非零),没有啥影响,程序继续执行。如果表达式为假(返回值为零),assert()就会报错,在标准报错流中stderr中写入一条错误信息(显示没有通过的表达式、表达式文件名和行号) 好处:
自动标识文件和出问题的行号存在一种无需更改代码就能开启或关闭assert()的机制,就是在#include <assert.h>
语句的前面,定义一个宏NDEBUG
#define NDEBUG#include <stdio.h>
对于宏NEDBUG
使用时,编译器就会禁用文件中assert()语句
。如果程序出现问题,可以注释掉,就可以重新启动assert()语句
不足:assert()
的引入了额外的检查,增加了程序的运行时间
debug
和release
版本下,一般debug
调试中使用,在release(发布版本)
选择禁用assert
,提高程序效率。在VS这样的集成开发环境中,release
版本中,直接优化掉了。 总结:debug版本有利于程序员排查问题,release版本不行用户使用程序的效率
传值调用和传址调用
在函数章节,提到形参是实参的一份临时拷贝,形参的改变不会影响到实参
#include <stdio.h>void Swap(int x,int y){ int tmp=x; x=y; y=tmp;}int main(){ int a=10;int b=20;Swap(a,b);}
这里x和y的值等于a和b的值,但是各属于独立的空间,那么x和y值交换,不会影响到实参a和b值的交换
对于这种将变量的数值传递给函数,调用方法:传值调用
对此可以使用指针
#include <stdio.h>void Swap(int *x,int *y){ int tmp=*x; *x=*y; *y=tmp;}int main(){ int a=10;int b=20;Swap(&a,&b);}
这里在main函数中将a和b的地址传递给了Swap函数,Swap函数通过地址间接的操作main函数中的a和b
对于这种将变量的地址传递给函数,调用方法:传址调用
谢谢大家的观看,这里是个人笔记,希望对你学习C有帮助。