免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: _HellAngel_
打印 上一主题 下一主题

[C] 脑乱了。。。我发现我完全不会为指针分配内存空间。。。 [复制链接]

论坛徽章:
3
寅虎
日期:2013-11-27 07:53:29申猴
日期:2014-09-12 09:24:152015年迎新春徽章
日期:2015-03-04 09:48:31
21 [报告]
发表于 2013-01-19 18:49 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
0
22 [报告]
发表于 2013-01-19 20:10 |只看该作者
或许应该说:

指针只需要指向可用的内存就行了。(可以理解「指向」为「赋值为某个地址」,比如 int *p = &a 就是将 p 指向 a,也就是将 p 赋值为 a 的地址)

而获取可用内存的一种方法是使用现有内存(比如 int *p = &a),另一种方法是自己分配可用内存(比如 malloc 成功时返回值就是一个可用的内存地址)。

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
23 [报告]
发表于 2013-01-19 22:25 |只看该作者
本帖最后由 Ager 于 2013-01-19 22:32 编辑
timothyqiu 发表于 2013-01-19 20:10
而获取可用内存的一种方法是使用现有内存(比如 int *p = &a),另一种方法是自己分配可用内存(比如 malloc 成功时返回值就是一个可用的内存地址)。


嗯,的确如此。我能想到的方法,也就这两种。

To 楼主:

简单地重申一下我以前所解释过的:

第一,指针,只是一种“引用”某个/某系列数据的Handle。在这个视角下,这种“引用”可以只存在于你的观念中,可以跟所谓“地址”无关,甚至可以跟所谓“内存”无关。

第二,然而,数据之于你,是在你的观念中;但之于计算机,则是由“内存”来承载。在这样的范畴中,跟“内存”有关的机构与方法,被呈现出来。比如:函数malloc成为承载数据的内存与指针的引用机制之间的纽带。

第三,再然而,计算机并不具备人类所具有的那种以独特的方法去观察、找寻、分辨客体的能力。计算机只能倚靠某种“时间性的(一维性的)”方法,去找寻或分辨不同的“内存”客体。在这样的范畴中,“地址”机制被呈现出来。

三个范畴,三个层次,可以随你的学习过程,依次进深。

以上,仅供参考,呵呵 —— :)

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
24 [报告]
发表于 2013-01-19 22:41 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
25 [报告]
发表于 2013-01-19 22:57 |只看该作者
本帖最后由 Ager 于 2013-01-19 23:03 编辑

To 楼主:

楼上PM大虾已经把问题讲解得很细致了!

如果你还是不太理解的话,我估计 —— 你是在这两个字上犯了难:

“分配内存空间” 之 “分配” 二字。

是不?

呵呵。。。。。

论坛徽章:
1
白羊座
日期:2014-03-22 18:23:03
26 [报告]
发表于 2013-01-20 00:41 |只看该作者
回复 25# Ager


的确= =。“分配”二字着实很难理解= =。。。。楼上pm大神解释得挺详细的= =。。我消化消化。。。“分配”啊。。。这个分配。。。。。。。着实让人蛋疼= =
   

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
27 [报告]
发表于 2013-01-20 17:10 |只看该作者
本帖最后由 Ager 于 2013-01-20 22:25 编辑
_HellAngel_ 发表于 2013-01-20 00:41
回复 25# Ager

的确= =。“分配”二字着实很难理解= =。。。。楼上pm大神解释得挺详细的= =。。我消化消化。。。“分配”啊。。。这个分配。。。。。。。着实让人蛋疼= =




在下面,我将努力地试图为你正确地解释清楚这个问题。但囿于视角与知识的限制,我知道这种正确性是不可能达到所谓“100%”的;作为追求“正确性”的一个必要部分,我希望那些富有知识与经验的版友与大虾,能够不断对我的论述做出归正。总之,这里的话语世界是开放的。此外,所谓“正确性”也只能在双方的话语(彼此互动)中呈现出来(极端地说,如果你完全读不懂我所讲的,这里就不存在什么“正确性”了),也就是说,你也有一份完善相关知识的“正确性”的权利与义务:你应该力求用不易招致误会的语言,清楚地表达你的意思。呵呵。。



什么叫“分配(Allocate)”?

在计算机学科中,“分配”通常的意思是:该动作的施行者,在一大片特定的资源中,拿出其中的一部分,用来满足分配动作的受益者的特定需求。这样的需求,其目标可以是完成一项具体的任务。

逐项解释上述描述的几个关键词:

“分配”动作的施行者 —— 在不同的层面,可以由不同的当事人扮演这个角色:在编程者看来,自己永远是向计算机发号施令并且能够从计算机反馈的信息中解读意义的人,所以他可以是An Allocator;在代码看来,自己才是为编程者达成目的的人,所以他也可以是An Allocator;在某个库函数看来,自己才是令代码中某个标识符发生实际意义的人,所以他也可以是An Allocator;在编译器看来,自己才是让库函数真正变成计算机可以执行二进制代码的人,所以他也可以是An Allocator;在操作系统看来,自己才是实现与控制二进制代码流向宏指令解释器的人,所以他也可以是An Allocator;在计算机硬件的某个机构看来,前面的一切只有在他这里才能得以实施,所以他也可以是An Allocator;…… 在统治阶级看来,自己才是一切资源的分配者,其他的所有人都是TMD地在做梦!

也就是说,我们解读“分配”这个动词,必须根据特定的现场(语境),判断它的主语是谁。不能承认这一点,就会引致论坛上无穷无尽的争端。

“一大片特定的资源” —— 相比“分配”这个词的多义性来说,“资源”这个概念相对比较单纯。通常在一件事情上,它只有一个比较单纯的意思。比如:数据被承载,需要存储器,存储器上的空间就是这里的“一大片特定的资源”;我们的计算机接入网络,接入设备(Interface/网络适配器)需要一个让网络实体可以识别它的代码,比如是IP地址,那么,一系列IP地址所组成的集合,就是这里的“一大片特定的资源”;同理地,电话号码集合、机动车牌照号码集合、学生的学号集合、考生的准考证号码集合、一对夫妻想要让他们自己的生育行为是“合法的”所必需的那个生育证(俗称“准生证”)号码集合、可供我们死后“入土为安”的墓地空间的集合 …… 都是这里所说的“一大片特定的资源” —— 想要获得这样的资源,必须付出相应的代价(金钱、户口地位等等)。

为什么一定要“付出代价”?因为:资源是有限的! 只要是谈到“资源”二字,就意味者它一定是有限的。“无限的资源”只存在于人们的“理念”当中,比如一切自然数所组成的集合能够被认为是一项“资源”的话,那么它显然是无限的。在计算机世界中,各式各样的资源,同样都是有限的,不过初学者在编写他们的“小程序”时,获取这类特定状况下的资源,代价倒是非常小的。

基于“资源是有限的”这麽一个事实,资源的分配,自然就是有条件的。比如说:分配动作将资源的全体,划分成两大部分,一个是尚未被分配的部分,可以称它作是“空闲”的;另一个是已经被分配(为了完成一项具体的任务)的部分,可以称它作是“被分配”或“被占用”的; —— 分配动作,就是将“空闲”储备中的一部分“变”成“被分配的”(分配动作,只负责发生这种“变化”,不负责如何使用“被分配的”资源)。当“空闲”储备不足以满足完成一项具体的任务的需求的时候(比如“空闲”储备太少或太小了),分配就无法完成,即分配动作失败了。

关于分配动作成败的信息,并不是可有可无的。一个设计良好的分配过程,必须在它结束时明确地反馈出它的成败信息 —— 不论是哪个层面上的“分配”。

反过来,把那些“被分配”或“被占用”的内存空间,“归还”到“空闲”群体中,这个动作,叫做“释放内存”,即解除(to free)对那些内存空间的“奴役”。

“分配动作的受益者” —— 根据场合(语境)的不同,扮演此角色的当事者也不同,可以是编程者,也可以是一个“变量”(“变量”需要一个存储空间to being“变量”之“变”),也可以是一个数据结构的实例,也可以是一段程序,也可以是你的老板。

下面谈谈C语言中有关“内存分配”的事情。

先从“内存分配”这一动作的目的来说,也就是说,从内存本身的视角看,它到底被分配出去要做啥事?目前,只有一件事情是可以肯定的:内存就是为了承载数据(包括狭义的数据即运算的对象以及实现运算的代码比如程序、子程序/函数、指令等等)

一个在编程者观念中的“变量”,如何存在于计算机世界中,即一个观念状的符号,如何承载可被任意赋值或可被任意变化操弄的数据?(当然须在规范的约束之下)

显然,这样的数据,需要在存储空间(索性统称“内存”)中占有一席之地,并且编程者可以利用某个符号来Handle其中某个“一席之地”。

C语言通过“声明”与“定义”这两个不同的机制,来实现上述事情。

当你在程序代码中写下:
  1. char fuu;
复制代码
的时候,便是向编译器“说明”:一个程序代码层面上的“fuu”标识符,将在未来的程序代码中,始终用来Handle这个观念中的“变量”于内存所占有的那“一席之地”(是为“左值”机构),除非在这项“说明”因为某些原因而丧失了之于编译器的“可见性”。这一类关于“变量”、函数的“说明”,用C语言的行话来说,叫做“声明”。

注意:“声明”是编程者(的代码)与编译器之间的事情,一种契约机制。它不同于“语句”可以对应于可以“被CPU执行的”指令序列。所以,声明不是语句。

显然,在某些情况下,编译器践行这种契约之初,必须着手让程序为“变量”获得上述的“一席之地”,此时,编译器负责从某个范围内的空闲内存中,分配出一块空间,令“变量”之数据得被承载 —— 这块被分配空间的尺寸大小,由声明中的“类型指定符”确定(如上面代码中的“char”,它意味着一个固定的尺寸被确定下来,在你的编译环境中,可能是一个Byte)。这个分配动作,叫做“定义”。而在另一些情况下,编译器仅仅负责实行代码中的标识符与某个“一席之地”之某种关联,但并不会进行前述的那种分配工作,所以:声明并非总是定义,除非它是“定义性声明”。而又在另一些情况下,“声明”这种“说明”,并不为标识符与“变量”、函数的“一席之地”之间的关系服务,而是为标识符与某种类型服务,这种情况叫做“关于类型的声明”。

由此可见,这种针对“变量”的定义,是一种在编程者看来的最简单的内存分配动作 —— 该动作,由编程者(的代码)让编译器实现。

显然,除了“一般的变量”需要内存空间之外,作为“变量”的指针,也需要内存空间,所以,对指针的定义性声明,也是为指针本身分配了内存空间,比如以下的代码。但这和指针指向一块被分配出来的内存空间,是完全不同的两回事,我估计你是混淆了,关于这个问题更详细的讨论,后面还会有。

在这个意义上说,“为指针(它自己本身)分配内存空间”完全是编译器的事情,这个事情在编译器看到你所写的关于指针的定义性声明的时候,就已经确定了!
  1. char *pfuu;        //这是令编译器为pfuu获取一块内存空间,在你的编译环境中,这样的空间尺寸是固定的,比如是4 Bytes。

  2. pfuu = & fuu;                //这是令编译器将某种标识符fuu可以Handle起“一席之地”之“引用”机制(其中包括但不仅仅包括“地址”信息,严格说来是“指针”)赋给了pfuu。
复制代码
此外,值得注意的是:有些占据内存“一席之地”的数据,在它们的身上,并不存在用一个显明的名字(标识符)去Handle它们的内存空间的机制,即它们缺乏观念中的“变量”性质,它们是常量,如:
  1. 123 或 'A'

  2. int *p123 = &123;        //这是错误的,因为C语言杜绝了让程序去Handle这类常量的内存空间,这类常量只能是右值。
  3. 123 = 345;                //由此也可以看出,123这个常量无法成为取指针运算符&的操作数,也同样地无法成为赋值运算符的左侧操作数。

  4. char *pA = &'A';        //同上。

  5. int a123 = 123;                //相比之下,惟有这种做法,才是正确的。但注意:这是将常量的数据值,复制进了一块被“a123”所Handle起的内存空间。
  6. int *pa123 = &a123;
复制代码
不过,有一种被称为“字符串常量”的东西,却可以直接被赋予一个指针,如:
  1. char *pHK = "Hello Kitty!";
复制代码
对此,我们不会感到陌生,因为这是我们最经常使用字符串的方法之一。这样看来,字符串常量似乎自己就是一个指针。不过,我们宁可这样说:一个必须由一对双引号组成的字符串常量,在这样的赋值表达式中,被编译器视为一个指针,且编译器可以Handle到这块由字符H、e、l、l、o、…… t、t、y、!与\0所占据的内存空间,但不可以由那种之于“变量”的那种名字(标识符)来Handle变量自己的方法来Handle它,除了由字符串常量它自己本身,这类似于“匿名”效应。

合法地,字符指针pHK经由这样的初始化之后,指向了上述以字符'\0'结尾的一系列数据所占据的内存空间。

但是,你不可以(至少是不应该)修改这个空间上的任何东西,如:

  1. * (pHK + 3) = 'a';

  2. pHK[3] = 'a';
复制代码
这种试图对上述内存空间上的数据进行修改的动作,在程序运行起来后,是十分危险的,因为C语言实现对上述内存空间进行了保护,即规定了:只可读,不可写。所有对这种保护的侵犯,都将在程序运行时招致风险 —— 但在编译时,你却不知道,因为编译器对此很可能不报错。

然而,这样的做法,却是合法的:

  1. char pHK[] = "Hello Kitty!";

  2. * (pHK + 3) = 'a';

  3. pHK[3] = 'a';
复制代码
仔细对比一下,想一想为什么? —— 因为:此处的字符串常量,被作为一个字符类型的数组初始化动作下的右值(数据),其数据被复制进了一块为字符串类型数组分配的内存空间中去了,而这种内存空间与前面那种内存空间具有不同的特性:这种内存空间,若非Handle它的标识符在声明时被“const”所限制,就是既可读、又可写的。

讲到这里,一种差别,就呈现了出来,那就是:编译时 vs 运行时。

前面所讲到的所有关于内存空间的分配动作,都是在编译器编译你的程序代码的时候,可以确定的。如果在这种确定过程中出错,就是所谓的“编译时错误”。

而有些错误,仅仅在被编译好的程序被运行的时候,才会发生,是为“运行时错误”。

“运行时错误”比“编译时错误”更加危险,因为编译器和编程者(若非程序使用者)是无法在编程与编译现场知晓它的,甚至,编程者很可能不能预见到它。比如:

一个期待除数的程序,在被运行的时候,接收到一个由使用者输入的整数0作为除数。

因为,编译器永远无法预知未来,它只负责检查/保证实现这个除法的程序的本身之正确性。

另一方面,某些在编译器看来无法预知或在编译场合无法确定的事情,对于我们的程序的灵活性来说,却是十分必要的。

比如,当我们定义某个数组的时候,不论声明中的方括号里是写明一个整数,还是什么也不写,若非某些相对高级的特性参与,编译器总会在编译阶段,为这个数组规定好其规模(即在“为这个数组分配多大尺寸的内存空间”这件事情上,做出确定性的工作),这个规模在程序运行的时候,不能伸缩,直到它消亡。

那么,如果我们希望在程序运行的时候,再进行这种内存分配(比如实行这种分配的尺寸参数,是由使用者临场决定的),那么,我们就可以使用库函数malloc。

关于该函数的原型是:
  1. #include <stdlib.h>
  2. void *malloc(size_t size);
复制代码
由函数原型提供的函数声明器,我们可以知道:

该函数仅需要一个类型为size_t的参数size,经由这个参数,程序(而不是编译器)可以获悉临场分配内存的规模(即size个Bytes),若临场分配成功(或有效),则该函数返回一个指向被分配内存空间的void *类型数据(所谓“通用指针”),若失败(或无实际效果,如参数是size_t类型的0),则返回NULL或一个在将来可以藉其正确释放内存的可靠指针。

如果该函数正确地返回一个“通用指针”,可以将其赋予一个你声明好的指针(可以不必写明类型转换),有如:
  1. char *ptr = malloc( HowManySomebodyWants * sizeof(char) );
复制代码
程序(的使用者,不一定是你,但肯定不是编译器)可以利用这块尺寸为HowManySomebodyWants * sizeof(char)个Bytes的内存空间,承载某些特定的数据,完成某些特定的任务。

这种“利用”的方法,显然就是通过指针ptr来Handle起这块内存空间。

—— 这才是“为指针所指向的内存空间分配内存空间”,但显然这种说法太不合理了。正确的理解应该是:我们藉由库函数malloc这样的工具,为程序获取到了一块被分配的内存空间,某个指针可以指向这块内存空间。

当这块内存空间不再被需要的时候(比如其所承载的数据已经被运算完毕),应当将它交还到“空闲”群体中去。这种“交还”动作,仍然藉由指针ptr来实行,即用库函数free。

关于该函数的原型是:
  1. #include <stdlib.h>
  2. void free(void *ptr);
复制代码
该函数,将把这个此前由函数malloc获取到的指针ptr所指向的内存空间,交还于“空闲”,是为“释放内存”。

在编程中,应当养成良好的习惯:使用库函数free及时地将上述内存空间释放,即让malloc与free在一段工作上首尾呼应、成对出现,否则将可能导致“内存泄露”。

特别注意:

(1)指针ptr本身并不会包含关于它所指向的内存空间的规模尺寸信息。即若你用表达式 sizeof(ptr)的话,将总是仅仅得到一个“常数”,比如是整数4 —— 那是单位指针数据在内存空间中所占据的大小。那么,库函数free是如何仅仅藉着一个参数即指针ptr就可以释放一片具有规模尺寸的内存空间的呢?这个事情,自有实现来负责,目前你完全不必为此操心。

(2)库函数free之所谓“释放”工作,这里的“释放”的对象是内存空间,而不是作为它参数的指针ptr。指针无所谓“被释放”,在库函数free被called之后,作为“变量”的指针本身及其数据值,是仍然存在的(甚至是不变的),但你不可以再透过这个指针去Handle任何内存空间,包括让它再次成为库函数free的参数(即再次调用free(ptr))—— 因为此时ptr所指向的内存空间已经“归位”于“空闲”,那是一片混沌叵测的世界,其上有荒漠、有深海,其中藏有猛兽妖怪,千万不要用指针把它们Handle出来,否则它们会把你吃掉!

呵呵 —— :)



论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
28 [报告]
发表于 2013-01-20 17:22 |只看该作者
回复 27# Ager

尼玛太长了。。。不看了,眼花
   

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
29 [报告]
发表于 2013-01-20 17:49 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
30 [报告]
发表于 2013-01-20 17:52 |只看该作者
本帖最后由 Ager 于 2013-01-20 17:54 编辑
pmerofc 发表于 2013-01-20 17:49
回复 27# Ager


有个错别字
“声名不是语句。”


谢谢!{:3_193:} 已经更正了。。

呵呵,好奇怪,用拼音输入法键出了这麽多“声明”,怎么会冒出一个“声名”来 ……? 缺觉,头昏了。。。

{:3_202:}
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP