github仓库不定期更新: https://github.com/han-0111/CppLearning
示例代码HelloWorld:
#include
int main()
/*打印输出HelloWorld!*/
{
std::cout << "Hello World!" << std::endl;
std::cin.get();
return 0;
}
分解代码:
#include
预处理语句
编译器在收到源文件之后,优先执行预处理语句,预处理语句的执行是在实际编译发生之前。
#:标明在该符号之后的都是预处理语句。
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
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
void Log(const char* message);
int main(void)
/*打印输出HelloWorld*/
{
Log("Hello World!");
std::cin.get();
return 0;
}
Log.cpp:
#include
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)。
示例:
新建一个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
.XMM
include listing.inc
.model flat
INCLUDELIB MSVCRTD
INCLUDELIB OLDNAMES
msvcjmc 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 ; COMDAT
push ebp
mov ebp, esp
pop ebp
ret 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 2
push ebp
mov ebp, esp
mov ecx, OFFSET __CDAE11DE_math@cpp
call @__CheckForDebuggerJustMyCode@4
mov eax, DWORD PTR _a$[ebp]
imul eax, DWORD PTR _b$[ebp]
; Line 5
pop ebp
ret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
_TEXT ENDS
END
可以通过上面的汇编代码看到自定义的函数?Multiply@@YAHHH@Z,但是函数在这里会被装饰成带有随机字符和@符号的形式,这就是函数签名,用来独一无二的标记该函数,链接器(linker)就是通过这个函数签名来找到函数的。
总结来说,编译器就是将源代码文件生成包含机器码和我们定义的常量数据的目标文件object file
预处理阶段编译器检查所有的预处理语句(由#符号标记的语句),常见的预处理指令有:
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 INTEGER int
INTEGER Multiply(int a, int b) // INTEGER由于在预处理中重新定义成了int, 所以和使用int效果一致
{
int result = a * b;
return result;
}
这段代码中的预处理语句就是:把代码中出现的INTEGER字符替换成int 字符,效果和直接使用int是一样的。
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
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
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
void Log(const char* message); // 声明有一个函数Log
int 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
#include"Log.h"
void InitLog()
{
Log("Initialized Log");
}
一个Math.cpp:
#include
#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
void Log(const char* message)
{
std::cout << message << std::endl;
}
void InitLog()
{
Log("Initialized Log");
}
Math.cpp就等同于:
#include
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):给一个存储在内存中的数据一个名字,从而来使用这个数据,且这个数据是可以更改的。
代表在给定的范围内存储一个整型数字。
#include
int main()
{
int variable = 1; // 定义一个整型变量variable, 并初始化为整数2
std::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
int main()
{
int variable = 2147483648; // 定义一个整型变量variable, 并初始化为整数2
std::cout << "The int variable number is : " << variable << std::endl; // 打印输出
std::cin.get();
}
此时并不会如期望的那样输出2147483648,而是:

如果用-2147483648来赋值的话就是正常的,因为并没有超出整型变量的范围。
可以使用关键字unsigned限定变量是无符号位:
#include
int main()
{
unsigned int variable = 2147483648; // 定义一个无符号位的整型变量
std::cout << "The int variable number is : " << variable << std::endl; // 打印输出
std::cin.get();
}
此时刚才的临界点的数值就是可以正确打印的了。
大小:1 bytes
存储内容:字符(character)
#include
int main()
{
char c = 'A'; // 定义一个字符型变量c,并初始化为字符 'A'
std::cout << "The char variable is : " << c << std::endl; // 打印输出
std::cin.get();
}
对于char类型的变量也可以按照ASCII码赋值,编译器会根据ASCII码中数值和字符的对应关系存储对应的字符:
#include
int main()
{
char c = 65; // 定义一个字符型变量c,并初始化为65,对应ASCII码表中的字符A
std::cout << "The char variable is : " << c << std::endl; // 打印输出
std::cin.get();
}
上面两种的输出是一致的:

int类型也是一样:
#include
int main()
{
int c = 'A'; // 定义一个整型变量c,并用字符 'A'赋值,会转换成对应的ASCII码的数值65
std::cout << "The char variable is : " << c << std::endl; // 打印输出
std::cin.get();
}
代码会输出字符A对应的ASCII码整数65。
C++中的数据类型的使用非常灵活,自我感觉完全就是取决于写代码的人来决定!😃
C++程序中重点关注的还是内存,当我使用某个数据类型的变量时,会分配多少的内存!
大小:2 bytes
大小:4 bytes
大小:8 bytes
浮点数(小数)
大小:4 bytes
#include
int main()
{
float variable = 1.2f; // 定义一个浮点型变量并初始化为1.2
std::cout << "The char variable is : " << variable << std::endl; // 打印输出
std::cin.get();
}
C++中有两个表示小数类型的关键字float和double,但是当使用float定义变量时如果不在小数后面加一个字符f(大小写都可以)的话,实际上还是以double类型存储的:

加了f的话才会被认定为float类型:

双精度浮点数
大小:8 bytes
布尔型(boolean):true或者false
大小:1 bytes
实际只有1 bits 就可以表示该类型数据了,只有两个值嘛,但是实际上内存寻址是无法定位到bits的,只能定位带bytes,所以该类型的数据大小是1bytes.
#include
int main()
{
bool variable = false; // 定义一个bool型变量并初始化为false
std::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
int main()
{
bool variable = 6; // 定义一个bool型变量并初始化为6
std::cout << "The char variable is : " << variable << std::endl; // 打印输出
std::cin.get();
}

C++提供了一个查看数据大小的方法:sizeof():
#include
int main()
{
bool variable = 6; // 定义一个bool型变量并初始化为6
std::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
int main()
{
std::cout << "The size of int is : " << sizeof(int) << std::endl; // 打印输出
std::cin.get();
}
输出:

函数就是编写的代码块去执行某个特定的任务
最常用的场景就是把重复的代码写成一个函数,到需要的时候直接调用函数来完成,既节省代码量整体代码的结构也会更加清晰。
函数基本包含:
示例:
#include
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
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
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
void Log(const char* message)
{
std::cout << message << std::endl;
}
int main()
{
Log("Hello World");
}
确实,不过当项目工程庞大的时候,把所有代码写在一个文件里会变得非常难维护。
如果if怎么怎么样就怎么怎么样,否则else怎么怎么样:
#include "Log.h" // Log.h是头文件章节中的内容
#include
int main()
{
int x = 5;
bool compareResult = x == 5; // bool类型变量,赋值为0时结果为false,除此之外全为true,此处x == 5为比较运算,此时为正确的, 值就是1, 也就是true
if (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 >::get (0A4A0B4h)]
}
}
if语句的参数不一定非要是一个确定的变量,只要是可以返回一个结果的内容,都可以,上面的代码就可以直接写成下面的内容:
#include "Log.h"
#include
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
int main()
{
int x = 5;
if (x == 5)
Log("Hello World!");
std::cin.get();
}
if只检查数字!!!!!!
与if对应就是else:
#include "Log.h"
#include
int main()
{
const char* ptr = 0; // 指针ptr指向空(NULL)
if (ptr)
Log(ptr); // 判定条件为false, 跳转至else
else
Log("ptr is null"); // 打印输出相应内容
std::cin.get();
}
即使
if语句写成了两行,但实际上仍然是一句代码,判断一条语句的结束就是看;,一个分号表示该句结束。
除了上述这样给代码两条分支以外,还可以叠加使用if,就是else if:
#include "Log.h"
#include
int main()
{
const char* ptr = "Hello"; // 指针ptr
if (ptr)
Log(ptr); // 判定条件为true, 进入该分支,打印输出
else if (ptr == "Hello") // 尽管为true,但是并不会执行,因为第一个分支的判定true就会跳出分支结构
Log("ptr is Hello");
else
Log("ptr is null");
std::cin.get();
}
需要注意,如果代码已经进入了一个分支,那么其他分支是不会执行的,上面代码中的第二个条件是不会执行的,因为如果ptr == "Hello"是成立的,那么在第一个条件语句判断时就是成立的,就只会执行第一个分支内的代码,后面的就不会执行了。
只有前面的分支判断条件为false时,后面的分支判断才会执行。如果第一个分支的条件成立,那么自然执行第一个分支的内容,后面的分支会直接跳过
如果我们想让中间的分支也执行,可以再重新创建一个分支:
#include "Log.h"
#include
int main()
{
const char* ptr = "Hello"; // 指针ptr
if (ptr)
Log(ptr); // 判定条件为true,该分支结束,继续执行分支结构后面的代码
if (ptr == "Hello") // 进入第二个分支
Log("ptr is Hello"); // 打印输出相应内容
else
Log("ptr is null");
std::cin.get();
}
这样就会打印两条语句。
也就是说**if和else是相对应的,上面代码的else对应的就是第二个if**
另外else if并不是关键字,它只是else和if的一种组合,else相当于它所对应的if的另一条分支,只不过在这一条分支中,首先是一个条件判断而已,所做的事情就是:
#include "Log.h"
#include
int main()
{
const char* ptr = "Hello";
if (ptr)
Log(ptr);
else
{
if (ptr == "Hello")
Log("ptr is Hello");
else
Log("ptr is null");
}
std::cin.get();
}
虽然分支和比较在一个应用中非常重要,但是比较会降低代码的运行效率,能用其他的方法代替的话就尽量代替掉比较操作
循环就是多次执行相同的代码,比如:重复打印"Hello World"五次:
#include
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
#include"Log.h"
int main()
{
// for 循环实现重复打印 5 次 "Hello World"
int i = 0; // 循环开始时i = 0
bool condition = true; // 循环条件condition
for (; condition; ) // for 三个部分必须有, 但可以没有内容, 如果condition也去掉的话, 就没有退出循环的条件判定了, 程序就会进入死循环
{
Log("Hello World"); // 调用函数打印内容
if (!(i < 5)) // 如果 i < 5 不成立
{
condition = false; // 循环条件更改为false, 然后本次循环结束后,重新检查condition,发现为false, 程序就不会再执行循环体了
}
i++;
}
std::cin.get();
}
i:不一定非得是整数,其他形式也是可以的,甚至可以是一些自定义的函数,想象力有多大只要符合三部分的功能都可以;
while循环语法结构:
while(循环判定条件)
{
循环体
}
注意事项
do...while循环语法格式:
do
{
循环体
}while(循环条件);
注意事项:
do一遍循环体的代码,然后再判断是否继续循环控制流(control flow)语句基本上可以解释为可以更好的控制循环。
continue
只能用在循环内部
结束本次循环,如果还有下一次循环,执行下一次循环;如果没有,就结束整个循环
示例:只打印4次
#include
#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
#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
计算机运行一个程序 ,或者运行我们的代码,就是把程序和代码推到内存当中,所有的东西都会被存储在内存中,然后cpu根据指令在内存中给出相应的操作。
指针就是管理和操作内存的重要道具
指针本质上还是一个整数数字,它存储的是一个内存地址。
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
定义一个指针的语法格式:
类型* 名字
int* i
示例代码:
#include
#define LOG(x) std::cout << x << std::endl
int main()
{
void* ptr = 0;
/*
定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针
等同于:
void* ptr = NULL;
void* ptr = nullptr; C++11标准引入
*/
int i = 6; // 定义一个整型变量 i, 初始化为 6
ptr = &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
#define LOG(x) std::cout << x << std::endl
int main()
{
int* ptr = 0;
/*
定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针
等同于:
void* ptr = NULL;
void* ptr = nullptr; C++11标准引入
*/
int i = 6; // 定义一个整型变量 i, 初始化为 6
ptr = &i;
/*&取地址符,将变量i的地址给前面定义的指向NULL的无类型指针ptr, 这样是正确的,因为指针
就是数字,本质上没有类型区分*/
LOG(ptr);
std::cin.get();
}

使用整型指针与使用无类型的指针没什么区别!!!
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
指针之所以区分类型是因为编译器需要明白我们定义的这个指针操作的是多大的内存块,指向int就是我们的指针可以操作这4个字节的块,char就可以更改1字节的块,就是内存块的大小问题!但是本质上就是上面的一句话,方便学习理解。

*逆向引用(dereferencing)来取地址中的值(访问地址中的数据)#include
#define LOG(x) std::cout << x << std::endl
int main()
{
int* ptr = 0;
int a = 6;
ptr = &a; // ptr指向a的地址
LOG(ptr); // 地址
LOG(*ptr); // 解引用,实际的值
std::cin.get();
}
分配内存
#include
#define LOG(x) std::cout << x << std::endl
int main()
{
char* buffer = new char[8]; // 分配8个字节的内存,并返回指向这块内存的开始地址的指针buffer
memset(buffer, 1, 8); // 为指定的地址buffer所指向的内存中填充8个字节的1
delete[] buffer;
std::cin.get();
}

可以看到buffer指向的地址中用了8个字节保存了8个1。
*。指针只是一个存储着内存地址的整数!!!
引用和指针几乎一致,引用是基于指针的一种语法结构,使得代码更易读易写。
引用:指对现有变量引用的一种方式,**引用必须引用一个已经存在的变量,引用本身并不是一个新的变量。**并不占用内存。简单说引用就是给起个别名。
示例:
#include
#define LOG(x) std::cout << x << std::endl
int main()
{
int var = 6;
int& ref = var; // 创建一个引用,相当于给变量var起一个别名
LOG(ref); // 直接输出引用ref, 结果是: 6
ref = 5; // 更改引用ref的值,就是更改原来变量var 的值
LOG(ref); // 5
LOG(var); // 5
}
为什么要有引用?
先看一个c++的示例:
#include
#define LOG(x) std::cout << x << std::endl
void AddSelf(int value)
{
/*自加1*/
value++;
}
int main()
{
int var = 5; // 初始化整型变量 var = 5
AddSelf(var); // 执行AddSelf函数, 自加
LOG(var); // 输出var
}
自定义函数的形参int vaule是一个变量,函数功能是实现自加1,按逻辑来说,把变量var作为实参传入函数中,操作的应该的是var这个实参,但是实际最后输出的结果并没有自加1。
为什么会这样?
因为如果一个函数的形参是一个变量,那么当调用函数的时候,函数中会创建一个新的变量value并赋值传入的实参中的数。
上面的自定义函数实际运行中更像是下面的代码:
#include
#define LOG(x) std::cout << x << std::endl
void AddSelf(int value)
{
/*自加1*/
int value = 5; // 实际调用函数的时候会定义形参并赋值为传入的实参
value++;
}
int main()
{
int var = 5; // 初始化整型变量 var = 5
AddSelf(var); // 执行AddSelf函数, 自加
LOG(var); // 输出var
}
如果希望函数实际的更改变量本身的值怎么办?这时候就可以采用引用的方法:
#include
#define LOG(x) std::cout << x << std::endl
void AddSelf(int& value)
{
/*自加1*/
value++;
}
int main()
{
int var = 5; // 初始化整型变量 var = 5
AddSelf(var); // 执行AddSelf函数, 自加
LOG(var); // 输出var
}
道理与前面一致,前面形参为变量时,当调用函数的时候,创建一个变量来存储传入的实参,操作的是这个新的变量;现在如果形参是一个引用的话,当调用函数时,创建一个引用来引用传入的实参,操作的是这个引用,然而引用前面讲过,更改引用会更改原来变量的值,所以理所当然就可以通过把形参设置为引用的方法使函数对变量本身产生作用。
一个引用只能引用一个变量,不能引用了一个变量后更改该引用的变量对象
int main()
{
int var = 5; // 初始化整型变量 var = 5
int var_2 = 6;
int& ref = var;
ref = var_2; // 并不是引用var_2, 而是把var_2的值赋值给ref,也就是赋值给var
LOG(var); // 6
}
引用必须初始化,必须引用一个确定的变量,不能仅仅是定义一个引用
面向对象编程是一种编程思想,不注重解决问题的详细过程,而是设计具有相应属性和方法的对象,通过这些具有不同功能的对象来完成任务。
类是一种将数据和函数组织在一起的方式
C++中的类的示例:
#include
#define LOG(x) std::cout << x << std::endl
class Player // class 关键字 表示这是一个类, Player 类的名称
{
public: // public: 表示冒号后面的内容为公有内容,可以从类的外部进行访问
int x = 0; // 定义类的两个属性, 在类中的变量通常称为 属性
int y = 0; // 表示玩家的位置坐标
void Move(int stride) // 定义一个方法, 在类中的函数通常称为 方法
{
/*该方法实现移动玩家位置*/
x = x + stride; // x不需要额外定义,在类的内部就是该类的属性x
y = 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的成员默认情况是private(私有)的,而struct正好相反;
c++中保留struct是考虑到与c的兼容性,c中是没有class的。简单的方式完成两种代码的转换只需要一个宏定义:
在c++代码种使用
#define class struct
在c代码中使用
#define struct class

这样就把c++中的class变成了struct, 反之亦然。
何时使用class以及什么时候使用struct看自己的编程风格了(毕竟两者的区别就是上面两个而已<‘’ – ‘’>),建议如果需要使用的某个东西仅包含属性(数据),那么就用struct,如果这个东西除了属性还有其他功能的方法那就用class吧。
写一个日志类,用来打印不同的信息:
#include
class Log
{
/*Log类, 根据不同的级别设置来打印不同的信息*/
public:
/*设置三种日志级别*/
const int LogLevelError = 0;
const int LogLevelWarning = 1;
const int LogLevelInfo = 2;
private:
int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Info
public:
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关键字修饰的符号在链接阶段是局部的。只对当前所在的翻译单元可见(只在所在的文件中可见)其他的文件是无法访问被修饰的内容的。main.cpp:
#include
#define LOG(x) std::cout< 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
#define LOG(x) std::cout< 建议:尽量让全局变量或者函数使用static关键字修饰
static修饰的符号只会有一个实例。比如在类的内部定义一个属性使用static修饰,那么当多次实例化该类时,这个属性只有一个:#include
#define LOG(x) std::cout< 一切正常,但是当在类的内部使用static关键字来修饰两个属性时:
#include
#define LOG(x) std::cout< 此时编译代码就会报错:

如果更改e1的初始化方式:
#include
#define LOG(x) std::cout< 此时报错内容为:

无法解析的外部符号,那么我们就把这两个定义一下:
#include
#define LOG(x) std::cout< 编译终于通过了,但是会发现一个很奇怪的现象,两个对象的属性值竟然是一样的,都是:2 3。可是明明对e的两个属性赋值为0和1啊。
这就是开始说到的:类或者结构体内部的static修饰的符号无论实例化几次,这些符号仅存在一个!!!会被最新的值覆盖掉!
拿上面的例子来说:static修饰了两个符号x和y,那么所有类的实例中的这两个属性指向的是同一个共享空间!
所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过Entity::x的方式来引用:
#include
#define LOG(x) std::cout< 注意事项:
基于所有实例的静态内容都共享内容这一特性,可以把需要所有实例都要使用的相同内容设置为静态。
使用静态内容可以不实例化:
#include
#define LOG(x) std::cout< 静态方法不能访问非静态变量。静态的内容没有引用指针,本质上类内的每个非静态方法都会获得当前的类实例作为参数:
void Print(Entity e) // 传入类的实例
{
/*输出两个属性*/
std::cout << e.x << ' ' << e.y << std::endl;
}
但是静态方法是没有这个隐藏的参数的,在实际运行中就类似下面这样:

静态的方法由于没有隐藏参数并不知道实例对象是谁的实例!!
static示例代码:
#include
#define LOG(x) std::cout< 上面的代码循环执行了5次,每次循环的内容为:
1 2 3 4 5 如果去掉关键字static:
#include
#define LOG(x) std::cout< 由于每次循环都重新定义了v,并初始化为0,所以每次输出的结果都是 1,也就是1 1 1 1 1 。
这就是说,当使用static关键字修饰时,被修饰的内容的生命周期为程序的整个运行周期,在程序运行过程中,始终存在在内存当中,其中的值是一直存在的,而没有static关键字的话,就会在每次循环开始时,都会创建一个新的变量,在内存中设定一个新的地址用来存储对应的值。
#include
#define LOG(x) std::cout< 即使向上面的代码,使用了A, B, C,没有指定类型但是一旦出现在枚举中,它们实际的值就是1, 2, 3。
0,依次递增
#include
#define LOG(x) std::cout< enum Example : unsigned char // 枚举类型, 里面包含三个值
{
A, B, C
};
之前的Log类:
#include
class Log
{
/*Log类, 根据不同的级别设置来打印不同的信息*/
public:
/*设置三种日志级别*/
const int LogLevelError = 0;
const int LogLevelWarning = 1;
const int LogLevelInfo = 2;
private:
int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Info
public:
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
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."); // 打印日志中的信息
}
Constructors,一种特殊的函数,在类的每次实例化的时候运行
示例:
#include
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
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();
}
构造函数可以设置参数,设置参数后需要在实例化该类时传入参数,一个类可以有多个构造函数,根据参数不同来划分:
#include
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
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可以使用两个类中的内容(属性和方法)。
std中代码示例:
#include
#include
int main()
{
std::string s; // 默认初始化 空字符串
std::cout << s << std::endl;
std::cout << sizeof(s) << std::endl; // 28字节
}
#include
#include
int main()
{
std::string s; // 默认初始化 空字符串
std::string s1 = "copy"; // 拷贝初始化
std::string s2("directly"); // 直接初始化
std::string s3(5, 'h'); // 直接初始化 s3存储的是 5 个 h
std::cout << s.empty() << std::endl; // 1 empty()方法返回bool值,为真时表示s是空字符串
}
string对象会自动忽略字符串中的空白项,以第一个字符为开始,顺序向后遇到空白结束#include
#include
int main()
{
std::string s; // 默认初始化 空字符串
std::cin >> s; // 输入字符串
std::cout << s << std::endl; // 从顺序遇到的第一个字符开始,顺序向后第一个空白结束
}

采用同样的输入,如果使用两个string对象接收的话,每个对象会存储一个连续的字符串:
#include
#include
int main()
{
std::string s, s1; // 默认初始化 空字符串
std::cin >> s >> s1; // 输入字符串
std::cout << s << s1 << std::endl; // 顺序输出两个字符串
}

字符串的比较
字符串本身有有长度的不同,获取字符串的长度可以简单地使用size(),注意sizeof()是获得对象占用内存空间的大小
#include
#include
using namespace std;
int main()
{
string s("cpp");
cout << s.size() << endl; // 3
cout << sizeof(s) << endl; // 28
}
字符串之间的大小不同,字符串的大小比较:顺序比较对应位置的两个字符,如果字符相同,顺序向后,直到出现不同的字符(包括同一字符的大小写,也属于不同的字符),字符大的字符串就大,与字符串的长度无关
#include
#include
using namespace std;
int main()
{
string s1("abc"); // 长度为3
string s2("de"); // 长度为2
if (s1 > s2) // false
cout << "s1" << endl;
else if (s1 == s2) // false
cout << "s1 == s2" << endl;
else
cout << "s2" << endl; // run s2 大于 s1, 因为 d 大于 a
}
字符串的相加
字符串可以直接使用+运算符相加,效果是左侧的字符串后面拼接右侧的字符串的内容。
#include
#include
using namespace std;
int main()
{
string s1("abc");
string s2("de");
cout << s1 + s2 << endl; // abcde
}
字符串对象可以直接与字符串常量相加,但两个字符串常量是不能直接+的。
#include
#include
using namespace std;
int main()
{
string s1("abc");
cout << s1 + "de" << endl; // abcde
cout << "abc" + "de" << endl; // error
}
取出字符串中的字符
字符串是由一个个字符顺序连接组成的,使用c++11 标准中提出的一种语法: 范围for循环可以遍历字符串中的字符
#include
#include
using namespace std;
int main()
{
std::string s("abcde");
char c;
c = s[0]; // 第一个字符"a"
cout << "c : " << c << endl; // c : a
for (auto a : s) // 定义的变量 : 序列对象
cout << a << endl;
}
虚函数可以让我们在子类中重写方法
有两个类:A,B
B是A的子类,在A中的方法设定为虚(virtual),那么就可以在B类中重写该方法
代码示例:
#include
#include
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_Name
std::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); // entity
Player* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址
printName(p); // entity
}
多态:不同的子类可以有相同的方法,但可以根据各个子类的需求使用该方法来实现不同的效果。
上面代码可以看到,p明明是子类的实例化对象,但是执行GetName()方法却使用的父类中的方法。
这时就需要使用虚函数。虚函数可以让我们在子类中重写父类方法。
虚函数的写法:在定义方法时,使用关键字virtual限制:
virtual std::string GetName() { return "Entity"; }
现在把上面的代码做下面的调整:
virtualoverride(加不加都可以,但为了代码可读性建议加上,代表这是一个从父类继承的方法)结果就可以像我们所期待的那样,各自执行各自的方法:
#include
#include
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_Name
std::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); // entity
Player* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址
printName(p); // Rabbit
}
虚函数的使用虽然方便,但是也会带来一些代价:
纯虚函数允许我们定义一个在基类中没有实现的函数,强制子类完成该函数
接口(Interface):一个只包含未实现的方法,并作为一个模板的类。由于此接口类实际上不包含方法实现,所以无法实例化这个类。
#include
#include
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
#include
#include
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
#include
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
#include
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;
}
示例代码HelloWorld:
#include
int main()
/*打印输出HelloWorld!*/
{
std::cout << "Hello World!" << std::endl;
std::cin.get();
return 0;
}
分解代码:
#include
预处理语句
编译器在收到源文件之后,优先执行预处理语句,预处理语句的执行是在实际编译发生之前。
#:标明在该符号之后的都是预处理语句。
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
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
void Log(const char* message);
int main(void)
/*打印输出HelloWorld*/
{
Log("Hello World!");
std::cin.get();
return 0;
}
Log.cpp:
#include
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)。
示例:
新建一个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
.XMM
include listing.inc
.model flat
INCLUDELIB MSVCRTD
INCLUDELIB OLDNAMES
msvcjmc 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 ; COMDAT
push ebp
mov ebp, esp
pop ebp
ret 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 2
push ebp
mov ebp, esp
mov ecx, OFFSET __CDAE11DE_math@cpp
call @__CheckForDebuggerJustMyCode@4
mov eax, DWORD PTR _a$[ebp]
imul eax, DWORD PTR _b$[ebp]
; Line 5
pop ebp
ret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
_TEXT ENDS
END
可以通过上面的汇编代码看到自定义的函数?Multiply@@YAHHH@Z,但是函数在这里会被装饰成带有随机字符和@符号的形式,这就是函数签名,用来独一无二的标记该函数,链接器(linker)就是通过这个函数签名来找到函数的。
总结来说,编译器就是将源代码文件生成包含机器码和我们定义的常量数据的目标文件object file
预处理阶段编译器检查所有的预处理语句(由#符号标记的语句),常见的预处理指令有:
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 INTEGER int
INTEGER Multiply(int a, int b) // INTEGER由于在预处理中重新定义成了int, 所以和使用int效果一致
{
int result = a * b;
return result;
}
这段代码中的预处理语句就是:把代码中出现的INTEGER字符替换成int 字符,效果和直接使用int是一样的。
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
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
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
void Log(const char* message); // 声明有一个函数Log
int 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
#include"Log.h"
void InitLog()
{
Log("Initialized Log");
}
一个Math.cpp:
#include
#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
void Log(const char* message)
{
std::cout << message << std::endl;
}
void InitLog()
{
Log("Initialized Log");
}
Math.cpp就等同于:
#include
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):给一个存储在内存中的数据一个名字,从而来使用这个数据,且这个数据是可以更改的。
代表在给定的范围内存储一个整型数字。
#include
int main()
{
int variable = 1; // 定义一个整型变量variable, 并初始化为整数2
std::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
int main()
{
int variable = 2147483648; // 定义一个整型变量variable, 并初始化为整数2
std::cout << "The int variable number is : " << variable << std::endl; // 打印输出
std::cin.get();
}
此时并不会如期望的那样输出2147483648,而是:

如果用-2147483648来赋值的话就是正常的,因为并没有超出整型变量的范围。
可以使用关键字unsigned限定变量是无符号位:
#include
int main()
{
unsigned int variable = 2147483648; // 定义一个无符号位的整型变量
std::cout << "The int variable number is : " << variable << std::endl; // 打印输出
std::cin.get();
}
此时刚才的临界点的数值就是可以正确打印的了。
大小:1 bytes
存储内容:字符(character)
#include
int main()
{
char c = 'A'; // 定义一个字符型变量c,并初始化为字符 'A'
std::cout << "The char variable is : " << c << std::endl; // 打印输出
std::cin.get();
}
对于char类型的变量也可以按照ASCII码赋值,编译器会根据ASCII码中数值和字符的对应关系存储对应的字符:
#include
int main()
{
char c = 65; // 定义一个字符型变量c,并初始化为65,对应ASCII码表中的字符A
std::cout << "The char variable is : " << c << std::endl; // 打印输出
std::cin.get();
}
上面两种的输出是一致的:

int类型也是一样:
#include
int main()
{
int c = 'A'; // 定义一个整型变量c,并用字符 'A'赋值,会转换成对应的ASCII码的数值65
std::cout << "The char variable is : " << c << std::endl; // 打印输出
std::cin.get();
}
代码会输出字符A对应的ASCII码整数65。
C++中的数据类型的使用非常灵活,自我感觉完全就是取决于写代码的人来决定!😃
C++程序中重点关注的还是内存,当我使用某个数据类型的变量时,会分配多少的内存!
大小:2 bytes
大小:4 bytes
大小:8 bytes
浮点数(小数)
大小:4 bytes
#include
int main()
{
float variable = 1.2f; // 定义一个浮点型变量并初始化为1.2
std::cout << "The char variable is : " << variable << std::endl; // 打印输出
std::cin.get();
}
C++中有两个表示小数类型的关键字float和double,但是当使用float定义变量时如果不在小数后面加一个字符f(大小写都可以)的话,实际上还是以double类型存储的:

加了f的话才会被认定为float类型:

双精度浮点数
大小:8 bytes
布尔型(boolean):true或者false
大小:1 bytes
实际只有1 bits 就可以表示该类型数据了,只有两个值嘛,但是实际上内存寻址是无法定位到bits的,只能定位带bytes,所以该类型的数据大小是1bytes.
#include
int main()
{
bool variable = false; // 定义一个bool型变量并初始化为false
std::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
int main()
{
bool variable = 6; // 定义一个bool型变量并初始化为6
std::cout << "The char variable is : " << variable << std::endl; // 打印输出
std::cin.get();
}

C++提供了一个查看数据大小的方法:sizeof():
#include
int main()
{
bool variable = 6; // 定义一个bool型变量并初始化为6
std::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
int main()
{
std::cout << "The size of int is : " << sizeof(int) << std::endl; // 打印输出
std::cin.get();
}
输出:

函数就是编写的代码块去执行某个特定的任务
最常用的场景就是把重复的代码写成一个函数,到需要的时候直接调用函数来完成,既节省代码量整体代码的结构也会更加清晰。
函数基本包含:
示例:
#include
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
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
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
void Log(const char* message)
{
std::cout << message << std::endl;
}
int main()
{
Log("Hello World");
}
确实,不过当项目工程庞大的时候,把所有代码写在一个文件里会变得非常难维护。
如果if怎么怎么样就怎么怎么样,否则else怎么怎么样:
#include "Log.h" // Log.h是头文件章节中的内容
#include
int main()
{
int x = 5;
bool compareResult = x == 5; // bool类型变量,赋值为0时结果为false,除此之外全为true,此处x == 5为比较运算,此时为正确的, 值就是1, 也就是true
if (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 >::get (0A4A0B4h)]
}
}
if语句的参数不一定非要是一个确定的变量,只要是可以返回一个结果的内容,都可以,上面的代码就可以直接写成下面的内容:
#include "Log.h"
#include
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
int main()
{
int x = 5;
if (x == 5)
Log("Hello World!");
std::cin.get();
}
if只检查数字!!!!!!
与if对应就是else:
#include "Log.h"
#include
int main()
{
const char* ptr = 0; // 指针ptr指向空(NULL)
if (ptr)
Log(ptr); // 判定条件为false, 跳转至else
else
Log("ptr is null"); // 打印输出相应内容
std::cin.get();
}
即使
if语句写成了两行,但实际上仍然是一句代码,判断一条语句的结束就是看;,一个分号表示该句结束。
除了上述这样给代码两条分支以外,还可以叠加使用if,就是else if:
#include "Log.h"
#include
int main()
{
const char* ptr = "Hello"; // 指针ptr
if (ptr)
Log(ptr); // 判定条件为true, 进入该分支,打印输出
else if (ptr == "Hello") // 尽管为true,但是并不会执行,因为第一个分支的判定true就会跳出分支结构
Log("ptr is Hello");
else
Log("ptr is null");
std::cin.get();
}
需要注意,如果代码已经进入了一个分支,那么其他分支是不会执行的,上面代码中的第二个条件是不会执行的,因为如果ptr == "Hello"是成立的,那么在第一个条件语句判断时就是成立的,就只会执行第一个分支内的代码,后面的就不会执行了。
只有前面的分支判断条件为false时,后面的分支判断才会执行。如果第一个分支的条件成立,那么自然执行第一个分支的内容,后面的分支会直接跳过
如果我们想让中间的分支也执行,可以再重新创建一个分支:
#include "Log.h"
#include
int main()
{
const char* ptr = "Hello"; // 指针ptr
if (ptr)
Log(ptr); // 判定条件为true,该分支结束,继续执行分支结构后面的代码
if (ptr == "Hello") // 进入第二个分支
Log("ptr is Hello"); // 打印输出相应内容
else
Log("ptr is null");
std::cin.get();
}
这样就会打印两条语句。
也就是说**if和else是相对应的,上面代码的else对应的就是第二个if**
另外else if并不是关键字,它只是else和if的一种组合,else相当于它所对应的if的另一条分支,只不过在这一条分支中,首先是一个条件判断而已,所做的事情就是:
#include "Log.h"
#include
int main()
{
const char* ptr = "Hello";
if (ptr)
Log(ptr);
else
{
if (ptr == "Hello")
Log("ptr is Hello");
else
Log("ptr is null");
}
std::cin.get();
}
虽然分支和比较在一个应用中非常重要,但是比较会降低代码的运行效率,能用其他的方法代替的话就尽量代替掉比较操作
循环就是多次执行相同的代码,比如:重复打印"Hello World"五次:
#include
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
#include"Log.h"
int main()
{
// for 循环实现重复打印 5 次 "Hello World"
int i = 0; // 循环开始时i = 0
bool condition = true; // 循环条件condition
for (; condition; ) // for 三个部分必须有, 但可以没有内容, 如果condition也去掉的话, 就没有退出循环的条件判定了, 程序就会进入死循环
{
Log("Hello World"); // 调用函数打印内容
if (!(i < 5)) // 如果 i < 5 不成立
{
condition = false; // 循环条件更改为false, 然后本次循环结束后,重新检查condition,发现为false, 程序就不会再执行循环体了
}
i++;
}
std::cin.get();
}
i:不一定非得是整数,其他形式也是可以的,甚至可以是一些自定义的函数,想象力有多大只要符合三部分的功能都可以;
while循环语法结构:
while(循环判定条件)
{
循环体
}
注意事项
do...while循环语法格式:
do
{
循环体
}while(循环条件);
注意事项:
do一遍循环体的代码,然后再判断是否继续循环控制流(control flow)语句基本上可以解释为可以更好的控制循环。
continue
只能用在循环内部
结束本次循环,如果还有下一次循环,执行下一次循环;如果没有,就结束整个循环
示例:只打印4次
#include
#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
#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
计算机运行一个程序 ,或者运行我们的代码,就是把程序和代码推到内存当中,所有的东西都会被存储在内存中,然后cpu根据指令在内存中给出相应的操作。
指针就是管理和操作内存的重要道具
指针本质上还是一个整数数字,它存储的是一个内存地址。
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
指针只是一个存储着内存地址的整数!!!
定义一个指针的语法格式:
类型* 名字
int* i
示例代码:
#include
#define LOG(x) std::cout << x << std::endl
int main()
{
void* ptr = 0;
/*
定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针
等同于:
void* ptr = NULL;
void* ptr = nullptr; C++11标准引入
*/
int i = 6; // 定义一个整型变量 i, 初始化为 6
ptr = &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
#define LOG(x) std::cout << x << std::endl
int main()
{
int* ptr = 0;
/*
定义一个无类型指针,赋值0, 意思是该指针无效, 是一个指向NULL的空指针
等同于:
void* ptr = NULL;
void* ptr = nullptr; C++11标准引入
*/
int i = 6; // 定义一个整型变量 i, 初始化为 6
ptr = &i;
/*&取地址符,将变量i的地址给前面定义的指向NULL的无类型指针ptr, 这样是正确的,因为指针
就是数字,本质上没有类型区分*/
LOG(ptr);
std::cin.get();
}

使用整型指针与使用无类型的指针没什么区别!!!
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值
指针之所以区分类型是因为编译器需要明白我们定义的这个指针操作的是多大的内存块,指向int就是我们的指针可以操作这4个字节的块,char就可以更改1字节的块,就是内存块的大小问题!但是本质上就是上面的一句话,方便学习理解。

*逆向引用(dereferencing)来取地址中的值(访问地址中的数据)#include
#define LOG(x) std::cout << x << std::endl
int main()
{
int* ptr = 0;
int a = 6;
ptr = &a; // ptr指向a的地址
LOG(ptr); // 地址
LOG(*ptr); // 解引用,实际的值
std::cin.get();
}
分配内存
#include
#define LOG(x) std::cout << x << std::endl
int main()
{
char* buffer = new char[8]; // 分配8个字节的内存,并返回指向这块内存的开始地址的指针buffer
memset(buffer, 1, 8); // 为指定的地址buffer所指向的内存中填充8个字节的1
delete[] buffer;
std::cin.get();
}

可以看到buffer指向的地址中用了8个字节保存了8个1。
*。指针只是一个存储着内存地址的整数!!!
引用和指针几乎一致,引用是基于指针的一种语法结构,使得代码更易读易写。
引用:指对现有变量引用的一种方式,**引用必须引用一个已经存在的变量,引用本身并不是一个新的变量。**并不占用内存。简单说引用就是给起个别名。
示例:
#include
#define LOG(x) std::cout << x << std::endl
int main()
{
int var = 6;
int& ref = var; // 创建一个引用,相当于给变量var起一个别名
LOG(ref); // 直接输出引用ref, 结果是: 6
ref = 5; // 更改引用ref的值,就是更改原来变量var 的值
LOG(ref); // 5
LOG(var); // 5
}
为什么要有引用?
先看一个c++的示例:
#include
#define LOG(x) std::cout << x << std::endl
void AddSelf(int value)
{
/*自加1*/
value++;
}
int main()
{
int var = 5; // 初始化整型变量 var = 5
AddSelf(var); // 执行AddSelf函数, 自加
LOG(var); // 输出var
}
自定义函数的形参int vaule是一个变量,函数功能是实现自加1,按逻辑来说,把变量var作为实参传入函数中,操作的应该的是var这个实参,但是实际最后输出的结果并没有自加1。
为什么会这样?
因为如果一个函数的形参是一个变量,那么当调用函数的时候,函数中会创建一个新的变量value并赋值传入的实参中的数。
上面的自定义函数实际运行中更像是下面的代码:
#include
#define LOG(x) std::cout << x << std::endl
void AddSelf(int value)
{
/*自加1*/
int value = 5; // 实际调用函数的时候会定义形参并赋值为传入的实参
value++;
}
int main()
{
int var = 5; // 初始化整型变量 var = 5
AddSelf(var); // 执行AddSelf函数, 自加
LOG(var); // 输出var
}
如果希望函数实际的更改变量本身的值怎么办?这时候就可以采用引用的方法:
#include
#define LOG(x) std::cout << x << std::endl
void AddSelf(int& value)
{
/*自加1*/
value++;
}
int main()
{
int var = 5; // 初始化整型变量 var = 5
AddSelf(var); // 执行AddSelf函数, 自加
LOG(var); // 输出var
}
道理与前面一致,前面形参为变量时,当调用函数的时候,创建一个变量来存储传入的实参,操作的是这个新的变量;现在如果形参是一个引用的话,当调用函数时,创建一个引用来引用传入的实参,操作的是这个引用,然而引用前面讲过,更改引用会更改原来变量的值,所以理所当然就可以通过把形参设置为引用的方法使函数对变量本身产生作用。
一个引用只能引用一个变量,不能引用了一个变量后更改该引用的变量对象
int main()
{
int var = 5; // 初始化整型变量 var = 5
int var_2 = 6;
int& ref = var;
ref = var_2; // 并不是引用var_2, 而是把var_2的值赋值给ref,也就是赋值给var
LOG(var); // 6
}
引用必须初始化,必须引用一个确定的变量,不能仅仅是定义一个引用
面向对象编程是一种编程思想,不注重解决问题的详细过程,而是设计具有相应属性和方法的对象,通过这些具有不同功能的对象来完成任务。
类是一种将数据和函数组织在一起的方式
C++中的类的示例:
#include
#define LOG(x) std::cout << x << std::endl
class Player // class 关键字 表示这是一个类, Player 类的名称
{
public: // public: 表示冒号后面的内容为公有内容,可以从类的外部进行访问
int x = 0; // 定义类的两个属性, 在类中的变量通常称为 属性
int y = 0; // 表示玩家的位置坐标
void Move(int stride) // 定义一个方法, 在类中的函数通常称为 方法
{
/*该方法实现移动玩家位置*/
x = x + stride; // x不需要额外定义,在类的内部就是该类的属性x
y = 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的成员默认情况是private(私有)的,而struct正好相反;
c++中保留struct是考虑到与c的兼容性,c中是没有class的。简单的方式完成两种代码的转换只需要一个宏定义:
在c++代码种使用
#define class struct
在c代码中使用
#define struct class

这样就把c++中的class变成了struct, 反之亦然。
何时使用class以及什么时候使用struct看自己的编程风格了(毕竟两者的区别就是上面两个而已<‘’ – ‘’>),建议如果需要使用的某个东西仅包含属性(数据),那么就用struct,如果这个东西除了属性还有其他功能的方法那就用class吧。
写一个日志类,用来打印不同的信息:
#include
class Log
{
/*Log类, 根据不同的级别设置来打印不同的信息*/
public:
/*设置三种日志级别*/
const int LogLevelError = 0;
const int LogLevelWarning = 1;
const int LogLevelInfo = 2;
private:
int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Info
public:
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关键字修饰的符号在链接阶段是局部的。只对当前所在的翻译单元可见(只在所在的文件中可见)其他的文件是无法访问被修饰的内容的。main.cpp:
#include
#define LOG(x) std::cout< 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
#define LOG(x) std::cout< 建议:尽量让全局变量或者函数使用static关键字修饰
static修饰的符号只会有一个实例。比如在类的内部定义一个属性使用static修饰,那么当多次实例化该类时,这个属性只有一个:#include
#define LOG(x) std::cout< 一切正常,但是当在类的内部使用static关键字来修饰两个属性时:
#include
#define LOG(x) std::cout< 此时编译代码就会报错:

如果更改e1的初始化方式:
#include
#define LOG(x) std::cout< 此时报错内容为:

无法解析的外部符号,那么我们就把这两个定义一下:
#include
#define LOG(x) std::cout< 编译终于通过了,但是会发现一个很奇怪的现象,两个对象的属性值竟然是一样的,都是:2 3。可是明明对e的两个属性赋值为0和1啊。
这就是开始说到的:类或者结构体内部的static修饰的符号无论实例化几次,这些符号仅存在一个!!!会被最新的值覆盖掉!
拿上面的例子来说:static修饰了两个符号x和y,那么所有类的实例中的这两个属性指向的是同一个共享空间!
所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过Entity::x的方式来引用:
#include
#define LOG(x) std::cout< 注意事项:
基于所有实例的静态内容都共享内容这一特性,可以把需要所有实例都要使用的相同内容设置为静态。
使用静态内容可以不实例化:
#include
#define LOG(x) std::cout< 静态方法不能访问非静态变量。静态的内容没有引用指针,本质上类内的每个非静态方法都会获得当前的类实例作为参数:
void Print(Entity e) // 传入类的实例
{
/*输出两个属性*/
std::cout << e.x << ' ' << e.y << std::endl;
}
但是静态方法是没有这个隐藏的参数的,在实际运行中就类似下面这样:

静态的方法由于没有隐藏参数并不知道实例对象是谁的实例!!
static示例代码:
#include
#define LOG(x) std::cout< 上面的代码循环执行了5次,每次循环的内容为:
1 2 3 4 5 如果去掉关键字static:
#include
#define LOG(x) std::cout< 由于每次循环都重新定义了v,并初始化为0,所以每次输出的结果都是 1,也就是1 1 1 1 1 。
这就是说,当使用static关键字修饰时,被修饰的内容的生命周期为程序的整个运行周期,在程序运行过程中,始终存在在内存当中,其中的值是一直存在的,而没有static关键字的话,就会在每次循环开始时,都会创建一个新的变量,在内存中设定一个新的地址用来存储对应的值。
#include
#define LOG(x) std::cout< 即使向上面的代码,使用了A, B, C,没有指定类型但是一旦出现在枚举中,它们实际的值就是1, 2, 3。
0,依次递增
#include
#define LOG(x) std::cout< enum Example : unsigned char // 枚举类型, 里面包含三个值
{
A, B, C
};
之前的Log类:
#include
class Log
{
/*Log类, 根据不同的级别设置来打印不同的信息*/
public:
/*设置三种日志级别*/
const int LogLevelError = 0;
const int LogLevelWarning = 1;
const int LogLevelInfo = 2;
private:
int m_LogLevel = LogLevelInfo; // 私有属性, 默认日志级别为Info
public:
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
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."); // 打印日志中的信息
}
Constructors,一种特殊的函数,在类的每次实例化的时候运行
示例:
#include
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
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();
}
构造函数可以设置参数,设置参数后需要在实例化该类时传入参数,一个类可以有多个构造函数,根据参数不同来划分:
#include
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
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可以使用两个类中的内容(属性和方法)。
std中代码示例:
#include
#include
int main()
{
std::string s; // 默认初始化 空字符串
std::cout << s << std::endl;
std::cout << sizeof(s) << std::endl; // 28字节
}
#include
#include
int main()
{
std::string s; // 默认初始化 空字符串
std::string s1 = "copy"; // 拷贝初始化
std::string s2("directly"); // 直接初始化
std::string s3(5, 'h'); // 直接初始化 s3存储的是 5 个 h
std::cout << s.empty() << std::endl; // 1 empty()方法返回bool值,为真时表示s是空字符串
}
string对象会自动忽略字符串中的空白项,以第一个字符为开始,顺序向后遇到空白结束#include
#include
int main()
{
std::string s; // 默认初始化 空字符串
std::cin >> s; // 输入字符串
std::cout << s << std::endl; // 从顺序遇到的第一个字符开始,顺序向后第一个空白结束
}

采用同样的输入,如果使用两个string对象接收的话,每个对象会存储一个连续的字符串:
#include
#include
int main()
{
std::string s, s1; // 默认初始化 空字符串
std::cin >> s >> s1; // 输入字符串
std::cout << s << s1 << std::endl; // 顺序输出两个字符串
}

字符串的比较
字符串本身有有长度的不同,获取字符串的长度可以简单地使用size(),注意sizeof()是获得对象占用内存空间的大小
#include
#include
using namespace std;
int main()
{
string s("cpp");
cout << s.size() << endl; // 3
cout << sizeof(s) << endl; // 28
}
字符串之间的大小不同,字符串的大小比较:顺序比较对应位置的两个字符,如果字符相同,顺序向后,直到出现不同的字符(包括同一字符的大小写,也属于不同的字符),字符大的字符串就大,与字符串的长度无关
#include
#include
using namespace std;
int main()
{
string s1("abc"); // 长度为3
string s2("de"); // 长度为2
if (s1 > s2) // false
cout << "s1" << endl;
else if (s1 == s2) // false
cout << "s1 == s2" << endl;
else
cout << "s2" << endl; // run s2 大于 s1, 因为 d 大于 a
}
字符串的相加
字符串可以直接使用+运算符相加,效果是左侧的字符串后面拼接右侧的字符串的内容。
#include
#include
using namespace std;
int main()
{
string s1("abc");
string s2("de");
cout << s1 + s2 << endl; // abcde
}
字符串对象可以直接与字符串常量相加,但两个字符串常量是不能直接+的。
#include
#include
using namespace std;
int main()
{
string s1("abc");
cout << s1 + "de" << endl; // abcde
cout << "abc" + "de" << endl; // error
}
取出字符串中的字符
字符串是由一个个字符顺序连接组成的,使用c++11 标准中提出的一种语法: 范围for循环可以遍历字符串中的字符
#include
#include
using namespace std;
int main()
{
std::string s("abcde");
char c;
c = s[0]; // 第一个字符"a"
cout << "c : " << c << endl; // c : a
for (auto a : s) // 定义的变量 : 序列对象
cout << a << endl;
}
虚函数可以让我们在子类中重写方法
有两个类:A,B
B是A的子类,在A中的方法设定为虚(virtual),那么就可以在B类中重写该方法
代码示例:
#include
#include
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_Name
std::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); // entity
Player* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址
printName(p); // entity
}
多态:不同的子类可以有相同的方法,但可以根据各个子类的需求使用该方法来实现不同的效果。
上面代码可以看到,p明明是子类的实例化对象,但是执行GetName()方法却使用的父类中的方法。
这时就需要使用虚函数。虚函数可以让我们在子类中重写父类方法。
虚函数的写法:在定义方法时,使用关键字virtual限制:
virtual std::string GetName() { return "Entity"; }
现在把上面的代码做下面的调整:
virtualoverride(加不加都可以,但为了代码可读性建议加上,代表这是一个从父类继承的方法)结果就可以像我们所期待的那样,各自执行各自的方法:
#include
#include
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_Name
std::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); // entity
Player* p = new Player("Rabbit"); // 划分一个存储Player对象的内存空间并通过构造函数初始化该对象, 使用指针p指向该内存地址
printName(p); // Rabbit
}
虚函数的使用虽然方便,但是也会带来一些代价:
纯虚函数允许我们定义一个在基类中没有实现的函数,强制子类完成该函数
接口(Interface):一个只包含未实现的方法,并作为一个模板的类。由于此接口类实际上不包含方法实现,所以无法实例化这个类。
#include
#include
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
#include
#include
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
#include
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
#include
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;
}