• C++基础知识记录


    github仓库不定期更新: https://github.com/han-0111/CppLearning


    github仓库不定期更新: https://github.com/han-0111/CppLearning

    C++如何工作

    示例代码HelloWorld

    #include
    
    int main()
    /*打印输出HelloWorld!*/
    {
    	std::cout << "Hello World!" << std::endl;
    	std::cin.get();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    分解代码:

    #include 
    
    • 1
    • 预处理语句

      编译器在收到源文件之后,优先执行预处理语句,预处理语句的执行是在实际编译发生之前

      #:标明在该符号之后的都是预处理语句。

      include:去寻找一个文件,寻找的文件就是后面<>括起来的文件,在这个示例代码中要找的文件就是iostream,然后将该文件的内容拷贝到当前文件中,这种文件通常被称为头文件

      iostream头文件为输入输出流,使用cout输出以及使用cin输入都是该文件所提供的方法,有先预处理该头文件才能正常使用对应的输入输出方法。

    int main()
    
    • 1
    • int

      表示该函数的返回值是一个整型值,每个函数都有自己的返回值,函数就相当于数学中的函数,有输入也有输出,输入就是该函数的参数,输出就是函数的返回值

    • main函数

      每一个C++程序都必须有且只有一个main函数,该函数是程序的入口,当开始运行程序时,计算机会从main函数开始,由上至下一行行顺序执行代码。

      main后面的括号就是需要传入的参数,但打印输出不需要出入任何参数,所以什么都不写,当没有设置任何参数,默认括号内为void

      main函数可以不给返回值,也就是可以去掉return 0;这一行。虽然去掉了但是程序会默认返回了 0 0 0

    {
        std::cout << "Hello World!" << std::endl;
        std::cin.get();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • {}:括起来的内容为函数体,代码中要求所有的括号和大括号都严格对应,代表着一块儿代码的整体。

    • 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    自定义函数先定义后使用,使用时直接使用函数名加参数。

    上述代码都在一个.cpp文件中,但是为了方便代码项目管理,一般将不同功能的函数或者类放在不同的位置,因此,也可以按照如下的格式:

    HelloWorld.cpp

    #include
    
    void Log(const char* message);
    
    int main(void)
    /*打印输出HelloWorld*/
    {
    	Log("Hello World!");
    	std::cin.get();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Log.cpp

    #include
    
    void Log(const char* message)
    {
    	/*使用该函数进行输出操作*/
    	std::cout << message << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后仅编译这个文件,会在文件夹中发现出现了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
    	.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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    可以通过上面的汇编代码看到自定义的函数?Multiply@@YAHHH@Z,但是函数在这里会被装饰成带有随机字符和@符号的形式,这就是函数签名用来独一无二的标记该函数,链接器(linker)就是通过这个函数签名来找到函数的。

    总结来说,编译器就是将源代码文件生成包含机器码和我们定义的常量数据的目标文件object file

    预处理(Preprocessing)

    预处理阶段编译器检查所有的预处理语句(由#符号标记的语句),常见的预处理指令有:

    • include
    • define
    • if
    include

    include:去寻找一个文件,然后将该文件的内容拷贝到当前文件中,这种文件通常被称为头文件

    示例:

    int Multiply(int a, int b)
    {
    	int result = a * b;
    	return result  // 明显这里少一个分号
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此时编译提醒缺少;

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    写一个头文件semicolon.h

    ;
    
    • 1

    这个头文件里只有一个;

    既然include是把文件拷贝过来,那么现在这个文件里就只有一个;,乘法函数里缺少一个;,为了验证include是拷贝内容,可以在少分号的位置使用#include "semicolon.h"试一下:

    int Multiply(int a, int b)
    {
    	int result = a * b;
    	return result
    #include "semicolon.h"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时编译就可以完全通过了。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    那么include确实是复制文件的内容到当前位置。

    define

    define:后面跟两个参数,使用第二个参数替换第一个参数。简单说就是把第一个参数的实际含义替换成后面的参数

    示例:

    #define INTEGER int
    
    INTEGER Multiply(int a, int b)  // INTEGER由于在预处理中重新定义成了int, 所以和使用int效果一致
    {
    	int result = a * b;
    	return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这段代码中的预处理语句就是:把代码中出现的INTEGER字符替换成int 字符,效果和直接使用int是一样的。

    if/endif

    if预处理语句可以依据特定条件包含或者剔除代码。

    示例:

    #if 0
    int Multiply(int a, int b)
    {
    	int result = a * b;
    	return result;
    }
    #endif // 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时明显判定条件为false,所以编译器是不会编译中间的代码的

    预处理结果如下:

    #line 1 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"
    
    
    
    
    
    
    #line 8 "D:\\Cpp\\HelloWorld\\HelloWorld\\math.cpp"
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当把判定条件改成true(1)时:

    #if 1
    int Multiply(int a, int b)
    {
    	int result = a * b;
    	return result;
    }
    #endif // 1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时的预处理结果如下:

    #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"
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    链接器

    链接(Linking)的主要工作就是找到每个符号和函数的位置,并把它们链接在一起。

    链接器的功能:

    • 在单个代码文件中,链接器用来找到函数的位置,包括主函数的位置。
    • 在包含多个代码文件的项目中,链接器用来将这些文件链接到一个程序。

    示例:

    /*没有main函数*/
    #include
    
    void Log(const char* message)
    {
    	std::cout << message << std::endl;
    
    }
    int Multiply(int a, int b)
    {
    	Log("Multiply");
    	return a * b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如果此时仅编译代码,是没有问题的:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    但是如果进行生成(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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    **比较特殊的一点:**在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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    虽然在主函数中没有调用Multiply函数,自然就没有调用Log,可是仍然会引发链接错误!!!!!!!!!

    因为链接器会这样认为:这个Multiply函数在这个文件中没有被调用,ok,可是其他文件仍然有可能会使用,那我还是要链接它的,等等,没有Log啊,给这小子报个错误,快去改!

    visual studio 2022貌似修改了这个错误,测试了下并不会报错

    怎么改呢?

    在函数类型前加一个关键字static,表明这个函数只在这个translation unit里面使用就可以了。

    相关注意事项:

    • 声明的函数的类型,参数类型和数量必须和实际定义的函数保持一致
    • 避免函数名重复,如果两个函数名字一样,链接器会不知道链接哪一个。
    • 同一个文件中不要出现已经声明了一个函数,又定义了一个同名的函数,即使这个函数功能与其他文件中的函数功能不一致。同样会引发链接错误。

    一种比较复杂的情况

    现在有一个头文件Log.h

    void Log(const char* message)
    {
    	std::cout << message << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4

    一个Log.cpp

    #include
    #include"Log.h"
    
    void InitLog()
    {
    	Log("Initialized Log");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    一个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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这种情况是会引发链接错误的:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    因为#include预处理的作用就是把包含的文件内容拷贝过来,所以此时Log.cpp就等同于:

    #include
    void Log(const char* message)
    {
    	std::cout << message << std::endl;
    }
    
    void InitLog()
    {
    	Log("Initialized Log");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在运行main函数时,调用Multiply函数,函数中又调用Log函数,但是此时就相当于在Math.cpp有一个Log函数,在Log.cpp也有一个Log函数,虽然是一样的内容,但是链接器会不知道到底该链接哪一个,这种情况就是上面说的有了重名的函数,链接器无法辨别链接哪一个。

    解决方法:

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

      static void Log(const char* message)
      {
      	std::cout << message << std::endl;
      }
      
      • 1
      • 2
      • 3
      • 4

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

    2. 使用关键字inline,这样在调用函数时,只复制函数内部的语句。

      inline void Log(const char* message)
      {
      	std::cout << message << std::endl;
      }
      
      • 1
      • 2
      • 3
      • 4

    变量

    编程的本质就是在操作数据,数据的读写,增删改查等等。

    变量(variable):给一个存储在内存中的数据一个名字,从而来使用这个数据,且这个数据是可以更改的。

    • 创建变量时,会被存储在内存中的两个地方之一:

    变量类型

    int

    代表在给定的范围内存储一个整型数字。

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    传统整型大小是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
      
      int main()
      {
      	int variable = 2147483648;  // 定义一个整型变量variable, 并初始化为整数2
      
      	std::cout << "The int variable number is : " << variable << std::endl;  // 打印输出
      	std::cin.get();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      此时并不会如期望的那样输出2147483648,而是:

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      如果用-2147483648来赋值的话就是正常的,因为并没有超出整型变量的范围。

    可以使用关键字unsigned限定变量是无符号位

    #include
    
    int main()
    {
    	unsigned int variable = 2147483648;  // 定义一个无符号位的整型变量
    
    	std::cout << "The int variable number is : " << variable << std::endl;  // 打印输出
    	std::cin.get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    此时刚才的临界点的数值就是可以正确打印的了。

    char

    大小:1 bytes

    存储内容:字符(character)

    #include
    
    int main()
    {
    	char c = 'A';  // 定义一个字符型变量c,并初始化为字符 'A'
    
    	std::cout << "The char variable is : " << c << std::endl;  // 打印输出
    	std::cin.get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    对于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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面两种的输出是一致的:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    int类型也是一样:

    #include
    
    int main()
    {
    	int c = 'A';  // 定义一个整型变量c,并用字符 'A'赋值,会转换成对应的ASCII码的数值65
    
    	std::cout << "The char variable is : " << c << std::endl;  // 打印输出
    	std::cin.get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    代码会输出字符A对应的ASCII码整数65。

    C++中的数据类型的使用非常灵活,自我感觉完全就是取决于写代码的人来决定!😃

    C++程序中重点关注的还是内存,当我使用某个数据类型的变量时,会分配多少的内存!

    short

    大小:2 bytes

    long

    大小:4 bytes

    long long

    大小:8 bytes

    float

    浮点数(小数)

    大小:4 bytes

    #include
    
    int main()
    {
    	float variable = 1.2f;  // 定义一个浮点型变量并初始化为1.2
    
    	std::cout << "The char variable is : " << variable << std::endl;  // 打印输出
    	std::cin.get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    C++中有两个表示小数类型的关键字floatdouble,但是当使用float定义变量时如果不在小数后面加一个字符f(大小写都可以)的话,实际上还是以double类型存储的:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    double

    双精度浮点数

    大小:8 bytes

    bool

    布尔型(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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实际运行结果并不是输出字符:false,而是数字 0

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    因为计算机只处理数字,0代表false1代表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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    如何查看数据大小

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    可以看到该变量的大小为1 bytes

    sizeof()也可以查看类的大小:

    #include
    
    int main()
    {
    	std::cout << "The size of int is : " << sizeof(int) << std::endl;  // 打印输出
    	std::cin.get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    输出:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    函数

    函数就是编写的代码块去执行某个特定的任务

    最常用的场景就是把重复的代码写成一个函数,到需要的时候直接调用函数来完成,既节省代码量整体代码的结构也会更加清晰。

    函数基本包含:

    • 函数的类型
    • 函数名
    • 参数
    • 返回值

    示例:

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    调用函数非常简单,直接用函数名称就可以了,需要注意的是函数的返回值需要对应的变量来存储。

    函数并不是越多越好,阳极必衰,实际上函数过多反而会显得代码十分混乱,很难维护,而且代码执行会更慢!!!每次调用函数时,编译器会生成一个调用指令,这意味着,在一个运行的程序中,为了调用一个函数,需要为这个函数创建一整个栈框架(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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上面这段代码中就声明了一个函数Log,但是并没有函数的实体,真正的函数体在项目中的其他文件中,但是如果我有多个翻译单元,也就是多个.cpp,它们都使用了这个函数,岂不是每个文件都需要这样一句声明。这也太蠢了吧!

    这就是头文件的作用:声明函数!

    头文件的预处理指令#include的作用不就是复制粘贴文件内容嘛,那么基本上就是这样:

    函数文件:Log.cpp

    #include
    
    void Log(const char* message)
    {
    	std::cout << message << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    头文件Log.h

    #pragma once  // 头文件保护符,防止同一个翻译单元include该文件两次
    
    void Log(const char* message);
    
    • 1
    • 2
    • 3

    主函数文件main.cpp:

    #include "Log.h"
    
    int main()
    {
    	Log("Hello World");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在函数内容比较简单,所以看起来好像很复杂啊,毕竟上面的这三个文件加起来做的事情和下面一个文件是一致的:

    #include
    
    void Log(const char* message)
    {
    	std::cout << message << std::endl;
    }
    
    int main()
    {
    	Log("Hello World");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    确实,不过当项目工程庞大的时候,把所有代码写在一个文件里会变得非常难维护。

    条件语句

    如果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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    重要但是尽量少用,会让代码执行效率变低

    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)]  
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    if语句的参数不一定非要是一个确定的变量,只要是可以返回一个结果的内容,都可以,上面的代码就可以直接写成下面的内容:

    #include "Log.h"  
    #include
    
    int main()
    {
    	int x = 5;
    	if (x == 5) 
        {
    		Log("Hello World!");
    		std::cin.get();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    因为==运算符会有两种结果: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();
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    即使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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    需要注意,如果代码已经进入了一个分支,那么其他分支是不会执行的,上面代码中的第二个条件是不会执行的,因为如果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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这样就会打印两条语句。

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

    另外else if并不是关键字,它只是elseif的一种组合,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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    虽然分支和比较在一个应用中非常重要,但是比较会降低代码的运行效率,能用其他的方法代替的话就尽量代替掉比较操作

    循环语句

    循环就是多次执行相同的代码,比如:重复打印"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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    for循环

    • 语法格式:

      for (控制变量;循环条件;控制变量更新)
      {
          循环执行的代码
      }
      
      • 1
      • 2
      • 3
      • 4
      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;
      }
      
      • 1
      • 2
      • 3
      • 4
    • 注意事项

      • for循环中()的部分分为三个内容:

        1. 循环开始时的标记
        2. 循环终止条件
        3. 每次循环执行结束后对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();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
      • i:不一定非得是整数,其他形式也是可以的,甚至可以是一些自定义的函数,想象力有多大只要符合三部分的功能都可以

    while循环

    • 语法结构:

      while(循环判定条件)
      {
          循环体
      }
      
      • 1
      • 2
      • 3
      • 4
    • 注意事项

      • 循环判定条件为一个bool值
      • 通常会在循环体内进行判定条件的更新,但是也不是硬性规定
      • 判定条件给一个永真值就会进行死循环

    do...while循环

    • 语法格式:

      do
      {
          循环体
      }while(循环条件);
      
      • 1
      • 2
      • 3
      • 4
    • 注意事项:

      • 该语句无论循环条件是否成立都会先执行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();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
    • 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();
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
    • return

      • 直接完整地退出函数

    指针(Pointer)

    指针的本质

    计算机运行一个程序 ,或者运行我们的代码,就是把程序和代码推到内存当中,所有的东西都会被存储在内存中,然后cpu根据指令在内存中给出相应的操作。

    指针就是管理和操作内存的重要道具

    指针本质上还是一个整数数字,它存储的是一个内存地址。

    指针只是一个存储着内存地址的整数!!!

    指针只是一个存储着内存地址的整数!!!

    指针只是一个存储着内存地址的整数!!!

    • 定义一个指针的语法格式:

      类型* 名字
      int* i
      
      • 1
      • 2

    示例代码:

    #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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    如果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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    使用整型指针与使用无类型的指针没什么区别!!!

    最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值

    最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值

    最重要的还是:指针和变量本质上都是数字,一个表示地址,一个表示真实的值

    指针之所以区分类型是因为编译器需要明白我们定义的这个指针操作的是多大的内存块,指向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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 分配内存

      #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();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    可以看到buffer指向的地址中用了8个字节保存了8个1。

    • 指针也是变量,也就说指针同样可以指向指针,只需要在定义时使用两个*

    指针只是一个存储着内存地址的整数!!!

    引用(Reference)

    引用和指针几乎一致,引用是基于指针的一种语法结构,使得代码更易读易写。

    引用:指对现有变量引用的一种方式,**引用必须引用一个已经存在的变量,引用本身并不是一个新的变量。**并不占用内存。简单说引用就是给起个别名。

    示例:

    #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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    为什么要有引用?

    先看一个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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    自定义函数的形参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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如果希望函数实际的更改变量本身的值怎么办?这时候就可以采用引用的方法:

    #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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    道理与前面一致,前面形参为变量时,当调用函数的时候,创建一个变量来存储传入的实参,操作的是这个新的变量;现在如果形参是一个引用的话,当调用函数时,创建一个引用来引用传入的实参,操作的是这个引用,然而引用前面讲过,更改引用会更改原来变量的值,所以理所当然就可以通过把形参设置为引用的方法使函数对变量本身产生作用

    注意事项

    • 一个引用只能引用一个变量,不能引用了一个变量后更改该引用的变量对象

      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
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    • 引用必须初始化,必须引用一个确定的变量,不能仅仅是定义一个引用

    类(class)

    面向对象编程是一种编程思想,不注重解决问题的详细过程,而是设计具有相应属性和方法的对象,通过这些具有不同功能的对象来完成任务。

    类是一种将数据和函数组织在一起的方式

    • 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);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29

    class: 关键字,表明这是一个类。后面接类名,然后是一对{},最后要有一个;

    public:关键字,接一个:,后面接属性或方法。表明后面的内容是公有的,可以让类外面的成员访问。(c++的类中的内容默认为私有(private),外部是不可以直接访问的)

    属性和方法: 类内的数据通常称为属性,函数通常称为方法。

    class(类) VS struct(结构体)

    • 一个class的成员默认情况是private(私有)的,而struct正好相反;

    • c++中保留struct是考虑到与c的兼容性,c中是没有class的。简单的方式完成两种代码的转换只需要一个宏定义:

      在c++代码种使用
          #define class struct
      
      在c代码中使用
          #define struct class
      
      • 1
      • 2
      • 3
      • 4
      • 5

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      这样就把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.");  // 打印日志中的信息
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    上述代码仅作示例,有很多问题而且非常不规范。接下来逐步完善上面的代码。

    static关键字

    两种含义:

    • 类或结构体之外:类外的static关键字修饰的符号在链接阶段是局部的。只对当前所在的翻译单元可见(只在所在的文件中可见)其他的文件是无法访问被修饰的内容的。

    main.cpp

    #include
    
    #define LOG(x) std::cout<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    static.cpp

    static int s_Variable = 6;
    
    • 1

    在上面的这种文件结构下,编译无错误,输出结果是 10。

    如果更改static.cpp中的内容为:

    int s_Variable = 6;
    
    • 1

    此时编译是可以通过的,但是在运行时会引发链接错误:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    链接器会找到一个或多个多重定义的符号,是因为链接器不仅会找到main.cpp中的变量s_Variable,而且由于static.cpp中的变量不是静态的(static),也会被链接器找到,所以会引发该错误。

    除上面的把static.cpp中的全局变量s_Variablestatic修饰外,还有另外的解决方法:在main.cpp中,使用关键字extern修饰该全局变量:

    #include
    
    #define LOG(x) std::cout<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    建议:尽量让全局变量或者函数使用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修饰了两个符号xy,那么所有类的实例中的这两个属性指向的是同一个共享空间!

所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过Entity::x的方式来引用:

#include

#define LOG(x) std::cout<

注意事项:

局部作用域中的static

示例代码:

#include

#define LOG(x) std::cout<

上面的代码循环执行了5次,每次循环的内容为:

  1. 定义一个静态局部变量v,并初始化为整型数字0;
  2. v自加1;
  3. 打印输出v的值, 结果为1 2 3 4 5

如果去掉关键字static

#include

#define LOG(x) std::cout<

由于每次循环都重新定义了v,并初始化为0,所以每次输出的结果都是 1,也就是1 1 1 1 1

这就是说,当使用static关键字修饰时,被修饰的内容的生命周期为程序的整个运行周期,在程序运行过程中,始终存在在内存当中,其中的值是一直存在的,而没有static关键字的话,就会在每次循环开始时,都会创建一个新的变量,在内存中设定一个新的地址用来存储对应的值。

枚举(enums)

#include

#define LOG(x) std::cout<

即使向上面的代码,使用了A, B, C,没有指定类型但是一旦出现在枚举中,它们实际的值就是1, 2, 3

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#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.");  // 打印日志中的信息
}

构造函数vs析构函数

构造函数

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),在销毁一个对象的时候运行

当一个对象被销毁的时候析构函数都会被调用

示例:

#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可以使用两个类中的内容(属性和方法)。

string

代码示例:

#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是空字符串
}

注意事项及相关操作

#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;  // 顺序输出两个字符串
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虚函数(VirtualFunctions)

虚函数可以让我们在子类中重写方法

有两个类: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"; }

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

  1. 父类中的方法前加一个virtual
  2. 子类中的方法在类型后加一个override(加不加都可以,但为了代码可读性建议加上,代表这是一个从父类继承的方法)

结果就可以像我们所期待的那样,各自执行各自的方法:

#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);  

}

可见性

可见性是面向对象中的概念,指一个类中的成员或者方法是否可见。

可见指的是谁可以访问或者调用他们,可见性不会影响程序的实际运行,也不会对程序性能有影响。

三个基础的可见修饰符(访问修饰符):

C++如何工作

示例代码HelloWorld

#include

int main()
/*打印输出HelloWorld!*/
{
	std::cout << "Hello World!" << std::endl;
	std::cin.get();
	return 0;
}

分解代码:

#include 
int main()
{
    std::cout << "Hello World!" << std::endl;
    std::cin.get();
    return 0;
}

示例代码:

#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.cppLog.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.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
	.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

预处理(Preprocessing)

预处理阶段编译器检查所有的预处理语句(由#符号标记的语句),常见的预处理指令有:

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 int

INTEGER 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

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

解决方法:

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

    static void Log(const char* message)
    {
    	std::cout << message << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4

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

  2. 使用关键字inline,这样在调用函数时,只复制函数内部的语句。

    inline void Log(const char* message)
    {
    	std::cout << message << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4

变量

编程的本质就是在操作数据,数据的读写,增删改查等等。

变量(variable):给一个存储在内存中的数据一个名字,从而来使用这个数据,且这个数据是可以更改的。

变量类型

int

代表在给定的范围内存储一个整型数字。

#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),但是数据类型的实际大小取决于编译器。

可以使用关键字unsigned限定变量是无符号位

#include

int main()
{
	unsigned int variable = 2147483648;  // 定义一个无符号位的整型变量

	std::cout << "The int variable number is : " << variable << std::endl;  // 打印输出
	std::cin.get();
}

此时刚才的临界点的数值就是可以正确打印的了。

char

大小: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++程序中重点关注的还是内存,当我使用某个数据类型的变量时,会分配多少的内存!

short

大小:2 bytes

long

大小:4 bytes

long long

大小:8 bytes

float

浮点数(小数)

大小:4 bytes

#include

int main()
{
	float variable = 1.2f;  // 定义一个浮点型变量并初始化为1.2

	std::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

int main()
{
	bool variable = false;  // 定义一个bool型变量并初始化为false

	std::cout << "The char variable is : " << variable << std::endl;  // 打印输出
	std::cin.get();
}

实际运行结果并不是输出字符:false,而是数字 0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因为计算机只处理数字,0代表false1代表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();
}

这样就会打印两条语句。

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

另外else if并不是关键字,它只是elseif的一种组合,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循环

while循环

do...while循环

控制流

控制流(control flow)语句基本上可以解释为可以更好的控制循环。

指针(Pointer)

指针的本质

计算机运行一个程序 ,或者运行我们的代码,就是把程序和代码推到内存当中,所有的东西都会被存储在内存中,然后cpu根据指令在内存中给出相应的操作。

指针就是管理和操作内存的重要道具

指针本质上还是一个整数数字,它存储的是一个内存地址。

指针只是一个存储着内存地址的整数!!!

指针只是一个存储着内存地址的整数!!!

指针只是一个存储着内存地址的整数!!!

示例代码:

#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字节的块,就是内存块的大小问题!但是本质上就是上面的一句话,方便学习理解。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

指针的用法

#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();
}

可以看到buffer指向的地址中用了8个字节保存了8个1。

指针只是一个存储着内存地址的整数!!!

引用(Reference)

引用和指针几乎一致,引用是基于指针的一种语法结构,使得代码更易读易写。

引用:指对现有变量引用的一种方式,**引用必须引用一个已经存在的变量,引用本身并不是一个新的变量。**并不占用内存。简单说引用就是给起个别名。

示例:

#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
}

道理与前面一致,前面形参为变量时,当调用函数的时候,创建一个变量来存储传入的实参,操作的是这个新的变量;现在如果形参是一个引用的话,当调用函数时,创建一个引用来引用传入的实参,操作的是这个引用,然而引用前面讲过,更改引用会更改原来变量的值,所以理所当然就可以通过把形参设置为引用的方法使函数对变量本身产生作用

注意事项

类(class)

面向对象编程是一种编程思想,不注重解决问题的详细过程,而是设计具有相应属性和方法的对象,通过这些具有不同功能的对象来完成任务。

类是一种将数据和函数组织在一起的方式

class: 关键字,表明这是一个类。后面接类名,然后是一对{},最后要有一个;

public:关键字,接一个:,后面接属性或方法。表明后面的内容是公有的,可以让类外面的成员访问。(c++的类中的内容默认为私有(private),外部是不可以直接访问的)

属性和方法: 类内的数据通常称为属性,函数通常称为方法。

class(类) VS struct(结构体)

类的写法

写一个日志类,用来打印不同的信息:

#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_Variablestatic修饰外,还有另外的解决方法:在main.cpp中,使用关键字extern修饰该全局变量:

#include

#define LOG(x) std::cout<

建议:尽量让全局变量或者函数使用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修饰了两个符号xy,那么所有类的实例中的这两个属性指向的是同一个共享空间!

所以,引用实例的静态内容是没有意义的,无论怎样那两个东西就是那两个东西,所以一般可以使用下面的做法,通过Entity::x的方式来引用:

#include

#define LOG(x) std::cout<

注意事项:

局部作用域中的static

示例代码:

#include

#define LOG(x) std::cout<

上面的代码循环执行了5次,每次循环的内容为:

  1. 定义一个静态局部变量v,并初始化为整型数字0;
  2. v自加1;
  3. 打印输出v的值, 结果为1 2 3 4 5

如果去掉关键字static

#include

#define LOG(x) std::cout<

由于每次循环都重新定义了v,并初始化为0,所以每次输出的结果都是 1,也就是1 1 1 1 1

这就是说,当使用static关键字修饰时,被修饰的内容的生命周期为程序的整个运行周期,在程序运行过程中,始终存在在内存当中,其中的值是一直存在的,而没有static关键字的话,就会在每次循环开始时,都会创建一个新的变量,在内存中设定一个新的地址用来存储对应的值。

枚举(enums)

#include

#define LOG(x) std::cout<

即使向上面的代码,使用了A, B, C,没有指定类型但是一旦出现在枚举中,它们实际的值就是1, 2, 3

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#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.");  // 打印日志中的信息
}

构造函数vs析构函数

构造函数

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),在销毁一个对象的时候运行

当一个对象被销毁的时候析构函数都会被调用

示例:

#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可以使用两个类中的内容(属性和方法)。

string

代码示例:

#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是空字符串
}

注意事项及相关操作

#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;  // 顺序输出两个字符串
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虚函数(VirtualFunctions)

虚函数可以让我们在子类中重写方法

有两个类: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"; }

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

  1. 父类中的方法前加一个virtual
  2. 子类中的方法在类型后加一个override(加不加都可以,但为了代码可读性建议加上,代表这是一个从父类继承的方法)

结果就可以像我们所期待的那样,各自执行各自的方法:

#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);  

}

可见性

可见性是面向对象中的概念,指一个类中的成员或者方法是否可见。

可见指的是谁可以访问或者调用他们,可见性不会影响程序的实际运行,也不会对程序性能有影响。

三个基础的可见修饰符(访问修饰符):

  • 相关阅读:
    TikTok 推荐引擎强大的秘密
    如何批量修改图片宽高尺寸
    【简单选择排序】
    删除表数据
    观察神秘的RQ
    43、Flink之Hive 读写及详细验证示例
    【python】基于随机森林和决策树的鸢尾花分类
    docker的常用指令
    [CISCN2019 华北赛区 Day1 Web1]Dropbox 1
    Debian利用现有软件包或者光盘镜像搭建本地软件源
  • 原文地址:https://blog.csdn.net/hanxiaochuan1/article/details/134424316