(四)指针

指针

通过指针程序员可以直接对内存进行操作,这样的优点是使程序紧凑、简洁、高效;
计算机保存地址的工具就是指针。所以说:
指针:就是用来保存内存地址的变量。

注:指针地址、指针保存的地址和该地址的值 这三个概念一定不要混淆哦


什么是地址?

假如我们要去动物园,那么我们就得先知道动物园的地址,然后我们就可通过该地址找到动物园
同理,计算机要找到变量i,必须先找到i的地址,也就是i在内存中的编号,然后通过该编号,计算机访问到了i并且对它进行操作。
那么计算机将如何获得i的地址呢?

//通过一个例子来了解计算机获取地址
例:

#include <iostream>
using namespace std;
int main(){
  int i=1;
  //&的作用是获取变量i在内存中的地址
  cout<<&i<<endl;
}

使用指针的原因

因为在操作大型数据和类时,由于指针可以通过内存地址直接访问数据,从而避免在程序中复制大量的代码,因此指针的效率最高。
一般来说,指针会有三大用途:
  1:处理堆中存放的大型数据.
  2:快速访问类的成员数据和函数.
  3:以别名的方式向函数传递参数.

//下面的可看可不看 没多大作用
内存的栈和堆
  一般来说,程序就是与数据打交道,在执行某一功能的时候,将该功能所需要的数据加载到内存中,然后在执行完毕的时候释放掉内存。
  数据在内存中的存放共分为以下几个形式:
    1.栈区(stack)--由编译器自动分配并且释放,该区域一般存放函数的参数值、局部变量的值等。
    2.堆区(heap)--一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。
    3.寄存器区--用来保存栈顶指针和指令指针。
    4.全局区(静态区 static)--全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局
     变量和未初始化的静态变量在相邻的另一块区域.程序结束后由系统释放。
    5.文字常量区--常量字符串是放在这里的,程序结束后由系统释放。
    6.程序代码区--存放函数体的二进制代码。
  函数参数和局部变量存放在栈中,当函数运行结束并且返回时,所有的局部变量和参数就都被系统自动清楚掉了,为的是释放掉它们所占用
  的内存空间,全局变量可以解决这个问题,但是全局变量永远不会被释放,而且由于全局变量被所有的类成员和函数所公用,所以它的值很
  容易被修改.使用堆可以一举解决这两个问题。
    堆和栈的区别:
      1.从内存的申请方式上的不同
        栈:由系统自动分配,例如我们在函数中声明一个局部变量 int a;那么系统就会自动在栈中为变量a开辟空间。
        堆:需要程序员自己申请,因此也需要指明变量的大小
      2.系统相应的不同
        栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将提示overflow,也就是栈溢出。
        堆:系统收到程序申请空间的要求后,会遍历一个操作系统用于记录内存空闲地址的链表,当找到一个空间大于所申请空间的
          堆结点后,就会将该结点从记录内存空闲地址的链表中删除。并将该结点的内存分配给程序,然后在这块内存区域的首
          地址处记录分配的大小,这样我们在使用delete来释放内存的时候,delete才能正确地识别并删除该内存区域的所有
          变量.另外,我们申请的内存空间在堆结点上的内存空间不一定相等,这时系统就会自动将堆结点上多出来的那一部分内存
          空间回收到空闲链表中
      3.空间大小的不同
        栈:在WINDOW下,栈是一块连续的内存的区域,它的大小是2M,也有的说是1M,总之该数值是一个编译时就确定的常数。是
          由系统预先根据栈顶的地址和栈的最大容量定义好的。假如你的数据申请的内存空间超过栈的空间,那么就会提示
          overflow.因此,别指望栈存储比较大的数据
        堆:堆是不连续的内存区域。各块区域由链表将它们串联起来,关于链表的知识将在后面了解。这里只需要知道链表将各个
          不连续的内存区域连接起来,这些串联起来的内存空间叫做堆,它的上线是由系统中有效的虚拟内存来定的。因此获得
          的空间比较大,而且获取空间的方式也比较灵活。
      4.执行效率的不同
        栈:栈是由系统自动分配,因此速度较快.但是程序员不能对其进行操作。
        堆:堆是由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来很方便。
      5.执行函数时的不同
        栈:在函数调用时,第一个进栈的是被调用函数下一行的内存地址。其次是函数的参数,假如参数多于一个,那么次序是从右
          往左。最后才是函数的局部变量。
          由于栈的先进后出原则,函数结束时正好与其想反,首先是局部变量先出栈,然后是参数,次序是从左到右,这是所有的变
          量都已出栈,指针自然地指到第一个进栈的那行内存地址,也就是被调用函数的下一行内存地址。程序根据该地址跳转到
          被调用函数的下一行自动执行。
        堆:堆是一大堆不连续的内存区域,在系统中由链表将它们串接起来,因此在使用的时候必须由程序员来安排。它的机制是
         很复杂的,有时候为了分配一块合适的内存,程序员需要按照一定的算法在堆内存中搜索可用的足够大小的空间,如果
          没有满足条件的空间,那么就要向系统发出申请增加一部分内存空间,这样就才有机会分到足够大小的内存,然后将计
          算机后的数值返回。显然,堆的运行效率比栈要低得多,而且也容易产生碎片,但是好处是堆可以存储相当大的数据,
          并且一些细节也可以由程序员来安排。
    终结:
      栈的内存小,但是效率高,不过存储的数据只在函数内有效,超出函数就消失了。堆的可存储空间可以非常大,但是容易产生
      内存碎片,效率也较低,好处是灵活性比较强。比如说我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为
      全局的,那么这个时候创建一个堆对象无疑是良好的选择。
      由于堆和栈各有优缺点,因此好多时候我们是将堆和栈结合使用的,比如在存储一些较大数据的时候,我们将数据存放到堆中,
      却将指向该数据的指针放在栈中,这样就可以有效的提高程序的执行速度,避免一些不该有的碎片。不过,一般来说,假如不
      是特大的数据,我们都是使用栈,比如:函数调用过程中的参数,返回地址,和局部变量都存放到栈中。这样可以大大加快程
      序的运行速度。

 


使用指针保存内存地址

由于每一个被定义的变量都有自己的地址,因此你完全可以使用指针来存放任何已被定义的变量的地址,即使它没有被赋值

int main(){
  int i;
  //定义了一个指针p,星号(*)代表变量p是一个指针.要注意它的类型也是int
  int *p;
  //将i 的地址 赋值给p
  p=&i;
  return 0;
}

空指针

我们知道指针就是用来保存内存地址的变量,因此我们定义一个指针后一定要用它来保存一个内存地址,假如我们不那么做,那么该指针就是一个
失控指针,它可以指向任何地址,并且对该地址的数值进行修改或者删除,后果是非常可怕的.解决办法是将指针初始化为0

int main(){
  int *p;
  //将指针初始化为0 这样指针p就不会因为我们的疏忽而随意指向任意一个地址并且修改该地址的值。
  p=0;
  //空指针的地址 00000000;
  return 0;
}


指针和类型

由于不同类型的变量在内存中所占用的字节不同,而指针又是用来保存内存地址的变量,因此指针只能存储与它类型相同变量的地址

int main(){
  //定义了一个双精度型变量a.编译器接到此定义通知后会在内存中开辟一块内存区域.该区域的大小刚好可以存放双精度型数值
  double a=3.14;
  //定义了一个指向整型变量的指针变量p.编译器知道了指针指向的类型,才能对其进行正确的处理与运算。
  int *p1;
  int b=6;
  double *p2;
  p1=&b;
  p2=&a;
  
  count<<"p1:"<<p1<<endl;
  count<"p2:"<<p2<<endl;
  
  p1++;
  p2++;

  count<<"p1:"<<p1<<endl;
  count<"p2:"<<p2<<endl;
  return 0;
}
//经过输出发现 进行自加后 内存地址进行了改变 int类型的内存移动了4个字节 double移动了8个字节
//由于指针的类型的不同决定了指针运算方式也不同,所以我们不能将一种类型的指针赋给另一种类型的指针


用指针来访问值

运算符*被称为间接引用运算符,当使用星号*时,就读取它后面变量中所保存的地址处的值

例如:
int main(){
  int a=1;
  int *p=0;
  p=&a;
  //指针变量p前面的间接运算符*的含义是:"存储在此地址处的值".*p的意思就是读取该地址处的值.
  //由于p存储的是a的地址,因此执行的结果是输出a的值:1.
  cout<<*p
}

指针对数值的操作

//没什么难度 可看可不看
int main(){
  //简化书写类似于定义一个变量有某种特殊含义
  //这里的意思就是简化short int的书写方式为ut,即用ut就代表short int
  typedef unsigned short int ut;
  ut i=5;
  ut *p=0;
  p=&i;
  
  cout<<"i="<<i<<endl;
  cout<<"*p"<<*p<<endl;
  cout<<"用指针来修改存放在i中的数据\n";
  
  *p=90;
  cout<<"i="<<i<<endl;
  cout<<"*p"<<*p<<endl;
  cout<<"用i来修改存放在i中的数据\n";
  i=9;
  cout<<"i="<<i<<endl;
  cout<<"*p"<<*p<<endl;
}


更换指针保存的地址

//没什么难度 可看可不看
int main(){
  int i=0;
  int j=1;
  int *p=&i;
  count<<*p<endl;
  p=&j;
  count<<*p<endl;
}


指针和堆

 

  从文章开头处的内存栈堆的说明中可以了解到堆的好处是可以存储比较大的数据,而且存储的数据只要不是程序员手动将其释放那么就会永远保
存到堆中,而栈存储的数据只是在函数内有效,超出函数就消失了,而全局标量保存的数据只有程序结束才会释放,很容易被修改
  堆是一大堆不连续的内存区域,在系统中由链表将它们串联起来,它无法像栈那样对其中的内存单元命名,而系统为了数据隐秘,堆中的每个内存单
元都是匿名的,因此你必须现在堆中申请一个内存单元的地址,然后把它保存在一个指针中。这样你只有使用该指针才可以访问到该内存单元的数据。
  采用这种匿名的内存访问方式,而不是使用公开的全局变量,好处是只有使用特定的指针才能访问特定的数据。这样就避免了任何试图修改它的
非法操作
  要做到这一点,我们首先得创建一个堆,然后定义一个指向该堆的指针。这样就只能通过该指针才能访问堆中数据。
  在C++中使用关键字new创建一个堆并分配内存,在new后面跟一个要分配的对象类型,编译器根据这个类型来分配内存。
  示例:
  int *p;
  p=new int;
  第一行定义了一个指向整型的指针变量p,第二行用new在堆中创建一个int类型的内存区域,然后将该区域的内存地址赋给指针变量p。这样p所
指向的就是这块新建的内存区域。在这里要注意的是,new int在堆中被编译器分配了4个字节的空间。假如是new double那么就要分配8个字节的
内存空间。
  另外当如下写时:
  int *p=new int;
  这样在定义指针p的同时初始化了它的值为一个在堆中新建的int型存储区的内存地址。你可以像使用普通指针一样使用它,并把赋值给它所指向
的内存空间

  演示代码如下:(使用指针访问new 存储区)
  
  int main(){
    int *p;
    //new int当在内存创建成功时返回值为内存的地址,所以可以直接赋值给p
    p=new int;
    //将5赋值到了p所指向的内存空间
    *p=5;
    cout<<*p;
    return 0;
  }

 最后需要注意的:
  由于计算机的内存是有限的,因此可能会出现没有足够内存而无法满足new的请求,在这种情况下,new会返回0,该值被赋给指针后,那么该指针
  就是一个空指针,空指针不会指向有效数据。new除了返回空值外,还会引发异常,具体在后面的一场错误处理中在介绍。

 用指针删除堆中空间

  由于使用new创建的内存空间不会被系统自动释放,因此假如你不去释放它,那么该区域的内存将始终不能为其他数据所使用,而指向该内存
的指针是一个局部变量,当定义该指针的函数结束并返回时,指针也就消失了,那么我们就再也找不到该快内存地址,我们把这种情况叫做内存泄
漏。这种糟糕的情况将一直持续到程序结束该区域的内存才能恢复使用。因此假如你不需要一块内存空间,那么就必须对指向它的指针使用关键
字delete.
  int main(){
    int *p=new int;
    //删除指针指向的内存空间
    delete p;
    p=new int;
    //删除指针指向的内存空间
    delete p;
    //注意不要delete两次,加入再次删除时虽然编译不会报错,但是运行时会造成程序崩溃
    //delete p; 这种做法是错误的
    return 0;
  }

  例2:(对内存的总结)
  main(){
    int *p=new int;
    *p=3600;
    cout<<*p<<endl;
    delete p;
    //这里输出了-572662307 这个数是不确定的 因为当我们将内存释放后,那么这块内存就会被其他程序所使用
    //所以当delete p后一定要将p清零,这样可以防止造成错误操作
    cout <<*p<<endl;
    //将p清零,防止造成错误操作。
    p=0;
    p=new int;
    *p=8;
    cout<<*p<<endl;
    delete p;
    return 0;
  }
  如果以上代码能看到并能正确的预测输出结果,就代表你掌握了这快内容哦^_^.

内存泄漏

假如没有删除一个指针就对其重新赋值
例如:
int main(){
  int *p=new int;
  p=new int;
}
这样就会造成内存泄漏
因为第一行定义了一个指针p并使其指向一块内存空间,第二行又将一块新的内存空间的地址赋给了p,这样第一行所开辟的那块内存空间就无法
再使用了,因为指向它的指针现在已经指向了第二块空间
指针变量p只能保存一个地址,对它重新赋值则表示以前的地址被覆盖,加入该地址的内存空间没有被释放,那么你将无法再次通过指针p访问它.
因为此时的指针变量p记录的是第二块内存的地址
所以为了避免内存泄漏 在做第二个引用的时候要先 delete p;
假如暂时不想删除第一块内存空间,那么你可以这么做:
int *p1=new int;
int *p2=new int;
分别用两个指针来指向两块内存空间,这样每块空间都有一个指针来指向,也就不会造成找不到某块空间的内存泄漏现象


在堆中创建对象

我们既然可以在堆中保存变量,那么也就可以保存对象,我们可以将对象保存在堆中,然后通过指针来访问它。
例:
int main(){
  Human *p;
  p=new Human;
  return 0;
}
第一行定义了一个Human类的指针p,第二行使用new创建一块内存空间,同时又调用了Human类的默认构造函数来构造一个对象,它所占用的内
存大小根据Human类对象的成员变量来决定,加入该类有两个int型成员变量,那么该对象占用为2乘以4等于8个字节.构造函数一般都是在创建
对象时被自动调用,它的作用就是初始化该对象的成员数据.本行的右半部分创建一个对象完毕后,跟着将该对象的内存地址赋给左边的指针变
量p

在堆中删除对象

假如我们要删除在堆中创建的对象,我们可以直接删除指向该对象的指针,这样会自动调用对象的析构函数来销毁该对象同时释放内存
例:
class Human{
  public:
    Human(){cout<<"构造函数执行中..\n"; i=999;}
    ~Human(){cout<<"析构函数执行中...\n";}
  private:
    int i;
};
int main(){
  Human *p=new Human;
  //跟释放变量内存写法一样,在对象中这样写调用的是函数的析构函数
  delete p;
  return 0;
}

访问堆中的数据成员

假如我们要访问对象的数据成员和函数,我们使用成员运算符"."。
例:
Human Jack;
Jack.i;
那么如何用指针操作呢?如下:
(*p).get();
p为对象的指针,使用括号是为了保证先使用*号读取p的内存地址中的值,即堆中对象,然后再使用成员运算符"."来访问成员函数get()。
由于这样做比较麻烦,因此c++专门为指针来间接访问对象的成员设置了一个运算符: 成员指针运算符(->)。该符号可以实现读取对象的内存
地址并且访问该对象的成员的作用。
因此:
  (*p).get(); 可以写为 p->get();

在构造函数中开辟内存

我们可以将类的数据成员定义一个指针,然后在构造函数中开辟新空间,将该空间的地址赋给指针.而在析构函数中释放该内存
class Human{
  public:
    //int是一个整型,不是类对象,所以new int(999)不会调用构造函数,而是将999这个数值存储到新建的内存区域中
    //这样就是在构造函数中开辟一个新空间
    Human(){cout<<"构造函数执行中...\n";i=new int(999);}
    //在析构函数中释放改内存,防止造成内存泄漏
    ~Human(){cout<<"析构函数执行中...\n";delete i;}
    //*i是int类型
    int get(){return *i;}
  private:
    int *i;
}
//该例仅仅是为了说明构造函数中也可以开辟堆中空间,在实际程序中,一个在堆中创建的对象通过成员指针再创建新空间用来保存数据并没有
//什么意义。以为在堆中创建对象时已经为它的所有数据成员提供了保存的空间

对象在栈和堆中的不同

例:
int main(){
  Human Jack;
  return 0;
}
一个存储在栈中的对象,如:
Human Jack;
会在超出作用域时,比如说遇到右大括号,自动调用析构函数来释放该对象所占用的内存。

而一个存储在堆中的对象,如:
Human *Jack=new Human;
delete Jack;
则需要程序员自行对其所占用的内存进行释放.否则该对象所占用的内存直到程序结束才会被系统回收

this指针

学生在发新课本时一般都要将自己的名字写在课本上,以说明该课本是自己的,避免与别的学生混淆.同样对象也要在属于自己的每个成员身上
写下自己的名字,以证明该成员是自己的成员,而不是别的对象的成员。this变量帮助对象做到这一点,this变量记录每个对象的内存地址,
然后通过间接访问运算符->访问该对象的成员。

例:
class A{
  public:
    //由于方法中i的值不需要改变,所以可以加修饰符const
    int get()const{return i;}
    void set(int x){i=x;cout<<"this变量保存的内存地址:\t"<<this<<endl;}
  private:
    int i;
};
int main(){
  A a;
  a.set(9);
  cout<<"对象a的内存地址:\t"<<&a<<endl;
  cout<<a.get()<<endl;
  A b;
  b.set(999);
  cout<<"对象b的内存地址:\t"<<&b<<endl;
  cout<<b.get()<<endl;
  return 0;
}
//通过打印的结果可以看出:
//this变量记录每个单独的对象的内存地址,而this指针则指向每个单独的对象.因此不同的对象输出的this变量的内存地址也不同
//在默认情况下this指针是不写的。比如第七行 this-i=x; 假如我们写i=x; 编译器会自动在成员变量i前面加上this指针,用来表示这
//个i成员是属于某个对象的。
//由于this指针保存了对象的地址,因此我们可以通过该指针直接读取某个对象的数据,它的作用将会在后面的重载运算符中得到演示,对于
//this指针的创建与删除由系统(编译器)来控制,所以我们不需要进行这方面的操作

指针的常见错误

1.删除一个指针后一定要将该指针设置为空指针,因为删除该指针只会释放它所指向的内存空间,不会删除指针,因此这个指针还存在,并且它
依然会指向原来的内存空间,因此这是如果你再次尝试使用该指针,那么将会导致程序出错。

指针的加减运算

指针可以进行加法运算

例:
int *p=new int;
p++;
将指针变量p中的内存地址自加。由于p指向的是int型变量,因此执行加1操作会将原来的内存地址增加4个字节

p--;
概念跟++一样,将内存地址自减.

可以通过cout<<p<<endl;来查看结果

p=p-2;
"-2"后空间地址,因为是int型变量,因此执行-2操作会将原来的内存地址减8个字节。

 指针的赋值运算

指针也可以进行赋值运算,比如说将一个变量地址赋给另一个指针变量地址
int main(){
  int *p=new int;
  cout<<"p:"<<p<<endl;
  int *p1=new int;
  cout<<"p1"<<p1<<endl;
  p=p1;
  cout<<"赋值后..\n";
  cout<<"p:"<<p<<endl;
  return 0;
}

指针的相减运算

用p-p1,将结果保存到p指向的内存空间中,这样p指向的内存空间保存的就是p与p1的内存地址差

指针的比较运算

两个指针之间也可以进行比较运算。

 常量指针

定义常量指针:int *const p;这个指针它自身的值是不可以改变的,但是它指向的目标却是可以改变的
例:
int main(){
  int a=3;
  int *const p=$a;
  cout<<"a:"<<a<<endl;
  a=4;
  cout<<"a:"<<a<<endl;
  return;
}
例2:
class A{
public:
  int get() const{return i;}
  void set(int x){i=x;}
private:
  int i;
};
int main(){
  A*p=new A;
  cout<<"p:"<<p<<endl;
  p=p+1;
  cout<<"p:"<<p<<endl;
  A *const p1=new A;
  //p1=p1+1; 这样会报错提示不可被改变
  p1->set(11);
  cout<<p1->get();
  return 0;
}
通过这个例子进一步说明了常量指针的特性,常量指针自身不可改变,但是它指向的目标却可以改变.无论这个目标是变量还是对象。

  

 指向常量的指针

假如将上面例2中的 A*const p1=new A;写为 const A* p1=new A;那么p1就变成了指向常量的指针,该指针指向的目标是不可修改的,但是该指针可以
被修改
指向常量的指针只是限制我们修改它指向的目标,它自身是可以被修改的.

指向常量的常指针

假如 const A* const p1=new A;这种写法,则代表指向常量的常指针,它指向的目标是不可修改的,并且该指针也不可以被修改.

二级指针

    //如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指针的指针变量,也称为"二级指针"
    
    //通过指针访问变量称为间接访问.由于指针变量直接指向变量,所以称为"一级指针".而如果通过指向指针的指针变量来访问变量则构成"二级指针".

    地址变量                变量
    地址---------------> 值

    指针变量1            指针变量2            变量
    地址1------------>地址2------------>值

  


 

指针到这里就结束了,下一张介绍的为 "引用"

 

 

 

 

posted @ 2013-02-21 20:02  王世桢  阅读(268)  评论(0编辑  收藏  举报