文章目录
一、指针是什么、为什么使用它?
指针是什么?
指针:是一种数据类型,占用内存空间,用来保存内存地址。 例如:
在32位平台下,指针大小为4个字节;64位下,指针大小为8个字节。
void test()
{
int *p1 = (int *)100;
int *p2 = (int *)200;
printf("p1:sizeof = %d\n",sizeof(p1));
printf("p2:sizeof = %d\n",sizeof(p1));
}
我们这里是在32位平台下测试的:
为什么使用指针、什么情况下使用指针?
①函数之间无法通过传参共享变量。属于调用者,函数之间的名字空间相互独立是可以重名的,函数之间的数据传递都是值传递。
例如:我们想通过该函数使a,b的值交换一下,a若变为100,b为50则成功
void func(int a,int b)
{
a = 100;b = 50;
}
int main()
{
int a = 50;int b = 100;
func(a,b);
printf("%d %d\n",a,b);
}
输出结果:修改失败
以上的代码块中函数之间就无法共享变量(main的a.b和func的a.b是相互独立的)它们变量存放的内存位置并不相同。
我们利用指针改进。
void func(int* a,int* b)
{
*a = 100;*b = 50;
}
int main()
{
int a = 50,b = 100;
func(&a,&b);
printf("%d %d\n",a,b);
}
输出结果:修改成功
②函数调用时传参的效率太差,指针可以优化函数之间传参的效率
typedef struct Stu
{
char name[20];
int age;
char sex;
}Stu;
void intput_stu(Stu* stu)//输入函数
{
stu->age = 10;
strcpy(stu->name,"XiaoHong");
stu->sex = 'N';
}
void show_stu(Stu* stu)//显示函数
{
printf("%s %d %c\n",stu->name,stu->age,stu->sex);
}
int main()
{
Stu stu;
intput_stu(&stu);
show_stu(&stu);
}
③堆内存无法与标识符建立联系,只能配合指针使用
int *p =NULL;
p = malloc(4);
如何使用指针?
定义: 类型 * 变量名p;
解引用(根据地址访问内存): * 指针变量名
使用方法:①定义指针变量②关联指针变量③解引用
int main(void)
{
int a = 0,b=8;
int *p;//第一步:定义指针变量
p = &a;//第二步:关联指针指向的地址
*p = 555;//第三步:解引用
printf("*p = %d. a = %d\n", *p,a);//500
*p = b;
printf("*p = %d. b = %d\n", *p,b);//8
return 0;
}
注意要点:
① 在定义指针变量的时候如果没有关联指向地址,那么在之后就不可以用* p=&a来关联地址,但是可以直接在定义指针变量时直接关联int * p = &a;
② 因为指针不是普通变量所以在解引用时不可以直接将指针当做普通变量的左值来进行直接赋值。只能让指针通过“*”来解引用比如 * p = 555;
③ 指针实现的是间接访问,当指针定义之后,一旦关联了其他变量(存储地址)的左值,就相当于指针和变量的存储地址相等,牵一发而动全身,其中一个改变,两个都会改变;
④ 当指针定义之后需进行解引用时,要判断指针是否关联了其他变量(存储地址),如果未关联就是野指针,大概率会造成不可预料的错误;
指针使用时星号* 作用、&作用
星号作用:
① 第一种是指针定义时,*结合前面的类型用于表明要定义的指针的类型
int * p 去掉指针变量名 p ,*与类型int结合 表示指针类型为 int *
② 第二种功能是指针解引用,解引用时*p表示p指向的变量本身(存储地址)
*p = b 表示*p指向的变量b的左值(存储地址)
&作用: 取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址。
比如 :&a 意思是取变量a的左值(存储地址)
注意:在与指针相关的使用中 & 此符号只在指针定义需关联存储地址时以及作为函数传输的实参进行使用。
二、空指针和野指针
空指针
空指针:即NULL指针,它作为一个特殊的指针,表示不指向任何东西。 要使一个指针为NULL,可给它赋值为0;对指针的解引用操作可以获得它所指向的值,但对NULL指针解引用是一个非法的操作,在解引用前必须确保它不是一个NULL指针。
不允许向空指针或非法的地方拷贝内存: 如下所示为空指针:编译器会报错。
void test()//不允许向空指针或非法地方拷贝内存
{
//给空指针p指向的内存区域拷贝内存
char* p = NULL;
strcpy(p,"1111");//错误
}
如下所示为非法地址:编译器同样也崩溃;
void test()//不允许向空指针或非法地方拷贝内存
{
//随意找了一块地址并未申请,直接将1122拷贝进去,程序崩溃
char* q = (char *)0x1122;
strcpy(q,"1122");//错误
}
野指针
野指针:野指针指向一个已删除的对象或未申请访问受限内存区域的指针。结果是不可知的。
出现野指针的情况:
1.指针变量未初始化;
void test()
{
int* p;//未初始化
printf("%d\n",*p);
}
上述代码:p未进行初始化,程序崩溃。
2.指针释放后未置空;
此时我们使用了整型指针p,然后将其释放,但未将p置为空,p的指向还是刚才那块内存,为一个随机值。
void test()//malloc后free了,但是指针未置空
{
int* p = (int*)malloc(sizeof(int));
*p = 100;
free(p);
printf("%d\n",*p);
}
输出为;
因此,我们在释放后还需要将其置空:因此它是空指针,解引用若报错即为正确的。
p = NULL;//free后指针置空,防止野指针
此时程序报错,符合我们预期;
3.指针操作超越变量作用域;
如下:a在dowork执行完后就会被释放,但此时用一个指针p2记录它的地址并且返回,为野指针。
int *dowork()
{
int a = 10;
int *p = &a;
return p;
}
void test()
{
int *p2 = dowork();//野指针
printf("%d\n",*p2);//编译器暂时保留
printf("%d\n",*p2);
}
输出结果:第一次是编译器暂时保留,第二次输出就不是该值了,为野指针。
野指针与空指针区别
区别:空指针可以多次释放;野指针内存被释放,不能够再使用这块内存;
void test()
{
int* p = NULL;
free(p);//空指针可以多次被释放
free(p);
free(p);
}
野指针释放后,已无权限对该块内存进行操作,再次对该块内存进行操作会导致崩溃;
void test()
{
int* p2 = (int*)malloc(100);
free(p2);//释放p2,不能再使用了
free(p2);//再次操作,程序崩溃
}
三、指针的步长
指针的步长:
1.表示指针变量+1之后跳跃的字节数量。
char* 为一个一个加:如下
void test()
{
char* p = NULL;
printf("%d\n",p);
printf("%d\n",p+1);
}
输出为:
double* 为8个8个加,不同类型加的步长不一样:如下
void test()
{
double* p2 = NULL;
printf("%d\n",p2);
printf("%d\n",p2+1);
}
输出为:
2.表示解引用时,通过步长来取数据。
我们创建了一个1024个字节全都是0的数字,把a = 1000放进字符数组中;再取4个字节即a出来。
void test()
{
char buf[1024] = {0};
int a = 1000;
char* p = buf;//通过p找到buf的首地址
memcpy(buf,&a,sizeof(int));
printf("buf中的a = %d\n",*(int*)p);
}
此时,成功取出a为1000:
此时,我们1000放在了buf+1的地址,那么如何取出来呢
将buf+1后:
memcpy(buf+1,&a,sizeof(int));
我们只用在输出时将其步长+1即可找到该地址将其取出;
printf("buf中的a = %d\n",*(int*)(p+1));
3.通过步长对自定义数据类型的修改。
这里我们会使用一个宏函数:offsetof(type, member-designator) 会生成一个类型为 size_t 的整型常量,它是计算一个结构成员相对于结构开头的字节偏移量。
它需要的头文件为:
#include <stddef.h>//offsetof的头文件
例如:将结构体p1.d的值修改为100。
struct Person
{
char a;//0-3
int b;//4-7
char buf[64];//8-71
int d;//72-75
};
void test()
{
struct Person p1 = {'a',10,"hello world",100};
char*p = (char*)&p1;
printf("p1.d的偏移量为%d\n",offsetof(struct Person,d));//计算结构体属性的偏移量
//打印p1.d的值
printf("p1.d的值为:%d\n",*(int*)((char*)&p1+(offsetof(struct Person,d))));
}
修改成功:
四、指针的间接赋值
1.指针间接赋值成立的三大条件:
(1)2个变量(一个普通变量,一个成员变量)。
(2)建立关系。
(3)通过 * 操作指针指向的内存。
void test()
{
//1.一个普通变量和一个成员变量
int a = 10;
int *p = NULL;
p = &a;//建立关系
*p = 100;//通过星号进行赋值
printf("a = %d\n",a);
}
2.通过函数进行间接赋值:
值传递:不能进行间接赋值;
地址传递:可以进行修改;
下图为值传递:最后输出10,未修改成功a2的值。
void ChangeValue(int a)//值传递,不能进行间接赋值
{
a = 1000;
}
void test()
{
int a2 = 10;
ChangeValue(a2);
printf("a2 = %d\n",a2);
}
下面为一个地址传递:最后成功将a2修改为1000;
void ChangeValue(int *p)//a2为实参,*p为形参 int*p = &a2;
{
*p = 1000;
}
void test()
{
int a2 = 10;
ChangeValue(&a2);
printf("a2 = %d\n",a2);
}
输出结果:
3.若提前知道要修改的指针的地址,可操作该地址进行修改(一般很难知道地址,这里就不测试了);
五、指针做函数参数的输入输出特性
指针做函数参数,具备输入、输出特性。
输入:主调函数分配内存,被调函数使用这块内存;
输出:被调函数分配内存,主调函数使用这块内存;
简单举几个例子看看就好了:
实例1:在栈上为主调函数分配内存,被调函数使用。
void func(char* p)//被调函数
{
strcpy(p,"Hello World");
}
void test()//主调函数
{
//主调函数分配到栈上
char buf[1024] = {0};
func(buf);
}
使用成功,输出:hello world,test函数为主调函数,在栈上开辟了一块内存,func为被调函数,能够使用这块内存,为输入特性;
实例2:在堆上为主调函数分配内存,被调函数使用。
void PrintString(char* str)//被调函数
{
printf("%s\n",str);
}
void test2()//主调函数
{
//分配到堆区
char* p = (char*)malloc(sizeof(char)*64);
memset(p,0,64);
strcpy(p,"Hello World");
PrintString(p);
free(p);
p = NULL;
}
堆上主调函数分配内存,被调函数成功使用,为输入特性。
实例3:被调函数分配内存,主调函数使用。
void allocateSpace(char** pp)//被调函数在堆上分配内存
{
char* temp = (char*)malloc(sizeof(char)*64);
memset(temp,0,64);
strcpy(temp,"Hello world");
*pp = temp;
}
void test3()//主调函数
{
char* p = NULL;
allocateSpace(&p);
printf("%s\n",p);
}
被调函数在堆上分配内存,主调函数使用,为输出特性;
这三个实例最后输出都为:
参考博客:
指针的用法
指针的运用
C指针的使用