C++学习笔记 指针与堆

1. 栈与堆的区别

数据在内存中存放共分为六个形式:

栈区(stack):由编译器自动分配并且释放,该区域一般存放函数的参数值、局部变量的值等。只要栈的剩余空间大于所申请的空间,系统将会为程序提供分配,否则会提示overflow,也就是栈溢出。在windows下,栈是一块连续的内存区域。由于是系统自动分配,所以速度快,程序员不能对其进行操作。

堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。系统收到程序申请空间的要求后,会遍历一个操作系统用于记录内存空闲地址的链表,当找到一个空间大于所申请空间的堆结点后,就会将该结点从记录内存空闲地址的链表中删除,并将该结点的内存分配给程序,然后再这块内存区域的首地址处记录分配的大小。堆是不连续的内存区域,各块区域由链表将它们串联起来,堆与指针是紧密相连的。由于是由程序员分配的内存,所以速度较慢,而且容易产生内存碎片,不过用起来很方便。

全局区(静态区)(static):全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和为初始化的静态变量在响铃的另一块区域,程序结束后由系统释放。

寄存器区:用来保存栈顶指针和指令指针。

文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。

程序代码区:存放函数体的二进制代码。 

2. 指针与堆的创建和删除

指针可以通过内存地址直接访问数据,从而避免在程序中复制大量的代码,因此指针的效率很高,一般来说,指针有三大用途:

1) 处理堆中存放的大型数据;

2) 快速访问类的成员数据和函数;

3) 以别名的方式向函数传递参数。

我们要创建一个堆,必须定义一个指向该堆的指针,这样才能通过该指针访问堆中的数据。在C++中用关键字new来创建一个堆并分配内存,在new后面跟一个要分配的对象类型,编译器根据这个类型来分配内存。如:

      int * p;
      p = new int;//创建一个int类型的内存区域(4个字节),将该区域的地址赋给p,即p指向这块区域。int也可以改为double,系统分配8个字节的空间。
      *p = 1;//将1放到这块区域内
      cout<<*p;

注意:由于计算机的内存是有限的,因此可能会出现没有足够内存而无法满足new的要求,在这种情况下,new会返回一个0,该值被赋给指针后,那么该指针就是一个空指针,空指针不会指向有效数据的。

由于使用new创建的内存空间不会被系统自动释放,因此如果程序员不去释放它,那么该区域的内存将始终不能为其他数据所使用,而指向该内存的指针是个局部变量,当定期该指针的函数结束并返回时,指针也就消失了,那么我们就再也找不到该块内存区域了,即内存泄露!这种情况将一直持续到程序结束该区域的内存才能恢复使用。所以在不需要一块内存空间时,我们就必须对指向它的指针使用关键字delete来释放这个内存空间。

     int * p = new int;
     *p = 3600;
     cout<<*p<<endl;//输出3600
     delete p;//释放空间
     p = 0;
     cout<<*p<<endl;//输出一个随机数
     p = new int;//又在堆中新建一个内存空间
     *P = 8;
     cout<<*p<<endl;//输出8
     delete p;

3. 在堆中创建与删除对象

我们可以将对象保存在堆中,然后通过指针来访问它。

#include <iostream>
using namespace std;
class Human
{
public:
      Human(){cout<<"构造函数执行中...\n"; i=999;}//定义一个Human类的构造函数
      ~Human(){cout<<"析构函数执行中...\n";}//这一行是为了显示确实删除了对象,不写这一行也没关系
private:
      int i;
};
int main()
{
      Human *p = new Human;//用new Human在堆中创建一个Human类对象,创建时系统会自动调用类的构造函数来初始化对象的成员数据,
      //即输出一行汉字,且i=999。创建对象完毕后,就会返回该对象的地址,这个地址赋给了左边的指针变量p,我们只能通过指针p去访问它。
      delete p;//直接删除指向该对象的指针,就会自动调用对象的析构函数来销毁该对象同时释放内存。
      return 0;
}

4. 在堆中访问数据成员

#include <iostream>
using namespace std;
class Human
{
public:
      Human(){cout<<"构造函数执行中...\n"; i=999;}
      ~Human(){cout<<"析构函数执行中...\n";}
      int get()const(return i;)
private:
      int i;
};
int main()
{
      Human Jack;
      cout<<Jack.get()<<endl;//这是在栈中访问数据成员的方法
      Human *p = new Human;
      (*p).get();//*p表示读取内存地址中的值,即堆中对象,然后再访问成员函数get();
      //但是这样做比较麻烦,C++专门为用指针来间接访问对象的成设置了一个运算符->,该符号可以实现读取对象的内存地址并且访问该对象的成员的作用。如下:
      cout<<p->get();
      delete p;
      return 0;
}

5. 在构造函数中开辟内存空间

我们可以将类的数据成员定义为一个指针,然后在构造函数中开辟新空间,将该空间的地址赋给指针,在析构函数中释放该内存。

#include <iostream>
using namespace std;
class Human
{
public:
      Human(){cout<<"构造函数执行中...\n"; i=new int(999);}//由于int是个整型不是类对象,所以new int(999)不会调用构造函数,
      //而是将999这个数值存储到新建的内存区域中。所以在构造函数中开辟了新空间,将空间的地址赋给指针变量i
      ~Human(){cout<<"析构函数执行中...\n";delete i;}//析构函数中释放掉内存空间。
      int get()const(return *i;)
private:
      int *i;
};
int main()
{
      Human *p = new Human;
      cout<<p->get();
      delete p;
      return 0;
}

该例子仅仅是为了说明构造函数中也可以开辟堆中的空间,在实际程序中,一个在堆中创建的对象通过成员指针再创建新空间用来保存数据并没有什么意义。因为在堆中创建对象时已经为它的所有数据成员提供了保存的空间。

6. this指针

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

#include <iostream>
using namespace std;
class A
{
public:
      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;//输出0013FF7C
      cout<<a.get()<<endl; //输出0013FF7C
      A b;
      b.set(9);
      cout<<”对象b放内存地址:\t”<<&b<<endl;//输出0013FF78
      cout<<b.get()<<endl; //输出0013FF78
      return 0;
}

这说明this变量记录每个单独的对象的内存地址,而this指针则指向每个单独的对象,因此不同的对象输出的this变量的内存地址也不同,因此可以通过该指针直接读取某个对象的数据。

在默认情况下,this指针可以省略不写,比如上面的void set()函数里的i=x;其实应该是this->i=x;但是编译器会自动在成员i前面加上this指针。另外this指针的创建和删除由编译器来完成,程序员无需操心。

7. 指针的常见错误

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

#include <iostream>
using namespace std;
int main()
{
      int *p=new int;
      *p=3;
      cout<<”将3赋给指针p的地址后,p读取的值:\t”<<*p<<endl;//输出3
      delete p;//p变成了迷途指针,因为它指向的空间已经不存在了
      cout<<”删除空间后,p读取的值:\t”<<*p<<endl;//输出-572948696随机数,等于告诉编译器可以用改内存区域保存其他数据
      long *p1=new long;//由于编译器会默认将释放掉的内存空间回收然后分配给新开辟的空间,所以p1指向的空间就是刚刚p指向的空间,下面的两行输出可以证明
      cout<<”创建新空间后,p中保存的地址:\t”<<p<<endl;//输出000329F8
      *p1 = 999999;
      cout<<”指向新空间的指针p1保存的地址:\t”<<p1<<endl; //输出000329F8
      *p=23;
      cout<<”将23赋给指针p的地址后,p读取的值:\t”<<*p<<endl;//23
      cout<<”将23赋给指针p的地址后,p1读取的值:\t”<<*p1<<endl;//23
      /*由于这两个指针都指向一块内存,所以上面两行输出的结果都是23,这种错误非常难调试,因为这种值的改变不容易令人联想到是被删除的那个指针导致的错误,
      因此在删除一个指针后一定要将其赋为空,即delete p; p=0;虽然在后面使用空指针是非法的,容易使程序崩溃,但我们宁愿程序崩溃,也不愿意调试起来很困难。*/
      delete p1;
      return 0;
}

8. 常量指针、指向常量的指针、指向常量的常指针

常量指针:指针前加上const关键字后,该指针就变成了常量指针,它自身的值是不可以改变的,但是它指向的目标却是可以改变的。

#include <iostream>
using namespace std;
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;//error 这句是非法的,p1不允许被改变
      p1->set(11);
      cout << int get(); //输出11, 说明p1所指的目标却可以改变,这个目标可以是变量也可以是对象
      return 0;
}

指向常量的指针:将const放到最前面,如const A *p = new A;与指针常量相反,该指针指向的目标是不可以改变的,但是该指针可以被修改。

所以修改后,上面程序中p1=p1+1;是合法的,但是p1->set(11);就是非法的了。

指向常量的常指针:顾名思义,就是在最前面和指针前面都加上const,如:const A * const p = new A; 这样指针的值以及指针所指向的目标都不允许被修改。

所以修改后,上面程序中p1=p1+1;和p1->set(11);都是非法的。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值