指针

作者: gpfworld | 来源:发表于2018-12-31 23:08 被阅读0次

    C++(大多数语言的)内存结构

    (1)内存结构图

    (2)不同的管理区域
      (1)名称
      (2)作用
      (3)管理特点
      (4)按照不同的分类划分内存区域进行理解
    (3)理解内存结构的重要性
      (1)加深对语言的理解,提高编程质量
      (2)特别是对于带有指针的C和C++语言,有助于理解段错误的产生的原因
      (3)内存的作用
        (1)临时存放(缓存)代码和程序中用到的数据
        (2)一定需要内存吗
        (3)实际上内存是硬盘的缓存
    (4)有和没有os区别
      (1)如果没有OS,直接使用物理内存
        (1)好处
         管理直接,不会浪费额外的管理空间,特别是对于单片机类的微控制器,节约空间很重要,传递结构时,使用值传递和地址传递对内存的影响都会特别大。
        (2)不好的地方
        1、内存的管理需要需要相应内存管理库(栈/静态区管理)和程序员亲历亲为,很麻烦,要求对程序编译再反汇编后的不同的段有相当理解,如果还需实现malloc的话,还需要安装malloc的库。
        2、很容易造成内存管理不善
        3、很难实现复杂内存管理,没有OS提供的内存管理,很难实现多进程多任务等机制
      (2)有os的情况
      有os的情况下,内存管理的结构并没有发生变化,但是在物理内存上,内核架构了一层模拟的虚拟内存,这个虚拟内存会合理的管理物理内存的使用,当然物理内存的使用效率最大化。我们编写的程序在有操作系统的机器上运行时,栈/堆/静态区等程序的内存管理是基于虚拟内存上实现的。
        (1)好处
        1、程序员负担轻,不需要费心内存的管理(合理安排使用)
        2、可以非常容易的实现多进程多任务机制
        3、内存管理本身会消耗很多的内存
        (2)虚拟内存管理需要消耗很多用于用于管理物理内存的管理空间,所以只有内存很大的计算机才能运行操作系统。
    (5)高级语言提供内存开辟接口
      不管语言编写的程序在不在os上运行,高级语言都提供堆空间的空间开辟接口,比如C的malloc和free函数,C++的malloc和free函数以及new和delete,java的new。
      这些高级语言如果是直接运行在裸机上的话,就需要提供相应的函数库或者类库来实现内存的管理,比如对于单片机等微控制器,只用C语言开发的程序都是运行在裸机上的,如果你需要使用malloc函数时,就需要移植malloc对应的函数库。
    对于c和c++而言,在内存的管理上,使用malloc和new进行开辟空间时,需要手动的释放空间,当然这样做的话,灵活性就非常的高,而且内存利用率高,但是人为管理代价高,而且风险高,很容易造成内存泄漏。
      对于java自带的内存管理机制的语言(还有c#),不管这个语言编写的程序是运行在os上,还是像早期一样运行在裸机上,完全实现了内存全自动化管理,只需要开空间,释放空间的事情不需要人为管理,语言自己会自己搞定,代价是这样的自动管理机制浪费管理空间。
      像c和c++这种语言成为非托管语言,跨平台靠编译器实现,java和c#这类的称为托管语言,跨平台靠托管平台实现。所谓跨平台,有跨软件平台和跨硬件平台之分,java的内存实现了完全自动的话管理,这些完全的自动话全部是靠托管平台实现的。
    (6)内存的访问
      (1)读和写
      (2)访问权限:程序的内存布局中,不同的管理区域规定了不同的访问权限
      (3)内存的访问和和管理一定会涉及地址和指针,但是在不同的语言中,指针暴露的情况不同,C和C++指针暴露给程序员,java等的指针完全隐藏。

    (2)数据存放的两种模式

      1、变量:权限,可读可写,内容可以通过反复的赋值而被改变。
      2、常量:权限,可读不可写,内容初始化后就不能再变。
      (1)常量的实现方式
      1、常量的空间开辟于只读存储器中。
      2、常量开辟在可读可写的内存中,但是软件(OS或者语言管理平台设定了读写权限)。
      3、编译器检查实现,典型的c和C++中的const修饰变量时的实现
      (2)初始化和赋值的本质区别
      初始化:初始化由编译器在编译完成,程序运行时只是复制到相应内存空间即可
      赋值:程序在具体运行的过程中由赋值语句临时决定具体的值,实现内存中数据的动态改变

    (3)指针

      (1)指什么?
        (1)指针变量
        (2)指针常量
        当我们说到指针的时候,根据实际语境,我们自己应该清楚指的是指针常量还是指针变量。
      (2)指针变量/指针常量用来存放什么
    不管是常量还是变量,都是用来存放数据的,指针变量和指针常量存放的数据就是内存地址。
      (3)指针声明(定义),初始化,赋值
        (1)定义和声明只是为了学习的需要做的区分,在编译器眼里没有区分,或者全都是声明。
        (2)在编译器眼中只有强弱符号的区分,如果给了初始化的声明就是强符号,全局强符号在程序中只能有一个。没有初始化的声明就是弱符号,弱符号可以有多个。强弱符号的作用主要是为了实现多个.c或者多个.cpp(编译)链接成为一个可执行文件时,将同名的弱符号和强符号统一成为一个,使得在a中定义的全局变量可以在其它文件中也能够被使用。

    (4)指针使用需要注意的问题

    (1)符号& :各自的作用
      (1)&:取地址符
      (2)
    :标准说法解引用,通俗理解,找到地址指向的空间,找到后读或者写
    (2)我们说指针指向某个空间(变量或者函数块内存空间),指得是指针存放的数据(地址)指向了某个空间。
    (3)对于指针来讲,如果不涉及强制转换的情况下,进行初始化或者赋值运算时,要求左值得类型与右值得类型要相同。

    (4)所有普通变量的地址是一级地址,需要使用相同类型的类型的一级指针变量来存储,一级指针变量的地址是二级地址,需要使用同等类型的指针变量来存储。
    (5)一般情况下,3级以上的指针变量不会提高程序效率,反而会降低程序效率。
    (6)对于多级指针需要关注p, &p, *p,在使用多级指针是需要注意指针断裂的问题。
    (7)为什么需要指针
      (1)为了高效率的使用大块内存,比如数组。
      (2)利用指针的指向malloc和new开辟的空间,方便的实现内存自由管理,特别是c++中指针指向new开辟的对象空间。
      (3)高效率的使用函数(函数指针),c语言使用函数指针也可以实现以面向对象的思想来开发大型项目。
    (8)一定需要指针吗
      只要有存储器,不管是内存还是外存,就一定有指针,只是对于语言来说指针完全可以被封闭起来。
      但是对于开发偏向底层的软件来说,指针的是很重要的,高效利用内存。
    (9)目前c++面临的尴尬地位
      (1)底层程序开发中,有一个很难回避的矛盾
      如果程序想要运行效率高的话,就必须使用带有可以操作指针的语言进行开发。
      如果用汇编无疑效率是最高的,但是使用带有指针的语言的话,由于语言本身的复杂性导致构建大型项目比较艰难。
      如果我们只考虑开发效率的话,我们就应该选择像java/c#这类的简单易理解的语言,开发大型项目的效率非常高,复杂的事情都留给了语言的托管平台去做了,但是这类语言的托管平台是非常耗费管理资源的。
      (2)目前的情况
      目前计算机的内存非常大,我们不需要考虑内存不够用的情况,我们只需要考虑开发效率问题,大多数都会选择没有指针的语言。
      如果需要开发非常偏底层的软件系统的话,实际上C语言已经完全能够胜任,不管是开发开发偏上层的还是开发偏下层的,实际上C++的与语言的复杂度都要比c和java要难,而且很容易产生内存操作的bug。
      (3)未来的it开发的趋势
      总之如今C++的应用面越来越窄,主要是由C++本身的难度和复杂度决定的。毕竟市场只希望以更少的成本和最快效率进行开发,以更少的资源应付维护。除非是一些嵌入式设备的公司,考虑到系统能力(java也可以开发界面,但是java管理平台太吃资源,嵌入式设备玩不起),还会坚持用C++进行项目开发。
      未来的趋势应该是以JAVA、(andriod / object-C)、.net、python等开发效率高为主导的研发市场,所以建议以C++知识为基础,积极向C/java/安卓/ object-C/.net/python方向发展。
      如果守在C++上不是非常明智。更加未来的趋势是,软件的开发会越来越模块化,越来越简单化,甚至简单到傻瓜式的操作,很多操作很复杂的语言,在未来的地位会越来越弱,至于会不会被淘汰,就无从得知了。
      (4)因此学习C++,一定要过好c语言这一关。

    (10)与指针相关的强制转换
      (1)强类型语言和弱类型语言
      站在数据类型的强弱来分,语言分为强类型语言和弱类型语言,语言中的数据类型不是必要的,
      完全可以将所有的数据类型弄成以一种类型,典型的看汇编和脚本语言,这两种那个语言中是没有数据类型,或者说所有的数据都是一个类型。
      (2)数据类型好处
      让编译器做大量的严格类型检查,可以帮助我们检查大量的人为错误,提高编程效率。
      (3)具体来讲,数据类型有哪些作用呢(基本类型,结构体等聚合类型,类类型等)
      (1)程序员编程识别,特别是在利用右值向左值赋值时,类型的识别尤为重要。
      (2)给编译器识别的:规定了数据的解析方式
        (1)存储格式的识别:int和double的存储格式就不同
        (2)存储空间大小的确定,特别是在空间的查找时,永远找到的都是变量的第一个字节的空间,至于后续还有多少字节空间也算做是变量的空间,就看类型的规定了。
      (4)普通变量的强制转换
        (1)强制转换时,改变的是中间临时变量,原变量内容不发生改变。
        (2)强制转换的目的,数据解析方式的改变
        (3)强制转换的改变的是空间的存储结构和数据的大小。
          (1)如果小空间向大空间强转,一般来说问题不大
          (2)大空间向小空间的强制转换需要特别注意,会导致数据的丢失
          (3)需要注意,强制转换可能会导致存储结构的变化。

    (3)指针的强制转换
      理解了普通变量的强制转换后,指针变量的强制转换就好理解了。
    (1)指针变量的强制转换修改的是指针所指向空间的解释方式。
    (4)指针的强制转换对于C++的意义
      在c++中,需要实现多态时,有一个必不可少的步骤就是指向子类的指针需要向上转型为一个父类型的指针,这叫做向上转型,在java中一样涉及向上转型,本质上也是指针操作。
    (5)指针强制转换符号
    (1)reinterpret_cast<>():const的指针不能强制转为非const的指针,但是反过来,非const转为const可以
    (1)正确的情况
    double b = 100;
    int * const pb1 = (int *)&b;
    const int *pb2 = NULL;
    pb2 = reinterpret_cast<const int *>(pb1);
    (2)不正确的情况
    double b = 100;
    const int * const pb1 = (int *)&b;
    int *pb2 = NULL;
    pb2 = reinterpret_cast<int *>(pb1);
    (2)c中的符号():对()来说,不存在const的问题,但是会有警告。

    (6)隐士强制转换和显示强制转换
      (1)c和c++中都允许对普通类型(结构体联合体除外)进行隐式强制转换
      (2)c允许对指针进行隐式强制转换,但可能是有警告,c++绝对不允许对指针进行隐式强制转换,必须显式转换

    (7)指针类型与普通类型之间的强制转换
      除了能在普通变量之间,以及指针之间进行强制转换外,很多时候还会可以在普通变量与指针之间进行转换
    (8)void *类型的作用
      (1)无类型:表示类型待定
      (2)作用:当类型无法统一时,可以使用void *类型,等到临时使用时根据实际情况强转为需要的类型,起到类型统一的作用。
      (3)c中允许对void *类型进行隐式转换,但是c++中不允许(很严格),必须显式转换
    (9)c和C++中空指针的不同定义
      (1)c中的空指针NULL
    #define NULL (void *)0
      (2)C++中的空指针0
    #define NULL 0
    在c++中,使用NULL和0来表示空指针是一样的
      (3)空指针的意义
    防止野指针

    (10)指针可能导致的错误
      (1)编译错误和警告
      (2)运行时错误
      (1)段错误
        (1)大段错误
        (2)小段错误
        (3)导致错误的原因
          (1)野指针,或者指针被人为的指定为了NULL
          (2)导致运行结果不对
            (1)导致错误的原因:野指针

    (11)如何才能安全的使用指针
      (1)指针定义时,一定要初始化为NULL,防止野指针
      (2)每次使用指针之前一定要先判断指针不为NULL
      (3)指针使用完成后,如果不在使用其指向的空间时,我们应该将其从新赋值为NULL
      (4)指针与数组的关系
      (1)数组是指针的典型使用,如果想要深刻理解
        (1)一维数组使用时如何使用指针进行访问的
        (2)type p*与 type p[]
      (2)多维数组与只针对关系
    在讲解数组时进行深入讲解

      (5)C语言的字符串如何表示的
        (1)一维数组表示
        (2)字符串指针
          (1)这种表示方式的存储方式
          (2)[]或者*访问字符串内容是应该注意的问题
        (3)字符串指针数组
      (6)动态分配内存
      (1)c方式   malloc free
        (1)普通变量
        (2)数组空间
      (2)C++方式    new delete
        (1)普通变量
        (2)数组空间
          int *p = new [4];
          delete [] p;
      (3)注意内存泄漏的问题
    (7)如果不考虑C++中引用方式的话,函数传参数时,使用指针的原则是什么
      (1)效率
        数组和结构体
       (2)修改
      (3)const与指针
    (8)强制转换总结
    C风格的强制类型转换(Type Cast)很简单,不管什么类型的转换统统是:
    TYPE b = (TYPE)a

    C++风格的类型转换提供了4种类型转换操作符来应对不同场合的应用。

      const_cast,字面上理解就是去const属性。
    
      static_cast,命名上理解是静态类型转换。如int转换成char。
    
      dynamic_cast,命名上理解是动态类型转换。如子类和父类之间的多态类型转换。
    
      reinterpreter_cast,仅仅重新解释类型,但没有进行二进制的转换。
    
      4种类型转换的格式,如:
      TYPE B = static_cast(TYPE)(a)
    
      const_cast
    
      去掉类型的const或volatile属性。
    
      struct SA {
    
      int i;
    
      };
    
      const SA ra;
    
      //ra.i = 10; //直接修改const类型,编译错误
    
      SA &rb = const_cast<SA&>(ra);
    
      rb.i = 10;
    
      static_cast
    
      类似于C风格的强制转换。无条件转换,静态类型转换。用于:
    
      1. 基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。
       (基类和子类之间的动态类型转换建议用dynamic_cast)
    
      2. 基本数据类型转换。enum, struct, int, char, float等。static_cast不能进行无关类型(如非基类和子类)指针之间的转换。
    
      3. 把空指针转换成目标类型的空指针。
    
      4. 把任何类型的表达式转换成void类型。
    
      5. static_cast不能去掉类型的const、volitale属性(用const_cast)。
    
      int n = 6;
    
      double d = static_cast<double>(n); // 基本类型转换
    
      int *pn = &n;
    
      double *d = static_cast<double *>(&n) //无关类型指针转换,编译错误
    
      void *p = static_cast<void *>(pn); //任意类型转换成void类型
    
    
      dynamic_cast
    
    
      有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL):
    
      1. 安全的基类和子类之间转换。
    
      2. 必须要有虚函数。
    
      3. 相同基类不同子类之间的交叉转换。但结果是NULL。
    
      class BaseClass {
    
      public:
    
      int m_iNum;
    
      virtual void foo(){};
    
      //基类必须有虚函数。保持多态特性才能使用dynamic_cast
    
      };
    
      class DerivedClass: public BaseClass {
    
      public:
    
      char *m_szName[100];
    
      void bar(){};
    
      };
    
      BaseClass* pb = new DerivedClass();
    
      DerivedClass *pd1 = static_cast<DerivedClass *>(pb);
    
      //子类->父类,静态类型转换,正确但不推荐
    
      DerivedClass *pd2 = dynamic_cast<DerivedClass *>(pb);
    
      //子类->父类,动态类型转换,正确
    
      BaseClass* pb2 = new BaseClass();
    
      DerivedClass *pd21 = static_castDerivedClass *>(pb2);
    
      //父类->子类,静态类型转换,危险!访问子类m_szName成员越界
    
      DerivedClass *pd22 = dynamic_cast<DerivedClass *>(pb2);
    
      //父类->子类,动态类型转换,安全的。结果是NULL
    
    
      reinterpreter_cast
    

    仅仅重新解释类型,但没有进行二进制的转换:

    1. 转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。

    2. 在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针
      (先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。但不能将非32bit的实例转成指针。

    3. 最普通的用途就是在函数指针类型之间进行转换。

    4. 很难保证移植性。

      int doSomething(){return 0;};
    
      typedef void(*FuncPtr)();
    
      //FuncPtr is 一个指向函数的指针,该函数没有参数,返回值类型为 void
    
      FuncPtr funcPtrArray[10];
    
      //10个FuncPtrs指针的数组 让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:
    
      funcPtrArray[0] = &doSomething;
    
      // 编译错误!类型不匹配,reinterpret_cast可以让编译器以你的方法去看待它们:funcPtrArray
    
      funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);
    
      //不同函数指针类型之间进行转换
    

    总 结

    去const属性用const_cast。

    基本类型转换用static_cast。

    多态类之间的类型转换用daynamic_cast。

    不同类型的指针类型转换用reinterpreter_cast。

    相关文章

      网友评论

          本文标题:指针

          本文链接:https://www.haomeiwen.com/subject/efrglqtx.html