第一章从C到C++
名字空间vs库(头文件)
-
本质不同:
-
名字空间是逻辑概念,类似于贴标签:一个名字空间是一种标签纸
-
库是物理概念,类似于工具箱:一个库含有多个同类工具
-
-
目的不同:
-
名字空间为了区别不同工具箱的同名工具,避免同名歧义
-
库为了将不同用途的工具分别装在不同的箱子里,实现解耦
-
#include<修水管工具箱>
#include<修电路工具箱>
手套.穿戴();×
蓝色::手套.穿戴();√
绿色::开关.打开();√
使用名字空间区分同名符号,避免同名歧义
string是C++的字符串类(类理解为"类型")
-
字符串本质就是一串字符(char)
-
C语言中用char[]来存放字符串,用char*表示一个字符串
-
但是烦人的指针合法性问题和'\0'问题
-
C++中新增了string类来表示字符串,并提供了一系列工具供使用(工具——函数(功能、方法……))
-
string实际上封装了char[]
C语言
char s[] ="abcde"; char *str="abcde"; printf("%s",str); printf("%d",strlen(s));
C++
std::string s = "abcde"; std::cout<< s.length(); std::cout<<s;
变量(对象)s的一个工具(成员方法)
通过 对象.函数()方式调用length()的用途是返回它的长度
自增++和自减--运算符
-
C++引入了自增++和自减--符号:将变量自身值增/减1
-
前自增(减)和后自增(减)的区别:
-
前:先自增自减,后执行表达式
-
后:先执行表达式,后自增自减
-
(布尔)bool类型
-
回顾C语言判断逻辑真假规则:表达式值为非0->肯定,值为0->否定
-
C++引入了bool类型,取值只能为true(真)或false(假)
-
bool变量本质是单字节无符号整数0或1
bool a; a=2; if(a){ cout<<a; }
输出:1
Key
Key1:
C++是在C语言基础上改进发展而来的,是C语言的一个超集
-
关于C语言和C++的关系,以下说法正确的是:(A) A.C++兼容C语言
B.C语言部分兼容C++
C.C++部分兼容C语言
D.C语言兼容C++
分析:兼容指包含,包括了
-
关于C语言和C++编译器,以下说法正确的是:
A.C语言编译器能编译C语言和C++源代码
B.C++编译器只能编译C++源代码
C.C++编译器只能编译C语言源代码
D.C++编译器能编译C语言和C++源代码
任何合法的C语言代码都是合法的C++代码,因此C++编译器可以编译C语言代码
Key2:
标准输入输出是利用<iostream>库的cin和cout这两个流对象
Key3:
输入输出流可理解为河流,"<<"放入一艘船,">>"捞出一艘船
1.在C++中使用流进行输入输出,其中用于屏幕输出的对象是: A.cin B.cerr C.cfile D.cout
分析:cerr是标准错误输出对象流,cin是标准输入对象流,没有cfile
-
C++中的标准输入输出是通过输入输出库中的输出输入流对象实现的
写出一条向屏幕打印整型变量n的输出语句:std::cout<<n;
Key4:
string类是C++的字符串类,提供了(封装)了许多工具(成员函数)供使用
-
若string s ="ABCDE"; 则以下说法错误的是:
A.s[2]='c';将s内容变为"ABcDE"
B.s.clear();将s内容清空,变为空字符串
C.cout<<s.length();将输出6~~将输出5
D.s.append("123");将s的内容变为"ABCDE123"
分析:string已经对char[]进行了封装,字符串结束符'\0'不属于字符串的有效内容,因此string类字符串的长度就是字符串内容的长度。事实上在使用string时,无需考虑'\0'
Key5:
前自增(减)运算符先自增(减)再计算表达式,后自增(减)反之
-
循环while(int i=0) i--; 执行次数是0
-
已知i=5,j=0,下列各式中,使j的值为6的表达式是:
A.j=i+(++j) B.j=j+i++ C.++i-j-- D.i+++j
分析:牢牢抓住前置先增减,后置后增减,将每个式子中,带自增自减运算符的变量参与运算时的值先写出来
A选项:++j先自增后运算,j=5+1
B选项:i++先运算后自增,j=0+5,D选项同理为5
C选项:首先++i先自增为6,j--先运算为0,j=6-0得到6,但是此时j--还需要完成自减,j最终值为5
Key6
bool变量的值为判断结果true(真)或false(假),其实质是1或0
-
该程序段中,while循环执行的次数和程序输出结果是:
5,1
int a[]={5,1,2,7,-1,13,-2,9}; bool b =flase; int i =0; while(!b){ if(a[i]<0){ b=true; } i++; } cout<<b;//b=true=1
-
下列循环利用两个布尔变量来判断int数组a中是否存在连续的两个0.则(1)处代码应为:
A.flag1 = true B.flag2=false C.flag1=false D.flag2=true
bool flag1=false,flag2=false; for(int i =0;i<sizeof(a)/sizeof(int);i++){ if(a[i]==0){ if(flag1){ flag2=true; break; } flag1=true; } else{flag1=false;} } cout<<(flag2?"yes":"no");
分析: flag1用来标记是否已经发现一个0
flag2用来标记是否已经发现连续的两个0
a[i]为0时,若flag1已经为true,则找到连续的两个0,因此将flag2置true并离开循环
Key7
逻辑运算符的优先级:!>&&>||
1.(a)处填入下列哪式将程序执行else块:
A.b1||!b2&&!b3 B.b1||b2&&b3
C.b1&&b2||b2&&b3 D.!b1||b2&&b2||b3
bool b1=true,b2=false,b3=true; if( ){ //if块 }else{ //else块 }
分析:
此类题型选项看似复杂,技巧为抓住优先级最低的逻辑或||
如果最外层逻辑或的任何一侧出现了可以确定为true的表达式,则整个表达式为true
第二章C++函数
理解以下名词:
-
参数、返回值与函数体
-
声明与定义
-
形参与实参
-
默认参数
-
引用
-
按值传参、指针传参与引用传参
-
函数重载
-
内联函数(inline关键字)
回答以下三项技术分别解决了什么问题:
-
引用传参
-
内联函数
-
函数重载
熟悉以下题型
-
判断两个函数是否能构成重载
-
判断默认参数是否会导致歧义
-
使用引用传参实现就地修改函数参数的效果
-
判断何处适用内联函数
复习函数
函数
-
函数声明(原型)
-
函数定义(实现)
-
函数的调用(call )
-
函数的参数
-
函数体
-
函数的返回值
-
函数的目的
-
函数是面向过程的体现:输入(参数)->处理流程->输出(返回值)
-
目的:将重复发生的过程进行统一描述,实现代码复用与解耦
-
代码复用:避免写重复的代码
-
解耦:避免发生改动时牵一发而动全身
-
函数声明与定义
函数定义明确了函数:
-
接受什么参数
-
返回什么值
-
具体进行什么操作(实现)
int addTwolnt(int a,int b){ int c=a+b; return c; }
函数声明明确了函数:
-
接受什么参数
-
返回什么值
int addTwolnt(int a,int b);
int->指明返回值类型
int a,int b->指明参数列表
函数形参与实参
-
形式参数:书写函数定义时用来描述函数体的变量
-
实际参数:调用函数时,外部调用方实际传递给函数的变量
int addTwolnt(int a,int b){ return a+b; } // int m= 10,n=20; cout<<addTwolnt(m,n);
函数值传参与指针传参
如果对C语言理解比较深刻的话,你会意识到
-
值传参与指针传参实际上没有区别!
-
Why?回顾一个指针变量的值是什么?是一个地址!
-
指针传参实际就是指针的值传参(按值传参)
//交换两变量的值 void swap(int *pa,int *pb){ int temp = *pa; *pa = *pb; *pb = temp; } int m=0,n=20; int *pm=&m,*pn=&n; swap(pm,pn); /*实际上就是pm和pn对pa和pb的值传参(拷贝) 全因指针的值是地址而让指针传参显得特殊…*/
默认值(default)的概念
-
回顾:计算机程序必要因素之确定性
-
程序不怕做错事,怕无法确定自己到底要做什么
-
默认值:告诉程序当数据/指令缺失时,默认用什么来代替,避免失去确定性
函数默认参数
int power(int n,int x=2); int power (int n,int x){ int ans=1; for(int i =0;i<x;i++){ ans*=n; } return ans; } int main(){ cout<<power(5);//答案:25 cout<<power(4,3);//答案:64 }
没实参->用默认值
传了实参->用传进来的
注意:
必须在函数声明中声明默认参数
函数声明就是函数的身份证
外部调用方不看定义只看声明
默认参数必须在形参列表的结尾!避免歧义
引用与引用传参
引用:其实就是别名
-
百变的我?
-
学生口中的"刘老师"
-
同事中的"小刘"
-
父母中的"妹妹"
-
好友口中的"老刘"
-
…
-
上面指的都是同一个实体:我
-
这些别名都在"引用"我
-
引用同一个实体(变量)的别名
-
特指左值引用,即给一个已经有名字的变量起别名,所以不可存在空引用!
int main(){ int a=10; int &b=a,&c=b; //"int &":应当理解为一个整体,即变量b和c的类型是"int &" //中文名为"整型引用" cout<<b;//输出10 c++; cout<<a;//输出11,因为c是a的引用("别名")c自增就是a自增 }
-
思考:指针传参解决了什么问题?
-
避免按值传参发生的拷贝,实现了原地改动调用方传入参数的功能
-
-
指针传参还有什么问题?
-
代码里夹杂着间接引用符号'*'难写难看难读,还有符号优先级的问题!
-
void swap(int *p1,int *p2){ int temp=*p1; *p1=*p2; *p2=temp; }
-
这个问题怎么解决?
-
如果形参就是实参的一个别名,岂不是…
-
void swap(int& a,int& b){ int temp=a; a=b; b=temp; } int main(){ int m= 1,n=2; //swap_p(&m,&n); //cout<<m<<","<<n<<endl; swap(m,n); cout<<m<<","<<n<<endl; return 0; }
-
这个方法就是引用传参
函数重载
(overload)
over:重复 load:装载
-
思考:函数解决了什么问题?
-
避免重复书写相同的过程/流程代码,实现代码复用和解耦
-
-
函数还有什么问题?
-
C++是强类型语言,同一套操作若要用在不同类型/数量的参数上,则需要编写不同函数!
-
-
所以呢?
-
调用方需针对不同的实参写不同的函数调用代码,if-else增多,需判断类型信息
-
函数名称不同,有多少种参数列表就需要多少个函数名
-
…
-
-
解决思路:允许多个同名函数存在,分别处理不同类型/数量的参数…
-
这就是函数重载
函数重载
-
多个函数的名字相同,参数列表(数量、类型)不同
int add(int a,int b){ return a+b; } int add(int a,int b,int c){ return a+b+c; } int add(int a,int b,int c,int d){ return a+b+c+d; }
cout << add(1,2); cout<<add(1,2,3); cout<<add(1,2,3,4); //都能正确运行,编译器会根据实参类型/数量,自动匹配调用哪个函数
-
多个函数的名字相同,参数列表(数量、类型)不同
string myAdd(int a,int b){ return std::to_string(a)+std::to_string(b); } string myAdd(int a,string s){ return std::to_string(a)+s; } string myAdd(string s,int a){ return s+std::to_string(a); } string myAdd(string s1,string s2){ return s1+s2; } //都能正确运行,编译器会根据实参类型/数量,自动匹配调用哪个函数
-
所以overload解决了什么问题?
-
减少了函数调用方的代码冗余,现在调用方对不同类型/数量的实参可以写完全一样的代码了!
-
-
别激动,overload也存在问题
-
最重要的一环:通过调用时的实参列表,和多个重载函数中的一个进行匹配
-
匹配是编译器自动完成的,但如果你的调用代码有可能产生歧义…
-
int func(int a,int b){ return a+b; } int func(int& a,int& b){ return a+b; }
避免overload歧义
-
如果调用时实参能匹配多个(>1个)重载函数,则编译器遇到歧义,产生编译错误
int func(int a,int b){ return a+b; } int func(int& a,int& b){ return a+b; } int func(int a,int b,int c=2){ return a+b+c; } int func(int a,int b){ return std::to_string(a)+std::to_string(b); }
-
不允许仅有返回值不同的函数重载:重载是针对参数列表的!
overload和返回值无关,只要满足:
声明时:①名字相同②参数列表不同
调用时:③不产生匹配歧义
内联函数(inline)
"额外开销"的概念
-
一位学生的每日计划:
-
8:00-9:00 坐公交车去图书馆 1小时
-
9:00-11:00 学习 2小时
-
11:00-12:00 坐公交车回家 1小时
-
……
-
学习效率=46%
-
函数的目的:将重复的发生的流程统一起来,实现代码复用和解耦
-
但是!如果某个函数的功能非常简单,但被反复调用的话,存在什么问题?
-
提示:"函数调用"这个行为本身也是有一定开销的(调用栈)
-
-
那么调用这个函数的"额外开销"占比就很大了
-
此时可以建议编译器在编译的时将函数直接在调用处展开,避免函数调用行为的额外开销
-
这就是函数内联
饮水机复用—>结果走去茶水间的开销大于接一杯水的收益—>不如每个办公室配一台饮水机
内联函数:指建议编译器译时将某个函数在调用处直接展开,避免运行时调用开销
inline int getMax(int a,int b,int c){ return a>b?(a>c?a:c):(b>c?b:c); } int main(){ cout<<getMax(1,2,3); cout<<getMax(3,2,1); cout<<getMax(2,1,3); cout<<getMax(1,2,3); cout<<getMax(3,2,1); cout<<getMax(2,1,3); } //如果编译器接受了建议,这些调用将在编译时,被原地展开成函数内容
注:C语言中的`?:`是什么意思? ?在C语言中表示疑问的意思 :在C语言中表示判断的结果选择 二者同时出现,两者组成结构选择语句 条件运算符(?:)是C语言中唯一的一个三目运算符,它是对第一个表达式作真/假检测,然后根据结果返回另外两个表达式中的一个。 二、使用步骤 <表达式1>?<表达式2>:<表达式3> 在运算中,首第一个表达式进行检验,如果为真,则返回表达式2的值;如果为假,则返回表达式3的值。 例代码如下(示例): max = ((a>b)?a:b)>c?((a>b)?a:b):c; 1 在上述代码求a,b,c中的max值,先求表达式((a>b)?a:b)中的max值,若a>b为真,则输出a的值;若a>b为假则输出b的值。再用((a>b)?a:b)所比较出来的值与c进行比较,若((a>b)?a:b)>c为真则输出((a>b)?a:b)的值;若((a>b)?a:b)>c为假,则输出c的值。
-
并不是写了inline关键字就一定会被内联,只是提出建议,由编译器决定是否采纳
-
内联这个动作发生在编译时,提升运行时的效率
Key
Key1
为了避免歧义,默认参数应当放在形参列表的最后面
Key2
默认参数应当在函数声明里设置
-
在C++中,下列关于函数参数默认值的描述中正确的是:
A.设置参数默认值时,应当全部设置
B.设置参数默认值后,调用函数不能再对参数赋值
C.设置参数默认值时,应当从右向左设置
D.只能在函数定义(函数声明)时设置参数默认值
-
以下代码中,编写带默认参数的函数正确的是:
int func(int a,int b=2){…}
分析:函数声明对外表明了函数的名字、返回值与参数列表,它就是函数的身份证,B选项是声明同时定义。
Key 3
引用的本质是已定义变量的别名,因此不可存在空引用
Key4
除函数形参外,其他引用定义时必须赋初始值
void swap(int &a,int &b){ temp=a; a=b; b=temp; } //…省略 int m=10,n=20; swap(m,n);
类型& 变量名是定义一个该类型的引用
&变量 是对变量取地址,得到一个指针
类型引用和取地址的区别
引用是给已定义的变量起别名
引用:在声明的时候一定要初始化
#include <iostream> using namespace std; int main() { int a = 88; int &c = a; //声明变量a的一个引用c,c是变量a的一个别名,如果引用,声明的时候一定要初始化 int &d = a; //引用声明的时候一定要初始化,一个变量可以有多个引用 cout<<"a="<<a<<endl; cout<<"c="<<c<<endl; cout<<"====================="<<endl; c=99; cout<<"a="<<a<<endl; return 0; } //&(引用)==>用来传值,出现在变量声明语句中位于变量 左边时,表示声明的是引用. //&(取地址运算符)==>用来获取首地址,在给变量赋初值时出现在等号右边或在执行语句中作为一元运算符出现时,表示取对象的地址.
总而言之,和类型在一起的是引用,和变量在一起的是取址
实例如下:1)引用在赋值=的左边,而取地址在赋值的右边,比如
int a=3; int &b=a; //引用 int *p=&a; //取地址
2)和类型在一起的是引用,和变量在一起的是取址。 举例同样如上,还有下例:
int function(int &i) { } //引用
3)对于vector,上面2条同样适合
vector<int> vec1(10,1); //initialize vec1: 10 elements, every element's value is 1 vector<int> &vec2 = vec1; // vec2 is reference to vec1 vector<int> *vec3 = &vec2; //vec3 is addresss of vec1 and vec2
--指针-对于一个类型T,T就是指向T的指针类型,也即一个T类型的变量能够保存一个T对象的地址,而类型T是可以加一些限定词的,如const、volatile等等。见下图,所示指针的含义
char c = 'a'; char *p = &c; //p里面存放的是c的地址 //--引用-引用是一个对象的别名,主要用于函数参数和返回值类型,符号X&表示X类型的引用。见下图,所示引用的含义: int i=1; int &r = i; //此时i=r=1; 若执行r=2;//此时i=r=2; int *p = &r; //p指向r;
指针和引用的区别
1.首先,引用不可以为空,但指针可以为空。前面也说过了引用是对象的别名,引用为空——对象都不存在,怎么可能有别名!故定义一个引用的时候,必须初始化。因此如果你有一个变量是用于指向另一个对象,但是它可能为空,这时你应该使用指针;如果变量总是指向一个对象,i.e.,你的设计不允许变量为空,这时你应该使用引用。如下图中,如果定义一个引用变量,不初始化的话连编译都通不过(编译时错误)
而声明指针是可以不指向任何对象,也正是因为这个原因,使用指针之前必须做判空操作,而引用就不必。
2.其次,引用不可以改变指向,对一个对象"至死不渝";但是指针可以改变指向,而指向其它对象。说明:虽然引用不可以改变指向,但是可以改变初始化对象的内容。例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。
3.再次,引用的大小是所指向的变量的大小,因为引用只是一个别名而已;指针是指针本身的大小,4个字节
4.最后,引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)
总之,用一句话归纳为就是:指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。
Key5
函数重载三要素:①名称相同②参数列表不同③调用不产生匹配歧义
Key6
仅有返回值不同不能构成重载!
函数重载的目的是:方便调用方编写代码,提高可读性
以下三个均可与int func(int,int)构成重载函数
int func(int&,int&); int func(int,int,int); string func(int,string);
string func(int,int);
不能与int func(int,int);
构成重载函数
Key7
若一函数功能简单,则函数调用的额外开销占比比较高
Key8
inline关键字只是建议编译器将函数内联,是否内联由编译器自行决定
Key9
函数内联发生在编译时,提高的是运行效率
-
下列哪个类型函数不适合声明为内联函数:
A.函数体语句较多
B.函数体语句较少
C.函数执行时间较长
D.函数执行时间较短
-
在内联函数内允许使用的是:
A.if-else语句 B.Switch语句 C.赋值语句 D.以上都允许
-
关于下列函数,说法正确的是:
inline int func(int a,int b){return a+b;}
A.将在预编译阶段进行内联展开
B.将在编译时进行内联展开
[解释] 不一定
C.将在运行时进行内联展开
D.不确定其是否会进行内联展开
第三章-面向对象1-抽象与封装
从面向过程到面向对象
-
随着计算机发展,问题/场景/系统越来越复杂,面向过程在大型系统开发中捉襟见肘
-
面向对象思想应运而生,核心思想:
既然随着系统参与实体的增多,过程变得复杂,那就不费力描述每一个可能的过程了,转而描述每一个实体。如果每一个实体都被正确描述了,那么将这些实体置于系统中,系统就能正确运行。
-
对于问题:求解不同图形的周长和面积
-
以面向过程的思路(按流程走):
-
确定图形是什么图形,三角形,正方形,圆形?…
-
获取计算所需要的信息:
for△:底和高
for□:边长
for○:半径
-
…
3. 根据不同的计算公式进行计算
4. 得到结果
-
以面向对象的思路:
图形类
属性:边长、半径、高、角度…
行为:求面积,求周长,缩放,错切,旋转…
抽象与UML图
抽象 Abstract
图书馆管理系统:
图书
-属性:名字,编号…
-方法:借出,还入…
读者
-属性:姓名,身份证号…
-方法:注册,还欠费…
学生信息管理系统:
学生
-属性:姓名,学号…
-方法:查成绩,查课表…
老师
-属性:姓名,院系…
-方法:登成绩,上线课程…
游戏:
-属性:ID,血量,位置…
-方法:吃药,移动,开枪…
面向对象的四个特征之一
抽象(分析问题,识别出各个实体及其属性和行为)
UML类图
Unitfied Modeling Language
-
识别出问题中各个实体(属性+行为)后,需用规范的方式描述
类和对象
定义类
-
定义一个类=定义它的属性(成员变量)+行为(成员函数)
class类名{ 访问控制修饰符: 定义成员变量 定义成员函数 };//不要忘了结尾的分号
class Circle{ public: float radius; Circle(float radius){ this->radius=radius; } float getS(){ return 3.14*radius*radius; } float getC(){ return 2*3.14*radius; } };
结构体vs类
-
简单理解:结构体+行为(成员函数)=类
-
事实上C++中也支持结构体定义成员方法,俩者并无本质区别了
-
根据使用场景选择结构体或类:
-
结构体:主要记录数据,极少行为(如资源配置信息、网络连接信息等)
-
类:既有属性也有行为(如学生类、用户类、玩家类等)
-
类vs对象
-
类-------实例化------------>对象
特殊的成员函数:
构造函数与析构函数(constructor,destructor)
-
构造函数和析构函数是两种特殊的类成员函数
-
构造:对象实例化时,在分配得到的空间上构造对象(如初始化成员变量、分配资源等)
-
默认构造函数:没有参数的构造函数
-
有参的构造函数:有参数的构造函数
-
-
析构:对象生命周期结束时,回收空间前,完成对象的清理工作(如释放资源等)
-
构造函数和析构函数都没有返回值!
-
析构函数没有参数!
class A { public: int n; char* data = nullptr; A(int n){ this->n=n; data = (char*)malloc(100); } ~A(){ free(data); } };
this指针详解
this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
所谓当前对象,是指正在使用的对象。例如对于stu.show();
,stu 就是当前对象,this 就指向 stu。
下面是使用 this 的一个完整示例:
#include <iostream> using namespace std; class Student{ public: void setname(char *name); void setage(int age); void setscore(float score); void show(); private: char *name; int age; float score; }; void Student::setname(char *name){ this->name = name; } void Student::setage(int age){ this->age = age; } void Student::setscore(float score){ this->score = score; } void Student::show(){ cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl; } int main(){ Student *pstu = new Student; pstu -> setname("李华"); pstu -> setage(16); pstu -> setscore(96.5); pstu -> show(); return 0; }
运行结果: 李华的年龄是16,成绩是96.5
this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。
本例中成员函数的参数和成员变量重名,只能通过 this 区分。以成员函数setname(char *name)
为例,它的形参是name
,和成员变量name
重名,如果写作name = name;
这样的语句,就是给形参name
赋值,而不是给成员变量name
赋值。而写作this -> name = name;
后,=
左边的name
就是成员变量,右边的name
就是形参,一目了然。
注意,this 是一个指针,要用->
来访问成员变量或成员函数。
this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值。本例中,this 的值和 pstu 的值是相同的。
我们不妨来证明一下,给 Student 类添加一个成员函数printThis()
,专门用来输出 this 的值,如下所示:
void Student::printThis(){ cout<<this<<endl; }
然后在 main() 函数中创建对象并调用 printThis():
Student *pstu1 = new Student; pstu1 -> printThis(); cout<<pstu1<<endl; Student *pstu2 = new Student; pstu2 -> printThis(); cout<<pstu2<<endl;
运行结果: 0x7b17d8 0x7b17d8 0x7b17f0 0x7b17f0
可以发现,this 确实指向了当前对象,而且对于不同的对象,this 的值也不一样。
几点注意:
-
this 是 const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
-
this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
-
只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用(后续会讲到 static 成员)。
this 到底是什么
this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
this指针
-
this的中文含义:这、这个、当前这个
-
this指针在类定义内部使用,指向当前对象
封装
Encapsulate
面向对象的四个特征之二:封装
-
封装:将类的一些成员变量或方法藏起来(访问控制属性),不允许外界直接操作
访问控制属性--->(public------->公有 protected---->保护 private------>私有)
-
不允许直接操作≠不允许操作,而是通过自定义的特定方法操作
-
访问控制属性为public的成员
外部可以直接通过对象.名字
访问
A a; a.xxx=10;//合法 a.func();//合法
-
访问控制属性为private的成员,外部不可直接通过
对象.名字
访问
A a; a.xxx=10;//不合法 a.func();//不合法
getter/setter方法
-
为某些私有成员变量提供外部读写方法:get_xxx(读)/set_xxx(写)
-
getter和setter一般是public的,不然没意义
class Book{ private: string name; int count; public: Book(){…} int get_count(){ return count; } };
getter函数的通常格式(设xxx的类型为T):
T get_xxx() const{ //因为const,常成员函数就不能修改类成员变量
return xxx;
}
setter函数的通常格式(设xxx的类型为T):
void set_xxx(const T& xxx){
this->xxx=xxx;
}
class Book{ private: string name; int count; public: Book(){…} void set_name(const string& name){ this->name=name; } };
Key
Key1
抽象:识别问题/场景/系统中事物的属性和行为
Key2
UML类图可用于规范化描述一个事物的属性与行为
Key3
类定义主要定义一个类的成员变量和成员函数
Key4
使用class关键字进行类定义
-
根据UML类图,完成C++的类定义:
Key5
类是对某一类事物的描述,对象是该事物真实存在的一个实体
-
下列关于类和对象的描述中,错误的是:
A.类是对某一事物的抽象
B.一个类只能有一个对象
C.类和对象的关系类似数据类型与变量的关系
D.对象在内存中真实存在
Key6
构造函数是特殊的成员函数,名称为类名,通常目的是初始化对象
Key7
①创建对象时自动调用②可以有多个重载③不可以有返回值!
-
为Students类补充一个默认构造函数和一个有参构造函数:
Student(){ id = 1001; name = "Mike"; age = 18; score=85; }
student(int_id,string_name,int_age,float_score){ id=_id; name=_name; age=_age; score=_score; }
-
关于构造函数的叙述正确的是:
A.构造函数可以有返回值
B.构造函数的名字必须与类名完全相同
C.构造函数必须带有参数
D.构造函数必须定义,不能默认
Key8
this指针在类成员函数定义内部使用,指向当前对象
-
利用this指针编写Student的有参构造函数,避免变量名覆盖问题:
Student(int id,string name,int age,float score){ this->id=id; this->name=name; this->age=age; this->score=score; }
-
this指针保证每个对象拥有自己的数据成员,但共享处理这些数据成员的代码。(√)
Key9
封装:控制类成员在外部的可见性
Key10
public标记公有成员/private标记私有成员私有
-
设a是类A的一个对象,则两行代码皆不会产生编译错误的是:
class A{ private: int n; string s; public: A(int n,string s){this->n=n;this s->=s;} string get_s() const{return n;} int get_n() const{return n;} private: void increase_n(){n++;} };
//true cout << a.get_n(); string t=a.get_s(); //false a.get_n()=10;//get_()是常量!
Key11
getter函数通常会被设置为const函数,setter函数则通常接收const参数
-
给定类A的定义如下,则下列其私有成员变量的getter/setter合理的是:
void set_n(const int n){this->n=n;}
class A{ private: int n; public: A(int n){this->n=n;} };
第四章-面向对象2-继承与多态
-
继承
-
虚函数
-
多态
理解以下名词:
-
继承
-
父类(基类)和子类(派生类)
-
公有、私有与保护继承
-
虚函数与重写
-
(运行时)多态
-
动态联编与静态联编
-
纯虚函数、抽象类与接口
思考并回答以下问题:
-
为什么类需要支持继承
-
为什么类需要支持(运行时)多态
熟悉以下题型:
-
三种继承方式对三种访问控制属性成员的作用
-
在子类中访问父类的同名成员
-
在父类中声明虚函数并在子类中重写
-
通过虚函数重写+指向子类的父类指针实现运行时多态
继承
同一对象的多重身份
-
回顾面向对象思想:首先识别系统中的实体,然后提炼属性/行为,然后……
-
问题来了:同一个实体可能有多重身份,这些身份之间往往有层次递进关系
父类与子类
-
父类与子类:不同身份之间的层次递进关系
-
张华首先是一个人,具体一点是一个学生,再具体一点是大学生……
-
人<---学生<---大学生<---…
类的派生与继承Inherit
-
人和学生之间是层次递进关系,而非并列关系:若一个实体是学生,则一定是人
-
那么在定义学生类时,无需把属于人的那一部分属性和方法再定义一遍
-
直接让学生继承自人:获得人的属性和方法
-
目的?
-
还是因为:代码复用
面向对象的四个特征之三:继承
class A{ public: int nA; A(){ nA=1; } void funcA(){ cout<<"funcA\n"; } }; class B:public A{ public: int nB; B(){ nB = 2; } void funcB(){ cout<<"funcB\n"; } }; class C:public B{ public: int nC; C(){ nC=3; } void funcC(){ cout <<"funcC\n"; } }; int main(){ C c; c.funcA(); c.funcB(); c.funcC(); return 0; }
-
C c
-
则对象c中既有继承得来的属性nA、nB和方法funcA、funcB
-
也有自己的专属的属性nC和方法funcC
public (公有继承)
protected(保护继承)
private(私有继承)
-
继承方式:决定父类成员在子类中的访问控制属性
-
父类的private成员不会被子类继承
-
公有继承不改变控制属性,保护继承和私有继承指示父类成员在子类的相应控制属性
-
父类的public成员 | 父类的protected成员 | 父类的private成员 | |
---|---|---|---|
子类中的访问控制属性 | |||
public继承 | public | protected | 不可见 |
protected继承 | protected | protected | 不可见 |
private继承 | private | protected | 不可见 |
父子同名成员并存
class Father{ public: int n = 1; void func(){ cout<<"This is Father"; } }; class Son{ public: int n = 2; void func(){ cout <<"This is Son"; } void set(){ Father::n=-1; n=-2; } } int main(){ Son son; son.func(); son.Father::func(); son.set(); cout <<son.Father::n; cout<<son.n; }
-
子类中同时有两个n和两个func()
-
直接使用默认指子类成员
-
如果需要使用父类的成员,需要使用父类名字空间显式指明
虚函数
Virtual "虚"
-
回顾:继承来源于同一对象可能有多重身份,且这些身份有层级递进关系
-
实践中会出现这类情况:
-
父类中的某些行为需要在子类中被更加具体地细化
-
父类中的某些行为不可确定,必须在子类中实现
-
-
于是虚函数的概念产生:
-
父类的虚函数可以在子类中被重写(override),即重新实现,但参数和返回值必须保持一致
-
-
含有虚函数的类叫做虚类
class Human{ public: virtual void say(){ cout <<"I'm human"; } }; class Student:public Human{ public: void say(){ cout <<"I'm a student"; } };
纯虚函数和抽象类
-
某些类是抽象的,不是具体的,不可独立存在;
-
我可以是男人或女人,但不可能仅是"人类对象"
-
可以有矩形对象、三角形对象…但不能有"图形对象"
-
父类中的某些行为不可确定,必须在更精确的子类中定义
-
纯虚函数:不实现,仅声明为纯虚函数,留待子类里重写定义
-
含有纯虚函数的类叫抽象类,仅有纯虚函数的类叫接口
class Shape{ public: virtual float getS()=0; virtual float getC()=0; }; class Circle:public Shape{ private: float radius; public: Circle(float radius){ this->radius=radius; } float getS(){return 3.14*radius*radius;} float getC(){return 2*3.14*radius;} }; int main(){ //Shape s; Circle c(3.5f); cout<<c.getS()<<endl; return 0; }
多态 Polymorphism
-
已多次强调:同一对象可以有多重层级递进身份
-
有这种情况:同一对象在不同的场合中,被外界所关注的是不同的身份
-
但他的本质和应有的行为并不会因外界眼光而改变
面向对象的四个特征之四:多态
-
理解多态:
-
一个对象就是内存中的一个实体,它只能属于一个确定的类:最精确的子类
-
它可能在不同处被视为不同身份,但它本质行为方式应与外界如何看待它无关!
-
-
问题:如何保证一个对象执行其最本质身份的行为?
-
利用虚函数重写+指针!!!
-
指向子类对象的父类指针!!!
多态的实现例子1
class Human{ public: virtual void say(){ cout<<"I'm human\n"; } }; class Student:public Human{ public: virtual void say(){ cout<<"I'm a student\n"; } }; class CollegeStudent:public Student{ public: void say(){ cout<<"I'm a college student\n"; } };
CollegeStudent a; Human*p1=(Human*)&a;//指向子类对象的父类指针 Student*p2=(student*)&a;//指向子类对象的父类指针 CollegeStudent*p3=&a; p1->say();//通过指针调用的是对象本质子类的方法 p2->say();//通过指针调用的是对象本质子类的方法 p3->say();//通过指针调用的是对象本质子类的方法 //答案: //I'm a college student //I'm a college student //I'm a college student
多态的实现例子2
//父类 class Human{ public: virtual void toilet()=0 }; //子类 class Man:public Human{ public: void toilet(){ cout<<"我去上男厕所"; } }; class Woman:public Human{ public: void toilet(){ cout<<"我去上女厕所"; } }; class Non:public Human{ public: void toilet(){ cout<<"我去无性别厕所"; } }; //调用 Man man1,man2,man3; Woman woman1,woman2; Non non1,non2; //很多很多对象,通过函数func让他们上厕所 func(&man1); func(&woman2); func(&non2); void func(Human *human){ human->toilet(); }//human可能指向三种不同的实际对象,事实上func并不关心实际是什么,反正都当成Human,能toilet就行
多态的意义:代码复用
-
通过"虚函数+指向子类对象的父类指针",可以把不同的子类统一视为其共同父类
-
于是无需针对不同的子类写相同的逻辑,统一视作其共同父类,利用指针操作即可
-
本质是虚函数能做什么和怎么做分离,父类指定要做什么,子类来实现具体做法
图形类
-
问题:写一个函数,求一个图形的面积和周长之比
-
没有多态:需要为每种图形都实现一个函数
-
有多态:只需实现一个函数
-
它不关心这个图形具体是什么,反正能求面积和周长即可
#include<iostream> using namespace std; class Shape{ public: virtual float getS() = 0; virtual float getC() = 0; }; class Circle :public Shape{ private: float radius; public: Circle(float radius){ this->radius= radius; } float getS(){return 3.14*radius*radius; } float getC(){return 2*3.14*radius; } }; class Rectangle:public Shape{ private: float a; float b; public: Rectangle(float a,float b){ this->a=a; this->b=b; } float getS(){return a*b; } float getC(){return 2*(a+b); } }; void display(Shape* ptr){ //此处实现多态:通过父类指针调用子类重用的虚函数 cout<<"S:"<<ptr->getS() <<endl; cout<<"C:"<<ptr->getC() <<endl; } int main(){ Circle c(3.5f); Rectangle r(3.f,5.f); cout<<"Circle:"<<endl; display(&c); cout<<"Rectangle:"<<endl; display(&r); return 0; }
静态联编与动态联编
-
上述利用虚函数重写+指针实现的多态特指运行时多态,与之相对的是编译时多态
静态联编=编译时多态=函数重载=overload
动态联编=运行时多态=虚函数重写=override
-
联编(bind):确定具体要调用多个同名函数中哪一个
-
静态联编:在编译时就确定了要调用的是哪个函数(根据多个重载函数的参数列表确定)
-
动态联编:直到运行时才知道实际调用的是哪个函数(根据指针指向对象的实际身份)
#include<iostream> using namespace std; class Shape{ public: virtual float getS() = 0; virtual float getC() = 0; }; class Circle :public Shape{ private: float radius; public: Circle(float radius){ this->radius= radius; } float getS(){return 3.14*radius*radius; } float getC(){return 2*3.14*radius; } }; class Rectangle:public Shape{ private: float a; float b; public: Rectangle(float a,float b){ this->a=a; this->b=b; } float getS(){return a*b; } float getC(){return 2*(a+b); } }; void display(Shape* ptr){ //此处实现多态:通过父类指针调用子类重用的虚函数 cout<<"S:"<<ptr->getS() <<endl; cout<<"C:"<<ptr->getC() <<endl; } int main(){ Circle c(3.5f); Rectangle r(3.f,5.f); cout<<"Circle:"<<endl; display(&c); cout<<"Rectangle:"<<endl; display(&r); return 0; }
Key
key1
继承:子类直接获得父类的成员,实现代码复用
key2
但又不是随意获得,要视继承方式和访问控制属性而定
以下成员中,能通过派生类对象访问的是:
A.公有继承的基类公有成员
B.公有继承的基类保护成员
C.保护继承的基类公有成员
D.保护继承的基类保护成员
分析:此处说的是"通过派生类访问",意即在外部以a.xxxx的方式访问。
如果题目问"派生类能访问的",则意指在类定义内部访问,则除了基类私有成员以外的都可访问。
Key3
声明虚函数的关键字是virtual,表明子类可以重写(override)它
key4
override需要保持参数和返回值一致,否则就是一个新函数而不是重写
Key5
含有纯虚函数(=0)的叫抽象类,不可实例化
-
下列关于虚函数的说法,正确的是:
A.虚函数是一个static(virtual)类型的成员函数
B.基类中采用virtual声明一个虚函数后,派生类中定义相同原型的函数可以不加virtual声明
C.虚函数是一个非成员函数
D.派生类中的虚函数与基类中相同原型的虚函数具有不同的参数个数或类型
分析:子类重写基类的虚函数时,可以继续通过virtual关键字申明为虚函数,表明允许被它的子类(孙子)继续重写,如果没有这个需要或者不允许继续重写则不加。
-
下列叙述中不正确的是:
A.含虚函数的类称为抽象类
B.不能直接由抽象类建立对象
C.抽象类不能作为派生类的基类
D.纯虚函数没有其函数的实现部分
Key6
多态:将不同类型的子类对象统一视作基类对象,实现代码复用
Key7
多态:虚函数重写+通过指向子类对象的父类指针调用
-
类声明如下,给定代码段的输出内容:
A.F1F1
B.S2S1S2
C.S2F1S2
D.S1S1S1
class Father{ public: int n=1; virtual void func(){cout<<"F"<<get_n();} int get_n() const{return n;} }; claas Son:public Father{ public: int n=2; void func(){cout <<"S"<<get_n();} }; Son obj: Father* p1=&obj; Son* p2=&obj; obj.func(); p1->func(); p2->func();
第五章-面向对象3-更多知识点
本章内容
-
深拷贝与浅拷贝
-
运算符重载
-
类的模块化编程
-
类的静态成员
-
构造函数参数列表
-
构造/析构顺序
-
*多继承与菱形继承
-
*友元函数
-
*虚函数表
深拷贝与浅拷贝
运算符重载
类的模块化编程
类的静态成员
构造/析构顺序
构造函数参数列表
*多继承与菱形继承
*友元函数
*虚函数表
-
如何拷贝一个对象?逐一拷贝成员变量?
-
如果对象持有间接资源呢(动态内存、文件句柄、网络连接等…)
-
浅拷贝:只简单复制成员变量的值
-
深拷贝:对于外部资源也复制一份
拷贝构造函数
-
拷贝构造函数:拷贝已存在同类对象来构造新对象,因此参数是该类的引用
class A{ public: int n; char* ptr; A(int n){ this->n=n; ptr=(char*)malloc(100); } //A的拷贝构造函数 /*A(A& other){ this->n=other.n; this->ptr=other.ptr//浅拷贝 }*/ //析构函数 ~A(){ free(ptr); cout<<"成功析构一个A对象"<<endl; } //深拷贝 /* A(A& other){ this->n=other.n; this->ptr=(char*)malloc(100); memcpy(this->ptr,other.ptr,100); }*/ };
A a1(10); A a2(a1); A a3(a1);//等价于A a3(a1);
默认拷贝构造函数
-
如果没有显式定义拷贝构造函数,则类会默认拥有一个浅拷贝构造函数
-
因此,如果类内拥有间接资源,记得按需自定义一个深拷贝构造函数
class A{ public: int n; char* ptr; A(int n){ this->n=n; ptr=(char*)malloc(100); } }; /*自动生成 A(A& other){ this->n=other.n; this->ptr=other.ptr; } */
运算符重载
-
运算符可理解为函数:如cout<<x等价于调用cout对象的成员函数<<,且将x作为<<函数的参数
-
那有了函数为什么还要有运算符重载机制呢?
-
就问你printf("%d",n)好看还是cout<<n好看
-
思考:有一个类Vector表示直角坐标系下的向量,如何按照数学定义实现两个向量对象的加法?
class Vector{ public: int a; int b; Vector(int a,int b){ this->a=a; this->b=b; } //定义函数 Vector add_two_vec(Vector& v1,Vector& v2){ Vector result(v1.a+v2.a,v1.b+v2.b); result result; } 然后调用函数即可 Vector v3=add_two_vec(v1,v2);//这样有什么问题?不直观!我就要v3=v1+v2 };
-
为自定义的类重载一个运算符函数,其第一个操作数对象本身,其他操作数是该函数参数
-
为Vector类重载'+'号,使'+'作为Vector类的一个成员函数,参数是另一个Vector对象的引用
class Vector{ public: int a; int b; Vector(int a,int b){ this->a=a; this->b=b; } Vector operator +(Vector& other){ return Vector(this->a+other.a,this->other.b); } //这样就可以Vector v3=v1+v2; //v1+v2本质在于调用v1对象的operator+函数, //将v2引用传参给other,v1.+(v2);但不能这么写,只是意会 };
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
Box operator +(const Box&); Box operator+(const Box&,const Box&);
重载赋值号(等号)
-
在定义对象时直接使用赋值号相当于调用拷贝构造函数,如A a1 =a2;相当于A a1(a2);
-
对已定义对象赋值相当于调用重载的operator = 函数,如无显式定义,则默认进行对象的浅拷贝
classA{ public: int n; char* ptr; A(int n){ //省略...... } A& operator =(A& other){ this->n=other.n; this->ptr=other.ptr; return *this; } }
mymcpy
拷贝
为什么有了指针还要引用
-
引用与指针的作用类似:使得函数可以就地修改参数,避免函数的拷贝
-
且引用的底层实现也利用了指针
-
因此你应该有此疑问非常久了:为什么有了指针还要有引用
-
现在学习了运算符重载,你应该可以回答这个问题了:
引用使得使用运算符重载时可以不必带着*符号
既清晰又美观,有避免歧义
类的模块化编程
-
在C语言中,为了代码的封装,我们会将函数的声明与定义分开
-
函数声明在.h头文件中,定义在.c源代码文件中,将.c编译为二进制文件后同.h一起交付给使用方
-
这样使用方编写时只看见声明,不会知道具体实现方式
-
C++中编写类也可以使用相同的模块化方式,将声明放在.h文件中,实现放在.cpp文件中
//MyClass文件 #ifndef MYCLASS_H #define MYCLASS_H class MyClass{ private: int n; public: MyClass(int n); int get_n(); void set_n(int n); }; #endif
//MyClass.cpp文件: #include"MyClass.h" MyClass::MyClass(int n) { this->n=n; } int MyClass::get_n(){ return n; } void MyClass::set_n(int n){ this->n=n; }
//main.cpp文件: #include<iostream> #include"MyClass.h" int main(){ Myclass a(5); std::cout<<a.get_n(); }
类的静态成员
静态static的概念
-
回顾:函数中的static变量存在于全局/静态区,生命周期伴随整个程序,不随这函数结束而死亡
void func(){ static int n=1; cout<<n+1<<"\n" } //省略....... func();//1 func();//2 func();//3
类的静态成员
-
隶属于类,不属于任何一个对象,生命周期贯穿整个程序
-
类比:静态成员变量是写在设计图纸上的数据,与楼无关
-
类内可直接访问,外部则需要通过类名::变量名访问
-
静态成员变量必须在类声明外部单独初始化!
-
格式:数据类型 类名::变量名=初始值;
一个例子搞懂类的static成员:对象计数
-
类利用其static成员变量来记录它的实例化对象的实时数目
class A{ public: static int count; A(){ count++; } ~A(){ count--; } }; int A::count = 0;
A a1; cout<<A::count<<"\n"; A a2; cout<<A::count<<"\n"; { A a3; count<<A::count<<"\n"; } count<<A::count<<"\n";
第六章
构造函数是在创建对象时被执行的
对象的生命周期:分配内存->构造->使用->析构->回收内存
构造函数的特征:
-
构造函数的函数名与类名相同
-
构造函数可以重载
-
构造函数可以设置缺省参数
-
构造函数必须指定类型说明(×)
第七章
全局对象cin
类:istream
成员:
cin.get(ch)
cin.operator>>()
全局对象cout
类:ostream
成员:
cout.put('A')
cout.operator<<()
第八章模板
-
模板与泛型的概念
-
模板函数
-
模板类
理解以下与函数有关的名词:
-
泛型编程
-
参数类型化
-
模板函数
-
模板类
-
模板类型参数
-
参数占位符
-
模板实例化
-
类型判断
思考并回答以下问题:
-
泛型编程/类型参数化的目的是什么
-
C++如何确保模板逻辑可以正确应用在类型参数上
熟悉以下关键字:
-
template
-
typename
熟悉以下代码写法:
-
编写和实例化模板函数
-
通过类型判断自动实例化模板函数
-
*编写带类型参数和非类型参数的模板类
模板与泛型的概念
模板template
模板编程的目的
-
思考:如何利用函数重载实现不同类型数据的自定义相加(拼接)?
//实现三个重载函数: string myAdd(int a,int b){ return to_string (a)+to_string(b); } string myAdd(float a,float b){ return to_string (a)+to_string(b); } string myAdd(double a,double b){ return to_string (a)+to_string(b); } //调用 cout<<myAdd(111,999); cout<<myAdd(3.14f,2.27f); cout<<myAdd(3.1415926535,2.277777777); //调用方确实减少了代码量,实现了代码复用 //但实现方仍然冗余:一样的逻辑为三种类型各写一遍
模板编程的目的
-
回顾:从函数->函数重载 ->面向对象 ->继承/多态->…核心目的?
-
代码复用
-
但以上技术仍然是类型依赖的,需要为不同类型进行相应的实现,如函数重载
-
即"数据类型"信息是一个超参数,代码逻辑依赖这个超参数信息
-
模板编程:
类型参数化,将类型信息也视作一个普通参数,使代码逻辑与类型信息分离
——泛型思想
模板编程的目的
-
对不同的类型变量进行自定义的拼接相加
//通过模板实现,实现一个模板函数 template <typename T1,typename T2> string myAdd(T1 a,T2 b){ return to_string(a)+to_string(b); } //调用 cout <<myAdd<int,int>(111,999); cout<<myAdd<float,float>(3.14f,2.27f); cout<<myAdd<int,float>(8,3.14f);
模板函数
template <typename T1,typename T2> string myAdd(T1 a,T2 b){ return to_string(a)+to_string(b); }
-
template关键字:表明接下来定义一个模板
-
typename关键字:表明该模板参数是一个类型名
-
T1,T2:是两个类型形式参数,是类型占位符
-
于是在my Add函数中就可以用T1和T2指代数据类型,编写代码逻辑