C++的指针

1 篇文章 0 订阅

C++的指针

在C++中,指针被称为是C/C++中的精髓所在,指针是存放内存地址的一种变量,特殊的地方就在它存放的是内存地址。

计算机中的内存都是编址的,每个地址都有一个符号。指针是一个无符号整数,它是一个以当前系统寻址范围为取值范围的整数,声明指针和声明一个无符号整数实质上并无区别。

一、变量内存实质

(一)变量的实质

要理解指针,首先就要理解“变量”存储实质,内存空间图:

根据图中,内存只不过是一个存放数据的空间,可以将其理解成装水果的篮子、电影院的座位。在电影院中每个位置都有编号,而内存中要存放各种各样的数据,所以我们需要知道这些数据存放的位置。所以内存也需要像影院那样需要编号。也就是说内存编址(为内存进行地址编码)。内存按一个字节接着一个字节的次序进行编址,每个字节都有个编号,称为内存地址。

内存编址:

                               

程序中写下了语言声明:

int i;

char a;

它其实是内存中申请一个名为i的整形变量宽度空间(DOS 下的 16 位编程中其宽度为 2 个字节),和一个名为 a 的字符型变量宽度的空间(占 1 个字节)。

在内存中映象:

图中可看出,i在内存起始地址为6的上面申请了两个字节的空间(假设int的宽度为16位,不同系统中 int 的宽度可能是不一样的,最常用的win32环境下为4个字节)并将其命名为i。a在内存地址为8上申请一个字节的空间,并命名为a。这样我们就拥有两个不同类型的变量,看变量如何给变量进行赋值。

(二)赋值给变量

i=30;

a='p';

两个语句将30存入i变量的内存空间中,将'p'字符存进a变量的内存空间中,可以这样理解

将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;
}

输出的&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是一个指针,其实它也是一个变量,与变量并没有实质的区别。

(说明:这里我假设了指针只占 2 个字节宽度,实际上在 32 位系统中,指针的宽度是 4 个字节宽的,即 32 位。)
由图示中可以看出,我们使用“int *pi”声明指针变量 —— 其实是在内存的某处声明一个一定宽度的内存空间,并把它命名为 pi。你能在图中看出pi 与前面的 i、a 变量有什么本质区别吗?没有,当然没有!肯定没有!!真的没有!!!pi 也只不过是一个变量而已嘛!那么它又为什么会被称为“指针”?关键是我们要让这个变量所存储的内容是什么。现在我要让 pi 成为具有真正“指针”意义的变量。请接着看下面语句:

pi=&i;

把i地址的编号赋值给pi。并在pi里面写上i的地址编号;

执行完 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;
}

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;
}

三、二级指针(指针的指针)

(一)二级指针,是一种指向指针的指针。我们可以通过它实现间接访问数据,和改变一级指针的指向问题。

                                                                                  

#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=&pi;
  	cout<<"**ppi= "<<**ppi<<endl;
  	cout<<endl;
  	**ppi=40;
  	cout<<"i= "<<i<<endl;
  	cout<<"*pi= "<<*pi<<endl;
  	cout<<"**ppi= "<<**ppi<<endl;
	return 0;
}

结果如下:

(二)间接数据访问

1.改变一级指针指向

#include <iostream>
#include <string>
using namespace std;
int main(){
  	int i=30;
  	int *pi=&i;
  	cout<<"一级指针*pi= "<<*pi<<endl;
  	int* *ppi=&pi;
  	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;
}

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;
}

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*修饰数组内容,即数组的每个元素。

#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;
}

(三)二级指针与指针数组

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;
}

(三)完美匹配前提

数组名,赋给指针以后,就会少了唯独的概念,所以用二级指针访问指针数组,需要维度,也可以不需要。

#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;
}

五、堆空间与指针

(一)堆上的一维空间

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;
}

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;
}

(2)函数指针变量定义与赋值

#include<iostream>
void print(){
    printf("banner\n");
}
void dis(){
    printf("banner\n");
}
int main(){
    void (*pf)()=print;
    pf();
    pf=dis;
    pf();
    return 0;
}

(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;
}

(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;
}

本质:回调函数,本质也是一种函数调用,先将函数以指针的方式传入,然后,调用。这种写法的好处是,对外提供函数类型,而不是函数定义。这样我们只需要依据函数类型和函数功能提供函数就可以了。给程序的书写带来了很大的自由。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值