考研复试 算法设计&数据结构

算法的基本概念和特性
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。
算法的五个特性:

  1. 输入: 算法具有0个或多个输入
  2. 输出: 算法至少有1个或多个输出
  3. 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
  4. 确定性:算法中的每一步都有确定的含义,不会出现二义性
  5. 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成

算法时间和空间复杂性的概念
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
时间复杂度是一个函数,它定量描述了该算法的运行时间。

有哪些排序算法
排序算法:直接插入排序、冒泡排序、选择排序、快速排序、基数排序、堆排序
快速排序的时间复杂度,说明复杂度是怎么来的?
快速排序的时间主要耗费在划分操作上,对长度为n的区间进行划分,共需n-1次关键字的比较,时间复杂度为O(n)。
对n个元素进行快速排序的过程构成一棵递归树,在这样的递归树中,每一层最多对n个元素进行划分,所花的时间为O(n)。当初始排序数据随机分布,使每次分成的两个子区间中的元素个数大致相等时,递归树高度为log2n,快速排序呈现最好情况,即最好情况下的时间复杂度为O(nlog2n)。快速排序算法的平均时间复杂度也是O(nlog2n)。所以快速排序是一种高效的算法。
基本思想基于分治法,每一趟选择当前所有子序列中的一个关键字(通常第一个)作为枢轴,将子序列中比枢轴小的移到枢轴前边,比枢轴大的移到枢轴后边,通过一趟排序将待排序表划分为独立的两部分,然后分别递归地对两个字表重述上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
有哪些排序算法,有什么优劣

  • 快速排序是冒泡的改进版,是目前已知的最快的排序算法
    优点:极快,数据移动少
    缺点:不稳定
  • 缩小增量排序,又称希尔排序
    优点:块,数据移动少
    缺点:不稳定,d的取值是多少,应取多少个不同的值,都无法确切的指导,只能凭借经验来取
  • 插入排序
    优点:稳定,快;
    缺点:比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量 k 庞大的时候,但用链表可以解决 这个问题。
  • 选择排序
    每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的 数列的最后,直到全部待排序的数 据元素排完。
    优点:移动数据的次数已知(n-1 次);
    缺点:比较次数多。
  • 冒泡排序
    优点:稳定;
    缺点:慢,每次只能移动相邻两个数据
排序算法平均时间复杂度最坏最好空间复杂度稳定性复杂性
插入排序O(n^2)O(n^2)O(n)O(1)稳定
希尔排序O(n^1.3)不稳定
冒泡排序O(n^2)O(n^2)O(n)O(1)稳定
快速排序O(nlog2n)O(n^2)O(nlog2nO(log2n)不稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
堆排序O(nlog2n)O(nlog2n)O(nlog2nO(1)不稳定
归并排序O(nlog2n)O(nlog2n)O(nlog2nO(n)稳定
基数排序O(d(n+r))O(d(n+r))O(d(n+r))稳定

排序算法的应用

  • 选取排序方法需要考虑的因素
    ①待排序的元素数目n。
    ②元素本身信息量的大小。
    ③关键字的结构及其分布情况。.
    ④稳定性的要求。
    ⑤语言工具的条件,存储结构及辅助空间的大小等。
  • 若n较小,可直接插入排序或简单选择排序。由于直接插入排序所需的移动次数较简单选择排序的多,因而当记录本身信息量较大时,用简单选择排序较好
  • 若已基本有序,则选直接插入或冒泡排序为宜
  • 若n较大,则应采用时间复杂度为O(nlog2n):快速排序、堆排序、归并排序
    • 快速排序被认为是目前基于比较的内部排序方法中最好的方法,当待排序的关键字随机分布时,快速排序的平均时间最短。
    • 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,这两种排序都是不稳定的。
    • 若要求排序稳定且时间复杂度为O(nlog2n), 则可选用归并排序。但本章介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后两两归并。直接插入排序是稳定的,因此改进后的归并排序仍是稳定的。
  • 在基于比较的排序方法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的n个关键字随
    机分布时,任何借助于“比较”的排序算法,至少需要O(nlog2n)的时间。
  • 若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
  • 当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构。

对全国人民的年龄进行排序应该用什么算法
基数排序,因为基数排序最适用于基数很大但关键字较小的序列。
采用多关键字排序的思想,借助“分配”和“收集”两种操作对单逻辑关键进行排序的方法
快速排序最佳,其所需时间是最省,但快速排序在最坏的情况下的时间性能不如堆排序和归并排序。
当空间大资源足,要求时间效率时,可采用归并排序。
堆排序虽然较之快排慢一些,但特别适合海量数据的排序。如在100万个数据里面找出前1000大的数据。可以用建立一个小顶堆存储1000元素。
当序列中的记录基本有序或n值较小,插入排序是最佳的排序方法。

如何在100W个,或者更多的数据里面找到前50个;
堆排序(数据较大用log2n复杂度的堆排序的速度最快)

希尔排序
缩小增量排序,将待排序列按某种规则分成几个子序列,分别对这几个子序列进行直接插入排序
时间复杂度与增量选取有关,当n在某个特定范围时,希尔排序的实践复杂度约为O(n^1.3)
在最坏情况下时间复杂度为O(n^2)
当相同关键字的记录被划分到不同的子表时,可能会改变相对次序,不稳定。
仅适用于线性表为顺序存储。

基数排序
采用多关键字排序的思想,借助“分配”和“收集”两种操作对单逻辑关键进行排序的方法。稳定。按位排序时必须稳定
在这里插入图片描述
在这里插入图片描述
C语言可以写多少个程序,为什么?
如果不考虑现实物理意义上的存储空间,则为可数无限个,和自然数集的基数是等势的;否则是有限的。
原因:我们设长度为n的C语言程序(包括头文件等)的个数为f(n)。那么显然,对于任意的自然数n,f(n)是有限的,因为它最大为x的n次方(x为合法的C语言字符集基数)。对于其中能够编译通过的每个程序,从位置0,1,2,…开始排起。这样对于n=0,1,2…,都按上述次序排起,也就是第一个由n+1个字符所组成的可以编译通过的程序恰排在最后一个由n个字符所组成的可以编译通过的程序之后。如此便将C语言可以写的程序数与自然数集建立一一对应,故答案是可数无限个。但是在现实世界中存储空间不能无限大,一个C语言程序一定是由有限个字符所组成的,从这个角度来看,上述讨论总会排到终点,故答案是有限个。

语言本质是一个工具,工具能做多少事情关键还在于使用者。

面向对象和面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用时一个一个依次调用。
面向对象是把构成问题事物分解成各个对象。建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在解决整个问题的步骤中的行为。

什么是平衡二叉树
平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树(有别于 AVL 算 法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树,同时,平衡二叉树必定是二叉搜索树,反 之则不一定。
红黑树(red black tree) 是一种自平衡二叉查找树,是以最后陪你过数据结构,典型用途是实现关联数组。一种特化的AVL树(平衡二叉树),都是在进行插入和删除时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。可以在O(logn)时间内做查找,插入和删除(n是树中元素的数目)。
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

面向对象的对象是什么?对象和对象之间怎么作用?
面向对象就是:把数据及对数据的操作方法放在一起,作为一个相互依存的整体——对象。
对同类对象抽象出其共性,形成类。类中大多数数据,只能用本类的方法进行处理。类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。程序流程由用户在使用中决定。
对象即人为对各种具体物体抽象后的一个概念,人们每天都要接触各种各样的对象,如手机就是一个对象。静态地调用对方的接口(函数)。

通常设计一个好的算法应考虑
正确性:算法应当能够正确地解决求解问题。
可读性:算法应当具有良好的可读性,以助于人们理解。
健壮性:当输入非法数据时,算法也能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。

C语言问题 局部变量能不能和全局变量重名
可以,局部会屏蔽全局,要用全局变量,需要用"::"
在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量,对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。

动态规划程序设计Dynamic Programming,DP通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式例如二分查找树,背包问题、最短路径、资源分配。

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。
  • 步骤
    -1. 描述优解的结构特征
    -2. 递归定义一个最优解的值
    -3. 自底向上计算一个最优解的值
    -4. 从已计算的信息中构造一个最优解

  • 基本概念
    动态规划过程每次决策依赖于当前状态,又随即引起状态转移。一个决策序列就是在变化的状态中产生出来的,所以,多阶段最优化决策解决问题的过程就称为动态规划

  • 基本思想
    基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

    由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

    与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

数组 链表的区别和优劣

  • 主要区别
    • 数组的元素个数是固定的而组成链表的结点个数可按需要增减;
    • 数组元素的存诸单元在数组定义时分配,链表结点的存储单元在程序执行时动态向系统申请:
    • 数组中的元素顺序关系由元素在数组中的位置(即下标)确定,链表中的结点顺序关系由结点所包含的指针来体现。
    • 对于不是固定长度的列表,用可能最大长度的数组来描述,会浪费许多内存空间
    • 对于元素的插人、删除操作非常频繁的列表处理场合,用数组表示列表也是不适宜的。若用链表实现,会使程序结构清晰,处理的方法也较为简便。
  • 数组
    优点:使用方便,查询效率比链表高,内存为一连续的区域
    缺点:大小固定,不适合动态存储,不方便动态添加
  • 链表
    优点:插入删除速度快;内存利用率高;大小不固定,拓展灵活
    缺点:不能随机查找,必须从第一个开始遍历,查找效率低

P 和 NP 问题
计算机科学领域的最大难题,关系到计算机完成一项任务的速度到底有多快
P对NP问题“P/NP问题”,这里的P指多项式时间(Polynomial),一个复杂问题如果能在多项式时间内解决,那么它便被称为P问题,这意味着计算机可以在有限时间内完成计算;NP指非确定性多项式时间(nondeterministic polynomial),一个复杂问题不能确定在多项式时间内解决,假如NP问题能找到算法使其在多项式时间内解决,也就是证得了P=NP。
NPC 问题-首先,它得是一个 NP 问题;然后,所有的 NP 问题都可以约化到 它。 NP-Hard 问题-所有的 NP 问题都可以约化到它,不一定是 NP 问题

P问题所有能用多项式时间算法计算得到结果的问题,称为多项式问题,也就是P(polynomial)
NP类问题(Non-Deterministic Polynomial Problems):NP问题是指存在多项式算法能够验证的非决定性问题
NP概念的奥妙在于,它躲开了求解到底需要多少时间这样的问题,而仅仅只是强调验证需要多少时间
NP完全(NP Complete)问题,也叫做NPC问题:一个属于NP类的问题,但目前为止没办法在P时间内解决(也就是说目前只能在P时间内判断解的正确性

栈是怎么作用的
定义:一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素

  1. 保存现场/上下文
  2. 传递参数:汇编代码调用c函数时,需传递参数
  3. 保存临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量

Heap和stack的概念和区别
内存分配策略
程序运行时的内存分配有三种策略:静态、栈式、堆式

静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构 (比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.

栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在**编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配**,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.

区别

  1. heap 是堆,stack 是栈。

  2. stack 的空间由操作系统自动分配和释放,heap 的空间是手动申请 和释放的,heap 常用 new 关键字来分配。

  3. stack 空间有限,heap 的空间是很大的自由区。 在 Java 中,若只是声明一个对象,则先在栈内存中为其分配地址空间,若再 new 一下,实例 化它,则在堆内存中为其分配地址。

  4. 从堆和栈的功能和作用,堆主要用来存放对象的,栈主要是用来执行程序的

  5. 举例:数据类型 变量名;这样定义的东西在栈区。如: Object a =null; 只在栈内存中分配空间 new 数据类型();或者 malloc(长度); 这样定义的东西就在 堆区如:Object b =new Object(); 则在堆内存中分配空间

     https://wenku.baidu.com/view/418d150d4a7302768e99397f.html
     一个由C/C++编译的程序占用的内存分为以下几个部分:
     1、栈区(stack) :由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似
     于数据结构中的栈。
     2、堆区(heap) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与
     数据结构中的堆是两回事,其分配方式倒是类似于链表。
     3、全局区(静态区static) :全变量和静态变量的存储是放在一块的, 初始化的全局变量和静态
     变量在--块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系
     统释放。
     4、文字常量区:常量字符串就是放在这里的。程序结束后由系统释放。
     5、程序代码区:存放函数体的二进制代码。
    

算法的确定性是什么?与确定性相对应的算法是什么
算法中每一条指令必须由确切的含义,读者理解时不会产生二义性。即对于相同的输入只能得出相同的输出。
相对应的是随机算法

确定性算法是利用问题的解析性质,产生一确定的有限或无限点序列使其收敛于全局最优解。这类方法依据某一确定性策略搜索局部极小,并试图跳跃已获得的局部极小而达到某个全局最优点,能充分利用问题的解析性质,从而计算效率高。如填充函数法、打洞函数法、D.C.规划算法、区间法、单调规划、分支定界方法和积分水平集方法等,这些算法的构造都涉及到已知目标函数的某些局部性质或者全局性质。
随机算法采用了一定程度的随机性作为其逻辑的一部分。该算法通常使用均匀随机位作为辅助输入来指导自己的行为,超过随机位的所有可能的选择实现了“平均情况下的”良好业绩的希望。从形式上看,该算法的性能将会是一个随机变量,由随机位决定;因此无论是运行时间,或输出(或两者)是随机变量。

一段代码填空

C语言程序题

问程序输出结果
char a[]=”a//gd/sjr/a”;

strlen(a) 12
sizeof(a) 121=12 字节长度乘以字符串长度
strlen(a+3) 9
1=9
strlen 计算字符串的长度,以\0’为字符串结束标记。
sizeof 计算的则是分配的数组所占的内存空间的大小,不受里面存储的内容影响
char 占一个字节,int 占 4 个字节, double 占 8 个字节

不递归的方法实现广度优先和深度优先

  • 广度优先

    1. 顶点v入队列
    2. 当队列非空时则继续执行,否则算法结束
    3. 当队列取得队头顶点v;访问顶点v并标记顶点v已经被访问
    4. 查找顶点v的第一个邻接顶点col.
    5. 若v的邻接顶点col未被访问过,则col入队列
    6. 继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)
      直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)
  • 深度优先
    使用一个栈S来记忆下一步可能访问的顶点,同时是用了一个访问标记数组visited[i]来记忆第i个顶点是否在栈内或曾经在栈内,若是则它以后不能再进栈。
    在这里插入图片描述

      广度优先搜索(Breadth-First-Search, BFS)类似于二叉树的`层序遍历`算法。基本思想是:首先
      访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1, w2,..., wi然后依次访
      问w1, w2,..., wi的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访
      问过的邻接顶点,直至图中所有顶点都被访问过为止。若此时图中尚有顶点未被访问,则另选图中
      个未曾被访问的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止。
      
      深度优先搜索(Depth-First-Search, DFS) 类似于树的先序遍历。如其名称中所暗含的意思一样, 
      这种搜索算法所遵循的搜索策略是尽可能“深”地搜索一个图。
      它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访
      问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点w2.....重复上述过程。当不能再继续
      向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续
      上述搜索过程,直至图中所有顶点均被访问过为止。
    

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

迷宫算法(递归回溯法)
即深度优先搜索算法,迷宫的搜索者从起点开始将一只手扶着墙面前行,总能保证不会迷 失并且找到迷宫中存在的出口。如果墙是相互连接的,那么他们可以被转换成一个回路或 者环。那么摸着墙走就可以被看作是从一个圆环的起点进行至其终点。
回溯法
对一个包括有很多个结点,每个结点有若干个搜索分支的问题,把原问题分解为若干个子问题求解的算法;当搜索到某个结点发现无法再继续搜索下去时,就让搜索过程回溯(回退)到该节点的前一个结点,继续搜索该节点外的其他尚未搜索的分支;如果发现该结点无法再搜索下去,就让搜索过程回溯到这个结点的前一结点继续这样的搜索过程;这样的搜索过程一致进行到搜索到问题的解或者搜索完了全部可搜索分子没有解存在为止。

typedef vs #define
1.原理不同
#define是C语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。
typedef是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已经存在的类型一个别名,但不能在一个函数定义里面使用typedef。
2.功能不同
typedef用来定义类型的别名,起到类型易于记忆的功能。另一个功能是定义机器无关的类型。
#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
3.作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,
而typedef有自己的作用域。
4.对指针的操作不同

#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1, p2;
INTPTR2 p3, p4;
含义分别为,
声明一个指针变量p1和一个整型变量p2
声明两个指针变量p3、p4

#define是C语言中定义的语法,是预处理指令
#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

getchar() & putchar() 函数
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内 只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。

gets() & puts() 函数
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

全局变量和局部变量的区别如下:

  1. 作用域不同:全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等
  2. 内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区
  3. 生命期不同:全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了
  4. 使用方式不同:全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。函数内部会优先使用局部变量再使用全局变量。

private -只有当前的类才能访问该字段或方法。
protected -仅此类的当前类和子类(有时 还包括相同包装的类)将有权访问该字段或方法。
public -任何类都可以引用该字段或调用 该方法。假定这些关键字被用作类定义中字段或方法声明的一部分。

new 和 malloc 的区别
1.属性
new/delete 是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持
2.参数
使用new操作符申请内存分配时无需指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显示地指出所需内存的尺寸
3.返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型
4.自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
5.重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
6. 内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

解释C++中静态函数和静态变量
在这里插入图片描述
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态成员不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的静态成员是类的所有对象中共享的成员,而不是某个对象的成员。使用静态数据成员可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率
1、静态变量的定义:静态数据成员定义或说明时在前面加关键词static:
2、静态变量初始化:与一般成员初始化不同<数据类型><类名>::<静态数据成员名>=<值>
(1) 初始化在类体外进行,而前面不加static,(这点需要注意)以免与一般静态变量或对象相混淆。
(2) 初始化时不加该成员的访问权限控制符private,public等。
(3) 初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员。
3、静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化。
4、静态方法不能直接调用一般成员,可以通过对象引用实现调用
7. 把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符::就可以访问。
8. 静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。

  • 成员函数:指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
    在这里插入图片描述
    类静态数据成员在编译时创建并初始化;在该类的任何对象建立之前就存在,不属于任何对象,而非静态类成员变量则是属于对象所有的。类静态数据成员只有一个拷贝,为所有此类的对象所共享。
    (2)类静态成员函数属于整个类,不属于某个对象,由该类所有对象共享。
  1. static 成员变量实现了同类对象间信息共享。
  2. static 成员类外存储,求类大小,并不包含在内。
  3. static 成员是命名空间属于类的全局变量,存储在 data 区的 rw 段。
  4. static 成员只能类外初始化。
  5. 可以通过类名访问(无对象生成时亦可),也可以通过对象访问。

静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成 对静态数据成员的封装。
静态成员函数只能访问静态数据成员。原因:非静态成员函数,在调用时 this 指针时被 当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针。this 指针不能在静态成员函数中使用,因为静态成员函数不是通过它们所属类的任何实例调用的。而且,静态成员函数除非指定该成员属于哪个实例,否则不能访问其类的实例成员。
定义静态函数的好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突
<2> 静态函数不能被其他文件所用。 存储说明符auto,register,extern,


在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量
1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。
4)定义全局静态变量的好处:
<1>不会被其他文件所访问,修改
<2>其他文件中可以使用相同名字的变量,不会发生冲突。
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量
1)内存中的位置:静态存储区
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。

注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置,从原来的栈中存放改为静态存储区。但是局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问。
当static用来修饰全局变量的时候,它就改变了全局变量的作用域(在声明他的文件之外是不可见的),但是没有改变它的存放位置,还是在静态存储区中。

静态函数
定义静态函数的好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突
<2> 静态函数不能被其他文件所用。 存储说明符auto,register,extern,static,对应两种存储期:自动存储期和静态存储期。 auto和register对应自动存储期。具有自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。
关键字extern和static用来说明具有静态存储期的变量和函数。用static声明的局部变量具有静态存储持续期(static storage duration),或静态范围(static extent)。虽然他的值在函数调用之间保持有效,但是其名字的可视性仍限制在其局部域内。静态局部对象在程序执行到该对象的声明处时被首次初始化。

STL(Standard Template Library)标准模板库,常见的 STL 容器有哪些?算法用过哪几个?
借助模板把常用的数据结构及其算法都实现了一遍,并且做到了数据结构和算法的分离。
本质是封装有数据结构的模板类
STL 就位于各个 C++ 的头文件中,即它并非以二进制代码的形式提供,而是以源代码的形式提供。

在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。
vector <int> a; //定义 a 数组,当前数组长度为 0,但和普通数组不同的是,此数组 a 可以根据存储数据的数量自动变长。

常见STL容器http://c.biancheng.net/view/6675.html

容器,它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)
迭代器(iterator)迭代器和 C++ 的指针非常类似,它可以是需要的任意类型,通过迭代器可以指向容器中的某个元素,如果需要,还可以对该元素进行读/写操作
迭代器提供了一种方法,使它能够按照顺序访问 某个容器所含的各个元素,但无需暴露该容器的内部结构。它将容器和算法分开,好让这 二者独立设计。
按功能强弱分为输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器5 种

正向迭代器 	容器类名::iterator  迭代器名;
常量正向迭代器 	容器类名::const_iterator  迭代器名;
反向迭代器 	容器类名::reverse_iterator  迭代器名;
常量反向迭代器 	容器类名::const_reverse_iterator  迭代器名;

3类:

  • 序列容器 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。

    array 容器是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。
    array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
    vector<T>(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
    deque<T>(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
    list<T>(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
    forward_list<T>(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。
    
    • vector 容器
    1. array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。

    2. 向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。

    3. begin() 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。
      end() 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
      rbegin() 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。
      rend() 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
      cbegin() 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。

    4. push_back()
      该成员函数的功能是在 vector 容器尾部添加一个元素
      emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。
      push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);
      而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程

    5. insert() 函数的功能是在 vector 容器的指定位置插入一个或多个元素。用于在 vector 容器指定位置之前插入一个新的元素。
      在这里插入图片描述

      cend() 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
      crbegin() 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
      crend() 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。

  • 排序容器(关联式容器) 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。

    • 关联式容器在存储元素时还会为每个元素在配备一个键,整体以**键值对 <key,value> **的方式存储到容器中。相比前者,关联式容器可以通过键值直接找到对应的元素,而无需遍历整个容器。另外,关联式容器在存储元素,默认会根据各元素键值的大小做升序排序。

      map 	定义在 <map> 头文件中,使用该容器存储的数据,其各个元素的键必须是**唯一**的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序(调用 std::less<T>)。
      set 	定义在 <set> 头文件中,使用该容器存储的数据,各个元素**键和值完全相同**,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less<T>)。
      multimap 	定义在 <map> 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的**键可以重复**。
      multiset 	定义在 <set> 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的**值可以重复**(一旦值重复,则意味着**键也是重复的**)。
      
  • 哈希容器 C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。
    哈希函数
    一般的线性表,树中,记录在结构中的相对位置是随机的,即和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较“的基础上,查找的效率依赖于查找过程中所进行的比较次数。 理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。

容器适配器
容器适配器是一个封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能。

stack<T>:是一个封装了 deque<T> 容器的适配器类模板,默认实现的是一个后入先出(Last-In-First-Out,LIFO)的压入栈。stack<T> 模板定义在头文件 stack 中。
queue<T>:是一个封装了 deque<T> 容器的适配器类模板,默认实现的是一个先入先出(First-In-First-Out,LIFO)的队列。可以为它指定一个符合确定条件的基础容器。queue<T> 模板定义在头文件 queue 中。
priority_queue<T>:是一个封装了 vector<T> 容器的适配器类模板,默认实现的是一个会对元素排序,从而保证最大元素总在队列最前面的队列。priority_queue<T> 模板定义在头文件 queue 中。

容器适配器本质上还是容器,只不过此容器模板类的实现,利用了大量其它基础容器模板类中已经写好的成员函数。当然,如果必要的话,容器适配器中也可以自创新的成员函数。
迭代器适配器,其本质也是一个模板类,比较特殊的是,该模板类是借助以上 5 种基础迭代器实现的。换句话说,迭代器适配器模板类的内部实现,是通过对以上 5 种基础迭代器拥有的成员方法进行整合、修改,甚至为了实现某些功能还会添加一些新的成员方法。由此,将基础迭代器“改头换面”,就变成了本节要讲的迭代器适配器。

反向迭代器(reverse_iterator) 	又称“逆向迭代器”,其内部重新定义了递增运算符(++)和递减运算符(--),专门用来实现对容器的逆序遍历。
安插型迭代器(inserter或者insert_iterator) 	通常用于在容器的任何位置添加新的元素,需要注意的是,此类迭代器不能被运用到元素个数固定的容器(比如 array)上。
流迭代器(istream_iterator / ostream_iterator)
流缓冲区迭代器(istreambuf_iterator / ostreambuf_iterator) 	输入流迭代器用于从文件或者键盘读取数据;相反,输出流迭代器用于将数据输出到文件或者屏幕上。
输入流缓冲区迭代器用于从输入缓冲区中逐个读取数据;输出流缓冲区迭代器用于将数据逐个写入输出流缓冲区。
移动迭代器(move_iterator) 	此类型迭代器是 C++ 11 标准中新添加的,可以将某个范围的类对象移动到目标范围,而不需要通过拷贝去移动。
STL 常用算法(排序、合并、搜索和分区)

sort()排序函数,sort (first, last)对容器或普通数组中[first, last) 范围内的元素进行排序,默认进行升序排序,该函数基于快速排序实现,对于指定区域内值相等的元素,sort() 函数无法保证它们的相对位置不发生改变。

  • stable_sort (first, last) 和 sort() 函数功能相似,不同之处在于,对于 [first, last) 范围内值相同的元素,该函数不会改变它们的相对位置。
  • partial_sort (first, middle, last) 从 [first,last) 范围内,筛选出 muddle-first 个最小的元素并排序存放在 [first,middle) 区间中。
  • partial_sort_copy (first, last, result_first, result_last) 从 [first, last) 范围内筛选出 result_last-result_first 个元素排序并存储到 [result_first, result_last) 指定的范围中。
  • is_sorted (first, last) 检测 [first, last) 范围内是否已经排好序,默认检测是否按升序排序。
  • is_sorted_until (first, last) 和 is_sorted() 函数功能类似,唯一的区别在于,如果 [first, last) 范围的元素没有排好序,则该函数会返回一个指向首个不遵循排序规则的元素的迭代器。
  • void nth_element (first, nth, last) 找到 [first, last) 范围内按照排序规则(默认按照升序排序)应该位于第 nth 个位置处的元素,并将其放置到此位置。同时使该位置左侧的所有元素都比其存放的元素小,该位置右侧的所有元素都比其存放的元素大。

merge()函数将 2 个有序序列合并为 1 个有序序列,前提是这 2 个有序序列的排序规则相同(要么都是升序,要么都是降序)。并且最终借助该函数获得的新有序序列,其排序规则也和这 2 个有序序列相同。
OutputIterator merge (InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, InputIterator2 last2, OutputIterator result);
first1、last1、first2 以及 last2 都为输入迭代器,[first1, last1) 和 [first2, last2) 各用来指定一个有序序列;result 为输出迭代器,用于为最终生成的新有序序列指定存储位置;comp 用于自定义排序规则。该函数会返回一个输出迭代器,其指向的是新有序序列中最后一个元素之后的位置。

  • void inplace_merge (BidirectionalIterator first, BidirectionalIterator middle,
    BidirectionalIterator last);
    first、middle 和 last 都为双向迭代器,[first, middle) 和 [middle, last) 各表示一个有序序列
    merge() 函数会将最终合并的有序序列存储在其它数组或容器中,inplace_merge() 函数则将最终合并的有序序列存储在 [first, last) 区域中

find()函数用于在指定范围内查找和目标元素值相等的第一个元素。
InputIterator find (InputIterator first, InputIterator last, const T& val);
其中,first 和 last 为输入迭代器,[first, last) 用于指定该函数的查找范围;val 为要查找的目标元素。
find() 函数除了可以作用于序列式容器,还可以作用于普通数组。
find_end() 函数在序列 A 中查找序列 B 最后一次出现的位置
search() 函数用于在序列 A 中查找序列 B 第一次出现的位置。
adjacent_find() 函数在指定范围内查找 2 个连续相等的元素
partition() 函数可根据用户自定义的筛选规则,重新排列指定区域内存储的数据,使其分为 2 组,第一组为符合筛选条件的数据,另一组为不符合筛选条件的数据。
lower_bound() 函数用于在指定区域内查找不小于目标值的第一个元素。也就是说,使用该函数在指定范围内查找某个目标值时,最终查找到的不一定是和目标值相等的元素,还可能是比目标值大的元素。
reverse_copy() 算法可以将源序列复制到目的序列中,目的序列中的元素是逆序的。定义源序列的前两个迭代器参数必须是双向迭代器。目的序列由第三个参数指定,它是目的序列的开始迭代器,也是一个输出迭代器。如果序列是重叠的,函数的行为是未定义的。这个算法会返回一个输出迭代器,它指向目的序列最后一个元素的下一个位置。
unique() 算法可以在序列中原地移除重复的元素,这就要求被处理的序列必须是正向迭代器所指定的。在移除重复元素后,它会返回一个正向迭代器作为新序列的结束迭代器。可以提供一个函数对象作为可选的第三个参数,这个参数会定义一个用来代替
rotate() 算法会从左边选择序列的元素
transform() 可以将函数应用到序列的元素上,并将这个函数返回的值保存到另一个序列中,它返回的迭代器指向输出序列所保存的最后一个元素的下一个位置。
all_of() 算法会返回 true,前提是序列中的所有元素都可以使谓词返回 true。
any_of() 算法会返回 true,前提是序列中的任意一个元素都可以使谓词返回 true。
none_of() 算法会返回 true,前提是序列中没有元素可以使谓词返回 true。

const,解释其作用
const指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。可以修饰变量,参数,返回值,甚至函数体。const可以提高程序的健壮性、安全性和可靠性。

主要作用:
1.可以定义const常量,具有不可变形
2.便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
3.可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 同宏定义一样,可以做到不变则已,一变都变!
4.可以保护被修饰的东西,防止意外的修改,增强程序的健壮性
5.可以节省空间,避免不必要的内存分配。
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干份拷贝。
6.提高了效率。
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

const int a; //a的值不能被修改
int const a; //同上
const int *a;// 指针指向的内容不可以被修改 但是指针指向的地址可以修改。
int const a;//a表示的地址不能给修改
int const
a const; //a指向的值和表示的地址都不能被修改
(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。

1.const 修饰类的成员变量,成员常量不能被修改。
2.const 修饰函数承诺在本函数内部不会修改类内的数据成员,不会调用其它非 const 成员 函数。
3.如果 const 构成函数重载,const 对象只能调用 const 函数,非 const 对象优先调用非 const 函数。
4.const 函数只能调用 const 函数。非 const 函数可以调用 const 函数。
5.类体外定义的 const 成员函数,在定义和声明处都需要 const 修饰符。

补充

一个参数既可以是const还可以是volatile吗?解释为什么。
可以,(硬件状态寄存器)但没有必要,因为const修饰的参数不能被修改,没有必要再用volatile修饰
用变量a给出下面的定义
a)一个整型数(An integer)
int a;
b)一个指向整型数的指针(A pointer to an integer)
int *a;
c)一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)
int **a;
d) 一个有10个整型数的数组(An array of 10 integers)
int a[10];
e) 一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers to integers)
int *a[10];
f) 一个指向有10个整型数数组的指针(A pointer to an array of 10 integers)
int (*a)[10]
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
int *a( int ){}
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
int (* a[10])( int ) //类似这种定义应该采用分解法来做。 先定义单个函数指针 int (*a)(int),然后再是有10个指针的数组 int (*a[10])(int)

嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。

#define BIT3 (1<<2)
void setBit3( int a ){
    a |= BIT3;
    printf("%d\n", a);
}

void clearBit3(int a ){
    a &=~BIT3;
    printf("%d\n", a);
}

下面的代码输出是什么,为什么?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6)?puts("> 6") : puts("<= 6");
}

大于6. 有符号数转换为无符号数。

评价下面的代码片断:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;

FFFF有16bit,不同位数的系统最大数不一样,表示最大数应该使用 ~0

找错题
试题1:
void test1()
{
char string[10];
char
str1 = “0123456789”;
strcpy( string, str1 );
}
*
溢出 还有\0
试题2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1[i] = ‘a’;
}
strcpy( string, str1 );
}

str1没有\0结束符,程序崩溃。
试题3:
void test3(char
str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
*
改为strlen( str1 ) < 10 这种方式不会报错,但是会造成隐藏的严重bug, 会修改内存里面的数据

写出字符串strcpy的函数实现过程式.
strcpy把含有’\0’结束符的字符串复制到另一个地址空间,返回值的类型为char*

void strcpy(char * desc, const  char *src ){
	while(*(desc+i) ==*(desc+i) );
      for( int i=0; *(src+i)!='\0'; i++){
     	*(desc+i) = *(src+i);
	  }
}

strcpy 为什么要返回char*
strcpy函数返回的值可以作另一个函数的实参,实现链式操作。

*请计算sizeof的值和strlen的值。
void func ( char str )
{
sizeof( str ) = ?
}
char str[10] = “hello”;
strlen(str);

sizeof求字符串所占的字节数
strlen求字符串的长度,结束符\0之前的字符个数
10和5
1.sizeof(数组名)————数组名表示整个数组,求整个数组大小,单位:字节
2.&数组名————取整个数组的地址
3.除此之外,所有遇到的数组名都是首元素地址
strlen(arr),参数是地址,从这个地址往后找遇到\0停止

宏定义问题
#include “stdafx.h”
#define SQR(X) XX
int main(int argc, char
argv[])
{
int a = 10;
int k = 2;
int m = 1;
a /= SQR(k+m)/SQR(k+m);
printf("%d\n",a);
return 0;
}
a =a/ (k+mk+m/k+mk+m)

*写一个“标准”宏min,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时会发生什么事?
least = min(p++, b);

#define MIN(a,b) ((a<b)?a:b)
会进行文本替换:
#define MIN(a,b) ((*p++<b)?*p++:b)
可以看到++执行了两次。

编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh”函数头是这样的:
//pstr是指向以’\0’结尾的字符串的指针
//steps是要求移动的n
void loopmove ( char * pstr, int steps )
{
//请填充…
}

void LoopMove(char * pStr, int steps)
{
    int len = strlen(pStr);
    int st = steps % len; // 取余
        // 字符串长度为0,或不需移动,或移动步数小于等于0.返回,也可报错。
    if (len == 0 || st == 0 || steps <= 0) return;
    char temp[100] = {0};
    memcpy(temp, pStr+len-st, st);
    memcpy(temp+st, pStr, len-st);
    memcpy(pStr, temp, len);
}
strcpy和memcpy主要有以下3方面的区别。
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。

这道题目的结果是什么啊?
#include “stdafx.h”
#define SQR(X) XX
int main(int argc, char
argv[])
{
int a = 10;
int k = 2;
int m = 1;
a /= SQR(k+m)/SQR(k+m);
printf("%d\n",a);
return 0;
}
a =a/ (k+mk+m/k+mk+m)

注意文本替换就行,所以宏定义的最佳时间就是对内容加括号。#define SQR(X) (X*X)

下面是C 语言中两种if 语句判断方式。请问哪种写法更好?为什么?
int n;
if (n == 10) // 第一种判断方式
if (10 == n) // 第二种判断方式

也是代码的最佳实践问题。 如果按照第一种写法误写成n=10的话会引起bug,第二种这种情况编译器会报错提示。

什么是虚函数?什么是纯虚函数?
虚函数声明如下: virtual ReturnType FunctionName(Parameter);引入虚函数是为了动态绑定。
纯虚函数声明如下:virtual ReturnType FunctionName()= 0;引入纯虚函数是为了派生接口。
虚函数
定义一个函数为虚函数,不代表函数为不被实现的函数
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数

C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要再派生类中声明该方法为虚方法。

当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = &b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual方法,请看后面),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。
如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。
纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0;
引入原因
  1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

纯虚函数是否可以实例化
c++中包含纯虚函数的类是不允许被实例化的!!!,进一步说,如果继承该类的类不重写这个纯虚函数的话,也是不允许被实例化的。即,包含纯虚函是的类派生出来的类都必须重写这个纯虚函数!
3、不能声明为虚函数的有哪些
1、静态成员函数; 2、类外的普通函数; 3、构造函数; 4、友元函数
虚函数是为了实现多态特性的。虚函数的调用只有在程序运行的时候才能知道到底调用的是哪个函数,其是有有如下几点需要注意:
(1) 类的构造函数不能是虚函数
构造函数是为了构造对象的,所以在调用构造函数时候必然知道是哪个对象调用了构造函数,所以构造函数不能为虚函数。
(2) 类的静态成员函数不能是虚函数
类的静态成员函数是该类共用的,与该类的对象无关,静态函数里没有this指针,所以不能为虚函数。
(3)内联函数
内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略。
(4)友元函数
友元函数与该类无关,没有this指针,所以不能为虚函数。

抽象函数:规定其非虚子类必须实现的函数,必须被重写
构成多态的条件
多态存在的三个条件:
必须存在继承关系;
继承关系中必须有同名的虚函数,并且它们是覆盖关系(重载不行)。
存在基类的指针,通过该指针调用虚函数。

覆盖和重载的区别
覆盖也称为重写(override)
子类重写从基类继承过来的函数、函数名、返回值、参数列表都必须和基类相同。当子类的对象调用成员函数时,如果成员函数有被覆盖则调用子类中覆盖的版本,否则调用从基类继承过来的函数。
如果子类覆盖的是基类的虚函数,则可以用来实现多态。当子类重新定义基类的虚函数之后,基类指针可以根据赋给它不同子类指针动态的调用子类中的虚函数,做到动态绑定。

重载指允许在相同作用域中存在多个同名的函数,这些函数的参数表不同,编译器根据函数不同的形参表对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。

void Fun(int a);
void Fun(double a);
void Fun(int a, int b);
void Fun(double a, int b);

区别

重载要求函数名相同,但是参数列表必须不同;覆盖要求函数名、参数列表、返回值必须相同。重载描述的是同一个类中不同成员函数之间的关系;覆盖是子类和基类之间不同成员函数之间的关系。重载的确定是在编译时确定,是静态的;虚函数则是在运行时动态确定。

快速排序的思想、时间复杂度、实现以及优化方法
1、 快速排序的三个步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot);
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大;
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
2、 快速排序代码的实现

void quicksort(vector<int> &v,int left, int right)
{  
    if(left < right)//false则递归结束
    {    
        int key=v[left];//基数赋值
        int low = left;                
        int high = right;   
        while(low < high)    //当low=high时,表示一轮分割结束
        {                        
            while(low < high && v[high] >= key)//v[low]为基数,从后向前与基数比较
            {                                
                high--;                        
            }
            swap(v[low],v[high]);

            while(low < high && v[low] <= key)//v[high]为基数,从前向后与基数比较
            {                                
                low++;                        
            }      
            swap(v[low],v[high]);
        }                 
        //分割后,对每一分段重复上述操作
        quicksort(v,left,low-1);               
        quicksort(v,low+1,right);
    }
}

优化
常见的做法“三数取中”法(序列太短还要结合其他排序法,如插入排序、选择排序等)

void insertion_sort(vector<int> &unsorted,int left, int right)  //插入排序算法        
{          
    for (int i = left+1; i <= right; i++)          
    {          
        if (unsorted[i - 1] > unsorted[i])          
        {         
            int temp = unsorted[i];         
            int j = i;         
            while (j > left && unsorted[j - 1] > temp)  

            {           
                unsorted[j] = unsorted[j - 1];          
                j--;          
            }     
            unsorted[j] = temp;      
        }      
    }     
}  

int SelectPivotOfThree(vector<int> &arr,int low,int high)  //三数取中,同时将中值移到序列第一位  
{    
    int mid = low + (high - low)/2;//计算数组中间的元素的下标    

    //使用三数取中法选择枢轴  
    if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]    
    {    
        swap(arr[mid],arr[high]);  
    }    
    if (arr[low] > arr[high])//目标: arr[low] <= arr[high]    
    {    
        swap(arr[low],arr[high]);  
    }    
    if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]    
    {    
        swap(arr[mid],arr[low]);  
    }    
    //此时,arr[mid] <= arr[low] <= arr[high]    
    return arr[low];    
    //low的位置上保存这三个位置中间的值    
    //分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了    
}
//三数取中函数不仅仅要实现取中,还要将中值移到最低位,从而保证原分割函数依然可用。
————————————————
版权声明:本文为CSDN博主「沙师弟哪里去」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yuechuxuan/article/details/81878438

C语言中变量的存储类型有哪些
C语言中的存储类型有auto,extern,register,static四种
auto:自动变量,可以不写,默认情况下局部变量就是auto
register:寄存器变量,把经常被调用的变量声明为寄存器变量,快速存储
extern: 静态存储类别的变量,在另一个文件中,通过extern 全局变量名的声明
static:静态存储类别的变量
静态存储类别的变量,从程序的开始处就存在,其生命期伴随整个程序,一直存在程序的执行过程。static变量仅仅在变量的作用范围内可见,而全局变量是在所有地方都可见的,这就是static变量与全局变量的区别,全局变量是不显示用static修饰的全局变量,但全局变量默认是静态的,作用域是整个工程。


解释下封装、继承和多态?(面向对象三个特征)

  • 封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(称为类)。
    封装的意义在于保护或者防止代码(数据)被我们无意中破坏

  • 继承: 继承主要实现重用代码,节省开发时间。 子类可以继承父类的一些东西。允许依据另一个类来定义一个类
    当创建

    // 基类
    class Animal {
        // eat() 函数
        // sleep() 函数
    };
    
    //派生类
    class Dog : public Animal {
        // bark() 函数
    };
    
  • 多态Polymorphism:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可 以通过指向基类的指针,来调用实现派生类中的方法。

    • 多态的实现方式
      分为方法的重载和重写,
      重载是指方法的名和返回类型相同,但是参数不同,它是一种编译时多态。
      重写是指子类继承父类的方法,但是在子类自己类体里又写了一个方法,跟父类中的方法名、返回类型、参数列表都完全一样,但是方法体不同,也就是说它有自己的实现方式.这就是重写,它是 一种运行时多态

面向对象程序设计的多态的实现机制是什么?
在这里插入图片描述

多态是面向对象程序设计中改代码重用的一个重要机制,表示当同一个操作作用在不同对象时,会有不同的语义,从而产生不同的结构
在继承体系下,将父类的某个函数给成虚函数(即加上virtual关键字),在派生类中对这个函数进行重写,利用父类的指针或引用调用虚函数。

通过动态绑定技术实现
含有虚函数的类都有虚表
当子类对弗雷的虚函数没有重写时,子类的虚表指针保存的是父类的虚表;
当子类对父类的虚函数重写时,子类的虚表指针保存的是子类的虚表;
当子类中有自己的虚函数时,在虚表中将此虚函数地址添加在后面。

c++的多态性概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数

虚函数 是在基类中使用关键字virtual声明的函数。在派生类中重新定义基类中定的虚函数,会告诉编译器不要静态链接到该函数。
在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
纯虚函数 在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

  • 作用 “一个接口,多种方法”
  • 方法的重载(overload
    重载是指同一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时就可以确定到底调用哪个方法,它是一种编译时多态。重载可以被看作一个类中的方法多态性。
  • 方法的覆盖(override)
    子类可以覆盖父类的方法,因此同样的方法会在父类与子类中有着不同的表现形式。在java语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象。同样,接口的引用变量也可以指向其实现类的实例对象。而程序调用的方法在运行期才动态绑定(绑定指的是将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态绑定的方法实现了多态。由于只有在运行时才能确定调用哪个方法,因此通过方法覆盖实现的多态也可以本称为运行时多态,示例如下:

指针和引用的区别
引用访问一个变量是直接访问,而指针是间接访问。
引用是一个变量的别名,本身不单独分配自己的内存空间,而指针有自己的内存空间。

  1. 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用仅是一个别名
  2. 引用使用时无需解引用(*),指针需要解引用
  3. 引用只能在定义时被初始化一次,之后不可变;指针可变;
  4. 引用没有 const,指针有 const;
  5. 引用不能为空,指针可以为空;
  6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大 小;
  7. 指针和引用的自增(++)运算意义不一样;
  8. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a 是不合法的)
  9. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
指针引用
变量,存储地址,指向内存的一个存储单元别名
间接访问直接访问
有自己的内存空间不单独分配自己的内存空间
可变只能在定义时被初始化一次
有const没有const
可为空不能为空
“sizeof 指针”得到的是指针本身的大小“sizeof 引用”得到的是所指向的变量(对象)的大小
可以多级别只能一级

指针与数组的区别

  1. 存储方式的不同:数组是存储的成员内容的本身,而指针存储的是首地址
  2. 运算方式的不同:数组名是一个常量不能进行自增自减运算,而指针是一个变量,以进行自增自减的运算
  3. 赋值运算的操作:数组可以进行初始化,但并不能通过复制语句进行整体的赋值,指针能指向另一条语句。

什么是内存泄漏?面对内存泄漏和指针越界,有哪些方法?通常采用哪些方法来避免和减少这类错误?
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单 元即为内存泄露。 使用的时候要记得指针的长度。 malloc 的时候得确定在那里 free. 对指针赋值的时候应该注意被赋值指针需要不需要释放. 动态分配内存的指针最好不要再次赋值
怎么编一个C语言程序检测内存泄漏
其原理是在申请和释放的时候做下标记,通过标记来判断是否出现内存泄漏

#include<iostream>
using namespace std;
void TestMemoryLeak(){
	int *p = (int*)malloc(sizeof(int)*10);
	if(p==NULL)
		cout<<"内存不足!"<<endl;
		//通过释放p和不释放p来对不
		//free(p);
	if(_CrtDumoMemeryLeaks())
		cout<<"内存泄漏"<<endl;
	else
		cout<<"没有内存泄漏"<<endl;
}

hash 冲突及解决办法
关键字值不同的元素可能会映射到哈希表的同一地址上
解决方法:

  1. 开放定址法:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
  2. 再哈希法:同时构造多个不同的哈希函数。
  3. 链地址法:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
  4. 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

栈的原理
它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

class和struct的区别
C++是什么
C++是在C语言的基础上开发的一种面向对象编程语言,应用广泛。C++支持多种编程范式--面向对象编程、泛型编程和过程化编程。其编程领域众广,常用于系统开发,引擎开发等应用领域,是最受广大程序员受用的最强大编程语言之一,支持类:类、封装、继承、多态、重载等特性!
C和C++的区别
C++是C的扩展,是在C的基础上添加类,C是结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入进行运算处理得到输出;而对于C++,首先要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现对事件控制。

java和C区别,哪个安全性更好
C语言是面向过程的,java是面向对象的
2.语言跨平台:
C语言不可以跨平台,因为Java可以跨平台,在windows 和 unix 等系统上都可以很好的运行。
3.指针管理:
指针是c语言最大的优点,它可以使用户几乎可以访问计算机的所有内存资源和其他部分资源(就是指那里打那里)。同时也是c语言程序最难掌握和调试的问题,并且给系统的安全性和稳定性带来很大的困难。 而java中没有指针的概念,尽管也有数组和对象的引用的概念,但它的管理全部交给系统管理,这样限制了用户的资源的访问,但是也给java系统带来安全性和稳定性。JAVA语言让编程者无法找到指针来直接访问内存无指针,并且增添了自动的内存管理功能,从而有效地防止了c语言中指针操作失误,如野指针所造成的系统崩溃。但也不是说JAVA没有指针,虚拟机内部还是使用了指针,只是外人不得使用而已。这有利于Java程序的安全
4.封装
在java中引入了package的概念,使面向对象和面向组件开发更加方便,而在c语言中没有package概念,需要其他方式来实现。Java都能够实现面向对象思想(封装,继乘,多态)。而由于c语言为了照顾大量的C语言使用者,而兼容了C,使得自身仅仅成为了带类的C语言,多多少少影响了其面向对象的彻底性!JAVA则是完全的面向对象语言,它句法更清晰,规模更小,更易学。它是在对多种程序设计语言进行了深入细致研究的基础上,据弃了其他语言的不足之处,从根本上解决了c语言的固有缺陷。
5.数据类型及类
Java是完全面向对象的语言,所有函数和变量部必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。而c语言允许将函数和变量定义为全局的。
6.自动内存管理
Java程序中所有的对象都是用new操作符建立在内存堆栈上, Java自动进行无需内存回收操作,不需要程序员进行删除。而c语言中必须由程序贝释放内存资源,增加了程序设计者的负扔。Java中当一个对象不被再用到时,无用内存回收器将给它加上标签以示删除。JAVA里无用内存回收程序是以线程方式在后台运行的,利用空闲时间工作。
7. 字符串:
C语言不支持字符串变量,在c语言程序中使用Null终止符代表字符串的结束,在Java中字符串是用类对象(strinR和stringBuffer)来实现的,这些类对象是Java语言的核心!

Java没有函数,作为一个比c语言更纯的面向对象的语言,Java强迫开发人员把所有例行程序包括在类中,事实上,用方法实现例行程序可激励开发人员更好地组织编码。

java和C++在安全性方面的比较
https://www.docin.com/p-495024879.html

  1. 全局变量
    java中,不能在所有类之外定义全局变量,只能通过在一个类中定义公用、静态的变量来实现一个全局变量。
  2. 指针
  3. 内存管理

什么是面向对象思想(OOP)
面向对象是一种思想,是相对于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。面向对象有三大特征:封装性、继承性、多态性,其中封装性指的是隐藏了对象的属性和实现细节,仅对外提供公共的访问方式,这样就隔离了具体的变化,便于使用,提高了复用性和安全性。对于继承性,就是两种事物间存在着一定的所属关系,那么继承的类就可以从被继承的类中获得一些属性和方法;这就提高了代码的复用性。继承是作为多态的前提的。多态是说父类或接口的引用指向了子类对象,这就提高了程序的扩展性,也就是说只要实现或继承了同一个接口或类,那么就可以使用父类中相应的方法,提高程序扩展性,但是多态有一点不好之处在于:父类引用不能访问子类中的成员

栈在编程中的应用
函数调用把函数调用之后要执行的指令的地址压入栈中,然后将要调用的函数压入栈中,随后将(部分)参数压入栈中,然后把要执行的指令的地址改为所调用的函数的地址,当这个函数执行完成之后,便会找到当前保存的返回地址,再接着执行原本函数中的代码。

def inner(foo):
    print("进入inner函数")
    print("离开inner函数")


def outter(bar):
    print("进入outter函数")
    inner(bar)
    print("离开outter函数")

if __name__ == "__main__":
    outter("hey")
/*运行结果
$ python stack.py
进入outter函数
进入inner函数
离开inner函数
离开outter函数*/

如何评价算法/什么是算法,算法的精确定义是啥
答:算法是对特定问题求解步骤的-种描述,它是指令的有限序列,其中每一条指令表示一个
或多个操作.
精确定义是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系
统的方法描述解决问题的策略机制.
算法的五个特性:有穷性、确定性、可行性、输入、输出
确定性:算法中每一条指令必须有确切的含义,不会产生二义性

C语言能写多少程序
答:可数无穷个.
语言只是工具,我们无法界定一个工具能做多少种事。
我觉得关键还在于程序员的能力
如果不考虑现实物理意义上的存储空间,则为可数无限个,和自然数集的基数是等势的;否则是有限的。
原因:我们设长度为n的C语言程序(包括头文件等)的个数为f(n)。那么显然,对于任意的自然数n,f(n)是有限的,因为它最大为x的n次方(x为合法的C语言字符集基数)。对于其中能够编译通过的每个程序,从位置0,1,2,…开始排起。这样对于n=0,1,2…,都按上述次序排起,也就是第一个由n+1个字符所组成的可以编译通过的程序恰排在最后一个由n个字符所组成的可以编译通过的程序之后。如此便将C语言可以写的程序数与自然数集建立一一对应,故答案是可数无限个。
但是在现实世界中存储空间不能无限大,一个C语言程序一定是由有限个字符所组成的,从这个角度来看,上述讨论总会排到终点,故答案是有限个。

函数模块里面有什么,视图是什么,作用和特点,封装是什么(内部属性和外部属性是什么)
函数模块里面有方法和数据
视图是从一个基本表或是多个基本表导出的表,是一个虚表。在存储时只存储视图的定义,而无需
存储对应的数据
1)简化数据查询操作,减少数据的冗余
2)使用户能从多角度看到同一数据
3)提高了数据的安全性和保密度
4)提供了一定程度的逻辑独立性
封装:在面向对象编程中,封装是将对象运行所需的资源封装在程序对象中。隐藏对象的属性和实现细节,仅对外公开接口

模块,又称构件,是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。它具有两个基本的特征:外部特征和内部特征。外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能;内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)。

  • 视图是一个虚拟的表,它并没有自己的数据,在数据库中也不存在它的物理结构,只存在它的定义。它的数据是动态的,来自于其他的一个表或多个表。引用视图时,数据库会对相应的表进行查询,把符合要求的数据也就是查询结果提取出来,放入到视图中。所以在视图中所看到的只是我们向用户提供的有访问权限的一部分数据或字段,真正的表的结构或内容,用户是看不到的。
  • 视图的特点:
    1、视图通常也被称为子查询,是从一个或多个表导出的虚拟的表,其内容由查询定义。具有普通表的结构,但是不实现数据存储;
    2、对视图的修改:单表视图一般用于查询和修改,会改变基本表的数据;
    3、多表视图一般用于查询,不会改变基本表的数据;
    4、 视图可以使应用程序和数据库表在一定程度上独立。如果没有视图,应用一定是建立在表上的,有了视图之后,程序可以建立在视图之上,从而程序与数据库表被视图分割开来。
  • 视图作用
    • 简单性:简化用户对数据的理解,简化操作,被经常使用的查询可以被定义为视图,从而使用户不必为以后的操作没指定全部的条件;
    • 安全性:通过视图用户只能查询和修改他们所能见到的数据。但不能授权到数据库特定行和特定的列上。通过视图,用户可以被限制在数据的不同子集上:使用权限可被限制在另一视图的一个子集上,或是一些视图和基表合并后的子集上。
    • 逻辑数据独立性。视图可帮助用户屏蔽真实表结构变化带来的影响。

拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。(用类的一个对象去初始化另一个对象)
  • 复制对象把它作为参数传递给函数。(当函数的形参是类的对象时)
  • 复制对象,并从函数返回这个对象。(当函数的返回值是累的对象或引用时)
classname (const classname &obj) {
   // 构造函数的主体
}
class Line
{
   public:
      int getLength( void );
      Line( int len );             // 简单的构造函数
      Line( const Line &obj);      // 拷贝构造函数 格式为:类名(const 类名& 对象名);
      ~Line();                     // 析构函数
 
   private:
      int *ptr;
};
  • 为什么拷贝构造函数必须是引用(参数为引用值)传递,不能是值传递
    防止递归引用
    当一个对象以传递值的方式传一个函数时,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至
    栈溢出
    (Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。

  • 深拷贝与浅拷贝
    浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧连各国对象指向同一个外部内容(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在)
    深拷贝:如果在复制这个对象时为新对象制作了外部对象的独立复制,就是深拷贝

  • 拷贝构造函数的作用
    复制对象,再使用这个对象的实例来初始化这个对象的一个新的实例

  • 什么情况下必须定义拷贝构造函数
    当类的对象用于函数值传递时(值参数、返回类对象),拷贝构造函数会被调用。如果对象复制非简单的值拷贝

  • 拷贝构造函数和赋值函数的区别
    1) 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
    2)一般,数据成员含指针对象时,需要考虑:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象
    3)实现不一样。拷贝构造函数首先是一个构造函数,它调用时通过参数的对象初始化产生一个对象。
    赋值函数是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。

FLoyd算法,说说算法思想
答:Floyd算法是一种最短路径算法
(1)采用邻接矩阵作为存储结构,把关系矩阵看成是没有经过任何中间节点,
直接可以到达的每一对顶点间的最短路径表示
(2)经过多次迭代,每次增加一个新的结点,在允许这个节点作为中间接节点
的条件下,计算出每一对顶点间的最短路径的缩短变化
(3)直到把所有顶点都考虑进去为止,结果为最短路径

局部变量和全局变量可以同名
可以同名。当局部变量和全局变量同名,在函数内引用这个变量时,会用到同名的局部变
量,而不会用到全局变量。

float x 和0比较
不能直接做等值比较
if (fabs(x) < 0.00001f)

const float EPSINON = 0.00001; if ((x >= - EPSINON) && (x <= EPSINON)

静态变量,全局变量的编程题
什么程序适合线程,线程的特性


Java的特点
  1. 简单性:
    Java是纯面向对象语言,语法简单明了,易于掌握。
    Java使用接口取代了多重继承,并且取消了指针,因为多重继承和指针会使程序变得复杂。
    Java还会自动地收集内存垃圾,使得内存管理变得更为简单。
    Java还提供了丰富的类库、API文档以及第三方开发包,还有大量Java的开源项目。
  2. 面向对象性:
    面向对象性是Java语言的基础。
    java语言提倡“万物皆对象”,语法中不能在类外面定义单独的数据和函数。
    Java语言最外部的数据类型是对象,所有的元素都要通过类和对象来访问。
  3. 分布性:
    Java的分布性包括:1.操作分布。2.数据分布
    ⑴操作分布:即在多个不同的主机上不知相关操作。
    ⑵数据分布:将数据分别存放在不同的主机上,这些主机是网络中的不同成员。
  4. 可移植性:
    Java程序具有与体系结构无关的特性。
    Java的类库也提供了针对不同平台的接口,所有这些类库也可以被移植。
  5. 安全性
    Java语言删除了类C语言的指针和内存释放等语法,有效地避免了用户对内存的非法操作
    Java程序代码要经过代码程序校验、指针校验等很多测试步骤才能够运行。所以未经允许的Java程序不能出现损害系统平台的行为。
    Java可以编写出防病毒和防修改系统。
  6. 健壮性
    Java会检查程序在编码和运行时的错误,并消除错误。
  7. 多线程性
    Java应用程序可以在同一时间并行执行多项任务。而且相应的同步机制可以保证不同线程能够正确地共享数据。
  8. 高性能性
    Java编译后的字节码是在解释器中运行的,所以它的速度较多数交互式运用程序提高了很多。
  9. 动态性
    Java可以动态调整库中方法和增加变量,并且客户端不需要任何更改。在Java中进行动态调整是非常简单和直接。

什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
java虚拟机是一个可以执行java字节码的虚拟机进程。java源文件被编译成能被Java虚拟机执行的字节码文件
允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

static关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?
“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。
java中也不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖。
Java中的方法覆盖(Overriding)和方法重载(Overload)是什么意思
Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。
与此相对,方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。
Java中,什么是构造方法?什么是构造方法重载?什么是复制构造方法?
当新对象被创建的时候,构造方法会被调用。每一个类都有构造方法。在程序员没有给类提供构造方法的情况下,Java编译器会为这个类创建一个默认的构造方法。
Java中构造方法重载和方法重载很相似。可以为一个类创建多个构造方法。每一个构造方法必须有它自己唯一的参数列表。
Java不支持像C++中那样的复制构造方法,这个不同点是因为如果你不自己写构造方法的情况下,Java不会创建默认的复制构造方法
Java支持多继承么?
Java中类不支持多继承,只支持单继承(即一个类只有一个父类)。 但是java中的接口支持多继承,,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。
接口和抽象类的区别是什么?
Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:
接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
类可以实现很多个接口,但是只能继承一个抽象类
类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
抽象类可以在不提供接口方法实现的情况下实现接口。
Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
接口是绝对抽象的,不可以被实例化,抽象类也不可以被实例化。
也可以参考JDK8中抽象类和接口的区别


什么是程序局部性?为什么会有程序的空间局部性
程序局部性是指程序在运行时呈现出局部性规律,在一段时间间隔内,程序的执行是局限在某个部分,所访问的存储空间也只局限在某个区域。
程序的空间局部性是指若一个存储单元被访问,那么它附近的单元也可能被访问,这是由于程序的顺序执行引起的。

迭代和递归的特点,并比较优缺点

什么是红黑树
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组

  • 红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
  • 它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目

类和对象的区别
类是对象的抽象,对象是类的具体实例。
类是抽象的,不占用内存,而对象是具体的,占用内存。
例如:类就是水果,对象就是苹果

数组维数下标有什么类型
长整型
数组(Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。数组通常采用一个整数来作下标,在多维数组之中采用一系列有序的整数来标注,如在[ 3,1,5 ] 。这种整数列表之中整数的个数始终相同,且被称为数组维数。

栈在编程和程序的应用

内存优化

深拷贝与浅拷贝
B/S 和 C/S 的优缺点

用循环比递归效率高吗?
1.所谓的递归慢到底是什么原因呢?
递归的实现是通过调用函数本身,函数调用的时候,每次调用时要做地址保存,参数传递等,这是通过一个递归工作栈实现的。具体是每次调用函数本身要保存的内容包括:局部变量、形参、调用函数地址、返回值。那么,如果递归调用N次,就要分配N局部变量、N形参、N调用函数地址、N返回值。这势必是影响效率的。

2.用循环效率会比递归效率高吗?
递归与循环是两种不同的解决问题的典型思路。当然也并不是说循环效率就一定比递归高,递归带有栈操作,循环则不一定,两个概念不是一个层次,不同场景做不同的尝试。
2.1递归算法:
优点:代码简洁、清晰,并且容易验证正确性。
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。但是,对于某些问题,如果不使用递归,那将是极端难看的代码。

2.2循环算法:
优点:速度快,结构简单。
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。

2.3递归算法和循环算法总结:

  1. 一般递归调用可以处理的算法,也通过循环去解决常需要额外的低效处理。

  2. 现在的编译器在优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
    3.递归和循环两者完全可以互换。如果用到递归的地方可以很方便使用循环替换,而不影响程序的阅读,那么替换成递归往往是好的。(例如:求阶乘的递归实现与循环实现。)
    4.递归调用过多导致的栈溢出
    函数调用的参数是通过栈空间来传递的,在调用过程中会占用线程的栈资源。而递归调用,只有走到最后的结束点后函数才能依次退出,而未到达最后的结束点之前,占用的栈空间一直没有释放,如果递归调用次数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,导致程序的异常退出。

     1、循环(loop),指的是在满足条件的情况下,重复执行同一段代码。比如,while语句。
    
     2、迭代(iterate),指的是按照某种顺序逐个访问列表中的每一项。比如,for语句。
     
     3、遍历(traversal),指的是按照一定的规则访问树形结构中的每个节点,而且每个节点都只访问一次。
     
     4、递归(recursion),指的是一个函数不断调用自身的行为。通俗的解释:递归就像往存钱罐里存钱,先往里边塞钱,2块,5块,10块这样的塞,叫入栈。取钱的时候,后塞进去的先取出来,这叫出栈。具体多少钱,要全部出栈才知道。
    

为什么会有程序的空间局部性
逻辑结构和物理结构
B树和B+树的区别
1:关键字的数量不同,b+树有m个关键字,也有m个叶子节点。关键字只是用来存储索引。B树虽然也有m个子结点,但是其只拥有m-1个关键字。
2:存储的位置不同;B+树中的数据都存储在叶子结点上,也就是其所有叶子结点的数据组合起来就是完整的数据,但是B树的数据存储在每一个结点中,并不仅仅存储在叶子结点上。
3:分支结点的构造不同;B+树的分支结点仅仅存储着关键字信息和儿子的指针(这里的指针指的是磁盘块的偏移量),也就是说内部结点仅仅包含着索引信息。
4:查询不同;B树在找到具体的数值以后,则结束,而B+树则需要通过索引找到叶子结点中的数据才结束,也就是说B+树的搜索过程中走了一条从根结点到叶子结点的路径。
B+树比B 树更适合实际应用中操作系统的文件索引和数据库索引

如何实现循环队列,有何好处
1、循环队列的优点:
  可以有效的利用资源。用数组实现队列时,如果不移动,随着数据的不断读写,会出现假满队列的情况。即尾数组已满但头数组还是空的;循环队列也是一种数组,只是它在逻辑上把数组的头和尾相连,形成循环队列,当数组尾满的时候,要判断数组头是否为空,不为空继续存放数据。
2、循环队列的缺点:
  循环队列中,由于入队时尾指针向前追赶头指针;出队时头指针向前追赶尾指针,造成队空和队满时头尾指针均相等。因此,无法通过条件front==rear来判别队列是"空"是"满"。

C++

说一下static关键字的作用

  1. 全局静态变量
    在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
    静态存储区,在整个程序运行期间一直存在。
    初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
    作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
  2. 局部静态变量
    在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
    内存中的位置:静态存储区
    初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
    作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
  3. 静态函数
    在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
    函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
    warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
  4. 类的静态成员
    在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
  5. 类的静态函数
    静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
    在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

说一下C++和C的区别

  • 设计思想上:
    C++是面向对象的语言,而C是面向过程的结构化编程语言
  • 语法上:
    C++具有封装、继承和多态三种特性
    C++相比C,增加了许多类型安全的功能,如强制转化
    C++支持范式编程,如模板类、函数模板等。

说一下C++中static关键字的作用

将设计模式中的某个模式应用到自己的这个项目中

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值