C语言——指针(1)
时间:2024-03-27 14:10:46 来源:网络cs 作者:亙句 栏目:卖家故事 阅读:
1. 内存和地址
1.1 内存
在了解内存之前,我们先举生活中的一个例子。假如我们要在一栋公寓中找一位朋友,如果我们一个一个房间的找,这样会很浪费时间,但如果我们知道房号,那我们便能快速精准的找到我们的朋友。
如果把上面的例子对照到计算机中,又是怎么样呢?
我们知道计算机CPU在处理数据时,需要的数据要从内存中读取获得,处理后的数据也会放回内存中。那么,计算机是如何从内存中获取数据的呢?我们平常买电脑的时候,电脑内存是8GB/16GB/32GB等,这些内存空间是如何进行有效的管理呢?
其实也是把内存分为一个个内存单元,每个内存单元的大小取1个字节,每个内存单元都有独属于自己的内存编号。
计算机常见单位补充:
一个比特位可以存储一个二进制位0或者1。
1. bit---比特位 1 byte = 8 bit
2. byte--字节 1 KB = 1024 byte
3. KB 1 MB =1024 KB
4.MB 1 GB = 1024 MB
5.GB 1 TB = 1024 GB
6.TB 1 PB ==1024 TB
7.PB
其中一个内存单元的大小为一个字节,可以存8个比特位,就好比一个八人间宿舍,一间宿舍可以住8个人。
每个内存单元都有自己的编号,有了这个内存编号,CPU就可以迅速找到一个内存空间。
在生活中,我们把门牌号叫做地址,但在C语言中,给其起了一个特殊的名字--指针。
我们可以特殊理解为:
门牌号==地址==指针
图示: 0XFFFFFFF是内存编号。
1.2 如何理解编址
CPU要访问内存中的某个内存单元,就必须要知道这个内存单元在内存的哪一个位置,由于内存中的内存单元有很多个,为了方便寻找,我们要给内存单元编址。
计算机中的编址,并不是把每个内存单元的地址记录下来的,而是通过硬件完成的。
以生活中的钢琴为例,钢琴上没有“嘟擂咪发嗦啦”这样的信息,但未何演奏者就能准确的弹琴呢?
这是因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质上是一种约定出来的事实。
硬件编址也是如此。硬件编址是是以CPU所知道的位置对内存的空间进行编址的。
首先,我们必须了解计算机中是有很多硬件的,而硬件之间是要相互协助的,所谓协助,至少相互之间要进行相互之间的数据传递。
但是硬件与硬件之间是相互独立的,那它们是如何进行联系的呢?答案很简单,就是通过"线”来连接。
而CPU与内存之间也是有大量的数据进行交互的。CPU与内存之间也是通过“线”来连接的。
不过我们今天关注一组线,叫做地址总线。
图:
解释:32位机器有32根地址总线,每根线只有两种状态,1或者0(电脉冲有无),那么1根线就能表示1中含义,两根线就能表示4种含义,32根线就能表示2^32种含义,每一种含义都能代表一个地址 。
首先,我们通过控制总线对计算机下读数据或者写数据的命令,将命令传给CPU,CPU在通过数据总线寻找要读取的数据或者要写的数据在内存中的地址,将地址信息传给内存,在内存上就可以找到该地址对应的数据,将数据通过数据总线传入到CPU内寄存器中。
2. 指针变量和指针
2.1 取地址符号操作符(&)
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量本质上就是向内存申请空间,比如:
比如,上述的代码就是创建了整形变量a,其向内存申请了4个字节的空间,用于存放10,其中每一个字节都有属于自己的地址,上图中4个字节的地址分别是:
1. 0x006FFD70
2. 0x006FFD71
3. 0x006FFD72
4. 0x006FFD73
其中0x006FFD70就是变量a的地址。
那我们要如何获取变量a的地址呢?这里我们就要用到取地址操作符---&。
#include <stdio.h>int main(){ int a = 10; &a;//取出a的地址 printf("%p\n", &a); return 0;}
按照我画图的例子,会打印:006FFD70
&a 取出的是变量a所占的4个字节中地址较小的字节的地址。
2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
我们通过取址操作符(&)拿的的地址是一串数值,有时候我们也要将其存储起来方便后期使用,因为它是一个地址,不能用创建普通类型来存储,所以在C语言中规定,将地址值存储到指针变量中。
#include <stdio.h>int main(){ int a = 10; int* pa = &a;//取出a的地址并存储到指针变量pa中 return 0;}
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会被理解为地址。
2.2.1 如何拆解指针变量
我们看到pa的类型是 int* ,那我们该如何理解指针的类型呢?
int* pa 中的 * 是在说明pa是一个指针变量,int 则在说明pa是一个指向整形的指针。
图解
2.2.3 解引用操作符——*
我们将地址保存起来,未来是需要使用的,但我们要如何使用呢?
在C语言中我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里我们必须学习一个操作符------ 解引用操作符--* 。
#include <stdio.h>int main(){int a = 10;printf("a未改变前的值为%d\n", a);int* pa = &a;*pa = 0;printf("a改变后的值:%d", a);return 0;}
上述代码第7行就使用了解引用操作符,*pa的意思就是通过pa存放的地址,找到指向的空间,*pa本质上就是变量a了,*pa=0也就等于a=0 。
如下图所示
这时候我们就很疑惑,如果想把a变为10,那直接写a=10不就好了吗? 为什么偏偏要用一个指针变量呢?
其实这里a的值的修改是交给pa来操作的,这样对a的修改就多了一种方法,写代码就更加灵活了,存在即合理(除了蚊子),后期我们慢慢就能理解了。
2.3 指针变量的大小
前面的内用我们了解到,32为机器就有32根地址总线,每根地址地址线出来的电信号转换成数字信号后是0或1,那我们把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit,需要4个字节才能存储。
所以如果在32位机器中,用指针变量来存储地址,那么这个指针变量的大小必须是4个字节大小才可以。
同理,在64位机器中,一个地址就是64根地址线产生的二进制序列组成的,存储起来就要8个字节,所以指针变量的大小就应该是8个字节。
int main(){printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(double*));}
结论:
1.32位平台下地址是32个bit位,指针变量的大小为4个字节。
2.64位平台下地址是64个bit位,指针变量的大小为8个字节。
3.指针变量的大小是与指针类型无关的,只与平台类型有关,在相同的平台下,指针类型的大小都是相同的。
3.指针变量类型的意义
指针变量的大小与类型无关,只要在相同平台,指针变量的大小都是一样的,为什么还要各种各样的指针类型呢?
其实指针类型也是有特殊意义的。接下来让我们来学习一下。
3.1 指针的解引用
对比下面两段代码,主要在调试时观察内存的变化。
#include <stdio.h>int main(){ int n = 0x11223344; int *pi = &n; *pi = 0; return 0;}
#include <stdio.h>int main(){ int n = 0x11223344; char *pc = (char *)&n; *pc = 0; return 0;}
通过调试我们发现,代码1会将n的4个字节全部改为0,但代码2只会将n的1个字节改为0.
结论:通过以上例子可知,指针类型决定了,对指针进行解引用时的访问权限有多大,也就是一次可以操作多少个字节。
比如:int* 类型的指针一次能访问4个字节,而char* 类型的指针一次只能访问1个字节。
3.2 指针+-整数
观察以下代码,调试观察地址的变化
#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个字节。这就是指针变量类型带来的变化。
结论:指针的类型决定了指针向前或向后走一步的距离有多大。
3.3 void*指针
在指针类型中有一种特殊的类型是void*类型,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型的地址。但是也有局限性,void* 类型的指针无法进行指针的+-运算和解引用的运算。
#include <stdio.h>int main(){ int a = 10; int* pa = &a; char* pc = &a; return 0;}
在上面的代码中,将一个int类型变量的地址赋值给一个char*类型的指针变量。编译器就会爆出一个警告如下图:
这就是因为类型不兼容导致的,而使用void*类型就不会有这样的问题。
使用void*类型接收地址:
#include <stdio.h>int main(){ int a = 10; void* pa = &a; void* pc = &a; *pa = 10; *pc = 0; return 0;}
运行代码:
这里可以看出,void* 类型的指针可以接收不同类型的指针,但是无法进行指针运算。
那么void*类型的指针有什么作用呢?
一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的指针,这样的设计可以实现泛型编程的效果。
4.const修饰指针变量
4.1 const修饰变量
变量是可以修改的,如果把这个变量的地址交给一个指针变量,通过指针变量就可以改变这个变量的值。
如:
#include <stdio.h>int main(){ int m = 0; m = 20;//m是可以修改的 const int n = 0; n = 20;//n是不能被修改的 return 0;}
不通过指针变量,用const修饰的n是无法改变的。
但是如:
#include <stdio.h>int main(){ const int n = 0; printf("n = %d\n", n); int*p = &n; *p = 20; printf("n = %d\n", n);return 0;}
如果我们绕过n,通过n的地址,使用指针是可以成功改变n的值的。
需要注意的是:虽然n被const修饰,但n还是个变量。
4.2 const修饰指针变量
const修饰指针变量的时候有两种情况,分别是*在const的左边或者在const的右边。
4.2.1 const在*右边
当const在*右边时,如 int *const a,const修饰的时指针变量本身,保证了指针变量本身的内容无法被改变,但是a指向的内容是可以被改变的。
4.2.2 const在*左边
当const在*左边时,如int const * a,这是可以将*a看成一个整体,这时const修饰的时指针a指向的内容,而不是指针本身,所以这时候,指针本身的内容是可以改变的,但是这时候,指针所指向的内容是无法被改变的。
4.2.3 const在*两边都有
当*两边都有const修饰时,这种情况下,指针本身的内容也无法被改变,指针指向的内容也无法被改变。
5.指针运算
指针的基本运算有三种:
1. 指针 + - 整数
2. 指针 - 指针
3. 指针的关系运算
5.1 指针+ - 整数
指针加减整数我们在前面已经提到过指针的加减,下面我们再通过数组来了解一下指针+-整数。
我们知道数组在内存中是连续存放的,所以只要我们知道第一个元素的地址,我们便能顺藤摸瓜找到其他元素。
#include <stdio.h>int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = &arr[0];int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i)); //这里就是指针加减整数}return 0;}
一开始我们把指针赋值在arr[0]的位置,随着指针进行加上i ,指针p也会随着i的增加去访问后面的元素。从而会打印出数组的各个元素。
5.2 指针 - 指针
下面我们以一段代码为例
#include <stdio.h>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;}
运行如下图
结论:由此我们可以得知指针 - 指针得到的是两个指针之间元素的个数。
5.3 指针的关系运算
指针的关系运算也就是指针之间大小的比较运算。下面我们还是以代码为例
#include <stdio.h>int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = &arr[0];while (p < arr + sz) //指针之间的关系运算{printf("%d ", *p);p++;}}
运行代码
6.野指针
我们知道指针是有指向内容的,但一有疏忽,就可能导致指针指向的内容不明确,这样就导致了野指针的出现。
那野指针如何定义呢?
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针成因
1. 指针未初始化
#include <stdio.h>int main(){ int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0;}
以上代码就是指针未初始化导致了野指针的例子
解释:通过函数栈帧可知,在调用main()函数之后,内存会给main()函数开辟一块函数栈帧,也就是一块空间,在创建完空间之后,编译器就会先给main()函数里面随机给上初始值,就如上面的代码,由于没给指针初始化,所以导致该指针随机指向一个内容,因此导致了野指针的出现。
2 指针越界访问
#include <stdio.h>int main(){ int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0;}
有i=11的时候,指针p就会指向arr[11]的位置,但由于arr数组大小只有10,当指针p指向arr[11]的时候,就没有对其访问的权限,这也变向导致指针没有明确指向的内容,就导致了野指针。
3. 指针指向的空间释放
首先我们得知道我们在调用一个函数,内存会为这个函数开辟一块空间,但在调用完函数之后,这块内存就会被回收到内存中去。
如以下代码
#include <stdio.h>int* test(){ int n = 100; return &n;}int main(){ int*p = test(); printf("%d\n", *p); return 0;}
当我们调用test()这个函数的时候,内存就会为它开辟一块空间,这时候指针是有权限来访问这块空间的,但是当函数调用完之后,为这个函数开辟的空间就会被内存回收,这个时候指针就没有权限访问这块空间了,这样就导致了野指针。
6.2 如何规避野指针
6.2.1 指针初始化
我们在设计指针的时候要记得给指针初始化,也就是给指针赋值,如果明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针赋初值NULL。
NIULL是C语言中定义的一个标识符常量,值为0,0也是地址,但这个地址是无法使用的,读写该地址时会报错。
6.2.2 小心指针越界
一个程序向内存申请了多少空间,指针就能访问多少空间,不能超出访问范围,超出了就是越界访问。
6.2.3 指针变量不再使用时,及时设置NULL,指针使用前检查有效性
当指针变量指向一块区时,我们可以通过指针访问该区域,后期不在使用指针的时候,我们要及时将其设置为NULL。
因为有一个约定熟成的规则:
只要是NULL指针就不去访问,同时使用前可以判断指针是否为NULL。
例子:
int main(){ int arr[10] = {1,2,3,4,5,67,7,8,9,10}; int *p = &arr[0]; for(i=0; i<10; i++) { *(p++) = i; } //此时p已经越界了,可以把p置为NULL p = NULL; //下次使⽤的时候,判断p不为NULL的时候再使⽤ //... p = &arr[0];//重新让p获得地址 if(p != NULL) //判断 { //... } return 0;}
6.2.4 避免返回局部变量的地址
如造成野指针的第3个例子,不要返回局部变量的地址。
7. 指针的使用和传址调用
7.1 strlen的模拟实现
库函数strlen的功能是求字符串的长度,统计的时 \0 之前的字符个数。
函数原型如下:
参数str接受一个字符串的起始地址,然后开始统计字符串中\0之前字符个数,最终返回长度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。 代码如下:int my_strlen(const char * str){ int count = 0; assert(str); while(*str) { count++; str++; } return count;}int main(){ int len = my_strlen("abcdef"); printf("%d\n", len); return 0;}
7.2 传值调用和传址调用
学习指针的目的是使用指针解决问题,那什么问题非指针不可呢?
例如:写一个函数实现交换两个变量的值
也许我们会这样写
#include <stdio.h>void Swap1(int x, int y){ int tmp = x; x = y; y = tmp;}int main(){ int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(a, b); printf("交换后:a=%d b=%d\n", a, b); return 0;}
但运行代码我们会发现失败了,这是为什么呢?
让我们调试下代码
当main()函数进行传参时,swap()函数会开辟空间来存储形参,由上图可知,a和b的值确实传给了x和y,但a和b的地址分别于x和y不同,这样我们就得知x和y是一块独立的空间 ,所以当我们在swap()函数里面交换它们的值时,是不会影响a和b的。
结论:实参传递给形参时,形参会单独创建一份临时空间来接收实参,对实参的修改不影响实参。
则这种时候我们就可以使用指针了,我们可以通过指针来间接交换a和b的值
如:
#include <stdio.h>void Swap2(int*px, int*py){ int tmp = 0; tmp = *px; *px = *py; *py = tmp;}int main(){ int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0;}
运行代码
这时候发现就成功了。
这种将变量的地址传递给函数,这种函数调用就叫:传址调用。
传址调用,可以让函数于主函数之间建立真正的联系。如果函数内部改变主函数的变量的值,就可以使用传址调用。
本文链接:https://www.kjpai.cn/gushi/2024-03-27/149491.html,文章来源:网络cs,作者:亙句,版权归作者所有,如需转载请注明来源和作者,否则将追究法律责任!