前言:这篇文章将全方面带你了解指针的作用。
1.内存和地址
1.1内存
计算机cpu在处理数据时,在内存读取数据,处理后的数据再放回内存。
把内存划分为一个个内存单元,每个内存单元大小为一个字节。
补充:一个字节有8个比特位,比特位是存储二进制的0或1。
每一个内存单元都有一个编号,有了这个编号cpu可以快速找到一个内存空间。
c语言中将编号称作指针,因此我们可以理解为: 内存单元编号==地址==指针。
1.2如何理解编址
首先必须理解,计算机内有很多硬件单元,硬件单元之间是相互合作的,至少相互之间要能够相互之间进行数据传递。
但是硬件间相互独立,是如何实现联系的呢?
答案很简单,用“线”连接起来。
而cpu和内存之间有大量数据交互,所以,两者也需要用线连接。
cpu在内存中访问空间,必须知道空间的位置,因此对字节空间进行编址。
计算机编址并不是把每个字节地址记录下来,而是通过硬件设计完成。
我们可以简单理解,32位的机器中,cpu和内存间存在32根地址总线,每根线有0和1两种状态,因此32根就可以表示2^32种地址。
例如:地址信息通过地址总线,可以在内存中找到地址对应的数据,再通过地址总线将数据传入cpu内寄存器。
2.指针变量和地址
2.1取地址操作符(&)
理解地址和内存关系,我们再回到c语言中,c语言中创建变量就是在向内存申请空间,比如:
其中&为取地址操作符,我们发现变量a有四个字节,其中内存中44的地址和变量a的地址相同,那是因为变量的地址取自最小的字节编号。
因此我们只要知道第一个字节地址,就能访问4个字节的数据。
2.2解引用操作符(*)
#include<stdio.h>
int main()
{
int a=10;
int*pa=a;
return 0;
}
在这个代码中pa的类型是int*,其中*说明pa是指针变量,而int说明pa指向的对象是整形(int)。
#include<stdio.h>
int main()
{
int a=10;
int*pa=a;
*pa=20;
printf("a=%d",a);
return 0;
}
在这个代码中*pa,pa指向的是a的地址,而*pa表示解引用a的地址,*pa等价于a。
a=20
2.3指针变量的大小
指针变量是专门存放地址的。而指针变量的大小取决于机器,在32位的平台上,指针大小为32比特位,也就是4字节,同理64位平台上指针大小是8字节。
注意指针变量大小与类型无关,只与平台有关。
3.指针变量类型的意义
指针变量大小和类型无关,那么我们为什么还要有各种各样的指针类型呢?
3.1指针的解引用
根据代码我们发现pa类型为int*,对*pa赋值为0,int a的每个字节都变成00;
当pa类型为char*时,对*pa进行赋值,只改变了int a的第一个字节内容。
结论 :指针类型决定了,对指针解引用时有多大的访问权限。
比如:char*类型解引用时每次只能访问一个字节,而int*可以访问4个字节。
3.2指针+-整数
#include<stdio.h>
int main()
{
int arr[4] = {0};
int * pa =&arr[0];
char* pb = &arr[0];
printf("pa=%p\n", pa);
printf("pb=%p\n", pb);
for (int i = 0; i < 4; i++)
{
pa=pa+1;
pb=pb+1;
}
printf("pa=%p\n", pa);
printf("pb=%p\n", pb);
return 0;
}
由图中代码运算发现,int*pa和char*pb虽然类型不同,但是他们都是arr[0]的地址。对两个指针变量分别进行4此加1操作,pa数值变大了16,而pb只变大了4。
这是由于他们的指针指向的类型不同,指针指向int类型,每加1,指针所访问的内存地址加4;而指针指向char类型,每加1,指针所访问的内存地址加1。
结论:指针类型决定加一或减一,指针向后或向前走一步有多大(距离)。
3.3void*指针
在指针中有一种特殊的类型是void*类型的,可以理解为无具体类型的指针,void*可以接受任意类型的地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。
#include<stdio.h>
int main()
{
int a;
char b;
char*p=&a;//这种操作会使得编译器报警告。
void*p1=&a;
p1=&b;//void*类型可以接受任意类型的地址。
p1++;
*p1=10;//这两种操作都是错误的,void*类型指针无法进行解引用和加减整数操作。
}
4.const修饰指针
当const修饰int类型时,我们就无法修改该整形。
在指针中同样可以用const修饰,但const有两种不同的用法,将const分别放到“*”前后,作用就不同。
这种情况,如果int* p=&a;虽然无法直接对a修改,都是可以通过*p对a的内存进行修改。
4.1当const放到“*”前
const放到“*”前,有两种写法,如:int a,b;const int* p=&a和int const *p=&a,其实这两种写法的作用是相同的,都是限制的是*p,这样就无法修改*p,但可以修改指针p所指向的对象,如p=&b。
#include <stdio.h>
int main()
{
int a=5,b;
const int *p1=&a;
int const *p2=&b;
*p1=1;
*p2=1;
}
当对p1指向对象进行修改:
#include <stdio.h>
int main()
{
int a=5,b=4;
const int *p1=&a;
printf("%d\n",*p1);
p1=&b;
printf("%d",*p1);
}
结论:当const修饰指针时,const在“*”前面,则*p不能修改;但是p可以修改。
补充:1.p里面存放的是地址(a的地址)。2.p是变量,有自己的地址。3.*p是p指向的空间。
4.2当const放到“*”后
const放到“*”后,如int* const p,这样写限制的是指针p,使指针p指向的对象无法修改;但是*p的修改不受限制。
#include <stdio.h>
int main()
{
int a=5,b=4;
int* const p1=&a;
printf("%d\n",*p1);
p1=&b;
printf("%d",*p1);
}
这样编译器会报错:
但可以对*p进行修改:
#include <stdio.h>
int main()
{
int a=5,b=4;
int* const p=&a;
printf("%d\n",*p);
*p=b;
printf("%d",*p);
}
总结:当const在“*”后,可以修改*p,不能修改指针p的指向。
5.指针的运算
5.1指针+-整数
因为数组的地址是连续的,所以只需要知道首元素地址,就能访问其他的所有元素。
#include <stdio.h>
int main()
{
int arr[5]={3,4,5,6,7};
int* p=&arr[0];
for(int i=0;i<5;i++)
{
printf("%d/n",*(p+i));//p+i为指针指向的地址,*(p+i)就是该地址的空间。
}
return 0;
}
5.2指针-指针
指针-指针=地址-地址。
#include <stdio.h>
int main()
{
int arr[5]={3,4,5,6,7};
printf("%d",&arr[4]-&arr[0]);
return 0;
}
对于上面的代码,我们很容易错误以为结果是16,实际上运算结果是4。
在c语言中,指针-指针不等于地址见字节个数,而是两个指针间元素个数。
注意:这种运算前提是,两个指针必须指向同一块空间。
#include <stdio.h>
int main()
{
int arr[5]={3,4,5,6,7};
char srr[5]={a,b,c,d,e};
printf("%d",&char[4]-&arr[0]);
return 0;
}
因为两个指针指向不是同一块空间,无法确定两块空间之间有没有空隙,并且无法确定元素的类型是char还是int。
5.3指针的关系运算
指针大小比较,即地址大小比较。
#include <stdio.h>
int main()
{
int arr[5]={4,5,6,7,8};
int *p=&arr[0];
int sz=sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz)//arr是数组名,也是数组首元素地址。
{
printf("%d",*p);
p++;
}
return 0;
}
6.野指针
概念:野指针就是指针位置不可知的(随机的,不正确的,没有明确限制的)
6.1野指针的成因
1.指针没有初始化
#include <stdio.h>
int main()
{
int* p;
return 0;
}
非法访问内存。
注:局部变量不初始化,为随机值;全局变量不初始化,为0。
2. 指针越界
#include <stdio.h>
int main()
{
int arr[5]={0};
int* p=arr[0];
for(int i=0;i<10;i++)
{
*p=1;
p++;//当指针超出arr范围访问就是越界访问,p就成了野指针。
}
return 0;
}
3.指针指向的空间释放
int* test()
{
int a=10;//假如a地址为0x001233CD
return &a;//将a的地址返回
}
int main()
{
int*p=test();//此时p接收a地址,p指向0x001233CD
printf("%d",*p);//但是a的地址在上一个函数结束后,销毁了,因此p此时是野指针。
return 0;
}
6.2如何避免野指针
6.2.1初始化野指针
如果知道指针明确指向哪里,直接赋值地址,如果不知道指向哪里,可以赋值NULL。
如:int* p=NULL;此时*p不能赋值,*p=10这种操作是错误的。
6.2.2小心指针越界
一个程序内存申请了那些空间,通过指针也就只能访问那么多空间,不能超出范围访问,超出了就是越界。
6.2.3指针变量不再使用时,及时置NULL
一个指针我们使用完后,如果不再使用,即使对指针置NULL,如:int a=10;int *p=&a; p=NULL。
同时,如果用一个指针前,先去判断他是不是空指针,如果不是我们再去使用他。
6.2.4避免返回局部变量的地址
不要将局部变量的地址返回。
7.assert断言
assert.h头文件定义了宏assert(),用于确保运行时程序符合指定条件,如果不符合,就报错终止运行。这个宏通常被叫做“断言”。
#include <stdio.h>
#include <assert.h>
int main()
{
int*p=NULL;
assert(p!=NULL);
return 0;
}
上述代码中,assert可以判断条件是否满足,如果条件是真的,即表达式返回值非0,则代码运行不会有影响;反之条件错误,表达式返回值为0,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过表达式,以及包含表达式的文件名和行号。
assert()会减少程序运行效率,因此我们一般在Debug中使用,在Relase模式中是禁用assert。
若是已确定程序没有问题,为了减少程序运行负担,可以选择不再使用assert(),此时只需在头文件assert.h前加上#define NDEBUG,即可使assert()失效,如需再使用断言,将#define NDEBUG删去即可。
#include <stdio.h>
#define NDEBUG//这个宏必须写在assert.h前面。
#include <assert.h>
int main()
{
int*p=NULL;
assert(p!=NULL);
return 0;
}
8.指针的使用和传值调用
8.1strlen的模拟实现
库函数strlen的功能是求字符串长度,统计字符串中\0之前的字符个数。
函数原型:
size_t strlen(const char* str);
模拟实现,参数str接收一个地址开始,只要不是\0,计数器就加1,最终返回长度数值。
size_t my_strlen(const char*str)
{
assert(str);\\空指针ASCII码值为0,所以如果str指向NULL,程序报错。
size_t count=0;
while(*str)// \0的ASCII码值为0,如果是\0,则循环终止。
{
count++;
str++;
}
return count;
}
8.2传值调用和传址调用
8.2.1传值调用
当我们需要利用函数完成两个值的交换:
#include <stdio.h>
void swp(int x, int y)
{
int w = x;
x = y;
y = w;
}
int main()
{
int a = 10, b = 20;
printf("a=%d,b=%d\n", a, b);
swp(a, b);
printf("a=%d,b=%d", a, b);
}
通过运行程序我们发现,通过给函数传值,并没有对a和b的值造成修改:
这是因为,传值调用函数时,函数的形参是实参的一份临时拷贝!形参有自己的独立空间,对形参的修改不会影响实参。
通过监视我们发现,x和y虽然对a和b数值进行了拷贝,但是它们的地址却不是同一块空间。通过函数对x和y的值进行交换,并不能影响a和b内存上的数值发生改变。
8.2.2传址调用
我们可以通过传址的方法,对a和b进行交换:
#include <stdio.h>
void swp(int *x, int *y)
{
int w = *x;
*x = *y;
*y = w;
}
int main()
{
int a = 10, b = 20;
printf("a=%d,b=%d\n", a, b);
swp(&a, &b);
printf("a=%d,b=%d", a, b);
}
通过运行我们发现,a和b的值发生了改变。
通过监视我们发现指针x和y它们分别指向的是a和b地址,:
因此我们对x和y解引用,也就是对a和b地址上的空间进行修改。
结论:如果我们只是需要用到函数外变量的值,不需要对变量进行改变,那么我们只需要传值调用。
反之,如果我们需要修改函数外变量的值,那么我们就需要,传址调用。