深入理解指针1
一.内存和地址以及指针间的关系
举一个生活中的例子,假如你去找你的朋友玩,你的朋友告诉了你酒店的名字,但是没有告诉告诉你他具体住哪一件房间,于是你为了找到你的朋友,只好一间房间一间房间的去找,但是如果告诉了你房间号,那你就可以很快的找到你的朋友,所以在生活中有了具体的地址,就可以很快的提高效率。
那么在计算机中也是一样的,计算机的 cpu 处理的数据都是在内存中获取的,处理好的数据,也会放到内存里,那么内存是如何管理自己的数据的?其实,++内存里会划分很多个内存单元,每一个内存单元占一个字节空间++,也就是8个比特位,每个内存单元都会有自己的编号,而这个内存单元编号就相当于酒店的房间号,也就是地址,有了地址,我们就可以访问 cpu里的数据。我们可以把内存看成一个酒店,内存单元
总结:
我们可以把内存看成一个酒店,内存单元看成酒店里的每个房间,房间号看成内存单元的编号,这个房间号就是一个地址,而房间号=内存编号=地址=指针,所以指针就是地址。
1.内存中常见的单位:
c
bit ------ 比特位
byte ------ 字节
KB
MB
GB
TB
PB
1byte=8bit
1KB=1024B
1MB=1024KB
1GB=1024MB
1TB=1024GB
1PB=1024TB
2.内存究竟是如何编址的呢?
cpu 访问内存中的数据前,必须要知道它的地址,因为内存中很多字节,所以要给内存进行编址,就相当于酒店里有很多间房间,为了方便管理,我们需要给房间编上号码。那么,内存是如何进行编址的呢?
计算机中的编址是通过硬件设备完成的。我们都知道计算机中有很多硬件设备,它们的通信就是用线连接起来的,有数据总线,地址总线,控制总线,但是今天我们只讨论地址总线,因为地址总线是用来传输内存单元编号的,假设你的电脑是32位的,那么就有32根地址总线,每一根地址总线传输一个比特位,每根线表示两种含义,0或1,那么两根地址总线就可以表示2^2^种含义,那么32根地址总线就可以表示2^32^种含义,
地址总线将地址传给内存,内存找到该地所对应的数据,再通过数据总线将数据传给 cpu 内寄存器。
二.指针变量和地址
1. 取地址操作符&
c语言中创建变量的本质就是在内存中申请一块内存空间,就相当于你去酒店住房,需要找前台给你开一个房间,比如
int a=10;
,就是向内存中申请4个字节的空间,用来存放10,每一个字节都有对应的地址,注意!!!&取地址操作符要与按位与操作符&区分开来,按位与操作符是双目符,也就是有2个操作数,而取地址操作符是单目操作符,他的意思是取内存单元中的编号,也就是地址,即指针。
2.指针变量
&取地址操作符取出来的地址也是1个数值,有的时候我们想把这个数值存储起来,方便后期使用,那么我们就可以存到指针变量里,这个指针变量就是专门用来存放地址的,换句话说,存在指针变量里的值,都会被当作地址使用。
3.如何创建指针变量
c
int main()
{
int a = 10;
int *p = &a; //把a的内存单元编号,即地址取出来,然后存放到指针变量p里面
//int*是变量p的类型
//int表示指针变量所指向的地址里面的数据是整型
//*解引用操作符,表示p是指针变量
//总结:创建指针变量的时候要说明 指针变量类型* 变量名
return 0;
}
c
char ch='a';
char* p=&a;//把a的地址取出来,然后赋值给指针变量p
printf("%p",p);
//char*是变量p的类型,*表示p是一个指针变量,char表示指针指向一个字符类型的数据
4.解引用操作符*(间接访问操作符)
我们将地址保存了起来,以后如果想使用这个地址访问到他里面存放的数据,怎么操作呢?就好比你找到了酒店房间的地址,你想进这个房间拿里面的物品,或打扫卫生,但是你没有钥匙。所以接下来我们需要了解一个操作符,解引用操作符('*'),它就相当于一把钥匙,有了他,我们只要拿到了地址,就可以通过地址,找到地址指向的对象。
c
int main()
{
int a = 10;
int* p = &a;//创建指针变量p,存放a的地址
*p = 0;//*p的意思是找到p所指向的内存空间,然后访问里面的数据,把a的值修改成0
printf("%d", a);
return 0;
}
5.指针变量大小
int类型的变量是4个字节,char类型的变量是1个字节,那指针变量占内存多少个字节呢?
sizeof() 操作符计算变量或类型的大小,即在内存中占多少个字节。
c
printf("%d\n",sizeof(int));
printf("%d\n",sizeof(float));
sizeof("%d\n",sizeof(double));
printf("%d\n",sizeof(long));
printf("%d\n",sizeof(char));
判断下面两个指针变量所占内存大小,
c
int a=10;
char ch='a';
int*p=&a;
char*arr=&ch;
printf("%d\n",sizeof(p));
printf("%d\n",sizeof(arr));
printf("%d\n",sizeof(int*));
printf("%d\n",sizeof(char*));
//指针变量p和arr的大小都是一样的,指针变量大小却决于地址大小,如果是32位环境,那么指针变量的大小都是4个字节,如果是64位环境,指针变量大小就是8个字节
指针变量大小与类型无关,取决于地址大小
c
printf("%d\n",sizeof(int*));
printf("%d\n",sizeof(char*));
printf("%d\n",sizeof(float*));
printf("%d\n",sizeof(double*));
printf("%d\n",sizeof(long*));
总结:
c
//指针变量的大小取决于地址的大小,32位平台下,指针大小是4个字节
//64位环境下,指针大小是8个字节,在相同的平台下,指针大小是相同的
6.指针变量类型的意义
既然在相同的平台下,指针大小都是一样的,与指针的类型无关,那么为什么还要有各种类型的指针呢?
其实指针的类型是有意义的,指针变量的类型决定了指针在解引用的时,就是访问指针指向的对象的时候,一次访问几个字节,
1.指针变量的类型决定了指针解引用时的权限
c
//对比下面两组代码,并用调试观察他们在内存中的变化
c
int a=0x11223344;//0x开头表示十六进制
int*p=&a;
*p=0;
printf("%d",a);
c
int a=0x11223344;
char* p2=&a;
*p2=0;
printf("%d",a);
如果是int*类型的,那么指针访问的时候,就会访问4个字节
如果是char *,那么指针访问的时候,就会访问1个字节 ,所以通过解引用操作符将a的值修改成0的时候,因为只拿到第一个字节里的数据,所以只修改第一个字节里的数据。
2.指针变量类型决定了指针的步长
指针变量的类型决定了指针==+1/-1==时,一次跳过几个字节,相当于指针的步长;
*或者说指针变量的类型==+n/-n==时,一次跳过了n个值,如果是char,一次跳过n个字符,如果是int*;一次跳过n个整型。**
7.指针加减整数
c
int main()
{
int a = 10;
char* pc = &a;
int* pi = &a;
printf("%p\n", &a);
printf("%p\n", pc);
printf("%p\n", pi);
printf("char*类型:%p\n", pc+1);
printf("int*类型:%p\n", pi+1);
return 0;
}
c
//这是上面代码的运行结果,我们发现char*类型的指针+1,一次跳过了一个字节,而int*类型指针+1,一次跳过了4个字节
总结:
指针变量的类型决定了指针+n/-n时,一次跳过几个整型或几个字符。
如果是char*类型+1,一次跳过1个字节,跳过一个字符;
如果是int*类型+1,一次跳过4个字节,跳过1个整型
8.指针变量类型的使用
竟然知道了指针类型具有特殊的意义,那么怎么使用呢?
之前遍历数组元素是用的下标的方式,现在学了指针类型的特殊意义,我们可以用指针来访问数组元素
在写代码之前我们要想清楚是一个整型一个整型的访问呢,还是一个字符一个字符的访问呢
c
//访问数组元素之下标方式
c
int arr[]={1,2,3,4,5,6,7,8,9,10};
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
c
//访问数组元素之指针方式,因为数组在内存中是连续存放的,知道了首元素的地址,再通过指针加减整数的方式就可以顺藤摸瓜拿到后面的元素
c
int arr[]={1,2,3,4,5,6,7,8,9,10};
int*p=&arr[0];//指针变量p存储的是数组首元素的地址
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
for(i=0;i<sz;i++)
{
// printf("%d ",*(p+i));//指针加减整数得到的还是地址,要想拿到里面的数据,需要解引用
//另一种写法:
printf("%d ",*p);
p=p+1;
//p+1的结果是个地址,不能写成*p=p+1,*p他里面存储的是一个数据,你不能把地址赋值给他,这样写是错的
//
}
四.const修饰指针
1.const修饰变量
const是c语言的关键字,当const用来修饰变量的时候,在定义的时候必须进行初始化,该变量就变成了一个常量,不能再对其进行赋值操作,修改。
c
#include <stdio.h>
int main() {
const int num = 10;
// 下面这行代码会导致编译错误,因为 num 是只读的
// num = 20;
printf("num 的值是: %d\n", num);
return 0;
}
c
//变量a用const修饰后,不能修改了,但是我们可以用指针中的解引用来修改a的值
c
int main()
{
const int a=10;
int*p=&a;
*p=20;
printf("%d",a);
return 0;}
但是这样做显然是不合理的,我们用const修饰指针a的目的就是为了让a的值不发生修改,如果拿到了a的地址,就可以修改,这就会有很大的风险,那么怎么才能即使拿到了a的地址,也不让a的值被修改呢?
2.const修饰指针变量
const修饰指针时,分两种情况讨论,const在*的左边和右边的代表意义是不同的,
c
当const在*的左边的时候
c
int a=10;
int b=20;
const int *p=&a;
int const *p=&a;//这两种写法都对
*p=20;//会报错,const在*左边时,不能修改指针变量指向的内容
p=&b;//但是可以修改指针变量本身,即指针变量的指向
c
当const在*右边的时候
c
int a=10;
int b=30;
int* const p=&a;
*p=20;//const在*右边的时候,可以修改指针指针变量指向的内容
p=&b;//会报错,但是不能修改指针变量本身,就是里面存的地址
当然,还有一种情况就是*左右两边都有const,这种情况就是即不能修改指针指针变量本身,也不能修改指针指向的内容
c
int main()
{
int a = 110;
int b = 220;
const int* const p = &a;
*p = 20;
p = &b;
return 0;
}
总结:
1.当const在*左边的时候,指针指向的内容不能被修改,但是指针变量本身可以被修改
2.当const在*左边的时候,指针指向的内容可以被修改,但是指针变量本身不能被修改
3.当*左右两边都有const的时候,指针 *变量本身和指针指向的内容都不能被修改
五.指针运算
1.指针加减整数
在数学里,如果500+1就等于501,因为500是一个
int类型
的,而1也是1个int
类型的,所以结果也是一个int类型的
,但是int*类型的指针变量+1
,得到的是地址。指针加减整数其实在前面已经举过例子了,就是用指针的方式访问数组的元素,前提条件是数组在内存中是连续存放的,地址由低到高变化,不光数组这个例子,以后只要遇到连续的的空间,都可以用指针加减整数的方法
c
//前面我们举得是整型数组的例子,下面用字符数组举例
c
#include <stdio.h>
#include <string.h>
int main()
{
char arr[]="abcdef";
int i=0;
int sz=strlen(arr);//统计字符串长度,并不会包括\0
char*p=&arr[0];
for(i=0;i<sz;i++)
{
printf("%c",*p);//对指针变量解引用,就可以拿到指针指向的内容
p++;//指针变量跳过一个字节,即1个字符,指向下一个字符的地址
//另一种写法
// printf("%c",*(p+i));
}
return 0;
}
2.指针- 指针
指针减指针其实就是地址减地址,得到的是一个整型数,
c
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int ret = &arr[5] - &arr[0];//指针减指针得到的指针之间的元素个数
printf("%d", ret);//打印结果为5
return 0;
}
c
//错误代码示例,指针减指针的前提是指针指向的都是同一片空间
c
int main()
{
int arr[10]={0};
char str[11]={0};
int ret=&arr[9]-&str[5];//这样写就不行,首先它们是不同类型的数组,指向的不是同一片空间,其次我们也不知道str数组空间和arr数组空间之间相差多少个元素,这是不确定的
printf("%d",ret);
return 0;
}
总结:
1.指针减指针得到的是一个整型数,是一个绝对值,他表示指针之间相差多少个元素,大地址减小地址得到的是一个正数,小地址减大地址,得到的是一个负数。
2.指针减指针的前提是,指向的空间必须是同一片空间,不然没意义,就像日期-日期=天数,日期加天数都是有意义的,但是日期+日期就没有意义了。
3.指针的关系运算
指针和指针之间是可以比较大小的,指针比较大小就是地址与地址之间比较大小
c
//除了利用指针加减整数来遍历数组元素之外,还可以利用指针的关系运算的方式
c
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
//当元素的地址是否小于第10个元素的地址,意味着还有元素没有访问完,大于等于第10个元素的地址,就表示数组里的元素已经全部访问完了
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//int*p=&arr[0};
while (p<arr+sz)//指针和指针之间进行比较
{
printf("%d ", *p);
p++;//指针+1,跳过一个整型元素
}
return 0;
}
有个规律,
1.数组里,如果想知道某个元素的地址,首元素的地址加上元素对应的下标就可以得到地址;
2.假设数组有n个元素,那么第n+1个元素的下标就等于数组元素的个数
六.野指针
所谓的野指针就是没有没有明确的指向,你可以把他理解为一条野狗,四处为家,是危险的。
1.什么样的情况下会出现野指针
1.指针变量未初始化
c
int a=10;
int*p;//指针变量没有初始化会报错,且他是一个野指针
*p=20;
2.指针越界访问
c
//数组只有10个元素,但是你用指针访问了11个元素,越界了
c
int arr[]={1,2,3,4,5,6,7,8,9,10};
int sz=sizeof(arr)/sizeof(arr[0]);
int i=0;
int *p=arr;
for(i=0;i<=sz;i++)//sz=10,i<=10,意味着当下标为10的时候还可以访问,但是下标为10就已经是第11个元素了
{
printf("%d ",*(p+i));
}
c
//正常情况下说,如果代码没有越界访问的话,就是写成下面的样子
int main()
{
int arr[10] = { 0 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
*p = i;//将每一个元素的值改成对应的下标
p++;//指针跳过1个整形元素
//这样写也是对的
//*(p++) = i;//第一步:*p=i,第二步:p++
}
for (i=0;i<10;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
3.指针指向的空间释放
就是返回栈空间的地址
c
int* test()
{
int n=100;//n是局部变量,它的作用域就是test函数内
return &n;//一旦出了函数体,局部变量向内存申请的空间就没有意义了,已经被回收了,
}
int main()
{
int*p=test();
printf("%d",*p);//那么此时p就变成了野指针,因为n申请的内存空间已经销毁了
return 0;
}
2.如何避免野指针
1.指针初始化
如果不知道指针指向哪里,但又必须初始化,因为如果不初始化,指针就是野指针,我们可以赋值为NULL,
NULL
是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错.
举个例子,如果指针没有初始化就是一条野狗,但是现在我们将指针初始化为NULL,就相当于用绳子把野狗绑到了树上,虽然它看似安全,但是我们也不能在它旁边撒尿,得绕着他走,所以指针赋值为NULL后,就不能使用指针了,即不能访问里面的数据了,直到你为他找到新的地址
c
int main()
{
int a = 10;
int* p;//没有初始化,就是野指针
//如果暂时不知道指针指向哪里,可以赋值为空指针
int* p = NULL;
*p = 100;//但是赋值为空指针后,就不能使用指针p了
//如果确定了指针指向的地址,再重新赋值、
int* p = &a;
return 0;
}
2.小心指针越界
c
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i=0;i<sz;i++)
{
printf("%d ", *(p + i));
}
//循环结束后会来到这,此时i=8,指针已经指向第9个元素了,已经越界了,所以指针属于野指针
p = NULL;//为了避免p变成野指针,赋值为空指针,你可以理解把一条野狗拴在树上
//赋值为空指针后,指针p就不能使用了,除非指向新的地址
return 0;
}
3.避免返回局部变量的地址
返回局部变量地址其实就是返回栈空间地址,因为局部变量是存放在栈里的,
c
int* test()
{
int arr[]={1,2,3,4,5,6};//出了函数体,数组向内存申请的空间就释放了
return arr;//返回首元素地址
}
int main()
{
int*p=test();//定义指针变量p接受函数返回的地址
*p=200;//p此时是野指针,因为他返回的是局部变量的地址
retrun 0;}
4.使用指针前检查下是否是空指针
c
int a=20;
int*p=&a;
if(p!=NULL)//p如果为空指针返回的就是0,0为假;如果不是空指针,返回的就是非0,C语言中非0就是真
{
*P=200;
}
其实还可以用assert断言来判断是否是空指针,更方便,后面有详细介绍
5.不使用指针的时候设置为空指针
c
int a=10;
int*p=&a;
*p=200;
p=NULL;//这意味着p不再指向任何有效的地址,指针p不能在使用了
七.assert断言
在
assert.h
头文件中,包含了宏assert(),这个宏常常被称为断言,用于在运行程序的时候判断程序是否符合某个条件,如果不符合就报错,程序终止运行。assert()的参数是一个表达式,如果表达式为真,则返回非零,则程序正常运行;如果返回的值是0,assert就会报错
如果每次在使用指针前都要用if语句判断是不是空指针,就会很蛮烦,我们可以用assert断言来判断程序中有没有空指针。如果确定程序没有问题,不需要再做断言,就可以在~#include#include <assert.h>
前面定义一个宏NDEBUG
, 他就会禁用程序中所有的assert语句;如果程序又出现问题,那么就可以把#define NDEBUG
这条语句注释掉,这样就重新使用了assert语句。
c
#include <stdio.h>
#include <assert.h>
#define NDEBUG //开启或关闭 assert() 的机制
int main()
{
int a=10;
int*p=&a;
assert(p!=NULL);//指针p如果等于空指针,为0,表达式就为假,程序就会报错
//assert(p);这样写也是对的
*p=20;
return 0;}
八.传值调用和传址调用
1.传值调用
学习指针的目的是为了解决问题,那么什么问题非指针不可呢?下面下一个函数,这个函数的功能是负责交换两个变量的值 , 想一下为什么下面的代码最终没有出现交换的效果?
c
void swap(int x,int y)//x=a y=b
{
int c=0;
c=x;
x=y;
y=c;
}
int main()
{
int a=20;
int b=30;
printf("交换前:a=%d b=%d\n",a,b);
swap(a,b); //传值调用
printf("交换后:a=%d b=%d\n",a,b);
return 0;}
通过调试我们可以发现,x和y的值都发生改变了,但是a和b的值并没有发生改变,这是因为我们函数的实参传递的是a和b的值,把值传给形参后,因为形参是实参的一份临时拷贝,形参有自己的独立空间,所以在函数swap内部交换,并不会影响a和b。所以这个代码就必须用指针来解决,要把a和b的地址传给形参,也就是传址调用。
2.传址调用
c
void swap(int*x,int*y)//指针x存放a的地址,指针y存放b的地址
{
int c=0;
c=*x;//其实是把x的的值,也就是a的值赋值给c
*x=*y;//把y的值赋值给x
*y=c;//把x的值赋值给y
}
int main()
{
int a=20;
int b=30;
printf("交换前:a=%d b=%d\n",a,b);
swap(&a,&b); //传址调用
printf("交换后:a=%d b=%d\n",a,b);
return 0;}
总结:
如果传的外部值需要发生修改,那么就要传地址,如果不发生修改,那么就传值
c
//下面的代码就不要传地址,直接传值就可以解决
c
//返回最大值
int max(int a,int b)
{
if(a>b)
return a;
else
return b;
}
int main()
{
int a=20;
int b=3;
int m=max(a,b);
printf("%d",m);
return 0;
}
九.练习:求字符串长度
(一共有4种方法)
方法1:
c
#include <stdio.h>
#include <string.h>//使用strlen函数需要引用头文件
int main()
{
char arr[]="hello";
printf("%d",strlen(arr));//strlen函数统计字符串长度,不包括\0
return 0;
}
方法2:
c
char str[]="hello";//字符串以\0结尾
int count=0;
char*p=str;//数组名即首元素地址
while(*p !='\0')//只要不是字符'\0',就一直统计,直到遇到\0,因为字符串以\0结束
{
count++;
p++;
}
printf("%d",count);
c
//还可以用函数的方法
int my_strlen(char* str)
{
int count=0;
while((*str)!='\0')
{
count++;
str++;
}
return count;
}
int main()
{
char str[]="hello";
int len=my_strlen(str);
printf("%d",len);
return 0;}
方法3:
c
//利用指针-指针的方式,因为指针-指针得到是相差的元素的个数,那么我们只要知道了\0的地址,用\0的地址减去第一个元素的地址,就可以得到字符串的长度
int my_strlen(char* str)
{
char* start=str;//将第一个元素的地址存储起来,后面指针减指针的时候需要用到
while((*str)!='\0')
{
str++;
}
//循环结束后,指针str已经指向\0的地址了
return str-start;
}
int main()
{
char str[]="hello";
int len=my_strlen(str);
printf("%d",len);
return 0;}
方法4:
c
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)//首先我们的意图是求取字符串的长度,所以不希望str指向的内容发生改变,用const来修饰指针str,
//统计的字符串的长度肯定不会是一个负数,因为strlen函数的返回类型是size_t,就是无符号整型,他表示正数,所以我们在这里把my_strlen的函数返回类型设置为size_t
{
size_t count=0;
//在使用指针前,需要判断下str是不是空指针
assert(str!=NULL);
while((*str)!='\0')
{
count++;
str++;
}
return count;
}
int main()
{
char str[]="hello";
size_t len=my_strlen(str);
printf("%zd",len);
return 0;}
创作不易!多多支持