0.为什么使用指针
假如我们定义了 char a=’A’ ,当需要使用 ‘A’ 时,除了直接调用变量 a ,还可以定义 char *p=&a ,调用 a 的地址,即指向 a 的指针 p ,变量 a( char 类型)只占了一个字节,指针本身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。
但如果要引用的是占用内存空间比较大东西,用指针也还是 4 个字节即可。
程序员书籍资源,值得收藏!,点击查看
h这一定是你需要的电子书资源,全!值得收藏!
使用指针型变量在很多时候占用更小的内存空间。 变量为了表示数据,指针可以更好的传递数据,举个例子:
第一节课是 1 班语文, 2 班数学,第二节课颠倒过来, 1 班要上数学, 2 班要上语文,那么第一节课下课后需要怎样作调整呢?方案一:课间 1 班学生全都去 2 班, 2 班学生全都来 1 班,当然,走的时候要携带上书本、笔纸、零食……场面一片狼藉;方案二:两位老师课间互换教室。
显然,方案二更好一些,方案二类似使用指针传递地址,方案一将内存中的内容重新“复制”了一份,效率比较低。
- 在数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。
一个数据缓冲区 char buf[100] ,如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int ,使用如下语句进行赋值:
代码语言:javascript复制*(short*)&buf[0]=DataId;
*(short*)&buf[2]=DataType;
*(int*)&buf[4]=DataValue;
- 数据转换,利用指针的灵活的类型转换,可以用来做数据类型转换,比较常用于通讯缓冲区的填充。
- 指针的机制比较简单,其功能可以被集中重新实现成更抽象化的引用数据形式
- 函数指针,形如: #define PMYFUN (void*)(int,int) ,可以用在大量分支处理的实例当中,如某通讯根据不同的命令号执行不同类型的命令,则可以建立一个函数指针数组,进行散转。
- 在数据结构中,链表、树、图等大量的应用都离不开指针。
1. 指针强化
1.1 指针是一种数据类型
操作系统将硬件和软件结合起来,给程序员提供的一种对内存使用的抽象,这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。
内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有唯一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范围就是 0 ~ 4294967296 ,十六进制表示就是 0x00000000~0xffffffff ,当程序使用的数据载入内存时,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。
1.1.1 指针变量
指针是一种数据类型,占用内存空间,用来保存内存地址。
代码语言:javascript复制void test01(){
int* p1 = 0x1234;
int*** p2 = 0x1111;
printf("p1 size:%dn",sizeof(p1));
printf("p2 size:%dn",sizeof(p2));
//指针是变量,指针本身也占内存空间,指针也可以被赋值
int a = 10;
p1 = &a;
printf("p1 address:%pn", &p1);
printf("p1 address:%pn", p1);
printf("a address:%pn", &a);
}
1.1.2 野指针和空指针
1.1.2.1 空指针
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。
对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。
如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。 不允许向NULL和非法地址拷贝内存:
代码语言:javascript复制void test(){
char *p = NULL;
//给p指向的内存区域拷贝内容
strcpy(p, "1111"); //err
char *q = 0x1122;
//给q指向的内存区域拷贝内容
strcpy(q, "2222"); //err
}
1.1.2.2 野指针
在使用指针时,要避免野指针的出现:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
什么情况下会导致野指针?
- 指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
- 指针释放后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
- 指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
代码语言:javascript复制void test(){
int* p = 0x001; //未初始化
printf("%pn",p);
*p = 100;
}
操作野指针是非常危险的操作,应该规避野指针的出现:
- 初始化时置 NULL
指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。
- 释放时置 NULL
当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。
1.1.2.3 void*类型指针
void是一种特殊的指针类型,可以用来存放任意对象的地址。一个void指针存放着一个地址,这一点和其他指针类似。不同的是,我们对它到底储存的是什么对象的地址并不了解。
代码语言:javascript复制double a=2.3;
int b=5;
void *p=&a;
cout<<p<<endl; //输出了a的地址
p=&b;
cout<<p<<endl; //输出了b的地址
//cout<<*p<<endl;这一行不可以执行,void*指针只可以储存变量地址,不可以直接操作它指向的对象
由于void是空类型,只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。
1.1.2.4 void*数组和指针
- 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
- 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的。指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
- 数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在32位平台下,无论指针的类型是什么,sizeof(指针名)都是 4 ,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8 。
- 数组名作为右值的时候,就是第一个元素的地址
int main(void)
{
int arr[5] = {1,2,3,4,5};
int *p_first = arr;
printf("%d",*p_first); //1
return 0;
}
- 指向数组元素的指针 支持 递增 递减 运算。p= p 1意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。在数组中相邻内存就是相邻下标元素。
1.1.3 间接访问操作符
通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。
注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。
代码语言:javascript复制int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]&arr1
1)在指针声明时,* 号表示所声明的变量为指针
2)在指针使用时,* 号表示操作指针所指向的内存空间
- *相当通过地址(指针变量的值)找到指针指向的内存,再操作内存
- *放在等号的左边赋值(给内存赋值,写内存)
- *放在等号的右边取值(从内存中取值,读内存)
//解引用
void test01(){
//定义指针
int* p = NULL;
//指针指向谁,就把谁的地址赋给指针
int a = 10;
p = &a;
*p = 20;//*在左边当左值,必须确保内存可写
//*号放右面,从内存中读值
int b = *p;
//必须确保内存可写
char* str = "hello world!";
*str = 'm';
printf("a:%dn", a);
printf("*p:%dn", *p);
printf("b:%dn", b);
}
1.1.4 指针的步长
指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针 1时候,移动多少字节单位。
思考如下问题:
代码语言:javascript复制int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;
//为什么*p1打印出来正确结果?
printf("%xn", *p1);
//为什么*p2没有打印出来正确结果?
printf("%xn", *p2);
//为什么p1指针 1加了4字节?
printf("p1 =%dn", p1);
printf("p1 1=%dn", p1 1);
//为什么p2指针 1加了1字节?
printf("p2 =%dn", p2);
printf("p2 1=%dn", p2 1);
1.1.5 函数与指针
1.1.5.1 函数的参数和指针
C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。
代码语言:javascript复制void change(int a)
{
a ; //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。
}
int main(void)
{
int age = 60;
change(age);
printf("age = %d",age); // age = 60
return 0;
}
有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。
传递变量的指针可以轻松解决上述问题。
代码语言:javascript复制void change(int* pa)
{
(*pa) ; //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,
//会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
int age = 160;
change(&age);
printf("age = %d",age); // age = 61
return 0;
}
比如指针的一个常见的使用例子:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap(int *,int *);
int main()
{
int a=5,b=10;
printf("a=%d,b=%dn",a,b);
swap(&a,&b);
printf("a=%d,b=%dn",a,b);
return 0;
}
void swap(int *pa,int *pb)
{
int t=*pa;*pa=*pb;*pb=t;
}
在以上的例子中,swap函数的两个形参pa和pb可以接收两个整型变量的地址,并通过间接访问的方式修改了它指向变量的值。在main函数中调用swap时,提供的实参分别为&a,&b,这样就实现了pa=&a,pb=&b的赋值过程,这样在swap函数中就通过pa修改了 a 的值,通过pb修改了 b 的值。因此,如果需要在被调函数中修改主调函数中变量的值,就需要经过以下几个步骤:
- 定义函数的形参必须为指针类型,以接收主调函数中传来的变量的地址;
- 调用函数时实参为变量的地址;
- 在被调函数中使用*间接访问形参指向的内存空间,实现修改主调函数中变量值的功能。
指针作为函数的形参的另一个典型应用是当函数有多个返回值的情形。比如,需要在一个函数中统计一个数组的最大值、最小值和平均值。当然你可以编写三个函数分别完成统计三个值的功能。但比较啰嗦,如:
代码语言:javascript复制int GetMax(int a[],int n)
{
int max=a[0],i;
for(i=1;i<n;i )
{
if(max<a[i]) max=a[i];
}
return max;
}
int GetMin(int a[],int n)
{
int min=a[0],i;
for(i=1;i<n;i )
{
if(min>a[i]) min=a[i];
}
return min;
}
double GetAvg(int a[],int n)
{
double avg=0;
int i;
for(i=0;i<n;i )
{
avg =a[i];
}
return avg/n;
}
其实我们完全可以在一个函数中完成这个功能,由于函数只能有一个返回值,可以返回平均值,最大值和最小值可以通过指针类型的形参来进行实现:
代码语言:javascript复制double Stat(int a[],int n,int *pmax,int *pmin)
{
double avg=a[0];
int i;
*pmax=*pmin=a[0];
for(i=1;i<n;i )
{
avg =a[i];
if(*pmax<a[i]) *pmax=a[i];
if(*pmin>a[i]) *pmin=a[i];
}
return avg/n;
}
1.1.5.2 函数的指针
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址。我们可以把函数的这个首地址赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针的定义形式为:
代码语言:javascript复制returnType (*pointerName)(param list);
returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。
用指针来实现对函数的调用:
代码语言:javascript复制#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b)
{
return a>b ? a : b;
}
int main()
{
int x, y, maxval;
//定义函数指针
int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
printf("Input two numbers:");
scanf("%d %d", &x, &y);
maxval = (*pmax)(x, y);
printf("Max value: %dn", maxval);
return 0;
}
1.1.5.3 结构体和指针
结构体指针有特殊的语法: -> 符号
如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员
代码语言:javascript复制typedef struct
{
char name[31];
int age;
float score;
}Student;
int main(void)
{
Student stu = {"Bob" , 19, 98.0};
Student*ps = &stu;
ps->age = 20;
ps->score = 99.0;
printf("name:%s age:%d
",ps->name,ps->age);
return 0;
}
1.2 指针的意义_间接赋值
1.2.1 间接赋值的三大条件
通过指针间接赋值成立的三大条件:
- 2个变量(一个普通变量一个指针变量、或者一个实参一个形参)
- 建立关系
- 通过 * 操作指针指向的内存
void test(){
int a = 100; //两个变量
int *p = NULL;
//建立关系
//指针指向谁,就把谁的地址赋值给指针
p = &a;
//通过*操作内存
*p = 22;
}
1.2.2 如何定义合适的指针变量
代码语言:javascript复制void test(){
int b;
int *q = &b; //0级指针
int **t = &q;
int ***m = &t;
}
1.2.3 间接赋值:从0级指针到1级指针
代码语言:javascript复制int func1(){ return 10; }
void func2(int a){
a = 100;
}
//指针的意义_间接赋值
void test02(){
int a = 0;
a = func1();
printf("a = %dn", a);
//为什么没有修改?
func2(a);
printf("a = %dn", a);
}
//指针的间接赋值
void func3(int* a){
*a = 100;
}
void test03(){
int a = 0;
a = func1();
printf("a = %dn", a);
//修改
func3(&a);
printf("a = %dn", a);
}
1.2.4 间接赋值:从1级指针到2级指针
代码语言:javascript复制void AllocateSpace(char** p){
*p = (char*)malloc(100);
strcpy(*p, "hello world!");
}
void FreeSpace(char** p){
if (p == NULL){
return;
}
if (*p != NULL){
free(*p);
*p = NULL;
}
}
void test(){
char* p = NULL;
AllocateSpace(&p);
printf("%sn",p);
FreeSpace(&p);
if (p == NULL){
printf("p内存释放!n");
}
}
1.2.4 间接赋值的推论
- 用1级指针形参,去间接修改了0级指针(实参)的值。
- 用2级指针形参,去间接修改了1级指针(实参)的值。
- 用3级指针形参,去间接修改了2级指针(实参)的值。
- 用n级指针形参,去间接修改了n-1级指针(实参)的值。
1.3 指针做函数参数
指针做函数参数,具备输入和输出特性:
- 输入:主调函数分配内存
- 输出:被调用函数分配内存
1.3.1 输入特性
代码语言:javascript复制void fun(char *p /* in */)
{
//给p指向的内存区域拷贝内容
strcpy(p, "abcddsgsd");
}
void test(void)
{
//输入,主调函数分配内存
char buf[100] = { 0 };
fun(buf);
printf("buf = %sn", buf);
}
1.3.2 输出特性
代码语言:javascript复制void fun(char **p /* out */, int *len)
{
char *tmp = (char *)malloc(100);
if (tmp == NULL)
{
return;
}
strcpy(tmp, "adlsgjldsk");
//间接赋值
*p = tmp;
*len = strlen(tmp);
}
void test(void)
{
//输出,被调用函数分配内存,地址传递
char *p = NULL;
int len = 0;
fun(&p, &len);
if (p != NULL)
{
printf("p = %s, len = %dn", p, len);
}
}
1.4 字符串指针强化
1.4.1 字符串指针做函数参数
1.4.1.1 字符串基本操作
代码语言:javascript复制//字符串基本操作
//字符串是以0或者'