C语言深度剖析书籍学习记录 第四章 指针和数组

  •  p 称为指针变量,p 里存储的内存地址处的内存称为 p 所指向的内存。 指针变量 p 里存储的任何数据都将被当作地址来处理
  • 一个基本的数据类型(包括结构体等自定义类型)加上“*” 号就构成了一个指针类型的模子。这个模子的大小是一定的,与“*”号前面的数据类型无 关。
  • “*”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在 32 位 系统下,不管什么样的指针类型,其大小都为 4byte。可以测试一下 sizeof(void *)。
  • int *p = NULL;  这时候通过编译器查看 p 的值为 0x00000000。这句代码的意思是:定义一个指针 变量 p,其指向的内存里面保存的是 int 类型的数据;
  • 在定义变量 p 的同时把 p 的值设置为 0x00000000,而不是把*p 的值设置为 0x00000000。
  • 这个过程叫做初始化,是在编译的时候 进行的。避免野指针,指向一段关键数据的地址造成数据的损毁
  • int *p;  *p = NULL; 同样,我们可以在编译器上调试这两行代码。第一行代码,定义了一个指针变量 p,其指向 的内存里面保存的是 int 类型的数据;但是这时候变量 p 本身的值是多少不得而知,也就是 说现在变量 p 保存的有可能是一个非法的地址。第二行代码,给*p 赋值为 NULL,即给 p 指向的内存赋值为 NULL;但是由于 p 指向的内存可能是非法的,所以调试的时候编译器可 能会报告一个内存访问错误。这样的话,我们可以把上面的代码改写改写,使 p 指向一块合 法的内存:
  • int i = 10; int *p = &i; *p = NULL;  在编译器上调试一下,我们发现 p 指向的内存由原来的 10 变为 0 了;而 p 本身的值, 即内存地址并没有改变。经过上面的分析,相信你已经明白它们之间的区别了。不过这里还有一个问题需要注 意,也就是这个 NULL。初学者往往在这里犯错误。注意 NULL 就是 NULL,它被宏定义为 0:
    #define NULL 0
  • 很多系统下除了有 NULL 外,还有 NUL(Visual C++ 6.0 上提示说不认识 NUL)。NUL 是 ASCII 码表的第一个字符,表示的是空字符,其 ASCII 码值为 0。其值虽然都为 0,但表示的意思 完全不一样。同样,NULL 和 0 表示的意思也完全不一样。一定不要混淆。
  • 另外还有初学者在使用 NULL 的时候误写成 null 或 Null 等。这些都是不正确的,C 语 言对大小写十分敏感啊。当然,也确实有系统也定义了 null,其意思也与 NULL 没有区别, 但是你千万不用使用 null,这会影响你代码的移植性。

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

  • 假设现在需要往内存 0x12ff7c 地址上存入一个整型数 0x100。我们怎么才能做到呢?我 们知道可以通过一个指针向其指向的内存地址写入数据,那么这里的内存地址 0x12ff7c 其 本质不就是一个指针嘛。所以我们可以用下面的方法:   int *p = (int *)0x12ff7c;   *p = 0x100;
  • 需要注意的是将地址 0x12ff7c 赋值给指针变量 p 的时候必须强制转换。至于这里为什么选择内存地址 0x12ff7c,而不选择别的地址,比如 0xff00 等。这仅仅是为了方便在 Visual C++ 6.0 上测试而已。如果你选择 0xff00,也许在执行*p = 0x100;这条语句的时候,编译器 会报告一个内存访问的错误,因为地址 0xff00 处的内存你可能并没有权力去访问。
  • 既然这 样,我们怎么知道一个内存地址是可以合法的被访问呢?也就是说你怎么知道地址 0x12ff7c 处的内存是可以被访问的呢?其实这很简单,我们可以先定义一个变量 i,比如:int i = 0;  变量 i 所处的内存肯定是可以被访问的。然后在编译器的 watch 窗口上观察&i 的值不就 知道其内存地址了么?这里我得到的地址是 0x12ff7c,仅此而已(不同的编译器可能每次给 变量 i 分配的内存地址不一样,而刚好 Visual C++ 6.0 每次都一样)。你完全可以给任意一个 可以被合法访问的地址赋值。得到这个地址后再把“int i = 0;”这句代码删除。
  • *(int *)0x12ff7c = 0x100;  这行代码其实和上面的两行代码没有本质的区别。先将地址 0x12ff7c 强制转换,告诉编译 器这个地址上将存储一个 int 类型的数据;然后通过钥匙“*”向这块内存写入一个数据。

  • 如上图所示,当我们定义一个数组 a 时,编译器根据指定的元素个数和元素的类型分配确定 大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为 a。名字 a 一旦 与这块内存匹配就不能被改变。a[0],a[1]等为 a 的元素,但并非元素的名字。数组的每一个 元素都是没有名字的。那现在再来回答第一章讲解 sizeof 关键字时的几个问题:
  • sizeof(a)的值为 sizeof(int)*5,32 位系统下为 20。
  • sizeof(a[0])的值为 sizeof(int),32 位系统下为 4。
  • sizeof(a[5])的值在 32 位系统下为 4。并没有出错,为什么呢?我们讲过 sizeof 是关键字 不是函数。函数求值是在运行的时候,而关键字 sizeof 求值是在编译的时候。虽然并不存在 a[5]这个元素,但是这里也并没有去真正访问 a[5],而是仅仅根据数组元素的类型来确定其 值。所以这里使用 a[5]并不会出错。
  • sizeof(&a[0])的值在 32 位系下为 4,这很好理解。取元素 a[0]的首地址。 sizeof(&a)的值在 32 位系统下也为 4,这也很好理解。取数组 a 的首地址
  • &a[0]和&a 的区别   a[0]是一个元素,a 是整个数组,虽然&a[0]和&a 的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。举个 例子:湖南的省政府在长沙,而长沙的市政府也在长沙。两个政府都在长沙,但其代表的 意义完全不同。这里也是同一个意思。 

数组名 a 作为左值和右值的区别

  • 简单而言,出现在赋值符“=”右边的就是右值,出现在赋值符“=”左边的就是左值。  比如,x=y。
  • 左值:在这个上下文环境中,编译器认为 x 的含义是 x 所代表的地址。这个地址只有 编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。
  • 右值:在这个上下文环境中,编译器认为 y 的含义是 y 所代表的地址里面的内容。这 个内容是什么,只有到运行时才知道。
  • C 语言引入一个术语-----“可修改的左值”。意思就是,出现在赋值符左边的符号所代 表的地址上的内容一定是可以被修改的。换句话说,就是我们只能给非只读变量赋值。
  • 当 a 作为右值的时候代表的是什么意思呢?很多书认为是数组的首地址,其实这是非常 错误的。a 作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组 的首地址。这是两码事。但是注意,这仅仅是代表,并没有一个地方(这只是简单的这么 认为,其具体实现细节不作过多讨论)来存储这个地址,也就是说编译器并没有为数组 a 分配一块内存来存其地址,这一点就与指针有很大的差别。
  • a 不能作为左值!编译器会认为数组名作为左值代表 的意思是 a 的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数组的某个元素而无法把数组当一个总体进行访问。所以我们可以把 a[i]当左值,而无法把 a 当左值。其实我们完全可以把 a 当一个普通的变量来看,只不过这个变量内部分为很多小块, 我们只能通过分别访问这些小块来达到访问整个变量 a 的目的。
  • A),char *p = “abcdef”;
  • B),char a[] = “123456”;

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

  • 例子 A)定义了一个指针变量 p,p 本身在栈上占 4 个 byte,p 里存储的是一块内存的首地址。这块内存在静态区,其空间大小为 7 个 byte,这块内存也没有名字。对这块内存的访 问完全是匿名的访问。比如现在需要读取字符‘e’,我们有两种方式:
  • 1),以指针的形式:*(p+4)。先取出 p 里存储的地址值,假设为 0x0000FF00,然后加 上 4 个字符的偏移量,得到新的地址 0x0000FF04。然后取出 0x0000FF04 地址上的值。
  • 2),以下标的形式:p[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操作。p[4]这个操作会被解析成:先取出 p 里存储的地址值,然后加上中括号中 4 个元素的偏 移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标的形式访问在本质上 与以指针的形式访问没有区别,只是写法上不同罢了。
  • 偏移量的单位是元 素的个数而不是 byte 数

#include <iostream>

int main(){
    int a[] = {1,2,3,4,5,6,7,8,9,10};
    int *ptr = (int*)(&a + 1);
    printf("%d,%d",*(a+1),*(ptr-1));
}
  • 对指针进行加 1 操作,得到的是下一个元素的地址,而不是原有地址值直接加 1。所以,一个类型为 T 的指针的移动,以 sizeof(T) 为移动单位。 因此,对上题来说,a 是一个一 维数组,数组中有 5 个元素; ptr 是一个 int 型的指针。 

  • 这就是为什么 extern char a[]与 extern char a[100]等价的原因。因为这只是声明,不分配 空间,所以编译器无需知道这个数组有多少个元素。这两个声明都告诉编译器 a 是在别的文 件中被定义的一个数组,a 同时代表着数组 a 的首元素的首地址,也就是这块内存的起始地 址。数组内地任何元素的的地址都只需要知道这个地址就可以计算出来。
  • 但是,当你声明为 extern char *a 时,编译器理所当然的认为 a 是一个指针变量,在 32 位系 统下,占 4 个 byte。这 4 个 byte 里保存了一个地址,这个地址上存的是字符类型数据。虽 然在文件 1 中,编译器知道 a 是一个数组,但是在文件 2 中,编译器并不知道这点。大多数 编译器是按文件分别编译的,编译器只按照本文件中声明的类型来处理。所以,虽然 a 实际 大小为 100 个 byte,但是在文件 2 中,编译器认为 a 只占 4 个 byte。
  • 编译器会把存在指针变量中的任何数据当作地址来处理。所以,如果需要 访问这些字符类型数据,我们必须先从指针变量 a 中取出其保存的地址。如下图: 

 

  • 在文件 1 中,编译器分配 4 个 byte 空间,并命名为 p。同时 p 里保存了字符串常量“abcdefg” 的首字符的首地址。这个字符串常量本身保存在内存的静态区,其内容不可更改。
  • 在文件 2 中,编译器认为 p 是一个数组,其大小为 4 个 byte,数组内保存的是 char 类型的数据。
  • 在 文件 2 中使用 p 的过程如下图: 

 

 指针数组和数组指针

  • 初学者总是分不出指针数组与数组指针的区别。其实很好理解:
  • 指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身 决定。它是“储存指针的数组”的简称。
  • 数组指针:首先它是一个指针,它指向一个数组。在 32 位系统下永远是占 4 个字节, 至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。
  • 下面到底哪个是数组指针,哪个是指针数组呢:
    • A),int *p1[10];         指针数组
    • B),int (*p2)[10];       数组指针
  • 这里需要明白一个符号之间的优先级问题。 “[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,int * 修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10 个 指向 int 类型数据的指针,即指针数组。
  • 至于 p2 就更好理解了,在这里“()”的优先级比 “[]”高,“*”号和 p2 构成一个指针的定义,指针变量名为 p2,int 修饰的是数组的内容, 即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2 是一个指 针,它指向一个包含 10 个 int 类型数据的数组,即数组指针。
  • 我们可以借助下面的图加深 理解:

  • char a[5]={'A','B','C','D'};
  • char (*p3)[5] = &a;     正确
  • char (*p4)[5] = a;       需要类型转换
  • &a 是整个数组的首地址,a 是数组首元素的首地址,其值相同但意义不同

 

  •  根据上面的讲解,&a+1 与 a+1 的区别已经清楚。
  • ptr1:将&a+1 的值强制转换成 int*类型,赋值给 int* 类型的变量 ptr,ptr1 肯定指到数 组 a 的下一个 int 类型数据了。ptr1[-1]被解析成*(ptr1-1),即 ptr1 往后退 4 个 byte。所以其 值为 0x4。
  • ptr2:按照上面的讲解,(int)a+1 的值是元素 a[0]的第二个字节的地址。然后把这个地址 强制转换成 int*类型的值赋给 ptr2,也就是说*ptr2 的值应该为元素 a[0]的第二个字节开始的 连续4个byte的内容。
  • 其内存布局如下图:

 

 

  •  使用一个指针分配内存空间,复制数据
  • 在运行 strcpy(str,”hello”)语句的时候发生错误。这时候观察 str 的值,发现仍然为 NULL。 也就是说 str 本身并没有改变,我们 malloc 的内存的地址并没有赋给 str,而是赋给了_str。 而这个_str 是编译器自动分配和回收的,我们根本就无法使用。所以想这样获取一块内存是 不行的。那怎么办?
  • 两个办法: 使用return返回内存空间 
  • 使用二级指针

 

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

函数指针

  • char * (*fun1)(char * p1,char * p2);   fun1 指针变量指向的是一个函数

 

  •  给函数指针赋值时,可以用&fun 或直接用函数名 fun。这是因为函数名被 编译之后其实就是一个地址,所以这里两种用法没有本质的差别

*(int*)&p ----这是什么?

  • void (*p)();    这行代码定义了一个指针变量 p,p 指向一个函数,这个函数的参数和返回值都是 void。
  • &p 是求指针变量 p 本身的地址,这是一个 32 位的二进制常数(32 位系统)。
  • (int*)&p 表示将地址强制转换成指向 int 类型数据的指针。
  • (int)Function 表示将函数的入口地址强制转换成 int 类型的数据。
  • *(int*)&p=(int)Function;表示将函数的入口地址赋值给指针变量 p。
  • 那么(*p) ();就是表示对函数的调用。
  • 其实函数指针与普通指针没什么差别,只是指向的内容不同而已。 使用函数指针的好处在于,可以将实现同一功能的多个模块统一起来标识,这样一来更
    容易后期的维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦 合度以及使接口与实现分开。

(*(void(*) ())0)();

  • 第一步:void(*) (),可以明白这是一个函数指针类型。这个函数没有参数,没有返回值。
  • 第二步:(void(*) ())0,这是将 0 强制转换为函数指针类型,0 是一个地址,也就是说一个函数存在首地址为 0 的一段区域内。
  • 第三步:(*(void(*) ())0),这是取 0 地址开始的一段内存里面的内容,其内容就是保存在首地址为 0 的一段区域内的函数。
  • 第四步:(*(void(*) ())0)(),这是函数调用。

函数指针数组  char * (*pf[3])(char * p);

  • 这是定义一个函数指针数组。它是一个数组,数组名为 pf,数组内存储了 3 个指向函数的 指针。这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函 数。这念起来似乎有点拗口。不过不要紧,关键是你明白这是一个指针数组,是数组

 

函数指针数组指针 char * (*(*pf)[3])(char * p);

  • 注意,这里的pf和上一节的pf就完全是两码事了。上一节的pf并非指针,而是一个数组名; 这里的 pf 确实是实实在在的指针。这个指针指向一个包含了 3 个元素的数组;这个数字里 面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针、参数为一个 指向字符的指针的函数。

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值