本节内容
1.方法的由来;
2.方法的定义与调用;
3.构造器(一种特殊的方法);
4.方法的重载(Override);
5.如何对方法进行debug;
6.方法的调用与栈*
*推荐书目:CLR via C#和C# in Depth, 3rd Edition
1.方法的由来
①方法(method)的前身是C/C++语言的函数(function)
方法是面向对象范畴的概念,在非面向对象语言中仍然称为函数。
可以使用C/C++语言做对比。
*当函数以成员的身份出现时我们就叫它方法(始于C++)
②永远都是类(或结构体)的成员
C#语言中函数不可能独立于类(或结构体之外)。
只有作为类(或结构体)的成员时才被称为方法。
而在C++中是可以的,称为“全局函数”。
③是类(结构体)最基本的成员之一
最基本的成员只有两个——字段与方法(成员变量与成员方法),本质还是数据+算法
方法表示类(或结构体)“能做什么事情”。
④为什么需要方法和函数
目的1:隐藏复杂的结构;
目的2:复用(resue,重用);
C++中#include<iostream>
#include "Student.h"//如果是标准类库的话用尖括号,如果是自己定义的则用引号。
2.方法的声明与调用
①声明方法的语法详解
参见C#语言文档(声明/定义不分家);
Parameter全称为”formal parameter”;
形式上的参数,简称”形参”:形式参数参与构成算法的算法逻辑(即在算法中作为变量参与计算);
Parameter是一种变量:形式参数变量;
②方法的命名规范:
大小写规范:pascal法则(每个单词首字母大写);
需要以动词或者动词短语作为名字;
③重温静态(static)方法和实例方法:静态方法是隶属于类的,类的对象不能调用;实例方法(非静态方法)隶属于类的对象,对象可以调用。
④调用方法:方法名后面的圆括号不能省略。
声明函数时”()”里面的是形参;调用函数时”()”里面是实参。
Argument中文C#文档的官方译法为”实际参数”简称”实参”可理解为调用方法时的真实条件。
调用方法时的Argument列表要与定义方法时的parameter列表相匹配,C#是强类型语言,argument是值,parameter是变量,值与变量(个数与数据类型)一定要匹配不然会报错。
3.构造器
构造器(constructor)是类型的成员之一;
狭义的构造器是指“实例构造器”(instance constractor);
如何调用构造器;
声明构造器;
构造器的内存原理;
构造函数的作用为构造类的实例(对象)并初始化。
①当调用默认构造函数时:
如语句:Student stu=new Student();①
Class Student
{
Public int ID;
Public string Name;
}
*由于①语句是在main函数了的,stu引用变量为局部变量,先在栈中分配4个字节的空闲内存;*new操作符创建了Student类的一个实例,通过调用Student();默认构造函数,在堆内存中开辟一块空内存大小为该实例所包含的数据之和的空间存放实例,(int ID;4个字节,string name4个字节)所以分配8个字节的内存空间,由于默认构造函数默认赋值为0,故该块内存里的数据初始值为0;*最后将该内存的首地址转化为2进制数,复制到引用变量stu的栈内存当中。
②当调用有参构造函数时:
第一步和第三步操作与调用默认构造函数时相同;不同之处在于第二步:
&当参数为数值类型时:首先在堆内存上这8字节的实例内存空间里,int类型占4个字节,传进来的实参值为1(假设)则把1(都要转化为2进制)转化为2进制存在该内存里;
&当参数为引用类型时(记住凡是类类型都是引用类型):剩下的4个字节是string类型的存储空间,(假设传进来的实参为”it’OK”)
由于string是类类型的,而类类型是引用类型,所以,这4个字节里面存放的只是引用变量,还要再去堆内存中找一块位置存放实参”it’OK”。找到内存空间后把”it’sOK”这个字符串数据转化为2进制数据存入其中。(最后该内存首地址转化为2进制存在string类型栈内存中与上面一致)
4.方法的重载(Overload)
①调用重载方法的示例;
②声明带有重载的方法
方法签名(method signature)由方法的名称、类型形参(讲泛型的时候会遇到)的个数和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用(ref)或输出(out))组成。方法签名不包含返回值。(主要看方法名和参数列表)
实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成;
重载决策(到底调用哪一个重载):用于在给定了参数列表和一组候选函数成员的情况下、选择一个最佳函数成员来实施调用。
5.如何对方法进行debug(核心技能)
设置断点(breakpoint);
观察方法调用时的call stack;
Step-in,Step-over,Step-out;
观察局部变量的值与变化。(可以通过局部变量窗口观察,还可以把鼠标放在变量上观察,也可以把鼠标放在变量上出现的小窗口固定住来观察)
调用栈(call stack)和内存栈的关系:比如递归,不断的调用自己,直到有一个停止的值,若没有,则调用栈就会越来越深直到把内存栈占爆了,这时候就stackOverflow(栈溢出)
Step-in(F11)(逐语句):进入方法当中,最细腻的debug手段
Step-over(F10)(逐过程):不走进方法直接跳过去,比较粗犷一些的debug手段
一般为两者结合使用,先用F10快速的大范围的排查是哪个方法出现问题再使用F11走进方法一步一步debug。(怀疑有bug的地方就F11仔细的排查)
Step-out(跳出):作用为跳转到调用断点当前方法的类或方法里面(按顺序)例如3调用2,2调用1,则在1中设置断点,第一次按Shift F11跳到2处,再按一次跳转到3处。
*综合使用这三种方法会使我们具有强大的debug能力。
6.方法的调用与栈
①方法调用时栈内存的分配
对stack frame(一个方法在被调用过程中栈内的布局)的分析
*栈内存由高字节位向低字节位发展,发展到最低限度之后就会Overflow,
例如C#fun中的main函数输出语句依次调用三个方法,调用时,main方法先在栈内存里开辟一个内存空间叫stack frame(栈帧)。
方法的调用关系有调用和被调用,调用者叫做Caller,被调用者叫做Callee,那么问题来了比如main函数中调用其他函数: Console.WriteLine(c.GetConeVolume(100, 100));那么传进去的两个参数(100)哪个函数来管理呢,不同的语言规定不同,C#是按C++的标准,参数归主调者(caller)管,就是这里的main管。在栈内存中分别开辟参数相应类型大小的空间,先压哪个参数进去呢?不同的语言规定不同,C#遵从C++的规则先左后右,谁调用谁负责把参数压进栈里故该片内存也是由main管(也是main函数的stack frame),*即方法运行时都先在栈内开辟stack frame内存空间 再把方法内包含的数据都压进该片栈内存中。现在开始方法的调用,遵循谁调用谁压栈的原则,即使方法内无局部变量该方法也会有stack frame(存放方法返回值地址等),方法的返回值是存在cpu的寄存器(学汇编时有讲到)里面的。
*一个方法运行完的出结果返回了值之后,相应的stack frame(栈帧)就会被清空,相应的函数(方法)调用结束相关参数没有用了其在栈帧中的内存也会被清空。(可以联想汇编语言的压栈与出栈)
*这里也可以解释一下overflow,就是因为异常方法一直调用,一直
开辟栈帧,没有一层层的返回,相应参数只进栈不出栈,直至开辟空间到栈顶造成“栈溢出”这就是栈溢出的原理。