• 彻底了解c/c++指针


    参见 http://hi.baidu.com/%C6%BF%D6%D0%B5%C4%C5%AE%CE%D7/blog/item/6fbc82cfc5f31b0b92457e3c.html 

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

    1.1.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()分配的,位于内存的堆区中,并且该对

    象未命名。

      

    1.1.2.       下面是关于new 操作的说明: 

    部分引自<<C++面向对象开发>>

    1、new运算符返回的是一个指向所分配类型变量(对象)的指针。对所创建的变

    量或对象,都是通过该指针来间接操作的,而动态创建的对象本身没有名字。

    2、一般定义变量和对象时要用标识符命名,称命名对象,而动态的称无名对象(

    请注意与栈区中的临时对象的区别,两者完全不同:生命期不同,操作方法不同

    ,临时变量对程序员是透明的)。

    3、堆区是不会在分配时做自动初始化的(包括清零),所以必须用初始化式

    (initializer)来显式初始化。new表达式的操作序列如下:从堆区分配对象,然

    后用括号中的值初始化该对象。

    1.1.3.       下面是从堆中申请数组

    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.1.4.       通过指针使堆空间,编程中的几个可能问题

    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)就是这个原因。但必须记住释放该对象所占堆空间,并

    只能释放一次,在函数内建立,而在函数外释放是一件很容易失控的事,往往会

    出错,所以永远不要在函数体内申请空间,让调用者释放,这是一个很差的做法

    。你再怎么小心翼翼也可能会带来错误。

    1.1.5.       类在堆中申请内存 :

    通过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;

               Total_value=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.2.        再来看一下指针数组和数组指针

    如果你想了解指针最好理解以下的公式,他们的核心就是Bruce Eckel 说的“从右到左的辨认规则”

    (http://blog.csdn.net/lop5712/archive/2004/09/12/102176.aspx) 

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

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

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

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

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

    (6)       二级指针的指针  int (**p)[2]=new (int(*)[3])[2];

    (7)       指针的指针;                int ** cc=new (int*)[10];

    1.2.0 从右到左的辨认规则

          http://blog.csdn.net/lop5712/archive/2004/09/12/102176.aspx

    1.2.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 [] ;

    1.2.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];     //二级指针――>指向2维数组的指针;;

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

    b2=new int [30] [20];

          两个数组都是由600个整数组成,前者是只有一个元素的三维数组,每个元

    素为30行20列的二维数组,而另一个是有30个元素的二维数组,每个元素为20个

    元素的一维数组。

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

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

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

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

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

    1.2.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 ** pnew 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 ……这样会防止内存泄露。

    1.2.4.       指针的指针;

    int ** cc=new (int*)[10]; 声明一个10个元素的数组,数组每个元素都是一

    个int *指针,每个元素还可以单独申请空间,因为cc的类型是int*型的指针,所 <

    BR>以你要在堆里申请的话就要用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],否则又会造成内存泄露.

    1.2.5.       指针数组

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

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

    具体的就不再多说了 :)

    1.3.        函数指针 

    关于函数指针,我想在我们可能需要写个函数,这个函数体内要调用另一个函数

    ,可是由于项目的进度有限,我们不知道要调用什么样的函数,这个时候可能就

    需要一个函数指针;

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

    int  (*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》),把自己的程序尽量的简单明了,这是个非常

    非常好的习惯。

    由于写的匆忙,可能其中有遗漏的地方,大家发现希望能指正:)

    GOOD LUCK ! 

     原文地址 http://kurtxue.spaces.live.com/blog/cns!3fda83d39d3b011!181.entry

    文章出处:DIY部落(http://www.diybl.com/course/3_program/vc/vc_js/20090307/159137_2.html)

    //      附录:clase AAA 完整代码 vs2005

  • 相关阅读:
    使用者自订控件 / User Control
    File 与 Log #1ASP.NET的档案与I/O Stream(为系统记录Log文件)
    JavaScript + ASP.NET
    [VB][ASP.NET]FileUpload控件「批次上传 / 多档案同时上传」的范例
    Android开发之道(4)程序框架基础
    虚拟机网络设置
    POST 模拟登陆
    android 面试二
    SSCANF 正则表达式
    android 之 @ 与? 的区别
  • 原文地址:https://www.cnblogs.com/carl2380/p/1924636.html
Copyright © 2020-2023  润新知