一、地址的引入
概念
地址是一个十六进制表示的整数,用来映射一块空间,是系统用来查找数据位置的依据。地址标识了存储单元空间,而字节就是最小的存储单位。
按字节的编址方式:每一个字节都有一个唯一的地址。例如:一个int型的变量是4个字节,就会对应4个地址,我们只需要取到这个变量的首地址就能得到完整的int型数据。
用一个例子感受变量存放的地址:
#include <stdio.h>
int main()
{
int a=10;
int b=11;
int* p=&a;
int* p2=&b;
printf("a的地址是:%p\n",p);
printf("b的地址是:%p\n",p2);
return 0;
}
结果:可以发现两者地址相差4个字节,说明int型变量用4个字节的空间存放
a的地址是:0x7ea5d1dc
b的地址是:0x7ea5d1d8
二、指针分类型与指针偏移量
用sizeof发现linux下所有指针类型的大小均为8字节。
首先明白:指针所占用的空间与指针指向的内容和内容的大小无关。
其次明白:在不同的操作系统及编译环境下,指针类型所占用的字节数是不同的
例如:
编译生成16位的代码时,指针占2个字节
编译生成32位的代码时,指针占4个字节
编译生成64位的代码时,指针占8个字节
整型指针,字符指针
#include <stdio.h>
int main()
{
int a = 5;
char b = 'A';
int *pa = &a;//存放整型数的指针叫整型指针
char *pb = &b;//而这叫字符型指针
//printf("int型指针 pa 的地址是%p,指针偏移(++pa)的地址是:%p\n",pa,++pa);
//printf("char型指针 pb 的地址是%p,指针偏移(++pb)的地址是:%p\n",pb,++pb);
printf("int 型指针pa的地址是%p\n",pa);
printf("int 型指针偏移(++pa)后的地址是:%p\n\n",++pa);
printf("char 型指针pb的地址是%p\n",pb);
printf("char 型指针偏移(++pb)后的地址是:%p\n",++pb);
return 0;
}
结果:可以看到指针类型不同,其每次偏移的地址量也不同。
pi@raspberrypi:~/Desktop $ ./a.out
int 型指针pa的地址是0x7ead81ec
int 型指针偏移(++pa)后的地址是:0x7ead81f0
char 型指针pb的地址是0x7ead81eb
char 型指针偏移(++pb)后的地址是:0x7ead81ec
三、为什么要用到指针
1、函数传过去指针,可以取里面的值改变,比如交换两个数的值,
#include <stdio.h>
int main()
{
int a = 10;
printf("address of a: %p\n",&a);
volatile unsigned int* p =(volatile unsigned int*)0x......
printf("address of p: %p\n",p);
return 0;
}
2、单片机或arm中寄存器地址
volatile和编译器的优化有关:
编译器的优化
在本次线程内,当读取一个变量时,为了提高读取速度,编译器进行优化时有时会先把变量读取到一个寄存器中(寄存器比内存要快!);以后,再读取变量值时,就直接从寄存器中读取;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以保持一致。
当变量因别的线程值发生改变,上面寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当寄存器因别的线程改变了值,原变量的值也不会改变,也会造成应用程序读取的值和实际的变量值不一致。
3、节省内存
指针的使用使得不同区域的代码可以轻易的共享内存数据,当然也可以通过数据的复制达到相同的效果,但是这样往往效率不太好。
指针节省内存主要体现在参数传递上,比如传递一个结构体指针变量和传递一个结构体变量,结构体占用内存越大,传递指针变量越节省内存,也就是可以减少不必要的数据复制。
4、动态分配内存
常常可以看到,程序使用的内存在一开始就进行分配(静态内存分配)。这对于节省计算机内存是有帮助的,因为计算机可以提前为需要的变量分配内存。
但是大多应用场合中,可能一开始程序运行时不清楚到底需要多少内存,这时候可以使用指针,让程序在运行时获得新的内存空间(动态内存分配),并让指针指向这一内存更为方便。
5、函数多个返回值
有时候我们总是希望一个功能子函数的返回值可以有很多个,但奈何用return只能返回一个。
四、定义指针指向数组
数组的首地址
int arry[4] = {1,2,3,4};
int *p;
p = &arry[0];
//p = arry;
见怪不怪
1、指针当作数组名,下标法访问
2、数组名拿来加
arry++可行否(×);//数组常量,指针变量
sizeof(arry),可以算出其长度;
sizeof(p);是8;操作系统中8byte表示一个指针变量;
五、函数指针(重点)
1、无参无返的函数指针
这是函数指针最简单的一种形式
#include <stdio.h>
void print()//要被指向的函数
{
printf("hello world\n");
}
int main()
{
void (*pprint)() = NULL;//定义函数指针
pprint = print; //函数指针赋值:指向函数的首地址(就是函数名)
//如同数组的首地址,是数组名
pprint(); //调用方法1
(*pprint)(); //调用方法2
printf("函数指针pprint的地址是%p\n",pprint);
printf("函数指针偏移(++pprint)后的地址是:%p\n",++pprint);
return 0;
}
结果:
hello world
hello world
函数指针pprint的地址是0x1046c
函数指针偏移(++pprint)后的地址是:0x1046d
2、有参有返的函数指针
#include <stdio.h>
int sum(int a,int b)//要被指向的函数
{
int c = 0;
c = a+b;
return c;
}
int main()
{
int total1;
int total2;
int (*psum)(int a,int b) = NULL;//定义函数指针
psum = sum; //函数指针赋值,指向函数的首地址(就是函数名)
//如同数组的首地址,是数组名
total1 = psum(5,6); //调用方法1
total2 = (*psum)(6,9);//调用方法2
printf("total1:%d\ntotal2:%d\n",total1,total2);
printf("%p\n",psum);
printf("%p\n",++psum);
return 0;
}
结果
total1:11
total2:15
0x10440
0x10441
3、结构体中的函数指针
比较常见的还是和结构体的结合
#include <stdio.h>
#include <string.h>
struct student
{
int english;
int japanese;
int math;
int chinese;
char name[128];
int (*pLanguage)(int english,int japanese);//顺便复习函数指针怎么使用
};
int Language(int eng,int jap)//函数指针所指向的函数
{
int total;
total = eng + jap;
return total;
}
int main()
{
int lanSum;
struct student stu1 = {
.japanese = 90,
.english = 100,
.math = 90,
.name = "cpc",
.pLanguage = Language,
};
lanSum = stu1.pLanguage(stu1.english,stu1.japanese);
printf("stu1的名字是%s,他的语言综合分数是%d\n",stu1.name,lanSum);
printf("%p\n",stu1.pLanguage);
printf("%p\n",++stu1.pLanguage);
return 0;
}
4、规律总结
函数指针无非就三步走:
A、定义
类型 (*指针名)();
void (*pprint)() = NULL;
两个括号很好记
B、赋值
指针名 = 函数名
pprint = print;
C、调用
如有参数则调用时传
pprint(); //调用方法1
(*pprint)(); //调用方法2
六、用一个结构体指针做一个最简单的学生成绩管理
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stud
{
char* name;
int score;
};
int main()
{
int num;
int i;
printf("需要录入几个学生的成绩?\n");
scanf("%d",&num);
//这里开辟了num个结构体所需要的空间,动态分配内存
struct stud *pstu = (struct stud *)malloc(sizeof(struct stud)*num);
for(i=0;i<num;i++){
pstu->name = (char* )malloc(sizeof(char)*16);
memset(pstu->name,0,sizeof(char)*16);
printf("请输入第%d个学生的名字\n",i+1);
scanf("%s",pstu->name);
printf("请输入第%d个学生的成绩\n",i+1);
scanf("%d",&pstu->score);
pstu++;
}
pstu -= num;//指针回头
for(i=0;i<num;i++){
printf("%s:%d\n",pstu->name,pstu->score);
pstu++;
}
return 0;
}
七、野指针
1、定义
野指针指向的地址是随机(又称为:"垃圾"内存)的,无法得知他的地址,操作系统自动对其进行初始化。
2、野指针是怎样生成的?
(1)创建指针时没有对指针进行初始化
(2)使用free释放指针后没有将其指向NULL
3、有什么危害
当一个指针成为野指针,指向是随机的,当你使用它时,危害程度也是随机而不可预测的。一般会造成内存泄漏也很容易遭到黑客攻击,只要将病毒程序放入这块内存中,当使用到这个指针时就开始执行。
4、如何避免
定义指针时进行初始化
如果没有确定指向,就让它指向NULL
NULL在宏定义中是#define NULL (void **) 0,代表的是零地址,零地址不能进行任何读写操作
要给指针指向的空间赋值时,先给指针分配空间,并且初始化空间
简单示例:
//char型指针
char *p = (char *)malloc(sizeof(char));
memset(p,0,sizeof(char));
//int型指针
//指针(指向地址)游标卡尺 开辟空间大小
int *p = (int *)malloc(sizeof(int));
memset(p,0,sizeof(int));
//结构体指针
struct stu *p = (struct stu *)malloc(sizeof(struct stu));
memset(p,0,sizeof(struct stu));
malloc动态内存分配,用于申请一块连续的指定大小的内存块区域以void类型返回分配的内存区域地址。voidmalloc(unsigned int size),因为返回值时void*,所以要进行强制转换。
memset将某一块内存中的内容全部设置为指定的值,
这个函数通常为新申请的内存做初始化工作,是对较大的结构体或数组进行清零操作的一种最快方法。void *memset(void *s, int ch, size_t n);
释放指针同时记得指向NULL
free(p);
p = NULL;
5、malloc与内存泄漏
情景:
程序刚跑起来的时候没问题,时间久了程序崩溃,大多为内存泄漏。
最常见的情况是在无限的循环中一直申请空间。用malloc申请的空间,程序不会主动释放,只有当程序结束后,系统才回收空间。
避免在循环中一直申请空间,即使合理释放(free,指向NULL)
6、指针类型小测试
int *p[4];
int (*p)[4];
int *p();
int(*p)();
指针数组,数组中存放的是一系列的地址
数组指针,指向一个数组
只是一个普通的函数,其返回值是int* 的指针
函数指针,指向一个函数