0x00
众所周知,指针是C语言的核心,没有搞懂指针就相当于没有学过C语言。今天我们就来仔细盘一下指针这个玩意。
本文正确食用方法:
1.没了解过指针的可以学习指针
2.学习过指针但忘的差不多的可以用以回顾
3.等未来忘了后用以温习指针(笔者写本文目的(ง •_•)ง)
0x01
什么是指针
指针是指向另一数据对象的变量(加粗),指针变量储存了它所指数据对象的地址信息。
0x02如何声明指针:
int *p; // 声明一个 int 类型的指针 p
char *p // 声明一个 char 类型的指针p
int *arr[6] // 声明一个指针数组,该数组有6个元素,其中每个元素都是一个指向 int 类型对象的指针
*p //访问p所指向的对象
&a //这个就不用说了,取地址符
卖了半天关子,指针到底有什么作用呢?
--------------------------指针用法-------------------------
0x03
对函数调用的变量进行修改
我们知道,调用函数时,函数调用的参数其实是一种值传递,并没有对该变量产生影响。那么如果我们恰好就是要对该变量进行操作,那么就需要使用指针来实现
假设我们现在要使用函数来实现交换两个变量的值:
void swap(int a,int b){ int temp; temp = a; a = b; b = temp;}
显然,这样是行不通的,因为函数调用时,只会将变量的值传入函数,对该变量本身并不起作用。所以最终两个变量的值并没有交换。
此时,我们就要使用指针来进行值的交换:
void swap(int *a,int *b){ int temp; temp = *a; *a = *b; *b = temp;}
这样,就成功完成了指针的值的变换。在以上代码中,虽然变量的值本身并没有发生改变,但是指针和指针所指变量的指向关系改变了,如下图:
原本,地址和地址所指的值如上图所示
后来,地址和地址所指的值的指向关系发生了改变,调用函数使得粉红色的箭头发生了改变。
0x04
一个函数需要返回多个值
在C语言中,一个函数最多只能返回1个值,那么如果我们需要用一个函数返回多个值时就需要使用指针来实现。
如:已知两个数a,b。用一个函数求a+b和a-b
我们可以提前定义变量,然后通过函数,将数值“装”进变量当中
代码如下:
void fx(int a,int b,int* c,int* d){ *c=a + b; *d=a - b;}
容易发现,虽然函数没有返回值,但是真正的运算结果已经“装”在了*c、*d当中,以此我们就间接的实现了一个函数返回多个值。
在编写函数来进行计算时,我们经常会需要异常处理机制,比如对某一个数开根号时,我们需要被开方的数必须大于0,如果输入的数字小于0时,让该函数返回-1来表示计算不成功,代码如下:
double fx (double a){ if(a<0) return -1; a = sqrt(a); return a;}
但是有的时候,我们会遇到计算结果包含了所有的数的情况,比如在计算两个数相除的时候,结果包含了全体实数,此时再使用return -1这种伎俩就已经没用了。这个时候,我们就可以让答案使用指针带回,计算是否正常使用函数的返回值带回,如:
int fx(double a,double b,double* result){ flag= 1; if(b == 0) flag= 0; else *result = a/b; return flag;}
容易看出,若被除数为0,则返回0来表示运算出错,若返回1,则表示本次计算有效。
0x05
关于指针与数组
我们可以使用指针来操作数组。那么指针和数组到底有什么关系呢?让我们先来看一段代码:
int a[3] = {1,2,3};printf(“%d”,*a);
上述代码的运行结果为“1”,这揭示了一个重要的事情:
重点:当我们直接把数组当成指针来用时,这个指针指向的是数组的第一个单元,即:a[0]
实际上,数组和指针还有更密切的关系,比如在函数的参数表中,我们可以完全将指针和数组进行替换:
void fx(int a[]);
void fx(*a);
以上两条声明完全等价,甚至在访问数组的某一个单元时,都可以使用指针来进行。如:
a[0] 与 *a 等价
a[1] 与 *(a+1)
a[2] 与 *(a+2)
a[x] 与 *(a+x)
除此之外,指针的许多运算都可以有效操作数组,如:++、--、+=、-=、+、-都可以用来进行同上的指针运算,他们都用来对数组的某一个单元进行访问。
其中,还有一个特殊的运算:两个指针相减,它用来计算所访问的两个单元之间相差多少个单元,如:
int a[3]={1,2,3};int *p1 = &a[0];int *p2 = &a[2];printf("%p",p2-p1);
上述代码的执行结果为2,即:p2和p1之间相差两个单元
0x06
关于*p++
*p++ 也是个普通的语句,跟上面的指针运算差不多,为啥要单独拿出来说呢?
因为它有一个很特殊的性质,让我们来慢慢解开它神秘的面纱
首先,++的优先级大于*的优先级,应该先进行++的运算
但是,++作为一个后置运算,是需要等到整条语句执行完毕后,再执行的
所以,它的实际作用效果是:将p所指向的那个数据取出来,然后将指针指向下一个位置
在计算机当中,*p++会直接被翻译成一句汇编指令,因此若能巧妙运用*p++,可以有效提高程序的运行效率
比方说,我们在遍历字符数组时,可以使用这种代码
char p[5] = {a,b,c,d,e};while(*p !=/0){ xxxxxxxxx; //某个操作 *p++;}
这样就不必使用for语句了,省时又省力!
0x07
关于指针与结构体
指针在结构体中的应用其实跟数组差不多,如果指针指向的是结构体数组,那么就把它当作数组,没有什么特殊的,指针运算那些都能照常使用。
至于指向结构体的指针,则在链表、树等数据结构中广泛运用,也没有什么特别的。
笔者主要是想在这里区分两个运算符: .和-> 的区别
(这里当时坑了笔者好久ヽ(*。>Д
许多人容易把.和->混用,网上有些文章甚至直接说.和->毫无区别,可以随便替换
.运算是成员访问运算符,用于访问结构体中的某一个成员
而->是指向成员运算符,虽然也是访问结构体中的某一个运算符,但两者有差异
下面我们用两张图来理解两者之间的差异
.运算符时,直接访问结构体中的某一个成员如:
stu.a
->运算符时,是通过指针p来间接访问结构体中的某一个单元,如:
p->a
如果没有区别开这两种运算符,在编译时会报错
区分以上两个运算符后,容易得出:(*p).a和p->a是等价的
注:由于.运算符的优先级大于*运算符的优先级,所以(*p).a中必须要加上括号
0x08
动态内存分配
有的时候,我们会碰上数组大小不够的问题,除了提前定义更大的数组,还可以给它“扩容”,手动给它分配内存,这种行为被称为“动态内存分配”。
本文主要介绍malloc() 和 free() 函数原型在头文件:stdlib.h中,所以使用前记得在头部加上
#include
malloc() 内存分配,它会返回无类型的指针变量(即:void*)比如:
int *p;p = (int *)malloc(400);
执行上述语句后相当于分配了400个字节的内存空间并返回了void*型的指针,所以我们用(int *)进行强制类型转换,将其转换为int *类型的指针再赋给p。
但是,申请了内存后系统并不会自动回收这一块内存空间,需要手动再使用free将其释放。只需执行:
free(p);
这时,我们先前所申请的空间就会被释放。
随着程序越来越大,系统资源也会消耗殆尽,所以free空间一定要及时
一个编程好习惯:有借有还。只要向系统申请了内存空间,一定要在用完后及时将它归还。
0x09
关于指针的安全性
C语言的指针是一把双刃剑,许多人选择C语言是因为它强大的指针,同样也有很多人不选择C语言是因为它危险的指针。接下来让我们看看C语言在程序中的潜在危险性。
1.指针指向数组时越界
当我们对数组外的单元进行操作时,就构成了数组越界。在使用指针操作数组时,由于使用者的操作不当和C语言本身的特性,非常容易造成这种指针指向的数组越界的情况。这种情况又称为缓冲区溢出
小知识:缓冲区溢出:缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击行动。缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。利用缓冲区溢出攻击,可以导致程序运行失败、系统关机、重新启动等后果。第一个缓冲区溢出攻击就是有名的蠕虫病毒,发生在二十年前,它曾造成了全世界6000多台网络服务器瘫痪。
2.没有对象的野指针
所谓野指针,就是指针指向了一块随机的空间,不受程序的直接控制。
产生原因:
①在指针最初定义的时候,没有给他进行初始化(比如指直接int *p后,不让它指向任何的对象)。
②free或delete了指针后,实际上仅仅释放了它所指向的空间的内存,并没有销毁指针本身,所以产生了野指针。
③返回了已被内存已被回收的对象时。比如一个函数在作用完后内存被回收,但却仍然有某个指向该函数的指针,此时该指针沦为野指针。
野指针的问题在于,它平时存在的时候没有什么影响,但随着程序越来越复杂,某一天这个指针被某个程序调用时,就会产生未知的结果,严重时甚至会导致程序崩溃。这种问题一般很难被找出来,需要耗费大量精力来寻找。
0x0a
后话
指针是C语言的精髓,希望大家能妥善的使用指针,让C语言最大化的为自己所用!
非常感谢你的阅读,喜欢记得加关注噢(●'◡'●) D区