C++的指针
在C++中,指针被称为是C/C++中的精髓所在,指针是存放内存地址的一种变量,特殊的地方就在它存放的是内存地址。
计算机中的内存都是编址的,每个地址都有一个符号。指针是一个无符号整数,它是一个以当前系统寻址范围为取值范围的整数,声明指针和声明一个无符号整数实质上并无区别。
一、变量内存实质
(一)变量的实质
要理解指针,首先就要理解“变量”存储实质,内存空间图:
![](https://img-blog.csdnimg.cn/20191008221255898.png)
根据图中,内存只不过是一个存放数据的空间,可以将其理解成装水果的篮子、电影院的座位。在电影院中每个位置都有编号,而内存中要存放各种各样的数据,所以我们需要知道这些数据存放的位置。所以内存也需要像影院那样需要编号。也就是说内存编址(为内存进行地址编码)。内存按一个字节接着一个字节的次序进行编址,每个字节都有个编号,称为内存地址。
内存编址:
![](https://img-blog.csdnimg.cn/20191008222404473.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tpbmcxMzA1OTU5NTg3MA==,size_16,color_FFFFFF,t_70)
程序中写下了语言声明:
int i;
char a;
它其实是内存中申请一个名为i的整形变量宽度空间(DOS 下的 16 位编程中其宽度为 2 个字节),和一个名为 a 的字符型变量宽度的空间(占 1 个字节)。
在内存中映象:
![](https://img-blog.csdnimg.cn/20191008223156835.png)
图中可看出,i在内存起始地址为6的上面申请了两个字节的空间(假设int的宽度为16位,不同系统中 int 的宽度可能是不一样的,最常用的win32环境下为4个字节)并将其命名为i。a在内存地址为8上申请一个字节的空间,并命名为a。这样我们就拥有两个不同类型的变量,看变量如何给变量进行赋值。
(二)赋值给变量
i=30;
a='p';
两个语句将30存入i变量的内存空间中,将'p'字符存进a变量的内存空间中,可以这样理解
![](https://img-blog.csdnimg.cn/20191009115744698.png)
将30存在以6为起始地址的两个字节空间里,a的内存地址为8上申请了一字节的空间存入了'p',那么变量i和a在哪里?
(三)变量位置
使用&i取i变量所在的地址编号,返回的是i变量的地址编号,(返回i变量地址编号)
#include <iostream>
#include <string>
using namespace std;
int main(){
int i=30;
cout<<"&i= "<<&i<<endl;
cout<<"i= "<<i<<endl;
return 0;
}
![](https://img-blog.csdnimg.cn/20191009120425388.png)
输出的&i的值为0x6ffe3c(windows 64位)就是我们图示中内存空间编码为6的内存地址。接下来就进入我们真正的主题——指针
二、指针
计算机中的内存都是编址的,每个地址都有一个符号,就像家庭地址或者IP地址一样。指针,是一个无符号整数(unsigned int),它是一个以当前系统寻址范围为取值范围的整数。声明指针和声明一个无符号整数实质上并无区别。
指针是存放内存地址的一种变量,特殊的地方就在它存放的是内存地址。因此,指针的大小不会像其他变量一样变化,只跟当前平台相关——不同平台内存地址的范围是不一样的,32位平台下,内存最大为4GB,因此只需要32bit就可以存下,所以sizeof(pointer)的大小是4字节。64位平台下,32位就不够用了,要想内存地址能够都一一表示,就需要64bit(但是目前应该没有这么大的内存吧?),因此sizeof(pointer)是8。
比如有天你说你要学习C++,要借我的这本 C++ Primer Plus,我把书给你送过去发现你已经跑出去打篮球了,于是我把书放在了你桌子上书架的第三层四号的位置。并写了一张纸条:你要的书在第三层四号的书架上。贴在你门上。当你回来时,看到这张纸条,你就知道了我借与你的书放在哪了。你想想看,这张纸条的作用,纸条本身不是书,它上面也没有放着书。那么你又如何知道书的位置呢?因为纸条上写着书的位置嘛!聪明!!!其实这张纸条就是一个指针了。它上面的内容不是书本身,而是
书的地址,你通过纸条这个指针找到了我借给你的这本书。
声明一个指向整型变量的指针的语句: int *pi;
pi是一个指针,其实它也是一个变量,与变量并没有实质的区别。
![](https://img-blog.csdnimg.cn/20191009121931898.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tpbmcxMzA1OTU5NTg3MA==,size_16,color_FFFFFF,t_70)
(说明:这里我假设了指针只占 2 个字节宽度,实际上在 32 位系统中,指针的宽度是 4 个字节宽的,即 32 位。)
由图示中可以看出,我们使用“int *pi”声明指针变量 —— 其实是在内存的某处声明一个一定宽度的内存空间,并把它命名为 pi。你能在图中看出pi 与前面的 i、a 变量有什么本质区别吗?没有,当然没有!肯定没有!!真的没有!!!pi 也只不过是一个变量而已嘛!那么它又为什么会被称为“指针”?关键是我们要让这个变量所存储的内容是什么。现在我要让 pi 成为具有真正“指针”意义的变量。请接着看下面语句:
pi=&i;
把i地址的编号赋值给pi。并在pi里面写上i的地址编号;
![](https://img-blog.csdnimg.cn/20191009122325361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tpbmcxMzA1OTU5NTg3MA==,size_16,color_FFFFFF,t_70)
执行完 pi=&i 后,在图示中的内存中,pi 的值是 6。这个 6 就是i 变量的地址编号,这样 pi 就指向了变量 i 了。你看,pi 与那张纸条有什么区别?pi 不就是那张纸条嘛!上面写着 i 的地址,而 i 就是那本厚书C++ Primer Plus。你现在看懂了吗?因此,我们就把 pi 称为指针。所以你要牢牢记住:指针变量所存的内容就是内存的地址编号 ! 也会随着你不断的学习对这句话会理解的越来越深。我们就可以通过这个指针 pi 来访问到 i 这个变量了:
#include <iostream>
#include <string>
using namespace std;
int main(){
int i=30;
cout<<"&i= "<<&i<<endl;
cout<<"i= "<<i<<endl;
int *pi=&i;
cout<<"*pi= "<<*pi<<endl;
return 0;
}
![](https://img-blog.csdnimg.cn/20191009122755708.png)
pi 内容所指的地址的内容(读上去好像在绕口令了),就是 pi 这张“纸条”上所写的位置上的那本 “书”—— i 。你看,Pi 的内容是 6,也就是说 pi 指向内存编号为 6 的地址。*pi嘛,就是它所指地址的内容,即地址编号 6 上的内容了,当然就是 30 这个“值”了。所以这条语句会在屏幕上显示 30。我们的纸条就是我们的指针,同样我们的 pi 也就是我们的纸条!剩下的就是我们如何应用这张纸条了。如何用?下面的代码并正确理解含义。
#include <iostream>
#include <string>
using namespace std;
int main(){
int a,*pa;
a=10;
cout<<"&a= "<<&a<<endl;
cout<<"a= "<<a<<endl;
pa=&a;
*pa=20;
cout<<"*pa= "<<*pa<<endl;
cout<<"a= "<<a<<endl;
return 0;
}
![](https://img-blog.csdnimg.cn/20191009130652142.png)
三、二级指针(指针的指针)
(一)二级指针,是一种指向指针的指针。我们可以通过它实现间接访问数据,和改变一级指针的指向问题。
![](https://img-blog.csdnimg.cn/20191009131040710.png)
#include <iostream>
#include <string>
using namespace std;
int main(){
int i=30;
cout<<"&i= "<<&i<<endl;
cout<<"i= "<<i<<endl;
int *pi=&i;
cout<<"*pi= "<<*pi<<endl;
int* *ppi=π
cout<<"**ppi= "<<**ppi<<endl;
cout<<endl;
**ppi=40;
cout<<"i= "<<i<<endl;
cout<<"*pi= "<<*pi<<endl;
cout<<"**ppi= "<<**ppi<<endl;
return 0;
}
结果如下:
![](https://img-blog.csdnimg.cn/20191009131522341.png)
(二)间接数据访问
1.改变一级指针指向
#include <iostream>
#include <string>
using namespace std;
int main(){
int i=30;
int *pi=&i;
cout<<"一级指针*pi= "<<*pi<<endl;
int* *ppi=π
cout<<"二级指针**ppi= "<<**ppi<<endl;
cout<<endl;
*pi=40;
cout<<"改变一级指针内容:*pi= "<<*pi<<endl;
cout<<"一级指针*pi= "<<*pi<<endl;
cout<<endl;
int b=10;
*ppi=&b;
cout<<"改变一级指针指向*pi= "<<*pi<<endl;
cout<<"二级指针**ppi= "<<**ppi<<endl;
return 0;
}
![](https://img-blog.csdnimg.cn/20191009133452871.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tpbmcxMzA1OTU5NTg3MA==,size_16,color_FFFFFF,t_70)
2.改变n-1级指针的指向
可以通过一级指针,修改0级指针(变量)的内容;
可以通过二级指针,修改一级指针的指向;
可以通过三级指针,修改二级指针的指向;
...
可以通过修改n级指针,修改n-1级指针的指向。
3.二级指针的步长
所以类型的二级指针,由于均指向一级指针类型,一级指针类型大小为4,所以二级指针步长也为4。
四、指针与数组
(一)指针与数组名
1.通过数组名访问数组元素
#include<iostream>
using namespace std;
int main(){
int i,a[]={1,2,3,4,5,6,7,8,9,10};
for(i=0;i<=9;i++)
cout<<a[i]<<" ";
cout<<endl;
for(i=0;i<=9;i++)
cout<<*(a+i)<<" ";
return 0;
}
![](https://img-blog.csdnimg.cn/20191009134655741.png)
2.通过指针访问数组元素
#include<iostream>
using namespace std;
int main(){
int i,a[]={1,2,3,4,5,6,7,8,9,10};
int *pa;
pa=a;
for(i=0;i<=9;i++)
cout<<pa[i]<<" ";
cout<<endl;
for(i=0;i<=9;i++)
cout<<*(pa+i)<<" ";
return 0;
}
3.数组名与指针变量区别
#include<iostream>
using namespace std;
int main(){
int i,a[]={1,2,3,4,5,6,7,8,9,10};
int *pa;
pa=a;
for(i=0;i<=9;i++){
cout<<*pa;
pa++; //指针值被修改
}
return 0;
}
可以看出,这段代码也是将数组各元素值输出。不过,你把循环体{}中的 pa改成 a 试试。你会发现程序编译出错,不能成功。看来指针和数组名还是不同的。其实上面的指针是指针变量,而 数组名只是一个指针常量。
(二)指针数组
虽然说指针数组,但是本质上还是数组,数组中每一个成员时一个指针。
char *pArray[10];
pArray先与"[]"结合,构成一个数组,char*修饰数组内容,即数组的每个元素。
![](https://img-blog.csdnimg.cn/2019100914130675.png)
#include<iostream>
#include<stdlib.h>
using namespace std;
int main(){
char *pArray[]={"apple","banner","cat","dog","egg"};
for(int i=0;i<sizeof(pArray)/sizeof(*pArray);i++){
cout<<pArray[i]<<endl;
}
return 0;
}
![](https://img-blog.csdnimg.cn/20191009141742118.png)
(三)二级指针与指针数组
1.指针数组名赋给二级指针的合理性
二级指针与指针数组名等价原因:
char **p是二级指针
char* array[N];array=&array[0]; array[0]本身是char* 型
char **p=array;
#include<iostream>
#include<stdlib.h>
using namespace std;
int main(){
char *pArray[]={"apple","banner","cat","dog","egg"};
cout << "**********pArray[i]************" << std::endl;
for(int i=0;i<sizeof(pArray)/sizeof(*pArray);i++){
cout<<pArray[i]<<endl;
}
char **pArr=pArray;
cout<<"***********pArr[i]****************"<<endl;
for(int i=0;i<sizeof(pArray)/sizeof(*pArray);i++){
cout<<pArr[i]<<endl;
}
return 0;
}
![](https://img-blog.csdnimg.cn/20191009142441695.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tpbmcxMzA1OTU5NTg3MA==,size_16,color_FFFFFF,t_70)
(三)完美匹配前提
数组名,赋给指针以后,就会少了唯独的概念,所以用二级指针访问指针数组,需要维度,也可以不需要。
#include<iostream>
#include<stdlib.h>
using namespace std;
int main(){
cout << "**********one************" <<endl;
int arr[10]={1};
for(int i=0;i<10;i++){
cout<<arr[i]<<endl;
}
int *parr=arr;
for(int i=0;i<10;i++)
cout<<*parr++<<endl;
cout<<"************two************"<<endl;
char *str="banner";
while(*str){
cout<<*str++<<endl;
}
char* pArray[]={"apple","banner","cat","dog","egg",NULL};
char **pa=pArray;
while(*pa!=NULL){
cout<<*pa++<<endl;
}
return 0;
}
![](https://img-blog.csdnimg.cn/20191009143552737.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2tpbmcxMzA1OTU5NTg3MA==,size_16,color_FFFFFF,t_70)
五、堆空间与指针
(一)堆上的一维空间
1.返回值返回(一级指针)
char* allocSpace(int n){
char *p=(char*)malloc(n);
return p;
}
2.参数返回(二级指针)
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;
int allocSpace(char **p,int n){
*p=(char*)malloc(n);
return *p==NULL?-1:1;
}
int main(){
char *p;
if(allocSpace(&p,100)<0){
return -1;
}
strcpy(p,"banner");
//cout<<p<<endl;
printf("%s\n",p);
free(p);
return 0;
}
输出结果为:banner
3.堆上的二维空间
二维数组,是一种二维空间,但不是代表着二维空间就是二维数组;二维空间并不一定就是二维数组,但是可以具有数组的访问形式,但也已经远远不是数组的定义。
3.1使用指针做函数返回值:
(1)当使用指针做为函数的返回值时,主函数处的char *p;将获得调用函数char *pf;的值,即一个地址值,如oxAE72。此时需要我们注意的是该地址值所指向的空间是否存在(即已向操作系统声明注册,不会被释放,即可能被其他操作修改);
(2)使用栈内存返回指针是明显错误的,因为栈内存将在调用结束后自动释放,而主函数使用该地址空间将很危险
char *GetMemory(){
char p[]="banner";
return p;
}
int main(){
char *str=GetMemory(); //报错,得到一块已经释放的内存
printf(str);
}
(3)使用堆内存返回指针,但需要注意的是内存泄露问题,在使用完成后在主函数中释放该段内存
char *GetMemory(){
char *p=new char[100];
return p;
}
int main(){
char *str=GetMemory();
delete[] str; //防止内存泄露问题
}
3.2使用指针做函数参数
1、有的情况下我们可能需要需要在调用函数中分配内存,而在主函数中使用,而针对的指针此时为函数的参数。此时应注意形参与实参的问题,因为在C语言中,形参只是继承了实参的值,是另外一个量(ps:返回值也是同理,传递了一个地址值(指针)或实数值),形参的改变并不能引起实参的改变。
2、直接使用形参分配内存的方式是错误的,因为实参的值并不会改变,下面则实参一直为NULL:
void GetMemory(char* p){
char *p=new char[100];
}
int main(){
char *str;
GetMemory(str);
strcpy(str,"hi"); //str=NULL;
}
3、由于通过指针是可以传值的,因为此时该指针的地址是在主函数中申请的栈内存,我们通过指针对该栈内存进行操作,从而改变了实参的值。
void Change(char *p){
*p='a';
}
int main(){
char a='a';
char *p=&a;
Change(p);
printf("%c\n",a);
}
(4)根据上述的启发,我们也可以采用指向指针的指针来进行在调用函数中申请,在主函数中应用。如下:假设a的地址为ox23,内容为'a';而str的地址是ox46,内容为ox23;而pstr的地址是ox79,内容为ox46。
我们通过调用函数GetMemory,从而将pstr的内容赋给了p,此时p = ox46。通过对*p(ox23)的操作,即将内存地址为ox23之中的值改为char[100]的首地址,从而完成了对char* str地址的分配。
void GetMemory(char** p)
{
char *p = new char[100];
}
int main()
{
char a = 'a';
char* str = &a;
char** pstr = &str;
GetMemory(pstr);
strcpy(str, "hi");
}
(5)注意指针的释放问题,可能形成悬浮指针。
当我们释放掉一个指针p后,只是告诉操作系统该段内存可以被其他程序使用,而该指针p的地址值(如ox23)仍然存在。如果再次给这块地址赋值是危险的,应该将p指针置为NULL。
调用函数删除主函数中的内存块时,虽然可以通过地址传递直接删除,但由于无法对该指针赋值(形参不能传值),可能造成悬浮指针,所以此时也应该采用指向指针的指针的形参。例如:
void MemoryFree(char** p)
{
delete *p;
*p = NULL;
}
int main()
{
char *str = new char[100];
char *pstr = &str;
MemoryFree(pstr);
}
4.多几指针作为参数输出
void allocSpace(void ***p,int base,int row,int line){
*p=malloc(row*sizeof(void*));
for(int i=0;i<row;i++){
(*p)[i]=malloc(base*line);
}
}
六、const修饰指针
(一)const int *pi 与 int *const pi
#include<stdio.h>
int main(){
int a=20;
int b=30;
const int *pi=&a;
printf("const int *pi=%d\n",*pi);
pi=&b;
printf("const int *pi=%d\n",*pi);
int *const pi2=&a;
printf("int *const pi2=%d\n",*pi2);
*pi2=10;
printf("int *const pi2=%d\n",*pi2);
return 0;
}
![](https://img-blog.csdnimg.cn/20191010115149572.png)
const int *pi = &a;这句代码中const 修饰的是*pi,将*pi定义为常量,所以给*pi重新赋值是非法的,而pi是普通的变量,可对其进行再赋值,如: pi = &b;我们再来看这句代码:int *const pi2 = &a;这句代码中const修饰的是 pi2,将pi2定义为常量,所以给pi2重新赋值是非法的,而*pi2则可以重新赋值。
- 如果 const 修饰在*pi 前,则不能改的是*pi(即不能类似这样:*pi=50;赋值)而不是指 pi。
- 如果 const 是直接写在 pi 前,则 pi 不能改(即不能类似这样:pi=&i;赋值)。
(1)int *pi指针指向const int i常量情况
#include<iostream>
int main(){
const int i1=10;
int *pi;
pi=&i1; //编译报错
return 0;
}
分析:const修饰的是i1,直接访问的是i1,间接访问是*pi,*pi就有间接访问修改的风险,因此将*pi需要用const修饰,从而杜绝间接访问修改常量内存块的风险。const int *pi; 这样便能够编译通过,也可以强制转换 pi=(int*)(&i1),同样能够通过编译输出一样结果。
(2)const int *pi 指针指向 const int i1 的情况
#include <stdio.h>
int main(void)
{
const int i1=40;
const int * pi;
pi=&i1;/* 两个类型相同,可以这样赋值。很显然,i1 的值无论是通过 pi 还是 i1 都不能修改的。 */
printf("i1 = %d\n",i1);
printf("pi = %d\n",pi);
printf("*pi = %d\n",*pi);
return 0;
}
(3)使用const int *const pi声明的指针
#include<iostream>
int main(){
int i=10;
const int *const pi=&i;
return 0;
}
七、函数与函数指针
(一)函数多惨返回
(1)引列
写一个函数,同时返回两个正整数数据的和与差。在函数中,只有一个返回值,将如何实现
int foo(int *sum,int *diff,int a,int b);
(2)解法
当我们既需要通过函数返回值来判断函数调用是否成功,又需要把数据传递出来,此时,就需要用到多参数返回,多参返回都是通过传递调用空间中的空间地址来实现,例如:通过参数返回堆上的一维空间,二维空间和初始化指针。
(二)函数指针
(1)函数本质是一段可执行性代码段。函数名,则是指向这代码段的首地址。
#include<iostream>
void print(){
printf("banner\n");
}
int main(){
print();
printf("%p\n",&print);
printf("%p\n",print);
int a;
int *p=&a; //函数也是一个指针
return 0;
}
![](https://img-blog.csdnimg.cn/20191010132811366.png)
(2)函数指针变量定义与赋值
#include<iostream>
void print(){
printf("banner\n");
}
void dis(){
printf("banner\n");
}
int main(){
void (*pf)()=print;
pf();
pf=dis;
pf();
return 0;
}
![](https://img-blog.csdnimg.cn/20191010134033642.png)
(3)函数指针类型定义
#include<iostream>
void print(){
printf("banner\n");
}
void dis(){
printf("banner\n");
}
typedef void (*PFUNC)();
int main(){
PFUNC pf=print;
pf();
pf=dis;
pf();
return 0;
}
![](https://img-blog.csdnimg.cn/20191010134536971.png)
(4)应用
函数指针的一个用法出现在 菜单驱动系统中。例如程序可以提示用户输入一个整数值来选择菜单中的一个选项。用户的选择可以做函数指针数组的下标,而数组中的指针可以用来调用函数。
#include <stdio.h>
void function0(int);
void function1(int);
void function2(int);
int main()
{
void (*f[3])(int) = {function0,function1,function2};
//将这 3 个函数指针保存在数组 f 中
int choice;
printf("Enter a number between 0 and 2, 3 to end: ");
scanf("%d",&choice);
while ((choice >= 0) && (choice <3))
{
(*f[choice])(choice);
//f[choice]选择在数组中位置为 choice 的指针。
//指针被解除引用,以调用函数,并且 choice 作为实参传递给这个函数。
printf("Enter a number between 0 and 2,3 to end: ");
scanf("%d",&choice);
}
printf("Program execution completed.");
return 0;
}
void function0(int a)
{
printf("You entered %d so function0 was called\n",a);
}
void function1(int b)
{
printf("You entered %d so function1 was called\n",b);
}
void function2(int c)
{
printf("You entered %d so function2 was called\n",c);
}
(三)回调函数
(1)当我们需要排序时候,升序/降序,都是写死在函数中了,如果将程序以库的形式出现,将会是怎样?
#include<iostream>
using namespace std;
void selectSort(int *p,int n){
for(int i=0;i<n-1;i++){
for(int j=i+1;j<n;j++){
if(p[i]<p[j]){
p[i]=p[i]^p[j];
p[j]=p[i]^p[j];
p[i]=p[i]^p[j];
}
}
}
}
int main(){
int arr[10]={5,4,8,3,2,1,9,7,6,0};
selectSort(arr,10);
for(int i=0;i<10;i++)
cout<<arr[i]<<" ";
return 0;
}
(2)回调(函数做参数)
#include<iostream>
using namespace std;
int callBackCompare(int a,int b)
return a<b?1:0;
void selectSort(int *p,int n,int(*pf)(int ,int)){
for(int i=0;i<n-1;i++){
for(int j=i+1;j<n;j++){
if(pf(p[i]<p[j])){
p[i]=p[i]^p[j];
p[j]=p[i]^p[j];
p[i]=p[i]^p[j];
}
}
}
}
int main(){
int arr[10]={5,4,8,3,2,1,9,7,6,0};
selectSort(arr,10,callBackCompare);
for(int i=0;i<10;i++)
cout<<arr[i]<<" ";
return 0;
}
本质:回调函数,本质也是一种函数调用,先将函数以指针的方式传入,然后,调用。这种写法的好处是,对外提供函数类型,而不是函数定义。这样我们只需要依据函数类型和函数功能提供函数就可以了。给程序的书写带来了很大的自由。