1. 指针

指针与地址:

指针的内存布局

    先看一个例子: 

  1. int *p;

    这里定义了一个指针p,一个“int *”类型的模子在内存上咔出了8个字节(根据编译器环境不同而不同,64位是8个字节,32位是4个字节)的空间,然后这个空间命令为p,同时限定这4个字节的空间里面只能存储某个内存地址,即使你存入别的任何数据,都被当做地址来处理,而且这个内存地址开始的连续4个字符上只能存储某个int类型的数据。

    如上图所示,我们把p称为指针变量,p里存储的内存地址处的内存称为p所指向的内存。指针变量p里存储的任何数据都将被当作地址来处理。

    我们可以这么理解:一个基本的数据类型(包括结构体等自定义类型)加上“*”号就构成了一个指针类型的模子。这个模子的大小是一定的,与“*”号前面的数据类型无关。“*”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在64位系统下,不管什么样的指针类型,其大小都是8byte,sizeof(void *)也是8个字节

    注意int *p = NULL 和 *p = NULL的区别

  1. int *p = NULL;  

    p的值为 0x00000000。解释为:定义一个指针变量p,其指向的内存里面保存的是int类型的数据,在定义变量p的同时把p的值设置为0x00000000,而不是把*p的值设置为0x00000000。这个过程叫做初始化,是在编译的时候进行的。

    然后再看下面的代码:

  1. int *p;   //定义一个指针变量p,指向内存里面保存的是int类型的数据,但是此时p本身的值不知道,也就是说现在变量p保存的可能是一个非法地址

  2. *p = NULL; //给*p赋值为NULL,即给p指向的内存赋值为NULL,但是由于p指向的内存可能是非法的,所以编译器会报告一个内存错误

    因此,我们可以改写上面的代码,使p指向一块合法的内存

  1. int i = 10;

  2. int  *p = &i;

  3. *p = NULL;

    调试的时候可以发现:p所指向的内存地址存储的数据从10变成了0,但是p本身的值,也就是内存地址并没有变。

    备注:NULL是一个宏定义  

  1. #define NULL 0


如何将数值存储到指定的内存地址

    假设现在需要往内存0x12ff7c地址上存入一个整型数0x100。我们怎么才能做到呢?我们知道可以通过一个指针向其指向的内存地址写入数据,那么这里的内存地址0x12ff7c其本质就是一个指针。所以我们可以用下面的方法:

  1. int *p = (int *)0x12ff7c;    //将地址0x12ff7c赋值给指针变量p的时候必须强制转换

  2. *p = 0x100;

    也可以这么写:

  1. *(int *)0x12ff7c = 0x100;

2. 数组

数组的内存布局

先看一个例子

  1. int a[5];    

    上面定义了一个数组,其包含了5个int型的数据,我们可以用a[0], a[1]等来访问数组里面的每一个元素,那么这些元素的名字就是a[0], a[1]...吗?我们看一个图:

    如上图所示,当我们定义一个数组a时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为a,名字a一旦与这块内存匹配就不能改变。a[0], a[1]等为a的元素,但并非元素的名字。数组的每一个元素都是没有名字的。那现在再看一下sizeof和数组的几个问题:

    在64位系统环境下:(sizeof关键字求值是在编译时)

  1. #include <stdio.h>

  2. int main(int argc, char const *argv[])

  3. {

  4. int a[5];

  5. printf("%lu\n", sizeof(a));     // 20

  6. printf("%lu\n", sizeof(&a)); //  8

  7. printf("%lu\n", sizeof(a[0]));  //  4

  8. printf("%lu\n", sizeof(&a[0])); //  8

  9. return 0;

  10. }

&a[0]与&a的区别

    a[0]是一个元素, a是整个数组,虽然&a[0]和&a的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。比如:湖南的省政府在长沙,而长沙的市政府也在长沙,两个政府都在长沙,但其代表的意义完全不同。这里也是同一个意思。


字符数组


字符串

    是一个以'\0'结尾的字符数组,是一串字符。

    定义及初始化:char arr[] = "abc"; 或 char arr[4] = {'a', 'b', 'c', 'd', '\0'};

    输出:printf("%s\n", s); 或 printf("%s\n", &arr[0]);

    赋值:strcpy(字符变量名,"字符串");


    特点

  1. #include <stdio.h>

  2. int main(int argc, char const *argv[])

  3. {

  4. char s[] = "abcd";

  5. char s1[] = {'a', 'b', 'c'};

  6. printf("%s\n", s1);   //  adcabcd 在内存访问到'\0'为止,所以连s一起输出

  7. return 0;

  8. }

指针与数组

    

以指针的形式访问和以下标的形式访问

    指针与数组之间似是而非的特点。例如,有如下定义:

  1. A)  char *p = "abcdef";

  2. B)  char a[] = "123456";

以指针的形式访问和以下标的形式访问指针 

   A定义了一个指针变量p,p本身在栈上占8个字节,p里面存储的是一块内存的首地址。这块内存在静态区,其空间大小为7个byte,这块内存没有名字,对这块内存的访问完全是匿名的访问。比如要读取字符'e',我们有两种方式:

    1)以指针的形式:*(p+4)  先取出p里存储的地址值,假设为0x0000FF00,然后加上4个字符的偏移量,得到新的地址0x0000FF04,然后取出0x000FF04地址上的值。

    2)以下标的形式:p[4]  编译器总是把下标的形式的操作解析为以指针的形式的操作。p[4]这个操作会被解析成:先取出p里存储的地址值,然后加上中括号中4个元素的偏移量,计算出新的地址,然后从新的地址中取出值,也就是说以下标的形式访问在本质上与指针的的形式访问没有区别,只是写法上不同罢了。

以指针的形式访问和以下标的形式访问数组

    B定义了一个数组a,a拥有7个char类型的元素,其空间大小为7。数组a本身在栈上面。对a的元素的访问必须先根据数组的名字a找到数组首元素的首地址,然后根据偏移量找到相应的值。这是一种典型的“具名 + 匿名”访问。

    指针和数组是两个完全不一样的东西,只是它们都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型的具名+匿名访问。一定要注意的是这个“以XXX的形式的访问”这种表达方式。

偏移量的单位是元素的个数而不是byte数,计算地址时需注意。

a和&a的区别

我们看一个例子:


  1. #include <stdio.h>

  2. int main(int argc, char const *argv[])

  3. {

  4. int a[5] = {1,2,3,4,5};

  5. int *ptr = (int *)(&a+1);

  6. printf("%d, %d\n", *(a+1), *(ptr-1));  // 2 5

  7. return 0;

  8. }

这个例子主要考察关于指针加减操作的理解

    对指针进行加1操作,得到的是下一个元素的地址,而不是原有地址值直接加1.所以一个类型为T的指针的移动,以sizeof(T)为移动单位。因此,a是一个一维数组,数组中有5个元素:ptr是一个int型的指针。

    &a+1:取数组a的首地址,该地址的值加上sizeof(a)的值,即 &a + 5*sizeof(int),也就是下一个数组的首地址,显然当前指针已经越过了数组的界限。

    (int *)(&a+1):则是把上一步计算出来的地址,强制转化为int *类型,赋值给ptr。

    *(a + 1):a,&a的值是一样的,但意思不一样,a是数组首元素的首地址,也就是a[0]的首地址,&a是数组的首地址,a+1是数组下一元素的首地址,即a[1]的首地址。&a+1是下一个数组的首地址,所以输出2.
    *(ptr - 1):因为ptr是指向a[5],并且ptr是int*类型,所以*(ptr-1)是指向a[4],输出5。

    

指针与数组的特性总结

指针数组
保存数据的地址,任何存入指针变量p的数据都会被当做地址来处理。p本身的地址由编译器另外存储,存储在哪里,我们并不知道。保存数据,数组名a代表的是数组首元素的首地址而不是数组的首地址。&a才是整个数组的首地址。a本身的地址由编译器另外存储,存储在哪里,我们并不知道。
间接访问数据,首先取得指针变量p的内容,把它作为地址,然后从这个地址提取数据或向这个地址写入数据。指针可以以指针的形式访问*(p+i); 也可以以下标的形式访问p[i]。但本质都是先取p的内容然后加上i*sizeof(类型)个byte作为数据的真正地址。直接访问数据,数组名a是整个数组的名字,数组内每个元素并没有名字。只能通过“具名+匿名”的方式来访问某个元素,不能把数组当一个整体来进行读写操作。数组可以以指针的形式访问*(a+i); 也可以以下标的形式访问 a[i]。但其本质都是a所代表的数组首元素的首地址加上i*sizeof(类型)个byte作为数据的真正地址。
通常用于动态数据结构通常用于存储固定数目且数据类型相同的元素。
相关的函数为malloc和free隐式分配和删除
通常指向匿名数据(当然也可指向具名数据)自身即为数组名

指针数组和数组指针

指针数组和数组指针的内存布局

    指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它使“存储指针的数组”的简称。

    数组指针:首先它使一个指针,它指向一个数组,在64位系统下永远是占4个字节,至于它指向的数组占多少字节,并不知道。它是“指向数组的指针”的简称。

  1. A)  int *p1[10];   //指针数组
  2. B)  int (*p2)[10]; //数组指针

这里有一个符号优先级的问题,‘[]’的优先级比‘*’要高。

p1先于‘[]’结合,构成一个数组的定义,数组名为p1,int修饰的数组的内容,即数组的每个元素。总的来讲,这是一个数组,其包含10个指向int类型数据的指针,即指针数组。

p2中‘()’的优先级比‘[]’高,‘*’号和p2构成一个指针的定义,指针变量名为p2,int修饰的是数组的内容,即每个数组的每个元素。数组在这里并没有名字,是一个匿名数组。总的讲,p2是一个指针,它指向一个包含10个int类型数据的数组,即数组指针。


也许可以这么理解数组指针

    通常定义指针是在数据类型后面加上指针变量名,那p2定义如下:

int (*)[10] p2;

int ()


a与&a之间的区别

  1. #include <stdio.h>
  2. int main(int argc, char const *argv[])
  3. {
  4. char a[5] = {'A', 'B', 'C', 'D'};
  5. char (*p1)[3] = &a;
  6. char (*p2)[3] = a;
  7. char (*p3)[5] = &a;
  8. char (*p4)[5] = a;
  9. /*
  10. 输出  D D BCD BCD 乱码 乱码   偏移单位由数组指针中的数组长度决定

  11. */
  12. printf("%s\n%s\n%s\n%s\n%s\n%s\n", *(p1+1), *(p2+1), *p1+1, *p2+1, *(p3+1), *(p4+1));
  13. return 0;
  14. }

地址的强制转换

先看一个例子

  1. struct Test
  2. {
  3. int Num;
  4. char *pcName;
  5. short sDate;
  6. char cha[2];
  7. short sBa[4];
  8. } *p;

假设p的值为0x100000,那么:

    p + 0x1 = 0x100018

    (unsigned long)p + 0x1 = 0x100001

    (unsigned int *)p + 0x1 = 0x100004

    首先需要明白一个知识点,指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是byte二十元素的个数。所以:

    p+0x1的值为0x100000+sizeof(Test) * 0x1。至于此结构体的大小为20byte,所以p + 0x1的值为:0x100014。

    (unsigned long)p+0x1的值涉及到强制类型转换,将指针变量p保存的值强制类型转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加另一个整数,所以其值为:0x1000001。

    (unsigned int *)p+0x1中,这里的p被强制转换成一个指向无符号整型的指针,所以其值为:0x1000000+sizeof(unsigned int) * 0x1,等于0x100004。


二维数组与指针

先看一个例子:

  1. int a[5][5];
  2. int (*p)[4];
  3. p = a;

  4. //&p[4][2] - &a[4][2]的值是?

    答案是 -4

    &a[4][2]表示的是: &a[0][0]+4*5*sizeof(int) + 2*sizeof(int)

    p[4]相对于p[0]来说是向后移动了4个“包含4int 类型元素的数组”, 即&p[4]表示的是&p[0]+4*4*sizeof(int)。由于p被初始化为&a[0],那么&p[4][2]表示的是&a[0][0] + 4*4*sizeof(int) + 2*sizeof(int)。 

    所以,&p[4][2]和&a[4][2]的值相差4个int类型的元素。可以用下面的内存布局图来表示:

二级指针

    二级指针的内存布局

    char **p;

    定义了一个二级指针变量 pp 是一个指针变量,毫无疑问在 64 位系统下占 8 byte。 它与一级指针不同的是,一级指针保存的是数据的地址,二级指针保存的是一级指针的地址。 

    任何指针变量都可以被初始化为NULL。

数组参数与指针参数

    数组作为参数传递到函数里面时,传入的是地址,占8位。数组并没有传递至函数内部

    C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。非数组形式的数据实参均以传值形式(对实参做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改传递给它的那份拷贝)调用。同样,函数的返回值也不能是一个数组,而只能是指针。明确一个概念:函数本身是没有类型的,只有函数的返回值才有类型。

  1. void fun(cahr a[])
  2. {
  3.    char c = a[3];
  4. }
  5. int main()
  6. {
  7.    char b[100] = "abcdefg";
  8.    fun(b);
  9.    return 0;
  10. }

指针参数

  1. void fun(char *p)
  2. {
  3.    char c = p[3];  //或者是 char c = *(p+3);
  4. }
  5. int main()
  6. {
  7.    char *p2 = "abcdefg";
  8.    fun(p2);
  9.    return 0;
  10. }
    上面的例子是对实参做一份拷贝并传递给被调用的函数,即对p2做一份拷贝,传递到函数内部的并非p2本身。
  1. void getMemory(char *p, int num)
  2. {
  3.    p = (char *)malloc(num*sizeof(char));
  4. }
  5. int main() {
  6.    char *str = NULL;
  7.    GetMemory(str, 10);
  8.    strcpy(str, "hello");
  9.    free(str);        //free 并没有起作用,内存泄漏
  10.    return 0;
  11. }

  在运行 strcpy(str,”hello”)语句的时候发生错误。这时候观察 str 的值,发现仍然为 NULL,也就是说 str 本身并没有改变。 

    所以,我们可以这么做:

  1. //第一: 用 return。
  2. char * GetMemory(char * p, int num) {
  3.    p = (char *)malloc(num*sizeof(char));
  4.    return p;
  5. }
  6. int main() {
  7.    char *str = NULL;
  8.    str = GetMemory(str,10);
  9.    strcpy(str, "hello");
  10.    free(str);
  11.    return 0;
  12. }
  1. 第二:用二级指针。
  2. void GetMemory(char ** p, int num) {
  3.    *p = (char *)malloc(num*sizeof(char));
  4.    return p;
  5. }
  6. int main() {
  7.    char *str = NULL;
  8.    GetMemory(&str,10);
  9.    strcpy(str,”hello”);
  10.    free(str);
  11.    return 0;
  12. }

    注意,这里的参数是&str而非str。这样的话传递过去的是str的地址,是一个值。在函数内部,用钥匙 “*”来开锁:*(&str),其值就是str,所以malloc分配的内存地址是真的赋值给了str本身。


二维数组参数与二维指针参数

    

  1. void fun(char a[3][4]);

    可以把 a[3][4]理解为一个一维数组 a[3],其每个元素都是一个含有 4 char 类型数据的数组。C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。”在这里同样适用。所以我们可以把这个函数声明改写成:

  1. void fun(char (*p)[4]);  //括号绝对不能省略,这样才能保证编译器把p解析为一个指向包含4个char类型数据元素的数组,即一维数组a[3]的元素。

    同样,一维数组“[]”内的数字完全可以省略,不过第二维的维数不能省略。
  1. void fun(cahr a[][4]);
    或者写为(这是因为参数*p[4],对于 p 来说,它是一个包含 4 个指针的一维数组,同样把这个一维数组也改写为指针的形式,那就得到上面的写法。):
  1. void fun(char **p);

二维数组和二维指针参数的等效关系:

数组参数等效的指针参数
数组的数组:char a[3][4]数组的指针:char (*p)[10]
指针数组:   char *a[5]指针的指针:char **p

需要注意:C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维再也不可改写。