Csharp笔记之结构化编程基础

变量、数据类型与表达式

“给变量赋值”是什么意思


这里我有个问题,老师说C#使用的是虚拟内存,那么C#不会用内存条,而是用硬盘里的虚拟内存???这显然是不对的,老师这里只是提了会用到虚拟内存,但我觉得一定会用到内存条。因为虚拟内存是在硬盘里的,程序只在虚拟内存中运行岂不是很变慢?设计者一定不会这么干!以前我一直以为声明变量都在内存条中申请的,但实际和虚拟内存有关系,至于是什么关系。我觉得这是我对内存条和虚拟内存之间的关系和观念模糊导致的。看了一下百度的解释,很模糊。现在就单纯的认为C#声明变量用到了内存条和虚拟内存吧。(在win8系统中,玩游戏时偶尔会出现内存不足的提示,但实际内存条才用到60%,他说的内存不足,应该是虚拟内存不足吧。后来升了win10就没出现过这个情况了)
网上查这方面的解释,很少,这篇文章也只是稍微提一下。

变量之间的相互赋值该如何理解

数据类型

数据类型分类


注意:
1、string是关键字,不可以设为变量名称,但如果要使用C#中的关键字作为变量名称,必须添加前缀@
2、说到@,也提一下@的字符串用法:字符串前使用@,可以一字不变的指定字符串,如:@”E:\MyStudy\C#”这就比”E:\MyStudy\C#“好的多了)
3、string是引用类型

C#中的常见数据类型

数有范围

这里需要特别注意一下数的范围,尤其是开发像计算器这样的软件。
因为计算机能表示的数是有范围的。

看看下面的代码:

double i = 0.0001;
double j = 0.000100000000000001;
Console.WriteLine(i==j);//输出:true

计算机不能精确地表达较大的浮点数,因此,当需要比较两个浮点数是否相等时,应该比较其差的绝对值是否在某个允许范围之内即可,无法做到像数学那样的精确比较。如:

if (Math.Abs(i - j) < 1e-10)//10的负十次方
    Console.WriteLine("true");
else
    Console.WriteLine("false");

好在.NET在4.0以上版本中提供了一个BigInteger类,支持大整数的加减乘除运算。

但还是要用绝对值判断的范围的方法缩小误差。

获取数据类型

使用变量.GetType()方法可以获取变量的类型;
使用typeof运算符关键字可以检测特定的变量是否是特定的类型。如:

int intValue = 100;
Console.WriteLine(intValue.GetType());//System.INt32
Console.WriteLIne(IntValue.GetType() == typeof(int));//true

值类型和引用类型

参考这篇文章
注意:值类型不一定分配在栈上,比如引用类型对象里的值类型字段,还有被lambda表达式捕获的值类型变量。

String与string的区别

在C#中,定义字符串可以混用“String”或“string”,因为在编译时,C#会将他们都对应到.NET基类库所定义的标准“System.String”类型。但是习惯上,把字符串当作对象时(有值的对象实体),我们用string。而我们把它当类时(需要字符串类中定义的方法),我们用String,比如:string greet = String.Format(“Hello {0}!”, place);
在java中,只有String,没有小写的string。

var类型的使用


其实,在我以前写过的代码中,基本没用过var,第一次接触var我记得是在JavaScript见到的,像phython这样的解释性语言中常见,但我觉得C#作为强类型语言,用var类型,代码的可读性就变差了,即不能直观的看出数据的类型。老师说“主要是省去了数据类型名称太长的情况,这在LINQ中常见”,我没有接触过,所以不好说,等我看到那部分再看看吧。

类型转换

显式(强制)转换和隐式转换很简单,这里主要记一下字符串与数值类型间的转换:

我习惯用两个To:
Convert.ToDouble(str);string—->数值类型
intValue.ToString();数值类型—->string

提示:Convert类中有很多基本数据类型转换的方法。

运算符和表达式


运算符没啥好说,截个图也未必会看,一般的加减乘除,取余数用%就没了。至于i++和++i的问题,其实没必要,只要不把i++放到Console.Write这样的输出语句或者表达式(1+ ++i + i++)中就可以了。
不过也总结一下吧:i++是先赋值后自增,++i是先自增后赋值;这里需要知道i++是什么时候自增,在C#中第一个i++后,同一个表达式中下一个i已经自增过的了,这和C不一样!在C中是在这一语句执行完毕后才自增。如:

int i=3;
int j=(i++)+(i++);
Console.Write("{0},{1}",i,j);

结果是:5,7
但是如果你用c语言可能就是别的答案了。
当然了,其实实际开发中没人会这么干,只有考试会这么出。最常用的for循环语句最后的++i或者i++效果是一样的。

表达式:变量+常量+运算符=表达式;表达式最后一定会是一个值,可以看成是一个变量。

常见转义字符

讲到运算符也截个转义符的图吧

选择结构与逻辑表达式

if/else选择结构


if/else选择语句没啥好说,注意嵌套就可以了

逻辑表达式的组合


运算符表达式也没什么好记的,倒是以前有个&&和&的区别问题;
&&:如果第一个操作数为 false,则不计算第二个操作数;
&:当且仅当两个操作数均为 true 时,其结果才为 true;
||:如果第一个操作数为true,则不计算第二个操作数;
|:当且仅当两个操作数均为 false 时,结果才为 false。

我一般都是&&或||,不用&或|,&&好处在于效率高一些。
其实,平时也不必太注意这个,比如:

int a = 1;
int b = 3;
if (a = 1 && b = 2)
{
    Console.WriteLine("a=1;b=2");
}
else
{
    Console.WriteLine("xxx");
}

结果是xxx。这显然是正确的!可以看到判断两个都为真时,如果第一个就是假的,那么第二个就不必看了!相同的,对于||,当第一个为真就可以判断这个是正确的了。不过,也可能会出现特殊情况吧,我到现在还没遇到过,现在就知道有这么一回事就好。

多分支结构


这里需要注意的是java和C#的switch语句有点不同,C#编辑器要求break一定要有,最好有default;而java是允许不加break的。

循环结构

while循环

while/do:

do/while:

注意:do/while最后有个分号“;”
他们的共同特点是当满足while()里的判断后,一直执行{}内的内容,直到不满足条件结束。不同是do/while先执行后判断;while/do先执行后判断。

for循环

对while和for的选择

当执行次数不定时,使用while较好;
当执行次数固定时,使用for较好。

foreach循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//遍历值类型集合
var IntValue = new List<int>(){1, 2, 3, 4};
foreach(var value in IntValue)//遍历整数集合
{
Console.WriteLine(value);
}

//遍历引用类型集合(对象集合)
var MyClasses = new List<MyClass>();//对象数组
for (int i = 0; i < 5; i++)//实例化五个对象
{
MyClass.Add(new MyClass()
{
Id = i,
Description = "class" + i
});
}
foreach (var obj in MyClasses)//遍历对象集合
{
Console.WriteLine("{0}:{1}",obj,Id,obj.Description);
}

注意:使用foreach遍历数据集合时,不要向集合中增删数据。因为,这会让数据集合的个数不稳定,产生错误。

for和foreach的区别

1、foreach查询更方便,即不需要知道集合内的元素个数,就可以很方便的遍历每个元素。
2、foreach只能查询不能改变;for既能查询又能改变 但是必须知道长度。所以不定集合建议用foreach。
3、C#中foreach效率比for更高一些,在查询时建议用foreach。

break和continue

break:提前结束循环,后面还没有执行的循环也不再执行。
continue:提前结束循环(本轮循环中的还没没有执行的代码不再执行),后面的循环继续执行。

死循环



做单片机的就喜欢用for(;;),我看着真不习惯;为什么不用while,多直观。

循环语句的等级性原则

各种类型的循环语句都是等价的,可以把一种类型的循环语句用另一种类型的循环语句替换而不会影响到程序的功能。

控制台编程之键盘输入



这个没啥好说,实际开发应该不会用到控制台吧。下面贴个现实一个检测用户输入键盘信息的代码吧。(主要看一下它是如何一直检测用户是否按下按键的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static void TestKey()
{

Console.WriteLine("请输入任意键,按ESC退出...");
ConsoleKeyInfo key;
do
{
while (!Console.KeyAvailable)
{

}
key = Console.ReadKey(true);
Console.WriteLine("key的值" + key.Key);
Console.WriteLine("keyChar的值" + (int)key.KeyChar);
Console.WriteLine("Modifiers的值" + key.Modifiers);
if (Console.CapsLock)
{
Console.WriteLine("现在是大写");
}
if (Console.NumberLock)
{
Console.WriteLine("Num Lock被按下");
}
if (key.Key == ConsoleKey.A)
{
Console.WriteLine("你按下了A键");
}
if (key.Modifiers != 0)//组合键,单独按shift没效果的。
{
if ((key.Modifiers & ConsoleModifiers.Shift) != 0)//这里的&,不是逻辑符号,而是二进制的和;如1和1是1,0和1是0
//其实,if (key.Modifiers == ConsoleModifiers.Shift)这么写也可以。
{
Console.WriteLine("shift被按下");
}
if ((key.Modifiers & ConsoleModifiers.Alt) != 0)
{
Console.WriteLine("alt被按下");
}
if ((key.Modifiers & ConsoleModifiers.Control) != 0)
{
Console.WriteLine("ctrl被按下");
}
}
Console.WriteLine();
}
while (key.Key != ConsoleKey.Escape);
Console.WriteLine("程序已退出,现在按任意键关闭窗口");
}

方法

方法与函数

结构化编程如C,叫函数(function)
面向对象编程如C#,叫方法(mothod)
但是,这个术语经常混用,可以看成是一回事,一般也不会注意细微的差别。
*注意:在C#中,所有方法都必须放到一个类中,不存在完全独立的方法。

形参与实参

形参:定义方法是指明的参数,如:

int Add (int x, int y)
{
    return x+y;
}

括号()里的int x, int y;就是形参,即形式参数。
而这里的return则是方法的返回值。方法名前的int是返回类型

实参:调用方法时,传入的“10”,“30”等,实际的具体的数值是实际参数,简称实参。

方法的重载

定义:在同一个类中,我们可以定义名字一样的方法,只要它们的参数列表不一样就行了,这种语法特性叫“方法的重载”
那么,什么叫参数列表不一样呢?
1、参数个数不一样;2、参数个数相同,但相同位置的参数,其类型不一样。
特别注意:返回值类型不作为方法重载判断的依据;如果出现两个方法名相同,参数类型和个数也一样,而方法的返回值不一样,此时编译器会报错。

.NET类库中的类生成随机数

递归

递归就是自己调用自己。如:

static void funtion()
{
    funtion();
}

但是这个递归显然是有问题的!这会引发堆栈溢出!

堆栈溢出

程序代码其实是由“线程(thread)”负责执行的。
操作系统会在创建线程时,会给每个线程配套一块内存区域,线程可以用这块区域存储一些数据。这块内存区域被称为:线程堆栈
线程堆栈有容量限制,当伊尔线程要保存的数据超过了这个容量时,就发生了“堆栈溢出”
堆栈溢出对编译器的影响就是报错,而如果是生成了可执行文件,运行后,内存就会不断减小,然后程序奔溃。

递归思想

一个递归的算法,会将一个难以处理的“大”问题的“规模”分多次地持续压缩,一直持续到压缩后的问题规模小到可以处理为止。其过程往往体现为代码要处理的数据量或计算量在递归前后不断“减小”

如,一个求阶乘的代码:

1
2
3
4
5
6
7
8
static int NJieChen(int n)
{

if (n == 1)
{
return 1;
}
return (n * NJieChen(n-1));
}

递归的特点

1、先从大到小,再从小到大;
2、每个步骤要干的事情都是类似的,只不过其规模“小一号”
3、必须要注意保证递归调用的过程可以终结,否则,将导致“堆栈溢出”。

递推

递推是递归的逆运算,其实递推才是一般的思路,递归才是逆思维。
即“递推”是从前到后解决问题,“递归”是从后到前,再回来。如:

递归编程的套路

1、每个递归函数的开头一定是判断递归结束条件是否满足的语句(一般是if语句)
2、函数体一定至少有一句是“自己调用自己”的
3、每个递归函数一定有一个控制递归可以终结的变量(通常是作为函数的参数而存在)。每次自己调用自己时,此变量会变化(通常是变小),并传送给被调用的函数。

感受:老师说递归在编程中是重要的方法和思想,很多软件开发中都用到了递归思想,但初学会感觉比较困难。确实,我自己写递归的时候,开始经常出现溢出,还有最关键的就是要找到n与n-1之间的关系才行。其次就是判断依据了。