github仓库不定期更新: https://github.com/han-0111/CppLearning
文章目录
- C++如何工作
- 编译器和链接器
- 编译器
- 预处理(Preprocessing)
- include
- define
- if/endif
- 链接器
- 一种比较复杂的情况
- 变量
- 变量类型
- int
- char
- short
- long
- long long
- float
- double
- bool
- 如何查看数据大小
- 函数
- 头文件
- 条件语句
- 循环语句
- `for`循环
- `while`循环
- `do...while`循环
- 控制流
- 指针(Pointer)
- 指针的本质
- 指针的用法
- 引用(Reference)
- 注意事项
- 类(class)
- class(类) VS struct(结构体)
- 类的写法
- static关键字
- 局部作用域中的`static`
- 枚举(enums)
- 使用枚举的例子
- 构造函数vs析构函数
- 构造函数
- 析构函数
- 类的继承
- string
- 初始化
- 注意事项及相关操作
- 虚函数(VirtualFunctions)
- 虚函数的代价
- 纯虚函数(接口)
- 可见性
- C++如何工作
- 编译器和链接器
- 编译器
- 预处理(Preprocessing)
- include
- define
- if/endif
- 链接器
- 一种比较复杂的情况
- 变量
- 变量类型
- int
- char
- short
- long
- long long
- float
- double
- bool
- 如何查看数据大小
- 函数
- 头文件
- 条件语句
- 循环语句
- `for`循环
- `while`循环
- `do...while`循环
- 控制流
- 指针(Pointer)
- 指针的本质
- 指针的用法
- 引用(Reference)
- 注意事项
- 类(class)
- class(类) VS struct(结构体)
- 类的写法
- static关键字
- 局部作用域中的`static`
- 枚举(enums)
- 使用枚举的例子
- 构造函数vs析构函数
- 构造函数
- 析构函数
- 类的继承
- string
- 初始化
- 注意事项及相关操作
- 虚函数(VirtualFunctions)
- 虚函数的代价
- 纯虚函数(接口)
- 可见性
github仓库不定期更新: https://github.com/han-0111/CppLearning
C++如何工作
示例代码HelloWorld
:
#include<iostream>int main()
/*打印输出HelloWorld!*/
{std::cout << "Hello World!" << std::endl;std::cin.get();return 0;
}
分解代码:
#include <iostream>
-
预处理语句
编译器在收到源文件之后,优先执行预处理语句,预处理语句的执行是在实际编译发生之前。
#
:标明在该符号之后的都是预处理语句。include
:去寻找一个文件,寻找的文件就是后面<>
括起来的文件,在这个示例代码中要找的文件就是iostream
,然后将该文件的内容拷贝到当前文件中,这种文件通常被称为头文件。iostream
头文件为输入输出流,使用cout
输出以及使用cin
输入都是该文件所提供的方法,有先预处理该头文件才能正常使用对应的输入输出方法。
int main()
-
int
表示该函数的返回值是一个整型值,每个函数都有自己的返回值,函数就相当于数学中的函数,有输入也有输出,输入就是该函数的参数,输出就是函数的返回值。
-
main函数
每一个C++程序都必须有且只有一个main函数,该函数是程序的入口,当开始运行程序时,计算机会从main函数开始,由上至下一行行顺序执行代码。
main
后面的括号就是需要传入的参数,但打印输出不需要出入任何参数,所以什么都不写,当没有设置任何参数,默认括号内为void
。main函数
可以不给返回值,也就是可以去掉return 0;
这一行。虽然去掉了但是程序会默认返回了 0 0 0。
{std::cout << "Hello World!" << std::endl;std::cin.get();return 0;
}
-
{}
:括起来的内容为函数体,代码中要求所有的括号和大括号都严格对应,代表着一块儿代码的整体。 -
std::
:名称空间标示符,C++标准库中的函数或者对象都是在命名空间std
(standard)中定义的,所以要使用标准函数库中的函数或对象都要使用std来限定。 -
cout
:输出语句。 -
>>
和<<
:移位运算符,但在头文件中已经对>>
和<<
进行了重载,使之分别作为流提取运算符和流插入运算符,用来输入和输出C++标准类型的数据。也可以简单理解为这两个运算符本质上就是一种函数。先不必深究。”流“的概念还没有搞透
-
endl
:输出换行符,在输出语句后使光标下移到新的一行继续操作。 -
;
:C++语句用分号表示结束,表示这一句代码的结束。 -
cin
:输入语句。 -
cin.get()
:用于从输入流中读取下一个字符的函数。它可以读取任何字符,包括空格和回车符,并将其存储在缓冲区中,直到程序需要使用它为止。如果没有这句代码来等待输入的话会直接打印输出后马上退出程序。
示例代码:
#include<iostream>void Log(const char* message)
/*使用该函数进行输出操作*/
{std::cout << message << std::endl;
}int main(void)
/*打印输出HelloWorld*/
{Log("Hello World!"); // 调用自己的函数进行输出std::cin.get();return 0;
}
自定义函数先定义后使用,使用时直接使用函数名加参数。
上述代码都在一个.cpp
文件中,但是为了方便代码项目管理,一般将不同功能的函数或者类放在不同的位置,因此,也可以按照如下的格式:
HelloWorld.cpp
:
#include<iostream>void Log(const char* message);int main(void)
/*打印输出HelloWorld*/
{Log("Hello World!");std::cin.get();return 0;
}
Log.cpp
:
#include<iostream>void Log(const char* message)
{/*使用该函数进行输出操作*/std::cout << message << std::endl;
}
HelloWorld.cpp
和Log.cpp
在同一个文件夹中
主函数中声明一个用到的函数,是告诉编译器:哎,编译器大哥,放心,我有这个Log
函数在,您别担心,信我就行。
编译器无条件相信,然后编译主函数,发现Log了,就会说:好的,小兄弟,我知道你有这个Log
,干啥的我不知道,但是能过我的检查条件,老链啊,去找Log
,该他干活了。
链接器:收到,这就去叫他。链接器找到Log
后告诉它:这活儿该你干了,过来干活。
然后Log
函数干完后给个结果,把结果给主函数,主函数再继续执行下一条语句。
编译器和链接器
程序代码本质上还是文本(text),将文本转换为程序需要两个关键操作,编译和链接
源文件(Source file)->编译(Compiling)->链接(Linking)->可执行文件
编译器
编译器做的事情就是将源代码文本转换成汇编语言,再将汇编语言转换成机器可以识别的机器码。
每个文件被编译成一个独立的obj
文件作为translation unit
编译器将源代码的文本转换为中继obj(object)
目标文件,该文件内容就是机器码,然后这些文件会传入链接器(Linker)。
- 编译器工作流程
- 预处理(pre-process):处理所有预处理语句
- 标记解释(tokenizing)和解析(parsing):将C++文本处理成编译器能处理的语言,创建抽象语法树(abstract syntax tree),就是代码的表达。简单说就是把代码转化成常数资料(constant data)或者指令(instructions)
- 生成机器码:将代码转化成cpu可以执行的机器码
示例:
新建一个math.cpp
文件,写一个简单的乘法函数:
int Multiply(int a, int b)
{int result = a * b;return result;
}
然后仅编译这个文件,会在文件夹中发现出现了math.obj
文件:
同时注意到只有3KB,但是之前的HelloWorld.obj
和Log.obj
文件都很大,尽管他们只是打印输出。这是因为这两个文件中都include
了iostream
,在这个头文件中包含了很多内容,所以编译时先执行预处理,此时也会把预处理中的文件一起编译进obj
目标文件中。
那么obj
里面是什么内容呢?
这文件里面就是机器可以理解的机器码,我们看不明白。但是可以使用VS中汇编程序输出FA选项生成math.asm
文件来查看编译过程中的汇编程序:
编译后打开math.asm
就可以看到代码对应的汇编语言:
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.35.32215.0 TITLE D:\Cpp\HelloWorld\HelloWorld\Debug\math.obj.686P.XMMinclude listing.inc.model flatINCLUDELIB MSVCRTD
INCLUDELIB OLDNAMESmsvcjmc SEGMENT
__CDAE11DE_math@cpp DB 01H
msvcjmc ENDS
PUBLIC ?Multiply@@YAHHH@Z ; Multiply
PUBLIC __JustMyCode_Default
EXTRN @__CheckForDebuggerJustMyCode@4:PROC
; Function compile flags: /Odt
; COMDAT __JustMyCode_Default
_TEXT SEGMENT
__JustMyCode_Default PROC ; COMDATpush ebpmov ebp, esppop ebpret 0
__JustMyCode_Default ENDP
_TEXT ENDS
; Function compile flags: /Ogtp
; COMDAT ?Multiply@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
?Multiply@@YAHHH@Z PROC ; Multiply, COMDAT
; File D:\Cpp\HelloWorld\HelloWorld\math.cpp
; Line 2push ebpmov ebp, espmov ecx, OFFSET __CDAE11DE_math@cppcall @__CheckForDebuggerJustMyCode@4mov eax, DWORD PTR _a$[ebp]imul eax, DWORD PTR _b$[ebp]
; Line 5pop ebpret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
_TEXT ENDS
END
可以通过上面的汇编代码看到自定义的函数?Multiply@@YAHHH@Z
,但是函数在这里会被装饰成带有随机字符和@
符号的形式,这就是函数签名,用来独一无二的标记该函数,链接器(linker)就是通过这个函数签名来找到函数的。
总结来说,编译器就是将源代码文件生成包含机器码和我们定义的常量数据的目标文件object file
预处理(Preprocessing)
预处理阶段编译器检查所有的预处理语句(由#
符号标记的语句),常见的预处理指令有:
- include
- define
- if
include
include
:去寻找一个文件,然后将该文件的内容拷贝到当前文件中,这种文件通常被称为头文件。
示例:
int Multiply(int a, int b)
{int result = a * b;return result // 明显这里少一个分号
}
此时编译提醒缺少;
。
写一个头文件semicolon.h
;
这个头文件里只有一个;
。
既然include
是把文件拷贝过来,那么现在这个文件里就只有一个;
,乘法函数里缺少一个;
,为了验证include
是拷贝内容,可以在少分号的位置使用#include "semicolon.h"
试一下:
int Multiply(int a, int b)
{int result = a * b;return result
#include "semicolon.h"
}
此时编译就可以完全通过了。
那么include
确实是复制文件的内容到当前位置。
define
define
:后面跟两个参数,使用第二个参数替换第一个参数。简单说就是把第一个参数的实际含义替换成后面的参数
示例:
#define INTEGER intINTEGER Multiply(int a, int b) // INTEGER由于在预处理中重新定义成了int, 所以和使用int效果一致
{int result = a * b;return result;
}
这段代码中的预处理语句就是:把代码中出现的INTEGER
字符替换成int
字符,效果和直接使用int
是一样的。
if/endif
if
预处理语句可以依据特定条件包含或者剔除代码。
示例:
#if 0
int Multiply(int a, int b)
{int result = a * b;return result;
}
#endif // 0
此时明显判定条件为false
,所以编译器是不会编译中间的代码的
预处理结果如下:
#line 1 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"#line 8 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"
当把判定条件改成true
(1)时:
#if 1
int Multiply(int a, int b)
{int result = a * b;return result;
}
#endif // 1
此时的预处理结果如下:
#line 1 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"int Multiply(int a, int b)
{int result = a * b;return result;
}
#line 8 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"
可以看到当判定条件成立时,编译器会预处理#if
和endif
所包含的内容;判定条件不成立时,编译器会直接忽略中间的内容。
链接器
链接(Linking)的主要工作就是找到每个符号和函数的位置,并把它们链接在一起。
链接器的功能:
- 在单个代码文件中,链接器用来找到函数的位置,包括主函数的位置。
- 在包含多个代码文件的项目中,链接器用来将这些文件链接到一个程序。
示例:
/*没有main函数*/
#include<iostream>void Log(const char* message)
{std::cout << message << std::endl;}
int Multiply(int a, int b)
{Log("Multiply");return a * b;
}
如果此时仅编译代码,是没有问题的:
但是如果进行生成(Build),会提示链接错误:
链接错误的提示就是
LNK
。
我们需要给这个文件一个main
函数:
#include<iostream>void Log(const char* message);int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
**比较特殊的一点:**在main
函数调用了该文件的Multiply
函数,Multiply
函数又调用了Log
函数,链接器去找这个函数,就会在Log.cpp
文件中找到这个函数,链接成功。
但是!!!如果在main
函数中没有调用Multiply
函数:
#include<iostream>void Log(const char* message); // 声明有一个函数Logint Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{// std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
虽然在主函数中没有调用Multiply
函数,自然就没有调用Log
,可是仍然会引发链接错误!!!!!!!!!
因为链接器会这样认为:这个Multiply
函数在这个文件中没有被调用,ok,可是其他文件仍然有可能会使用,那我还是要链接它的,等等,没有Log
啊,给这小子报个错误,快去改!
visual studio 2022貌似修改了这个错误,测试了下并不会报错
怎么改呢?
在函数类型前加一个关键字static
,表明这个函数只在这个translation unit里面使用就可以了。
相关注意事项:
- 声明的函数的类型,参数类型和数量必须和实际定义的函数保持一致。
- 避免函数名重复,如果两个函数名字一样,链接器会不知道链接哪一个。
- 同一个文件中不要出现已经声明了一个函数,又定义了一个同名的函数,即使这个函数功能与其他文件中的函数功能不一致。同样会引发链接错误。
一种比较复杂的情况
现在有一个头文件Log.h
:
void Log(const char* message)
{std::cout << message << std::endl;
}
一个Log.cpp
:
#include<iostream>
#include"Log.h"void InitLog()
{Log("Initialized Log");
}
一个Math.cpp
:
#include<iostream>
#include"Log.h"static int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
这种情况是会引发链接错误的:
因为#include
预处理的作用就是把包含的文件内容拷贝过来,所以此时Log.cpp
就等同于:
#include<iostream>
void Log(const char* message)
{std::cout << message << std::endl;
}void InitLog()
{Log("Initialized Log");
}
Math.cpp
就等同于:
#include<iostream>
void Log(const char* message)
{std::cout << message << std::endl;
}static int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
在运行main
函数时,调用Multiply
函数,函数中又调用Log
函数,但是此时就相当于在Math.cpp
有一个Log
函数,在Log.cpp
也有一个Log
函数,虽然是一样的内容,但是链接器会不知道到底该链接哪一个,这种情况就是上面说的有了重名的函数,链接器无法辨别链接哪一个。
解决方法:
-
在定义
Log
函数时使用static
限定,static void Log(const char* message) {std::cout << message << std::endl; }
这样即使被
include
到了多个文件,那么在每个文件中所复制过去的函数都是被限制在本文件中的,就不存在多个文件中重名的函数的问题了,即使重名,其工作范围也只是函数所在文件了。 -
使用关键字
inline
,这样在调用函数时,只复制函数内部的语句。inline void Log(const char* message) {std::cout << message << std::endl; }
变量
编程的本质就是在操作数据,数据的读写,增删改查等等。
变量(variable)
:给一个存储在内存中的数据一个名字,从而来使用这个数据,且这个数据是可以更改的。
- 创建变量时,会被存储在内存中的两个地方之一:
- 栈
- 堆
变量类型
int
代表在给定的范围内存储一个整型数字。
#include<iostream>int main()
{int variable = 1; // 定义一个整型变量variable, 并初始化为整数2std::cout << "The int variable number is : " << variable << std::endl; // 打印输出std::cin.get();variable = 2; // 更改变量variable的值std::cout << "The latest int variable number is : " << variable << std::endl; // 打印输出std::cin.get();return 0;
}
传统整型大小是4字节(byte),但是数据类型的实际大小取决于编译器。
-
变量所能表示的范围计算
因为实际大小在不同环境下不同,现在取传统大小4 bytes来计算:
-
1 byte8 bits --> 4 bytes32 bits
-
C++中
int
类型默认为带符号位,32 bits中有一位是要表示符号(+/-)的。剩下31 bits的位置中,每一个位置有2种取值:0或1。 -
int类型所能存储的最大数值为: 2 31 − 1 = 2 , 147 , 483 , 647 2 ^ {31} - 1 = 2,147,483,647 231−1=2,147,483,647,最小为 − 2 , 147 , 483 , 648 -2,147,483,648 −2,147,483,648,正数减1是因为还有整数0的存在。
-
如果超出这个范围,不会报错,但所存储的值就不是正确的了:
#include<iostream>int main() {int variable = 2147483648; // 定义一个整型变量variable, 并初始化为整数2std::cout << "The int variable number is : " << variable << std::endl; // 打印输出std::cin.get(); }
此时并不会如期望的那样输出
2147483648
,而是:如果用
-2147483648
来赋值的话就是正常的,因为并没有超出整型变量的范围。 -
可以使用关键字unsigned
限定变量是无符号位:
#include<iostream>int main()
{unsigned int variable = 2147483648; // 定义一个无符号位的整型变量std::cout << "The int variable number is : " << variable << std::endl; // 打印输出std::cin.get();
}
此时刚才的临界点的数值就是可以正确打印的了。
char
大小:1 bytes
存储内容:字符(character)
#include<iostream>int main()
{char c = 'A'; // 定义一个字符型变量c,并初始化为字符 'A'std::cout << "The char variable is : " << c << std::endl; // 打印输出std::cin.get();
}
对于char
类型的变量也可以按照ASCII码赋值,编译器会根据ASCII码中数值和字符的对应关系存储对应的字符:
#include<iostream>int main()
{char c = 65; // 定义一个字符型变量c,并初始化为65,对应ASCII码表中的字符Astd::cout << "The char variable is : " << c << std::endl; // 打印输出std::cin.get();
}
上面两种的输出是一致的:
int
类型也是一样:
#include<iostream>int main()
{int c = 'A'; // 定义一个整型变量c,并用字符 'A'赋值,会转换成对应的ASCII码的数值65std::cout << "The char variable is : " << c << std::endl; // 打印输出std::cin.get();
}
代码会输出字符A
对应的ASCII码整数65。
C++中的数据类型的使用非常灵活,自我感觉完全就是取决于写代码的人来决定!😃
C++程序中重点关注的还是内存,当我使用某个数据类型的变量时,会分配多少的内存!
short
大小:2 bytes
long
大小:4 bytes
long long
大小:8 bytes
float
浮点数(小数)
大小:4 bytes
#include<iostream>int main()
{float variable = 1.2f; // 定义一个浮点型变量并初始化为1.2std::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cin.get();
}
C++中有两个表示小数类型的关键字float
和double
,但是当使用float
定义变量时如果不在小数后面加一个字符f
(大小写都可以)的话,实际上还是以double
类型存储的:
加了f
的话才会被认定为float
类型:
double
双精度浮点数
大小:8 bytes
bool
布尔型(boolean):true
或者false
大小:1 bytes
实际只有1 bits 就可以表示该类型数据了,只有两个值嘛,但是实际上内存寻址是无法定位到bits的,只能定位带bytes,所以该类型的数据大小是1bytes.
#include<iostream>int main()
{bool variable = false; // 定义一个bool型变量并初始化为falsestd::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cin.get();
}
实际运行结果并不是输出字符:false
,而是数字 0
:
因为计算机只处理数字,0
代表false
,1
代表true
。
然而给bool
变量赋值时,只有赋值0
或者false
时结果为0
,也就是false
;赋值其他任意值结果都是1
,也就是true
。
#include<iostream>int main()
{bool variable = 6; // 定义一个bool型变量并初始化为6std::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cin.get();
}
如何查看数据大小
C++提供了一个查看数据大小的方法:sizeof()
:
#include<iostream>int main()
{bool variable = 6; // 定义一个bool型变量并初始化为6std::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cout << "The size of this char variable is : " << sizeof(variable) << std::endl; // 打印输出std::cin.get();
}
输出:
可以看到该变量的大小为1 bytes
。
sizeof()
也可以查看类的大小:
#include<iostream>int main()
{std::cout << "The size of int is : " << sizeof(int) << std::endl; // 打印输出std::cin.get();
}
输出:
函数
函数就是编写的代码块去执行某个特定的任务
最常用的场景就是把重复的代码写成一个函数,到需要的时候直接调用函数来完成,既节省代码量整体代码的结构也会更加清晰。
函数基本包含:
- 函数的类型
- 函数名
- 参数
- 返回值
示例:
#include<iostream>int Multiply(int a, int b)
{/*两个整数相乘*/std::cout << "This function is used" << std::endl;return a * b;
}int main()
{int result = Multiply(2, 3);std::cout << "result : " << result << std::endl;
}
调用函数非常简单,直接用函数名称就可以了,需要注意的是函数的返回值需要对应的变量来存储。
函数并不是越多越好,阳极必衰,实际上函数过多反而会显得代码十分混乱,很难维护,而且代码执行会更慢!!!每次调用函数时,编译器会生成一个调用指令,这意味着,在一个运行的程序中,为了调用一个函数,需要为这个函数创建一整个栈框架(stack frame),需要把所有参数之类的东西推(push)到栈上,还需要把返回地址放到栈上巴拉巴拉一堆内容,程序为了执行函数指令会在内存中跳来跳去,都是时间!!!
函数的使用主要目的是防止代码重复!!!
头文件
头文件传统上是用来声明某些函数类型,以便可以用于整个程序中,只是声明,而不是定义。
#include<iostream>void Log(const char* message);int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
上面这段代码中就声明了一个函数Log
,但是并没有函数的实体,真正的函数体在项目中的其他文件中,但是如果我有多个翻译单元,也就是多个.cpp
,它们都使用了这个函数,岂不是每个文件都需要这样一句声明。这也太蠢了吧!
这就是头文件的作用:声明函数!
头文件的预处理指令#include
的作用不就是复制粘贴文件内容嘛,那么基本上就是这样:
函数文件:Log.cpp
:
#include<iostream>void Log(const char* message)
{std::cout << message << std::endl;
}
头文件Log.h
:
#pragma once // 头文件保护符,防止同一个翻译单元include该文件两次void Log(const char* message);
主函数文件main.cpp
:
#include "Log.h"int main()
{Log("Hello World");
}
现在函数内容比较简单,所以看起来好像很复杂啊,毕竟上面的这三个文件加起来做的事情和下面一个文件是一致的:
#include<iostream>void Log(const char* message)
{std::cout << message << std::endl;
}int main()
{Log("Hello World");
}
确实,不过当项目工程庞大的时候,把所有代码写在一个文件里会变得非常难维护。
条件语句
如果if
怎么怎么样就怎么怎么样,否则else
怎么怎么样:
#include "Log.h" // Log.h是头文件章节中的内容
#include<iostream>int main()
{int x = 5;bool compareResult = x == 5; // bool类型变量,赋值为0时结果为false,除此之外全为true,此处x == 5为比较运算,此时为正确的, 值就是1, 也就是trueif (compareResult) { // if语句的格式,如果参数部分为true则执行{}内的代码,否则直接跳过Log("Hello World!"); // 打印输出字符std::cin.get();}
}
重要但是尽量少用,会让代码执行效率变低
bool
:布尔类型变量,true/false,但实质上只是两种数字:0和非零的任意数字,0代表false,非0代表true
==
:等于运算符,判断两端的值是否相等,相等返回true,否则返回false
上述代码的汇编语言:
int x = 5;
00A41BF3 mov dword ptr [x],5 bool compareResults = x == 5;
00A41BFA cmp dword ptr [x],5
00A41BFE jne main+29h (0A41C09h)
00A41C00 mov dword ptr [ebp-4Ch],1
00A41C07 jmp main+30h (0A41C10h)
00A41C09 mov dword ptr [ebp-4Ch],0
00A41C10 mov al,byte ptr [ebp-4Ch]
00A41C13 mov byte ptr [compareResults],al if (compareResults) {
00A41C16 movzx eax,byte ptr [compareResults]
00A41C1A test eax,eax
00A41C1C je main+57h (0A41C37h) Log("Hello World!");
00A41C1E push offset string "Hello World!" (0A47B30h)
00A41C23 call Log (0A4120Dh)
00A41C28 add esp,4 std::cin.get();
00A41C2B mov ecx,dword ptr [__imp_std::cin (0A4A0B0h)]
00A41C31 call dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::get (0A4A0B4h)] }
}
if
语句的参数不一定非要是一个确定的变量,只要是可以返回一个结果的内容,都可以,上面的代码就可以直接写成下面的内容:
#include "Log.h"
#include<iostream>int main()
{int x = 5;if (x == 5) {Log("Hello World!");std::cin.get();}
}
因为==
运算符会有两种结果:0或者1,而if
判断语句只要判断括号内的条件是不是0就行了,是0就是false,不执行if
里面的内容,是其他的值就是true,执行if
下面的内容。
如果if
语句只包含一行待执行的代码语句,可以去掉{}
,:
#include "Log.h"
#include<iostream>int main()
{int x = 5;if (x == 5) Log("Hello World!");std::cin.get();}
if
只检查数字!!!!!!
与if
对应就是else
:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = 0; // 指针ptr指向空(NULL)if (ptr) Log(ptr); // 判定条件为false, 跳转至elseelse Log("ptr is null"); // 打印输出相应内容std::cin.get();
}
即使
if
语句写成了两行,但实际上仍然是一句代码,判断一条语句的结束就是看;
,一个分号表示该句结束。
除了上述这样给代码两条分支以外,还可以叠加使用if
,就是else if
:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = "Hello"; // 指针ptrif (ptr)Log(ptr); // 判定条件为true, 进入该分支,打印输出else if (ptr == "Hello") // 尽管为true,但是并不会执行,因为第一个分支的判定true就会跳出分支结构Log("ptr is Hello"); elseLog("ptr is null");std::cin.get();
}
需要注意,如果代码已经进入了一个分支,那么其他分支是不会执行的,上面代码中的第二个条件是不会执行的,因为如果ptr == "Hello"
是成立的,那么在第一个条件语句判断时就是成立的,就只会执行第一个分支内的代码,后面的就不会执行了。
只有前面的分支判断条件为false时,后面的分支判断才会执行。如果第一个分支的条件成立,那么自然执行第一个分支的内容,后面的分支会直接跳过
如果我们想让中间的分支也执行,可以再重新创建一个分支:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = "Hello"; // 指针ptrif (ptr)Log(ptr); // 判定条件为true,该分支结束,继续执行分支结构后面的代码if (ptr == "Hello") // 进入第二个分支Log("ptr is Hello"); // 打印输出相应内容elseLog("ptr is null");std::cin.get();
}
这样就会打印两条语句。
也就是说**if
和else
是相对应的,上面代码的else
对应的就是第二个if
**
另外else if
并不是关键字,它只是else
和if
的一种组合,else
相当于它所对应的if
的另一条分支,只不过在这一条分支中,首先是一个条件判断而已,所做的事情就是:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = "Hello";if (ptr)Log(ptr); else {if (ptr == "Hello")Log("ptr is Hello"); elseLog("ptr is null");}std::cin.get();
}
虽然分支和比较在一个应用中非常重要,但是比较会降低代码的运行效率,能用其他的方法代替的话就尽量代替掉比较操作
循环语句
循环就是多次执行相同的代码,比如:重复打印"Hello World"五次:
#include<iostream>int main()
{// for 循环实现重复打印 5 次 "Hello World"for (int i = 0; i < 5; i++){std::cout << "Hello World" << std::endl;}std::cout << "###############" << std::endl;// while 循环实现重复打印 5 次 "Hello World"int j = 0;while (j < 5){std::cout << "Hello World" << std::endl;j++; // 整型变量j自加1}std::cin.get();
}
for
循环
-
语法格式:
for (控制变量;循环条件;控制变量更新) {循环执行的代码 }
for (int i = 0; i < 5; i++) // 创建一个整型变量i,初始化为0, 执行条件判断, i < 5 为true,执行循环体内的代码, 执行完毕,执行i++, 对i的值进行更新, 然后重新判断 i < 5 是否为true, 循环执行代码,直到i < 5 的值为false, 跳出循环体, 执行接下来的代码 {std::cout << "Hello World" << std::endl; }
-
注意事项
-
for
循环中()
的部分分为三个内容:- 循环开始时的标记
- 循环终止条件
- 每次循环执行结束后对
i
的值的更改
必须由三部分,但是,不需要全部有内容也是可以的:
#include<iostream> #include"Log.h"int main() {// for 循环实现重复打印 5 次 "Hello World"int i = 0; // 循环开始时i = 0bool condition = true; // 循环条件conditionfor (; condition; ) // for 三个部分必须有, 但可以没有内容, 如果condition也去掉的话, 就没有退出循环的条件判定了, 程序就会进入死循环{Log("Hello World"); // 调用函数打印内容if (!(i < 5)) // 如果 i < 5 不成立{condition = false; // 循环条件更改为false, 然后本次循环结束后,重新检查condition,发现为false, 程序就不会再执行循环体了}i++;}std::cin.get(); }
-
i
:不一定非得是整数,其他形式也是可以的,甚至可以是一些自定义的函数,想象力有多大只要符合三部分的功能都可以;
-
while
循环
-
语法结构:
while(循环判定条件) {循环体 }
-
注意事项
- 循环判定条件为一个bool值
- 通常会在循环体内进行判定条件的更新,但是也不是硬性规定
- 判定条件给一个永真值就会进行死循环
do...while
循环
-
语法格式:
do {循环体 }while(循环条件);
-
注意事项:
- 该语句无论循环条件是否成立都会先执行
do
一遍循环体的代码,然后再判断是否继续循环
- 该语句无论循环条件是否成立都会先执行
控制流
控制流(control flow)语句基本上可以解释为可以更好的控制循环。
-
continue
-
只能用在循环内部
-
结束本次循环,如果还有下一次循环,执行下一次循环;如果没有,就结束整个循环
-
示例:只打印4次
#include<iostream> #include"Log.h"int main() {// for 循环实现重复打印 5 次 "Hello World"for (int i = 0; i < 5; i++){if (i == 2)continue; // 如果i==2, 执行continue,跳出i == 2 时的循环, 也就是说i == 2 时下面打印输出的工作就没有进行了, 所以最后的输出就只有四次打印输出的结果Log("Hello World");}std::cin.get(); }
-
-
break
-
可以用在循环,switch语句中
-
直接跳出整个循环
-
示例,只打印两次
#include<iostream> #include"Log.h"int main() {// for 循环实现重复打印 5 次 "Hello World"for (int i = 0; i < 5; i++){if (i == 2)break; // i == 2 时, 跳出循环, 此时循环体只执行了i == 0, 1时的循环Log("Hello World");}std::cin.get(); }
-
-
return
- 直接完整地退出函数
指针(Pointer)
指针的本质
计算机运行一个程序 ,或者运行我们的代码,就是把程序和代码推到内存当中,所有的东西都会被存储在内存中,然后cpu根据指令在内存中给出相应的操作。
指针就是管理和操作内存的重要道具
指针本质上还是一个整数数字,它存储的是一个内存地址。
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
-
定义一个指针的语法格式:
类型* 名字 int* i
示例代码:
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{void* ptr = 0; /*定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针等同于:void* ptr = NULL;void* ptr = nullptr; C++11标准引入*/ int i = 6; // 定义一个整型变量 i, 初始化为 6ptr = &i; /*&取地址符,将变量i的地址给前面定义的指向NULL的无类型指针ptr, 这样是正确的,因为指针就是数字,本质上没有类型区分*/ LOG(ptr);std::cin.get();
}
如果debug一下就会发现:
i
:整型变量值为6
&i
:加了取地址符,值就是该变量所在的内存地址
ptr
:指向全0的无效内存地址
在给ptr
赋值以后:
ptr
指向了i
的地址,那么该地址内的内容是:
可以看到本质上一个整型变量在内存中只不过是32位也就是4字节的大小,存储的内容是数字6,还有对应的内存地址。
指针保存这个内存地址,变量本身保存实际的值,这个值也是数字:
即使是一个char
字符类型的变量,在实际的内存中也是以数字存储的。如下,一个char
字符在内存中占用8位也就是1个字节,而且保存的内容是74
,并不是字符t
综上,我的感觉就是无论是变量还是指针,本质上是并不区分类型的,所谓的类型只是方便编程规定的。
现在用整型指针指向整型变量会发现:
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{int* ptr = 0; /*定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针等同于:void* ptr = NULL;void* ptr = nullptr; C++11标准引入*/ int i = 6; // 定义一个整型变量 i, 初始化为 6ptr = &i; /*&取地址符,将变量i的地址给前面定义的指向NULL的无类型指针ptr, 这样是正确的,因为指针就是数字,本质上没有类型区分*/ LOG(ptr);std::cin.get();
}
使用整型指针与使用无类型的指针没什么区别!!!
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
指针之所以区分类型是因为编译器需要明白我们定义的这个指针操作的是多大的内存块,指向int
就是我们的指针可以操作这4个字节的块,char
就可以更改1字节的块,就是内存块的大小问题!但是本质上就是上面的一句话,方便学习理解。
指针的用法
- 通过指针(内存地址)取实际的值,或者说**指针就是来操作对应位置中规定大小的内存块。**使用
*
逆向引用(dereferencing)来取地址中的值(访问地址中的数据)
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{int* ptr = 0; int a = 6;ptr = &a; // ptr指向a的地址LOG(ptr); // 地址LOG(*ptr); // 解引用,实际的值std::cin.get();
}
-
分配内存
#include<iostream>#define LOG(x) std::cout << x << std::endlint main() {char* buffer = new char[8]; // 分配8个字节的内存,并返回指向这块内存的开始地址的指针buffermemset(buffer, 1, 8); // 为指定的地址buffer所指向的内存中填充8个字节的1delete[] buffer;std::cin.get(); }
可以看到buffer
指向的地址中用了8个字节保存了8个1。
- 指针也是变量,也就说指针同样可以指向指针,只需要在定义时使用两个
*
。
指针只是一个存储着内存地址的整数!!!
引用(Reference)
引用和指针几乎一致,引用是基于指针的一种语法结构,使得代码更易读易写。
引用
:指对现有变量引用的一种方式,**引用
必须引用一个已经存在的变量,引用本身并不是一个新的变量。**并不占用内存。简单说引用就是给起个别名。
示例:
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{int var = 6;int& ref = var; // 创建一个引用,相当于给变量var起一个别名LOG(ref); // 直接输出引用ref, 结果是: 6ref = 5; // 更改引用ref的值,就是更改原来变量var 的值LOG(ref); // 5LOG(var); // 5
}
为什么要有引用?
先看一个c++的示例:
#include<iostream>#define LOG(x) std::cout << x << std::endlvoid AddSelf(int value)
{/*自加1*/value++;
}int main()
{int var = 5; // 初始化整型变量 var = 5AddSelf(var); // 执行AddSelf函数, 自加LOG(var); // 输出var
}
自定义函数的形参int vaule
是一个变量,函数功能是实现自加1,按逻辑来说,把变量var
作为实参传入函数中,操作的应该的是var
这个实参,但是实际最后输出的结果并没有自加1。
为什么会这样?
因为如果一个函数的形参是一个变量,那么当调用函数的时候,函数中会创建一个新的变量value
并赋值传入的实参中的数。
上面的自定义函数实际运行中更像是下面的代码:
#include<iostream>#define LOG(x) std::cout << x << std::endlvoid AddSelf(int value)
{/*自加1*/int value = 5; // 实际调用函数的时候会定义形参并赋值为传入的实参 value++;
}int main()
{int var = 5; // 初始化整型变量 var = 5AddSelf(var); // 执行AddSelf函数, 自加LOG(var); // 输出var
}
如果希望函数实际的更改变量本身的值怎么办?这时候就可以采用引用的方法:
#include<iostream>#define LOG(x) std::cout << x << std::endlvoid AddSelf(int& value)
{/*自加1*/ value++;
}int main()
{int var = 5; // 初始化整型变量 var = 5AddSelf(var); // 执行AddSelf函数, 自加LOG(var); // 输出var
}
道理与前面一致,前面形参为变量时,当调用函数的时候,创建一个变量来存储传入的实参,操作的是这个新的变量;现在如果形参是一个引用的话,当调用函数时,创建一个引用来引用传入的实参,操作的是这个引用,然而引用前面讲过,更改引用会更改原来变量的值,所以理所当然就可以通过把形参设置为引用的方法使函数对变量本身产生作用。
注意事项
-
一个引用只能引用一个变量,不能引用了一个变量后更改该引用的变量对象
int main() {int var = 5; // 初始化整型变量 var = 5int var_2 = 6;int& ref = var;ref = var_2; // 并不是引用var_2, 而是把var_2的值赋值给ref,也就是赋值给varLOG(var); // 6 }
-
引用必须初始化,必须引用一个确定的变量,不能仅仅是定义一个引用
类(class)
面向对象编程是一种编程思想,不注重解决问题的详细过程,而是设计具有相应属性和方法的对象,通过这些具有不同功能的对象来完成任务。
类是一种将数据和函数组织在一起的方式
-
C++中的类的示例:
#include<iostream>#define LOG(x) std::cout << x << std::endlclass Player // class 关键字 表示这是一个类, Player 类的名称 { public: // public: 表示冒号后面的内容为公有内容,可以从类的外部进行访问int x = 0; // 定义类的两个属性, 在类中的变量通常称为 属性int y = 0; // 表示玩家的位置坐标void Move(int stride) // 定义一个方法, 在类中的函数通常称为 方法{/*该方法实现移动玩家位置*/x = x + stride; // x不需要额外定义,在类的内部就是该类的属性xy = y - stride;}}; // 类的定义要有 ; 作为结束int main() { Player one; // 创建一个Player类的对象, 通常称为 实例化一个对象one.x = 120; // 通过 对象名.属性 取到对应的属性one.y = 120;one.Move(5); // 通过 对象名.方法 执行相应的方法LOG(one.x);LOG(one.y); }
class
: 关键字,表明这是一个类。后面接类名,然后是一对{}
,最后要有一个;
。
public
:关键字,接一个:
,后面接属性或方法。表明后面的内容是公有的,可以让类外面的成员访问。(c++的类中的内容默认为私有(private),外部是不可以直接访问的)
属性和方法
: 类内的数据通常称为属性,函数通常称为方法。
class(类) VS struct(结构体)
-
一个class的成员默认情况是private(私有)的,而struct正好相反;
-
c++中保留struct是考虑到与c的兼容性,c中是没有class的。简单的方式完成两种代码的转换只需要一个宏定义:
在c++代码种使用#define class struct在c代码中使用#define struct class
这样就把
c++
中的class
变成了struct
, 反之亦然。 -
何时使用
class
以及什么时候使用struct
看自己的编程风格了(毕竟两者的区别就是上面两个而已<‘’ – ‘’>),建议如果需要使用的某个东西仅包含属性(数据),那么就用struct
,如果这个东西除了属性还有其他功能的方法那就用class
吧。
类的写法
写一个日志类,用来打印不同的信息:
#include<iostream>class Log
{/*Log类, 根据不同的级别设置来打印不同的信息*/
public:/*设置三种日志级别*/const int LogLevelError = 0;const int LogLevelWarning = 1;const int LogLevelInfo = 2;
private:int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Infopublic:void SetLevel(int level){/*设置日志打印信息的级别*/m_LogLevel = level;}void Error(const char* message){/*错误信息*/if (m_LogLevel >= LogLevelError)std::cout << "[ERROR]:" << message << std::endl;}void Warn(const char* message){/*警告信息*/if (m_LogLevel >= LogLevelWarning)std::cout << "[WARNING]:" << message << std::endl;}void Info(const char* message){/*所有信息*/if (m_LogLevel >= LogLevelInfo)std::cout <<"[INFO]:" << message << std::endl;}
};int main()
{Log log; // 实例化一个Log类,用来打印日志信息log.SetLevel(log.LogLevelInfo); // 设置日志信息的级别log.Warn("There is a warning!"); // 打印日志中的警告信息log.Error("There is an erron!!"); // 打印日志中的警告信息log.Info("There is information."); // 打印日志中的信息
}
上述代码仅作示例,有很多问题而且非常不规范。接下来逐步完善上面的代码。
static关键字
两种含义:
- 类或结构体之外:类外的
static
关键字修饰的符号在链接阶段是局部的。只对当前所在的翻译单元可见(只在所在的文件中可见)其他的文件是无法访问被修饰的内容的。
main.cpp
:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlint s_Variable = 10; // 全局变量s_Variable 初始化为整型常量 10int main()
{LOG(s_Variable); // 打印输出变量 s_Variable
}
static.cpp
:
static int s_Variable = 6;
在上面的这种文件结构下,编译无错误,输出结果是 10。
如果更改static.cpp
中的内容为:
int s_Variable = 6;
此时编译是可以通过的,但是在运行时会引发链接错误:
链接器会找到一个或多个多重定义的符号,是因为链接器不仅会找到main.cpp
中的变量s_Variable
,而且由于static.cpp
中的变量不是静态的(static),也会被链接器找到,所以会引发该错误。
除上面的把static.cpp
中的全局变量s_Variable
用static
修饰外,还有另外的解决方法:在main.cpp
中,使用关键字extern
修饰该全局变量:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlextern int s_Variable; // 表明该变量在其他的位置int main()
{LOG(s_Variable); // 打印输出变量 s_Variable
}
建议:尽量让全局变量或者函数使用static
关键字修饰
- 类或结构体内部:表示所修饰的符号为类或结构体的所有实例所共享的。即使该类被多次实例化,但是被
static
修饰的符号只会有一个实例。比如在类的内部定义一个属性使用static
修饰,那么当多次实例化该类时,这个属性只有一个:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:int x, y; // 两个普通的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1 = {2, 3}; // 第二次实例化Entity类 初始化器可以直接给对象的属性赋值e.Print(); // 0 1e1.Print(); // 2 3
}
一切正常,但是当在类的内部使用static
关键字来修饰两个属性时:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1 = {2, 3}; // 第二次实例化Entity类 初始化器可以直接给对象的属性赋值e.Print(); e1.Print();
}
此时编译代码就会报错:
如果更改e1
的初始化方式:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1;e1.x = 2;e1.y = 3;e.Print(); e1.Print();
}
此时报错内容为:
无法解析的外部符号,那么我们就把这两个定义一下:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int Entity::x; // 作用域::变量名
int Entity::y; // 作用域::变量名int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1;e1.x = 2;e1.y = 3;e.Print();e1.Print();
}
编译终于通过了,但是会发现一个很奇怪的现象,两个对象的属性值竟然是一样的,都是:2 3
。可是明明对e
的两个属性赋值为0和1
啊。
这就是开始说到的:类或者结构体内部的static
修饰的符号无论实例化几次,这些符号仅存在一个!!!会被最新的值覆盖掉!
拿上面的例子来说:static
修饰了两个符号x
和y
,那么所有类的实例中的这两个属性指向的是同一个共享空间!
所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过Entity::x
的方式来引用:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int Entity::x;
int Entity::y;int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity::x = 5;Entity::y = 6;e.Print(); // 5 6
}
注意事项:
-
基于所有实例的静态内容都共享内容这一特性,可以把需要所有实例都要使用的相同内容设置为静态。
-
使用静态内容可以不实例化:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity { public:static int x, y; // 两个静态的公有属性static void Print() // 设置为静态方法{/*输出两个属性*/std::cout << x << ' ' << y << std::endl;} };int Entity::x; int Entity::y;int main() {Entity::x = 5;Entity::y = 6;Entity::Print(); }
-
静态方法不能访问非静态变量。静态的内容没有引用指针,本质上类内的每个非静态方法都会获得当前的类实例作为参数:
void Print(Entity e) // 传入类的实例 {/*输出两个属性*/std::cout << e.x << ' ' << e.y << std::endl; }
但是静态方法是没有这个隐藏的参数的,在实际运行中就类似下面这样:
静态的方法由于没有隐藏参数并不知道实例对象是谁的实例!!
局部作用域中的static
- 变量的生命周期:变量实际的存在时间,也就是变量在被删除前在内存中停留多久。
- 作用域:可以访问该变量的范围,比如函数内部定义一个变量,通常是不能在其他函数里访问到这个变量,这样的变量称为局部(local)变量。
- 静态局部(local static)变量:声明一个变量,生命周期是整个程序的生存期。但作用域被限制在其所在作用域中。
示例代码:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlint main()
{for (int j = 0; j <= 4; j++){static int v = 0; // 静态局部变量 v 初始化为 0v++; // v自加1LOG(v); // 打印输出 v 的值}
}
上面的代码循环执行了5次,每次循环的内容为:
- 定义一个静态局部变量v,并初始化为整型数字0;
- v自加1;
- 打印输出v的值, 结果为
1 2 3 4 5
如果去掉关键字static
:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlint main()
{for (int j = 0; j <= 4; j++){int v = 0; // 局部变量 v 初始化为 0v++; // v自加1LOG(v); // 打印输出 v 的值}
}
由于每次循环都重新定义了v
,并初始化为0,所以每次输出的结果都是 1,也就是1 1 1 1 1
。
这就是说,当使用static
关键字修饰时,被修饰的内容的生命周期为程序的整个运行周期,在程序运行过程中,始终存在在内存当中,其中的值是一直存在的,而没有static
关键字的话,就会在每次循环开始时,都会创建一个新的变量,在内存中设定一个新的地址用来存储对应的值。
枚举(enums)
#include<iostream>#define LOG(x) std::cout<<x<<std::endlenum Example // 枚举类型, 里面包含三个值
{A, B, C
};
/*
int a = 0;
int b = 1;
int c = 2;
*/
int main()
{Example value = 1; // 报错, 当定义一个枚举类型的变量时,值必须是枚举所包含的值LOG(value);
}
- 在枚举中的值默认必须是整数
即使向上面的代码,使用了A, B, C
,没有指定类型但是一旦出现在枚举中,它们实际的值就是1, 2, 3
。
- 默认情况下第一个变量的值为
0
,依次递增
- 也可以自己设定值,但是必须是整数
#include<iostream>#define LOG(x) std::cout<<x<<std::endlenum Example // 枚举类型, 里面包含三个值
{A = 1, B = 3, C = 5
};
/*
int a = 0;
int b = 1;
int c = 2;
*/
int main()
{Example value = B; // 3LOG(value);
}
- 也可以自己设定枚举中的类型
enum Example : unsigned char // 枚举类型, 里面包含三个值
{A, B, C
};
使用枚举的例子
之前的Log类:
#include<iostream>class Log
{/*Log类, 根据不同的级别设置来打印不同的信息*/
public:/*设置三种日志级别*/const int LogLevelError = 0;const int LogLevelWarning = 1;const int LogLevelInfo = 2;
private:int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Infopublic:void SetLevel(int level){/*设置日志打印信息的级别*/m_LogLevel = level;}void Error(const char* message){/*错误信息*/if (m_LogLevel >= LogLevelError)std::cout << "[ERROR]:" << message << std::endl;}void Warn(const char* message){/*警告信息*/if (m_LogLevel >= LogLevelWarning)std::cout << "[WARNING]:" << message << std::endl;}void Info(const char* message){/*所有信息*/if (m_LogLevel >= LogLevelInfo)std::cout << "[INFO]:" << message << std::endl;}
};int main()
{Log log; // 实例化一个Log类,用来打印日志信息log.SetLevel(log.LogLevelInfo); // 设置日志信息的级别log.Warn("There is a warning!"); // 打印日志中的警告信息log.Error("There is an erron!!"); // 打印日志中的警告信息log.Info("There is information."); // 打印日志中的信息
}
其中的三种日志信息的等级就是一个使用枚举很好的例子:
#include<iostream>class Log
{/*Log类, 根据不同的级别设置来打印不同的信息*/
public:/*设置三种日志级别*///const int LogLevelError = 0;//const int LogLevelWarning = 1;//const int LogLevelInfo = 2;enum Level // 使用枚举来设置三种级别,与上面的功能一致{LevelError, LevelWarning, LevelInfo};
private:Level m_LogLevel = LevelInfo; // 同时将自己的设置级别也更改为枚举类型public:void SetLevel(Level level){/*设置日志打印信息的级别*/m_LogLevel = level;}void Error(const char* message){/*错误信息*/if (m_LogLevel >= LevelError)std::cout << "[ERROR]:" << message << std::endl;}void Warn(const char* message){/*警告信息*/if (m_LogLevel >= LevelWarning)std::cout << "[WARNING]:" << message << std::endl;}void Infom(const char* message){/*所有信息*/if (m_LogLevel >= LevelInfo)std::cout << "[INFO]:" << message << std::endl;}
};int main()
{Log log; // 实例化一个Log类,用来打印日志信息log.SetLevel(log.LevelWarning); // 设置日志信息的级别log.Warn("There is a warning!"); // 打印日志中的警告信息log.Error("There is an error!!"); // 打印日志中的警告信息log.Infom("There is information."); // 打印日志中的信息
}
构造函数vs析构函数
构造函数
Constructors,一种特殊的函数,在类的每次实例化的时候运行
示例:
#include<iostream>class Entity
{
public:float X, Y;void Print(){std::cout << X << "," << Y << std::endl;}
};int main()
{Entity e;std::cout << e.X << std::endl; // 编译报错:使用了未初始化的局部变量“X”e.Print();
}
上述代码报错并提示:使用了未初始化的局部变量“X”
但是当删除报错的代码时是可以正确运行的,但是会输出随机的值,这是因为当变量没有初始化的时候会存储内存中划分给这两个变量的位置存储的当时的值
所以初始化很重要
构造函数就是在我们实例化一个类的时候自动执行的函数,写法如下:
#include<iostream>class Entity
{
public:float X, Y;Entity() // 构造函数,在实例化该类的时候会自动执行该函数{X = 0.0f;Y = 0.0f;}void Print(){std::cout << X << "," << Y << std::endl;}
};int main()
{Entity e;std::cout << e.X << std::endl; e.Print();
}
- 如果不写构造函数,实际上仍然是存在的,这个构造函数被称为默认构造函数(default constructor),但是默认构造函数不做任何操作(相当于上面代码中的构造函数中什么内容都没有)
- C++中的变量需要手动初始化,不然就会被设定为之前留存在内存中的值。
- 构造函数会在实例化类的时候自动执行,由于使用类内部的静态内容时可以不实例化,那么当然也就不会执行构造函数。
构造函数可以设置参数,设置参数后需要在实例化该类时传入参数,一个类可以有多个构造函数,根据参数不同来划分:
#include<iostream>class Entity
{
public:float X, Y;Entity() // 构造函数,在实例化该类的时候会自动执行该函数{X = 3.3f;Y = 4.4f;}Entity(float x, float y) // 构造函数,在实例化该类的时候会自动执行该函数{X = x;Y = y;}void Print(){std::cout << X << "," << Y << std::endl;}
};int main()
{/*构造函数的参数需要在实例化该类时传入如果传入了参数,就会执行带参数的构造函数如果没有传入参数就会执行,没有参数的构造函数*/ Entity e(1.1f, 2.2f); e.Print();
}
析构函数
析构函数(Destructors),在销毁一个对象的时候运行
当一个对象被销毁的时候析构函数都会被调用
- 使用关键字
new
创建一个对象,那么这个对象会存在于堆上,调用delete
的时候,析构函数会被调用 - 对于基于栈的对象,当跳出作用域的时候这个对象会被删除,此时析构函数也会被调用
示例:
#include<iostream>class Entity
{
public:float X, Y;Entity(float x, float y){// 构造函数X = x;Y = y;}~Entity(){// 析构函数std::cout << "Destructors" << std::endl;}void Print(){std::cout << X << " " << Y << std::endl;}
};int main()
{Entity e(1.1f, 2.2f); e.Print(); // 1.1 2.2 // 当main函数执行完成会自动执行析构函数的内容 打印输出Destructors
}
类的继承
inheritance
把一系列类的通用部分放在基类中,然后使用继承基类的方法,使代码的重复度降低。
首先下面两个类:
class Entity
{
public:float X, Y;void Move(float xa, float ya){X += xa;Y += ya;}
};class Player
{
public:const char* Name;float X, Y;void Move(float xa, float ya){X += xa;Y += ya;}void PrintName(){std::cout << Name << std::endl;}
};
Player
类中的某些内容很明显是与Entity
重复的,此时就可以使用继承:
class Player : public Entity
{
public:const char* Name;void PrintName(){std::cout << Name << std::endl;}
};
此时Player
类就继承了Entity
的内容:两个属性和一个Move
方法,另外还有自身的属性和方法。
int main()
{Player pc;pc.Move(0.5f, 0.5f);pc.Name = "ok";pc.PrintName();
}
实例化Player
类以后,实例对象pc
可以使用两个类中的内容(属性和方法)。
string
- 表示可变长的字符序列
- 定义在命名空间
std
中
代码示例:
#include <iostream>
#include <string>int main()
{std::string s; // 默认初始化 空字符串std::cout << s << std::endl;std::cout << sizeof(s) << std::endl; // 28字节
}
初始化
#include <iostream>
#include <string>int main()
{std::string s; // 默认初始化 空字符串std::string s1 = "copy"; // 拷贝初始化std::string s2("directly"); // 直接初始化std::string s3(5, 'h'); // 直接初始化 s3存储的是 5 个 hstd::cout << s.empty() << std::endl; // 1 empty()方法返回bool值,为真时表示s是空字符串
}
注意事项及相关操作
- 输入字符到
string
对象会自动忽略字符串中的空白项,以第一个字符为开始,顺序向后遇到空白结束
#include <iostream>
#include <string>int main()
{std::string s; // 默认初始化 空字符串std::cin >> s; // 输入字符串std::cout << s << std::endl; // 从顺序遇到的第一个字符开始,顺序向后第一个空白结束
}
采用同样的输入,如果使用两个string
对象接收的话,每个对象会存储一个连续的字符串:
#include <iostream>
#include <string>int main()
{std::string s, s1; // 默认初始化 空字符串std::cin >> s >> s1; // 输入字符串std::cout << s << s1 << std::endl; // 顺序输出两个字符串
}
-
字符串的比较
-
字符串本身有有长度的不同,获取字符串的长度可以简单地使用
size()
,注意sizeof()
是获得对象占用内存空间的大小#include <iostream> #include <string> using namespace std;int main() {string s("cpp");cout << s.size() << endl; // 3cout << sizeof(s) << endl; // 28 }
-
字符串之间的大小不同,字符串的大小比较:顺序比较对应位置的两个字符,如果字符相同,顺序向后,直到出现不同的字符(包括同一字符的大小写,也属于不同的字符),字符大的字符串就大,与字符串的长度无关
#include <iostream> #include <string> using namespace std;int main() {string s1("abc"); // 长度为3string s2("de"); // 长度为2if (s1 > s2) // falsecout << "s1" << endl;else if (s1 == s2) // falsecout << "s1 == s2" << endl;else cout << "s2" << endl; // run s2 大于 s1, 因为 d 大于 a }
-
-
字符串的相加
-
字符串可以直接使用
+
运算符相加,效果是左侧的字符串后面拼接右侧的字符串的内容。#include <iostream> #include <string> using namespace std;int main() {string s1("abc");string s2("de");cout << s1 + s2 << endl; // abcde }
-
字符串对象可以直接与字符串常量相加,但两个字符串常量是不能直接
+
的。#include <iostream> #include <string> using namespace std;int main() {string s1("abc");cout << s1 + "de" << endl; // abcdecout << "abc" + "de" << endl; // error }
-
-
取出字符串中的字符
-
字符串是由一个个字符顺序连接组成的,使用c++11 标准中提出的一种语法: 范围for循环可以遍历字符串中的字符
#include <iostream> #include <string> using namespace std;int main() {std::string s("abcde");char c;c = s[0]; // 第一个字符"a"cout << "c : " << c << endl; // c : afor (auto a : s) // 定义的变量 : 序列对象cout << a << endl; }
-
虚函数(VirtualFunctions)
虚函数可以让我们在子类中重写方法
有两个类:A,B
B是A的子类,在A中的方法设定为虚(virtual),那么就可以在B类中重写该方法
代码示例:
#include<iostream>
#include<string>class Entity // 父类
{
public:std::string GetName() { return "Entity"; } // 父类方法,返回一个string
};class Player : public Entity // 子类
{
private:std::string m_Name; // 子类私有属性 m_Name
public:Player(const std::string& name) // 构造函数, 用于初始化 m_Name: m_Name(name) {} // 将传入的string值赋给m_Namestd::string GetName() { return m_Name; } // 继承父类的GetName方法
};
void printName(Entity* entity)
{std::cout << entity->GetName() << std::endl;
}
int main()
{Entity* e = new Entity(); // 划分一个存储Entity对象的内存空间, 使用指针e指向该内存地址printName(e); // entityPlayer* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址printName(p); // entity}
多态:不同的子类可以有相同的方法,但可以根据各个子类的需求使用该方法来实现不同的效果。
上面代码可以看到,p
明明是子类的实例化对象,但是执行GetName()方法却使用的父类中的方法。
这时就需要使用虚函数。虚函数可以让我们在子类中重写父类方法。
虚函数的写法:在定义方法时,使用关键字virtual
限制:
virtual std::string GetName() { return "Entity"; }
现在把上面的代码做下面的调整:
- 父类中的方法前加一个
virtual
- 子类中的方法在类型后加一个
override
(加不加都可以,但为了代码可读性建议加上,代表这是一个从父类继承的方法)
结果就可以像我们所期待的那样,各自执行各自的方法:
#include<iostream>
#include<string>class Entity // 父类
{
public:virtual std::string GetName() { return "Entity"; } // 使用关键字virtual表明这是一个虚函数
};class Player : public Entity // 子类
{
private:std::string m_Name; // 子类私有属性 m_Name
public:Player(const std::string& name) // 构造函数, 用于初始化 m_Name: m_Name(name) {} // 将传入的string值赋给m_Namestd::string override GetName() { return m_Name; } // 继承父类的GetName方法
};
void printName(Entity* entity)
{std::cout << entity->GetName() << std::endl;
}
int main()
{Entity* e = new Entity(); // 划分一个存储Entity对象的内存空间, 使用指针e指向该内存地址printName(e); // entityPlayer* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址printName(p); // Rabbit
}
虚函数的代价
虚函数的使用虽然方便,但是也会带来一些代价:
- 需要额外的内存来存储虚函数表
- 每次调用虚函数的时候程序需要遍历虚函数表来找到最重要运行的函数
纯虚函数(接口)
纯虚函数允许我们定义一个在基类中没有实现的函数,强制子类完成该函数
接口(Interface):一个只包含未实现的方法,并作为一个模板的类。由于此接口类实际上不包含方法实现,所以无法实例化这个类。
#include<iostream>
#include<string>class Printable
{// 接口类,仅包含一个纯虚函数
public:virtual std::string GetClassName() = 0;
};class Entity : public Printable
{// 使用接口类的时候必须把接口中的纯虚函数实现
public: std::string GetClassName() override { return "Entity."; }
};class Player : public Entity
{
private:std::string m_Name;
public:// 两个构造函数,根据实例化该类的时候传入的参数决定使用哪个函数Player() {}Player(const std::string& name) : m_Name(name) {} // 实现接口中的纯虚函数std::string GetClassName() override { return "Player."; }
};void printName(Printable* obj)
{std::cout << obj->GetClassName() << std::endl;
}int main()
{Entity* e = new Entity(); // 报错,因为Entity有纯虚函数,无法实例化printName(e); Player* p = new Player(); // 正常运行,因为Player是子类,在自己内部要实现纯虚函数的内容printName(p); }
可见性
可见性是面向对象中的概念,指一个类中的成员或者方法是否可见。
可见
指的是谁可以访问或者调用他们,可见性不会影响程序的实际运行,也不会对程序性能有影响。
三个基础的可见修饰符(访问修饰符):
-
private
- class的可见性默认为private
#include<iostream> #include<string>class Entity {// class 默认为private, 写不写下面这个都可以 private: // 只有在这个类中可以访问到int X, Y; public:Entity(){X = 0;} };class Player :public Entity { public:Player(){X = 1; // 成员 "Entity::X" 不可访问}};int main() {Entity* e = new Entity();e->X = 7; // 成员 "Entity::X" 不可访问 }
无论是自己的子类,或者是实例化的对象都无法访问到private的内容。
-
protected
-
该类以及它的所有派生类可以访问
#include<iostream> #include<string>class Entity { protected: // 只有在这个类及其子类中可以访问到int X, Y; public:Entity(){X = 0;} };class Player :public Entity { public:Player(){X = 1; // 子类可以访问父类中protected的内容} };int main() {Entity* e = new Entity();e->X = 7; // 成员 "Entity::X" 不可访问 }
子类是可以访问到
protected
的内容的,但是其他地方仍无法访问。
-
-
public
-
任何地方都可以访问
#include<iostream> #include<string>class Entity { public: // 公有,任何地方都可以访问int X, Y; public:Entity(){X = 0;} };class Player :public Entity { public:Player(){X = 1; } };int main() {Entity* e = new Entity();e->X = 7; }
-
C++如何工作
示例代码HelloWorld
:
#include<iostream>int main()
/*打印输出HelloWorld!*/
{std::cout << "Hello World!" << std::endl;std::cin.get();return 0;
}
分解代码:
#include <iostream>
-
预处理语句
编译器在收到源文件之后,优先执行预处理语句,预处理语句的执行是在实际编译发生之前。
#
:标明在该符号之后的都是预处理语句。include
:去寻找一个文件,寻找的文件就是后面<>
括起来的文件,在这个示例代码中要找的文件就是iostream
,然后将该文件的内容拷贝到当前文件中,这种文件通常被称为头文件。iostream
头文件为输入输出流,使用cout
输出以及使用cin
输入都是该文件所提供的方法,有先预处理该头文件才能正常使用对应的输入输出方法。
int main()
-
int
表示该函数的返回值是一个整型值,每个函数都有自己的返回值,函数就相当于数学中的函数,有输入也有输出,输入就是该函数的参数,输出就是函数的返回值。
-
main函数
每一个C++程序都必须有且只有一个main函数,该函数是程序的入口,当开始运行程序时,计算机会从main函数开始,由上至下一行行顺序执行代码。
main
后面的括号就是需要传入的参数,但打印输出不需要出入任何参数,所以什么都不写,当没有设置任何参数,默认括号内为void
。main函数
可以不给返回值,也就是可以去掉return 0;
这一行。虽然去掉了但是程序会默认返回了 0 0 0。
{std::cout << "Hello World!" << std::endl;std::cin.get();return 0;
}
-
{}
:括起来的内容为函数体,代码中要求所有的括号和大括号都严格对应,代表着一块儿代码的整体。 -
std::
:名称空间标示符,C++标准库中的函数或者对象都是在命名空间std
(standard)中定义的,所以要使用标准函数库中的函数或对象都要使用std来限定。 -
cout
:输出语句。 -
>>
和<<
:移位运算符,但在头文件中已经对>>
和<<
进行了重载,使之分别作为流提取运算符和流插入运算符,用来输入和输出C++标准类型的数据。也可以简单理解为这两个运算符本质上就是一种函数。先不必深究。”流“的概念还没有搞透
-
endl
:输出换行符,在输出语句后使光标下移到新的一行继续操作。 -
;
:C++语句用分号表示结束,表示这一句代码的结束。 -
cin
:输入语句。 -
cin.get()
:用于从输入流中读取下一个字符的函数。它可以读取任何字符,包括空格和回车符,并将其存储在缓冲区中,直到程序需要使用它为止。如果没有这句代码来等待输入的话会直接打印输出后马上退出程序。
示例代码:
#include<iostream>void Log(const char* message)
/*使用该函数进行输出操作*/
{std::cout << message << std::endl;
}int main(void)
/*打印输出HelloWorld*/
{Log("Hello World!"); // 调用自己的函数进行输出std::cin.get();return 0;
}
自定义函数先定义后使用,使用时直接使用函数名加参数。
上述代码都在一个.cpp
文件中,但是为了方便代码项目管理,一般将不同功能的函数或者类放在不同的位置,因此,也可以按照如下的格式:
HelloWorld.cpp
:
#include<iostream>void Log(const char* message);int main(void)
/*打印输出HelloWorld*/
{Log("Hello World!");std::cin.get();return 0;
}
Log.cpp
:
#include<iostream>void Log(const char* message)
{/*使用该函数进行输出操作*/std::cout << message << std::endl;
}
HelloWorld.cpp
和Log.cpp
在同一个文件夹中
主函数中声明一个用到的函数,是告诉编译器:哎,编译器大哥,放心,我有这个Log
函数在,您别担心,信我就行。
编译器无条件相信,然后编译主函数,发现Log了,就会说:好的,小兄弟,我知道你有这个Log
,干啥的我不知道,但是能过我的检查条件,老链啊,去找Log
,该他干活了。
链接器:收到,这就去叫他。链接器找到Log
后告诉它:这活儿该你干了,过来干活。
然后Log
函数干完后给个结果,把结果给主函数,主函数再继续执行下一条语句。
编译器和链接器
程序代码本质上还是文本(text),将文本转换为程序需要两个关键操作,编译和链接
源文件(Source file)->编译(Compiling)->链接(Linking)->可执行文件
编译器
编译器做的事情就是将源代码文本转换成汇编语言,再将汇编语言转换成机器可以识别的机器码。
每个文件被编译成一个独立的obj
文件作为translation unit
编译器将源代码的文本转换为中继obj(object)
目标文件,该文件内容就是机器码,然后这些文件会传入链接器(Linker)。
- 编译器工作流程
- 预处理(pre-process):处理所有预处理语句
- 标记解释(tokenizing)和解析(parsing):将C++文本处理成编译器能处理的语言,创建抽象语法树(abstract syntax tree),就是代码的表达。简单说就是把代码转化成常数资料(constant data)或者指令(instructions)
- 生成机器码:将代码转化成cpu可以执行的机器码
示例:
新建一个math.cpp
文件,写一个简单的乘法函数:
int Multiply(int a, int b)
{int result = a * b;return result;
}
然后仅编译这个文件,会在文件夹中发现出现了math.obj
文件:
同时注意到只有3KB,但是之前的HelloWorld.obj
和Log.obj
文件都很大,尽管他们只是打印输出。这是因为这两个文件中都include
了iostream
,在这个头文件中包含了很多内容,所以编译时先执行预处理,此时也会把预处理中的文件一起编译进obj
目标文件中。
那么obj
里面是什么内容呢?
这文件里面就是机器可以理解的机器码,我们看不明白。但是可以使用VS中汇编程序输出FA选项生成math.asm
文件来查看编译过程中的汇编程序:
编译后打开math.asm
就可以看到代码对应的汇编语言:
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.35.32215.0 TITLE D:\Cpp\HelloWorld\HelloWorld\Debug\math.obj.686P.XMMinclude listing.inc.model flatINCLUDELIB MSVCRTD
INCLUDELIB OLDNAMESmsvcjmc SEGMENT
__CDAE11DE_math@cpp DB 01H
msvcjmc ENDS
PUBLIC ?Multiply@@YAHHH@Z ; Multiply
PUBLIC __JustMyCode_Default
EXTRN @__CheckForDebuggerJustMyCode@4:PROC
; Function compile flags: /Odt
; COMDAT __JustMyCode_Default
_TEXT SEGMENT
__JustMyCode_Default PROC ; COMDATpush ebpmov ebp, esppop ebpret 0
__JustMyCode_Default ENDP
_TEXT ENDS
; Function compile flags: /Ogtp
; COMDAT ?Multiply@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
?Multiply@@YAHHH@Z PROC ; Multiply, COMDAT
; File D:\Cpp\HelloWorld\HelloWorld\math.cpp
; Line 2push ebpmov ebp, espmov ecx, OFFSET __CDAE11DE_math@cppcall @__CheckForDebuggerJustMyCode@4mov eax, DWORD PTR _a$[ebp]imul eax, DWORD PTR _b$[ebp]
; Line 5pop ebpret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
_TEXT ENDS
END
可以通过上面的汇编代码看到自定义的函数?Multiply@@YAHHH@Z
,但是函数在这里会被装饰成带有随机字符和@
符号的形式,这就是函数签名,用来独一无二的标记该函数,链接器(linker)就是通过这个函数签名来找到函数的。
总结来说,编译器就是将源代码文件生成包含机器码和我们定义的常量数据的目标文件object file
预处理(Preprocessing)
预处理阶段编译器检查所有的预处理语句(由#
符号标记的语句),常见的预处理指令有:
- include
- define
- if
include
include
:去寻找一个文件,然后将该文件的内容拷贝到当前文件中,这种文件通常被称为头文件。
示例:
int Multiply(int a, int b)
{int result = a * b;return result // 明显这里少一个分号
}
此时编译提醒缺少;
。
写一个头文件semicolon.h
;
这个头文件里只有一个;
。
既然include
是把文件拷贝过来,那么现在这个文件里就只有一个;
,乘法函数里缺少一个;
,为了验证include
是拷贝内容,可以在少分号的位置使用#include "semicolon.h"
试一下:
int Multiply(int a, int b)
{int result = a * b;return result
#include "semicolon.h"
}
此时编译就可以完全通过了。
那么include
确实是复制文件的内容到当前位置。
define
define
:后面跟两个参数,使用第二个参数替换第一个参数。简单说就是把第一个参数的实际含义替换成后面的参数
示例:
#define INTEGER intINTEGER Multiply(int a, int b) // INTEGER由于在预处理中重新定义成了int, 所以和使用int效果一致
{int result = a * b;return result;
}
这段代码中的预处理语句就是:把代码中出现的INTEGER
字符替换成int
字符,效果和直接使用int
是一样的。
if/endif
if
预处理语句可以依据特定条件包含或者剔除代码。
示例:
#if 0
int Multiply(int a, int b)
{int result = a * b;return result;
}
#endif // 0
此时明显判定条件为false
,所以编译器是不会编译中间的代码的
预处理结果如下:
#line 1 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"#line 8 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"
当把判定条件改成true
(1)时:
#if 1
int Multiply(int a, int b)
{int result = a * b;return result;
}
#endif // 1
此时的预处理结果如下:
#line 1 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"int Multiply(int a, int b)
{int result = a * b;return result;
}
#line 8 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"
可以看到当判定条件成立时,编译器会预处理#if
和endif
所包含的内容;判定条件不成立时,编译器会直接忽略中间的内容。
链接器
链接(Linking)的主要工作就是找到每个符号和函数的位置,并把它们链接在一起。
链接器的功能:
- 在单个代码文件中,链接器用来找到函数的位置,包括主函数的位置。
- 在包含多个代码文件的项目中,链接器用来将这些文件链接到一个程序。
示例:
/*没有main函数*/
#include<iostream>void Log(const char* message)
{std::cout << message << std::endl;}
int Multiply(int a, int b)
{Log("Multiply");return a * b;
}
如果此时仅编译代码,是没有问题的:
但是如果进行生成(Build),会提示链接错误:
链接错误的提示就是
LNK
。
我们需要给这个文件一个main
函数:
#include<iostream>void Log(const char* message);int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
**比较特殊的一点:**在main
函数调用了该文件的Multiply
函数,Multiply
函数又调用了Log
函数,链接器去找这个函数,就会在Log.cpp
文件中找到这个函数,链接成功。
但是!!!如果在main
函数中没有调用Multiply
函数:
#include<iostream>void Log(const char* message); // 声明有一个函数Logint Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{// std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
虽然在主函数中没有调用Multiply
函数,自然就没有调用Log
,可是仍然会引发链接错误!!!!!!!!!
因为链接器会这样认为:这个Multiply
函数在这个文件中没有被调用,ok,可是其他文件仍然有可能会使用,那我还是要链接它的,等等,没有Log
啊,给这小子报个错误,快去改!
visual studio 2022貌似修改了这个错误,测试了下并不会报错
怎么改呢?
在函数类型前加一个关键字static
,表明这个函数只在这个translation unit里面使用就可以了。
相关注意事项:
- 声明的函数的类型,参数类型和数量必须和实际定义的函数保持一致。
- 避免函数名重复,如果两个函数名字一样,链接器会不知道链接哪一个。
- 同一个文件中不要出现已经声明了一个函数,又定义了一个同名的函数,即使这个函数功能与其他文件中的函数功能不一致。同样会引发链接错误。
一种比较复杂的情况
现在有一个头文件Log.h
:
void Log(const char* message)
{std::cout << message << std::endl;
}
一个Log.cpp
:
#include<iostream>
#include"Log.h"void InitLog()
{Log("Initialized Log");
}
一个Math.cpp
:
#include<iostream>
#include"Log.h"static int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
这种情况是会引发链接错误的:
因为#include
预处理的作用就是把包含的文件内容拷贝过来,所以此时Log.cpp
就等同于:
#include<iostream>
void Log(const char* message)
{std::cout << message << std::endl;
}void InitLog()
{Log("Initialized Log");
}
Math.cpp
就等同于:
#include<iostream>
void Log(const char* message)
{std::cout << message << std::endl;
}static int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
在运行main
函数时,调用Multiply
函数,函数中又调用Log
函数,但是此时就相当于在Math.cpp
有一个Log
函数,在Log.cpp
也有一个Log
函数,虽然是一样的内容,但是链接器会不知道到底该链接哪一个,这种情况就是上面说的有了重名的函数,链接器无法辨别链接哪一个。
解决方法:
-
在定义
Log
函数时使用static
限定,static void Log(const char* message) {std::cout << message << std::endl; }
这样即使被
include
到了多个文件,那么在每个文件中所复制过去的函数都是被限制在本文件中的,就不存在多个文件中重名的函数的问题了,即使重名,其工作范围也只是函数所在文件了。 -
使用关键字
inline
,这样在调用函数时,只复制函数内部的语句。inline void Log(const char* message) {std::cout << message << std::endl; }
变量
编程的本质就是在操作数据,数据的读写,增删改查等等。
变量(variable)
:给一个存储在内存中的数据一个名字,从而来使用这个数据,且这个数据是可以更改的。
- 创建变量时,会被存储在内存中的两个地方之一:
- 栈
- 堆
变量类型
int
代表在给定的范围内存储一个整型数字。
#include<iostream>int main()
{int variable = 1; // 定义一个整型变量variable, 并初始化为整数2std::cout << "The int variable number is : " << variable << std::endl; // 打印输出std::cin.get();variable = 2; // 更改变量variable的值std::cout << "The latest int variable number is : " << variable << std::endl; // 打印输出std::cin.get();return 0;
}
传统整型大小是4字节(byte),但是数据类型的实际大小取决于编译器。
-
变量所能表示的范围计算
因为实际大小在不同环境下不同,现在取传统大小4 bytes来计算:
-
1 byte8 bits --> 4 bytes32 bits
-
C++中
int
类型默认为带符号位,32 bits中有一位是要表示符号(+/-)的。剩下31 bits的位置中,每一个位置有2种取值:0或1。 -
int类型所能存储的最大数值为: 2 31 − 1 = 2 , 147 , 483 , 647 2 ^ {31} - 1 = 2,147,483,647 231−1=2,147,483,647,最小为 − 2 , 147 , 483 , 648 -2,147,483,648 −2,147,483,648,正数减1是因为还有整数0的存在。
-
如果超出这个范围,不会报错,但所存储的值就不是正确的了:
#include<iostream>int main() {int variable = 2147483648; // 定义一个整型变量variable, 并初始化为整数2std::cout << "The int variable number is : " << variable << std::endl; // 打印输出std::cin.get(); }
此时并不会如期望的那样输出
2147483648
,而是:如果用
-2147483648
来赋值的话就是正常的,因为并没有超出整型变量的范围。 -
可以使用关键字unsigned
限定变量是无符号位:
#include<iostream>int main()
{unsigned int variable = 2147483648; // 定义一个无符号位的整型变量std::cout << "The int variable number is : " << variable << std::endl; // 打印输出std::cin.get();
}
此时刚才的临界点的数值就是可以正确打印的了。
char
大小:1 bytes
存储内容:字符(character)
#include<iostream>int main()
{char c = 'A'; // 定义一个字符型变量c,并初始化为字符 'A'std::cout << "The char variable is : " << c << std::endl; // 打印输出std::cin.get();
}
对于char
类型的变量也可以按照ASCII码赋值,编译器会根据ASCII码中数值和字符的对应关系存储对应的字符:
#include<iostream>int main()
{char c = 65; // 定义一个字符型变量c,并初始化为65,对应ASCII码表中的字符Astd::cout << "The char variable is : " << c << std::endl; // 打印输出std::cin.get();
}
上面两种的输出是一致的:
int
类型也是一样:
#include<iostream>int main()
{int c = 'A'; // 定义一个整型变量c,并用字符 'A'赋值,会转换成对应的ASCII码的数值65std::cout << "The char variable is : " << c << std::endl; // 打印输出std::cin.get();
}
代码会输出字符A
对应的ASCII码整数65。
C++中的数据类型的使用非常灵活,自我感觉完全就是取决于写代码的人来决定!😃
C++程序中重点关注的还是内存,当我使用某个数据类型的变量时,会分配多少的内存!
short
大小:2 bytes
long
大小:4 bytes
long long
大小:8 bytes
float
浮点数(小数)
大小:4 bytes
#include<iostream>int main()
{float variable = 1.2f; // 定义一个浮点型变量并初始化为1.2std::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cin.get();
}
C++中有两个表示小数类型的关键字float
和double
,但是当使用float
定义变量时如果不在小数后面加一个字符f
(大小写都可以)的话,实际上还是以double
类型存储的:
加了f
的话才会被认定为float
类型:
double
双精度浮点数
大小:8 bytes
bool
布尔型(boolean):true
或者false
大小:1 bytes
实际只有1 bits 就可以表示该类型数据了,只有两个值嘛,但是实际上内存寻址是无法定位到bits的,只能定位带bytes,所以该类型的数据大小是1bytes.
#include<iostream>int main()
{bool variable = false; // 定义一个bool型变量并初始化为falsestd::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cin.get();
}
实际运行结果并不是输出字符:false
,而是数字 0
:
因为计算机只处理数字,0
代表false
,1
代表true
。
然而给bool
变量赋值时,只有赋值0
或者false
时结果为0
,也就是false
;赋值其他任意值结果都是1
,也就是true
。
#include<iostream>int main()
{bool variable = 6; // 定义一个bool型变量并初始化为6std::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cin.get();
}
如何查看数据大小
C++提供了一个查看数据大小的方法:sizeof()
:
#include<iostream>int main()
{bool variable = 6; // 定义一个bool型变量并初始化为6std::cout << "The char variable is : " << variable << std::endl; // 打印输出std::cout << "The size of this char variable is : " << sizeof(variable) << std::endl; // 打印输出std::cin.get();
}
输出:
可以看到该变量的大小为1 bytes
。
sizeof()
也可以查看类的大小:
#include<iostream>int main()
{std::cout << "The size of int is : " << sizeof(int) << std::endl; // 打印输出std::cin.get();
}
输出:
函数
函数就是编写的代码块去执行某个特定的任务
最常用的场景就是把重复的代码写成一个函数,到需要的时候直接调用函数来完成,既节省代码量整体代码的结构也会更加清晰。
函数基本包含:
- 函数的类型
- 函数名
- 参数
- 返回值
示例:
#include<iostream>int Multiply(int a, int b)
{/*两个整数相乘*/std::cout << "This function is used" << std::endl;return a * b;
}int main()
{int result = Multiply(2, 3);std::cout << "result : " << result << std::endl;
}
调用函数非常简单,直接用函数名称就可以了,需要注意的是函数的返回值需要对应的变量来存储。
函数并不是越多越好,阳极必衰,实际上函数过多反而会显得代码十分混乱,很难维护,而且代码执行会更慢!!!每次调用函数时,编译器会生成一个调用指令,这意味着,在一个运行的程序中,为了调用一个函数,需要为这个函数创建一整个栈框架(stack frame),需要把所有参数之类的东西推(push)到栈上,还需要把返回地址放到栈上巴拉巴拉一堆内容,程序为了执行函数指令会在内存中跳来跳去,都是时间!!!
函数的使用主要目的是防止代码重复!!!
头文件
头文件传统上是用来声明某些函数类型,以便可以用于整个程序中,只是声明,而不是定义。
#include<iostream>void Log(const char* message);int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main()
{std::cout << Multiply(2, 4) << std::endl;std::cin.get();
}
上面这段代码中就声明了一个函数Log
,但是并没有函数的实体,真正的函数体在项目中的其他文件中,但是如果我有多个翻译单元,也就是多个.cpp
,它们都使用了这个函数,岂不是每个文件都需要这样一句声明。这也太蠢了吧!
这就是头文件的作用:声明函数!
头文件的预处理指令#include
的作用不就是复制粘贴文件内容嘛,那么基本上就是这样:
函数文件:Log.cpp
:
#include<iostream>void Log(const char* message)
{std::cout << message << std::endl;
}
头文件Log.h
:
#pragma once // 头文件保护符,防止同一个翻译单元include该文件两次void Log(const char* message);
主函数文件main.cpp
:
#include "Log.h"int main()
{Log("Hello World");
}
现在函数内容比较简单,所以看起来好像很复杂啊,毕竟上面的这三个文件加起来做的事情和下面一个文件是一致的:
#include<iostream>void Log(const char* message)
{std::cout << message << std::endl;
}int main()
{Log("Hello World");
}
确实,不过当项目工程庞大的时候,把所有代码写在一个文件里会变得非常难维护。
条件语句
如果if
怎么怎么样就怎么怎么样,否则else
怎么怎么样:
#include "Log.h" // Log.h是头文件章节中的内容
#include<iostream>int main()
{int x = 5;bool compareResult = x == 5; // bool类型变量,赋值为0时结果为false,除此之外全为true,此处x == 5为比较运算,此时为正确的, 值就是1, 也就是trueif (compareResult) { // if语句的格式,如果参数部分为true则执行{}内的代码,否则直接跳过Log("Hello World!"); // 打印输出字符std::cin.get();}
}
重要但是尽量少用,会让代码执行效率变低
bool
:布尔类型变量,true/false,但实质上只是两种数字:0和非零的任意数字,0代表false,非0代表true
==
:等于运算符,判断两端的值是否相等,相等返回true,否则返回false
上述代码的汇编语言:
int x = 5;
00A41BF3 mov dword ptr [x],5 bool compareResults = x == 5;
00A41BFA cmp dword ptr [x],5
00A41BFE jne main+29h (0A41C09h)
00A41C00 mov dword ptr [ebp-4Ch],1
00A41C07 jmp main+30h (0A41C10h)
00A41C09 mov dword ptr [ebp-4Ch],0
00A41C10 mov al,byte ptr [ebp-4Ch]
00A41C13 mov byte ptr [compareResults],al if (compareResults) {
00A41C16 movzx eax,byte ptr [compareResults]
00A41C1A test eax,eax
00A41C1C je main+57h (0A41C37h) Log("Hello World!");
00A41C1E push offset string "Hello World!" (0A47B30h)
00A41C23 call Log (0A4120Dh)
00A41C28 add esp,4 std::cin.get();
00A41C2B mov ecx,dword ptr [__imp_std::cin (0A4A0B0h)]
00A41C31 call dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::get (0A4A0B4h)] }
}
if
语句的参数不一定非要是一个确定的变量,只要是可以返回一个结果的内容,都可以,上面的代码就可以直接写成下面的内容:
#include "Log.h"
#include<iostream>int main()
{int x = 5;if (x == 5) {Log("Hello World!");std::cin.get();}
}
因为==
运算符会有两种结果:0或者1,而if
判断语句只要判断括号内的条件是不是0就行了,是0就是false,不执行if
里面的内容,是其他的值就是true,执行if
下面的内容。
如果if
语句只包含一行待执行的代码语句,可以去掉{}
,:
#include "Log.h"
#include<iostream>int main()
{int x = 5;if (x == 5) Log("Hello World!");std::cin.get();}
if
只检查数字!!!!!!
与if
对应就是else
:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = 0; // 指针ptr指向空(NULL)if (ptr) Log(ptr); // 判定条件为false, 跳转至elseelse Log("ptr is null"); // 打印输出相应内容std::cin.get();
}
即使
if
语句写成了两行,但实际上仍然是一句代码,判断一条语句的结束就是看;
,一个分号表示该句结束。
除了上述这样给代码两条分支以外,还可以叠加使用if
,就是else if
:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = "Hello"; // 指针ptrif (ptr)Log(ptr); // 判定条件为true, 进入该分支,打印输出else if (ptr == "Hello") // 尽管为true,但是并不会执行,因为第一个分支的判定true就会跳出分支结构Log("ptr is Hello"); elseLog("ptr is null");std::cin.get();
}
需要注意,如果代码已经进入了一个分支,那么其他分支是不会执行的,上面代码中的第二个条件是不会执行的,因为如果ptr == "Hello"
是成立的,那么在第一个条件语句判断时就是成立的,就只会执行第一个分支内的代码,后面的就不会执行了。
只有前面的分支判断条件为false时,后面的分支判断才会执行。如果第一个分支的条件成立,那么自然执行第一个分支的内容,后面的分支会直接跳过
如果我们想让中间的分支也执行,可以再重新创建一个分支:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = "Hello"; // 指针ptrif (ptr)Log(ptr); // 判定条件为true,该分支结束,继续执行分支结构后面的代码if (ptr == "Hello") // 进入第二个分支Log("ptr is Hello"); // 打印输出相应内容elseLog("ptr is null");std::cin.get();
}
这样就会打印两条语句。
也就是说**if
和else
是相对应的,上面代码的else
对应的就是第二个if
**
另外else if
并不是关键字,它只是else
和if
的一种组合,else
相当于它所对应的if
的另一条分支,只不过在这一条分支中,首先是一个条件判断而已,所做的事情就是:
#include "Log.h"
#include<iostream>int main()
{const char* ptr = "Hello";if (ptr)Log(ptr); else {if (ptr == "Hello")Log("ptr is Hello"); elseLog("ptr is null");}std::cin.get();
}
虽然分支和比较在一个应用中非常重要,但是比较会降低代码的运行效率,能用其他的方法代替的话就尽量代替掉比较操作
循环语句
循环就是多次执行相同的代码,比如:重复打印"Hello World"五次:
#include<iostream>int main()
{// for 循环实现重复打印 5 次 "Hello World"for (int i = 0; i < 5; i++){std::cout << "Hello World" << std::endl;}std::cout << "###############" << std::endl;// while 循环实现重复打印 5 次 "Hello World"int j = 0;while (j < 5){std::cout << "Hello World" << std::endl;j++; // 整型变量j自加1}std::cin.get();
}
for
循环
-
语法格式:
for (控制变量;循环条件;控制变量更新) {循环执行的代码 }
for (int i = 0; i < 5; i++) // 创建一个整型变量i,初始化为0, 执行条件判断, i < 5 为true,执行循环体内的代码, 执行完毕,执行i++, 对i的值进行更新, 然后重新判断 i < 5 是否为true, 循环执行代码,直到i < 5 的值为false, 跳出循环体, 执行接下来的代码 {std::cout << "Hello World" << std::endl; }
-
注意事项
-
for
循环中()
的部分分为三个内容:- 循环开始时的标记
- 循环终止条件
- 每次循环执行结束后对
i
的值的更改
必须由三部分,但是,不需要全部有内容也是可以的:
#include<iostream> #include"Log.h"int main() {// for 循环实现重复打印 5 次 "Hello World"int i = 0; // 循环开始时i = 0bool condition = true; // 循环条件conditionfor (; condition; ) // for 三个部分必须有, 但可以没有内容, 如果condition也去掉的话, 就没有退出循环的条件判定了, 程序就会进入死循环{Log("Hello World"); // 调用函数打印内容if (!(i < 5)) // 如果 i < 5 不成立{condition = false; // 循环条件更改为false, 然后本次循环结束后,重新检查condition,发现为false, 程序就不会再执行循环体了}i++;}std::cin.get(); }
-
i
:不一定非得是整数,其他形式也是可以的,甚至可以是一些自定义的函数,想象力有多大只要符合三部分的功能都可以;
-
while
循环
-
语法结构:
while(循环判定条件) {循环体 }
-
注意事项
- 循环判定条件为一个bool值
- 通常会在循环体内进行判定条件的更新,但是也不是硬性规定
- 判定条件给一个永真值就会进行死循环
do...while
循环
-
语法格式:
do {循环体 }while(循环条件);
-
注意事项:
- 该语句无论循环条件是否成立都会先执行
do
一遍循环体的代码,然后再判断是否继续循环
- 该语句无论循环条件是否成立都会先执行
控制流
控制流(control flow)语句基本上可以解释为可以更好的控制循环。
-
continue
-
只能用在循环内部
-
结束本次循环,如果还有下一次循环,执行下一次循环;如果没有,就结束整个循环
-
示例:只打印4次
#include<iostream> #include"Log.h"int main() {// for 循环实现重复打印 5 次 "Hello World"for (int i = 0; i < 5; i++){if (i == 2)continue; // 如果i==2, 执行continue,跳出i == 2 时的循环, 也就是说i == 2 时下面打印输出的工作就没有进行了, 所以最后的输出就只有四次打印输出的结果Log("Hello World");}std::cin.get(); }
-
-
break
-
可以用在循环,switch语句中
-
直接跳出整个循环
-
示例,只打印两次
#include<iostream> #include"Log.h"int main() {// for 循环实现重复打印 5 次 "Hello World"for (int i = 0; i < 5; i++){if (i == 2)break; // i == 2 时, 跳出循环, 此时循环体只执行了i == 0, 1时的循环Log("Hello World");}std::cin.get(); }
-
-
return
- 直接完整地退出函数
指针(Pointer)
指针的本质
计算机运行一个程序 ,或者运行我们的代码,就是把程序和代码推到内存当中,所有的东西都会被存储在内存中,然后cpu根据指令在内存中给出相应的操作。
指针就是管理和操作内存的重要道具
指针本质上还是一个整数数字,它存储的是一个内存地址。
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
-
定义一个指针的语法格式:
类型* 名字 int* i
示例代码:
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{void* ptr = 0; /*定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针等同于:void* ptr = NULL;void* ptr = nullptr; C++11标准引入*/ int i = 6; // 定义一个整型变量 i, 初始化为 6ptr = &i; /*&取地址符,将变量i的地址给前面定义的指向NULL的无类型指针ptr, 这样是正确的,因为指针就是数字,本质上没有类型区分*/ LOG(ptr);std::cin.get();
}
如果debug一下就会发现:
i
:整型变量值为6
&i
:加了取地址符,值就是该变量所在的内存地址
ptr
:指向全0的无效内存地址
在给ptr
赋值以后:
ptr
指向了i
的地址,那么该地址内的内容是:
可以看到本质上一个整型变量在内存中只不过是32位也就是4字节的大小,存储的内容是数字6,还有对应的内存地址。
指针保存这个内存地址,变量本身保存实际的值,这个值也是数字:
即使是一个char
字符类型的变量,在实际的内存中也是以数字存储的。如下,一个char
字符在内存中占用8位也就是1个字节,而且保存的内容是74
,并不是字符t
综上,我的感觉就是无论是变量还是指针,本质上是并不区分类型的,所谓的类型只是方便编程规定的。
现在用整型指针指向整型变量会发现:
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{int* ptr = 0; /*定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针等同于:void* ptr = NULL;void* ptr = nullptr; C++11标准引入*/ int i = 6; // 定义一个整型变量 i, 初始化为 6ptr = &i; /*&取地址符,将变量i的地址给前面定义的指向NULL的无类型指针ptr, 这样是正确的,因为指针就是数字,本质上没有类型区分*/ LOG(ptr);std::cin.get();
}
使用整型指针与使用无类型的指针没什么区别!!!
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
指针之所以区分类型是因为编译器需要明白我们定义的这个指针操作的是多大的内存块,指向int
就是我们的指针可以操作这4个字节的块,char
就可以更改1字节的块,就是内存块的大小问题!但是本质上就是上面的一句话,方便学习理解。
指针的用法
- 通过指针(内存地址)取实际的值,或者说**指针就是来操作对应位置中规定大小的内存块。**使用
*
逆向引用(dereferencing)来取地址中的值(访问地址中的数据)
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{int* ptr = 0; int a = 6;ptr = &a; // ptr指向a的地址LOG(ptr); // 地址LOG(*ptr); // 解引用,实际的值std::cin.get();
}
-
分配内存
#include<iostream>#define LOG(x) std::cout << x << std::endlint main() {char* buffer = new char[8]; // 分配8个字节的内存,并返回指向这块内存的开始地址的指针buffermemset(buffer, 1, 8); // 为指定的地址buffer所指向的内存中填充8个字节的1delete[] buffer;std::cin.get(); }
可以看到buffer
指向的地址中用了8个字节保存了8个1。
- 指针也是变量,也就说指针同样可以指向指针,只需要在定义时使用两个
*
。
指针只是一个存储着内存地址的整数!!!
引用(Reference)
引用和指针几乎一致,引用是基于指针的一种语法结构,使得代码更易读易写。
引用
:指对现有变量引用的一种方式,**引用
必须引用一个已经存在的变量,引用本身并不是一个新的变量。**并不占用内存。简单说引用就是给起个别名。
示例:
#include<iostream>#define LOG(x) std::cout << x << std::endlint main()
{int var = 6;int& ref = var; // 创建一个引用,相当于给变量var起一个别名LOG(ref); // 直接输出引用ref, 结果是: 6ref = 5; // 更改引用ref的值,就是更改原来变量var 的值LOG(ref); // 5LOG(var); // 5
}
为什么要有引用?
先看一个c++的示例:
#include<iostream>#define LOG(x) std::cout << x << std::endlvoid AddSelf(int value)
{/*自加1*/value++;
}int main()
{int var = 5; // 初始化整型变量 var = 5AddSelf(var); // 执行AddSelf函数, 自加LOG(var); // 输出var
}
自定义函数的形参int vaule
是一个变量,函数功能是实现自加1,按逻辑来说,把变量var
作为实参传入函数中,操作的应该的是var
这个实参,但是实际最后输出的结果并没有自加1。
为什么会这样?
因为如果一个函数的形参是一个变量,那么当调用函数的时候,函数中会创建一个新的变量value
并赋值传入的实参中的数。
上面的自定义函数实际运行中更像是下面的代码:
#include<iostream>#define LOG(x) std::cout << x << std::endlvoid AddSelf(int value)
{/*自加1*/int value = 5; // 实际调用函数的时候会定义形参并赋值为传入的实参 value++;
}int main()
{int var = 5; // 初始化整型变量 var = 5AddSelf(var); // 执行AddSelf函数, 自加LOG(var); // 输出var
}
如果希望函数实际的更改变量本身的值怎么办?这时候就可以采用引用的方法:
#include<iostream>#define LOG(x) std::cout << x << std::endlvoid AddSelf(int& value)
{/*自加1*/ value++;
}int main()
{int var = 5; // 初始化整型变量 var = 5AddSelf(var); // 执行AddSelf函数, 自加LOG(var); // 输出var
}
道理与前面一致,前面形参为变量时,当调用函数的时候,创建一个变量来存储传入的实参,操作的是这个新的变量;现在如果形参是一个引用的话,当调用函数时,创建一个引用来引用传入的实参,操作的是这个引用,然而引用前面讲过,更改引用会更改原来变量的值,所以理所当然就可以通过把形参设置为引用的方法使函数对变量本身产生作用。
注意事项
-
一个引用只能引用一个变量,不能引用了一个变量后更改该引用的变量对象
int main() {int var = 5; // 初始化整型变量 var = 5int var_2 = 6;int& ref = var;ref = var_2; // 并不是引用var_2, 而是把var_2的值赋值给ref,也就是赋值给varLOG(var); // 6 }
-
引用必须初始化,必须引用一个确定的变量,不能仅仅是定义一个引用
类(class)
面向对象编程是一种编程思想,不注重解决问题的详细过程,而是设计具有相应属性和方法的对象,通过这些具有不同功能的对象来完成任务。
类是一种将数据和函数组织在一起的方式
-
C++中的类的示例:
#include<iostream>#define LOG(x) std::cout << x << std::endlclass Player // class 关键字 表示这是一个类, Player 类的名称 { public: // public: 表示冒号后面的内容为公有内容,可以从类的外部进行访问int x = 0; // 定义类的两个属性, 在类中的变量通常称为 属性int y = 0; // 表示玩家的位置坐标void Move(int stride) // 定义一个方法, 在类中的函数通常称为 方法{/*该方法实现移动玩家位置*/x = x + stride; // x不需要额外定义,在类的内部就是该类的属性xy = y - stride;}}; // 类的定义要有 ; 作为结束int main() { Player one; // 创建一个Player类的对象, 通常称为 实例化一个对象one.x = 120; // 通过 对象名.属性 取到对应的属性one.y = 120;one.Move(5); // 通过 对象名.方法 执行相应的方法LOG(one.x);LOG(one.y); }
class
: 关键字,表明这是一个类。后面接类名,然后是一对{}
,最后要有一个;
。
public
:关键字,接一个:
,后面接属性或方法。表明后面的内容是公有的,可以让类外面的成员访问。(c++的类中的内容默认为私有(private),外部是不可以直接访问的)
属性和方法
: 类内的数据通常称为属性,函数通常称为方法。
class(类) VS struct(结构体)
-
一个class的成员默认情况是private(私有)的,而struct正好相反;
-
c++中保留struct是考虑到与c的兼容性,c中是没有class的。简单的方式完成两种代码的转换只需要一个宏定义:
在c++代码种使用#define class struct在c代码中使用#define struct class
这样就把
c++
中的class
变成了struct
, 反之亦然。 -
何时使用
class
以及什么时候使用struct
看自己的编程风格了(毕竟两者的区别就是上面两个而已<‘’ – ‘’>),建议如果需要使用的某个东西仅包含属性(数据),那么就用struct
,如果这个东西除了属性还有其他功能的方法那就用class
吧。
类的写法
写一个日志类,用来打印不同的信息:
#include<iostream>class Log
{/*Log类, 根据不同的级别设置来打印不同的信息*/
public:/*设置三种日志级别*/const int LogLevelError = 0;const int LogLevelWarning = 1;const int LogLevelInfo = 2;
private:int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Infopublic:void SetLevel(int level){/*设置日志打印信息的级别*/m_LogLevel = level;}void Error(const char* message){/*错误信息*/if (m_LogLevel >= LogLevelError)std::cout << "[ERROR]:" << message << std::endl;}void Warn(const char* message){/*警告信息*/if (m_LogLevel >= LogLevelWarning)std::cout << "[WARNING]:" << message << std::endl;}void Info(const char* message){/*所有信息*/if (m_LogLevel >= LogLevelInfo)std::cout <<"[INFO]:" << message << std::endl;}
};int main()
{Log log; // 实例化一个Log类,用来打印日志信息log.SetLevel(log.LogLevelInfo); // 设置日志信息的级别log.Warn("There is a warning!"); // 打印日志中的警告信息log.Error("There is an erron!!"); // 打印日志中的警告信息log.Info("There is information."); // 打印日志中的信息
}
上述代码仅作示例,有很多问题而且非常不规范。接下来逐步完善上面的代码。
static关键字
两种含义:
- 类或结构体之外:类外的
static
关键字修饰的符号在链接阶段是局部的。只对当前所在的翻译单元可见(只在所在的文件中可见)其他的文件是无法访问被修饰的内容的。
main.cpp
:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlint s_Variable = 10; // 全局变量s_Variable 初始化为整型常量 10int main()
{LOG(s_Variable); // 打印输出变量 s_Variable
}
static.cpp
:
static int s_Variable = 6;
在上面的这种文件结构下,编译无错误,输出结果是 10。
如果更改static.cpp
中的内容为:
int s_Variable = 6;
此时编译是可以通过的,但是在运行时会引发链接错误:
链接器会找到一个或多个多重定义的符号,是因为链接器不仅会找到main.cpp
中的变量s_Variable
,而且由于static.cpp
中的变量不是静态的(static),也会被链接器找到,所以会引发该错误。
除上面的把static.cpp
中的全局变量s_Variable
用static
修饰外,还有另外的解决方法:在main.cpp
中,使用关键字extern
修饰该全局变量:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlextern int s_Variable; // 表明该变量在其他的位置int main()
{LOG(s_Variable); // 打印输出变量 s_Variable
}
建议:尽量让全局变量或者函数使用static
关键字修饰
- 类或结构体内部:表示所修饰的符号为类或结构体的所有实例所共享的。即使该类被多次实例化,但是被
static
修饰的符号只会有一个实例。比如在类的内部定义一个属性使用static
修饰,那么当多次实例化该类时,这个属性只有一个:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:int x, y; // 两个普通的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1 = {2, 3}; // 第二次实例化Entity类 初始化器可以直接给对象的属性赋值e.Print(); // 0 1e1.Print(); // 2 3
}
一切正常,但是当在类的内部使用static
关键字来修饰两个属性时:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1 = {2, 3}; // 第二次实例化Entity类 初始化器可以直接给对象的属性赋值e.Print(); e1.Print();
}
此时编译代码就会报错:
如果更改e1
的初始化方式:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1;e1.x = 2;e1.y = 3;e.Print(); e1.Print();
}
此时报错内容为:
无法解析的外部符号,那么我们就把这两个定义一下:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int Entity::x; // 作用域::变量名
int Entity::y; // 作用域::变量名int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity e1;e1.x = 2;e1.y = 3;e.Print();e1.Print();
}
编译终于通过了,但是会发现一个很奇怪的现象,两个对象的属性值竟然是一样的,都是:2 3
。可是明明对e
的两个属性赋值为0和1
啊。
这就是开始说到的:类或者结构体内部的static
修饰的符号无论实例化几次,这些符号仅存在一个!!!会被最新的值覆盖掉!
拿上面的例子来说:static
修饰了两个符号x
和y
,那么所有类的实例中的这两个属性指向的是同一个共享空间!
所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过Entity::x
的方式来引用:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity
{
public:static int x, y; // 两个静态的公有属性void Print(){/*输出两个属性*/std::cout << x << ' ' << y << std::endl;}
};int Entity::x;
int Entity::y;int main()
{Entity e; // 实例化一次Entity类e.x = 0;e.y = 1;Entity::x = 5;Entity::y = 6;e.Print(); // 5 6
}
注意事项:
-
基于所有实例的静态内容都共享内容这一特性,可以把需要所有实例都要使用的相同内容设置为静态。
-
使用静态内容可以不实例化:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlclass Entity { public:static int x, y; // 两个静态的公有属性static void Print() // 设置为静态方法{/*输出两个属性*/std::cout << x << ' ' << y << std::endl;} };int Entity::x; int Entity::y;int main() {Entity::x = 5;Entity::y = 6;Entity::Print(); }
-
静态方法不能访问非静态变量。静态的内容没有引用指针,本质上类内的每个非静态方法都会获得当前的类实例作为参数:
void Print(Entity e) // 传入类的实例 {/*输出两个属性*/std::cout << e.x << ' ' << e.y << std::endl; }
但是静态方法是没有这个隐藏的参数的,在实际运行中就类似下面这样:
静态的方法由于没有隐藏参数并不知道实例对象是谁的实例!!
局部作用域中的static
- 变量的生命周期:变量实际的存在时间,也就是变量在被删除前在内存中停留多久。
- 作用域:可以访问该变量的范围,比如函数内部定义一个变量,通常是不能在其他函数里访问到这个变量,这样的变量称为局部(local)变量。
- 静态局部(local static)变量:声明一个变量,生命周期是整个程序的生存期。但作用域被限制在其所在作用域中。
示例代码:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlint main()
{for (int j = 0; j <= 4; j++){static int v = 0; // 静态局部变量 v 初始化为 0v++; // v自加1LOG(v); // 打印输出 v 的值}
}
上面的代码循环执行了5次,每次循环的内容为:
- 定义一个静态局部变量v,并初始化为整型数字0;
- v自加1;
- 打印输出v的值, 结果为
1 2 3 4 5
如果去掉关键字static
:
#include<iostream>#define LOG(x) std::cout<<x<<std::endlint main()
{for (int j = 0; j <= 4; j++){int v = 0; // 局部变量 v 初始化为 0v++; // v自加1LOG(v); // 打印输出 v 的值}
}
由于每次循环都重新定义了v
,并初始化为0,所以每次输出的结果都是 1,也就是1 1 1 1 1
。
这就是说,当使用static
关键字修饰时,被修饰的内容的生命周期为程序的整个运行周期,在程序运行过程中,始终存在在内存当中,其中的值是一直存在的,而没有static
关键字的话,就会在每次循环开始时,都会创建一个新的变量,在内存中设定一个新的地址用来存储对应的值。
枚举(enums)
#include<iostream>#define LOG(x) std::cout<<x<<std::endlenum Example // 枚举类型, 里面包含三个值
{A, B, C
};
/*
int a = 0;
int b = 1;
int c = 2;
*/
int main()
{Example value = 1; // 报错, 当定义一个枚举类型的变量时,值必须是枚举所包含的值LOG(value);
}
- 在枚举中的值默认必须是整数
即使向上面的代码,使用了A, B, C
,没有指定类型但是一旦出现在枚举中,它们实际的值就是1, 2, 3
。
- 默认情况下第一个变量的值为
0
,依次递增
- 也可以自己设定值,但是必须是整数
#include<iostream>#define LOG(x) std::cout<<x<<std::endlenum Example // 枚举类型, 里面包含三个值
{A = 1, B = 3, C = 5
};
/*
int a = 0;
int b = 1;
int c = 2;
*/
int main()
{Example value = B; // 3LOG(value);
}
- 也可以自己设定枚举中的类型
enum Example : unsigned char // 枚举类型, 里面包含三个值
{A, B, C
};
使用枚举的例子
之前的Log类:
#include<iostream>class Log
{/*Log类, 根据不同的级别设置来打印不同的信息*/
public:/*设置三种日志级别*/const int LogLevelError = 0;const int LogLevelWarning = 1;const int LogLevelInfo = 2;
private:int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Infopublic:void SetLevel(int level){/*设置日志打印信息的级别*/m_LogLevel = level;}void Error(const char* message){/*错误信息*/if (m_LogLevel >= LogLevelError)std::cout << "[ERROR]:" << message << std::endl;}void Warn(const char* message){/*警告信息*/if (m_LogLevel >= LogLevelWarning)std::cout << "[WARNING]:" << message << std::endl;}void Info(const char* message){/*所有信息*/if (m_LogLevel >= LogLevelInfo)std::cout << "[INFO]:" << message << std::endl;}
};int main()
{Log log; // 实例化一个Log类,用来打印日志信息log.SetLevel(log.LogLevelInfo); // 设置日志信息的级别log.Warn("There is a warning!"); // 打印日志中的警告信息log.Error("There is an erron!!"); // 打印日志中的警告信息log.Info("There is information."); // 打印日志中的信息
}
其中的三种日志信息的等级就是一个使用枚举很好的例子:
#include<iostream>class Log
{/*Log类, 根据不同的级别设置来打印不同的信息*/
public:/*设置三种日志级别*///const int LogLevelError = 0;//const int LogLevelWarning = 1;//const int LogLevelInfo = 2;enum Level // 使用枚举来设置三种级别,与上面的功能一致{LevelError, LevelWarning, LevelInfo};
private:Level m_LogLevel = LevelInfo; // 同时将自己的设置级别也更改为枚举类型public:void SetLevel(Level level){/*设置日志打印信息的级别*/m_LogLevel = level;}void Error(const char* message){/*错误信息*/if (m_LogLevel >= LevelError)std::cout << "[ERROR]:" << message << std::endl;}void Warn(const char* message){/*警告信息*/if (m_LogLevel >= LevelWarning)std::cout << "[WARNING]:" << message << std::endl;}void Infom(const char* message){/*所有信息*/if (m_LogLevel >= LevelInfo)std::cout << "[INFO]:" << message << std::endl;}
};int main()
{Log log; // 实例化一个Log类,用来打印日志信息log.SetLevel(log.LevelWarning); // 设置日志信息的级别log.Warn("There is a warning!"); // 打印日志中的警告信息log.Error("There is an error!!"); // 打印日志中的警告信息log.Infom("There is information."); // 打印日志中的信息
}
构造函数vs析构函数
构造函数
Constructors,一种特殊的函数,在类的每次实例化的时候运行
示例:
#include<iostream>class Entity
{
public:float X, Y;void Print(){std::cout << X << "," << Y << std::endl;}
};int main()
{Entity e;std::cout << e.X << std::endl; // 编译报错:使用了未初始化的局部变量“X”e.Print();
}
上述代码报错并提示:使用了未初始化的局部变量“X”
但是当删除报错的代码时是可以正确运行的,但是会输出随机的值,这是因为当变量没有初始化的时候会存储内存中划分给这两个变量的位置存储的当时的值
所以初始化很重要
构造函数就是在我们实例化一个类的时候自动执行的函数,写法如下:
#include<iostream>class Entity
{
public:float X, Y;Entity() // 构造函数,在实例化该类的时候会自动执行该函数{X = 0.0f;Y = 0.0f;}void Print(){std::cout << X << "," << Y << std::endl;}
};int main()
{Entity e;std::cout << e.X << std::endl; e.Print();
}
- 如果不写构造函数,实际上仍然是存在的,这个构造函数被称为默认构造函数(default constructor),但是默认构造函数不做任何操作(相当于上面代码中的构造函数中什么内容都没有)
- C++中的变量需要手动初始化,不然就会被设定为之前留存在内存中的值。
- 构造函数会在实例化类的时候自动执行,由于使用类内部的静态内容时可以不实例化,那么当然也就不会执行构造函数。
构造函数可以设置参数,设置参数后需要在实例化该类时传入参数,一个类可以有多个构造函数,根据参数不同来划分:
#include<iostream>class Entity
{
public:float X, Y;Entity() // 构造函数,在实例化该类的时候会自动执行该函数{X = 3.3f;Y = 4.4f;}Entity(float x, float y) // 构造函数,在实例化该类的时候会自动执行该函数{X = x;Y = y;}void Print(){std::cout << X << "," << Y << std::endl;}
};int main()
{/*构造函数的参数需要在实例化该类时传入如果传入了参数,就会执行带参数的构造函数如果没有传入参数就会执行,没有参数的构造函数*/ Entity e(1.1f, 2.2f); e.Print();
}
析构函数
析构函数(Destructors),在销毁一个对象的时候运行
当一个对象被销毁的时候析构函数都会被调用
- 使用关键字
new
创建一个对象,那么这个对象会存在于堆上,调用delete
的时候,析构函数会被调用 - 对于基于栈的对象,当跳出作用域的时候这个对象会被删除,此时析构函数也会被调用
示例:
#include<iostream>class Entity
{
public:float X, Y;Entity(float x, float y){// 构造函数X = x;Y = y;}~Entity(){// 析构函数std::cout << "Destructors" << std::endl;}void Print(){std::cout << X << " " << Y << std::endl;}
};int main()
{Entity e(1.1f, 2.2f); e.Print(); // 1.1 2.2 // 当main函数执行完成会自动执行析构函数的内容 打印输出Destructors
}
类的继承
inheritance
把一系列类的通用部分放在基类中,然后使用继承基类的方法,使代码的重复度降低。
首先下面两个类:
class Entity
{
public:float X, Y;void Move(float xa, float ya){X += xa;Y += ya;}
};class Player
{
public:const char* Name;float X, Y;void Move(float xa, float ya){X += xa;Y += ya;}void PrintName(){std::cout << Name << std::endl;}
};
Player
类中的某些内容很明显是与Entity
重复的,此时就可以使用继承:
class Player : public Entity
{
public:const char* Name;void PrintName(){std::cout << Name << std::endl;}
};
此时Player
类就继承了Entity
的内容:两个属性和一个Move
方法,另外还有自身的属性和方法。
int main()
{Player pc;pc.Move(0.5f, 0.5f);pc.Name = "ok";pc.PrintName();
}
实例化Player
类以后,实例对象pc
可以使用两个类中的内容(属性和方法)。
string
- 表示可变长的字符序列
- 定义在命名空间
std
中
代码示例:
#include <iostream>
#include <string>int main()
{std::string s; // 默认初始化 空字符串std::cout << s << std::endl;std::cout << sizeof(s) << std::endl; // 28字节
}
初始化
#include <iostream>
#include <string>int main()
{std::string s; // 默认初始化 空字符串std::string s1 = "copy"; // 拷贝初始化std::string s2("directly"); // 直接初始化std::string s3(5, 'h'); // 直接初始化 s3存储的是 5 个 hstd::cout << s.empty() << std::endl; // 1 empty()方法返回bool值,为真时表示s是空字符串
}
注意事项及相关操作
- 输入字符到
string
对象会自动忽略字符串中的空白项,以第一个字符为开始,顺序向后遇到空白结束
#include <iostream>
#include <string>int main()
{std::string s; // 默认初始化 空字符串std::cin >> s; // 输入字符串std::cout << s << std::endl; // 从顺序遇到的第一个字符开始,顺序向后第一个空白结束
}
采用同样的输入,如果使用两个string
对象接收的话,每个对象会存储一个连续的字符串:
#include <iostream>
#include <string>int main()
{std::string s, s1; // 默认初始化 空字符串std::cin >> s >> s1; // 输入字符串std::cout << s << s1 << std::endl; // 顺序输出两个字符串
}
-
字符串的比较
-
字符串本身有有长度的不同,获取字符串的长度可以简单地使用
size()
,注意sizeof()
是获得对象占用内存空间的大小#include <iostream> #include <string> using namespace std;int main() {string s("cpp");cout << s.size() << endl; // 3cout << sizeof(s) << endl; // 28 }
-
字符串之间的大小不同,字符串的大小比较:顺序比较对应位置的两个字符,如果字符相同,顺序向后,直到出现不同的字符(包括同一字符的大小写,也属于不同的字符),字符大的字符串就大,与字符串的长度无关
#include <iostream> #include <string> using namespace std;int main() {string s1("abc"); // 长度为3string s2("de"); // 长度为2if (s1 > s2) // falsecout << "s1" << endl;else if (s1 == s2) // falsecout << "s1 == s2" << endl;else cout << "s2" << endl; // run s2 大于 s1, 因为 d 大于 a }
-
-
字符串的相加
-
字符串可以直接使用
+
运算符相加,效果是左侧的字符串后面拼接右侧的字符串的内容。#include <iostream> #include <string> using namespace std;int main() {string s1("abc");string s2("de");cout << s1 + s2 << endl; // abcde }
-
字符串对象可以直接与字符串常量相加,但两个字符串常量是不能直接
+
的。#include <iostream> #include <string> using namespace std;int main() {string s1("abc");cout << s1 + "de" << endl; // abcdecout << "abc" + "de" << endl; // error }
-
-
取出字符串中的字符
-
字符串是由一个个字符顺序连接组成的,使用c++11 标准中提出的一种语法: 范围for循环可以遍历字符串中的字符
#include <iostream> #include <string> using namespace std;int main() {std::string s("abcde");char c;c = s[0]; // 第一个字符"a"cout << "c : " << c << endl; // c : afor (auto a : s) // 定义的变量 : 序列对象cout << a << endl; }
-
虚函数(VirtualFunctions)
虚函数可以让我们在子类中重写方法
有两个类:A,B
B是A的子类,在A中的方法设定为虚(virtual),那么就可以在B类中重写该方法
代码示例:
#include<iostream>
#include<string>class Entity // 父类
{
public:std::string GetName() { return "Entity"; } // 父类方法,返回一个string
};class Player : public Entity // 子类
{
private:std::string m_Name; // 子类私有属性 m_Name
public:Player(const std::string& name) // 构造函数, 用于初始化 m_Name: m_Name(name) {} // 将传入的string值赋给m_Namestd::string GetName() { return m_Name; } // 继承父类的GetName方法
};
void printName(Entity* entity)
{std::cout << entity->GetName() << std::endl;
}
int main()
{Entity* e = new Entity(); // 划分一个存储Entity对象的内存空间, 使用指针e指向该内存地址printName(e); // entityPlayer* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址printName(p); // entity}
多态:不同的子类可以有相同的方法,但可以根据各个子类的需求使用该方法来实现不同的效果。
上面代码可以看到,p
明明是子类的实例化对象,但是执行GetName()方法却使用的父类中的方法。
这时就需要使用虚函数。虚函数可以让我们在子类中重写父类方法。
虚函数的写法:在定义方法时,使用关键字virtual
限制:
virtual std::string GetName() { return "Entity"; }
现在把上面的代码做下面的调整:
- 父类中的方法前加一个
virtual
- 子类中的方法在类型后加一个
override
(加不加都可以,但为了代码可读性建议加上,代表这是一个从父类继承的方法)
结果就可以像我们所期待的那样,各自执行各自的方法:
#include<iostream>
#include<string>class Entity // 父类
{
public:virtual std::string GetName() { return "Entity"; } // 使用关键字virtual表明这是一个虚函数
};class Player : public Entity // 子类
{
private:std::string m_Name; // 子类私有属性 m_Name
public:Player(const std::string& name) // 构造函数, 用于初始化 m_Name: m_Name(name) {} // 将传入的string值赋给m_Namestd::string override GetName() { return m_Name; } // 继承父类的GetName方法
};
void printName(Entity* entity)
{std::cout << entity->GetName() << std::endl;
}
int main()
{Entity* e = new Entity(); // 划分一个存储Entity对象的内存空间, 使用指针e指向该内存地址printName(e); // entityPlayer* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址printName(p); // Rabbit
}
虚函数的代价
虚函数的使用虽然方便,但是也会带来一些代价:
- 需要额外的内存来存储虚函数表
- 每次调用虚函数的时候程序需要遍历虚函数表来找到最重要运行的函数
纯虚函数(接口)
纯虚函数允许我们定义一个在基类中没有实现的函数,强制子类完成该函数
接口(Interface):一个只包含未实现的方法,并作为一个模板的类。由于此接口类实际上不包含方法实现,所以无法实例化这个类。
#include<iostream>
#include<string>class Printable
{// 接口类,仅包含一个纯虚函数
public:virtual std::string GetClassName() = 0;
};class Entity : public Printable
{// 使用接口类的时候必须把接口中的纯虚函数实现
public: std::string GetClassName() override { return "Entity."; }
};class Player : public Entity
{
private:std::string m_Name;
public:// 两个构造函数,根据实例化该类的时候传入的参数决定使用哪个函数Player() {}Player(const std::string& name) : m_Name(name) {} // 实现接口中的纯虚函数std::string GetClassName() override { return "Player."; }
};void printName(Printable* obj)
{std::cout << obj->GetClassName() << std::endl;
}int main()
{Entity* e = new Entity(); // 报错,因为Entity有纯虚函数,无法实例化printName(e); Player* p = new Player(); // 正常运行,因为Player是子类,在自己内部要实现纯虚函数的内容printName(p); }
可见性
可见性是面向对象中的概念,指一个类中的成员或者方法是否可见。
可见
指的是谁可以访问或者调用他们,可见性不会影响程序的实际运行,也不会对程序性能有影响。
三个基础的可见修饰符(访问修饰符):
-
private
- class的可见性默认为private
#include<iostream> #include<string>class Entity {// class 默认为private, 写不写下面这个都可以 private: // 只有在这个类中可以访问到int X, Y; public:Entity(){X = 0;} };class Player :public Entity { public:Player(){X = 1; // 成员 "Entity::X" 不可访问}};int main() {Entity* e = new Entity();e->X = 7; // 成员 "Entity::X" 不可访问 }
无论是自己的子类,或者是实例化的对象都无法访问到private的内容。
-
protected
-
该类以及它的所有派生类可以访问
#include<iostream> #include<string>class Entity { protected: // 只有在这个类及其子类中可以访问到int X, Y; public:Entity(){X = 0;} };class Player :public Entity { public:Player(){X = 1; // 子类可以访问父类中protected的内容} };int main() {Entity* e = new Entity();e->X = 7; // 成员 "Entity::X" 不可访问 }
子类是可以访问到
protected
的内容的,但是其他地方仍无法访问。
-
-
public
-
任何地方都可以访问
#include<iostream> #include<string>class Entity { public: // 公有,任何地方都可以访问int X, Y; public:Entity(){X = 0;} };class Player :public Entity { public:Player(){X = 1; } };int main() {Entity* e = new Entity();e->X = 7; }
-