C++笔记
C++笔记(尚未整理完毕)
小建议
如果你拥有C语言程序设计基础以及数据结构相关知识,理解以下内容会更加容易。
如果你在阅读过程中遇到了不理解的概念请自行上网搜索或查阅相关专业书籍,还是不懂的话可以记下来然后先行跳过。因为计算机知识体系庞大,光有编程语言的基础是远远不够的,所以需要循序渐进地学习,但不用紧张,因为目前我们还不需要很深的知识,需要的是思考、动手和耐心。
如果你是非英语母语者且英语词汇量比较匮乏的话,建议你在后面编程的过程中,尽可能去留意这些英文缩写,使用合法的英文和数字等组合命名 函数(Function)
和 变量(Variable)
。平时可以上谷歌去熟悉全英文环境,可以去啃一些英文 markdown
文档(如果时间允许的话),让自己逐渐熟悉全英文环境,这对于后期的学习很有帮助。
本篇文章就是使用markdown语法编写的,文件后缀名是 .md
。
简介
计算机编程语言包括 机器语言
、汇编语言
和 高级语言
。机器语言使用二进制 0
和 1
,通过向计算机 终端(Terminal)
输入二进制数来使计算机执行各种指令,但这种语言对于人来说不易读、不易维护且耗时长,于是便逐渐发展出了汇编语言,在此基础上又进一步发展出了高级语言。
高级语言通过 编译器
转换成汇编语言,汇编语言通过 汇编器
转换成机器语言并最终使计算机运行 程序(Program)
。这种上一个程序的输出是下一个的输入的程序组合也称为 工具链
。
与 面向过程
的C语言不同,C++是由C语言发展而来的一门 面向对象
的高级语言,类似于C#, Java, Go, Rust 等等。
另外,除了以上提到的 编译型语言
,随着时代发展,又出现了 解释器
。解释器使程序直接读取程序并将其转化为中间形态后直接执行。这种 解释型程序(脚本)
与 编译程序
只需编译一次之后计算机就能直接执行编译好的机器指令不同,像是Python、PHP、Ruby、Perl等等语言每次执行都需要经历这个过程,因而程序运行比较慢,这种语言叫做 解释性语言(脚本语言)
。
虽然这类语言因为这种特性受到诸多限制而被一些程序员所诟病,但我们也应该清楚工具是为人服务的,不存在着一种完美的工具,我们应该做的是根据自己的实际需求和项目具体情况来选择工具以便高效地完成我们的工作。
准备工作 | 针对Windows系统
设备:
-
一台电脑(PC)
开发环境(IDE):
下载 社区版 | Microsoft Visual Studio
到你喜欢的位置,最好在C盘以外的位置新建一个文件夹并命名,因为C盘存储空间不足会影响系统性能,确保你有足够的空间来下载和安装开发环境。注意命名文件夹时尽量不要出现空格,尽量使用英文大小写加数字或者分隔符 _
的命名方式。
系统兼容性 | 需要注意你的系统版本是否支持,查看 系统要求
想要查看自己的系统版本可以通过windows系统方便的图形化操作找到并选择 设置
>> 系统
>> 系统信息
。
不过我更建议你学着使用命令解释器 cmd(command)
,因为在很多系统中并没有windows系统如此方便但繁杂的图形化界面,手动在控制台输入指令是最快捷的操作,事实上早期计算机工程师也是如此操作的。
具体操作方法:同时按下 win+R
(win键通常在键盘左侧alt键的左边),会弹出一个运行窗口,输入 cmd
,按 Enter
回车即可弹出命令提示符窗口。接着输入 ver
(version缩写),计算机就会返回系统版本信息到控制台。以后的学习中我们会用到许多快捷键(热键),多多使用这些操作以提高自己的效率。
如果你不知道如何清理C盘,可以查看 [文件清理软件收录]
你随时可以按 Alt+Tab 来切换窗口,继续我们的学习。
-
我们当然也可以使用记事本来编写代码,但是也要记得记事本生成的是文件后缀为
.txt
的文本文件,计算机是不能直接读懂的,想要计算机运行需要将其编译为可识别指令。 -
集成开发环境 (IDE) 集中了许多有助于你编写和调试代码的开发工具,其中就包括编译器。
-
你可能会注意到有相似名称的软件叫
VS code
,但是与集成开发环境不同,它只是代码编辑器。
运行环境(RE):
- 安装C++语言运行库
- 配置环境变量
新建项目
创建一个工程项目,命名可以为 myProject_01
,存放到安装目录的工程项目目录下。
新建文件
在项目下创建一个C++源文件,文件后缀是 .cpp
一、打印输出文本 Hello World!
我们可以试试直接在代码编辑区输入一段文本,你会发现文本底部会有红色波浪线,Microsoft Visual Studio(以下简称VS)
会报告错误,这是因为你写的文本内容无法被当做指令直接被计算机执行。简单点来说,就是计算机看不懂人话。
所以,我们要掌握正确的语法。
首先我们先来输入以下代码,然后 Enter
换行,这是预处理阶段的代码
1 | #include <iostream> |
当你输入 <
,你会发现VS自动补全了 >
,代码补全功能是这个软件的优势之一
接着输入以下代码。int是整型,这里代表该函数的返回类型是整数类型,函数末尾可以返回一个整数(通常是0),main是主函数,也是我们程序运行的入口,一个程序里只能写一个主函数。圆括号 ()
是该函数的参数列表,如果没有参数可以填void或者不填,花括号 {}
之间的作用域就是给我们用来输入想让计算机输出内容的代码的位置,我们也把花括号之间的区域称为一个 代码块(block)
1 | int main() |
注意输入法是英文
然后我们来完善主函数代码块,return 0
是一个语句,语句指用于执行某个操作的指令,必须以 ;
结尾,在这里的意思是返回一个整数0(习惯上通常代表程序正常终止)
1 | #include <iostream> |
如果你不慎输错了导致代码排版没对齐,建议你按键盘左上角往下第三个键 Tab
来缩进,大部分格式无误的情况下,VS会自动帮你缩进。
我们可以先按 Ctrl+F5
来执行上面的程序,程序正常执行后会弹出终端控制台窗口 cmd
,按任意键退出。
继续完善我们的代码,我们最终要输出语句打印到计算机屏幕上,如果你学过C语言可能会用到诸如 print()
,println()
和格式化输出函数 printf()
之类的用于输出语句,在C++里,我们一样可以用。这里的 Hello World!
是一个字符串,是不能作为指令让计算机来识别的,所以要用 ""
双引号表示,这样计算机不会将他当做指令从而导致程序出错,你可以用 ''
单引号来表示单个字符,不过那样做比较麻烦。
1 | #include <iostream> |
按 Ctrl+F5
执行,正常执行后关闭终端窗口。小插曲:为何不试试按下 Alt+F4
呢?(笑)
我们可以试试输出两个字符串或者把文本中的文字和感叹号分开打印
1 | #include <iostream> |
执行看看输出结果
或者这样
1 | #include <iostream> |
再执行看看输出结果
你会发现即便我们将他们分开写了,输出的文本依旧在同一行,那我们要如何让计算机执行换行操作呢?我们可以使用换行符 \n
1 | #include <iostream> |
执行看看
为了更简便地输出文本,我们可以直接用输出流运算符 <<
(重载左移操作符),使其输出的对象可以是C++内置的数据类型和标准库 std
包含的类的类型,接着要调用标准库的标准输出流对象 cout
。这样输入的数据会先存放到流缓冲区,待缓冲区刷新之后才输出到指定位置,当数据暂存于缓冲区时我们仍有对其进行修改的方法,这个后面会讲到。
1 | #include <iostream> |
执行看看
接下来,我们使用标准库里的 endl
代替 \n
1 | #include <iostream> |
你是否会觉得每调用一次标准库里的对象就要加上 std::
会有些麻烦?
我们可以在预处理下面一行输入 using namespace std
代表调用标准命名空间,这样就不用每遇到一个标准库里的对象就要声明一次 std::
1 | #include <iostream> |
endl (endline缩写) 和 \n 一样也有换行的作用,但除此之外还会刷新缓冲区
我们运行以下代码并观察终端窗口
1 | #include <iostream> |
你会发现上面的代码执行后的终端窗口总是直接退出了,那如果我们要让窗口显示输出内容之后便于我们观察使窗口停留在我们屏幕上可以怎么做?
试试把 return 0
替换成 system("pause")
完整代码
1 | #include <iostream> |
最后 ctrl+F5
执行程序(暂时不需要用到调试)
补充
1.输入输出流 iostream
:
也称为 I/O流
,是C++ 中用于输入和输出数据流的 基础类库(BCL)
,io
代表 input 和 output ,是 istream类
和 ostream类
两个类名的头字母缩写。stream
代表数据之间的传输操作。
iostream 也是由抽象基类 ios
派生出的 istream
和 ostream
通过 多重继承
而派生出的 类
。
除此之外,还有用于 文件(file)
输入和输出的 fstream
以及用于 字符串(string)
输入和输出的 strstream
。
2.预编译处理器指令 #include
:
预处理器将指示计算机的编译器在实际编译前所需完成的准备工作,这个过程也称为 预处理
。所有的预处理器指令以 #
开头,其中最常见的是 #include
,用于把 头文件
包含到源文件中。
头文件里一般是方法声明,一开始的C语言写的 .c
文件太长,所以后来就将源文件分开,一部分方法声明放在另一个文件里再写在源文件开头,所以称作头文件,而编译好的既包含外部源文件也包含头文件的二进制文件叫做 (静态)库文件
,在项目中显式包含的头文件也叫 外部依赖
,类似于Java里的 包(package)
。
此外,如果是自定义的头文件则不是用尖括号 <>
括起来表示,而应该用 ""
。
1 | #include<iostream> |
以后我们可能还会用到 #define
,这个预处理指令用于创建符号常量,该符号常量通常称作 宏
,这个指令也叫做 宏定义
。简单来说就是给常量取个名字,就像我们称呼圆周率3.1415926535……为π一样,π就是圆周率的名字,建立了这样的对应关系,使得我们可以用字符去替换常量,而且编译器也能读取并转换成相应的常量。
1 | #define PI 3.14 |
要注意宏定义只是简单的字符串代换,是在预处理阶段完成的。
3.命名空间
4. cin
和 cout
:
属于上面提及的两个类的对象,分别意为输入和输出,读作 c-in
,c-out
。C++标准命名空间你可以理解为包含了许多指令的代码库,也是现今在旧C++标准库基础上新的C++标准库,里面包含了这 cin 和 cout 这两个对象,需要执行输入输出操作时可以将这两个对象从库中调出来。
如:
1 | std::cout << "the string print on the screen." << std::endl; |
(这里的大于小于号事实上是箭头,代表数据流流向,不能随意改变)
5.int main(void){return 0;} :
Our programs need an entry point where your function is, so you need a function to receive what you input then output on the screen. main function is necessary.
6.system(“pause”) :
Use it so that our programs realize that it’s not end and your command window will not close itself automatically.
7.总结 :
now you see, when you want to print on the screen, your codes must contain 1 and 2(cout is necessary), the other one you need is a function, but that’s not what we want to talk about now.
Use keyword include
to taking the codes in the library(lib),like: #include<lib document>
常用库(lib):#include<math>
#include<string>
8.实操 :
1 | #include<iostream> |
模板:注意代码规范,可用Tab键缩进 可保留错误代码并注释掉用于复习
#include
using namespace std;
int main() //注释单行用”//“这个符号,了解编译器或者集成环境IDE的快捷注释按键,方便注释不用的代码
{}
多行注释 /**/
二、数据类型(变量与常量) data type (variable & constance)
知识补充:
• 1.存储单位:
①位(bit):即比特,计算机的最小存储单位。常见的民用计算机的机器语言只有”0”和”1”两位数字(二进制),一位存储单位里面只能是”0”或者”1”。你可能已经在你的计算机见过的类似“32位”或者“64位”操作系统的描述之类的。
②字节(byte):一字节等于八位,1byte = 8bits
一个英文字母或者数字是一个字节,而汉字是两个字节。
• 2.进制
①二进制 Base-2:01
从〇开始,逢二进一(位)
②十进制 Base-10:0123456789
③十六进制 Base-16:珠算就是,但我们要讲的是计算机,所以记住这个:0123456789ABCDEF(到10换成英文字母表就行)
以下的稍微了解就行:
④八进制:半斤八两(早期计量单位换算)
⑤十二进制:一年有十二个月
2.1.进制间的换算
①十进制换算成二进制:十进制数除以二,直到除数比被除数大,也就是余数变成0为止,从下至上取余数。
②二进制换算成十进制:比如:1010 =12^(4-1)+02^(3-1)+12^(2-1)+02^(1-1) = 10
1.变量 variable :提供可命名的存储空间,变量类型决定其大小、内存布局及可用操作集。
1.1 基本数据类型 fundamental data types:
1.1.0 void:该函数为空值,不能指定为一个变量的数据类型,通常用于描述函数的返回值类型,即不返回任何内容。
1.1.1 整数数据类型
①整型 int :
int 整数类型,通常为4个字节,用于存储整数,小数点后的数据会被直接忽略(不四舍五入)
short 短整型,<= int
long 长整型, >= int
long long 长长整型
②字符型 char :
char 字符型,1byte
③布尔类型 bool
bool 布尔类型,结果为真True或假False,1bytes
1.1.2 浮点数据类型
①浮点型 float : 由整数部分和小数点后面的小数部分组成,输入计算机后将以科学计数法来表示,en表示10的n次方。
float(单精度) 单精度浮点型,4bytes
double(双精度) 双精度浮点型,>= float
1.1.3 str()类型
④字符串 string :
String 字符串,可以用str()函数将任意整型和浮点型转为字符串
1.2.0 关于变量的分类
全局变量与局部变量:函数内或者函数的参数列表内的变量为局部变量,而函数外部定义的变量为全局变量。函数可以引用外部的变量,而外部却不可以引用内部变量。
1.2.1 变量地址
创建的每一个变量都会分配相应数据类型大小的存储空间,所有变量中的数字会以16进制存储在计算机中,该空间中每1个内存槽为1个字节,也就是每段有8位。当变量被创建后,相应的内存也会被划分出来与变量绑定,也就是说无法将其他变量指定给该内存位置。被创建的变量有了特定的存储段,这些段驻留在硬件上并分配了一个特定数字作为他的地址以便计算机定位该存储段并读取其中的1和0进行翻译,而这个地址就是变量地址。通常该地址为存储段的第一个字节。(指针)
2.常量 const :
①数字(值)
②字符字面值与字符串字面值:输入的”字符”在开辟的空间中会以值的形式保存(而不是字符本身,字符是给人看的,计算机在实际运算中用的是值)。
③加上关键字’const’的变量,如:const string my_TutorialStr
3.命名规范 :
①Camel Case(驼峰命名法) eg(例子) : myFunctionA 头部单词小写,两个“驼峰”的单词首字母大写。这样的写法较为易读。
②不要与标识符与关键字重复!!!(重点)
诸如:int、for、return等等这些在编译器内部是有用处的,不能用于命名(否则计算机因为识别成相关用途而导致编译出错)。
但是像”int”这样的就没问题,”“是分隔符,常用来在变量名中分隔单词,如:”my_name”这样的命名。
③数字不要放在变量名开头!!!(重点)
④尽量不要加一些奇奇怪怪的符号,容易出错。一般情况下命名纯英文就好(不排除不支持中文的情况,笔者并非都试过,你可以试试)。
4.语法
①进入函数,输出一个局部变量
数据类型 函数名(参数列表)
{
数据类型 变量名;
变量名 = 值;
cout << 变量名 << endl;
}
5.实操
①
#include
using namespace std;
void main() //定义了返回值为void的主函数
{
int a; //定义了一个整型a
a = 1; //将1赋值给a
cout << a << endl; //输出a到屏幕上
}
②
#include
using namespace std;
int main() //要不要试试写两个主函数会怎么样?
{
int x; //定义一个整型x,此处开辟了一个名字是x的变量空间,可以用来存储数据
x = 1; //将1赋值给x,注意这里的”=”不是等于号而是赋值,后面运算符会讲到
short b = 2; //你可能会在游戏加载界面看到「正在初始化」这样的语句,定义一个变量并赋值就叫做"初始化"
long c(3); //初始化不用"="而是用"()"来传递括号内的值给变量也是可以的
x = 5; //试着给x重新赋值,此时变量空间里的1被5代替,也就是覆写(overwritten)
cout << x << endl; //输出赋值后的变量到你的屏幕看看
cout << b << endl;
cout << c << endl;
char myCharA = 'a'; //此处数据类型是字符而不是字符串,所以要用单引号传递单个字符
cout << myCharA << endl;
string myStringA = "abc"
cout << myStringA << "\n"; //此处的"\n"与endl一样可用来换行,不同的是endl会刷新缓冲
system("pause"); //停止窗口,方便你继续操作。你也许可以试试把这句换成"return;",编译完成后窗口会自动关闭
//按下F5,Debug,启动!
}
三、运算符与优先级、转义符与标识符
1.算术运算符
运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
– 自减运算符,整数值减少 1 A– 将得到 9
增量运算符:
①前自增(减) ++a, –a : 先执行a+1,a-1的操作后赋值
②后自增(减) a++, a– : 先执行赋值,后将a+1,a-1之后的字面量在函数中保存为副本,以便下次使用
2.关系运算符
运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 为假。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 为假。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 为假。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。
3.逻辑运算符
运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 (A && B) 为假。
|| 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 (A || B) 为真。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 !(A && B) 为真。
分类.单目运算符与双目运算符
4.其他运算符:取地址运算符”&”
5.转义符与ASCII码
6.标识符
7.访问修饰符:
(缺省)无修饰符
public private protected
四、语句与表达式 statement & expression
1.statement : combine what you input and return a value. Like: 1+1,”here a sentence”.
2.expression : execute in sequence and carry out some action. Like: int x;
3.expression statement : statement that cause expression to be evaluated. Like: x = 1;
五、真值和字面值(量)
1.真值 True or False
①0的真值永远是假False,无论数据类型,其余则被计算机视为真True。
②真值可以用于控制语句不同情况下是否执行。
③所有字符都有一个数字值,因此也具有真值。
2.字面值:
在C++中,我们会常用1~10以及True之类的人类能看懂的字符来表示赋值给变量对应的常量值或者真值,名为其实,这些就叫做字面值。
六、作用域 Scope
{block;}花括号里的即代码的「作用域」,从左花括号到右花括号的区域也叫做「块」。在特定块的范围内创建一个变量,该变量具有局部作用域,即该变量只能作用于该区域内部而不能直接从外部调用,这种变量也称作「局部变量」。而在作用域之外的变量则称作「全局变量」,所有块中均可调用该变量。
七、if_else if条件循环语句和switch语句
①多次检查条件
if(condition)
{
表达式或语句;
}
else if
{
表达式或语句;
}
②检查一次条件
switch(selection)
{
case 1:
表达式或语句;
break;
case 2:
表达式或语句;
case 3:
表达式或语句;
break;
…
}
八、do_while()循环语句和for循环语句
①先执行再判断条件,不满足则跳出循环(break out)
do
{
表达式或语句;
}while(控制语句);
while(控制语句)
{
表达式或语句;
}
②先判断执行条件,满足则进入循环,在指定条件和次数内不断迭代
for(初始化 Initialization;控制语句 Condition Evaluation;自增语句 Increment)
{
表达式或语句;
}
九、函数 function(参数列表)
①函数原型(与C语言最大的不同)
如果在没有函数原型的情况下直接将你定义的函数放在主函数的下方会怎样?
②函数重载(函数名相同,传参不同)
当你需要重复利用一个函数,但需要他输出不同的数据类型的变量时,可以使用函数重载。
十、初始化
1.等号赋值
2.括号初始化
①()
②{}
十一、引用
为函数创建别名的方法。(类似于程序本身和快捷方式的关系)
注:引用必须在声明时初始化。(与指针的不同之处)
例如,你创建了一个函数,你想改变函数内的变量,而不是用一个新的常量字面值副本去替换原来的副本赋值给变量。你可以创建一个新的函数,通过取地址运算符”&”获得其输入参数中的变量地址并获取该地址,之后会复制一个该地址的副本,并将这个新函数作为前一个函数的别名,这样他们就实际上使用的是相同的内存。
void function(int& variable);
int main()
{
int my_var = 1;
function(my_var);
}
void function(int& variable = 2)
{
cout << variable << endl;
}
十二、数组 arr[] –一个变量多个值
①arr[n] = {1,2,3,4…n};
②利用for循环迭代来输出数组
③数组的内存空间连续
十三、枚举 enum
1.枚举是你自定义了一种数据类型,但事实上并没有创建一个实例或者变量,后续你可以通过声明来创建该数据类型的变量。
2.试着用switch语句来为枚举的自定义数据类型下的变量创建多个事件,这样当你想更新变量时不用经历繁复的检测,而是从多个事件中选择相应的事件,比if语句更加高效。
名词解释:
实例(instance):在面向对象程序设计中,“类”在实例化之后叫做一个“实例”。 “类”是静态的,不占进程内存,而“实例”拥有动态内存。
实例和对象基本上是同义词,它们常常可以互换使用。对象代表了类的一个特定的实例。对象具有身份(identity)和属性值(attribute values)2个特征。实例是对象的具体表示,操作可以作用于实例,实例可以有状态地存储操作结果。实例被用来模拟现实世界中存在的、具体的或原型的东西。
实例与对象的区别:对象就是类的特定实例,所有的对象都是实例,但并不是所有的实例都是对象。例如,一个关联(UML关系中的一种)的实例不是一个对象,它只是一个实例、一个连接。我们常见的实例都是类的实例,此时二者没有区别。除了类的实例外的实例都不是对象。
示例:
#include
using namespace std;
enum Playerstatus
{
ps_default,
ps_walk,
ps_run,
ps_attack,
ps_beatk
};
enum movement
{
move_defalt
};
int main()
{
Playerstatus status;
status = Playerstatus::ps_default;
if (status == Playerstatus::ps_default)
{
cout << "status already" << endl;
}
system("pause");
}
十四、结构体 struct
枚举与结构体?
十五、指针 pointer
1.基本特点
①使用场景:我们需要使用变量时,往往需要函数里的输入参数,而这个参数有可能会是非常长的数组或者结构体有着庞大的变量与函数的容器等等类似的,在内存中传递大的对象非常低效,程序会变慢,且这种情况在游戏中玩家是可视的,此时与其传递变量本身,不如直接传递该变量地址(一串数字),这是需要用到”指针”,”指针”就是保存其他变量地址的变量。之后我们就可以使用该地址访问变量并获得相应数据。
②内存地址一般以十六进制显示
③指针不需要像引用一样在声明时初始化。
示例:
#include
using namespace std;
int main()
{
int a = 100;
int* aPtr;
aPtr = &a;
cout << aPtr << endl; //输出变量地址
cout << *aPtr << endl; //解引用指针,输出变量
system("pause");
}
2.指针传递数组:传递的是数组第一个元素的地址(数组的内存空间是连续的)
示例:
①执行下面代码试一试
#include
using namespace std;
int main()
{
int a[] = {0,1,2,3,4,5};
int* aPtr;
aPtr = &a;
cout << aPtr << endl; //输出变量地址
cout << *aPtr << endl; //解引用指针,输出变量
aPtr++;
cout << *aPtr << endl; //解引用指针,输出变量
aPtr++;
cout << *aPtr << endl; //解引用指针,输出变量
system("pause");
}
②
#include
using namespace std;
int main()
{
int a[] = {0,1,2,3,4,5};
int* aPtr;
aPtr = a; //数组名代表其中第一个元素的地址,因而不需要取址符
cout << aPtr << endl; //输出变量地址
cout << *aPtr << endl; //解引用指针,输出变量
aPtr++;
cout << *aPtr << endl; //解引用指针,输出变量
aPtr++;
cout << *aPtr << endl; //解引用指针,输出变量
system("pause");
}
3.指针传递结构体
4.多重指针
5.堆与栈 Heap and Stack
5.1动态内存
①是什么
栈用于存储主函数等静态变量,调用这些静态变量时需要将栈顶的函数取出直到取出该变量所在的函数。堆则提供了动态内存,可以随时添加和删除动态内存里的函数和变量,在创建这些函数和变量时,使用new关键字在栈的构造函数中创建指针(变量),赋值给成员变量和函数,并返回获取的地址,从而让栈得以访问堆,实现类在构造函数中动态分配内存的目的。
②为什么要动态分配内存
空指针与野指针
5.2 实操
①谨慎尝试下面代码
#include
#include
using namespace std;
struct Character
{
string Name;
float Health;
};
int main()
{
for (int i = 0; i < 100000000;i++)
{
Character* PtrToChar = new Character();
PtrToChar->Name = "114514aaaaaaa";
cout << PtrToChar->Name << endl;
}
system("pause");
}
//上方的代码使用了new关键字,导致进程循环在堆中创建新的实例占用内存空间,而没有去释放空间,造成内存泄漏
②
#include
#include
using namespace std;
struct Character
{
string Name;
float Health;
};
int main()
{
for (int i = 0; i < 100000000;i++)
{
Character* PtrToChar = new Character();
PtrToChar->Name = "114514aaaaaaa";
cout << PtrToChar->Name << endl;
delete PtrToChar;
}
system("pause");
}
③
#include
#include
using namespace std;
struct Character
{
Character();
void PrintHealth();
string Name;
float Health;
};
Character::Character()
{
Name = “Default Name”;
Health = 100.f;
}
void Character::PrintHealth()
{
cout << “Health is:” << Health << endl;
}
int main()
{
for (int i = 0; i < 10;i++)
{
Character* PtrToChar = new Character();
cout << PtrToChar->Name << endl;
PtrToChar->PrintHealth();
delete PtrToChar;
}
system("pause");
}
//思考什么时候指针需要解引用,什么时候不需要?(普通成员函数与类成员函数)
十六、对象和类
示例:
#include
#include
using namespace std;
class Player
{
string Name;
int Age;
float Hitpoint;
void Attack()
{
cout << "Succeed" << endl;
}
};
int main()
{
Player p1;
Player p2;
system("pause");
}
2.构造函数(constructor)与完全限制名称:类中可以包含函数,当类体内没有函数时可以调用自动创建的默认构造函数,构造函数可以在类外部初始化类变量值,在类外部为其变量提供默认值,构造函数命名与类名相同,且构造函数没有返回值,不能说明返回类型为void。
实例:
①执行下面代码试一试
#include
#include
using namespace std;
class Dog
{
string name;
int age;
float Health;
void Bark()
{
cout << "Woof!" << endl;
}
};
int main()
{
//主函数内定义dog类的实例dog
Dog dog;
dog.Bark();
system("pause");
}
②
#include
#include
using namespace std;
class Dog
{
//先公开类,因为类体内默认私有变量
public:
string name;
int age;
float Health;
void Bark()
{
cout << "Woof!" << endl;
}
};
int main()
{
//主函数内定义dog类的实例dog
Dog dog;
//调用对象内的函数Bark
dog.Bark();
system("pause");
}
//到这里,你已经清楚了结构体的默认访问修饰符是public,而类默认private,所以要将其公开来访问其中的变量和函数
//那如果我们想要Bark函数在dog实例被创建后立即调用该怎么做?
③
#include
#include
using namespace std;
class Dog
{
public:
Dog()
{
Bark();
}
string name;
int age;
float Health;
void Bark()
{
cout << "Woof!" << endl;
}
};
int main()
{
//主函数内定义dog类的实例dog
Dog dog;
system("pause");
}
④类内声明,类外定义
#include
#include
using namespace std;
class Dog
{
public:
Dog();
string name;
int age;
float Health;
void Bark();
};
int main()
{
//declear variable dog of class Dog inside main function,instance will be created
//his constructor will be automatically called and the function Dog will run
Dog dog;
//access the member vairable with dopt operator
cout << dog.name << endl;
cout << dog.age << endl;
cout << dog.Health << endl;
dog.name = "Nameless";
dog.age = 18;
dog.Health = 100.f;
cout << dog.name << endl;
cout << dog.age << endl;
cout << dog.Health << endl;
system("pause");
}
Dog::Dog()
{
Bark();
name = “Default Name”;
age = 10;
Health = 100.f;
}
void Dog::Bark()
{
cout << “Woof!” << endl;
}
⑤对比构造体
#include
#include
using namespace std;
struct Cat
{
//重载
Cat();
string a;
int b;
void Meow();
};
Cat::Cat()
{
a = “Meow”;
b = 1;
Meow();
}
void Cat::Meow()
{
cout << a << endl;
cout << b << endl;
}
int main()
{
Cat cat;
system(“pause”);
}
⑥
//requirement:use struct or class,output a number pair which you can input num for it
#include
using namespace std;
class a
{
public:
int x, y;
void setPoint();
void numPair();
};
void a::setPoint()
{
cout << “please input num as X” << endl;
cin >> x;
cout << "please input num as Y" << endl;
cin >> y;
numPair();
}
void a::numPair()
{
cout << “(“ << x << “,” << y << “)” << endl;
}
int main()
{
a np;
//注意此处的setPoint()与上个案例中的构造函数Cat不同,此处并非构造函数,因而需要调用该方法
np.setPoint();
system(“pause”);
}
3.子类
①无子类,通过有参构造函数初始化变量或者重载构造函数并传参到主函数进行初始化值
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name,string Species, int Count);
string name;
string species;
int count;
void info();
};
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
animal::animal(string Name, string Species, int Count)
{
//参数列表中使用占位符,此处用占位符即可,传参到主函数中初始化
name = Name;
species = Species;
count = Count;
cout << “again” << endl;
}
void animal::info()
{
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
}
int main()
{
animal anm;
anm.info();
animal anm2("tu","rabbit",2);
anm2.info();
system("pause");
}
②有参构造与初始化列表
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name,string Species, int Count);
string name;
string species;
int count;
void info();
};
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
//初始化列表
animal::animal(string Name, string Species, int Count):name(“tutu”),species(“rabbit”),count(3)
{
cout << “nin chi le ma?” << endl;
}
void animal::info()
{
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
}
int main()
{
animal anm;
anm.info();
animal anm2("tu","rabbit",2);
anm2.info();
system("pause");
}
3.1继承
①子类
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name, string Species, int Count);
string name;
string species;
int count;
void info();
};
//创建子类,注意此处的访问修饰符只是针对父类。
class Dog:public animal
{
public:
Dog();
};
int main()
{
Dog dog;
dog.info();
system("pause");
}
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
animal::animal(string Name, string Species, int Count) :name(“tutu”), species(“rabbit”), count(3)
{
cout << “nin chi le ma?” << endl;
}
void animal::info()
{
cout << endl;
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
cout << endl;
}
Dog::Dog()
{
//因为子类的构造函数Dog()不含参,因而默认调用的是父类不含参的animal()函数
//程序将优先调用父类构造函数(观察程序输出顺序)
info();
cout << “here” << endl;
}
②如果要调用父类重载的构造函数呢?试试下面的代码
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name, string Species, int Count);
string name;
string species;
int count;
void info();
};
class Dog:public animal
{
public:
Dog();
Dog(string Name,string Species,int Count);
};
int main()
{
//此处初始化之后,看看程序输出的是什么?
Dog dog{“pipi”,”pig”,3};
dog.info();
system("pause");
}
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
animal::animal(string Name, string Species, int Count) :name(“tutu”), species(“rabbit”), count(3)
{
cout << “nin chi le ma?” << endl;
}
void animal::info()
{
cout << endl;
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
cout << endl;
}
Dog::Dog()
{
//因为子类的构造函数Dog()不含参,因而调用的是父类不含参的animal()函数
//程序将优先调用父类构造函数
info();
cout << “here” << endl;
}
Dog::Dog(string Name, string Species, int Count)
{
cout << “happy happy happy” << endl;
}
③调用父类重载有参构造函数
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name, string Species, int Count);
string name;
string species;
int count;
void info();
};
class Dog:public animal
{
public:
Dog();
Dog(string Name,string Species,int Count);
};
int main()
{
//为何值与此处不同?如果想用这种方法初始化该如何解决?
Dog dog{“pipi”,”pig”,3};
dog.info();
system("pause");
}
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
animal::animal(string Name, string Species, int Count) :name(“tutu”), species(“rabbit”), count(3)
{
cout << “nin chi le ma?” << endl;
}
void animal::info()
{
cout << endl;
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
cout << endl;
}
Dog::Dog()
{
//因为子类的构造函数Dog()不含参,因而调用的是父类不含参的animal()函数
//程序将优先调用父类构造函数
info();
cout << “here” << endl;
}
//子类调用父类重载构造函数语法
Dog::Dog(string Name, string Species, int Count):animal(Name,Species,Count)
{
cout << “happy happy happy” << endl;
}
//能否默认优先调用子类构造函数?
④
//#include
//using namespace std;
//
//int main()
//{
//
//}
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name, string Species, int Count);
string name;
string species;
int count;
void info();
};
class Dog:public animal
{
public:
Dog();
Dog(string Name,string Species,int Count);
};
int main()
{
//为何值与此处不同?如果想用这种方法初始化该如何解决?
Dog dog{“pipi”,”pig”,3};
dog.info();
system("pause");
}
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
animal::animal(string Name, string Species, int Count) :name(“tutu”), species(“rabbit”), count(3)
{
cout << “nin chi le ma?” << endl;
}
void animal::info()
{
cout << endl;
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
cout << endl;
}
Dog::Dog()
{
//因为子类的构造函数Dog()不含参,因而调用的是父类不含参的animal()函数
//程序将优先调用父类构造函数
info();
cout << “here” << endl;
}
//子类调用父类重载构造函数语法
Dog::Dog(string Name, string Species, int Count)
{
animal(Name,Species,Count);
cout << “happy happy happy” << endl;
}
//能否默认优先调用子类构造函数?
⑤
#include
using namespace std;
class animal
{
public:
animal();
//overload
animal(string Name, string Species, int Count);
string name;
string species;
int count;
void info();
};
class Dog:public animal
{
public:
Dog();
Dog(string Name,string Species,int Count);
};
int main()
{
//Dog子类创建实例dog
Dog dog{“pipi”,”pig”,3};
system("pause");
}
//父类animal重载无参构造函数
animal::animal()
{
name = “niu”;
species = “cow”;
count = 2;
cout << “an animal” << endl;
}
//父类animal重载有参构造函数
animal::animal(string Name, string Species, int Count)
:name(Name), species(Species), count(Count)
{
info();
cout << “nin chi le ma?” << endl;
}
//在外部定义父类animal的成员函数info()
void animal::info()
{
cout << endl;
cout << “name:” << name << endl;
cout << “type:” << species << endl;
cout << “count:” << count << endl;
cout << endl;
}
Dog::Dog()
{
//因为子类的构造函数Dog()不含参,因而调用的是父类不含参的animal()函数
//程序将优先调用父类构造函数
info();
cout << “here” << endl;
}
//子类调用父类重载构造函数语法
Dog::Dog(string Name, string Species, int Count):animal(Name, Species, Count)
{
cout << “happy happy happy” << endl;
}
//能否默认优先调用子类构造函数?
⑥一般情况下,没有定义构造函数的子类会自动调用父类无参构造函数的重载(默认构造函数),但初始化列表使得有参构造函数的重载得以调用而不去调用无参构造函数的重载,如果在子类类体里调用父类有参构造函数的重载,而不是用初始化列表,那么无参和有参的重载都会被调用。在继承中,父类定义的变量和函数都会在继承链上传递,被子类所继承,即便是多重继承,子类也可以调用他们。上述例子的继承关系为“is-a”关系。
⑦定义子类时对父类需要使用访问修饰符(access modifiers),访问权限也会被相应的子类继承。
public继承:如果定义子类时父类的访问修饰符为public,则无论父类类体内部的变量和函数访问权限如何,子类亦是如此。
protected继承:除了父类中访问权限为private的成员变量和函数仍在子类中的访问权限是private,其余的都将以访问权限为protected被子类继承。
private继承:若在派生新类使用该关键字,则父类所有的变量和函数访问权限在子类中也都是private。
但注意,父类的访问权限可以被子类继承,但子类不一定能够访问继承自父类的这些变量和函数,比如:当父类的成员变量和函数访问权限为private时,这时子类会继承父类成员变量和函数private,但不能直接访问他们(偷家函数)。
访问说明符的权限:
public全局可以访问,protected只能在同一类以及派生新类中访问,private只能在该类体内访问。后两种访问权限都对子类具有限制性,注意使用时的具体情况。
3.2封装
3.2_1.getter 和 setter 访问器函数
①试试看
#include
using namespace std;
class creatrue
{
public:
creatrue();
private:
string name;
float health;
};
int main()
{
creatrue Igno;
Igno.name = “Igno”;
system("pause");
}
creatrue::creatrue()
{
cout << “A creatrue has been createed!/n”;
}
②通过Getter和Setter函数访问并修改私有变量
#include
using namespace std;
class creature
{
public:
creature();
//Getter and Setter to send the Name
void SetName(string Name);
string GetName();
private:
string name;
float health;
};
int main()
{
creature Igor;
Igor.SetName(“w”);
cout << Igor.GetName() << endl;
system("pause");
}
creature::creature()
{
cout << “A creatrue has been createed!\n”;
}
//Setter
void creature::SetName(string Name)
{
name = Name;
}
//Getter
string creature::GetName()
{
return name;
}
应用场景:
某些待解锁权限,例如游戏通关秘钥和道具。
③
#include
using namespace std;
class creature
{
public:
creature();
//Getter and Setter to send the Name
void SetName(string Name);
string GetName();
void SetHealth(int Health);
int GetHealth();
//HP system
void TakeDamage(float damage);
private:
string name;
float health;
};
int main()
{
creature Igor;
Igor.SetName(“w”);
Igor.SetHealth(100);
Igor.TakeDamage(20);
cout << Igor.GetName() << endl;
cout << Igor.GetHealth() << endl;
system("pause");
}
creature::creature()
{
cout << “A creatrue has been createed!\n”;
}
//Setter
void creature::SetName(string Name)
{
name = Name;
}
//Getter
string creature::GetName()
{
return name;
}
void creature::SetHealth(int Health)
{
health = Health;
}
int creature::GetHealth()
{
return health;
}
void creature::TakeDamage(float damage)
{
health -= damage;
}
④
#include
using namespace std;
class creature
{
public:
creature();
//Getter and Setter to send the Name
void SetName(string Name);
string GetName();
void SetHealth(float Health);
int GetHealth();
//HP system
void TakeDamage(float damage);
private:
string name;
float health;
};
int main()
{
creature Igor;
Igor.SetName(“w”);
Igor.SetHealth(100.f);
Igor.TakeDamage(20.f);
cout << Igor.GetName() << endl;
cout << Igor.GetHealth() << endl;
system("pause");
}
creature::creature()
{
cout << “A creatrue has been createed!\n”;
}
//Setter
void creature::SetName(string Name)
{
name = Name;
}
//Getter
string creature::GetName()
{
return name;
}
void creature::SetHealth(float Health)
{
health = Health;
}
int creature::GetHealth()
{
return health;
}
void creature::TakeDamage(float damage)
{
float Total;
Total = health - damage;
if (Total <= 0.f)
{
cout << name << "died" << endl;
}
//为什么在else里无论输出Health还是Total得出来的都是同样的结果,按理来说将Health减去一次damage并赋值给他的时候Total不是要再减一次吗?
else
{
health -= damage;
cout << "health:" << Total << endl;
}
}
4.析构函数(Destructor):用于销毁。class ~Name()
①析构函数在使用delete关键字后将执行,同构造函数一样,其函数名与类名必须一致,且函数不能说明返回类型(没有返回值)
#include
using namespace std;
class Character
{
public:
Character();
~Character();
};
int main()
{
Character* PtrToCha = new Character();
delete PtrToCha;
system(“pause”);
}
//通过输出字符串来检测是否成功执行
Character::Character()
{
cout << “the function has been created!” << endl;
}
Character::~Character()
{
cout << “the function has been deleted!” << endl;
}
②
#include
using namespace std;
class Character
{
public:
Character();
~Character();
int* CharNum;
float* HP;
};
int main()
{
Character* PtrToCha = new Character();
delete PtrToCha;
system("pause");
}
//通过输出字符串来检测是否成功执行
Character::Character()
{
cout << “the function has been created!” << endl;
CharNum = new int(1);
HP = new float(100.f);
}
Character::~Character()
{
cout << “the function has been deleted!” << endl;
delete CharNum;
delete HP;
}
十七、静态变量 Static Var
1.static variables in functions : 静态变量存在于内存的各个部分(堆)中,例如局部变量。在函数中,局部变量的作用域无法超出函数的生命周期,基本上所有的局部变量都在声明他们的作用域的末尾被销毁,因此调用函数的时候会重复创建变量,初始化,运算然后删除变量的流程。静态变量存在于函数生命周期之外,受作用域影响,直到程序生命周期结束之前静态变量占用的内存都不会被删除,当第二次调用栈内的函数时,第一次初始化的步骤会自动忽略,且能够保证变量不会因为栈的特性而被删除。如果不初始化静态变量,默认初始化为0。
①第一种调用静态变量的方法 想想会输出什么?
#include
#include
using namespace std;
void AddToCount()
{
static int count = 0;
count++;
cout << count << endl;
}
int main()
{
AddToCount();
AddToCount();
system("pause");
}
②当你在函数内定义静态变量时,虽然静态变量在函数生命周期之外,但你无法从外部访问
#include
#include
using namespace std;
void AddToCount()
{
static int count = 0;
count++;
cout << count << endl;
}
int main()
{
for (int i = 0; i < 100; i++)
{
AddToCount();
}
count--;
system("pause");
}
③局部变量的调用
#include
#include
using namespace std;
class Item
{
public:
Item()
{
cout << “an item has been created!\n”;
}
~Item()
{
cout << “an item has been destroyed!\n”;
}
};
int main()
{
//局部变量会在声明他的作用域末尾被销毁
Item item;
system("pause");
}
④上个例子的解决方案:添加作用域
#include
#include
using namespace std;
class Item
{
public:
Item()
{
cout << “an item has been created!\n”;
}
~Item()
{
cout << “an item has been destroyed!\n”;
}
};
int main()
{
//由于添加了作用域,析构函数会在该作用域末尾执行
{
Item item;
}
system("pause");
}
⑤第二种调用静态变量的方法
#include
#include
using namespace std;
class Item
{
public:
Item()
{
cout << “an item has been created!\n”;
}
~Item()
{
cout << “an item has been destroyed!\n”;
}
};
int main()
{
//静态变量不存在于与局部变量相同的内存空间中
{
static Item item;
}
system("pause");
}
⑥第三种调用静态变量的方式 静态成员变量只能在类外初始化
#include
#include
using namespace std;
class Critter
{
public:
static int critterCount = 0;
};
int Critter::critterCount = 0;
int main()
{
system("pause");
}
⑦
#include
#include
using namespace std;
class Critter
{
public:
//静态常量
const static int critterCount = 0;
};
int main()
{
system("pause");
}
⑧无须声明即可使用静态函数 类中静态函数不能定义和声明分开,不能访问非静态成员,不能用对象点的方式调用
#include
#include
using namespace std;
class Critter
{
public:
Critter()
{
cout << "A critter is born!\n";
++critterCount;
}
static void AnnounceCount()
{
cout << critterCount << endl;
}
static int critterCount;
};
int Critter::critterCount = 0;
int main()
{
Critter::Critter();
Critter::AnnounceCount();
//Critter crit;
//cout << Critter::critterCount << endl;
//Critter crit2;
//cout << Critter::critterCount << endl;
system("pause");
}
⑨第四种调用静态变量的方式
#include
#include
using namespace std;
class Critter
{
public:
Critter()
{
cout << "A critter is born!\n";
++critterCount;
}
static void AnnounceCount()
{
cout << critterCount << endl;
}
static int critterCount;
};
int Critter::critterCount = 0;
int main()
{
Critter::AnnounceCount();
Critter* crit = new Critter;
Critter::AnnounceCount();
delete crit;
system("pause");
}
十八、虚函数
1.在派生子类中对同一函数的定义与父类不相同,使派生子类中的函数有着与父类不同的版本,即重写父类函数。隔代加virtual和override
2.区分重写(override)与重载(overload)
3.实例:
①直接调用会覆盖父级同名函数
#include
using namespace std;
class Object
{
public:
void BeginPlay();
};
class Actor:public Object
{
public:
void BeginPlay();
};
int main()
{
Object* obj = new Object;
obj->BeginPlay();
Actor* act = new Actor;
act->BeginPlay();
delete obj;
delete act;
system("pause");
}
void Object::BeginPlay()
{
cout << “Object BeginPlay has been called\n”;
}
void Actor::BeginPlay()
{
cout << “Actor BeginPlay has been called\n”;
}
②定义并重写继承方法(函数),将子类限定同名函数放入虚函数表
#include
using namespace std;
class Object
{
public:
virtual void BeginPlay();
};
class Actor:public Object
{
public:
//better to add virtual here even if it is
//add override to read clearly
virtual void BeginPlay() override;
};
int main()
{
Object* obj = new Object;
obj->BeginPlay();
Actor* act = new Actor;
act->BeginPlay();
delete obj;
delete act;
system("pause");
}
void Object::BeginPlay()
{
cout << “Object BeginPlay has been called\n”;
}
void Actor::BeginPlay()
{
cout << “Actor BeginPlay has been called\n”;
Object::BeginPlay();
}
十九、多态
1.
2.
3.实例
①
#include
using namespace std;
class Object
{
public:
virtual void BeginPlay();
};
class Actor:public Object
{
public:
virtual void BeginPlay() override;
};
int main()
{
Object* prt_to_object = new Object;
Actor* prt_to_actor = new Actor;
Object* ObjectArray[] = {prt_to_object,prt_to_actor};
for (int i = 0;i < 2;i++)
{
ObjectArray[i]->BeginPlay();
}
delete prt_to_object;
delete prt_to_actor;
system("pause");
}
void Object::BeginPlay()
{
cout << “Object BeginPlay() called.\n”;
}
void Actor::BeginPlay()
{
cout << “Actor BeginPlay() called.\n”;
}
二十、多继承
1.虚继承:当派生链中的子类包含同名函数的两个版本,想要正确调用某个版本时可以使用虚继承,即无论虚继承的同名函数有多少只调用一个副本。若(基)父类p的(派生类)子类a和b都有函数f(),且a和b共同的子类c的同名函数同时虚继承自a和b,那么调用c的同名函数时就相当于是取得p中函数f()的指针。(菱形继承问题)
2.上述子类a和b可以重写,但要完全限定名称以调用。
3.最后有争论,通过实践得出虚继承是可以进行虚函数操作,但只能A和B中只有一个可以进行虚函数操作
4.上述菱形继承中,可以说子类a,b,c和p是is-A关系,即a,b,c相当于p,但除了c能和p相互直接转换值,其余都不能说p相当于a或者b,类型无法确定,自然无法从p向下转型,此时需要用到动态转换。
二十一、转换
1.直接转型、向下转型与交叉转型