Thinking in C -- C指针

    技术2022-05-11  67

     忘记悲伤最好的办法可能就是让自己有事做吧。 因为工作需要,很多好朋友都在重新补最基础也是最重要的C语言。人常说没有学好指针就等于没有学过C,在学校时可能老师只是为了应付考试,而一直在讲那些死板的语法。但对这个C语言(也是任何编程思想)的精髓没有重视。     确实,在几年的编程过程中,感觉对指针的精通可以让我们对任何语言都触类旁通了。语言只是方便我们表达自己思想的助记符,真正的程序在内存中是如何运行的?理解指针可以让你更清楚的知道。    最近在网上看了一些讲解指针的文章,从这些文章中将自己对指针的了解加以整理,我认为基本上如果可以真正理解下面这些,那么你可以对别人说你对C的思想已经精通了。完全精通一门语言当然还要知道它常用的库函数,当然那些就要你自己多编,多记了。

    /*--------------------------------概念------------------------------------------*/

     

    指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。

     

    要搞清一个指针需要搞清指针的四方面的内容:

    [1]指针的类型,

    [2]指针所指向的类型,

    [3]指针的值或者叫指针所指向的内存区,

    [4]还有指针本身所占据的内存区。 --sizeof(指针变量名) 一般32位机都为4;

     

    指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的类型""指针所指向的类型"两个概念,是精通指针的关键点之一。

     

    [1]指针的类型: (指针本身在内存中的样子,类型)

      从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。

           [用途]在指针的赋值运算时,知道了指针的类型就可以来定右值得类型了;

          

    [2]指针所指向的类型:(指针所指向的内存块里应存的是什么样子(类型)的东东)

           当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

      从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。

     

    [3]指针的值或者叫指针所指向的内存区:

           指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。

          

    [小结]    

           :

           int(*ptr)[3];

           //指针的类型是int(*)[3] -- 一个数组指针,它相当于行指针,它所指的都是一个int()[3]--一维数组;

           //指针所指向的的类型是int()[3] -- 这个就是这个指针所指的内存中应存的东西的类型,及一维数组;

           指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在上面,指针所指向的类型已经有了(声明了),但由于指针还未初始化(未定义,未初始化),所以它所指向的内存区是不存在的,或者说是无意义的。

      以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?

    注意不知道指针指向哪里就开始用是很危险及隐蔽的错误!!!

     

    -------------------------------------实例分析----------------------------------

    例四:

     

      1chara[20];

      2int*ptr=a;

      ...

      ...

      3ptr+=5;

      在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5sizeof(int),在32位程序中就是加上了54=20。由于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。

    [说明]

           [1]因为指针的类型是int *,所以在第2步时不要误认为指针的类型是a的类型(char);

           [2]指针现在指向长度为20字节的数组a的首地址了,在第三句时ptr所指的地方正好越出了数组的边界

                  -- a[19] or (ptr+19).

                 

     

    --------------------------数组和指针的关系---------------------------------

      数组的数组名其实可以看作一个指针。看下例:

      例八:

    intarray[10]={0,1,2,3,4,5,6,7,8,9},value;

    ...

    ...

    value=array[0];//也可写成:value=*array;

    value=array[3];//也可写成:value=*(array+3);

    value=array[4];//也可写成:value=*(array+4);

    上例中,一般而言数组名array代表数组本身,类型是int[10],但如果把array看做指针的话,它指向数组的第0个单元,类型是int*,所指向的类型是数组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个指向数组第3个单元的指针,所以*(array+3)等于3。其它依此类推。

     

      例九:

    char*str[3]={

     "Hello,thisisasample!",

     "Hi,goodmorning.",

     "Helloworld"

    };

    chars[80]

    strcpy(s,str[0]);//也可写成strcpy(s,*str);

    strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));

    strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));

    上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char**,它指向的类型是char*

    *str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。 str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向的类型是char*

     

      *(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向 "Hi,goodmorning."的第一个字符'H',等等。

     

      下面总结一下数组的数组名的问题。声明了一个数组TYPE  array[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE[n];第二 ,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。

      在不同的表达式中数组名array可以扮演不同的角色。

      在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数测出的是整个数组的大小。

    在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第0号单元的值。sizeof(*array)测出的是数组单元的大小。

      表达式array+n(其中n=012....。)中,array扮演的是指针,故array+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。

    例十

    intarray[10];

    int(*ptr)[10];

    ptr=&array;

    上例中ptr是一个指针,它的类型是int(*)[10],他指向的类型是int[10] ,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。

     

      本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:

    int(*ptr)[10];

      则在32位程序中,有:

    sizeof(int(*)[10])==4

    sizeof(int[10])==40

    sizeof(ptr)==4

    实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。

    [Note]

    数组名可以看作是指针,但是数组名不是指针。也不会有自己的内存区

    &a

    &a[0]

    是最好的说明

    --------------------指针和结构类型的关系-------------------------------------------

    可以声明一个指向结构类型对象的指针。

      例十一:

    structMyStruct

    {

     int a;

     int b;

     int c;

    }

    MyStruct ss={20,30,40};

    //声明了结构对象ss,并把ss的三个成员初始化为203040

    MyStruct*ptr=&ss;

    //声明了一个指向结构对象ss的指针。它的类型是MyStruct*,它指向的类型是MyStruct

    int*pstr=(int*)&ss;

    //声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。

      请问怎样通过指针ptr来访问ss的三个成员变量?

      答案:

    ptr->a;

    ptr->b;

    ptr->c;

      又请问怎样通过指针pstr来访问ss的三个成员变量?

      答案:

    *pstr//访问了ss的成员a

    *(pstr+1);//访问了ss的成员b

    *(pstr+2)//访问了ss的成员c

      虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用pstr来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元:

      例十二:

    int array[3]={35,56,37};

    int*pa=array;

      通过指针pa访问数组array的三个单元的方法是:

    *pa;//访问了第0号单元

    *(pa+1);//访问了第1号单元

    *(pa+2);//访问了第2号单元

    从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。

      所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个"填充字节",这就导致各个成员之间可能会有若干个字节的空隙。

      所以,在例十二中,即使*pstr访问到了结构对象ss的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。

    过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。

          

    ---------------------------指针类型转换--------------------------------------

    没有隐式转化这一说,都要进行强转;

     

    如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP* 那么语法格式是:

      (TYPE*)p

      这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。

      一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,也会发生指针类型的转换。

      例十五:

    void fun(char*);

    int a=125, b;

    fun((char*)&a);

    ...

    ...

    void fun(char *s)

    {

    char c;

    c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;

    c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;

    }

     

    注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a的结果是一个指针,它的类型是int*,它指向的类型是int。形参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指针char*temp 然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指向的地址就是a的首地址。

    [Note] 编译器会自动用形参来转换指针的类型。

     

     

    想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:

      例十六:

    int a=123, b;

    int *ptr= &a;

    char *str;

    b=(int)ptr;//把指针ptr的值当作一个整数取出来。

    str=(char*)b;//把这个整数的值当作一个地址赋给指针str

    现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。

     

    -------------------------------------------指针的安全问题(重点)-------------------------------------

     

     

      例十七:

    char s='a';                          

    int *ptr;

    ptr=(int*)&s;

    *ptr=1298

    指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。

     

     

      让我们再来看一例:

      例十八:

      1char a;

      2int *ptr= &a;

      ...

      ...

      3ptr++;

      4*ptr=115;

      该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。

    在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不

    安全的。至于为什么,结合例十七来想一想,应该会明白的。

     

     


    最新回复(0)