提到指针,就要说说“内存之王”——数组,以及函数,和结构体(这个是C语言中最后学的,先普及一下)。
( 说明:1.本篇文章稍有些长,请有足够的耐心; 2.本篇几乎用c++书写代码,不过语言之间有相通性,关注核心代码; now,开启旅程吧! )
啥是指针?
我们知道,在C语言中,每种类型的变量都有相应的字节长度,如Visual C++为每个整型变量分配4个字节,为单精度浮点型变量分配4个字节,为字符型变量分配1个字节,内存区的每一个字节都有一个编号,这就是“ 地址 ”,它相当于旅馆中的房间号。在地址所标志的内存单元中存放的数据则相当于旅馆房间中居住的旅客。
由于通过地址能找到所需的变量单元,可以说, 地址指向该变量单元 。因此,我们将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
(在此节,请务必弄清楚存储单元的地址和存储单元的内容这两个概念的区别:假设程序已定了3个整型变量i,j,k,在程序编译时,系统可能分配地址为2000 ~ 2003的4个字节给变量i,2004 ~ 2007给变量j,2008 ~ 2011给变量k。。。)
数组与指针
数组的优点在于能随意查勘其中某一元素,但要注意其空间上的“确定性”——长度固定(变长数组除外),以至无法随意增、删、改、查,这就需要指针了。
指针,顾名思义,指向符。
“万物皆对象”,有对象,就会有指针,“*”代表指针元素,如:
int * p;
“p”是指针,“p”是指针变量;下一句 “万事可数组”,归类,一定程度上可便捷操作,但如果同一数组中多个元素相同,计算机又如何辨别?
用指针!指针指向元素的地址。如下:
这一个个小格子就是一个个连续的内存地址*,计算机的硬件指令非常依赖地址,数组表示法其实就是在变相地使用指针。
我们必须知道:任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量,因此:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。
再来看:
a[5]={0,1,2,3,4};
学过数组的话,就知道:数组名(如a[5]中的a)就代表数组元素的首地址(或说:数组首元素的地址),而如果这样:
short * p;
p=a;
//或:short * p=&a[0];
就会出现:
由上可知:在指针前面使用*运算符可得到该指针所指向对象的值。
来解释下:
int * p是设置一个short型的指针变量p,p=a即将此指针指向数组a[5]的首地址a[0],而&是(取)地址运算符。
再往下看,“p+1、p+2…”
???
对,指针也可以进行简单的运算,以上操作称为“位移”或“偏移”,即将指针p向后移动1位、2位。。。
上面实例输出:
打印的是指针依次加1后的地址。注意,地址是十六进制的,因此dd比dc大1,但是思考一下,0x7fff5fbff8dc +1是否为0x7fff5fbff8de ?系统中,地址按字节编址,short类型占2字节,double类型占8字节,在C中,指针加1指的是加一个存储单元,对数组而言,这意味着加一后地址是下一元素的地址,而非下一字节的地址。
如下图:
由上可知:指针也可以设为float,int,double等类型的。
链表和指针
后面就会学到:所谓链表,其实就是一堆线性排列的数据/元素在指针的移动下发生的一系列事。
对,就是 指针的移动
1.判断链表是否有环的问题
单链表中的环是指链表末尾的节点的 next 指针不为 NULL ,而是指向了链表中的某个节点,导致链表中出现了环形结构。
这种题当然可以用“穷举法”:遍历链表,记录已访问的节点;将当前节点与之前以及访问过的节点比较,若有相同节点则有环。
否则,不存在环。
but,效率过于低下,尤其是当链表节点数目较多,在进行比较时花费大量时间,时间复杂度大致在 O(n^2)。忘了它吧。。。
这样的题当然也可以用“哈希缓存法”,其大致思想是创建一个以节点 ID 为键的 HashSe t集合,用来存储曾经遍历过的节点,然后以后再遍历新节点时,与集合数值相比较,若等,则有环,若不等,则放入,接着比较。
but,不好意思,这种方法我不会。。。
我们依然用强大的“ 双指针法 ”,只是,这次变了一个名字,称之为“ 快慢指针法 ”:
(1)定义两个指针分别为 slow,fast,并且将指针均指向链表头节点。
(2)规定,slow 指针每次前进 1 个节点,fast 指针每次前进两个节点。
(3)当 slow 与 fast 相等,且二者均不为空,则链表存在环。
若链表中存在环,则快慢指针必然能在环中相遇。这就好比在环形跑道中进行龟兔赛跑。由于兔子速度大于乌龟速度,则必然会出现兔子与乌龟再次相遇情况。因此,当出现快慢指针相等,且二者不为NULL时(即二者不在尾节点处相遇),则表明链表存在环。
bool isExistLoop(ListNode* pHead) {
ListNode* fast;//慢指针,每次前进一个节点
ListNode* slow;//快指针,每次前进2个节点
slow = fast = pHead ; //两个指针均指向链表头节点
//当没有到达链表结尾,则继续前进
//快指针一次两个节点,慢指针一次一个节点,要求不能在尾节点处相遇,故而有下面while语句中的条件
while (slow != NULL && fast -> next != NULL) {
slow = slow -> next ; //慢指针前进一个节点
fast = fast -> next -> next ; //快指针前进两个节点
if (slow == fast) //若两个指针相遇,且均不为NULL则存在环
return true ;
}
//到达末尾仍然没有相遇,则不存在环
return false ;
}
2.删除链表中倒数第N个元素
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明给定的 n 保证是有效的。
那么一本题为例,分析一下:
采取双重遍历肯定是可以解决问题的,但题目要求我们一次遍历解决问题,那我们的思路得发散一下。
我们可以设想假设设定了 双指针 p和q的话,当q指向末尾的NULL,p与q之间相隔的元素个数为n时,那么删除掉p的下一个指针就完成了要求。
1.设置虚拟节点dummyHead指向head
2.设定双指针p和q,初始都指向虚拟节点dummyHead
3.移动q,直到p与q之间相隔的元素个数为n
4.同时移动p与q,直到q指向的为NULL,将p的下一个节点指向下下个节点
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode * dHead = new ListNode(0);
dHead -> next = head;
ListNode * p = dHead;
ListNode * q = dHead;
for(int i = 0 ; i < n + 1 ; i ++){
assert(q);
q=q->next;
}
while(q){
p=p->next;
q=q->next;
}
ListNode * delNode=p->next;
p->next=delNode->next;
delete delNode;
ListNode * pNode=dHead->next;
delete dHead;
return pNode;
}
};
结构体和指针
结构体指针有特殊的语法: -> 符号
如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员
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\n",ps->name,ps->age);
return 0;
}
函数和指针
函数的参数和指针
C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。
void change(int a)
{
a++; //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。
}
int main(void)
{
int age = 19;
change(age);
printf("age = %d\n",age); // age = 19
return 0;
}
有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。
传递变量的指针可以轻松解决上述问题。
void change(int* pa)
{
(*pa)++; //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,
//会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
int age = 19;
change(&age);
printf("age = %d\n",age); // age = 20
return 0;
}
再来一个老生常谈的问题:交换元素的值 (典型)
#include<stdio.h>
void swap_bad(int a,int b);
void swap_ok(int*pa,int*pb);
int main()
{
int a = 5;
int b = 3;
swap_ok(&a,&b); //OK
return 0;
}
void swap_ok(int*pa,int*pb)
{
int t;
t=*pa;
*pa=*pb;
*pb=t;
}
有的时候,我们通过指针传递数据给函数不是为了在函数中改变他指向的对象,相反,我们防止这个目标数据被改变。传递指针只是为了避免拷贝大型数据。
考虑一个结构体类型Student。我们通过show函数输出Student变量的数据。
typedef struct
{
char name[31];
int age;
float score;
}Student;
//打印Student变量信息
void show(const Student * ps)
{
printf("name:%s , age:%d , score:%.2f\n",ps->name,ps->age,ps->score);
}
我们只是在show函数中取读Student变量的信息,而不会去修改它,为了防止意外修改,我们使用了常量指针去约束。另外我们为什么要使用指针而不是直接传递Student变量呢?
从定义的结构看出,Student变量的大小至少是39个字节,那么通过函数直接传递变量,实参赋值数据给形参需要拷贝至少39个字节的数据,极不高效。而传递变量的指针却快很多,因为在同一个平台下,无论什么类型的指针大小都是固定的:X86指针4字节,X64指针8字节,远远比一个Student结构体变量小。
const和指针
const到底修饰谁?谁才是不变的?
我大致总结了一下,分享一下。
如果const 后面是一个类型,则跳过最近的原子类型,修饰后面的数据。(原子类型是不可再分割的类型,如int, short , char,以及typedef包装后的类型)
如果const后面就是一个数据,则直接修饰这个数据。
int main()
{
int a = 1;
int const *p1 = &a; //const后面是*p1,实质是数据a,则修饰*p1,通过p1不能修改a的值
const int*p2 = &a; //const后面是int类型,则跳过int ,修饰*p2, 效果同上
int* const p3 = NULL; //const后面是数据p3。也就是指针p3本身是const .
const int* const p4 = &a; // 通过p4不能改变a 的值,同时p4本身也是 const
int const* const p5 = &a; //效果同上
return 0;
}
typedef int* pint_t; //将 int* 类型 包装为 pint_t,则pint_t 现在是一个完整的原子类型
int main()
{
int a = 1;
const pint_t p1 = &a; //同样,const跳过类型pint_t,修饰p1,指针p1本身是const
pint_t const p2 = &a; //const 直接修饰p,同上
return 0;
}
总结与提示
指针和引用这2个名词的区别:他们本质上来说是同样的东西。指针常用在C语言中,而引用,则用于诸如Java,C#等 在语言层面封装了对指针的直接操作的编程语言中。
千万不要解引用为初始化的指针,必须先用已分配的地址初始化它。例如:
int * p; //未初始化的指针
*p=5; //严重的错误
第2行的意思是把5储存在p指向的位置,但是p未被初始化,其值是一个随机值,所以不知道5将储存在何处。
可以用一个现有变量的地址初始化该指针,或者使用malloc()函数为指针先分配内存。
(关于此类函数,今后再一一介绍)