【原创】浅谈指针(二)

上期链接

https://www.cnblogs.com/jisuanjizhishizatan/p/15365167.html

前言

最近,指针确实逐渐淡出我们的生活了。但是,指针又是必不可少的,它在日常编程中又有着很大的作用。曾经noip初赛的阅读程序写结果,还经常考指针题,以及函数的传参机制,例如*a++这个语句。然而近两年,这些题目也不再出现了。
指针其实是C++中一个非常值得深究的语法。到目前为止,可能说,没有人能够完全理解指针。我们所学的,只是指针中,极小的一部分。
好了,让我们开始吧,继续今天的指针学习。

函数的传参机制

现在,让我们输入两个数,将两数反转后输出。可能有人会问,标准库不是有一个swap函数吗?那好,我们就自己写一个swap函数。

#include<iostream>
using namespace std;
void Swap(int a,int b){
    int temp=a;a=b;b=temp;
}
int main(){
  int a,b;
  cin>>a>>b;
  Swap(a,b);
  cout<<a<<" "<<b;
  return 0;
}

那好,我们来运行吧。输入:4 5
输出:4 5

看来这个函数有点问题啊,没有交换变量的值。我们尝试把他写进主函数,像是这样:

#include<iostream>
using namespace std;
//void Swap(int a,int b){
//    int temp=a;a=b;b=temp;
//}
int main(){
  int a,b;
  cin>>a>>b;
  //Swap(a,b);
  int temp=a;a=b;b=temp;
  cout<<a<<" "<<b;
  return 0;
}

这样输出是正常的,这就奇怪了,为什么没有交换变量的值呢?

我们来做个实验。运行下面代码,看看它会输出什么?

#include<iostream>
using namespace std;
void Swap(int a,int b){
    int temp=a;a=b;b=temp;
    cout<<&a<<" "<<&b<<endl;
}
int main(){
  int a,b;
  cin>>a>>b;
  Swap(a,b);
  //int temp=a;a=b;b=temp;
  cout<<&a<<" "<<&b<<endl;
  return 0;
}

我在wandbox在线编译器运行了这个代码,输出是:
0x7ffdaa89e65c 0x7ffdaa89e658
0x7ffdaa89e68c 0x7ffdaa89e688

在菜鸟在线编译器的输出:
0x7ffd6da5b4fc 0x7ffd6da5b4f8
0x7ffd6da5b52c 0x7ffd6da5b528

可以看到,在多个编译器中,输出的main函数中ab的地址和swap函数中ab地址是不同的。既然它们被保存在不同的地址,swap函数中的ab交换了,但是main函数的ab没有交换。
就像某个上司让他的部下交换他档案柜的文件,但是上司给部下的文件是档案柜中的复印件,那么那位部下无论怎么做,都无法把档案柜的文件交换。这是同样的道理。
那我们怎么办呢?我们可以尝试用指针解决这个问题。这是唯一的方法。
(更准确的说,使用引用同样可以解决问题,引用会在下一章予以介绍)

#include<iostream>
using namespace std;
void Swap(int *a,int *b){
    int temp=*a;*a=*b;*b=temp;
}
int main(){
  int a,b;
  cin>>a>>b;
  Swap(&a,&b);
  cout<<a<<" "<<b<<endl;
  return 0;
}

我们如果向swap函数传递a和b的地址,那么,swap里面的a和b,其实就是main里面的a和b。这样一来,就可以交换了。如果那个上司告诉了他的部下档案柜的地址,那么部下就可以根据地址,找到柜子里的文件,自然也就可以交换柜子里的文件了。
有没有发现,这里swap函数中,参数是传递的地址,那么参数前必须加&号。想起来了吗?scanf也是这样写的!实际上,scanf也使用了指针的机制!

链表

struct LIST{
    int n;
    struct LIST* next;
};

链表中,需要知道下一个元素的地址才能进行查找下一个元素。因此,需要把指针作为结构体成员。最后一个元素的next置放NULL,通知程序“后面已经没有元素了”。
如果要查找节点s的下一个元素,就是(*s).next,注意括号虽然麻烦但是不可省略。
当然,还有一种简写形式,即
(*s).next=s->next

如果要删除元素,我们可以这样执行:

LIST *x=s->next;
s->next=s->next->next;
free(x);

连用了两次结构体运算符->。

函数指针

上一篇文章说过,程序是保存在内存中的,自然也可以使用指针指向我们的程序中的函数。这种指针称作函数指针。

#include<bits/stdc++.h>
using namespace std;
int f(int a){
    printf("a..%d",a);
}
int (*p)(int a);
int main(){
    p=f;
    (*p)(5);
}

int (*p)(int a);一句表示声明一个叫做a的函数指针。有人会问,这是指向函数的指针,先把表示指针的*号使用括号括起来是不是很奇怪?事实上,由于表示函数的()优先级比*高,如果不加括号,
int *p(int a);

编译器会把它当作一个返回值是int*的函数p。就不是函数指针了。

把函数指针当作参数使用

stdlib.h中,有一个函数atexit,作用是“当程序正常退出时执行这一函数”。程序实例:

#include<bits/stdc++.h>
using namespace std;
void f(){
    cout<<"Hello, World!";
}
int atexit(void (*func)(void));
int main(){
    atexit(f);
    return 0;
}

其中,atexit的参数就是一个函数指针,将地址f赋值给了atexit的参数地址func,在结束时执行f。
顺便一提,既然函数可以看成指针,那么能否对其执行取数值操作呢?使用*运算符可以取到地址的数值。答案是不能。在表达式中,如果对函数地址前添加*号,f暂时会变成函数。但由于在表达式中,它又会变为“指向函数的指针”。也就是说,这种情况下,对函数用*运算符无意义。
因此,

#include<bits/stdc++.h>
using namespace std;
int main(){
    (********printf)("hello");
    return 0;
}

这样的操作也能输出hello。

从1开始的数组

众所周知,C++的数组从0开始,但是使用某些指针的技巧,可以使数组下标从1开始计数。

#include<bits/stdc++.h>
using namespace std;
int a[10];
int *p;
int main(){
    p=&a[-1];
    for(int i=1;i<=10;i++)cin>>p[i];
    for(int i=1;i<=10;i++)cout<<p[i]<<" ";
}

程序把p指向了不存在的元素a[-1],这样,p[1]等于a[0],p[10]等于a[9],就可以让下标从1到10计算了。
当然,这个程序违反了C标准,标准规定指针只能指向数组内的元素和数组最后元素的下一个元素,其他情况均属于未定义(这与是否发生读写无关)。至于为什么标准允许让指针指向数组最后元素的下一个元素(例如在上例中指向不存在的a[10]),大家可以自己探究,我将会在下期给出答案。
顺带一提,fortran的数组从1开始计数,因此,为了把fortran程序移植到c程序过程中,经常使用这种“违背标准的技巧”。

完。下期再见。

posted @ 2021-10-04 09:48  计算机知识杂谈  阅读(227)  评论(6编辑  收藏  举报