C#的入门
转自擅码网并修改
文章目录
1.第一个C#程序"Hello world!"
程序代码如下:
using System;
namespace ConsoleApp2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello world!");
Console.ReadKey();
}
}
}
相比于C的主函数格式int main (void)
与int main (int argc,char** argv[])
;C#的主函数格式则为: static void Main(string[] args)
同时,C#比起C语言的两种注释方法:
//单行注释
/*
多行注释
*/
更多了一种:/// XML注释,如下所示
///<summary>
///两数相加
///</summary>
///<param name="a">第一个整数</param>
///<param name="b">第二个整数</param>
///<returns><returns>
static int twoAdd(int a,int b)
{
return a + b;
}
- 两个
///<summary>
之间是关于这个函数或者方法的解释 - 下行
///<param name="参数名">自定义注释,可不填</param>
是参数注释 - 最后一行,需要作者在
<returns>
中间填写返回的值的意义 - 在VS2019只需输入///,将自动产生XML注释,关于XML注释到后面再细讲
2.数据类型
2.1 C#的变量命名
规则:
在C的基础上(用英文或_开头,不能以数字开头),多出一项:可以以@符号开头
【_ @ 开头暂时不要用,它们有自己特殊的意义】;
注意事项:
与C一样,C#也区分英文的大小写;变量名不能与关键字重复。
规范:
Camel:变量名首单词的首字母小写,其余每个单词首字母单词大写,多用于给变量或字段或方法参数命名。 eg:highSchoolStudent;
2.2 C#的内置数据类型
C#相比于C,有了更多的内置数据类型。并且与C不同的是,由于微软有关于C#的标准规定,所以不会产生随系统的改变而改变数据类型。
这些是C#中已经规定的简单数据类型,其中uint、ushort、ulong和sbyte 为无符号整型,bool无法进行简单运算。
类型名 | 范围 | 大小 |
---|---|---|
bool | true 或 false | 1字节 |
int | -2,147,483,648 到 2,147,483,647 | 4字节 |
byte | -128到127 | 1字节 |
long | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 8字节 |
short | -32,768 到 32,767 | 2字节 |
float | ±1.5 x 10−45 至 ±3.4 x 1038 | 4字节 |
double | ±5.0 × 10−324 到 ±1.7 × 10308 | 8字节 |
decimal | ±1.0 x 10-28 至 ±7.9228 x 1028 | 16字节 |
char | U+0000 到 U+FFFF | 2字节 |
uint | 0 到 4,294,967,295 | 4字节 |
sbyte | 0到255 | 1字节 |
ulong | 0 到 18,446,744,073,709,551,615 | 8字节 |
ushort | 0 到 65,535 | 2字节 |
注意:特别是,bool 类型是不同的,并且不同于整型,bool 不能用于替代整型数值,反之亦然。
除此之外,C#还具有 object 和 string 两个内置引用类型,一个是对象类型,一个是字符串类型。
具体说明请查看微软的:msdn.
2.3 C#的变量的未初始化和使用
与C不同,C#的局部变量在定义后如果初始化,所有的C#编译器是不会随机赋值给它。
C#的局部变量在可以获取变量的值之前,必须对其进行明确赋值 (明确赋值)。
初始化不是在声明变量时进行的,编译器会通过检查所有可能的路径,如果检测到局部变量在初始化之前就使用了它的值,编译器会报错 错误代码 CS0165。反之声明变量时没初始化变量,但在之后未使用它,编译器不会报错。
3.运算符与表达式
3.1 “=”与“+”运算符
1.“=”运算符
与C一样,用作赋值,将等号右边的值,赋值给左边的变量
注意:编译器默认把不带f或F的浮点数常量视为double,所以给float赋值时要使用带f或F的浮点数。
2.“+”运算符
- 连接:[当“+”号两边有一边是字符串的时候, “+” 号就起到连接的作用]
string a = "憨憨";
string b = "皮皮虾";
char c = '3';
int d = 1;
a += b;//a为"憨憨皮皮虾"
a += c;//a为"憨憨皮皮虾3"
a += d;//a为"憨憨皮皮虾31"
- 相加:[当“+”号两边都是数字的时候,“+” 号就起到相加的作用]
3.2 算术运算符
- “+ - * / ”
加减乘除四则运算,结合性与优先级和C的一样
注意:与C一样,当 / 运算符有左右有一个值是浮点数,则结果为浮点数。
- %运算符
取余运算
注意:与C一样,%左右的值必须为整型数值。
3.3 ++与 - -
自增与自减,与C自增和自减的作用一样。
3.4 复合赋值运算符
有+= 、-= 、*= 、/= 、%=五个复合运算符,作用与C的一样。
3.5 关系运算符
有 > < >= <= == != 六种关系运算符
概念:关系运算符是用来描述两个事物之间的关系。用法与C相同。
由关系运算符连接的表达式称之为“关系表达式”。
注意:关系表达式的运算结果是bool 类型。
3.6 逻辑运算符
有&&与、||或、!非三种逻辑运算符。
注意:逻辑运算符两边放的一般都是 关系表达式 或 bool 类型的值;
注意:逻辑表达式的结果同样也是 bool 类型。
3.7 占位符
作用:让字符串与变量连接更舒服。
格式:{n},代表输出第n-1个参数,且n不能大于参数个数,n-1>0
例子如下:
Console.WriteLine("今天是{0}年{1}月{2}日",2020,1,12);
3.8 表达式
表达式是由一个或多个操作数以及零个或多个运算符组成的序列,其计算结果为一个值、对象、方法或命名空间 。 表达式可以包含文本值、方法调用、运算符及其操作数,或简单名称 。
简单名称可以是变量名、类型成员名、方法参数名、命名空间名或类型名。
4.用户输入与转义字符
4.1用户输入
函数:Console.ReadLine();
string name;
name = Console.ReadLine();
4.2转义字符
概念: 指的是一个 \ 加上 一个特殊的字符,组成了一个具有特殊意义的字符。
C#有以下几个转义字符:
转义字符 | 作用 |
---|---|
\’ | 单引号 |
\" | 双引号 |
\\ | 反斜杠 |
\0 | 空 |
\a | 警告(产生峰鸣) |
\b | 退格 |
\f | 换页 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
@与转义字符的关系 :用在字符串的前面起到两个作用
①取消“\”在字符串中的转义作用,使其单纯的就表示一个斜线——[多用于硬盘路径]
string name = @"D:\Game_R&D";
Console.WriteLine(@"D:\Game_R&D");
不加@,编译会提示无法识别的转义序列。
②字符串中的 " 要用 “” 表示
string str = @"aaa=""bbb""";
// 不加 @,可以写成
string str2 = "aaa=\"bbb\"";
5.类型转换与常量
5.1 类型转换
更加详细的查看内置数值转换(C# 参考)
- 隐式类型转换 [ 自动类型转换 ]
由于这种转换是类型安全且不会导致数据丢失,因此无需使用特殊语法。
示例包括:从较小整数类型到较大整数类型的转换以及从派生类到基类的转换。
从 int、uint、long 或 ulong 到 float 的隐式转换以及从 long 或 ulong 到 double的隐式转换可能会丢失精准率,但绝不会丢失一个数量级。 其他隐式数值转换不会丢失任何信息
条件1:两种类型兼容
条件2:原类型要小于目标类型(小范围的转大范围的)
例:int->double int->float
int a = 10;
double b;
b = a;
- 显式类型转换 [ 强制类型转换 ]
条件1:两种类型兼容
条件2:原类型大于目标类型(大范围的转小范围的)
例:double->int float->int
注意:显式类型转换可能会导致数据丢失或引发异常
double d = 5673.74;
int i;
// 强制转换 double 为 int
i = (int)d;//i的值为5673
- 表达式中的类型转换
有以下几种情况:
1.如果是相同类型进行简单运算,则大部分情况下不会进行类型升级
注:short与char除外,这两种数据类型会自动升级为int类型,再进行运算
2.如果表达式中最大范围的操作数为X类型,则整个表达式的其它数据类型提升为X类型再进行运算。
与C不同的是:C#中如果表达式中最大范围的操作数为float,float在表达式中并不会升为double。
注:
- 类似ulong和long、ushort和short、int和uint、sbyte与byte之间进行简单运算时不会自动升级,编译会报错。
例如:错误 CS0034 运算符“+”对于“long”和“ulong”类型的操作数具有二义性- 类似 ulong 与 ushort 或 uint 与 byte 这样的就通过编译。
- Convert类型转换
概念:
两个变量的类型如果相互兼容,可以使用 自动类型转换 或 强制类型转换。
如果两个变量的类型不兼容,比如 string 和 int ,string 和 double 。
这个时候我们就可以用 Convert 进行转换。
double a;
//由于Console.ReadLine()返回的是string类型的数据,所以要用Convert类型转换
a = Convert.ToDouble(Console.ReadLine());
5.2 常量
1.概念: 一个不能变化的量,常量一旦声明,就不可以再重新赋值。
2.命名规范: 常量名的命名一般是全大写,单词与单词之间用下划线分割。
3.语法: const 数据类型常量名 = 常数值;
6.分支结构之if-else语句
更加详细的请看 微软的官方文档
6.1与C的不同点:
C# 的if语句里的“判断条件” 只能是bool 类型的值,哪怕是关系表达式的返回值也是bool类型。而C的if语句的“判断条件”只看值,不管数据类型。
在C中不内置bool类型,直到C99才定义了bool类型,使用需要引入头文件<stdbool.h>
//C中可以成立
while ("hh")
{
printf("hanhan");
}
6.1与C的相同点:
- if语句的写法与C的一样
- if-else-if 里的else也可以省略
- if语句的执行顺序一样,先判断,成立的话执行,不成立的话跳过。
7.分支结构之switch-case语句
更加详细的请看 微软的官方文档
7.1与C的不同点:
若是对应case 中有代码段,在C中允许省略掉该 case 的 break,然后会继续向下执行直到结束或遇到 break 为止。
int Num=5;
switch (Num)
{
case 1:
System.out.println("one");
case 10:
System.out.println("ten");
default:
System.out.println("other");
}
而在C#中,若对应 case 中有代码段,则不可以省略该 case 的 break,否则编译器会报错:
//此为错误示例
int Num = 10;
switch (Num)
{
case 1:
Console.WriteLine("one");
case 10:
Console.WriteLine("ten");
default:
Console.WriteLine("other");
}
在C#中,default 不管有没有代码段,都不可以省略 break ,否则编译器报错如下
总之,C# 禁止当一个 switch 部分执行完后,再继续执行到下一个 switch 部分。 编译器会报错:错误 CS0163:“控制不能从一个 case 标签(<case 标签 >)贯穿到另一个 case 标签。”
通常通过使用 break、goto 或 return 语句显式退出开关部分来满足此要求。 不过,以下代码也有效,因为它确保程序控制权无法贯穿到 default switch 部分。
switch (caseSwitch)
{
case 1:
Console.WriteLine("Case 1...");
break;
case 2:
case 3:
Console.WriteLine("... and/or Case 2");
break;
case 4:
while (true)
Console.WriteLine("Endless looping. . . ."); //死循环,不会执行到下一个switch部分
default:
Console.WriteLine("Default value...");
break;
}
7.2与C的相同点:
- 当对应 case 里没有代码段时,可以将 break 省略。编译通过。此时,case 1 和 case 2为一个 switch 部分。
int Num = 10;
switch (Num)
{
case 1:
case 10:
default:
Console.WriteLine("other");
break;
}
- 可以不写 default 如果匹配表达式与其他任何 case 标签都不匹配,此时没有 default ,程序流就会贯穿 switch 语句。
8.迭代语句之for语句
详细的查看微软的C#文档
在指定的布尔表达式的计算结果为 true 时,for 语句会执行一条语句或一个语句块。
在 for 语句块中的任何点上,可以使用 break 语句中断循环,或者可以使用 continue 语句继续执行到循环中的下一次迭代。 还可以使用 goto、return 或 throw 语句退出 for 循环。
在C#中,for语句的四个部分也都可以为空,就像C一样。
for ( ; ; )
{
// Body of the loop.
}
//或者下面这种形式,无限循环
for ( ; ; )
;
“初始化表达式”部分
“初始化表达式”部分的语句仅在进入循环前执行一次 。
- 本地循环变量的声明和初始化,不能从循环外访问。
- 以下列表中显示用逗号分隔的零个或多个语句表达式:
赋值语句、方法的调用、自增或自减、通过使用 new 运算符来创建对象、await 表达式
“条件”部分
“条件”部分(如果存在)必须为布尔表达式 。 在每次循环迭代前计算该表达式。 如果“条件”部分不存在或者布尔表达式的计算结果为 true,则执行下一个循环迭代;否则退出循环 。
“迭代器”部分
“迭代器”部分定义循环主体每次迭代后将执行的操作 。 “迭代器”部分包含用逗号分隔的零个或多个以下语句表达式 :
赋值语句、方法的调用、自增或自减、通过使用 new 运算符来创建对象、await 表达式
9.迭代语句之while语句
详细的可查看微软的C#文档
1.同样 while 语句的判断条件必须为布尔表达式,且不可省略。
2.在指定的布尔表达式的计算结果为 true 时,while 语句会执行一条语句或一个语句块。 由于在每次执行循环之前都会计算此表达式,所以 while 循环会执行零次或多次。 这不同于 do 循环,该循环执行一次或多次。
3.在 while 语句块中的任何位置,都可使用 break 语句中断循环。
4.可通过使用 continue 语句直接步入 while 表达式的判断条件。 如果表达式计算结果为 true,则继续执行循环中的第一个语句。 否则,将在循环后的第一个语句处继续执行。
5.还可以使用 goto、return 或 throw 语句退出 while 循环。
6.while 的循环代码段只有一句时可以写成下面这种样子:
while (ture)
Console.Writeline("123");
10.迭代语句之do-while语句
1.同样 do-while 语句的判断条件必须为布尔表达式,且不可省略。
2.在指定的布尔表达式的计算结果为 true 时,do 语句会执行一条语句或一个语句块。 由于在每次执行循环之后都会计算此表达式,所以 do-while 循环会执行一次或多次。 这不同于 while 循环(该循环执行零次或多次)。
3.在 do 语句块中的任何点,都可使用 break 语句中断循环。
4.可通过使用 continue 语句直接步入 while 表达式的判断条件。 如果表达式计算结果为 true,则继续执行循环中的第一个语句。 否则,将在循环后的第一个语句处继续执行。
5.还可以使用 goto、return 或 throw 语句退出 do-while 循环。
6.do-while 的循环代码段只有一句时可以写成下面这种样子,不过并不建议:
do
Console.WriteLine(" ");
while (true);
11.数组之基本语法
详细的可查看微软的C#文档
11.1.数组的概念
概念:
数组是一个存储相同类型元素的固定大小的顺序集合。
数组类型是引用类型,声明数组变量只是为引用数组实例预留空间。 实际的数组实例是在运行时使用 new 运算符动态创建而成。 new 运算指定了新数组实例的长度,然后在此实例的生存期内固定使用这个长度。 数组元素的索引介于 0 到 Length - 1 之间。 new 运算符自动将数组元素初始化为其默认值(例如,所有数值类型的默认值为 0,所有引用类型的默认值为 null)。
11.2.数组的语法
1.声明
声明格式:数据类型[] 数组名;
如:int[] intArray; double[] doubleArray;
[] :指定数组的维度,默认是1 维数组。
多维数组的声明和初始化结合如下:
int[] a1 = new int[10];//一维
int[,] a2 = new int[10, 5];//二维
int[,,] a3 = new int[10, 5, 2];//三维
//从左开始依次为一维、二维、三维
2.初始化
初始化格式:数组名= new 数据类型[数组长度];
声明,初始化二合一格式:数据类型[] 数组名= new 数据类型[数组长度];
new : 创建数组实例。
注:左右两端的数据类型要一致。
11.3.赋值
赋值格式:数组名[下标] = 值;
声明,初始化,赋值三合一格式:
数据类型[] 数组名= new 数据类型[数组长度]{值1,值2,…,值N};
数据类型[] 数组名= new 数据类型[]{值1,值2,值3};
只要当数组长度确定为n且(n!=0),并且在声明和初始化的同时对数组进行赋值的时候,必须赋值相同个数。 否则就像下面这行代码,编译会不通过,会报错。
int[] shuzu = new int[10] {1,2,3,4,5,6,7,8,9};
//确定了数组长度n=10,却只赋值了9个
//编辑器报错:错误 CS0847 应为一个长度为“10”的数组初始值设定项
若数组长度不确定,则可以写成下面两种形式:
int[] a = new int[] {1, 2, 3};
int[] a = {1, 2, 3};
11.4.使用[取值]
使用格式:数组名[下标]
注意:
- 数组的下标是从0 开始。
- 数组的长度是固定,赋值与取值的时候,下标不能越界。
12.数组之元素遍历
12.1 for 循环遍历数组
数组长度获取:数组名.length;
for (int n = 0; n < 数组名.length; n++)
{
}
12.2 foreach 遍历数组
foreach(数组的数据类型 临时变量的标识符 in 数组名)
{
Console.WriteLine(临时变量);
}
string[] shengxiao = new string[]{ "子鼠","丑牛","寅虎", "卯兔", "辰龙" };
string tmp = "";
foreach (string n in shengxiao)
{
tmp += n;//foreach会将该数组当前循环的元素存入临时变量之中方便使用
}
注意:
- 在 foreach 语句中,类型和标识符都是必需的。
- foreach会将该数组当前循环的元素存入临时变量之中方便使用;
12.3 数组元素的初始值
数组的初始化:数据类型[] 数组名= new 数据类型[数组长度];
new 运算符自动将数组元素初始化为其默认值,可以在数组初始化后用遍历一个个输出查看。
各种类型的数组的默认值为:
- int[], 元素的值默认都是0;
- float[], 元素的值默认都是0;
- double[],元素的值默认都是0.0;
- string[], 元素的值默认都是null;
- bool[], 元素的值默认都是false;
13.数组之内存结构
13.1 值类型与引用类型
1.值类型
int,float,double,bool,char
内存:值类型的值存储在内存的栈中
演示:int 类型变量间传值
int a = 10;
int b = a;
注意:这个时候单独修改b 的值,a 的值不会发生改变,这种传递叫做值传递。 这个时候变量之间的传递就是拷贝一个具体的值给对方。
2.引用类型
数组,字符串
内存:引用类型的值存储在内存的堆中,而储存这个值的地址则储存在栈中。
演示:int 数组类型变量间传值
int[] intA = new int[]{00,111,222,444};
int[] intB = intA;
注意:这个时候单独修改intB 中元素的值,intA 的值是会发生改变,这种传递 叫做引用传递。
这个时候 变量之间的传递 就是 拷贝一个地址给对方。
13.2 数组的内存结构
1.数组在内存中是一块连续的存储空间存储的。
2.最低的地址对应第一个元素,最高的地址对应最后一个元素。
14.数组之二维数组
14.1 二维数组基本语法
1.声明与初始化
声明: 数据类型[,] 数组名;
初始化:数组名= new 数据类型[行数,列数];
声明,初始化二合一格式:
数据类型[,] 数组名= new 数据类型[行数,列数];
2.赋值与取值
赋值: 数组名[下标,下标] = 值;
取值: 数组名[下标,下标];
3.声明初始化赋值三合一:
数据类型[,] 数组名= new 数据类型[,]{
{0,0,0,0,0,0,0,0,0,0},
{1,1,1,1,1,1,1,1,1,1}
};
14.2 二维数组的遍历
遍历方法:for 语句的循环嵌套
循环次数的获取:数组名.GetLength(轴向下标); //获取某个轴向的元素总数。
轴向下标从0开始,依次表示第一维、第二维……
15.函数之基本语法
15.1 函数的概念
一段具有特定功能的代码,有自己的标识符,通过标识符可以重复调用这段代码来帮我们完成特定的事情。可以自己定义。
15.2 静态构造函数的声明与调用
1. Pascal 命名法
格式:每个单词的首字母都要大写其余小写,多用于类或方法。
eg:int HighSchoolStudent = 0;
2. 函数的声明
修饰符 返回值类型函数名(函数参数)//两个以上用,隔开,要具有参数类型
{
函数代码段;
}
格式说明:
修饰符: 这个决定了变量或函数对于另一个类的可见性。
返回值类型: 如果不需要返回值就写void,就是无返回值的意思;
函数名: 当前功能函数的名字,要符合Pascal 命名规范;
参数列表: 如果不需要参数,小括号中可以直接留空;如果需要参数,格式为:参数类型 参数名;
例子:
static Bus(int routeNum)
{
Console.WriteLine("Bus #{0} is created.", routeNum);
}
3. 函数的调用
调用形式:
-
函数名([实际参数]);
-
变量或常量.函数名([实际参数]);
自定义方法在Program中,实例成员要实例化对象【对象.方法】之后才能调用
自定义方法在Program中,静态成员可以直接调用函数,不需要【类名.方法】后才能调用
自定义方法在自己创建的类中,实例成员要实例化对象【对象.方法】之后才能调用
自定义方法在自己创建的类中,静态成员要用【类名.方法】之后才能调用
static void Main(string[] args)
{
string name = "hh";
TextOne(12);//第一种调用
name = "hh".ToUpper();//第二种调用,函数返回"HH"
Console.ReadKey();
}
static void TextOne(int n)
{
}
注意:
- 如果函数只声明不调用,则函数中的代码不会被执行。
- 与C不一样的是,C#函数不要求在主函数的位置前声明函数。
15.3 函数的参数与返回值
1.形参与实参
形参: 形式参数,在定义函数的时候,在参数列表中定义的参数。
实参: 实际参数,在调用函数的时候,传递给函数的具体值(可能是地址或者是数字)。
对于 static 函数,可以直接在函数调用的时候将对应数据类型的值填入括号内。
对于 public 函数无法实现。
2.返回值
格式: return 与函数返回值类型相同的表达式
作用:
- 在函数中返回要返回的值;
- 立即结束函数;
注: 若函数无返回值,则 return 可以省略,或者写成 return ;,return后不可接表达式。
16.函数之函数重载
16.1 何为重载?
函数的名称相同,但是参数列表不同。
调用该函数的时候,会根据不用的参数,自动选择合适的函数重载形式。
例:定义一个Add 的方法,实现整数,双精度浮点数相加操作。
static void Main(string[] args)
{
Add (1,2);
}
static int Add (int a,int b)
{
return a + b;
}
static double Add (double a, double b)
{
return a + b;
}
static double Add (double a, int b)
{
return a + b;
}
16.2 函数重载参数的情况
函数重载的参数个数和类型不能完全相同。
注意:函数的返回值和重载没有关系。
17.函数之高级参数
17.1 函数参数传递
调用函数,会将函数加载到栈空间中,传递的值,是直接拷贝了变量储存的值再传递,也就是 “值传递” 。
17.2 ref 参数
1.作用
使外部变量的地址传入函数中,从而在函数中改变外部变量的值。也就是"引用传递"。
2.要求
调用函数前,必须在函数外为传入函数的外部变量赋值。在函数中赋值与否都可以。
3.语法
形参和实参前面都要加 ref 关键字。
int num = 9;
//调用函数的时候实参前 + ref
Add(ref num);
//声明函数的时候形参前 + ref
static void Add(ref int num);
{
num += 10;
}
17.3 out 参数
1.作用
函数中如果返回多个值。
2.要求
函数外可以不为 out 变量赋值,而函数内必须为其赋值。
3.语法
形参和实参前面都要加 out 关键字。
int num = 10;
int a,b;
b = 10;
//调用函数的时候实参前 + out
Add(num,out a,out b);
//声明函数的时候形参前 + out
static void Add(int num,out int a,out int b);
{
num += 10;
a = num/2;
b = num*num;
}
17.4 params参数
后续更新
18.函数(方法)之递归调用
18.1 何为递归?
递归算法:一种在函数内 直接或者间接 地 调用自身函数或者方法的算法。
18.2 递归要点
- 递归时函数会一直调用自己,直到满足某个特定条件 才一层层结束。
(所以递归要有一个结束条件 来 return) - 递归调用时应该会传递一些参数,而且每次调用都会传递一个新的参数。
18.3 递归的优缺点
优点:
- 代码简洁
- 在树的前序,中序,后序遍历算法中,递归的实现明显要比循环简单得多。
缺点:
- 递归由于是函数调用自身,而函数调用是有时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址以及临时变量,而往栈中压入数据和弹出数据都需要时间。->效率
- 递归中很多计算都是重复的,由于其本质是把一个问题分解成两个或者多个小问题,多个小问题存在相互重叠的部分,则存在重复计算,如fibonacci斐波那契数列的递归实现。->效率
- 调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。->性能