侧边栏壁纸
博主头像
ifool博主等级

Stupid is as stupid does

  • 累计撰写 3 篇文章
  • 累计创建 1 个标签
  • 累计收到 1 条评论
标签搜索

目 录CONTENT

文章目录

learn C++

ifool
2022-12-16 / 0 评论 / 0 点赞 / 50 阅读 / 10,501 字

Learn Cpp

2022/12/15 C++是如何工作的

Main.cpp

#include <iostream>

void Log(const char* message);

int main() {
	Log("hello");
	//std::cout << "hello world" << std::endl;
	std::cin.get();
}

Log.cpp

#include <iostream>


void Log(const char* message) {
	std::cout << message << std::endl;
}
  • 预处理阶段编译器会将头文件复制黏贴到cpp文件中,编译器只编译cpp文件不编译头文件,编译完成后每个cpp文件会生成.obj文件,接下来链接器会将obj链接合并成一个exe可执行文件,对于不在主文件里的函数需要使用需要先在使用的cpp文件中声明,然后链接器会在链接的时候寻找声明的函数体

2022/12/16 编译器是如何工作的

编译器将每一个翻译单元编译成obj文件,通常来说一个cpp文件就是一个翻译单元,但也不一定,有可能存在多个cpp文件最后只生成一个obj文件

预处理阶段,编译器遍历预处理语句,并将头文件中的语句复制黏贴到cpp文件中

image-20221216185946814

编写代码math.cpp

int Multiply(int a, int b) {
	int result = a * b;
	return result;
#include "EndBrace.h"

编写头文件EndBrace.h并在math.cpp中引入

打开visual studio2022项目右键属性,在预处理部分器选项,将预处理到文件打开,我们就会生成对应的中间文件.i文件,也就是编译器预处理阶段后的产物,打开此选项后,不会再生成.obj文件,按下ctrl+F7编译通过,打开项目目录

image-20221216190156621

打开项目目录,会得到.i文件,通过文本编辑器打开

image-20221216190236476

我们得到如下内容

main.cpp

#line 1 "E:\\learn_c++\\helloworld\\helloworld\\Math.cpp"


int Multiply(int a, int b) {
	int result = a * b;
	return result;
#line 1 "E:\\learn_c++\\helloworld\\helloworld\\EndBrace.h"
}
#line 7 "E:\\learn_c++\\helloworld\\helloworld\\Math.cpp"

我们可以看到,预处理的结果就是将头文件中的内容黏贴到对应的cpp文件中,我们更换思路,定义一些宏

#define INTEGER int
INTEGER Multiply(INTEGER a, INTEGER b) {
	INTEGER result = a * b;
	return result;
#include "EndBrace.h"

编译后可以看到最后的中间产物是不变的

image-20221216191121009

也就是说编译器预处理阶段,就是还会将定义的宏直接替换掉,我们再添加一些东西

#if 1
#define INTEGER int
INTEGER Multiply(INTEGER a, INTEGER b) {
	INTEGER result = a * b;
	return result;
#include "EndBrace.h"

#endif

编译后,发现什么都没发生,产物还是一样

我们将条件修改成# if 0

image-20221216191645848

#if 0
#define INTEGER int
INTEGER Multiply(INTEGER a, INTEGER b) {
	INTEGER result = a * b;
	return result;
#include "EndBrace.h"

#endif

我们看到visual studio已经将我们书写的函数变为灰色,因为编译器不会执行这段语句,我们编译查看结果

image-20221216191808086

可以看到,确实是没有任何对应的语句产生

我们还原成最开始的函数语句,添加iostream标准库,编译

#include <iostream>


int Multiply(int a, int b) {
	int result = a * b;
	return result;
}

可以看到编译后的文件

image-20221216192244363

我们发现编译的文件大小直接从几十k上升到1000多k,打开对应的产物我们就能发现因为标准库的内容太多了,预处理阶段将里面的内容都复制黏贴到了cpp文件中

image-20221216192413090

预处理的文件也来到了恐怖的60000多行

这就是编译器预处理阶段的功能

接下来我们看一看生成的对应的obj内容是什么,我们关闭生成预处理文件的选项,再次编译,打开对应的math.obj可以看到全部都是16进制的数据

image-20221216192627720

可对于我们来说是不可读的,我们打开

image-20221216193207375

将汇编程序输出改成仅有程序集的列表点击确定后再次编译

image-20221216193324586

我们得到对应的.asm文件打开后发现对应的是可读的汇编指令,用文本编辑器打开,我们可以看到如下结果

image-20221216193143095

因为我们不是直接返回a*b所以我们可以发现汇编指令中mov了两次,我们修改代码,直接返回a*b再次编译,可以发现两次多余的mov消失了,也就是说如果我们不优化代码,编译器是会做多余的事情来降低我们代码的运行速度,也就是垃圾代码的运行效率低

image-20221216193504254

我们可以让编译器帮我们优化代码

右键项目打开属性

image-20221216202422021

选择优化,将优化改为优选速度

然后点击代码生成

image-20221216202459424

将基本运行库检查设为默认,点击确定,然后编译,我们可以看到生成的汇编代码又小了一点

image-20221216202354706

我们将代码简化,修改成返回5*2我们可以发现,输出的汇编代码

image-20221216203137518

仅仅是将10移动到寄存器中,这完全是没必要的因为所有的常数都是可以计算出答案的,也就是所谓的常数折叠

我们再书写一个函数

const char* Log(const char* message) {
	return message;
}

int Multiply() {
	Log("Multiply");
	return 5*2;
}

编译后查看汇编代码

image-20221216203456687

我们可以看到在乘法函数中调用了log而函数后的一串随机字符是函数签名,用于链接器将多个obj文件连接成一个.exe文件时寻找函数所需要的

当我们打开编译器性能优化,编译后我们发现生成的汇编代码中对于logcall指令不见了,因为我们调用这个函数并没有用任何变量接收这是完全没必要的,所以编译器将它优化掉了

2022/12/16 链接器是如何工作的

编译器将多个obj文件连接生成一个.exe文件,对于链接阶段,一定要有一个入口函数,不一定是main函数,但通常来说是main函数,来告诉链接器,程序的入口在这,不然就会导致链接错误

#include <iostream>

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

int Multiply(int a, int b) {
	Log("Multiply");
	return a*b;
}
//int main() {
//	std::cout << Multiply(5, 8) << std::endl;
//	std::cin.get();
//}

image-20221216233337936

我们将Log移动到单独的一个文件

Log.cpp

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

Math.cpp

#include <iostream>



int Multiply(int a, int b) {
	Log("Multiply");
	return a*b;
}
int main() {
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

我们尝试编译,会发现链接错误,因此我们需要先声明Log函数

image-20221216234309635

math.cpp

#include <iostream>

void Log(const char* message);

int Multiply(int a, int b) {
	Log("Multiply");
	return a*b;
}
int main() {
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

尝试编译发现已经可以编译通过,但是当我们构建时,还是报错

image-20221216234244961

我们添加对应的标准库

Log.cpp

#include <iostream>

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

发现已经可以链接通过,构建成功

image-20221216234444994

当我们将Log.cpp中的Log函数改名为Logr,再次编译我们发现可以编译通过,因为编译阶段并没有链接,编译器认为在某处有Log函数,虽然我们已经将它改为了Logr,当时当我们尝试构建,就会发现,链接错误

image-20221216235136281

链接器无法找到我们在Multiply调用的Log函数

image-20221216235228364

但是当我们将这句话注释,再次构建,就会发现没有问题

image-20221216235315163

因为链接器认为你并没有使用这个函数所以就不需要链接,但换个角度,如果我们注释main中调用的Multiply是不是也可以呢,答案时否定的

image-20221216235447833

还是出现了链接错误,因为链接器认为在这个cpp文件中不使用,可能在另一个cpp中会被调用,因此链接器尝试链接Log但是并没有找到这个函数的定义,我们可以修改将这个函数只定义在这个cpp中使用

#include <iostream>

void Log(const char* message);

static int Multiply(int a, int b) {
	Log("Multiply");
	return a*b;
}
int main() {
	//std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

添加static关键字,将函数绑定在当前文件,再次构建就没有问题了

image-20221216235857552

我们将Logr修改回Log,将函数定义修改为int,添加返回值0,再次构建发现失败了

image-20221217000242054

链接器链接时函数的声明和定义要一致,包括参数数量,返回值类型,不然链接仍然会失败

同时当我们在一个项目中有一个相同返回值类型,相同名字的函数,我们的链接器链接时仍然会报错,因为链接器不知道该链接哪个函数

Log.cpp

#include <iostream>

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

image-20221217001241044

放在不同文件中时Math.cpp

image-20221217001356423

一样报错,我们还可能在不经意间发生这种重复定义的错误,例如头文件,我们定义一个Log.h

Log.h

#pragma once

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

Math.h

#include <iostream>
#include "Log.h"
//void Log(const char* message);

static int Multiply(int a, int b) {
	Log("Multiply");
	return a*b;
}
int main() {
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}


Log.cpp

#include <iostream>
#include "Log.h"

void InitLog() {
	Log("Initialized Log");
}

结果是仍然链接错误,我们仍然重复定义了Log因为头文件的作用就是将头文件中的内容复制黏贴到cpp文件中

image-20221217002259892

我们尝试修改 添加static关键字,使头文件引入的函数成为每个cpp文件的内置函数,再次编译发现已经成功编译了

#pragma once

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

image-20221217002646964

或者我们可以使用inline关键字进行函数解耦

#pragma once

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

也可以编译成功,但最好的解决方式是,只在头文件中声明函数,在一个文件中定义函数,让后需要调用函数的地方引入头文件

Log.h

#pragma once

void Log(const char* message);

Log.cpp

#include <iostream>
#include "Log.h"


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

void InitLog() {
	Log("Initialized Log");
}

Math.cpp

#include <iostream>
#include "Log.h"
//void Log(const char* message);

static int Multiply(int a, int b) {
	Log("Multiply");
	return a*b;
}
int main() {
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}


image-20221217003137970

再次编译可以发现编译成功了

2022/12/17 变量

不同变量之间最本质的区别是占用内存的大小,因为计算机最终存储数据都是以数字存储在内存中

  • sizeof 操作符可以查看变量对应的字节数
#include<iostream>

int main() {
	int a = 1;
	short b = 2;
	long c = 3;
	bool d = true;
	std::cout << sizeof a << std::endl;
	std::cout << sizeof b << std::endl;
	std::cout << sizeof c << std::endl;
	std::cout << sizeof d << std::endl;

	std::cin.get();
}

2022/12/17 函数

函数是对于重复代码的抽象,让我们避免书写重复的代码,但应该注意

  • 不要定义太多无意义函数,函数的调用在底层调用call jump push命令,过多的函数调用会降低我们的运行速度
  • main函数是一个特殊的函数,可以不返回值哪怕定义了int类型,因为它默认返回0,当然这是个特例

2022/12/17 头文件

头文件通常用于函数得声明,但只能在一个地方定义,我们在需要调用某个函数时就将头文件包含进来,这样子链接器在链接时就知道这个函数在哪里

头文件的

#pragma once

称为预处理语句,表示只引入函数定义一次,避免多次引入同一个头文件造成错误,如果添加这个语句那么哪怕引入同一个头文件多次,仍然不会生效

他还有等价形式

//#pragma once
#ifndef _LOG_H
#define _LOG_H
void Log(const char* message);

#endif

结果也是一样的

2022/12/17 如何在Visual Studio中调试

  • 打下断点

image-20221217232455688

debug模式下运行

image-20221217232635003

选择调试->窗口->内存->内存1,打开内存监视窗口

image-20221217232904661

我们可以看到在a的这段赋值语句还没运行时,未初始化的内存4 byte显示为8个c,每两个c代表一个字节,我们可以看到string 变量未被赋值时也是8个c

image-20221217233203460

单步运行也就是按下快捷键F11,我们发现a的值发生了改变,a的值被初始化为8,16进制的表示法为08 00 00 00

2022/12/17条件分支语句 if

Main.cpp

#include <iostream>
#include "Log.h"
int main() {
	int x = 5;
	bool comparisonResult = x == 5;
	if (comparisonResult) {
		Log("Hello World!");
	}
	std::cin.get();
}

在x=5处下断点,开始运行,转到反汇编

image-20221218000844361

简单查看一下

image-20221218001410408

image-20221218001540869

else if 等于 else + if

#include <iostream>
#include "Log.h"
int main() {
	int x = 6;
	bool comparisonResult = x == 5;
	if (comparisonResult) {
		Log("equal 5");
	}
	else {
		if (x == 6)
			Log("euqal 6");
	}
	std::cin.get();
}

等价形式

#include <iostream>
#include "Log.h"
int main() {
	int x = 6;
	bool comparisonResult = x == 5;
	if (comparisonResult) {
		Log("equal 5");
	}
	else if (x == 6)
			Log("euqal 6");
	std::cin.get();

2022/12/18 Visual Studio的最佳设置

  • 新建src目录将所有资源文件放在里面
  • 设置输出文件和中间文件目录
  • 删除之前已经生成的没有必要的文件
  • 重新编译

image-20221218150202298

2022/12/18 循环for while

for循环第一个语句时初始化变量,第二个条件是判断条件,判断变量是否继续执行,第三个语句是在循环执行后执行的语句

#include <iostream>
#include "Log.h"
int main() {
	for (int i = 0; i < 5; i++)
	{
		Log("hello world");
	}
	std::cin.get();
}

image-20221218151808944

while循环

#include <iostream>
#include "Log.h"
int main() {
	int i = 0;
	while (i < 5) {
		Log("hello world");
		i++;
	}
	std::cin.get();
}

image-20221218152125022

二者也可以等价替换,for循环三个条件不写效果等价于while

可能有的区别

image-20221218152229657

还有一个do…while

循环无论如何都会执行一遍

#include <iostream>
#include "Log.h"
int main() {
	do {
		Log("hello world");
	} while (false);
	std::cin.get();
}

image-20221218152449329

2022/12/18 控制流语句

continue- 跳过本次迭代,进入下次迭代

break - 直接退出循环

return 在循环语句中,一旦遇到return整个循环直接退出

2022/12/18 指针

指针是保存内存地址的整数

指针的类型不重要,只有在对内存地址进行数据读写时有用,因为编译器需要知道要写入多大的数据,不同类型的数据对应的字节数不同

#include <iostream>
#define LOG(x) std::cout << x << std::endl
int main() {
	int a = 8;
	void* ptr = &a;
	std::cin.get();
} 

复制ptr的内存地址,黏贴到黏贴到内存监视器,我们可以看到a变量对应的数值8,也就是该指针所指向的内存地址所保存的数据

image-20221218235438718

我们修改指针所指向的内存地址存储的值,通过*ptr逆向引用修改内存地址对应的数据的值

#include <iostream>
#define LOG(x) std::cout << x << std::endl
int main() {
	int a = 8;
	int* ptr = &a;
	*ptr = 10;
	LOG(a);
	std::cin.get();
} 

image-20221219000051863

目前我们都是在栈上创建数据,下面我们在堆上创建数据

#include <iostream>
#define LOG(x) std::cout << x << std::endl
int main() {
	char* buffer = new char[8];
	memset(buffer, 0, 8);

	std::cin.get();
} 

申请一个8字节的数据,用0进行数据填充,我们可以看到我们申请了一个堆内存,并且将初值赋值为0

image-20221219000821847

指针的指针就是指向储存指针内存地址的指针

#include <iostream>
#define LOG(x) std::cout << x << std::endl
int main() {
	char* buffer = new char[8];
	memset(buffer, 0, 8);
	char** ptr = &buffer;
	std::cin.get();
} 

image-20221219001309481复制ptr内存地址,我们可以看到它所指向的buffer指针的内存地址98 ae 87 00

我们重新编排为00 87 ae 98 粘贴到内存地址查看器

image-20221219001528080

可以得到buffer指针对应内存地址储存的值

2022/12/18 引用

引用的本质是指针的语法糖,让我们更容易使用指针

引用是现有变量的一个别名他不是实际存在的变量,因此引用一旦定义必须立即赋值,因为它必须引用一些东西

实现a++

#include <iostream>
#define LOG(x) std::cout << x << std::endl

void Increament(int* num) {
	(*num)++;
}
int main() {
	int a = 8;
	Increament(&a);
	LOG(a);
	std::cin.get();
} 

image-20221219002905464

这是使用指针完成的

接下来使用引用

#include <iostream>
#define LOG(x) std::cout << x << std::endl

void Increament(int& num) {
	num++;
}
int main() {
	int a = 8;
	Increament(a);
	LOG(a);
	std::cin.get();
} 

image-20221219003032666

而下面这种方式是不行的

#include <iostream>
#define LOG(x) std::cout << x << std::endl

void Increament(int num) {
	num++;
}
int main() {
	int a = 8;
	Increament(a);
	LOG(a);
	std::cin.get();
} 

它的等价形式是

#include <iostream>
#define LOG(x) std::cout << x << std::endl

void Increament(int num) {
    int num = 8;
	num++;
}
int main() {
	int a = 8;
	Increament(a);
	LOG(a);
	std::cin.get();
}

会在函数内部重新创建一个变量,并不影响全局的变量

2022/12/18 类

面向对象是一种编程方式

类是对于数据和功能的结合

由类的类型构成的变量称为对象,新的对象变量称为实例。

类的成员默认都是私有的不可见的

#include <iostream>
#define LOG(x) std::cout << x << std::endl

class Player {

};
int main() {
	Player player;
	std::cin.get();
} 

类允许我们将变量分组到一个类型中,并为这些变量添加功能,类的本质还是一个语法糖,类并未给我们带来新的功能

2022/12/18 类和结构体的区别

类和结构体的唯一区别就是类的成员默认是私有的,结构体的成员默认是公开的

当我们只表示变量的一种结构,例如数学上的向量

不在结构体中使用继承

2022/12/18 如何写一个C++类

#include <iostream>


class Log {
public:
	const int LogLevelError = 0;
	const int LogLevelWarning = 1;
	const int LogLevelInfo = 2;

private:
	int m_LogLevel = LogLevelInfo;
public:
	void SetLevel(int level) {
		m_LogLevel = level;
	}
	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;
	}	
	void Error(const char* message) {
		if (m_LogLevel >= LogLevelError)
			std::cout << "[ERROR]:" << message << std::endl;
	}
};
int main() {
	Log log;
	log.SetLevel(log.LogLevelWarning);
	log.Warn("hello!");
	std::cin.get();
} 

2022/12/19 C++中的静态 static

static关键字可以让我们在一个翻译单元中定义变量或者函数时,只对这个翻译单元有效,链接器跨翻译单元寻找定义时不会链接有static关键字定义的东西

Main.cpp

int s_Variable = 10;
int main() {
	std::cout << s_Variable << std::endl;
	//Log log;
	//log.SetLevel(log.LogLevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

Static.cpp

int s_Variable = 10;

image-20221219191436687

如果我们不添加static关键字那么链接器就会报链接错误,因为有两处相同变量的定义

我们可以为Static.cpp中的变量添加static关键字,也可以删去Main.cpp中的函数定义,添加extern关键字,让链接器去外部寻找变量的定义

extern int s_Variable;
int main() {
	std::cout << s_Variable << std::endl;
	//Log log;
	//log.SetLevel(log.LogLevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

我们也可以得到正确的结果

2022/12/19 C++类和结构体中的static

struct Entity
{
	int x, y;
	void Print() {
		std::cout << x <<"," << y << std::endl;
	}
};
int main() {
	Entity e;
	e.x = 1;
	e.y = 2;
	e.Print();
	//std::cout << s_Variable << std::endl;
	//Log log;
	//log.SetLevel(log.LogLevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

但是当我们为x,y添加static前缀时 链接器就会报错

image-20221219195613713

我们需要定义一下x,y告诉链接器它们在哪

#include <iostream>


//class Log {
//public:
//	const int LogLevelError = 0;
//	const int LogLevelWarning = 1;
//	const int LogLevelInfo = 2;
//
//private:
//	int m_LogLevel = LogLevelInfo;
//public:
//	void SetLevel(int level) {
//		m_LogLevel = level;
//	}
//	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;
//	}	
//	void Error(const char* message) {
//		if (m_LogLevel >= LogLevelError)
//			std::cout << "[ERROR]:" << message << std::endl;
//	}
//};
struct Entity
{
	static int x, y;
	void Print() {
		std::cout << x <<"," << y << std::endl;
	}
};
int Entity::x;
int Entity::y;

int main() {
	Entity e;
	e.x = 1;
	e.y = 2;
	

	Entity e1;
	e1.x = 3;
	e1.y = 4;
	e.Print();
	e1.Print();
	//std::cout << s_Variable << std::endl;
	//Log log;
	//log.SetLevel(log.LogLevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

我们可以看到这样子编译就通过了,但是两次打印的结果都是3,4,也就是说两次的x,y变量指向同一个内存地址

image-20221219201002826

也就是说静态成员在所有类的实例中只有一个实例,它们指向同一块内存地址,共享内存

这种写法等价形式是

#include <iostream>


//class Log {
//public:
//	const int LogLevelError = 0;
//	const int LogLevelWarning = 1;
//	const int LogLevelInfo = 2;
//
//private:
//	int m_LogLevel = LogLevelInfo;
//public:
//	void SetLevel(int level) {
//		m_LogLevel = level;
//	}
//	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;
//	}	
//	void Error(const char* message) {
//		if (m_LogLevel >= LogLevelError)
//			std::cout << "[ERROR]:" << message << std::endl;
//	}
//};
struct Entity
{
	static int x, y;
	static void Print() {
		std::cout << x <<"," << y << std::endl;
	}
};
int Entity::x;
int Entity::y;

int main() {
	Entity e;
	Entity::x = 1;
	Entity::y = 2;
	

	Entity e1;
	Entity::x = 3;
	Entity::y = 4;
	Entity::Print();
	Entity::Print();
	//std::cout << s_Variable << std::endl;
	//Log log;
	//log.SetLevel(log.LogLevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

可以看到这样子这个结构体已经和实例没有什么关系了,一切都是静态的,我们甚至可以删掉实例化的语句,也是可以编译运行的

image-20221219201603084

#include <iostream>


//class Log {
//public:
//	const int LogLevelError = 0;
//	const int LogLevelWarning = 1;
//	const int LogLevelInfo = 2;
//
//private:
//	int m_LogLevel = LogLevelInfo;
//public:
//	void SetLevel(int level) {
//		m_LogLevel = level;
//	}
//	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;
//	}	
//	void Error(const char* message) {
//		if (m_LogLevel >= LogLevelError)
//			std::cout << "[ERROR]:" << message << std::endl;
//	}
//};
struct Entity
{
	int x, y;
	static void Print() {
		std::cout << x <<"," << y << std::endl;
	}
};


int main() {
	Entity e;
	e.x = 1;
	e.y = 2;
	

	Entity e1;
	e1.x = 3;
	e1.y = 4;
	e1.Print();
	e1.Print();
	//std::cout << s_Variable << std::endl;
	//Log log;
	//log.SetLevel(log.LogLevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

如果我们保留方法为静态的,变量为非静态的,我们仍然无法编译通过,因为静态方法无法访问非静态成员

image-20221219201914016

在类中所写的每个非静态方法,总是获取当前类的实例作为隐形参数在运行时,但是静态方法无法得到这个隐藏的类的实例作为参数,静态方法和在类外部书写函数是一样的

static void Print(Entity e) {
	std::cout << e.x << "," << e.y << std::endl;
}

非静态方法运行时就如上面的函数一样,需要获得一个类的实例作为参数,不然无法访问非静态成员

image-20221219202332835

2022/12/19 C++中的局部静态Local Static

当我们直接在函数中定义变量,那么在每次调用后这个变量就会被销毁

image-20221219204819086

而当添加static关键字,那么声明在函数中的局部变量的声明周期就会延长到整个程序结束

image-20221219205015476

其等价形式是 在函数外部定义变量

image-20221219205058252

但是我们在函数内使用static关键字可以避免变量被篡改

2022/12/20 C++枚举

枚举实际上是一个数值的集合,将一组数据整合在一起,使语义更加明确

我们通过引入枚举修改Log

#include <iostream>


class Log {
public:
	enum Level {
		LevelError = 0,
		LevelWarning,
		LevelInfo
	};

private:
	Level m_LogLevel = LevelInfo;
public:
	void SetLevel(Level level) {
		m_LogLevel = level;
	}
	void Warn(const char* message) {
		if(m_LogLevel>=LevelWarning)
			std::cout << "[WARNING]:" << message << std::endl;
	}	
	void Info(const char* message) {
		if (m_LogLevel >= LevelInfo)
			std::cout << "[INFO]:" << message << std::endl;
	}	
	void Error(const char* message) {
		if (m_LogLevel >= LevelError)
			std::cout << "[ERROR]:" << message << std::endl;
	}
};
int i = 0;
void Function() {
	i++;
	std::cout << i << std::endl;
}

int main() {

	Log log;
	log.SetLevel(Log::LevelWarning);
	log.Warn("hello!");

	std::cin.get();
} 

2022/12/20 C++构造函数

构造函数会在类实例化时自动调用来初始化声明的变量的内存

class Entity {
public:
	float X, Y;

	void Print() {
		std::cout << X << ", " << Y << std::endl;
	}
};

int main() {
	Entity e;
	//std::cout << e.X << std::endl;
	e.Print();

	//Log log;
	//log.SetLevel(Log::LevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

此时我们输出的是未被初始化的内存的值

image-20221220163605407

如果我们在类外直接输出类中的公开的未初始化的变量,还是会报错

class Entity {
public:
	float X, Y;

	void Print() {
		std::cout << X << ", " << Y << std::endl;
	}
};

int main() {
	Entity e;
	std::cout << e.X << std::endl;
	e.Print();

	//Log log;
	//log.SetLevel(Log::LevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

image-20221220163716064

但是新版本的g++编译器好像会默认初始化int float类型的数据,visual studio使用的msvc仍然不会,所以会报错

因此我们需要书写函数来初始化变量

class Entity {
public:

	float X, Y;
	void Init() {
		X = 0.0f;
		Y = 0.0f;
	}
	void Print() {
		std::cout << X << ", " << Y << std::endl;
	}
};

int main() {
	Entity e;
	e.Init();
	std::cout << e.X << std::endl;
	e.Print();

	//Log log;
	//log.SetLevel(Log::LevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

但是cpp为了提供了更加简单得到构造函数

#include <iostream>


//class Log {
//public:
//	enum Level {
//		LevelError = 0,
//		LevelWarning,
//		LevelInfo
//	};
//
//private:
//	Level m_LogLevel = LevelInfo;
//public:
//	void SetLevel(Level level) {
//		m_LogLevel = level;
//	}
//	void Warn(const char* message) {
//		if(m_LogLevel>=LevelWarning)
//			std::cout << "[WARNING]:" << message << std::endl;
//	}	
//	void Info(const char* message) {
//		if (m_LogLevel >= LevelInfo)
//			std::cout << "[INFO]:" << message << std::endl;
//	}	
//	void Error(const char* message) {
//		if (m_LogLevel >= LevelError)
//			std::cout << "[ERROR]:" << message << std::endl;
//	}
//};
//int i = 0;
//void Function() {
//	i++;
//	std::cout << i << std::endl;
//}

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

	//Log log;
	//log.SetLevel(Log::LevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

书写和类名一样的函数就是类的构造函数,会在类每次实例化时自动调用来实例化类中的变量

当我们不希望别人实例化我们的类时,我们可以删去构造函数,或者将构造函数设为private

2022/12/20 C++ 析构函数

在类被销毁时会自动调用,用来释放内存和卸载变量

书写的方式是在构造函数前加~

#include <iostream>


//class Log {
//public:
//	enum Level {
//		LevelError = 0,
//		LevelWarning,
//		LevelInfo
//	};
//
//private:
//	Level m_LogLevel = LevelInfo;
//public:
//	void SetLevel(Level level) {
//		m_LogLevel = level;
//	}
//	void Warn(const char* message) {
//		if(m_LogLevel>=LevelWarning)
//			std::cout << "[WARNING]:" << message << std::endl;
//	}	
//	void Info(const char* message) {
//		if (m_LogLevel >= LevelInfo)
//			std::cout << "[INFO]:" << message << std::endl;
//	}	
//	void Error(const char* message) {
//		if (m_LogLevel >= LevelError)
//			std::cout << "[ERROR]:" << message << std::endl;
//	}
//};
//int i = 0;
//void Function() {
//	i++;
//	std::cout << i << std::endl;
//}

class Entity {

public:
	
	float X, Y;
	Entity() {
		X = 0.0f;
		Y = 0.0f;
		std::cout << "Create the class" << std::endl;
	};


	void Print() {
		std::cout << X << ", " << Y << std::endl;
	}
	~Entity() {
		std::cout << "Destory the class" << std::endl;
	};
};

void Function() {
	Entity e;
	e.Print();
}

int main() {

	Function();
	//Log log;
	//log.SetLevel(Log::LevelWarning);
	//log.Warn("hello!");

	std::cin.get();
} 

2022/12/20 C++继承

继承是面向对象的特性之一,可以帮我我们减少书写重复的代码,我们可以将一个公共的功能放在一个基类中,让子类去继承这个基类拓展功能

#include<iostream>

class Entity {
public:
	float X, Y;
	void move(float xa, float ya) {
		X += xa;
		Y += ya;
	}
};
class Player:public Entity {
public:
	const char* Name;
	void PrintName() {
		std::cout << Name << std::endl;
	}
};
int main() {
	Player player;
	player.X;


}

子类是父类的超集,可以访问父类中所有的公开成员,也可以重写父类中的方法

2022/12/20 C++虚函数

当我们继承父类并且重写父类方法时,我们需要使用虚函数来确保调用的准确性

#include<iostream>
#include <string>

class Entity {
public:
	std::string GetName() { return "Entity"; }
};


class Player:public Entity {
private:
	std::string m_Name;
public:
	Player(const std::string& name)
		:m_Name(name) {}

	std::string GetName() { return m_Name; }
};

void PrintName(Entity* entity) {
	std::cout << entity->GetName ()<< std::endl;
}

int main() {
	Entity* e = new Entity();
	PrintName(e);

	Player* p = new Player("Cherno");
	PrintName(p);

	//Entity* entity = p;
	//std::cout << entity->GetName << std::endl;
	std::cin.get();
}

image-20221220233335794

我们希望子类调用自己重写的GetName方法,但是最终,程序仍然调用父类的GetName,为了避免这种情况的发生,我们需要虚函数,这样程序在调用时会生成一个vitual table表里记录了哪些子类重写了父类的方法,当调用时会在这个表中进行映射,使调用的是我们重写的方法,但是这是需要消耗内存资源的,虽然可以忽略不计。

当我们添加上virtual关键字和override关键字就发现,问题解决了

#include<iostream>
#include <string>

class Entity {
public:
	virtual std::string GetName() { return "Entity"; }
};


class Player:public Entity {
private:
	std::string m_Name;
public:
	Player(const std::string& name)
		:m_Name(name) {}

	std::string GetName() override { return m_Name; }
};

void PrintName(Entity* entity) {
	std::cout << entity->GetName ()<< std::endl;
}

int main() {
	Entity* e = new Entity();
	PrintName(e);

	Player* p = new Player("Cherno");
	PrintName(p);

	//Entity* entity = p;
	//std::cout << entity->GetName << std::endl;
	std::cin.get();
}

image-20221220233256959

2022/12/20 C++接口

C++的接口是本质是一个提供纯虚函数的类,只有函数的定义,没有函数功能的实现,更像是一个模板,让继承他的基类去实现功能,如果基类没有实现这个功能则无法实例化

#include<iostream>
#include <string>

class Printable {
public:
	virtual std::string GetClassName() = 0;
};

class Entity:public Printable {
public:
	std::string GetName() { return "Entity"; }
	std::string GetClassName() override { return "Entity";}
};
	

class Player:public Entity {
private:
	std::string m_Name;
public:
	Player(const std::string& name)
		:m_Name(name) {}

	std::string GetName()  { return m_Name; }
	std::string GetClassName() override { return "Player"; }
};

void PrintName(Entity* entity) {
	std::cout << entity->GetName ()<< std::endl;
}

void Print(Printable* obj) {
	std::cout << obj->GetClassName() << std::endl;
}
int main() {
	Entity* e = new Entity();
	//PrintName(e);

	Player* p = new Player("Cherno");
	//PrintName(p);
	Print(p);
	Print(e);


	//Entity* entity = p;
	//std::cout << entity->GetName << std::endl;
	std::cin.get();
}

EntityPlayer实现了Printable类的功能我们说这两个类实现了接口

2022/12/21 C++可见性

设置类中的成员的可见性可以帮助我们组织代码结构,理解代码

public:
全部可见 继承的子类和类外部都可以访问
private:
只有类内部的private的成员可以访问,继承和类外部都无法访问
protected:
类外部不可以访问,类内部和继承可以访问

2022/12/21 C++数组

数组以一组数据的集合,本质是一个指针,可以为我们在内存中连续储存数据

#include<iostream>

int main() {
	int example[5];
	int* ptr = example;
	for (int i = 0; i < 5; i++) {
		example[i] = 2;
	}
	//example[2] = 5;
	//*(ptr + 2) = 6;
	std::cin.get();
}

image-20221221213020521

我们可以看到数组在内存中连续写入了5个数值,当我们以下标访问数组成员时,实际上就是在对数组指针取偏移量,例如example[2]就是取了8字节的偏移量,因为一个int是4字节,同样我们也可以直接对数组指针进行运算,实际上就是和取偏移量是一样的

当我们在栈上创建数组时,一旦超过作用域就会被销毁,因此但我们在函数中需要创建返回一个数组时,我们需要用new关键字在堆上创建,在堆上创建的数组,当使用完毕我们需要用delete关键字删除释放内存

#include<iostream>



int main() {
	int example[5];
	for (int i = 0; i < 5; i++) {
		example[i] = 2;
	}
	int* another = new int[5];
	for (int i = 0; i < 5;i++) {
		another[i] = 2;
	}
	delete[] another;
	std::cin.get();
}

使用new关键字创建数组还可能造成间接寻址

当我们不使用new关键字时,一切正常,我们可以通过实例化对象的地址直接看到我们创建的数组数据

image-20221221220921823

但是当我们使用new关键字时,实例化对象指向的地址就不是数组内存地址,它包含另一个地址,而这个地址指向数组的内存地址

image-20221221221314147

而这个地址 指向我们的数组地址

image-20221221221404085

因此为了避免这样的内存跳跃带来的性能损失,我们需要尽量在栈上创建数组而不是堆上

同时我们无法得到数组的大小,虽然说堆上创建的数组可能可以得到,但是我们不建议这么做,因为一旦传入的变为数组指针,一切就都会发生错误。

#include<iostream>

class Entity{
public:
	int* example = new int[5];
	Entity() {

		int a[5];
		std::cout << sizeof(a)/sizeof(int) << std::endl;
		for (int i = 0; i < 5; i++) {
			example[i] = 2;
		}
	}

};

int main() {
	Entity e;
	std::cin.get();
}

如果我们再堆上创建那么传入的就是一个整数型指针,这样子结果就是错误的

#include<iostream>

class Entity{
public:
	int* example = new int[5];
	Entity() {

		int a[5];
		std::cout << sizeof(example)/sizeof(int) << std::endl;
		for (int i = 0; i < 5; i++) {
			example[i] = 2;
		}
	}

};

int main() {
	Entity e;
	std::cin.get();
}

我们可以看到,答案是错误的不是5而是1

image-20221221223207076

因此我们如果想用原始数组得到长度,那我们就需要自己维护数组的长度,定义一个数组长度的常量

#include<iostream>

class Entity{
public:
	static const int exampleSize=5;
	int example[exampleSize];

	Entity() {
		for (int i = 0; i < 5; i++) {
			example[i] = 2;
		}
	}

};

int main() {
	Entity e;
	std::cin.get();
}

必须要添加static关键字,因为这必须在编译的时候知道

我们也可以使用C++11提供的标准库的数组array

#include<iostream>
#include <array>


class Entity{
public:
	static const int exampleSize=5;
	int example[exampleSize];


	std::array<int, 5> another;
	Entity() {
		for (int i = 0; i < another.size(); i++) {
			another[i] = 2;
		}	for (int i = 0; i < exampleSize; i++) {
			example[i] = 2;
		}
	}

};

int main() {
	Entity e;
	std::cin.get();
}

它为我们提供了内置的获得数组大小的方法,同时他也会自动进行边界检查,防止数组越界,当然这是性能损失换来的,因此如果需要高性能,选择使用原始数组

2022/12/21 C++字符串

字符串实际上是一个接一个的一组字符,本质其实就是一个数组,因为字符使用Ascii码表示的

#include<iostream>


int main() {
	const char* name = "Cherno";
	std::cout << name << std::endl; 
	//name[2] = 'a';

}

image-20221221235046398

我们可以看到在内存中字母以Ascii码的形式储存,注意新版本的C++编译器我们不能省略const关键字不然会报错

我们也可以手动设置字符数组

#include<iostream>


int main() {
	const char* name = "Cherno";
	char name2[7] = { 'C','h','e','r','n','o' ,0};


	std::cout << name << std::endl; 
	std::cout << name2 << std::endl; 
	//name[2] = 'a';

}

添加0是终止字符,让程序知道字符数组的大小

C++11标准库也为我们提供了字符串的库String,让我们可以更方便地操作字符串

#include<iostream>
#include<string>

int main() {
	std::string name= "Cherno";

	std::cout << name << std::endl; 
	//name[2] = 'a';
	std::cin.get();
}

如果我们要将字符串输出到控制台我们就必须包含string这个头文件,因为<<操作符允许我们发送字符串的重载版本在string的头文件中

如果我们想要追加字符串有两种方式

一种是+=

#include<iostream>
#include<string>

int main() {
	std::string name= "Cherno";
	name += "hello";
	std::cout << name << std::endl; 
	//name[2] = 'a';
	std::cin.get();
}

另一种是直接在复制时进行显性转换

#include<iostream>
#include<string>

int main() {
	std::string name= std::string("Cherno")+"hello";
	//name += "hello";
	std::cout << name << std::endl; 
	//name[2] = 'a';
	std::cin.get();
}

我们不能直接相加,因为我们不能将两个指针(const char* array)进行相加

2023/01/02 C++字符串字面量

cpp中通常被双引号包裹的是字符的字面量,字符串的末尾一般都有一个\0标志着字符串的结束

Main.cpp

#include<iostream>
#include<string>

int main() {
	const char name[8] = "Cher\0no";
	std::cout << name << std::endl;
	std::cin.get();
}

image-20230102165446058

我们可以看到在内存中,字符串的字面量被\0分割,标志着字符串的结束,如果我们选择使用C标准库的函数来验证得到的也是一样的结果

image-20230102165717250

我们可以看到输出的字符数只有4,因为被\0截断了

一般来说字符的字面量是一个指向常量区的指针,修改它的值是被禁止的,但是在某些情况也是可以修改的

#include<iostream>
#include<string>

int main() {
	char name[] = "hello";
	name[2] = 'a';
	std::cout << name << std::endl;
	std::cin.get();
}

image-20230102171141063

但是我们不建议这样修改字符串的字面量,因为可能造成未定义的错误

一般而言,是禁止修改的

#include<iostream>
#include<string>

int main() {
	char* name = "hello";
	name[2] = 'a';
	std::cout << name << std::endl;
	std::cin.get();
}

image-20230102171311043

在编译时就无法通过

image-20230102171341876

一般的char类型是一字节8比特的字符,为了满足更多字符的需求,CPP提供了更大位宽的字符

#include<iostream>
#include<string>

int main() {
	const char* name = u8"Cherno";
	const wchar_t* name2 = L"Cherno";
	const char16_t* name3 = u"Cherno";
	const char32_t* name4 = U"Cherno";
}

通常来说wchar_t是两字节的,但是也不一定,还是根据编译器,在Windows下通常是2字节,在Linux和Mac是4字节的,因此如果需要指定两字节的字符,最好使用char16_t

如果想要追加字符串,我们可以使用构造函数将其中一个字符串的字面量转换为字符串

#include<iostream>
#include<string>

int main() {
	using namespace std::string_literals;
	std::string name0 = std::string("Hello") + "Cherno";
	std::cout << name0 << std::endl;
	//const char* name = u8"Cherno";
	//const wchar_t* name2 = L"Cherno";
	//const char16_t* name3 = u"Cherno";
	//const char32_t* name4 = U"Cherno";
}

但是cpp标准库为我们提供了更简单的方法

#include<iostream>
#include<string>

int main() {
	using namespace std::string_literals;
	std::string name0 = "Hello"s + "Cherno";
	std::cout << name0 << std::endl;
	//const char* name = u8"Cherno";
	//const wchar_t* name2 = L"Cherno";
	//const char16_t* name3 = u"Cherno";
	//const char32_t* name4 = U"Cherno";
}

我们可以看到结果也是正确的

image-20230102172448989

我们还可以通过R()来忽略转义字符来达到追加文本的操作

#include<iostream>
#include<string>

int main() {
	using namespace std::string_literals;
	std::string name0 = "Hello"s + "Cherno";
	std::cout << name0 << std::endl;
	const char* name1 = R"(line1
		line2
		Line3)";
	std::cout << name1 << std::endl;
	//const char* name = u8"Cherno";
	//const wchar_t* name2 = L"Cherno";
	//const char16_t* name3 = u"Cherno";
	//const char32_t* name4 = U"Cherno";
}

image-20230102172817416

可以看到结果也是正确的

也可以通过这种形式,来达到追加文本换行的效果

#include<iostream>
#include<string>

int main() {
	using namespace std::string_literals;
	std::string name0 = "Hello"s + "Cherno";
	std::cout << name0 << std::endl;
	const char* name1 = R"(line1
		line2
		Line3)";
	const char* name2 = "abc\n"
		"def\n"
		"efg\n";
	std::cout << name1 << std::endl;
	std::cout << name2 << std::endl;
	//const char* name = u8"Cherno";
	//const wchar_t* name2 = L"Cherno";
	//const char16_t* name3 = u"Cherno";
	//const char32_t* name4 = U"Cherno";
}

image-20230102173007841

0

评论区