为什么创建栈的结构体类型定义数组可以有对应的2个值?

在这篇笔记开始之前,我们需要对以下概念有所了解。

1.1 操作系统中的栈和堆

注:这里所说的堆和栈与数据结构中的堆和栈不是一回事。

我们先来看看一个由C/C++/OBJC编译的程序占用内存分布的结构:

栈区(stack):由系统自动分配,一般存放函数参数值、局部变量的值等。由编译器自动创建与释放。其操作方式类似于数据结构中的栈,即后进先出、先进后出的原则。

例如:在函数中申明一个局部变量int b;系统自动在栈中为b开辟空间。

堆区(heap):一般由程序员申请并指明大小,最终也由程序员释放。如果程序员不释放,程序结束时可能会由OS回收。对于堆区的管理是采用链表式管理的,操作系统有一个记录空闲内存地址的链表,当接收到程序分配内存的申请时,操作系统就会遍历该链表,遍历到一个记录的内存地址大于申请内存的链表节点,并将该节点从该链表中删除,然后将该节点记录的内存地址分配给程序。

但是p1本身是在栈中的。

链表:是一种常见的基础数据结构,一般分为单向链表、双向链表、循环链表。以下为单向链表的结构图:

单向链表是链表中最简单的一种,它包含两个区域,一个信息域和一个指针域。信息域保存或显示关于节点的信息,指针域储存下一个节点的地址。

上述的空闲内存地址链表的信息域保存的就是空闲内存的地址。

全局区/静态区:顾名思义,全局变量和静态变量存储在这个区域。只不过初始化的全局变量和静态变量存储在一块,未初始化的全局变量和静态变量存储在一块。程序结束后由系统释放。

文字常量区:这个区域主要存储字符串常量。程序结束后由系统释放。

程序代码区:这个区域主要存放函数体的二进制代码。

下面举一个前辈写的例子:

功能:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间。

在C语言中,结构体(struct)指的是一种数据结构。结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访问。

我们来看看结构体的定义:

  • struct:结构体关键字。

以上就是简单结构体的代码示例。结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针。结构体的变量也可以是指针。

下面我们来看看结构体成员的访问。结构体成员依据结构体变量类型的不同,一般有2种访问方式,一种为直接访问,一种为间接访问。直接访问应用于普通的结构体变量,间接访问应用于指向结构体变量的指针。直接访问使用结构体变量名.成员名,间接访问使用(*结构体指针名).成员名或者使用结构体指针名->成员名。相同的成员名称依靠不同的变量前缀区分。

最后我们来看看结构体成员存储。在内存中,编译器按照成员列表顺序分别为每个结构体成员分配内存。如果想确认结构体占多少存储空间,则使用关键字sizeof,如果想得知结构体的某个特定成员在结构体的位置,则使用offsetof宏(定义于stddef.h)。


闭包就是一个函数,或者一个指向函数的指针,加上这个函数执行的非局部变量。

说的通俗一点,就是闭包允许一个函数访问声明该函数运行上下文中的变量,甚至可以访问不同运行上文中的变量。

我们用脚本语言来看一下:

通过上面的代码我们可以看出,按常规思维来说,变量str是函数funB的局部变量,作用域只在函数funB中,函数funA是无法访问到str的。但是上述代码示例中函数funA中的callback可以访问到str,这是为什么呢,因为闭包性。

我们来看看block的原型:

上面的代码声明了一个block(^)原型,名字叫做myBlock,包含一个int型的参数,返回值为NSString类型的指针。

下面来看看block的定义:

上面的代码中,将一个函数体赋值给了myBlock变量,其接收一个名为paramA的参数,返回一个NSString对象。

注意:一定不要忘记block后面的分号。

定义好block后,就可以像使用标准函数一样使用它了:

由于block数据类型的语法会降低整个代码的阅读性,所以常使用typedef来定义block类型。例如,下面的代码创建了GetPersonEducationInfo和GetPersonFamilyInfo两个新类型,这样我们就可以在下面的方法中使用更加有语义的数据类型。

我们用一张大师文章里的图来总结一下block的结构:

由于Objective-C是强制类型语言,所以作为函数参数的block也必须要指定返回值的类型,以及相关参数类型。

上文说过,block实际是Objc对闭包的实现。

上面的代码在main函数中声明了一个整型,并赋值42,另外还声明了一个block,该block会将42返回。然后将block传递给logBlock函数,该函数会显示出返回的值42。即使是在函数logBlock中执行block,而block又声明在main函数中,但是block仍然可以访问到x变量,并将这个值返回。

注意:block同样可以访问全局变量,即使是static。

对于block外的变量引用,block默认是将其复制到其数据结构中来实现访问的,如下图:

通过block进行闭包的变量是const的。也就是说不能在block中直接修改这些变量。来看看当block试着增加x的值时,会发生什么:

编译器会报错,表明在block中变量x是只读的。

有时候确实需要在block中处理变量,怎么办?别着急,我们可以用__block关键字来声明变量,这样就可以在block中修改变量了。

基于之前的代码,给x变量添加__block关键字,如下:

对于用__block修饰的外部变量引用,block是复制其引用地址来实现访问的,如下图:

我们通过大师文章中的一张图来说明:

上图这个结构是在栈中的结构,我们来看看对应的结构体定义:

从上面代码看出,Block_layout就是对block结构体的定义:

isa指针:指向表明该block类型的类。

flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。

reserved:保留变量,我的理解是表示block内部的变量数。

invoke:函数指针,指向具体的block实现的函数调用地址。

descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。

variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。

block有几种不同的类型,每种类型都有对应的类,上述中isa指针就是指向这个类。这里列出常见的三种类型:

_NSConcreteGlobalBlock:全局的静态block,不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block。例如:

  1. _NSConcreteGlobalBlock类型的block要么是空block,要么是不访问任何外部变量的block。它既不在栈中,也不在堆中,我理解为它可能在内存的全局区。
  2. _NSConcreteStackBlock类型的block有闭包行为,也就是有访问外部变量,并且该block只且只有有一次执行,因为栈中的空间是可重复使用的,所以当栈中的block执行一次之后就被清除出栈了,所以无法多次使用。
  3. _NSConcreteMallocBlock类型的block有闭包行为,并且该block需要被多次执行。当需要多次执行时,就会把该block从栈中复制到堆中,供以多次执行。

3.3 编译器如何编译

我们通过一个简单的示例来说明:

注意:如果block的创建和调用都在一个函数里面,那么优化器(optimiser)可能会对代码做优化处理,从而导致我们看不到编译器中的一些操作,所以用__attribute__((noinline))给函数runBlockA添加noinline,这样优化器就不会在doBlockA函数中对runBlockA的调用做内联优化处理。

我们来看看编译器做的工作内容:

上面的代码结合block的数据结构定义,我们能很容易得理解编译器内部对block的工作内容。

上文中提到,如果我们想要在以后继续使用某个block,就必须要对该block进行拷贝操作,即从栈空间复制到堆空间。所以拷贝操作就需要调用Block_copy()函数,block的descriptor中有一个copy()辅助函数,该函数在Block_copy()中执行,用于当block需要拷贝对象的时候,拷贝辅助函数会retain住已经拷贝的对象。

4.下面来看几个具体的运行示例:


4.3代码块的递归调用 代码块想要递归调用,代码块变量必须是全局变量或者是静态变量,这样在程序启动的时候代码块变量就初始化了,可以递归调用

4.4在代码块中使用局部变量和全局变量 在代码块中可以使用和改变全局变量

而局部变量可以使用,但是不能改变。

在代码块中改变局部变量编译不通过。怎么在代码块中改变局部变量呢?在局部变量前面加上关键字:__block

}

1 程序运行为什么需要内存

1.1 计算机程序运行的目的

计算机为什么需要编程?编程已经编了很多年,已经写了很多程序,为什么还需要另外写程序?计算机有这个新的程序到底为了什么?

程序的目的是为了去运行,程序运行是为了得到一定的结果。计算机就是用来计算的,所有的计算机程序其实都是在做计算。计算就是在计算数据。所以计算机程序中很重要的部分就是数据。

计算机程序 = 代码 + 数据。计算机程序运行完得到一个结果,就是说代码 + 数据 (经过运行后) = 结果。

从宏观上来理解,代码就是动作,就是加工数据的动作;数据就是数字,就是被代码所加工的东西。

那么可以得出结论:程序运行的目的不外乎2个:结果、过程。

用函数来类比:函数的形参就是待加工的数据(函数内还需要一些临时数据,就是局部变量),函数本体就是代码,函数的返回值就是结果,函数体的执行过程就是过程。

} // 这个函数的执行就是为了得到结果 } // 这个函数的执行重在过程(重在过程中的printf),返回值不需要 } // 这个函数又重结果又重过程

1.2 计算机程序运行过程

计算机程序的运行过程,其实就是程序中很多个函数相继运行的过程。程序是由很多个函数组成的,程序的本质就是函数,函数的本质是加工数据的动作。

1.3 冯诺依曼结构和哈佛结构

冯诺依曼结构是:数据和代码放在一起。
哈佛结构是:数据和代码分开存在。

什么是数据:全局变量、局部变量。

在S5PV210中运行的linux系统上,运行应用程序时:这时候所有的应用程序的代码和数据都在DRAM,所以这种结构就是冯诺依曼结构;在单片机中,我们把程序代码烧写到Flash(NorFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash中,必须放在RAM(SRAM)中。这种就叫哈佛结构。

DRAM是动态内存,SRAM是静态内存。

1.5 总结:为什么需要内存呢

内存是用来存储可变数据的,数据在 程序中表现为全局变量、局部变量等(在gcc中,其实常量也是存储在内存中的)(大部分单片机中,常量是存储在flash中的,也就是在代码段),对我们写程序来说非常重要,对程序运行更是本质相关。

所以内存对程序来说几乎是本质需求。越简单的程序需要越少的内存,而越庞大越复杂的程序需要更多的内存。内存管理是我们写程序时很重要的话题。我们以前学过的了解过的很多编程的关键其实都是为了内存,譬如说数据结构(数据结构是研究数据如何组织的,数据是放在内存中的)和算法(算法是为了用更优秀更有效的方法来加工数据,既然跟数据有关就离不开内存)。

1.6 深入思考:如何管理内存(无OS时,有OS时)

对于计算机来说,内存容量越大则可能性越大,所以大家都希望自己的电脑内存更大。我们写程序时如何管理内存就成了很大的问题。如果管理不善,可能会造成程序运行消耗过多的内存,这样迟早内存都被你这个程序吃光了,当没有内存可用时程序就会崩溃。所以内存对程序来说是一种资源,所以管理内存对程序来说是一个重要技术和话题。

先从操作系统角度讲:操作系统掌握所有的硬件内存,因为内存很大,所以操作系统把内存分成1个1个的页面(其实就是一块,一般是4KB),然后以页面为单位来管理。页面内用更细小的方式来以字节为单位管理。操作系统内存管理的原理非常麻烦、非常复杂、非常不人性化。那么对我们这些使用操作系统的人来说,其实不需要了解这些细节。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。譬如在C语言中使用malloc free这些接口来管理内存。

没有操作系统时:在没有操作系统(其实就是裸机程序)中,程序需要直接操作内存,编程者需要自己计算内存的使用和安排。如果编程者不小心把内存用错了,错误结果需要自己承担。

再从语言角度来讲:不同的语言提供了不同的操作内存的接口。
譬如汇编:根本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址(譬如0xd0020010),非常麻烦;
譬如C语言:C语言中编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过API(malloc free)来访问系统内存。裸机程序中需要大块的内存需要自己来定义数组等来解决。
譬如C++语言:C++语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C++语言对内存的管理比C要高级一些,容易一些。但是C++中内存的管理还是靠程序员自己来做。如果程序员new了一个对象,但是用完了忘记delete就会造成这个对象占用的内存不能释放,这就是内存泄漏。
Java/C#等语言:这些语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完成后忘记释放,则虚拟机会帮我释放掉这些内存。听起来似乎C# java等语言比C/C++有优势,但是其实他这个虚拟机回收内存是需要付出一定代价的,所以说语言没有好坏,只有适应不适应。当我们程序对性能非常在乎的时候(譬如操作系统内核)就会用C/C++语言;当我们对开发程序的速度非常在乎的时候,就会用Java/C#等语言。

2 位、字节、半字、字的概念和内存位宽

2.1 什么是内存?(硬件和逻辑两个角度)

从硬件角度:内存实际上是电脑的一个配件(一般叫内存条)。根据不同的硬件实现原理还可以把内存分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后来的DDR1、DDR2·····、LPDDR)。

从逻辑角度:内存是这样一种东西,它可以随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址)、并且可以读写(当然了逻辑上也可以限制其为只读或者只写);内存在编程中天然是用来存放变量的(就是因为有了内存,所以C语言才能定义变量,语言中的一个变量实际就对应内存中的一个单元)。

2.2 内存的逻辑抽象图(内存的编程模型)

从逻辑角度来讲,内存实际上是由无限多个内存单元格组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和这个内存单元格唯一对应且永久绑定。

以大楼来类比内存是最合适的。逻辑上的内存就好象是一栋无限大的大楼,内存的单元格就好象大楼中的一个个小房间。每个内存单元格的地址就好象每个小房间的房间号。内存中存储的内容就好象住在房间中的人一样。

逻辑上来说,内存可以有无限大(因为数学上编号永远可以增加,无尽头)。但是现实中实际的内存大小是有限制的,譬如32位的系统(32位系统指的是32位数据线,但是一般地址线也是32位,这个地址线32位决定了内存地址只能有32位二进制,所以逻辑上的大小为2的32次方)内存限制就为4G。实际上32位的系统中可用的内存是小于等于4G的(譬如我32位CPU装32位windows,但实际电脑只有512M内存)。

内存单元的大小单位有4个:位(1bit) 字节(8bit) 半字(一般是16bit) 字(一般是32bit)。

在所有的计算机、所有的机器中(不管是32位系统还是16位系统还是以后的64位系统),位永远都是1bit,字节永远都是8bit。

历史上曾经出现过16位系统、32位系统、64位系统三种,而且操作系统还有windows、linux、iOS等很多,所以很多的概念在历史上曾经被混乱的定义过。

建议大家对字、半字、双字这些概念不要详细区分,只要知道这些单位具体有多少位是依赖于平台的。实际工作中在每种平台上先去搞清楚这个平台的定义(字是多少位,半字永远是字的一半,双字永远是字的2倍大小)。

编程时一般根本用不到字这个概念,那我们区分这个概念主要是因为有些文档中会用到这些概念,如果不加区别可能会造成你对程序的误解。

2.5 内存位宽(硬件和逻辑两个角度)

从硬件角度讲:硬件内存的实现本身是有宽度的,也就是说有些内存条就是8位的,而有些就是16位的。那么需要强调的是内存芯片之间是可以并联的,通过并联后即使8位的内存芯片也可以做出来16位或32位的硬件内存。

从逻辑角度讲:内存位宽在逻辑上是任意的,甚至逻辑上存在内存位宽是24位的内存(但是实际上这种硬件是买不到的,也没有实际意义)。从逻辑角度来讲不管内存位宽是多少,我就直接操作即可,对我的操作不构成影响。但是因为你的操作不是纯逻辑而是需要硬件去执行的,所以不能为所欲为,所以我们实际的很多操作都是受限于硬件的特性的。譬如24位的内存逻辑上和32位的内存没有任何区别,但实际硬件都是32位的,都要按照32位硬件的特性和限制来干活。

3 内存编址和寻址、内存对齐

内存在逻辑上就是一个一个的格子,这些格子可以用来装东西(里面装的东西就是内存中存储的数),每个格子有一个编号,这个编号就是内存地址,这个内存地址(一个数字)和这个格子的空间(实质是一个空间)是一一对应且永久绑定的。这就是内存的编址方法。

在程序运行时,计算机中CPU实际只认识内存地址,而不关心这个地址所代表的空间在哪里,怎么分布这些实体问题。因为硬件设计保证了按照这个地址就一定能找到这个格子,所以说内存单元的2个概念:地址和空间是内存单元的两个方面。

3.2 关键:内存编址是以字节为单位的

我随便给一个数字(譬如说7),然后说这个数字是一个内存地址,然后问你这个内存地址对应的空间多大?这个大小是固定式,就是一个字节(8bit)。

如果把内存比喻位一栋大楼,那么这个楼里面的一个一个房间就是一个一个内存格子,这个格子的大小是固定的8bit,就好象这个大楼里面所有的房间户型是一样的。

3.3 内存和数据类型的关系

int 整形(整数类型,这个整就体现在它和CPU本身的数据位宽是一样的)譬如32位的CPU,整形就是32位,int就是32位。

数据类型和内存的关系就在于:
数据类型是用来定义变量的,而这些变量需要存储、运算在内存中。所以数据类型必须和内存相匹配才能获得最好的性能,否则可能不工作或者效率低下。

在32位系统中定义变量最好用int,因为这样效率高。原因就在于32位的系统本身配合内存等也是32位,这样的硬件配置天生适合定义32位的int类型变量,效率最高。也能定义8位的char类型变量或者16位的short类型变量,但是实际上访问效率不高。

在很多32位环境下,我们实际定义bool类型变量(实际只需要1个bit就够了)都是用int来实现bool的。也就是说我们定义一个bool b1;时,编译器实际帮我们分配了32位的内存来存储这个bool变量b1。编译器这么做实际上浪费了31位的内存,但是好处是效率高。

问题:实际编程时要以省内存为大还是要以运行效率为重?答案是不定的,看具体情况。很多年前内存很贵机器上内存都很少,那时候写代码以省内存为主。现在随着半导体技术的发展内存变得很便宜了,现在的机器都是高配,不在乎省一点内存,而效率和用户体验变成了关键。所以现在写程序大部分都是以效率为重。

我们在C中int a;定义一个int类型变量,在内存中就必须分配4个字节来存储这个a。有这么2种不同内存分配思路和策略:

内存的对齐访问不是逻辑的问题,是硬件的问题。从硬件角度来说,32位的内存它 0 1 2 3四个单元本身逻辑上就有相关性,这4个字节组合起来当作一个int硬件上就是合适的,效率就高。

对齐访问很配合硬件,所以效率很高;非对齐访问因为和硬件本身不搭配,所以效率不高。(因为兼容性的问题,一般硬件也都提供非对齐访问,但是效率要低很多。)

4 C语言如何操作内存

4.1 C语言对内存地址的封装(用变量名来访问内存、数据类型的含义、函数名的含义)

结合内存来解析C语言语句的本质:
int a; // 编译器帮我们申请了1个int类型的内存格子(长度是4字节,地址是确定的,但是只有编译器知道,我们是不知道的,也不需要知道。),并且把符号a和这个格子绑定。

a = 5; // 编译器发现我们要给a赋值,就会把这个值5丢到符号a绑定的那个内存格子中。

a += 4; // 编译器发现我们要给a加值,a += 4 等效于 a = a + 4;编译器会先把a原来的值读出来,然后给这个值加4,再把加之后的和写入a里面去。

C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。

数据类型决定长度的含义:我们一个内存地址(0x),本来这个地址只代表1个字节的长度,但是实际上我们可以通过给他一个类型(int),让他有了长度(4),这样这个代表内存地址的数字(0x)就能表示从这个数字(0x)开头的连续的n(4)个字节的内存格子了(0x + 0x + 0x +

数据类型决定解析方法的含义:譬如我有一个内存地址(0x),我们可以通过给这个内存地址不同的类型来指定这个内存单元格子中二进制数的解析方法。譬如我 (int)0x,含义就是(0x + 0x + 0x + 0x)这4个字节连起来共同存储的是一个int型数据;那么我(float)0x,含义就是(0x + 0x + 0x + 0x)这4个字节连起来共同存储的是一个float型数据;

C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。

4.2 用指针来间接访问内存

关于类型(不管是普通变量类型int float等,还是指针类型int * float *等),只要记住:
类型只是对后面数字或者符号(代表的是内存地址)所表征的内存的一种长度规定和解析方法规定而已。

C语言中的指针,全名叫指针变量,指针变量其实很普通变量没有任何区别。譬如int a和int *p其实没有任何区别,a和p都代表一个内存地址(譬如是0x),但是这个内存地址(0x)的长度和解析方法不同。a是int型所以a的长度是4字节,解析方法是按照int的规定来的;p是int *类型,所以长度是4字节,解析方法是int *的规定来的(0x开头的连续4字节中存储了1个地址,这个地址所代表的内存单元中存放的是一个int类型的数)。

4.3 用数组来管理内存

数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。

int a; // 编译器分配4字节长度给a,并且把首地址和符号a绑定起来。
int b[10]; // 编译器分配40个字节长度给b,并且把首元素首地址和符号b绑定起来。

数组中第一个元素(a[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址就称为首地址;首元素a[0]的首地址就称为首元素首地址。

5.1 数据结构这门学问的意义

数据结构就是研究数据如何组织(在内存中排布),如何加工的学问。

5.2 最简单的数据结构:数组

为什么要有数组?因为程序中有好多个类型相同、意义相关的变量需要管理,这时候如果用单独的变量来做程序看起来比较乱,用数组来管理会更好管理。

5.3 数组的优势和缺陷

优势:数组比较简单,访问用下标,可以随机访问。
缺陷:1 数组中所有元素类型必须相同;2 数组大小必须定义时给出,而且一旦确定不能再改。

5.4 结构体隆重登场

结构体发明出来就是为了解决数组的第一个缺陷:数组中所有元素类型必须相同。

分析总结:在这个示例中,数组要比结构体好。但是不能得出结论说数组就比结构体好,在包中元素类型不同时就只能用结构体而不能用数组了。

5.5 题外话:结构体内嵌指针实现面向对象

总的来说:C语言是面向过程的,但是C语言写出的linux系统是面向对象的。
非面向对象的语言,不一定不能实现面向对象的代码。只是说用面向对象的语言来实现面向对象要更加简单一些、直观一些、无脑一些。

用C++、Java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情;但是用C来实现面向对象很麻烦,看起来也不容易理解,这就是为什么大多数人学过C语言却看不懂linux内核代码的原因。

使用这样的结构体就可以实现面向对象。
这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。

6 内存管理之栈(stack)

栈是一种数据结构,C语言中使用栈来保存局部变量。栈是被发明出来管理内存的。

6.2 栈管理内存的特点(小内存、自动化)

栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的必须后出来。

队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。

6.3 栈的应用举例:局部变量

C语言中的局部变量是用栈来实现的。

我们在C中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。

注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。

然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人写代码干预。

栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。

分析一个细节:C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?
定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的。如果你显式初始化怎么样?
C语言是通过一个小手段来实现局部变量的初始化的。
C语言编译器会自动把这行转成:

6.4 栈的约束(预定栈大小不灵活,怕溢出)

首先,栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存(这个缺点有点像数组)。

其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时 int a[10000]; 使用递归来解决问题时一定要注意递归收敛)

}

山东青年政治学院作为山东专升本招生院校之一,有没有同学心动呢?2022年山东青年政治学院专升本自荐考生专业综合能力测试要求有哪些?山东青年政治学院是一所公办本科院校,如果有自荐考生对该院校感兴趣,可以先来了解一下院校发布的2022年山东青年政治学院专升本自荐考生专业综合能力测试《计算机科学与技术》考试大纲,该大纲中对报考《计算机科学与技术》专业的自荐考生考试要求,考试形式,考试题型等都有说明。提炼大纲中的重要信息,可以对该院校的该专业专升本情况有更清晰的认识。

4.计算机科学与技术(080901)

4.1《C语言程序设计》考试要求

本科目考试要求考生掌握必要的基本概念、基本理论、较熟练的运算能力。主要考查学生识记、理解和应用能力,为进一步学习奠定基础。具体内容与要求如下:

1.程序的构成,main函数和其他函数;

2.头文件,数据说明,函数的开始和结束标志。

二、数据类型、运算符与表达式

1.C的数据类型(常量与变量,整型,实型,字符类型,指针类型)及其定义方法;

2.C运算符的种类、运算优先级和结合性;

3.不同类型数据间的转换与运算;

4.C表达式类型(赋值表达式、算术表达式、关系表达式、逻辑表达式、条件表达式、逗号表达式)和求值规则;

5.数据的输入和输出,输入输出函数的使用。

(一)选择结构程序设计

1.用if语句实现选择结构;

2.用switch语句实现多分支选择结构;

(二)循环结构程序设计

1.一维数组、二维数组的定义、初始化和引用方法;

2.一维数组的应用(如排序),二维数组的应用(如矩阵运算);

3.字符数组的定义、初始化和输入输出方法;

4.字符串与字符串处理函数的使用;

5.用二维数组处理多个字符串。

2.函数的类型和返回值;

3.形式参数与实在参数的两种传递方式;

4.函数的正确调用方法;

5.局部变量和全局变量;

6.变量的存储类别(自动、静态、外部),变量的作用域和生存期。

1.指针与指针变量的概念,指针与地址运算符;

2.指向变量、数组、字符串、函数的指针变量;

3.通过指针引用以上各类型数据;

4.用指针作函数参数。

1.结构体类型数据的定义方法和引用方法;

2.结构体嵌套和用指针引用结构体成员;

3.结构体数组的应用。

1.C语言中的文件类型;

2.文件类型指针(FILE类型指针);

5.文件状态检测(feof函数)。

考试采用闭卷、线上考试形式。试卷满分100分,考试时间60分钟。

考试题型从以下类型中选择:选择题、判断题、程序设计题、修改程序题、写程序结果题。

5.2《数据结构》考试要求

本科目考试内容包括各种数据组织中的数据逻辑结构、存储结构以及有关操作的算法,内容涉及线性结构、树型结构、状结构、查找和排序。考查要求可划分为“了解”、“理解”和“掌握”三个层次,旨在考查考生对各类数据结构进行运用的熟练程度、考生的计算思维以及考生运用和设计算法解决现实应用问题的能力。具体内容与要求如下:

一、基本概念与算法分析基础

(一)了解数据、数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、数据类型和抽象数据类型的基本概念。掌握数据逻辑结构和数据存储结构的分类。

(二)了解算法定义、性质、设计策略以及评价标准,理解算法与程序的区别。

(三)理解问题规模、语句频度、时间复杂性、空间复杂性的概念。掌握对非递归算法进行时间复杂性和空间复杂性分析的方法。

(一)理解线性表的概念、特点和抽象数据类型定义。

(二)掌握顺序表的实现方式、性质以及各种基本运算(取值、插入、删除、查找)。掌握单链表的实现方式、性质以及各种基本运算(取值、插入、删除、查找、创建)。理解单链表的变形(循环单链表、双向链表)以及基本运算(插入、删除)。理解顺序表与单链表在时空性能方面的差别。

(三)理解栈的概念以及抽象数据类型定义。掌握栈的两种存储结构实现以及各种基本运算(元素入栈、元素出栈、取栈顶元素)。了解栈的现实应用。

(四)理解队列的概念以及抽象数据类型定义。掌握队列的两种存储结构实现以及各种基本运算(元素入队、元素出队、取队头元素),理解标准顺序队列与循环队列之间的差别,掌握循环队列基本运算(求队列长度、元素入队、元素出队、取队头元素)。了解队列的现实应用。理解栈与队列在操作和应用方面的差别。

(五)了解数组的抽象数据类型定义。掌握数组的顺序存储结构以及该结构下的地址计算方法。了解特殊矩阵、稀疏矩阵的压缩存储方法。

(六)理解字符串的概念、基本操作(串赋值、串比较、求串长、串联接、求子串)以及抽象数据类型定义。了解字符串的存储结构。理解字符串模式匹配的BF(Brute-Force)算法。

(七)理解广义表的相关概念(广义表、广义表长度、表头、表尾),掌握广义表的基本操作(取表头、取表尾),了解广义表的存储结构。

(一)理解树的定义以及相关概念(结点、度、叶子、非终端结点、双亲、孩子、兄弟、祖先、子孙、层次、堂兄弟、深度、有序树、无序树、森林)以及树的抽象数据类型定义。

(二)掌握二叉树的定义、性质、各种存储结构和遍历算法(前序遍历、中序遍历、后序遍历和层次遍历)。了解线索二叉树的概念、分类、存储结构及线索化算法。

(三)掌握树的三种存储结构(双亲表示法、孩子表示法、孩子兄弟表示法)以及树、森林与二叉树间的相互转换方法。理解树和森林的遍历算法。

(四)掌握哈夫曼树的定义以及相关概念(路径、路径长度、树的路径长度、权、结点的带权路径长度、树的带权路径长度),理解哈夫曼编码的基本思想,掌握哈夫曼树的构造方法以及哈夫曼编码方法。

(一)理解的基本概念(有向、无向、子、有向完全、无向完全、稀疏、稠密、权、网、邻接点、度、入度、出度、路径、路径长度、回路、环、简单路径、连通、连通分量、强连通、强连通分量、连通的生成树)。掌握的邻接矩阵和邻接表存储结构,理解这两种存储结构的优缺点。

(二)理解的两种遍历的基本思想,掌握的两种遍历算法。

(三)掌握最小生成树的概念以及求的最小生成树的算法(Kruskal和Prim算法)。

(四)掌握求的单源最短路径问题算法(Dijkstra算法)以及所有顶点间最短路径问题算法(Floyd算法)。

(五)理解顶点表示活动网络(AOV网)的概念,掌握求拓扑排序的算法。

(六)理解边表示活动网络(AOE网)的概念,掌握求关键路径的算法。

(一)理解查找相关概念(查找表、关键字、动态查找表、静态查找表)及基于平均查找长度的效率评价方法。

(二)理解散列查找的基本思想和冲突的概念。了解散列函数的构造方法以及冲突处理方法。

(三)掌握顺序查找算法、折半查找算法,理解分块查找算法。

(四)了解二叉排序树、平衡二叉树、B-树和B+树的概念。

(一)掌握典型的插入排序算法(直接插入排序、希尔排序)。

(二)掌握典型的交换排序算法(起泡排序、快速排序)。

(三)了解典型选择排序算法的基本思想(简单选择排序、锦标赛排序、堆排序)。

(四)了解归并排序和基数排序的基本思想。

考试采用闭卷、线上考试形式。试卷满分100分,考试时间60分钟。

考试题型从以下类型中选择:单项选择题、判断题、辨析题、简答题、操作题、综合应用题、算法设计题。

以上就是2022年山东青年政治学院专升本自荐考生专业综合能力测试《计算机科学与技术》考试大纲的全部介绍了,有疑问的同学请在下方留言咨询。建议可以将大纲收藏起来,方便之后需要的时候能够快速找到。

  另外,如果是想要了解更多专升本资讯,可以通过进入山东时代学历网查看,关于专升本培训,如果是您在学习上感到吃力,想要尝试系统的教学方式,可以在下方对话框填写方式,老师会尽快回复到您,为您提供一些学习上的建议。

}

我要回帖

更多关于 栈和队列的抽象数据类型 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信