`
蒙面考拉
  • 浏览: 155369 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

指针和内存分配的深度理解

 
阅读更多

一 :关于指针和堆的内存分配

先来介绍一下指针: 指针一种类型,理论上来说它包含其他变量的地址,因此有的书上也叫它:地址变量。既然指针是一个类型,是类型就有大小,在达内的服务器上或者普通的PC机上,都是4个字节大小,里边只是存储了一个变量的地址而已。不管什么类型的指针,char * ,int * ,int (*) ,string * ,float * ,都是说明了本指针所指向的地址空间是什么类型而已,了解了这个基本上所有的问题都好象都变的合理了。

在C++中,申请和释放堆中分配的存贮空间,分别使用new和delete的两个运算符来完成:

指针类型 指针变量名 = new 指针类型 (初始化);

        delete 指针名;

例如:

1、 int *p = new int(0);

     它与下列代码序列大体等价:

2、int tmp = 0, *p = &tmp;

区别:p所指向的变量是由库操作符new()分配的,位于内存的堆区中,并且该对象未命名。

  

下面是关于new 操作的说明 : 部分引自《C++面向对象开发》

1、new运算符返回的是一个指向所分配类型变量(对象)的指针。对所创建的变量或对象,都是通过该指针来间接操作的,而动态创建的对象本身没有名字。

2、一般定义变量和对象时要用标识符命名,称命名对象,而动态的称无名对象(请注意与栈区中的临时对象的区别,两者完全不同:生命期不同,操作方法不同,临时变量对程序员是透明的)。

3、堆区是不会在分配时做自动初始化的(包括清零),所以必须用初始化式(initializer)来显式初始化。new表达式的操作序列如下:从堆区分配对象,然后用括号中的值初始化该对象。

下面是从堆中申请数组

1、申请数组空间:

指针变量名=new 类型名[下标表达式];

注意:“下标表达式”不是常量表达式,即它的值不必在编译时确定,可以在运行时确定。这就是堆的一个非常显著的特点,有的时候程序员本身都不知道要申请能够多少内存的时候,堆就变的格外有用。

2、释放数组空间:

delete [ ]指向该数组的指针变量名;

注意:方括号非常重要的,如果delete语句中少了方括号,因编译器认为该指针是指向数组第一个元素的,会产生回收不彻底的问题(只回收了第一个元素所占空间),我们通常叫它“内存泄露”,加了方括号后就转化为指向数组的指针,回收整个数组。delete [ ]的方括号中不需要填数组元素数,系统自知。即使写了,编译器也忽略。<<Think in c++>>上说过以前的delete []方括号中是必须添加个数的,后来由于很容易出错,所以后来的版本就改进了这个缺陷。

下面是个例子,VC上编译通过

#include<iostream>

using namespace std;

//#include <iostream.h>  //for VC

#include <string.h>

void main(){

     int n;

     char *p;

     cout<<"请输入动态数组的元素个数"<<endl;

     cin>>n; //n在运行时确定,可输入17

     p=new char[n]; //申请17个字符(可装8个汉字和一个结束符)的内存空间strcpy(pc,“堆内存的动态分配”);//

     cout<<p<<endl;

     delete []p;//释放pc所指向的n个字符的内存空间return ; 

}

通过指针使堆空间,编程中的几个可能问题

1.动态分配失败。返回一个空指针(NULL),表示发生了异常,堆资源不足,分配失败。

   data = new double [m]; //申请空间

   if ((data ) == 0)…… //或者==NULL

2.指针删除与堆空间释放。删除一个指针p(delete p;)实际意思是删除了p所指的目标(变量或对象等),释放了它所占的堆空间,而不是删除p本身,释放堆空间后,p成了空悬指针,不能再通过p使用该空间,在重新给p赋值前,也不能再直接使用p。

3.内存泄漏(memory leak)和重复释放。new与delete 是配对使用的, delete只能释放堆空间。如果new返回的指针值丢失,则所分配的堆空间无法回收,称内存泄漏,同一空间重复释放也是危险的,因为该空间可能已另分配,而这个时候又去释放的话,会导致一个很难查出来的运行时错误。所以必须妥善保存new返回的指针,以保证不发生内存泄漏,也必须保证不会重复释放堆内存空间。

4.动态分配的变量或对象的生命期。无名变量的生命期并不依赖于建立它的作用域,比如在函数中建立的动态对象在函数返回后仍可使用。我们也称堆空间为自由空间(free store)就是这个原因。但必须记住释放该对象所占堆空间,并只能释放一次,在函数内建立,而在函数外释放是一件很容易失控的事,往往会出错,所以永远不要在函数体内申请空间,让调用者释放,这是一个很差的做法。你再怎么小心翼翼也可能会带来错误。

类在堆中申请内存 :

通过new建立的对象要调用构造函数,通过deletee删除对象要调用析构函数。

CGoods *pc;

pc=new CGoods;      //分配堆空间,并构造一个无名对象

                              //的CGoods对象;

…….

delete pc;  //先析构,然后将内存空间返回给堆;        堆对象的生命期并不依赖于建立它的作用域,所以除非程序结束,堆对象(无名对象)的生命期不会到期,并且需要显式地用delete语句析构堆对象,上面的堆对象在执行delete语句时,C++自动调用其析构函数。

正因为构造函数可以有参数,所以new后面类(class)类型也可以有参数。这些参数即构造函数的参数。

但对创建数组,则无参数,并只调用缺省的构造函数。见下例类说明:

class CGoods{

          char Name[21];

          int  Amount;

          float Price;

          float Total_value;

public:

 CGoods(){}; //缺省构造函数。因已有其他构造函数,系统不会再自动生成缺省构造,必须显式声明。  

 CGoods(char* name,int amount ,float price){

           strcpy(Name,name);

           Amount=amount;

           Price=price;

           Totalvalue=price*amount;  }

           ……};//类声明结束

下面是调用机制 :

void main(){

      int n;

      CGoods *pc,*pc1,*pc2;

      pc=new CGoods(“hello”,10,118000);

      //调用三参数构造函数   pc1=new CGoods();  //调用缺省构造函数  cout<<”输入商品类数组元素数”<<endl;

      cin>>n;

      pc2 = new CGoods[n];

     //动态建立数组,不能初始化,调用n次缺省构造函数  

      ……

      delete pc;

      delete pc1;

      delete []pc2;  

}

申请堆空间之后构造函数运行;

释放堆空间之前析构函数运行;

再次强调:由堆区创建对象数组,只能调用缺省的构造函数,不能调用其他任何构造函数。如果没有缺省的构造函数,则不能创建对象数组。

---------------------下面我们再来看一下指针数组和数组指针―――――――――――――

如果你想了解指针最好理解以下的公式 :

     (1)int*ptr;//指针所指向的类型是int

   (2)char*ptr;//指针所指向的的类型是char

   (3)int**ptr;//指针所指向的的类型是int* (也就是一个int * 型指针)

   (4)int(*ptr)[3];//指针所指向的的类型是int()[3] //二维指针的声明

(1)指针数组:一个数组里存放的都是同一个类型的指针,通常我们把他叫做指针数组。

比如 int * a[10];它里边放了10个int * 型变量,由于它是一个数组,已经在栈区分配了10个(int * )的空间,也就是32位机上是40个byte,每个空间都可以存放一个int型变量的地址,这个时候你可以为这个数组的每一个元素初始化,在,或者单独做个循环去初始化它。

例子:

int * a[2]={ new int(3),new int(4) };     //在栈区里声明一个int * 数组,它的每一个元素都在堆区里申请了一个无名变量,并初始化他们为3和4,注意此种声明方式具有缺陷,VC下会报错

例如 :

int * a[2]={new int[3],new int[3]};

delete a[0];

delet a[10];

但是我不建议达内的学生这么写,可能会造成歧义,不是好的风格,并且在VC中会报错,应该写成如下 :

int * a[2];

a[0]= new int[3];

a[1]=new int[3];

delete a[0];

delet a[10];

这样申请内存的风格感觉比较符合大家的习惯;由于是数组,所以就不可以delete a;编译会出警告.delete  a[1];

注意这里 是一个数组,不能delete [] ;

( 2 ) 数组指针 : 一个指向一维或者多维数组的指针;

int * b=new int[10]; 指向一维数组的指针b ;

注意,这个时候释放空间一定要delete [] ,否则会造成内存泄露, b 就成为了空悬指针.

int (*b2)[10]=new int[10][10]; 注意,这里的b2指向了一个二维int型数组的首地址.

注意:在这里,b2等效于二维数组名,但没有指出其边界,即最高维的元素数量,但是它的最低维数的元素数量必须要指定!就像指向字符的指针,即等效一个字符串,不要把指向字符的指针说成指向字符串的指针。这与数组的嵌套定义相一致。

int(*b3) [30] [20];  //三级指针――>指向三维数组的指针;

int (*b2) [20];     //二级指针;

b3=new int [1] [20] [30];

b2=new int [30] [20];

      两个数组都是由600个整数组成,前者是只有一个元素的三维数组,每个元素为30行20列的二维数组,而另一个是有30个元素的二维数组,每个元素为20个元素的一维数组。

      删除这两个动态数组可用下式:

delete [] b3;  //删除(释放)三维数组;

delete [] b2;  //删除(释放)二维数组;

再次重申:这里的b2的类型是int (*) ,这样表示一个指向二维数组的指针。

b3表示一个指向(指向二维数组的指针)的指针,也就是三级指针.

( 3 ) 二级指针的指针

看下例 :

int (**p)[2]=new (int(*)[3])[2];

       p[0]=new int[2][2];

       p[1]=new int[2][2];

       p[2]=new int[2][2];

       delete [] p[0];

       delete [] p[1];

       delete [] p[2];

       delete [] p;

注意此地方的指针类型为int (*),碰到这种问题就把外边的[2]先去掉,然后回头先把int ** p=new int(*)[n]申请出来,然后再把外边的[2]附加上去;

p代表了一个指向二级指针的指针,在它申请空间的时候要注意指针的类型,那就是int (*)代表二级指针,而int (**)顾名思义就是代表指向二级指针的指针了。既然是指针要在堆里申请空间,那首先要定义它的范围:(int(*)[n])[2],n 个这样的二级指针,其中的每一个二级指针的最低维是2个元素.(因为要确定一个二级指针的话,它的最低维数是必须指定的,上边已经提到)。然后我们又分别为p[0],p[1],p[2]…在堆里分配了空间,尤其要注意的是:在释放内存的时候一定要为p[0],p[1],p[2],单独delete[] ,否则又会造成内存泄露,在delete[]p 的时候一定先delete p[0]; delete p[1],然后再把给p申请的空间释放掉 delete [] p ……这样会防止内存泄露。

(3)指针的指针;

int ** cc=new (int*)[10]; 声明一个10个元素的数组,数组每个元素都是一个int *指针,每个元素还可以单独申请空间,因为cc的类型是int*型的指针,所以你要在堆里申请的话就要用int *来申请;

看下边的例子  (vc & GNU编译器都已经通过);

       int ** a= new int * [2];     //申请两个int * 型的空间

       a[1]=new int[3];        //为a的第二个元素又申请了3个int 型空间,a[1]指向了此空间首地址处

       a[0]=new int[4];        ////为a的第一个元素又申请了4个int 型空间,a[0] 指向了此空间的首地址处

       int * b;

       a[0][0]=0;

       a[0][1]=1;

       b=a[0];

       delete [] a[0]       //一定要先释放a[0],a[1]的空间,否则会造成内存泄露.;

       delete [] a[1];

       delete [] a;

       b++;

       cout<<*b<<endl;       //随机数

注意 :因为a 是在堆里申请的无名变量数组,所以在delete 的时候要用delete [] 来释放内存,但是a的每一个元素又单独申请了空间,所以在delete [] a之前要先delete [] 掉 a[0],a[1],否则又会造成内存泄露.

(4) 指针数组 :

   

我们再来看看第二种 :二维指针数组

int *(*c)[3]=new int *[3][2];

如果你对上边的介绍的个种指针类型很熟悉的话,你一眼就能看出来c是个二级指针,只不过指向了一个二维int * 型的数组而已,也就是二维指针数组。

例子 :

 int *(*b)[10]=new int*[2][10];//

b[0][0]=new int[100];

b[0][1]=new int[100];

*b[0][0]=1;

cout <<*b[0][0]<<endl;    //打印结果为1

delete [] b[0][0];

delete [] b[0][1];

delete [] b;

cout<<*b[0][0]<<endl;    //打印随机数

 这里只为大家还是要注意内存泄露的问题,在这里就不再多说了。

如果看了上边的文章,大家估计就会很熟悉,这个b是一个二维指针,它指向了一个指针数组

第二种 :

        int **d[2];表示一个拥有两个元素数组,每一个元素都是int ** 型,这个指向指针的指针:)

   d不管怎样变终究也是个数组,呵呵,

   如果你读懂了上边的,那下边的声明就很简单了:

   d[0]=new int *[10];

   d[1]=new int * [10];

     delete [] d[0];

     delete [] d[1];

具体的就不再多说了 :)

二 : 函数指针 

关于函数指针,我想在我们可能需要写个函数,这个函数体内要调用另一个函数,可是由于项目的进度有限,我们不知道要调用什么样的函数,这个时候可能就需要一个函数指针;

int a();这个一个函数的声明;

ing (*b)();这是一个函数指针的声明;

让我们来分析一下,左边圆括弧中的星号是函数指针声明的关键。另外两个元素是函数的返回类型(void)和由边圆括弧中的入口参数(本例中参数是空)。注意本例中还没有创建指针变量-只是声明了变量类型。目前可以用这个变量类型来创建类型定义名及用sizeof表达式获得函数指针的大小:

unsigned psize = sizeof (int (*) ()); 获得函数指针的大小

// 为函数指针声明类型定义

typedef int (*PFUNC) ();

PFUNC是一个函数指针,它指向的函数没有输入参数,返回int。使用这个类型定义名可以隐藏复杂的函数指针语法,就我本人强烈建议我们大内弟子使用这种方式来定义;

下面是一个例子,一个简单函数指针的回调(在GNU编译器上通过,在VC上需要改变一个头文件就OK了)

#include<iostream>              //GNU 编译器 g++ 实现

using namespace std;

/*                              //vc 的实现

#include "stdafx.h"

#include <iostream.h>

*/

#define DF(F) int F(){  cout<<"this is in function "<<#F<<endl;\

      return 0;       \

}

//声明定义DF(F)替代 int F();函数;

DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g); DF(h); DF(i);     //声明定义函数 a b c d e f g h i

// int (*pfunc)();              //一个简单函数指针的声明

typedef int(*FUNC)();   //一个函数指针类型的声明

FUNC ff[] = {a,b,c,d,e,f,g,h,i};   //声明一个函数指针数组,并初始化为以上声明的a,b,c,d,e,f,g,h,i函数

FUNC func3(FUNC vv){    //定义函数func3,传入一个函数指针,并且返回一个同样类型的函数指针

      vv();

      return vv;

}

/*FUNC func4(int (*vv)()){      //func3的另一种实现

      vv();

      return vv;

}*/

int main(){

      for(int i=0;i<sizeof(ff)/sizeof (FUNC);i++){  //循环调用函数指针

              FUNC r=func3(ff[ i ]);

              cout<<r()<<endl;                //输出返回值,只是返回了0

      }

      return 0;

}

到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

好了,先到此为止吧,写这篇文章耗费了基本上快半天的时间了,很多事情还没有做,等改天有时间再回来整理,所有的源程序都放在openlab3服务器上我的目录下lib/cpp下,大家可以去拿。不知道的登陆openlab3 然后cd ~chengx/lib/cpp就可以看到了。

还有很复杂的声明可能也是一种挑战 比如<<Think in c++>>里的

int (*(*f4())[10]();的声明,f4是一个返回指针的函数,该指针指向了含有10个函数指针的数组,这些函数返回整形值;不是这个函数有特别之处,而是Bruce Eckel 说的“从右到左的辨认规则”是一种很好的方法,值得我们去学习,感谢他:)

最后我想应该跟大家说一下,写程序应该就象JERRY所说的:简单就是美;我们应该遵循一个原则 : KISS (Keep It Simple,Stupid ,尽量保持程序简单 出自 :《Practical C programming》),把自己的程序尽量的简单明了,这是个非常非常好的习惯。

分享到:
评论

相关推荐

    深度理解c++中的this指针

    下边给出一个日期类,通过这个实例,深度理解this指针。 #define _CRT_SECURE_NO_WARNINGS 1 #include using namespace std; class Date { public: void setYear(int year) { m_year = year; } v

    c/c++ 学习总结 初学者必备

    如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,由于“指向指针的指针”这个概念不容易理解,我们也可以用函数返回值来传递动态内存。 常常有人把return语句用错了。这里强调不要用return语句...

    C++初学者指南(原版)

    最后一个模块将介绍几个重要的、高级 C++ 主题,包括异常处理、模板、动态分配和命名空间。另外还介绍运行时类型 ID 和转换运算符。完成此模块后,您将掌握这种语言的核心元素,能够开始编写实际程序。 掌握检查的...

    值类型和引用类型的区别

    我这人不善于背理论的教条,喜欢把书本上那些生硬的话跟现实生活中常见 的事物联系起来理解和记忆。 直白点儿说:值类型就是现金,要用直接用;引用类型是存折,要用还得先去银行取现。 声明一个值类型变量,编译器...

    JVM讲解视频.zip

    oJVM内存模型深度理解 JVM内存模型结构 线程栈及栈帧内部结构详解 程序计数器详解 本地方法栈详解 堆详解 JVM启动参数设置 堆内存大小设置 方法区内存大小设置 栈分配内存大小设置 JVM对象...

    谭浩强C语言程序设计,C++程序设计,严蔚敏数据结构,高一凡数据结构算法分析与实现.rar

    *7.1.7 动态分配和撤销内存的运算符new和delete 7.2 共用体 7.2.1 共用体的概念 7.2.2 对共用体变量的访问方式 7.2.3 共用体类型数据的特点 7.3 校举类型 7.4 用typedef声明类型 习题 第3篇 基于对象的程序设计 第...

    谭浩强C语言程序设计,C++程序设计,严蔚敏数据结构,高一凡数据结构算法分析与实现.rar )

    *7.1.7 动态分配和撤销内存的运算符new和delete 7.2 共用体 7.2.1 共用体的概念 7.2.2 对共用体变量的访问方式 7.2.3 共用体类型数据的特点 7.3 校举类型 7.4 用typedef声明类型 习题 第3篇 基于对象的程序设计 第...

    宋劲彬的嵌入式C语言一站式编程

    4. 分配内存的函数 26. 链表、二叉树和哈希表 1. 链表 1.1. 单链表 1.2. 双向链表 1.3. 静态链表 1.4. 本节综合练习 2. 二叉树 2.1. 二叉树的基本概念 2.2. 排序二叉树 3. 哈希表 27. 本阶段总结 III. Linux系统编程...

    C++数据抽象和问题求解(第6版).[美]Frank M. Carrano(带详细书签).pdf

    C++片段2 指针、多态和内存分配 113 C2.1 变量的内存分配和方法的前期绑定 114 C2.2 需要解决的问题 115 C2.3 指针与程序的自由存储 116 C2.3.1 释放内存 118 C2.3.2 避免内存泄漏 119 C2.3.3 避免悬挂指针 ...

    C/C++程序员面试指南.杨国祥(带详细书签).pdf

    面试题18:简述C、C++程序编译的内存分配情况 面试题19:以下四段代码中哪段没有错误 第6章 字符串 6.1 数字字符串 面试题1:编码实现数字转化为字符串 面试题2:编码实现字符串转化为数字 6.2 字符串函数 面试题3:...

    C/C++笔试题(附答案,华为面试题系列)

    立的运行部分,这样的程序会利于理解和修改。 其他情况都使用单线程。 11.Windows是内核级线程么。 答:见下一题 12.Linux有内核级线程么。 答:线程通常被定义为一个进程中代码的不同执行路线。从实现方式上划分...

    Java 语言基础 —— 非常符合中国人习惯的Java基础教程手册

    实际上是一个指针,此指针指向对象所占有的内存区域。 因此,对象名(变量)实际上存放的是一个被实例化之后的对象所占有的内存区域 的指针。 例如: type objectName = new type ( [paramlist] ); 运算符 new 为...

    入门学习Linux常用必会60个命令实例详解doc/txt

    要想真正理解Linux系统,就必须从Linux命令学起,通过基础的命令学习可以进一步理解Linux系统。 不同Linux发行版的命令数量不一样,但Linux发行版本最少的命令也有200多个。这里笔者把比较重要和使用频率最多的命令...

Global site tag (gtag.js) - Google Analytics