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文件中
编写代码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
编译通过,打开项目目录
打开项目目录,会得到.i
文件,通过文本编辑器打开
我们得到如下内容
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"
编译后可以看到最后的中间产物是不变的
也就是说编译器预处理阶段,就是还会将定义的宏直接替换掉,我们再添加一些东西
#if 1
#define INTEGER int
INTEGER Multiply(INTEGER a, INTEGER b) {
INTEGER result = a * b;
return result;
#include "EndBrace.h"
#endif
编译后,发现什么都没发生,产物还是一样
我们将条件修改成# if 0
#if 0
#define INTEGER int
INTEGER Multiply(INTEGER a, INTEGER b) {
INTEGER result = a * b;
return result;
#include "EndBrace.h"
#endif
我们看到visual studio
已经将我们书写的函数变为灰色,因为编译器不会执行这段语句,我们编译查看结果
可以看到,确实是没有任何对应的语句产生
我们还原成最开始的函数语句,添加iostream
标准库,编译
#include <iostream>
int Multiply(int a, int b) {
int result = a * b;
return result;
}
可以看到编译后的文件
我们发现编译的文件大小直接从几十k上升到1000多k,打开对应的产物我们就能发现因为标准库的内容太多了,预处理阶段将里面的内容都复制黏贴到了cpp文件中
预处理的文件也来到了恐怖的60000多行
这就是编译器预处理阶段的功能
接下来我们看一看生成的对应的obj
内容是什么,我们关闭生成预处理文件的选项,再次编译,打开对应的math.obj
可以看到全部都是16进制的数据
可对于我们来说是不可读的,我们打开
将汇编程序输出改成仅有程序集的列表
点击确定后再次编译
我们得到对应的.asm
文件打开后发现对应的是可读的汇编指令,用文本编辑器打开,我们可以看到如下结果
因为我们不是直接返回a*b所以我们可以发现汇编指令中mov
了两次,我们修改代码,直接返回a*b
再次编译,可以发现两次多余的mov
消失了,也就是说如果我们不优化代码,编译器是会做多余的事情来降低我们代码的运行速度,也就是垃圾代码的运行效率低
我们可以让编译器帮我们优化代码
右键项目打开属性
选择优化,将优化改为优选速度
然后点击代码生成
将基本运行库检查设为默认,点击确定,然后编译,我们可以看到生成的汇编代码又小了一点
我们将代码简化,修改成返回5*2
我们可以发现,输出的汇编代码
仅仅是将10移动到寄存器中,这完全是没必要的因为所有的常数都是可以计算出答案的,也就是所谓的常数折叠
我们再书写一个函数
const char* Log(const char* message) {
return message;
}
int Multiply() {
Log("Multiply");
return 5*2;
}
编译后查看汇编代码
我们可以看到在乘法函数中调用了log
而函数后的一串随机字符是函数签名,用于链接器将多个obj
文件连接成一个.exe
文件时寻找函数所需要的
当我们打开编译器性能优化,编译后我们发现生成的汇编代码中对于log
的call
指令不见了,因为我们调用这个函数并没有用任何变量接收这是完全没必要的,所以编译器将它优化掉了
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();
//}
我们将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
函数
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();
}
尝试编译发现已经可以编译通过,但是当我们构建时,还是报错
我们添加对应的标准库
Log.cpp
#include <iostream>
void Log(const char* message) {
std::cout << message << std::endl;
}
发现已经可以链接通过,构建成功
当我们将Log.cpp
中的Log
函数改名为Logr
,再次编译我们发现可以编译通过,因为编译阶段并没有链接,编译器认为在某处有Log
函数,虽然我们已经将它改为了Logr
,当时当我们尝试构建,就会发现,链接错误
链接器无法找到我们在Multiply
调用的Log
函数
但是当我们将这句话注释,再次构建,就会发现没有问题
因为链接器认为你并没有使用这个函数所以就不需要链接,但换个角度,如果我们注释main
中调用的Multiply
是不是也可以呢,答案时否定的
还是出现了链接错误,因为链接器认为在这个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
关键字,将函数绑定在当前文件,再次构建就没有问题了
我们将Logr
修改回Log
,将函数定义修改为int
,添加返回值0,再次构建发现失败了
链接器链接时函数的声明和定义要一致,包括参数数量,返回值类型,不然链接仍然会失败
同时当我们在一个项目中有一个相同返回值类型,相同名字的函数,我们的链接器链接时仍然会报错,因为链接器不知道该链接哪个函数
Log.cpp
#include <iostream>
void Log(const char* message) {
std::cout << message << std::endl;
}
void Log(const char* message) {
std::cout << message << std::endl;
}
放在不同文件中时Math.cpp
一样报错,我们还可能在不经意间发生这种重复定义的错误,例如头文件,我们定义一个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
文件中
我们尝试修改 添加static
关键字,使头文件引入的函数成为每个cpp文件的内置函数,再次编译发现已经成功编译了
#pragma once
static void Log(const char* message) {
std::cout << message << std::endl;
}
或者我们可以使用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();
}
再次编译可以发现编译成功了
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中调试
- 打下断点
在debug
模式下运行
选择调试->窗口->内存->内存1,打开内存监视窗口
我们可以看到在a的这段赋值语句还没运行时,未初始化的内存4 byte显示为8个c,每两个c代表一个字节,我们可以看到string 变量未被赋值时也是8个c
单步运行也就是按下快捷键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处下断点,开始运行,转到反汇编
简单查看一下
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
目录将所有资源文件放在里面 - 设置输出文件和中间文件目录
- 删除之前已经生成的没有必要的文件
- 重新编译
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();
}
while循环
#include <iostream>
#include "Log.h"
int main() {
int i = 0;
while (i < 5) {
Log("hello world");
i++;
}
std::cin.get();
}
二者也可以等价替换,for循环三个条件不写效果等价于while
可能有的区别
还有一个do…while
循环无论如何都会执行一遍
#include <iostream>
#include "Log.h"
int main() {
do {
Log("hello world");
} while (false);
std::cin.get();
}
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,也就是该指针所指向的内存地址所保存的数据
我们修改指针所指向的内存地址存储的值,通过*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();
}
目前我们都是在栈上创建数据,下面我们在堆上创建数据
#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
指针的指针就是指向储存指针内存地址的指针
#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();
}
复制
ptr
内存地址,我们可以看到它所指向的buffer指针的内存地址98 ae 87 00
我们重新编排为00 87 ae 98 粘贴到内存地址查看器
可以得到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();
}
这是使用指针完成的
接下来使用引用
#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) {
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;
如果我们不添加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前缀时 链接器就会报错
我们需要定义一下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变量指向同一个内存地址
也就是说静态成员在所有类的实例中只有一个实例,它们指向同一块内存地址,共享内存
这种写法等价形式是
#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();
}
可以看到这样子这个结构体已经和实例没有什么关系了,一切都是静态的,我们甚至可以删掉实例化的语句,也是可以编译运行的
#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();
}
如果我们保留方法为静态的,变量为非静态的,我们仍然无法编译通过,因为静态方法无法访问非静态成员
在类中所写的每个非静态方法,总是获取当前类的实例作为隐形参数在运行时,但是静态方法无法得到这个隐藏的类的实例作为参数,静态方法和在类外部书写函数是一样的
static void Print(Entity e) {
std::cout << e.x << "," << e.y << std::endl;
}
非静态方法运行时就如上面的函数一样,需要获得一个类的实例作为参数,不然无法访问非静态成员
2022/12/19 C++中的局部静态Local Static
当我们直接在函数中定义变量,那么在每次调用后这个变量就会被销毁
而当添加static关键字,那么声明在函数中的局部变量的声明周期就会延长到整个程序结束
其等价形式是 在函数外部定义变量
但是我们在函数内使用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();
}
此时我们输出的是未被初始化的内存的值
如果我们在类外直接输出类中的公开的未初始化的变量,还是会报错
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();
}
但是新版本的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();
}
我们希望子类调用自己重写的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();
}
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();
}
Entity
和Player
实现了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();
}
我们可以看到数组在内存中连续写入了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
关键字时,一切正常,我们可以通过实例化对象的地址直接看到我们创建的数组数据
但是当我们使用new
关键字时,实例化对象指向的地址就不是数组内存地址,它包含另一个地址,而这个地址指向数组的内存地址
而这个地址 指向我们的数组地址
因此为了避免这样的内存跳跃带来的性能损失,我们需要尽量在栈上创建数组而不是堆上
同时我们无法得到数组的大小,虽然说堆上创建的数组可能可以得到,但是我们不建议这么做,因为一旦传入的变为数组指针,一切就都会发生错误。
#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
因此我们如果想用原始数组得到长度,那我们就需要自己维护数组的长度,定义一个数组长度的常量
#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';
}
我们可以看到在内存中字母以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();
}
我们可以看到在内存中,字符串的字面量被\0
分割,标志着字符串的结束,如果我们选择使用C标准库的函数来验证
得到的也是一样的结果
我们可以看到输出的字符数只有4,因为被\0
截断了
一般来说字符的字面量是一个指向常量区的指针,修改它的值是被禁止的,但是在某些情况也是可以修改的
#include<iostream>
#include<string>
int main() {
char name[] = "hello";
name[2] = 'a';
std::cout << name << std::endl;
std::cin.get();
}
但是我们不建议这样修改字符串的字面量,因为可能造成未定义的错误
一般而言,是禁止修改的
#include<iostream>
#include<string>
int main() {
char* name = "hello";
name[2] = 'a';
std::cout << name << std::endl;
std::cin.get();
}
在编译时就无法通过
一般的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";
}
我们可以看到结果也是正确的
我们还可以通过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";
}
可以看到结果也是正确的
也可以通过这种形式,来达到追加文本换行的效果
#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";
}
评论区