• 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 个元素的数组;这个数字里 面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针、参数为一个 指向字符的指针的函数。

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

  1. C语言深度剖析书籍学习记录 第六章 函数

    函数的好处 1.降低复杂性:使用函数的最首要原因是为了降低程序的复杂性,可以使用函数来隐含信息,从而使你不必再考虑这些信息. 2.避免重复代码段:如果在两个不同函数中的代码很相似,这往往意味着分解工作 ...

  2. C语言深度剖析书籍学习记录 第三章 预处理

    宏 _LINE_ 表示正在编译的文件的行号 _FILE_ 表示正在编译的文件的名字 _DATE_ 表示编译时刻的日期字符串,例如: "25 Dec 2007" _TIME_ 表示编 ...

  3. C语言深度剖析书籍学习记录 第七章 文件结构

  4. C语言深度剖析书籍学习记录 第五章 内存管理

    常见的内存错误 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存. 结构体成员指针未初始化 很多初学者犯了这个错误还不知道是怎么回事.这里定义了结构体变量 stu,但是他没 想到这 ...

  5. C语言深度剖析书籍学习记录 第一章 关键字

    C语言标准定义了32个关键字 union声明联合数据类型 Union declaration - cppreference.com 维护足够的空间来置放多个数据成员中的"一种",而 ...

  6. C语言深度剖析书籍学习记录 第二章 符号

    \ 连接符号,// \  可以把下一行也注释调 编译器 删除注释时,会使用空格进行替代  

  7. 《SysML精粹》学习记录--第四章

    <SysML精粹>学习记录 第四章:内部模块图(Internal Block Diagram,IBD) IBD介绍 组成部分属性 引用属性 连接器 项目流 内嵌组成部分和引用 小结 第四章 ...

  8. Solidity学习记录——第四章

    Solidity学习记录 第一章 创建生产僵尸的工厂 第二章 设置僵尸的攻击功能 第三章 编写DAPP所需的基础理论 第四章 完善僵尸功能 第五章 ERC721 标准和加密资产 文章目录 Solidi ...

  9. 《C语言深度剖析》学习笔记----C语言中的符号

    本节主要讲C语言中的各种符号,包括注释符.单引号双信号以及逻辑运算符等. 一.注释符 注释符号和注释在程序的预编译期就已经被解决了,在预编译期间,编译器会将注释符号和注释符号之间的部分简单的替换成为空 ...

最新文章

  1. AI已来,更有未来!科大讯飞全球1024开发者节,这场AI盛会你也可以云参与
  2. javascript(arguments)
  3. python2.7安装tensorflowgpu_Ubuntu16.04+Python2.7+CUDA9.0+cuDNN7.0+TensorFlow 1.6 安装随笔
  4. arduino定时器函数如何使用_excel如何使用函数公式来查找图片
  5. 春运公益片“情满回家路”上线 顺风车等出行方式再被呼吁
  6. java中堆 栈的英文_Java中的栈和堆
  7. 20110614 开机脚本,统一管理员密码,exe,布线
  8. Linux中root安装microsoft R open(MRO),普通用户如何使用?
  9. LabView-之1: 串口驱动
  10. 高速PCB设计之阻焊层和助焊层的检查
  11. 阿里云域名转入/转出操作教程
  12. QT画贝塞尔曲线 和 曲线与斜率、一阶导数 、二阶导数的关系
  13. ​Copyright到底是什么意思?
  14. VS2008下VLC播放器,实现播放、暂停、停止、快进、截图、进度条显示、进度条控制功能
  15. 数学速算法_小数减法如何学?3种心算速算法,快捷有效,让孩子爱上数学
  16. 短信网关 php,php使用ICQ网关发送手机短信_PHP
  17. %、.format()格式化
  18. matlab模糊自适应pid控制仿真程序,模糊自适应整定PID控制matlab仿真程序(刘金锟-先进PID控制及其MATLAB仿真)...
  19. MDM9x07 平台启动
  20. 期货柜台怎样进行配置?

热门文章

  1. 固定资产管理有关的计算机知识,计算机技术在固定资产管理中的具体应用.pdf...
  2. 【转】WPF从我炫系列3---内容控件的用法
  3. 【转】带你玩转Visual Studio——01.开篇介绍
  4. c#多线程总结(纯干货)
  5. Failed to instantiate file__from module__The specified list does
  6. SharePoint 2010 WSP包部署过程中究竟发生什么?
  7. php扇形分布图,php生成扇形比例图的实例代码
  8. 一种类的渐进式开发写法
  9. 【PTA天梯赛CCCC -2017决赛L1-6 】整除光棍 (20 分)(大数模拟除法)
  10. c/c++,字符,字符串,各种方式读入与对空格,回车的处理