1. 方法的由来
1.1 方法(Method)的前身是C/C++语言的函数(function)
1.1.1 方法是面向对象范畴的概念,在非面向对象语言中仍然称之为函数
1.1.2 使用C/C++语言做对比
① C语言示例:
#include <stdio.h>
int main()
{
printf("Hello,World!");
}
int 返回值 main 函数名称 ()它的参数表,可以为空 {...} 函数体
double Add(double a,double b)
{
return a+b;
}
int main()
{
double x =3.0;
double y =5.0;
double result=Add(x,y);
printf("%f + %f = %f",a,b,result);
return 0;
}
运行结果:
②C++语言示例
#includ<iostream>
int main()
{
std::cout<<"Hello,World!";
return 0;
}
std:: 就类似System. 访问名称空间里面的类or元素
定义自己的函数:
//这是Source.cpp 源文件
#includ<iostream>
double Add(double a,double b)
{
return a+b;
}
int main()
{
double x=3.0;
double y=5.0;
double result = Add(x,y);
std::cout << x << "+" << y << "=" << result;
return 0;
}
当一个函数,以类的成员身份出来时,它就变成了方法
这就是方法有个别名——成员函数
实例:
添加一个类:
准备两个文件:
输入Student出现两个文件:
Student.h是头文件——对文件的声明
Student.cpp是源文件——对文件的定义
.h的文件放入了头文件夹中,.cpp放入了源文件中
.h file文件夹——对方法的声明:
//这是.h 头文件
#pragma once
class Student
{
public:
Student();
~Student();
void SayHello(); //这里是返回值
}
.cpp文件——对方法的定义
//这是.cpp 源文件
#include "Student.h"
#include<iostream>
Student::Student()
{
}
Student::~Student()
{
}
void Student::SayHello() //两个冒号表示访问成员,访问SayHello,两个括号是参数列表
{ //花括号是函数体
std::cout<<"Hello! I'm a Student!";
}
Student的成员:SayHello Student ~Student
那我们如何访问它的方法?我们返回Source.cpp里面:
先把Student.h引用进来,然后用指针(很像引用变量),再调用。
//这是Source.cpp文件
#include<iostream> //标准的库就用尖括号
#include"Student.h" //自己定义的头文件就用引号
int main()
{
Student *pStu = new Student(); //*p是指针,指向Student实例的指针。
//题外话:有的人说引用变量相当于指针
pStu->SayHello();
return 0;
}
编译之后:
在给Student添加一个方法:让Student也会算加法
//这是.h头文件
#pragma once
class Student
{
public:
Student();
~Student();
void SayHello();
double Add(double a,double b); //注意:这里只有声明,没有定义。
}
然后再回到源文件.cpp中:
//这是.cpp源文件
#include "Student.h"
#include<iostream>
Student::Student()
{
}
Student::~Student()
{
}
void Student::SayHello()
{
std::cout<<"Hello! I'm a Student!";
}
double Student::Add(double a,double b)
{
return a+b;
}
现在回到Source.cpp:
//这是Source.cpp文件
#includ<iostream>
double Add(double a,double b)
{
return a+b;
}
int main()
{
Student *pStu = new Student();
double x=3.0;
double y=5.0;
double result = pStu->Add(x,y); //这里有修改
std::cout << x << "+" << y << "=" << result;
return 0;
}
然后执行一下:
这里Add就不再是个函数,而是Student的一个方法了(私下叫成员函数)。这就是函数向成员进行过渡。
1.2 永远都是类(或结构体)的成员
1.2.1 C#语言中函数不可能独立于类(或结构体)之外
namespace CSharpFun
{
double Add(double a,double b) //这里会报错,因为方法不能独立于名称空间中,需要在类里
{
return a+b;
}
class Program
{
static void Main(string() args)
{
}
}
}
1.2.2 只有作为类(结构体)的成员时才能被称为方法
1.2.3 C++中是可以的,称为“全局函数”
1.3 是类(或结构体)最基本的成员之一
1.3.1 最基本的成员只有两个——字段和方法(成员变量与成员函数),本质还是数据+算法
1.3.2 方法表示类(或结构体)“能做什么事情”
1.4 为什么需要方法和函数
1.4.1 目的1:隐藏复杂的逻辑
1.4.2 目的2:把大算法分解为小算法
1.4.3 目的3:复用(reuse,重用)
1.4.4 示例:计算圆面积、圆柱体积、圆锥体积
class Calculator
{
public double GetCircleArea(double r)
{
return 3.14*r*r;
}
public double GetCylinderVolume(double r,double h)
{
return 3.14*r*r*h;
}
public double GetConeVolume(double r,double h)
{
return 3.14*r*r*h/3;
}
}
复用函数:
public double GetCircleArea(double r)
{
return Math.PI*r*r;
}
public double GetCylinderVolume(double r,double h)
{
return GetCircleArea(r)*h;
}
public double GetConeVolume(double r,double h)
{
return GetCylinderVolume(r,h)/3;
}
}
2. 方法的声明与调用
2.1 声明方法的语法详解
2.1.1 参见C#语言文档(声明/定义不分家)
1. method-declaration:(方法声明)
method-header (函数头) method-body (函数体)
2. method-header:
重要的(红字):返回值,方法名(帕斯卡命名法:每个单词首字母大写GetCircleArea)和后面的圆括号-参数列表(必须要跟括号)
翻译:①特性(高级内容),②方法的修饰符(比较重要),③这个方法可以分开多个部分去写(高级内容),④返回值,⑤方法名,⑥泛型方法(形式参数列表(比较重要)),⑦这个字句是用来约束泛型的
3.method-modifiers:(方法修饰符)
method-modifier(修饰符)
method-modifiers method-modifier (多个修饰符加上一个修饰符)
一个修饰符有哪些:
这些有些可以组合,后续实操中慢慢学习
4.返回类型
type:①结构体、枚举类型;②类、接口等
void就是没有返回值
5.标识符
给方法命名,一定要给动词或者动词短语
6.方法体
①语句块(花括号括起来的一组语句)或
②; 使用;说明语句还没有实现,也叫作抽象方法
实例:
// 原本方法体:
public double GetCircleArea(double r)
{
return Math.PI*r*r;
}
// 下面是说明:
public//有效的修饰符 double//返回值 GetCircleArea//方法成员的名字(double r//形式参数列表(变量 )) //方法头
{ //方法体
return Math.PI*r*r;
}
2.1.2 Parameter全称为“formal parameter” 形式上的参数,简称“形参”
形式参数为什么叫形式:就是形式上要让这个参数在方法中跑得过去。也叫跑龙套
2.1.3 Parameter是一种变量
2.2 方法的命名规范
2.2.1 大小写规范
帕斯卡命名法
2.2.2 需要以动词或者从此短语作为名字
2.3 重温静态(static)方法和实例方法
方法前只有public,那这个方法是隶属于实例的。在主函数中写c.是可以看到GetCircleArea这个类的
如果增加了static(静态),那么这个方法就是静态方法,隶属于类。这样写c.,是看不见这个类的,只有写Calculator. 才能看见。
class program
{
static void Main(string[] args)
{
Calculator c = new Calculator();
double result = Calculator.GetCircleArea(100);
Console.WriteLine(result);
}
}
class Calculator
{
public double GetCircleArea(double r)
{
return Math.PI * r * r;
}
}
运算结果:
2.3 调用方法
调用方法:就是在方法名后面加上圆括号(),然后在括号中写入必要的实际参数。这对圆括号不能省略,这对圆括号在C#语言中叫方法调用操作符
声明方法圆括号中是形式参数(parameter)(声明变量),调用方法圆括号中是实际参数(argument)。
2.3.1 Argument 中文C#文档的官方译法为“实际参数”,简称“实参”,可理解为调用方法时的真实条件
2.3.2 调用方法时的argument列表要与定义方法时的parameter列表相匹配
匹配:数量要匹配,值类型要匹配(比如形参是double,实参也要是double)
C#是强类型语言,argument是值、parameter是变量,值与变量一定要匹配,不然编译器会报错
举例声明方法和调用方法常出现的错误:
正确写法:
class program
{
static void Main(string[] args)
{
Calculator c = new Calculator();
double x = 3.0;
double y = 4.0;
double result = Calculator.GetCylinderVolume(x,y); //正确写法
Console.WriteLine(result);
}
}
class Calculator
{
public static double GetCircleArea(double r) //声明方法
{
return Math.PI * r * r;
}
public static double GetCylinderVolume(double r, double h)
{
return GetCircleArea(r) * h;
}
}
}
错误写法一:写double是在声明变量,不需要。只要写实参就行了,不要带类型。
static void Main(string[] args)
{
Calculator c = new Calculator();
double x = 3.0;
double y = 4.0;
double result = Calculator.GetCylinderVolume(double x,double y); //错误写法
Console.WriteLine(result);
}
错误写法二:
static void Main(string[] args)
{
Calculator c = new Calculator();
double result = Calculator.GetCylinderVolume(double 3.0,double 4.0); //错误写法
Console.WriteLine(result);
}
3. 构造器
3.1 构造器(constructor)是类型的成员之一
说明构造器本身就是一种特殊的函数
构造函数→构造器
成员函数→方法
3.2 狭义的构造器指的是“实例构造器”(instance constructor)
当你跟老程序员提构造器时,他大脑里第一个反应就是狭义的构造器——实例构造器。
3.3 如何调用构造器
namespace C801
{
class program
{
static void Main(string[] args)
{
Student stu = new Student(); //这对()就是在调用他的构造器
}
}
class Student
{
public int ID;
public string Name;
}
}
3.4 声明构造器
当你声明一个类,又没有给他准备构造器的时候,编译器会自动准备一个默认构造器,就是把它们内存当中的字段进行初始化了。
Student stu = new Student();
Console.WriteLine(c.ID); //这里打印出来为0,就是默认构造器
自己设置一个构造器:
构造器组成部分
- 返回值:没有返回值类型,连void都不写
- 函数名:必须与类型名字完全一致
- 参数列表:空的()
- 函数体:{}
class program
{
static void Main(string[] args)
{
Student stu = new Student();
Console.WriteLine(stu.ID);
Console.WriteLine(stu.Name);
}
}
class Student
{
public Student() //自定义构造器
{
this.ID = 1;
this.Name = "No name";
}
public int ID;
public string Name;
}
害怕忘记赋值,所以在创建student实例的时候,强制程序员创建一个ID。就需要写一个带参数的构造函数:
namespace C801
{
class program
{
static void Main(string[] args)
{
Student stu = new Student(2, "Mr.Okay"); //这里要求必须要有参数
Console.WriteLine(stu.ID);
Console.WriteLine(stu.Name);
}
}
class Student
{
public Student(int initId,string initName) //带参数的构造器
{
this.ID = initId;
this.Name = initName;
}
public int ID;
public string Name;
}
}
运行结果:
但有时候我们又需要没有参数的构造器,这时需要再重新写一个新的构造器:
class program
{
static void Main(string[] args)
{
Student stu = new Student(2, "Mr.Okay");
Console.WriteLine(stu.ID);
Console.WriteLine(stu.Name);
Console.WriteLine("=======================");
Student stu2 = new Student(); //这里默认的构造器又回来了
Console.WriteLine(stu2.ID);
Console.WriteLine(stu2.Name);
}
}
class Student
{
public Student(int initId,string initName) //带参数的构造器
{
this.ID = initId;
this.Name = initName;
}
public Student()
{
this.ID = 1;
this.Name = "No Name";
}
public int ID;
public string Name;
}
3.4.1快捷键添加构造器
输入ctor然后敲Tab键,系统就为你准备好了一个构造器
public Student()
{
}
3.5 构造器的内存原理
3.5.1 调用默认构造器
class program
{
static void Main(string[] args)
{
Student stu = new Student();
}
}
class Student
{
public int ID; //整型类型占4个字节
public string Name; //字符串类型,也就是类 类型占4个字节,它里面存储的是实例的地址
}
①stu 局部变量,我们把它分配到栈里
栈内存地址是由比较高的地址往比较低的地址分配
②然后在堆里放student的实例,实例里有两个参数,int 4个字节,string 4个字节。
③构造器进行构造,把int 和string分开。
④把student的地址放到stu引用变量中,也就是30000006的二进制地址。
3.5.2 调用带参数的构造器
4. 方法的重载(Overload)
4.1 调用重载方法的示例
Console.WriteLine(); 可以接受十几种参数,这就是WriteLine重载。也就是说Console中有19个叫WriteLine的方法。但是这19个WriteLine的方法各不相同。
namespace C801
{
class program
{
static void Main(string[] args)
{
Console.WriteLine("Hello"); ; //字符串
Console.WriteLine(100); //整型
Console.WriteLine(200L); //长整型
Console.WriteLine(300D); //double类型
}
}
}
4.2 声明带有重载的方法
4.2.1 方法签名(方法的名字可以一样,但方法的签名不能一样)
方法签名(method signature)由方法的名称、类型形参(老师说以后讲泛型的时候再讲)的个数和它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。方法签名不包含返回类型。
方法签名 | ①名称 | |
②类型形参 | 个数 | |
③形参 | 类型和种类 |
示例:
class Calculator
{
public int Add(int a,int b)
{
return a + b;
}
public int Add(int a,int b,int c) //成功的重载1
{
return a + b + c;
}
public double Add(double x,double y) //成功的重载2
{
return x + y;
}
}
类型形参:就是在方法名后面加一对<>,然后放上参数名(比如说T),意思是这个T以后会在方法中干一些事情。
public int Add(int a,int b)
{
return a + b;
}
public int Add<T>(int a, int b) //T就是类型形参
{
T t; //....
return a + b;
}
参数的种类:
方法里的参数主要有三类:传值参数、传引用参数、输出参数
什么修饰符都不加的参数是传值的。比如Add括号里的int a。
传引用参数:那在int a前面加上ref,就变成传引用参数
public int Add(ref int a, int b)
{
return a + b;
}
输出参数:在int a 前面加out
public int Add(out int a, int b)
{
a = 100;
return a + b;
}
现在还不用关心参数种类和类型形参。
4.2 实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。
这里讲的是之前说的构造器的内容。构造器也可以重载。具体可以回到之前查阅,有相关举例。
4.3 重载决策(到底调用哪一个重载)
用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。
实例:
namespace C801
{
class program
{
static void Main(string[] args)
{
Calculator c = new Calculator();
int x =c.Add(100, 100); //这里就有三个重载可以调用,根据输入的值自动匹配
Console.WriteLine(x);
}
}
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Add( int a, int b,int c)
{
return a + b + c;
}
public double Add(double a,double b)
{
return a + b;
}
}
}
可以更换重载:
static void Main(string[] args)
{
Calculator c = new Calculator();
double x =c.Add(100D, 100D);
Console.WriteLine(x);
}
估计爆一些错误:
小结:方法的重载是工作中很重要的技能,老师希望大家认真学习和练习。
5. 如何对方法进行debug (重要,核心技能)
5.1 设置断点(breakpoint)
断点样式:
使用的代码:
namespace C0904
{
class Program
{
static void Main(string[] args)
{
Calculator c = new Calculator();
double a = Calculator.GetCircleArea(100);
}
}
class Calculator
{
public static double GetCircleArea(double r)
{
return Math.PI * r * r;
}
public double GetCylinderArea(double r,double h)
{
return GetCircleArea(r) * h;
}
public double GetConeVolume(double r,double h)
{
return GetCylinderArea(r,h) * h / 3;
}
}
}
使用开始调试,当红色断点上有个黄色小箭头时,就是程序停在了这一句
5.2 观察方法调用时的call stack(调用堆栈)
在vs右下角调用堆栈里,第一行是我们的断点,双击第二行,显示出谁调用了我们第一行,并用绿色表示。
这时候就可以说,我的call stack深度是2,就是两层。
那么如果把Main函数中求圆面积变成求圆锥体积,那么call stack深度就变成4了。
5.3 Step-in,Step-over,Step-out (三种调试方式)
5.3.1 Step-in 逐语句
按F11,走进我正在被调用的函数里面
这是最仔细的debug方法,每一句都要看。
5.3.2 Step-over 逐过程
按F11,进到第一个函数后,知道这个函数运算没有问题,那么按F10,就会直接跳到把函数结果运算出来的地方(原本还要跳转到求圆柱、圆面积,这样都省略了)
可以用F10先大范围的定位bug在什么地方,然后再用F11来逐句仔细寻找。
逐语句和逐过程往往是交替使用。
5.3.3 Step-out 跳出
当在圆柱中打一句断点,然后又不想跳转到圆面积的函数中,那就用Shift+F11,那就回到了调用它的那层,也就是圆锥体。
这三个加起来形成了强大的Debug能力,老师说希望大家在实践中慢慢摸索,并勤加练习。
5.4 观察局部变量的值与变化
vs左下角的窗口(locals)
输入要监视的项
小技巧:用鼠标移动到想要观察的参数,也可以看见数值。然后点那个小别针,就会把值固定在这里
6. 方法的调用与栈(stack,内存里的栈)
6.1 方法调用时栈内存的分配
6.1.1 对stack frame的分析
stack frame:一个方法被调用的时候,它在栈内存中的布局。
Main方法叫做调用者或者主调者caller,GetConeVolume叫做被调用者callee
Calculator.GetConeVolume(100,100);
这里面100,100两个函数遵循C++原则,归主调者Main管理
然后就可以进入圆锥方法了:
这里面有三个参数,但double r和double h已经被Main压到内存里了,所以只用压一个参数:cv
接着走红线的这个方法,那需要把r,h两个参数压如内存,这时候就是GetConeVolume做主:
继续下一个方法圆柱的,还有圆的,圆里面的参数都被圆柱压进去了,它就不用压参数了。
函数的返回值一般是存在CPU的寄存器中,可以理解为内嵌在CPU中非常快速的内存
这些流程走完后,就开始return了,从圆面积返回圆柱体积
↑ call stack 少了一层
一个函数执行完成返回后,他的内存就被清空了。
压进去的参数也就清空了,然后开始运行圆柱体自己内部的代码。
参数的值还在寄存器中,再按下F11,a的值才出现
再次返回到圆锥方法中,栈已经变成两层了
圆柱的方法和圆锥给他压的两个参数也清空了
圆锥方法也执行完了,清空内存,回到Main
最终完成计算,Main也清空