马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 万千只cnm 于 2021-8-6 14:03 编辑
c指针及其使用详解
众所周知 c语言学习过程当中 指针一直是个重点和难点 ,此帖谈一下我看完《c与指针》等书之后的见解;
希望能帮到大家,内容有不对的欢迎大家指正
目录:
一: 内存中数据是如何存储的
二:指针是如何寻址的
三:数组
:特性
:数组(名)与指针区别
四:类型
:数组名的类型
:指针类型
:类型相融
五:指针运算
六:复杂指针类型/声明
七:函数指针/指针作为参时
八:附:左右值区别(操作符 不懂的童鞋先看这
一
要想彻底理解指针及其概念 ,就得弄清楚数据如何存储的 ,这里涉及到底层的一些知识 推荐一下小甲鱼讲的王爽版本的汇编 : https://www.bilibili.com/video/BV1Rs411c7HG?from=search&seid=5397185164506602840
(满满年代感
1 cpu只认识二进制 不管是数据,还是要执行的代码(也是数据),在内存中就是一连串的二进制数字 CPU和内存之间需要读取或存储数组,之间就需要一个沟通的桥梁,这个桥梁就叫做地址总线(总线有三种:地址/数据/控制)。
示意图
那为什么叫地址总线而不叫数据总线?这是因为CPU在读取内存中的数据时的第一步是先找到数据的地址,有了地址才能把数据的具体位置找到,然后通过数据总线传输。
就像在商店里买东西,先得找到物品,才能付钱买
2
内存中最小的储存单元是一个字节,可以把这一个个字节看成一个个房间,在这个房间中能储存数据,这些房子会整整齐齐地排成一排。
地址与内容
这个时候就有问题了,CPU应该怎么确定数据是储存在哪个房间呢?这个时候就要靠地址了,可以把这个地址看成房间的号码,显而易见的是每一个房间的号码是必须要是不同的
如8086能寻 1MB的 范围地址(段地址加偏移量,类似下标法寻址法)如debug程序中:
debug
如上图最左边 073F:01bd 就是地址(073f是段地址 冒号右边就是偏移地址) ,右边就是地址中所储存的数据(以16进制显示,两个为一字节)
而我们的数据就是存储在这些地址中,对这些地址中的数据进行操作以达到程序的目的
但跟c语言和指针有什么关系呢?
二
1
我们都知道源代码是经过编译器和连接器 才能成为可执行文件方能运行
前面说了cpu是通过地址来寻找所需内容的,当然不认识变量名 ,这里就要提一下符号表: https://blog.csdn.net/qq_30120725/article/details/47064035
一个变量名其实就相当于一个符号,代表一个具体的地址位置 注意:代表一个位置(地址),地址里存放的数据(内容)与地址本身不同
编译时自动转换为内存中相应的具体地址。
举个列子 我们先写一个源程序
源代码
生成汇编程序:
汇编程序
可以看到并没有出现a这个变量名。
而是在栈段中操作,有同学就要问了,这也看不到地址呀?
众所周知 自动变量就是储存在栈中,栈当然也是内存中的地址啦,栈中数据通过ss sp两寄存器来对数据进行操作 ,相当于数组通过下标来寻址
这个例子中a就在栈顶以下8个内存单元 ,把666这个值“赋值”给对应位置(地址 movl $666, -8(%rbp) //地址为栈顶以下八个内存单元
2 当我们定义一个变量 我们只知道 b的值为666 怎么知道b变量的地址呢 ?
------- 在c语言中 我们可以用 &操作服来获取变量本身的地址
重点来了,当我们定义一个指针变量并初始化 我们把取了整型变量b的本身地址(不是里面的值
就可以说 指向整型类型的指针p 指向了整型变量b
3
综上
任何变量都有其本身的地址,还有其里面的内容(除放在register中的变量
假设 变量b的地址为:0x123 指针变量p的地址为0x678
p指向b时:
指针里的内容
(纯手画
可以看到 0x678这个地址当中的内容 是0x123,也就是a本身的地址,而不是a的值(内容)666
那我们如何通过指针 获取指向变量里的值呢? 当然是通过解引用
访问数据
正如 :此时 *p 表达式的值就是 a里的内容整型值666了
我们无需知道指针本身的地址,只需要知道指针里存放的内容,也就是指针的指向 (如 0x678 对我们并不重要,只需要知道指向0x123
看到这大家有没有理解指针呢?
三(数组
c语言中 指针与数组密不可分,也很容易混淆
数组就是数组 ,指针就是指针,不可以混为一谈
1
数组的特性:
数组里的元素类型是统一的
数组是可以随机访问的,里面的元素也是匿名的
--- 可以通过(且只能) 首地址+偏移地址 来访问数组里的元素 如array【2】== *(array+2)
2
提供一种理解 多维数组/复杂数组的角度 :
c语言的数组都是一维的,只不过元素可以是任意类型
如: 可以理解为: array是一个拥有两个元素的一维数组 里面元素的类型 是一个拥有三个int型元素的的数组
3
数组与指针的区别:
不同:
a: 数组在定义时就分配好所有空间 ,而指针只分配了本身的空间 int arrar [10]; //<span style="background-color: rgb(255, 255, 255);">分配好了 10 *sizeof(int) 字节的空间</span>
指针只为自身分配空间,而不会为指向的对象分配空间
b: 数组 :保存数据 指针:保存数据的地址
数组名就相当于一个符号,代表了首地址,里面元素线性排布
c:取值的方式不同
符号(变量)本身的地址编译时就可以知道 ,而符号里的内容运行时才可以知道
指针:
1 .取本身地址里面的内容 (指针分配了自身的空间
2. 对值(内容)进行解引用
数组:
1. 直接对本身地址进行解引用
------------------
可能有同学要问了:啥意思呀有什么区别?
举个列子
在一个源文件中 另一个源文件中extern int *array;
*array = 0;
编译器会认为是个指针
取array地址,访问里面内容 ----整型1 -->会认为整型1 是要访问的地址!
会将内存地址为 1的空间进行赋值 ,非常危险!!!!!
d:数组名是不可修改的左值
数组名能修改里面的元素不就找不到了
相同:
表达式中数组名就是指针,因为最终代码都是具体地址
如: int array[10];
int *p = array;
通过数组名和指针访问都是可行的 ,都是通过指针类型的运算获得元素的地址进行访问
-----
这个结论对于牛角尖的同学来说确实有列外:就是把数组当作整体来看
1 数组名作为sizeof 操作数时 ,此时是整个数组的大小
2 使用 &取数组的地址时
3 数组是一个字符串常量初始值
所谓指针的类型-- 就是指向 ~~ 的指针 ,可以构建非常复杂类型的指针
当然数组(名)也有类型
1
结论:
数组名的类型就是 指向其元素类型的指针
如
int array[2][3]; //一维数组 ,里面元素 是包含3个整型的数组
int (*p1)[3];// 数组指针
此时可以赋值,类型相同
array数组里的元素 是一个数组 --> array的类型就是 指向数组的指针!
注: 变量作为左值和右值 所代表的意义不同(详看第十节
2
附: 两个指针类型是否相融(是否可以进行赋值
要满足两点 :
1. 指针 指向的类型要相融 (一个指向char 一个指向int 当然不行
2. 等式左边指针 所指向的类型 必须具有右边指针 所指向类型的 全部限定符(const等
如:#include <stdio.h>
void fun(const char **p){
;
}
int main(int argc, char **argv) {
fun(argv);
return 0;
}
是否会发出警告呢,错在哪了 (不要上机测试)
欢迎评论
3
指针常量
用一个字符串常量给指针赋值 字符串常量的值是一个地址!且放在静态存储区 ----->p指向的内容是不可以修改的,本身可以修改
五(指针的运算
大部分类型变量都可以进行运算,指针也不列外
1
先说一下“步长” :
所谓步长,就是 指向类型的宽度(长度) 我们把array当作一个指针来看 --》 指向整型的指针
这里一个步长就是sizeof int 字节
2
运算:
加法运算(与整型) : 数组匿名访问 随机访问的基础 ,语法糖的原理
指针(数组名) +1 --> 地址+一个步长int array[10]={0};
array[2];
这里 array[2] 就是 : *(array +2) --》
1.取首地址
2.加上 2*sizeof(步长) ,这里就是地址+8(假设一个整型4字节) 指向后俩个元素
3.解引用进行访问
注:array也可以用指针替代 这也是数组为什么只能省略第一维的原因 :要知道元素步长 以通过运算访问
减法(指针之间):
指针减法需满足
同一个数组之间 && 指针类型相同 --> 返回的是其元素相隔的距离(会自动除以步长
-------------------
其他运算符(如加法) ,没有任何意义
六(复杂 指针/声明
指针与数组可以组合成非常恐怖的声明
但如果你看懂了上面几节 ,如指针数组 数组指针 等就会迎刃而解
复杂声明:
https://blog.csdn.net/wangweixaut061/article/details/6549768
指针数组等:
https://blog.csdn.net/warrior_harlan/article/details/78556346
总结小秘诀 :
1:复杂定义中 只要看见了 (*符号) -->就是一个指针
2: 声明转化为表达式 的结果类型 就是声明的类型
如 (*符号)-->是个指针 (*(*a)(int *))[5] 表达式的值就是int 类型 ,这也是c语言中非常有趣的现象
a到底是什么指针呢 欢迎评论
3 定义只需要 去掉符号,外面加一层括号--》 就是强制转换啦
七(函数/数组/指针
1
函数调用时其实 也是通过函数指针来完成的
int f(int );
int (*pf)(int) = &f;
// int (*pf)(int) = f; 也可以
函数名在被使用时,总是由编译器转换为函数指针,指向内存中的代码块
int ans = f(25);
int ans = (*pf)(25); //这三种都可以
int ans = pf(25);
这里 f被转换为指针
2
当我们向函数传数组时 :
char str[20]="fishc";
printf("%p \n%s\n",str,str);
输出:
结果
有同学就问了: 为什么一个标识符能打印两种不同结果呢 ?
----当数组作为函数参数时 都会被转换为指向元素的指针
形参就是真的指针,而不是当作;
实参 char str[10][5]; 匹配形参 char(*)[10];
char *str[10]; char **;
所以这就是
gets函数会溢出的原因 : 传数组,实际是指针 -->函数无法通过指针检查到数组边界!!!
八(左右值)
所谓左右值 就是出现在 = 左右两边
左边叫左值,右边叫右值(废话
https://fishc.com.cn/forum.php?mod=viewthread&tid=69833&extra=page%3D1%26filter%3Dtypeid%26typeid%3D571
1
能否作为左值 关键是:
符号能否代表一个具体位置(地址)
如数组名,就可以当左值(不能修改的左值),代表数组的首地址
如整型 666,不能当左值 ,只是个值(常量)!!
2
变量当左值与右值时 , 意义不一样 当整型变量a做右值 -->代表的是符号a地址当中的 值
------
当a做左值 --> 此时符号a代表本身的位置(地址) 要把值123存到a代表的地址中
我们只关心a的地址 来存放数据
3
操作符 来判断左右值
表达式是由许多操作符和符号组成的--》 一个表达式的值能否成为左右值 可以依据操作符
总结:
[] 下标访问
. 成员访问
-> 指针成员访问
* 间接访问
这四个操作符的表达式的值 能做左值(代表一个位置
--------------------------------
而其他操作符 如 : ++ * / 等
结果(值)都是一个值!! 只能当右值
最后: 本人水平有限希望能帮助大家,有什么错误或不理解地方都可以提问,我都会回答
也祝各位鱼友越来越牛,网站越办越好!
|