指针的认识与运用
1.什么是指针
C++语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有
唯一的内存地址。
CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。
这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。
也就是说:指针是一种保存变量地址的变量
。
前面已经提到内存其实就是一组有序字节组成的数组,数组中,每个字节大小固定,都是 8bit。
对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:
这是一个 4GB 的内存,可以存放
2
32
2^{32}
232 个字节的数据。
左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。
而指针变量保存的就是这个编号,也即内存地址。
内存的每个字节都有一个唯一的地址,变量的地址是分配给该变量的第一个字节的地址。
假设在程序中定义了以下变量:
char letter;
short number;
float amount;
下图说明了它们在内存中是如何安排的,并显示了它们的地址。
上图内存中的变量及其地址,变量 letter 显示在地址 1200 处,变量 number 在地址
1201 处,而变量 amount 则在地址 1203 处。
指针的三个属性
指针是一个对象,与指针相关的属性有三个,分别是指针本身的值(value)、指针所指的对象(content)以及指针本身在内存中的存储位置(address)。
他们的含义分别是:
指针本身的值是一个内存地址值,表示指针所指对象在内存中的存储地址;指针所指的对象可以通过解引用指针访问;因为指针也是一个对象,所以指针也存储在内存的某个位置,它有它的地址,这也是为什么有“指针的指针”的原因。
2.为什么要使用指针
在 C 语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。
总的来说,使用指针有如下好处:
- 指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速
高效; - C 语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;
- C 语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函
数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。
3.如何声明一个指针
3.1 声明并初始化一个指针
指针其实就是一个变量,指针的声明方式与一般的变量声明方式没太大区别:
指针类型如何表示:
XX 类型 * XX 指针类型
指针的声明比普通变量的声明多了一个一元运算符 “*”
。
#include<bits/stdc++.h>
using namespace std;
int *p1;//声明一个int类型的指针p1
char *p2;//声明一个char类型的指针p2
int **p3;//声明一个指针p3,该指针指向int类型的指针
int *a1[100];//声明一个指针数组,该数组有100个元素
int (*a2)[100];//声明一个数组指针,该指针指向一个int类型的一维数组
int *a3(int x,int y);
struct aa{
string name;
char sex;
float sorce;
}*p4;
运算符 “*”
是间接寻址或者间接引用运算符。
当它作用于指针时,将访问指针所指向的对象。
在上述的声明中: p 是一个指针,保存着一个地址,该地址保存着一个变量; *p 则会访问这个地址所指向的变量。
声明一个指针变量并不会自动分配任何内存。
在对指针进行间接访问之前,指针必须进行初始化:或是使他指向现有的内存,或者给它动态分配内存,否则我们并不知道指针指向哪儿,这将是一个很严重的问题。
初始化操作如下:
方法 1:使指针指向现有的内存
int x = 1;
int *p = &x; //指针 p 被初始化,指向变量 x,其中取地址符&用于产生操作数内存地址
方法 2:动态分配内存给指针
int *p;
p = (int *)malloc(sizeof(int) * 10); // malloc 函数用于动态分配内存
free§; // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用。
指针的初始化实际上就是给指针一个合法的地址,让程序能够清楚地知道指针指向哪儿。malloc 的全称是 memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以 void*类型返回分配的内存区域地址,所以必须通过 (int *) 来将强制转换。当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
这里定义了一个整型指针 p 保存 num 的地址(0x1000),num 的地址就是首字节的地址,而指针 p 本身的地址为 0x2000。
指针的几个相关操作说明表 |
---|
说 明 | 样 例 |
---|---|
指针定义: 类型说明符 *指针变量名; | int a=10; int *p; |
取地址运算符:& | p=&a; |
间接运算符:* | *p=20; |
指针变量直接存取的是内存地址 | cout<<p; 结果可能是:0x4097ce |
间接存取的才是储存类型的值 | cout<<*p; 结果是:20 |
一般的,我们可以这样看指针(int *p)与普通变量(int a)的对应关系:
p | &a |
---|---|
*p | a |
*p=3 | a=3 |
3.2 未初始化和非法的指针
如果一个指针没有被初始化,那么程序就不知道它指向哪里。它可能指向一个非法地址,这时,程序会报错,在 Linux 上,错误类型是 Segmentation fault(coredumped),提醒我们段违例或内存错误。 它也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改,而你并无意去修改它。 用一个例子简单的演示一下:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int *p;
*p=1;
printf("%d",*p);
return 0;
}
这个程序可以编译通过,但是无法正常运行。
要想使这个程序运行起来,需要先对指针 p 进行初始化:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int x=1;
int *p=&x;
printf("%d\n",*p);
*p=2; //x=2;
printf("%d\n",*p);
return 0;
}
这段代码的输出结果如下:
1
2
可以看到,对指针进行初始化后,便可以正常对指针进行赋值了。
3.3 NULL 指针
NULL 指针是一个特殊的指针变量,表示不指向任何东西。
可以通过给一个指针赋一个零值来生成一个 NULL 指针。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int *p=NULL;
printf("%p的地址为%d",*p);
return 0;
}
程序输出:
p的地址为0
可以看到指针指向内存地址 0。
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是为操作系统保留的。
但是,内存地址 0 有一个特别重要的意义,它表明该指针不指向一个可访问的内存位置。
指针的几个初始化操作说明表 |
---|
方 法 | 说 明 | |
---|---|---|
1 | int *p=NULL; | NULL 是特殊的地址 0,叫零指针 |
2 | int a; int *p=&a | p 初始化为 a 的地址 |
3 | int *p=new(int); | 申请一个空间给 p,*p 内容不确定 |
new 其实就是告诉计算机开辟一段新的空间,new 出来的是一段空间的首地址,所以一般需要用指针来存放这段地址。
4.指针的运算
C 指针的算术运算只限于两种形式:
1. 指针 +/- 整数 :
可以对指针变量 p 进行 p++、p–、p+i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数。
用一张图来说明一下:
在上图中,10000000 等是内存地址的十六进制表示(数值是假定的),p 是一个 int 类型的指针,指向内存地址 0x10000008 处。
则 p++ 将指向与 p 相邻的下一个内存地址,由于 int 型数据占 4 个字节,因此 p++ 所指的内存地址为 1000000b。
其余类推。不过要注意的是,这种运算并不会改变指针变量 p 自身的地址,只是改变了它所指向的地址。
2.指针 - 指针
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。
两个指针相减的结果的类型是 ptrdiff_t,它是一种有符号整数类型。
ptrdiff_t 是 C/C++标准库中定义的一个与机器相关的数据类型,ptrdiff_t 类型变量通常用来保存两个指针减法操作的结果ptrdiff_t 定义在 stddef.h(cstddef)这个文件内,ptrdiff_t 通常被定义为 long int 类型。
减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int a[10]={3,2,8,5,23,25,27,48,59,10};
int sub;
int *p1=&a[2];
int *p2=&a[8];
sub=p2-p1;
printf("%d",sub);
return 0;
}
输出结果为:6
5.指针与数组
在 C 语言中,指针与数组之间的关系十分密切。实际上,许多可以用数组完成的工作都可以使用指针来完成。
一般来说,用指针编写的程序比用数组编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。
5.1 指针与数组的关系
我们先声明一个数组:
int a[10]; // 声明一个 int 类型的数组,这个数组有10个元素
我们可以用 a[0]、a[1]、…、a[9] 来表示这个数组中的 10 个元素,这 10 个元素是存储在一段连续相邻的内存区域中的。
接下来,我们再声明一个指针:
int *p; // 声明一个 int 类型的指针变量
p 是一个指针变量,指向内存中的一个区域,如果我们对指针 p 做如下的初始化:
p = &a[0]; // 对指针进行初始化,p 将指向数组 a 的第 1 个元素 a[0]
我们知道,对指针进行自增操作会让指针指向与当前元素相邻的下一个元素,即 *(p + 1) 将指向 a[1] ;同样的, *(p + i) 将指向 a[i] 。
因此,我们可以使用该指针来遍历数组 a[10] 的所有元素。
可以看到,数组下标与指针运算之间的关系是一一对应的。
int a[105],int p=&a; |
---|
表示 | 意义 |
---|---|
p,a,&a[0] | 数组 a 的首地址,也是 0 号元素的首地址 |
p+1,a+1,&a[1] | 指向 1 号元素 a[1],即元素 a[1]的首地址 |
p+i,a+i,&a[i] | 指向 i 号元素 a[i],即元素 a[i]的首地址 |
区 别 | 指针 p 是一个变量,而数组名 a 是一个常量 |
两种方法访问数组 |
---|
下标法 | p[i],a[i] |
---|---|
下标法 | 直接访问数组元素 |
指针法 | (p+i),(a+i) |
指针法 | 间接访问数组元素 |
而根据定义,数组类型的变量或表达式的值是该数组第 1 个元素的地址,且数组名所代表的的就是该数组第 1 个元素的地址,故上述赋值语句可以直接写成:
p = a; // a 为数组名,代表该数组最开始的一个元素的地址
所以,数组名相当于是一个指针常量!
int arr[5] = {1,2,3,4.5};
int *p = arr; //用指针变量 p 指向 arr,p 中存储的是数组首元素的地址。
当我们在对数组成员进行访问时就可以利用数组名下标以及指针间接访问两种方式,如以下表达式所示:
arr[ 1 ]; //访问数组中的第 2 个元素。
*( arr+1 ); //与 arr[ 1 ] 作用相同。
*arr + 1 ; //*的优先级高于+,表示第一个元素的值+1。
arr[5]; //访问越界,产生随机值。
很显然,一个通过数组和下标实现的表达式可以等价地通过指针及其偏移量来实现,这就是数组和指针的互通之处。
但有一点要明确的是,数组和指针并不是完全等价,指针是一个变量,而数组名是一个常量,它是数组中第 1 个元素的地址。
数组可以看做是一个用于保存变量的容器。更直接的方法,我们可以直接看二者的地址,并不一样:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int x[10]={3,2,8,5,23,25,27,48,59,10};
int *p=x;
printf("x的地址为:%p\n",x);
printf("x[0]的地址为:%p\n",&x[0]);
printf("p的地址为:%p\n",&p);
p+=2;
printf("*(p+2)的值为:%d\n",*p);
return 0;
}
输出结果如下:
x 的地址为:000000000022fe20
x[0] 的地址为:000000000022fe20
p 的地址为:000000000022fe48
*(p+2)的值为:8
可以看到,x 的值与 x[0]的地址是一样的,也就是说数组名即为数组中第 1 个元素的地址。实际上,打印 &x 后发现,x 的地址也是这个值。
而 x 的地址与指针变量 p 的地址是不一样的。
故而数组和指针并不能完全等价。
5.2 指针数组
指针是一个变量,而数组是用于存储变量的容器,因此,指针也可以像其他变量一样存储在数组中,也就是指针数组。
指针数组:首先它是一个数组,数组的每一个元素都是指针,数组占多少个字节由数组本身决定,它是“储存指针的数组”的简称
声明一个指针数组的方法如下:
int *p[14]; // 声明一个指针数组,该数组有 10 个元素,其中每
个元素都是一个指向 int 类型的指针
在上述声明中,由于 [ ] 的优先级比 * 高,故 p 先与 [ ] 结合,成为一个数组 p[ ];
再由 int * 指明这是一个 int 类型的指针数组,数组中的元素都是 int 类型的指针。
数组的第 i 个元素是 *p[i],而 p[i] 是一个指针。
由于指针数组中存放着多个指针,操作灵活,在一些需要操作大量数据的程序中使用,可以使程序更灵活。
5.3 数组指针
数组指针:是一个指针,它指向一个数组。
在 32 位系统下永远是占 4 个字节,至于它指向的数组占多少字节,不知道,它是“指向数组的指针”的简称。
声明一个数组指针的方法如下:
int (*p)[10]; // 声明一个数组指针 p ,该指针指向一个数组
由于 ( ) 的优先级最高,所以 p 是一个指针,指向一个 int 类型的一维数组,这个一维数组的长度是 10,这也是指针 p 的步长。
也就是说,执行 p+1 时,p 要跨过 n 个 int 型数据的长度。
数组指针与二维数组联系密切,可以用数组指针来指向一个二维数组,如下:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int a[2][3]={1,2,3,4,5,6}//定义一个二维数组并初始化
int (*p)[3];//定义一个数组指针,指针指向一个含有3个元素的一维数组
p=a;//将二维数组的首地址赋给p,此时p指向a[0]或 &a[0][0]
printf("%d\n",(*p)[0]);
p++;//对p进行算数运算,此时p将指向二维数组的下一行的首地址,即 &a[1][0]
printf("%d\n",(*p)[0]);//输出4
printf("%d\n",(*p)[1]);//输出5
printf("%d\n",(*p)[2]);//输出6
return 0;
}
6.指针与结构体
6.1 简单介绍一下结构体
结构体是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。
由于结构体将一组相关的变量看做一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。
声明一个结构体的方式如下:
struct student
{
string name;
char sex;
int score;
};
student t={"xxx",'f',150};
可以使用 结构体名.成员 的方式来访问结构体中的成员,如下:
t.name; t.sex; t.score;
6.2 结构体指针
结构体指针是指向结构体的指针,结构体指针变量定义的一般形式:结构体名 *结构体指针变量名;
student *p; // 声明一个结构指针 p ,该指针指向一个 student 类型的结构
*p = &t; // 对结构指针的初始化与普通指针一样,也是使用取地址符 &
引用结构体指针变量指向的结构体变量的成员的方法如下:
1.指针名->成员名
2.(*指针名).成员名
即 (*p).score 与 p->score 是等价的。
#include<bits/stdc++.h>
using namespace std;
struct student{
char name[20];
char sex;
char score;
};
int main()
{
student t={"xxx",'f',150};
student *p=&t;
printf("%s\n",p->name);
printf("%c\n",(*p).sex);
return 0;
}
输出:
fire
f
7.指针与函数
C 语言的所有参数均是以“传值调用”的方式进行传递的,这意味着函数将获得参数值的一份拷贝。这样,函数可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。
7.1 指针作为函数的参数
传值调用的好处是是被调函数不会改变调用函数传过来的值,可以放心修改。
但是有时候需要被调函数回传一个值给调用函数,这样的话,传值调用就无法做到。为了解决这个问题,可以使用传指针调用。
指针参数使得被调函数能够访问和修改主调函数中对象的值。
用一个例子来说明:
#include<bits/stdc++.h>
using namespace std;
void swap1(int a,int b)//参数为int变量
{
int t;
t=a;
a=b;
b=t;
}
void swap2(int *a,int *b)//参数为指针,接受调用函数传递过来的变量地址作为参数
{ //对所指地址的内容进行操作
int t;//最终结果是,地址本身没有改变
t=*a;//但是这一地址所对应的内存段中的内容发生了变化
*a=*b;//即x,y的值发生了变化
*b=t;
}
int main()
{
int x=1,y=2;
swap1(x,y);//将x,y的值本身作为参数传递给了被调函数
printf("%d %5d\n",x,y);//输出结果:1 2
swap2(&x,&y);//将x,y的地址作为参数传递给了被调函数
printf("%d %5d\n",x,y);//传递过去的也是一个值,与传值调用不冲突
//输出结果:2 1
return 0;
}
0 7.2 指向函数的指针
在 C 语言中,函数本身不是变量,但是可以定义指向函数的指针,也称作函数指针,函数指针指向函数的入口地址。
这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。
声明一个函数指针的方法如下:
返回值类型 (* 指针变量名)([形参列表]);
int ( *pointer)(int *,int *); // 声明一个函数指针
上述代码声明了一个函数指针 pointer ,该指针指向一个函数,函数具有两个 int * 类型的参数,且返回值类型为 int。 下面的代码演示了函数指针的用法:
#include<bits/stdc++.h>
using namespace std;
void t1()
{
cout<<"test1";
}
void t2()
{
cout<<"test2";
}
void t3()
{
cout<<"test3";
}
void t4()
{
cout<<"test4";
}
void t5()
{
cout<<"test5";
}
typedef void(*LP)();
int main()
{
LP a[]={t1,t2,t3,t4,t5};
int x;
cin>>x;
a[x]();
return 0;
}
注意,声明一个函数指针时,() 不能漏掉。否则:int *p(void *,void*);
这表明 p 是一个函数,该函数返回一个指向 int 类型的指针。
7.3 函数返回指针
一个函数可以返回整数值、字符值、实型值等,也可以返回指针联系的数据(即地址)。
返回指针值的函数的一般定义形式为:
类型名 * 函数名(参数列表);
int *a(int x,int y);a
是函数名,调用它后得到一个指向整型数据的指针(地址)。x 和 y 是函数 a 的形参,为整型。
注意:在*a 的两侧没有括号;在 a 的两侧分别为*运算符和()运算符,由于()的优先级高于 *,因此 a 先于()结合。在函数前面有一个*,表示此函数是返回指针类型的函数。
【例】编写一个函数,用于在一个包含 N 个整数的数组中找到第一个质数,若有则返回函数的地址;否则返回 NULL(空指针)。
#include<bits/stdc++.h>
using namespace std;
int n;
int a[100000];
bool isprime(int n)//判断n是否为质数
{
if(n<2)
{
return 0;
}
int m=(int)sqrt(n+0.5);
for(int i=2;i<=m;i++)
{
if(n%i==0)
{
return 0;
}
}
return 1;
}
int* find()
{
for(int i=1;i<=n;i++)
{
if(isprime(a[i])==1)
{
return &a[i];//同价于:return a+i;
}
}
return NULL;//没找到,返回NULL(空指针)
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int *p=find();
if(p!=NULL)
{
printf("%d\n%d\n",p,*p);//输出这个质数的地址和它本身
}
else
{
printf("can't find!");
}
return 0;
}