C++基础知识记录

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.cppLog.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.objLog.obj文件都很大,尽管他们只是打印输出。这是因为这两个文件中都includeiostream,在这个头文件中包含了很多内容,所以编译时先执行预处理,此时也会把预处理中的文件一起编译进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"

可以看到当判定条件成立时,编译器会预处理#ifendif所包含的内容;判定条件不成立时,编译器会直接忽略中间的内容。

链接器

链接(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函数,虽然是一样的内容,但是链接器会不知道到底该链接哪一个,这种情况就是上面说的有了重名的函数,链接器无法辨别链接哪一个。

解决方法:

  1. 在定义Log函数时使用static限定

    static void Log(const char* message)
    {std::cout << message << std::endl;
    }
    

    这样即使被include到了多个文件,那么在每个文件中所复制过去的函数都是被限制在本文件中的,就不存在多个文件中重名的函数的问题了,即使重名,其工作范围也只是函数所在文件了。

  2. 使用关键字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. 1 byte8 bits --> 4 bytes32 bits

    2. C++中int类型默认为带符号位,32 bits中有一位是要表示符号(+/-)的。剩下31 bits的位置中,每一个位置有2种取值:0或1。

    3. int类型所能存储的最大数值为: 2 31 − 1 = 2 , 147 , 483 , 647 2 ^ {31} - 1 = 2,147,483,647 2311=2,147,483,647,最小为 − 2 , 147 , 483 , 648 -2,147,483,648 2,147,483,648,正数减1是因为还有整数0的存在。

    4. 如果超出这个范围,不会报错,但所存储的值就不是正确的了:

    #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++中有两个表示小数类型的关键字floatdouble,但是当使用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代表false1代表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();
}

这样就会打印两条语句。

也就是说**ifelse是相对应的,上面代码的else对应的就是第二个if**

另外else if并不是关键字,它只是elseif的一种组合,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循环中()的部分分为三个内容:

      1. 循环开始时的标记
      2. 循环终止条件
      3. 每次循环执行结束后对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_Variablestatic修饰外,还有另外的解决方法:在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修饰了两个符号xy,那么所有类的实例中的这两个属性指向的是同一个共享空间!

所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过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次,每次循环的内容为:

  1. 定义一个静态局部变量v,并初始化为整型数字0;
  2. v自加1;
  3. 打印输出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"; }

现在把上面的代码做下面的调整:

  1. 父类中的方法前加一个virtual
  2. 子类中的方法在类型后加一个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.cppLog.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.objLog.obj文件都很大,尽管他们只是打印输出。这是因为这两个文件中都includeiostream,在这个头文件中包含了很多内容,所以编译时先执行预处理,此时也会把预处理中的文件一起编译进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"

可以看到当判定条件成立时,编译器会预处理#ifendif所包含的内容;判定条件不成立时,编译器会直接忽略中间的内容。

链接器

链接(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函数,虽然是一样的内容,但是链接器会不知道到底该链接哪一个,这种情况就是上面说的有了重名的函数,链接器无法辨别链接哪一个。

解决方法:

  1. 在定义Log函数时使用static限定

    static void Log(const char* message)
    {std::cout << message << std::endl;
    }
    

    这样即使被include到了多个文件,那么在每个文件中所复制过去的函数都是被限制在本文件中的,就不存在多个文件中重名的函数的问题了,即使重名,其工作范围也只是函数所在文件了。

  2. 使用关键字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. 1 byte8 bits --> 4 bytes32 bits

    2. C++中int类型默认为带符号位,32 bits中有一位是要表示符号(+/-)的。剩下31 bits的位置中,每一个位置有2种取值:0或1。

    3. int类型所能存储的最大数值为: 2 31 − 1 = 2 , 147 , 483 , 647 2 ^ {31} - 1 = 2,147,483,647 2311=2,147,483,647,最小为 − 2 , 147 , 483 , 648 -2,147,483,648 2,147,483,648,正数减1是因为还有整数0的存在。

    4. 如果超出这个范围,不会报错,但所存储的值就不是正确的了:

    #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++中有两个表示小数类型的关键字floatdouble,但是当使用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代表false1代表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();
}

这样就会打印两条语句。

也就是说**ifelse是相对应的,上面代码的else对应的就是第二个if**

另外else if并不是关键字,它只是elseif的一种组合,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循环中()的部分分为三个内容:

      1. 循环开始时的标记
      2. 循环终止条件
      3. 每次循环执行结束后对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_Variablestatic修饰外,还有另外的解决方法:在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修饰了两个符号xy,那么所有类的实例中的这两个属性指向的是同一个共享空间!

所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过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次,每次循环的内容为:

  1. 定义一个静态局部变量v,并初始化为整型数字0;
  2. v自加1;
  3. 打印输出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"; }

现在把上面的代码做下面的调整:

  1. 父类中的方法前加一个virtual
  2. 子类中的方法在类型后加一个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;  
      }
      

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/143398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年亚太杯数学建模思路 - 案例:ID3-决策树分类算法

文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法&#xff0c;就是频繁模…

运营商大数据,金融贷款精准营销赢得客户

运营商大数据精准营销赢得客户推广 在大数据的新形势下&#xff0c;它推动了经济和金融的发展趋势。其中&#xff0c;大数据获取客户是企业营销和推广的一个阶段&#xff0c;是新一轮的转型发展。通过大数据准确获取客户是一个基本概念。 大数据根据您指定的物理模型选择客户&…

【ArcGIS Pro二次开发】(76):面积平差工具

之前做过一个【三调土地利用现状分类面积汇总】的工具&#xff0c;在流程中使用了面积平差的方法。 考虑了在其它场合可能也需要进行面积平差&#xff0c;因此单独提取出来作为一个工具。 平差实现的方法如下图&#xff1a; 主要的计算过程如上图所示&#xff0c;算出总面积差…

【luckfox】3、计算重量差

前言 本章结合之前的hx711驱动&#xff0c;实现读取质量&#xff0c;记录时间及剩余质量并存入csv文件&#xff0c;计算质量差并总计。 代码 luckfox-pico\project\app\test_app\hx711\hx711_app_addtime.c #include <stdio.h> #include <stdlib.h> #include &…

MySQL数据库约束

目录 数据库约束 1.NULL约束 2.UNIQUE&#xff1a;唯一约束 3.DEFAULT&#xff1a;默认值约束 4.PRIMARY KEY&#xff1a;主键约束 5.FOREIGN KEY&#xff1a;外键约束 数据库约束 以下为本篇文章会介绍的约束 (1)NOT NULL - 指示某列不能存储 NULL 值。 (2)UNIQUE - …

带头双向循环链表

目录 一、结构定义 二、结点创建 三、头结点初始化 四、链表打印 五、尾插 六、头插 七、尾删 八、头删 九、查找&#xff08;返回结点&#xff09; 十、任意位置插入 十一、任意位置删除 十二、利用LTInsert写尾插函数 十三、利用LTInsert写头插函数 十四、利用…

pipeline + node +jenkins+kubernetes部署yarn前端项目

1、编写Dockerfile文件 # Set the base image FROM node:16.10.0# WORKDIR /usr/src/app/ WORKDIR /home/option# Copy files COPY ./ /home/option/# Build arguments LABEL branch${BRANCH} LABEL commit${COMMIT} LABEL date${BUILD_DATE} ARG ENV# Set ENV variables ENV …

视频封装格式

FLV&#xff08;Flash Video&#xff09; FLV封装格式 Tag Data分为Audio&#xff0c;Video&#xff0c;Script三种 TS&#xff08;Transport Stream&#xff09;传输流 TS文件分为三层&#xff0c;&#xff08;倒叙更好理解&#xff09; TS层&#xff1a;在PES层基础上加入…

Leetcode——岛屿的最大面积

1. 题目链接&#xff1a;695. 岛屿的最大面积 2. 题目描述&#xff1a; 给你一个大小为 m x n 的二进制矩阵 grid 。 岛屿 是由一些相邻的 1 (代表土地) 构成的组合&#xff0c;这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都…

Moto edge s pro手机 WIFI和蓝牙连接不上 解决方法分享

2021年12月入手一台Moto Edge S Pro 12256版&#xff0c;看着性价比很高&#xff0c;越用越垃圾。屏幕显示没有vivo亮丽/APP图标很丑/屏幕上一点点水就失灵/拍照片边缘是模糊的/系统几乎不更新。 以上都可以忍受&#xff0c;但是&#xff1a; 用一年不到&#xff0c;蓝牙不能…

前端 vue 面试题 (一)

文章目录 v-if,v-show差别v-for和v-if虚拟dom解决什么问题vue的data为什么返回函数不返回对象比较vue&#xff0c;reactvue双向绑定原理vue虚拟dom 的diff算法vue 虚拟dom的diff算法的时间复杂度vue2与vue3的区别vue数据缓存&#xff0c;避免重复计算单页应用怎么跨页面传参vue…

Java 设计模式——中介者模式

目录 1.概述2.结构3.案例实现3.1.抽象中介类3.2.抽象同事类3.3.具体同事类3.4.具体中介类3.5.测试 4.优缺点5.使用场景 1.概述 &#xff08;1&#xff09;一般来说&#xff0c;同事类之间的关系是比较复杂的&#xff0c;多个同事类之间互相关联时&#xff0c;他们之间的关系会…

Python爬取股票交易数据代码示例及可视化展示。

文章目录 前言一、开发环境二、第三方模块三、爬虫案例步骤四、爬虫程序全部代码1.分析网页2.导入模块3.请求数据4.解析数据5.翻页6.保存数据 五、实现效果六、数据可视化全部代码1.导入数据2.读取数据3.可视化图表4.效果展示关于Python技术储备一、Python所有方向的学习路线二…

Windows本地配置带GPU的Pytorch环境

首先需要安装并配置好Anaconda环境&#xff0c;安装教程教程随便找一个就好。 第一步&#xff1a;安装好之后创建conda虚拟环境&#xff1a; conda create -n your_env_name pythonx.x 第二步&#xff1a;安装需要版本的pytorch&#xff1a;pytorch下载链接 cu100/torch-1.2…

VUE基础的一些总结

首先推荐观看VUE官方文档 目录 创建一个 Vue 应用 要创建一个 Vue 应用&#xff0c;你需要按照以下步骤操作&#xff1a; 步骤 1&#xff1a;安装 Node.js 和 npm 确保你的计算机上已经安装了 Node.js。你可以在 Node.js 官网 上下载并安装它。安装完成后&#xff0c;npm&…

小黑完成了最后一节健身课,顺利完成了跳绳比赛,乘飞机到达南京准备第二天领物资和南京城内闲逛的leetcode之旅:215. 数组中的第K个最大元素

小黑代码 class Solution:def findKthLargest(self, nums: List[int], k: int) -> int:# 数组长度n len(nums)nums list(map(lambda x:-x, nums))q []for i in range(n):heapq.heappush(q, nums[i])# 出堆target -1for i in range(k):target heapq.heappop(q)return -…

Java Web——TomcatWeb服务器

目录 1. 服务器概述 1.1. 服务器硬件 1.2. 服务器软件 2. Web服务器 2.1. Tomcat服务器 2.2. 简单的Web服务器使用 1. 服务器概述 服务器指的是网络环境下为客户机提供某种服务的专用计算机&#xff0c;服务器安装有网络操作系统和各种服务器的应用系统服务器的具有高速…

《向量数据库指南》——2023云栖大会现场,向量数据库Milvus Cloud成关注焦点

近期,广受关注的2023 云栖大会正式收官,来自全球各地的开发者集聚一堂,共同探索 AI 时代的更多可能性。 云栖大会是由阿里巴巴集团主办的科技盛宴,是中国最早的开发者创新展示平台。据悉,今年云栖大会的主题为“计算,为了无法计算的价值”,共吸引了全球 44 个国家和地区…

Qt DragDrop拖动与放置

本文章从属于 Qt实验室-CSDN博客系列 拖放操作包括两个动作&#xff1a;拖动(drag)和放下(drop或称为放置)。 拖动允许 对于要拖出的窗口或控件&#xff0c;要setDragEnabled(true) 对于要拖入的窗口或控件&#xff0c;要setAcceptDrops(true) 下面以一个具体的用例进行说…

Neo4j数据库介绍及简单使用

图数据库介绍 图数据库是一种专门设计用于存储和管理图形数据的数据库类型。在图数据库中&#xff0c;数据以图的形式表示&#xff0c;其中节点表示实体&#xff0c;边表示实体之间的关系。这种表示方式非常适合处理具有复杂关系的数据&#xff0c;如社交网络、推荐系统、网络…