ChernoCPP-1

Back to Top

01. Welcome to C++

C++ for hardware, C++ for game engines

02-04. Setup C++ in different OS

05. How C++ works

preprocessor statement
head file
main function: entry point

source file(Main.cpp) –compiler–> object files(Main.obj) –linker–> executable file(Main.exe) (Windows platform)

source file(file1.cpp, file2.cpp) –compiler–> object files(file1.obj, file2.obj) –linker–> executable file(file.exe) (Windows platform)

declaration and defination

06. How the C++ Compiler Works

text form to an actual executable binary

  • compiling(cpp->obj)
  • linking(obj->exe)

translation unit

preprocessor

How ‘#include ‘ works

All the compiler did was open the header file and copy whatever was in here. Let’s see a simple example.

1
2
// EndBrace.h
}
1
2
3
4
5
6
//Math.cpp
int math(int a, int b)
{
int result = a * b;
retrun result;
#include "EndBrace.h"
1
2
3
4
5
6
#include <xx>
#include "xx"
#define A B

#if
#endif

what’s actually inside the obj file

function signature

the complier’s work: It takes the source files and output an object file which contains machine code and any other constant data that we’ve defined.

07. How the C++ Linker Works

1
2
3
4
5
6
//Math.cpp
static int math(int a, int b)
{
int result = a * b;
retrun result;
}

08. Variables in C++

image-20231208164738792

int -> 4 Bytes(4×8=32 bits) -> (-$2^31$)~($2^31-1$)
unsigned int 4 Bytes(4×8=32 bits) -> $2^32$
char
short
long
long long
float -> float var = 5.5f;
double -> double var = 5.5;
bool -> 1 Byte (Although it only need 1 bit, 但在从内容中获取bool类型的数据是时候,我们没法寻址到每个bit,这能寻址到每个Byte). 0 means false and any other digits mean true. 为了节约这个内存空间,我们可以把8个bool类型的变量放在1个Byte的内存中,但这个是高级的操作了。

sizeof()

Variable

pointer: int* a;

reference: int& a;

09. Functions in C++

function and method

return value

10. C++ header file

.cpp and .h

所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码。但头文件不用被编译。我们把所有的函数声明全部放进一个头文件中,当某一个 .cpp 源文件需要它们时,它们就可以通过一个宏命令 “#include” 包含进这个 .cpp 文件中,从而把它们的内容合并到 .cpp 文件中去。当 .cpp 文件被编译时,这些被包含进去的 .h 文件的作用便发挥了。

理解 C++ 中的头文件和源文件的作用 | 菜鸟教程 (runoob.com)

1
2
3
4
#pragma once

任何一个以‘#’开始的语句都被称作预处理语句,‘pragma’是一个被输入到编译器或者是预处理器的指令,‘pragma once’意思是这说只include这个文件一次。
‘pragma once’被称为为‘header guard’(头文件保护符),其作用是防止我们把单个头文件多次include到一个单一translation unit里。

下面这两种写法等价,都是起到了头文件保护作用。前者是现在常用是格式,后者是之前的代码常用的格式

1
2
3
4
5
6
7
// Log.h

#pragma once

void InitLog();
void Log(const char* message);
struct Player {};
1
2
3
4
5
6
7
8
// Log.h
#ifdef _LOG_H_
#define _LOG_H_

void InitLog();
void Log(const char* message);
struct Player {};
#endif

#include & #include “xx.h”
<>只用于编译器的include路径,而””可以用于所有。

iostream这个东西看起来不想是文件呀?
iostream是一个文件,只不过没有拓展名,是写标准库的人决定这么去干的,为了区分C的标准库和C++的标准库。C标准库的头文件中一般都有‘.h’的拓展名,而C++的没有。

11. How to DEBUG C++ in VISUAL STUDIO

Breakpoints & Reading memory

12. CONDITIONS and BRANCHES in C++(if statements)

if and else

else if其实并不是一个关键词,而是else和if的一个组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1
if (ptr)
Log(ptr);
else if (ptr = "Hello")
Log("Ptr is Hello");


// 2
if (ptr)
Log(ptr);
else
{
if (ptr = "Hello")
Log("Ptr is Hello");
}

// 1和2实际上等价

13. BEST Visual Studio Setup for C++ Projects!

Virtual folder

Visual Studio项目中的文件夹是虚拟文件夹,起到一种筛选器的作用。

VS C++项目目录更改
Output Directory
Intermediate Directory

bin means binary

  • Output Directory: $(SolutionDir)bin$(Platform)$(Configuration)\
  • Intermediate Directory: $(SolutionDir)bin\intermediate$(Platform)$(Configuration)\

什么是 Visual Studio 解决方案和项目? - Visual Studio (Windows) | Microsoft Learn

14. Loops in C++ (for loops, while loops)

for loops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	for (int i = 0; i < 5; i++)
{
cout << "hello" << endl;
}
// step1, 定义变量i并赋值为0
// step2, 判断‘i < 5’是否成立,若成立,则进入循环体;若不成立,则退出循环。
// step3,执行到“}”时候,执行‘i++’。然后重复2和3。

// 等价写法
int i = 0;
for (; i < 5; )
{
cout << "hello" << endl;
i++;
}

while loops

1
2
3
4
5
6
int i = 0;
while (i < 5)
{
cout << "hello" << endl;
i++;
}

for loops和while loops怎么选择,这两个基本上一样,选择哪个,主要取决于是否需要新变量(当然也无所谓)。for loops中for (int i = 0; i < 5; i++)i是临时变量,跳出循环后i就没有定义了,而在while loops中,i是在循环体之外定义的,所有跳出while loops时,i依然有定义,其值是跳出while loops时i的数值。

do while loops

至少会执行循环一次。

1
2
3
4
5
6
int i = 0;
do
{
cout << "hello" << endl;
i++;
} while (i < 5);

15. Control Flow in C++ (break, continue, return)

continue: loops

1
2
3
4
5
6
7
8
9
10
11
  for (int i = 0; i < 5; i++)
{
if (i % 2 != 0)
continue;
cout << "i = " << i << endl;
}
/*
i = 0
i = 2
i = 4
*/

break: loops and switch

1
2
3
4
5
6
7
8
9
10
  for (int i = 0; i < 5; i++)
{
if (i % 2 != 0)
break;
cout << "i = " << i << endl;
}

/*
i = 0
*/

return: get out of the function entirely

16. Pointer in C++ ⭐

  • raw pointer(原始指针) ✔
  • smart pointer(智能指针)

Computer deal with memory. Memory is everything to a computer.

指针用于管理和操控内存。

A pointer is an integer, a number which stores a memory address. That is all that is!

1
2
3
void* ptr = 0; // 事实上这里的0并不是一个有效的地址,0就是个整数,符合上面说的那句话。
void* ptr = NULL; //与上面的语句等价,因为C++中NULL的定义就是‘#define NULL 0’
void* ptr = nullptr; // C++ 11中引入的新特性

#define NULL O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在stack上创建数据

int var1 = 5;
int* ptr1 = &var1;
cout << "the memory address of var1 is " << ptr1 << endl;
*ptr1 = 10;
cout << "the value stored in memory address of var1 is " << *ptr1 << endl;


int var2 = 6;
int* ptr2;
ptr2 = &var2;
*ptr2 = 12;
cout << "the memory address of var2 is " << ptr2 << endl;
cout << "the value stored in memory address of var2 is " << *ptr2 << endl;

//the memory address of var1 is 00DBFAB4
//the value stored in memory address of var1 is 10
//the memory address of var2 is 00DBFA9C
//the value stored in memory address of var2 is 12

首先说明,在C++中,内存分为5个区:堆、占、自由存储区、全局/静态存储区、常量存储区

  • :是由编译器在需要时自动分配,不需要时自动清除的变量存储区。通常存放局部变量、函数参数等。
  • :是由new分配的内存块,由程序员释放(编译器不管),一般一个new与一个delete对应,一个new[]与一个delete[]对应。如果程序员没有释放掉,资源将由操作系统在程序结束后自动回收。
  • 自由存储区:是由malloc等分配的内存块,和堆十分相似,用free来释放。
  • 全局/静态存储区:全局变量和静态变量被分配到同一块内存中(在C语言中,全局变量又分为初始化的和未初始化的,C++中没有这一区分)。
  • 常量存储区:这是一块特殊存储区,里边存放常量,不允许修改。
    (注意:堆和自由存储区其实不过是同一块区域(这句话是有问题的,下文解释),new底层实现代码中调用了malloc,new可以看成是malloc智能化的高级版本,详情参见new和malloc的区别及实现方法, 以及这一篇)

C++中堆(heap)和栈(stack)的区别(面试中被问到的题目)_c++堆和栈的区别-CSDN博客

1
2
3
4
char* buffer = new char[8]; //分配一个8字节的内存,并返回这块内存的开始地址给指针。
memset(buffer, 0, 8); //使用“0”数值填充buffer只想的内存,其内存大小为8 Bytes.

delete[] buffer;

new char[8]?

double pointer:双重指针(指针变量的指针,用一个指针变量b存储一个指针变量a的地址)

1
char** ptr = &buffer;

17. Reference in C++

在计算机如歌处理这两种关键字的角度看,指针和引用基本上是一回事。
引用是基于指针的一种(syntax sugar),来使得代码更易读更好学。

语法糖就相当于汉语里的成语。即,用更简练的言语表达较复杂的含义。在得到广泛接受的情况之下,可以提升交流的效率。

之所以叫【语法糖】,不只是因为加糖后的代码功能与加糖前保持一致,更重要的是,糖在不改变其所在位置的语法结构的前提下,实现了运行时等价。可以简单理解为,加糖后的代码编译后跟加糖前一毛一样。
之所以叫【语法糖】,是因为加糖后的代码写起来很爽,包括但不限于:代码更简洁流畅,代码更语义自然. 写得爽,看着爽,就像吃了糖。效率高,错误少,老公回家早…
PS: 据说还有一种叫做【语法盐】的东西,主要目的是通过反人类的语法,让你更痛苦的写代码其实它同样能达到避免代码书写错误的效果,但编程效率应该是降低了,毕竟提高了语法学习门槛,让人咸到忧伤…

什么是语法糖? - 知乎 (zhihu.com)

引用是指对某个已存在的变量的引用。

1
2
3
4
5
  int a = 5;
int* b = &a; // 这里的&不是引用,而是取变量a的地址。
int& ref = a; // 这里的&是引用,&紧跟着变量类型,&是变量类型的一部分。

// 不是出现了&符号就是引用。做好取地址和引用的区分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	int a = 5;
int& ref = a; // 创建了一个alias(别名)
// ref并不是一个变量,而是变量a的一个别名。ref只存在于源码中,编译器编译时,只有a这一个变量。



int a = 5;
int& ref = a;
ref = 10;

cout << "a = " << a << endl;
cout << "ref = " << ref << endl;

// a = 10
// ref = 10

举个例子!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

void IncreaseValue(int value)
{
value++;
}
int main()
{
int a = 5;
IncreaseValue(a);
cout << "a = " << a << endl;
cin.get();
}

// a = 5

// value的数值增加了,但这里的value只是形式参数,value的值的改变并不会影响实际参数a的数值。

要想使用函数把实参a的值进行改变,可以使用指针的方式来实现!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

void IncreaseValue(int* value)
{
(*value)++; // 这里要考虑多个运算符号的运算先后顺序
}
int main()
{
int a = 5;
IncreaseValue(&a);
cout << "a = " << a << endl;
cin.get();
}

// a = 6

// 使用指针的方法,传到IncreaseValue函数中的不是变量a的值,而是变量a的地址。
// 使用*value解引用,把改内存地址上的数值增加,从而改变了变量a的值。

使用指针可以改变实参a的值,但是使用引用能更方便的实现此功能!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

void IncreaseValue(int& value)
{
value++;
}
int main()
{
int a = 5;
IncreaseValue(a);
cout << "a = " << a << endl;
cin.get();
}

// a = 6

// int& value = a; 所以value的变化等价于a的变化。

注意:一旦声明了一个引用,就不能更改它所引用的对象了!

1
2
3
4
5
6
7
8
9
10
11
  int a = 5;
int b = 10;

int& ref = a;
ref = b;

cout << "a = " << a << endl;

// a = 10

// ref已经作为变量a的引用了,‘ref = b;’不能把ref作为b的引用,只是把b的数值赋值给了ref,也就是赋值给了a。

另外,因为ref并不是一个实际的变量,声明ref的时候必须立刻将其作为一个真正变量的引用!

1
2
3
4
5
6
7
8
  int a = 5;

int& ref; // 这是非法的,必须立刻声明ref为真正变量的引用!
ref = a;

// 1>F:\MicrosoftVisualStudio\Microsoft Visual Studio\MSBuild\Microsoft\VC\v170\Microsoft.CppCommon.targets(741,5): error MSB6006: "CL.exe" exited with code 2.
// 1>E:\userDoc\ChernoDevCPP\NewProject\NewProject\src\Main.cpp(7,10): error C2530: 'ref': references must be initialized
// 1>Done building project "NewProject.vcxproj" -- FAILED.

18. Classes in C++⭐

Object-Oriented Programming(OOP)

Class and Object(类与对象)

  • C++支持:面向过程、面向对象、基于对象、泛型编程四种类型的编程;
  • C不支持米那些对象编程;
  • JAVA, C#只适合面向对象编程(不是不可以其他风格,只是最好编写面向对象编程风格的程序)

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

在面对很多很多变量的时候,使用class能使得代码更简洁和方便维护。

由类类型定义的变量叫做对象(object),创建新对象的过程叫做实例化(instance)。

visibility(访问控制)

visibility(访问控制)

默认情况下,类中的成员的访问控制都是私有的,意味着只有类内部的函数才能方位这些变量。

public

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
#include <iostream>
using namespace std;

class Player
{
public: // 公有,表示允许在类外访问这些变量
int x, y;
int speed;
};

// 类外的函数
void Move(Player& player, int xa, int ya)
{
player.x += xa * player.speed;
player.y += ya * player.speed;
}

int main()
{
Player player1;

player1.x = 0;
player1.y = 0;
player1.speed = 10;

Move(player1, 1, -1);

cout << " x = " << player1.x << endl;
cout << " y = " << player1.y << endl;

cin.get();
}


为了使得代码更简洁,可以把函数写到类内,作为方法。这样可以使得当我们为特定的类调用Move函数的时候就是调用他自己的。

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
#include <iostream>
using namespace std;

class Player
{
public: // 公有,表示允许在类外访问这些变量
int x, y;
int speed;

// 在类内定义函数
void Move(int xa, int ya) // 类内的函数我们叫它为"方法"(methods)
{
x += xa * speed;
y += ya * speed;
}
};


int main()
{
Player player1;

player1.x = 0;
player1.y = 0;
player1.speed = 10;

player1.Move(1, -1);

cout << " x = " << player1.x << endl;
cout << " y = " << player1.y << endl;

cin.get();
}

类也是一种语法糖。

19. Classes vs Struct in C++

C++中Class和Struct有什么区别?

  • 基本上没什么区别😅
  • 使用 class 时,类中的成员默认都是 private 属性的,而使用 struct 时,结构体中的成员默认都是 public 属性的.
  • C++中struct存在的唯一原因是因为它想要维持与C之间的兼容性,因为C中没有类但有结构体。如果把C++中的struct删除之后,C++与C存在兼容性问题。
  • C++中class与struct的使用,主要还是有个人编程风格决定吧。
    • 在讨论Plain Old Data(POD)时候,使用struct; 在讨论比较复杂功能的时候,使用class;
    • 在使用继承的时候,使用class;

C 语言 中,**结构体** 只能存放一些 变量 的集合,并不能有 **函数**,但 C++ 中的结构体对 C 语言中的结构体做了扩充,可以有函数,因此 C++ 中的结构体跟 C++ 中的类很类似。C++ 中的 struct 可以包含成员函数,也能继承,也可以实现多态。

但在 C++ 中,使用 class 时,类中的成员默认都是 private 属性的,而使用 struct 时,结构体中的成员默认都是 public 属性的。class 继承默认是 private 继承,而 struct 继承默认是 public 继承。

C++ 中的 class 可以使用模板,而 struct 不能使用模板。

C++ class和struct区别-C++类与结构体区别-嗨客网 (haicoder.net)

POD 是 Plain Old Data 的缩写,是 C++ 定义的一类数据结构概念,比如 int、float 等都是 POD 类型的。Plain 代表它是一个普通类型,Old 代表它是旧的,与几十年前的 C 语言兼容,那么就意味着可以使用 memcpy() 这种最原始的函数进行操作。两个系统进行交换数据,如果没有办法对数据进行语义检查和解释,那就只能以非常底层的数据形式进行交互,而拥有 POD 特征的类或者结构体通过二进制拷贝后依然能保持数据结构不变。也就是说,能用 C 的 memcpy() 等函数进行操作的类、结构体就是 POD 类型的数据

什么是 POD 数据类型? - 知乎 (zhihu.com)

20. How to write a C++ Class

Log Class: error, warning and message or trace.

插一个VS使用小技巧,如何让VS和VSCode一样有代码预览窗口。

image-20231119164236488
image-20231119164315161
image-20231119164327659
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
#include <iostream>
using namespace std;

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 Error(const char* message)
{
if (m_LogLevel >= LogLevelError)
cout << "[ERROR]:" << message << endl;
}
void Warn(const char* message)
{
if (m_LogLevel >= LogLevelWarning)
cout << "[WARNING]:" << message << endl;
}
void Info(const char* message)
{
if (m_LogLevel >= LogLevelInfo)
cout << "[INFO]:" << message << endl;
}
};

// Log类中出现了两个public,只是因为这是一种个人的编程风格。把公共变量放在一部分,把公共方法放在一部分……

int main()
{
Log log;

log.SetLevel(log.LogLevelError);

log.Error("Hello error!");
log.Warn("Hello warning!");
log.Info("Hello info!");

cin.get();
}

21. Static in C++

Static这部分从21~23

Static这部分从21~23

  • 类或结构体内的静态变量
  • 类或结构体外的静态变量

static 是 C/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性。

static 关键字用来解决全局变量的访问范围问题

  • (1)在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • (2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • (3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
  • (4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
  • (5)考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。

C/C++ 中 static 的用法全局变量与局部变量 | 菜鸟教程 (runoob.com)

22. Static for Classes and Struct in C++

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
#include <iostream>
using namespace std;

struct Entity
{
int num;

void Print()
{
cout << num << endl;
}
};

int main()
{
Entity e1;
e1.num = 2;

Entity e2;
e2.num = 5;

cout << e1.num << endl; // 2
cout << e2.num << endl; //5

cout << &(e1.num) << endl; // 008FFBC4
cout << &(e2.num) << endl; // 008FFBB8

cin.get();
}

上面这段代码比较容易理解,e1和e1是结构体Entity的两个不同的实例,不同实例中的num是不同的变量,我们从两个变量的地址也可以看得出来。

如果把结构体Entity的变量变为static类型的话,情况又有什么不一样呢?

1
2
3
4
5
# 仅仅把‘int num;’改为‘static int num;’可以吗?
# 不可以!在‘ctrl+F7’编译单个代码文件时成功了,但是在允许代码时候会有“链接错误”。

09:36:12:204 1>Main.obj : error LNK2001: unresolved external symbol "public: static int Entity::num" (?num@Entity@@2HA)
09:36:12:250 1>E:\userDoc\ChernoDevCPP\NewProject\bin\Win32\Debug\NewProject.exe : fatal error LNK1120: 1 unresolved externals

要解决这个问题,我们必须在代码中定义这些静态变量,像这样,

1
int Entity::num;

现在代码能运行了,看一下结果

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
#include <iostream>
using namespace std;

struct Entity
{
static int num;

void Print()
{
cout << num << endl;
}
};

int Entity::num; // 定义这些静态变量,让链接器能连接到这些变量

int main()
{
Entity e1;
e1.num = 2;

Entity e2;
e2.num = 5;

cout << e1.num << endl; // 5
cout << e2.num << endl; // 5

cout << &(e1.num) << endl; // 00BCA138
cout << &(e2.num) << endl; // 00BCA138

cin.get();
}

所以,e1.nume2.num本质上都是同一个变量,所以这样的写法是没有意义的。可以写成如下的形式,

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
#include <iostream>
using namespace std;

struct Entity
{
static int num;

void Print()
{
cout << num << endl;
}
};

int Entity::num;

int main()
{
Entity e1;
Entity::num = 2;

Entity e2;
Entity::num = 5;

cout << Entity::num << endl; // 5
cout << Entity::num << endl; // 5

cout << &(Entity::num) << endl; // 00BCA138
cout << &(Entity::num) << endl; // 00BCA138

cin.get();
}

23.Local Static in C++⭐

  • 生命周期(lifetime)
  • 作用域(scope)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

void Function()
{
int i = 0;
i++;
cout << "i = " << i << endl;
}

int main()
{
Function();
Function();
Function();

// cout << "main: i = " << i << endl; // Compilation error
cin.get();
}
//i = 1
//i = 1
//i = 1

// i是定义在函数Function中的变量,其作用域和生存时间都是在函数Function内部,从main函数中就是访问不到变量i了。
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
#include <iostream>
using namespace std;

int i = 0;

void Function()
{
i++;
cout << "i = " << i << endl;
}

int main()
{
Function();
Function();
Function();

cout << "main: i = " << i << endl;
cin.get();
}

// i = 1
// i = 2
// i = 3
// main: i = 3

// i定义在了函数外部,所以是个全局变量,其作用域和生存时间都是在整个程序中,这样就可以在main函数中访问到了i。
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
#include <iostream>
using namespace std;

void Function()
{
static int i = 0;
i++;
cout << "i = " << i << endl;
}

int main()
{
Function();
Function();
Function();

// cout << "main: i = " << i << endl; // Compilation error
cin.get();
}


//i = 1
//i = 2
//i = 3

// static声明的i是静态变量,这样的效果和全局变量类似,但是并不能在所有函数中访问到i,i的作用域仅在其所定义的函数内部。

24. Enums in C++

枚举类型的定义:枚举类型(enumeration)是 C++ 中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。

1
enum <类型名> {<枚举常量表>};

C++ 枚举类型详解 | 菜鸟教程 (runoob.com)

25. Constructors in C++

Constructors是一种特殊的method,它在实例化时被调用。

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

C++ 类构造函数 & 析构函数 | 菜鸟教程 (runoob.com)

  1. 对于一个类,在实例化之后,如果直接调用类内的变量,会用链接错误,因为类内的变量未被初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Entity
{
public:
float X, Y;

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

int main()
{
Entity e;
cout << e.X << ", " << e.Y << endl; // linking error
e.Print();

cin.get();
}
// error C4700: uninitialized local variable 'e' used
  1. 手动初始化。在类内定义一个初始化函数,把类内的变量初始化一个值,这样就不会有链接错误了。但这样不够优雅,在类有多个实例化时,需要每次实例化之后都使用这个初始化函数。而C++提供了更优雅有效的方式,就是构造函数(Constructors)
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
#include <iostream>
using namespace std;

class Entity
{
public:
float X, Y;

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

void Init()
{
X = 0.0f;
Y = 0.0f;
}
};

int main()
{
Entity e;
e.Init();
cout << e.X << ", " << e.Y << endl;
e.Print();

cin.get();
}
  1. Constructors是一种特殊的method,它在实例化时被调用以初始化实例。
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
#include <iostream>
using namespace std;

class Entity
{
public:
float X, Y;

Entity() //Constructor method的名字和类名相同,没有返回值
{
X = 0.0f;
Y = 0.0f;
}
void Print()
{
cout << X << ", " << Y << endl;
}
};

int main()
{
Entity e;
cout << e.X << ", " << e.Y << endl;
e.Print();

cin.get();
}

在C++中其实有一个默认的Constructor,但是它本身不做任何事情,方法内部是空的,就像这样

1
2
3
4
Entity() //Constructor method的名字和类名相同,没有返回值
{

}

因此,C++不能自动帮我们初始化内存空间,得自己手动完成这个过程。

  1. 含参数的构造函数。3中,类中变量的初始化数值是在类中写定的,使用含参数的构造函数能在实例化的时候确定类中变量的初始化数值,如下
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
#include <iostream>
using namespace std;

class Entity
{
public:
float X, Y;

Entity(float x, float y)
{
X = x;
Y = y;
}
void Print()
{
cout << X << ", " << Y << endl;
}
};

int main()
{
Entity e(5.0f, 6.0f);
cout << e.X << ", " << e.Y << endl;
e.Print();

cin.get();
}

26. Destructors in C++

  • Constructor:构造函数
  • Destructors: 析构函数

构造函数通常是设置变量的地方启动或执行所需要执行的任何类型的初始化,类似的,析构函数是取消初始化任何内容的地方你可能需要删除或清楚任何已使用的内存。

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
#include <iostream>
using namespace std;

class Entity
{
public:
float X, Y;

Entity() // 构造函数
{
X = 0.0f;
Y = 0.0f;
cout << "Created Entity!" << endl;
}

~Entity() // 析构函数
{
cout << "Destroyed Entity!" << endl;
}


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

void Function()
{
Entity e;
e.Print();
}
int main()
{
Function();
cin.get();
}


/*
Created Entity!
0, 0
Destroyed Entity!


E:\userDoc\ChernoDevCPP\NewProject\bin\Win32\Debug\NewProject.exe (process 22128) exited with code 0.
Press any key to close this window . . .
*/

析构函数在类的实例化是生命周期末期被调用!
如上面的代码,“Entity e;”创造了一个实例化e,这时候调用构造函数,函数“Function()”是实例化的作用域,“Function()”函数结束的时候,调用了析构函数。
如果不使用析构函数,可能会导致内存泄漏。

27. Inheritance in C++⭐

继承提供了一种来实现把多个类之间的公共代码转换为基类的方式,就像是一种模板。

Polymorphic(多态)

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
#include <iostream>
using namespace std;

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()
{
cout << Name << endl;
}
};


int main()
{
Player player;

player.Name = "Tom";

player.X = 0;
player.Y = 0;
player.Move(5, 5);

player.PrintName();

cin.get();
}

继承 (inheritance) 就是在一个已存在的类的基础上建立一个新的类.

  • 已存在的类: 基类 (base class) 或父类 (father class)
  • 新建立的类: 派生类 (derived class) 或子类 (son class)
  • 一个新类从已有的类获得其已有特性, 称为类的继承.
  • 通过继承, 一个新建的子类从已有的父类那里获得父类的特性
  • 派生类继承了基类的所有数据成员和成员函数, 并可以对成员做必要的增加或调整

从已有的类 (父类) 产生一个新的子类, 称为类的派生.

  • 类的继承是用已有的类来建立专用新类的编程技术
  • 一个基类可以派生出多个派生类, 每一个派生类又可以作为基类再派生出新的派生类. 因此基类和派生类是相对而言的
  • 派生类是基类的具体化, 而基类则是派生类的抽象

版权声明:本文为CSDN博主「我是小白呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46274168/article/details/11659272

多态

多态polymorphism,基本上来说就是使用一个单一的符号来表示多个不同的类型

28. Virtual functions in C++⭐

Virtual functions(虚函数)

虚函数允许我们覆盖基类中的方法。

虚函数引入了一种动态分配(Dynamic Dispatch)的东西,通常使用VTable(虚函数表)来实现编译。VTable中包含基类中所有虚函数的映射,以便我们能在运行时映射它们向正确的覆写函数。

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
#include <iostream>
#include <string>
using namespace std;

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

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

string GetName() { return m_Name; }
};

void PrintName(Entity* entity)
{
cout << entity->GetName() << endl;
}
int main()
{
Entity* e = new Entity();
PrintName(e);

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

cin.get();
}

// Entity
// Entity

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
#include <iostream>
#include <string>
using namespace std;

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

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

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

void PrintName(Entity* entity)
{
cout << entity->GetName() << endl;
}
int main()
{
Entity* e = new Entity();
PrintName(e);

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

cin.get();
}

// Entity
// Cherno

29. Interfaces in C++(Pure Virtual Functions)⭐

Pure Virtual Functions(纯虚函数),C++中的纯虚函数的本质上犹如Java和C#中的抽象方法和接口
原理上来讲,纯虚函数允许我们定义一个在基类中没有实现的函数,然后迫使在子类中实际实现,

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
51
#include <iostream>
#include <string>
using namespace std;

class Printable
{
public:
virtual string GetClassName() = 0; // pure virtual function
};

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

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

string GetName() override { return m_Name; }
string GetClassName() override { return "Palyer"; }
};

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

void Print(Printable* obj)
{
cout << obj->GetClassName() << endl;
}

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

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

Print(e);
Print(p);

cin.get();
}

30. Visibility in C++

private, protected, public

  • private

    私有(private)成员

私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有友元函数可以访问私有成员。

默认情况下,类的所有成员都是私有的。

C++ 类访问修饰符 | 菜鸟教程 (runoob.com)

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
#include <iostream>
#include <string>
using namespace std;

class Entity
{
// int X, Y; // 如果类中定义的变量没有指定visibility,则默认就是private。
private:
int X;
public:
Entity()
{
X = 0; // right!
}
};

class Player : public Entity
{
public:
Player()
{
// X = 10; // error!
}
};
int main()
{
Entity e;
// e.X = 10; // error!
cin.get();
}
  • protected

protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。

C++ 类访问修饰符 | 菜鸟教程 (runoob.com)

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
#include <iostream>
#include <string>
using namespace std;

class Entity
{
protected:
int X;
void Print() {}
public:
Entity()
{
X = 0; // right!
Print(); // right!
}
};

class Player : public Entity
{
public:
Player()
{
X = 10; // right!
Print(); // right!
}
};


int main()
{
Entity e;
//e.X = 10; // error!
cin.get();
}
  • public

公有成员在程序中类的外部是可访问的。

C++ 类访问修饰符 | 菜鸟教程 (runoob.com)

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
#include <iostream>
#include <string>
using namespace std;

class Entity
{
public:
int X;
void Print() {}
public:
Entity()
{
X = 0; // right!
Print(); // right!
}
};

class Player : public Entity
{
public:
Player()
{
X = 10; // right!
Print(); // right!
}
};


int main()
{
Entity e;
e.X = 10; // right!
e.Print(); // right!
cin.get();
}

31. Arrays in C++

Array and Pointer

1
2
3
4
5
6
7
8
9
10
11
12
int example[5];
int* ptr = example;

example[2] = 10;
cout << "example[2] =" << example[2] << endl;

*(ptr + 2) = 20;
cout << "example[2] =" << example[2] << endl;

*(int*)((char*)ptr + 8) = 30; // 两次强制类型转换
cout << "example[2] =" << example[2] << endl;
# 这三行代码等价!ptr+2中的“2”并不是数值2,指针+2的时候会自动根据数据类型来计算实际的字节数。

Stack and Heap

Stack:

  1. 和堆一样存储在计算机 RAM 中。
  2. 在栈上创建变量的时候会扩展,并且会自动回收。
  3. 相比堆而言在栈上分配要快的多。
  4. 用数据结构中的栈实现。
  5. 存储局部数据,返回地址,用做参数传递。
  6. 当用栈过多时可导致栈溢出(无穷次(大量的)的递归调用,或者大量的内存分配)。
  7. 在栈上的数据可以直接访问(不是非要使用指针访问)。
  8. 如果你在编译之前精确的知道你需要分配数据的大小并且不是太大的时候,可以使用栈。
  9. 当你程序启动时决定栈的容量上限。

Heap:

  1. 和栈一样存储在计算机RAM。
  2. 在堆上的变量必须要手动释放,不存在作用域的问题。数据可用 delete, delete[] 或者 free 来释放。
  3. 相比在栈上分配内存要慢。
  4. 通过程序按需分配。
  5. 大量的分配和释放可造成内存碎片。
  6. 在 C++ 中,在堆上创建数的据使用指针访问,用 new 或者 malloc 分配内存。
  7. 如果申请的缓冲区过大的话,可能申请失败。
  8. 在运行期间你不知道会需要多大的数据或者你需要分配大量的内存的时候,建议你使用堆。
  9. 可能造成内存泄露。

什么是堆? 什么是栈? - 知乎 (zhihu.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int arr1[5]; // an array created on the stack
for (int i = 0; i < 5; i++)
{
arr1[i] = i;
}

int* arr2 = new int[5]; // an array created on the heap
for (int i = 0; i < 5; i++)
{
arr2[i] = i;
}
delete[] arr2;

// arr1的生命周期结束后,内存空间会被自动回收。
// arr2没有生命周期的概念,必须手动来释放。

这里发现了一个新且有趣的知识点!在stack上定义的变量,自动初始化为“cccc”,而在heap上定义的变量,是自动初始化为“cdcd”,不知道是为什么会这样??

image-20231208163944488
image-20231208164302702

C++11 standard array

  1. size of array

在原生数组中,计算数组的大小使用sizeof()方法,但是这种方法也仅仅适用于定义在stack上的数组;对于定义在heap上的数组,使用sizeof()后,返回值是指针的大小,下面的例子中,返回值是4,即整型类型的指针的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Entity
{
public:
int array1[5];
int* array2 = new int[5];

Entity()
{
int count = sizeof(array1) / sizeof(int); // 4*5 / 4 = 5
cout << "count of array1 is " << count << endl;

count = sizeof(array2) / sizeof(int); // 4 / 4 = 1
cout << "count of array2 is " << count << endl;

for (int i = 0; i < count; i++)
{
array1[i] = i;
array2[i] = i;
}

}
};

需要注意的是,当定义一个stack上的数组的时候,数组的大小必须是在编译时就需要注意的常量!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// error!
int array1Size = 5;
int array1[array1Size];

// error!
const int array1Size = 5;
int array1[array1Size];

// 使用static方法!
// right!
static const int array1Size = 5;
int array1[array1Size];

// 使用constexpr方法!
// 没搞懂?❓🎯
  1. std::array
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <array>
using namespace std;

int main()
{
array<int, 5> array1;

for (int i = 0; i < array1.size(); i++)
{
array1[i] = i;
}

cin.get();
}

32. How Strings Work in C++

String, Pointer, Array, Memory address

String is a group of characters

ascii-table

image-20231215144322539

字符串结束的标志”\0“,在内存中存储的就是0。

image-20231215145247579

const char* name = "Cherno";

这声明了一个指向常量字符的指针。这意味着指针name指向的字符串内容是不可修改的。你可以通过name指针读取字符串,但是尝试通过name指针修改字符串的内容将导致编译错误。

1
2
3
4
5
const char* name = "Cherno";
// 可以读取字符串
char firstChar = name[0];
// 但不能修改字符串
// name[0] = 'X'; // 这会导致编译错误

char* name = "Cherno";

这声明了一个指向字符的指针,但没有使用const。这意味着指针name指向的字符串内容是可修改的。然而,这在 C++ 中是不安全的,因为字符串常量(像 “Cherno”)通常存储在只读的内存区域,尝试修改它们可能导致未定义的行为。

1
2
3
char* name = "Cherno";
// 尽管没有编译错误,但修改字符串是不安全的
// name[0] = 'X'; // 可能导致未定义的行为

总的来说,如果你知道字符串不会被修改,最好使用第一个声明,即带有const的版本,以提高代码的安全性。如果你确实需要修改字符串,最好将字符串复制到一个可修改的内存区域,例如使用char[]数组:

char name[] = "Cherno";

1
2
char name[] = "Cherno"; // 可以修改
name[0] = 'X'; // 安全

image-20231215150357514

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main()
{
// 用pointer定义一个字符串,使用const意味着这个字符串不能修改
const char* name = "Cherno";
// char* name = "Cherno"; // 不要用这种写法,
std::cout << name << std::endl;

// char* name1 = "Cherno"; // 不要用这种写法,
// std::cout << name1 << std::endl;

// 用array定义一个字符串,可以修改
// char name2[6] = { 'C', 'h', 'e', 'r', 'n','o' }; // error!
char name2[7] = { 'C', 'h', 'e', 'r', 'n','o', '\0' }; // '\0': null termination character

name2[0] = 'A';
std::cout << name2 << std::endl;

std::cin.get();
}

Standard string (std::string)

  1. 字符串定义与字符串函数。使用string定义的字符串变量其本质还是const char* name.
1
2
3
4
5
std::string name = "Cherno";
cout << name << endl;

std::cout << name.size() << std::endl; // 6
std::cout << name.find("no") << std::endl; // 4, 第一次出现“no”时候的索引
  1. 字符串拼接
1
2
3
4
5
6
7
8
9
10
// string name = "Cherno" + " hello"; // wrong!

// method 1
std::string name1 = "Cherno1";
name1 += " hello";
std::cout << name1 << std::endl;

// method 2
std::string name2 = std::string("Cherno2") + " hello";
std::cout << name2 << std::endl;
  1. 字符串作为函数参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>

void PrintString(std::string onename)
{
onename += " hello";
std::cout << onename << std::endl;
}

int main()
{
std::string name = "Cherno";
PrintString(name); // Cherno hello
std::cout << name << std::endl; // Cherno

std::cin.get();
}

std::string onename是对std::string name = "Cherno"的复制,在PrintString函数中对onename做出的修改,实际上并不会影响原来name的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>

void PrintString(std::string& onename) // 传引用的话情况就会不一样了
{
onename += " hello";
std::cout << onename << std::endl;
}

int main()
{
std::string name = "Cherno";
PrintString(name); // Cherno hello
std::cout << name << std::endl; // Cherno hello

std::cin.get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>

void PrintString(const std::string& onename) // 如果有const的话,又是另外一种情况,这时候,onename即name的值不能改变
{
// onename += " hello";
std::cout << onename << std::endl;
}

int main()
{
std::string name = "Cherno";
PrintString(name); // Cherno
std::cout << name << std::endl; // Cherno

std::cin.get();
}

33. String Literals in C++

1. 字符串字面量

生成自ChatGPT

字符串字面量(String literals)是在源代码中直接表示字符串值的一种方式。在C++中,字符串字面量通常是由双引号括起来的字符序列。

例如:

1
const char* str = "Hello, World!";

上述代码中,"Hello, World!" 就是一个字符串字面量。这个字符串字面量的类型是一个 const char 数组(C++中字符串字面量的类型是一个字符数组),并且它以 null 字符 '\0' 结尾。

字符串字面量可以用于初始化字符数组、字符串指针、以及各种支持字符串操作的标准库类(比如 std::string)。

在C++中,有一些特殊的字符串字面量前缀,用于指定不同的字符集和字符宽度,如前面提到的:

  • L 前缀表示宽字符(wchar_t)。
  • u 前缀表示UTF-16字符串(char16_t)。
  • U 前缀表示UTF-32字符串(char32_t)。

示例:

1
2
3
const wchar_t* wideStr = L"Wide String";
const char16_t* utf16Str = u"UTF-16 String";
const char32_t* utf32Str = U"UTF-32 String";

字符串字面量是C++中处理字符串的基本元素之一,而且它们的使用非常普遍。在实际编程中,字符串字面量经常用于初始化字符串,作为函数参数传递,或者用于各种字符串操作。

2. 字符串长度与数组大小

image-20231215172042237

There is a question! 字符串有6个字符,为什么定义中的长度是7呢?

因为要考虑到‘null termination character ’也要占用一个字符长度。

image-20231215172636875

3. const char* name

Cherno在视频中的这两行代码是可以编译的,但是我这个VS的版本编译不了。

Cherno在视频中提到了,这两行代码能不能编译取决于所使用的编译器。Cherno用的是VS2017可以编译,但在VS2019不能编译,VS2022中也不能编译,要求必须有const关键字。

4. diffierent ‘char’

1
2
3
4
5
6
7
	const char* name = "Cherno";
// const char* name = u8"Cherno"; // 与上面的写法一样,只是上面省略了‘u8’
const wchar_t* name2 = L"Cherno"; // wchar_t: Wide char -> 2 bytes

// C++ 11引入
const char16_t* name3 = u"Cherno"; // 16 bits = 2 bytes
const char32_t* name4 = U"Cherno"; // 32 bits = 4 bytes

解释来自ChatGPT

这是 C++ 中用于定义字符串的一些不同方式,涉及到字符集和字符宽度:

  1. **const char\* name = "Cherno";**(UTF-8)

    这是一个普通的 C 字符串(null-terminated string),使用 const char* 类型的指针来指向以 null 结尾的字符序列。这是常见的字符串表示方式,其中字符的宽度是 1 字节。

  2. const wchar_t\* name2 = L"Cherno";

    这是使用宽字符集的字符串,L 前缀表示宽字符(wide character)。wchar_t 类型的指针指向以 null 结尾的宽字符序列。宽字符通常是 2 字节,用于支持更广泛的字符集,特别是国际化和多语言环境。

  3. **const char16_t\* name3 = u"Cherno";**(UTF-16)

    这是使用 UTF-16 编码的 Unicode 字符串,u 前缀表示 UTF-16 字符。char16_t 类型的指针指向以 null 结尾的 UTF-16 字符序列。每个字符通常占据 2 字节。

  4. **const char32_t\* name4 = U"Cherno";**(UTF-32)

    这是使用 UTF-32 编码的 Unicode 字符串,U 前缀表示 UTF-32 字符。char32_t 类型的指针指向以 null 结尾的 UTF-32 字符序列。每个字符通常占据 4 字节。

这些不同的字符串表示方式主要用于处理不同的字符集和字符宽度需求。在选择使用哪种类型的字符串时,需要考虑你的应用程序的特定要求,特别是对字符集的支持和国际化的需求。 C++11 引入了这些新的字符串类型和前缀,以更好地支持 Unicode 字符和不同的字符宽度。

虽然我们一直说 wchar 每个字符都是 2 字节,但实际上是由编译器决定的。(Windows:2 bytes,Linux:4 bytes)。 如果你希望它一直是 2 bytes,你可以用char16_t

5. 在字符串上附加一些东西

  1. string_literals

in C++ 17

string_literals

  1. R method
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
#include <iostream>
#include <string>

#include <stdlib.h>

int main()
{
// method 1
std::string name1 = std::string("Line1\n") + "Line2\n" + "Line3\n";
std::cout << name1 << std::endl << std::endl;

// method 2
using namespace std::string_literals;
std::string name2 = "Line1\n"s + "Line2\n" + "Line3\n";
std::cout << name2 << std::endl << std::endl;

// method 3
const char* name3 = R"(Line1
Line2
Line3)";

std::cout << name3 << std::endl << std::endl;

// method 4
const char* name4 = "Line1\n"
"Line2\n"
"Line3\n";

std::cout << name4 << std::endl << std::endl;

std::cin.get();
}

6. the memory of the string literals and how it works

字符串字面量总是存储在只读内存(read-only memory)中

34. CONST in C++⭐

我比较喜欢把const叫做一个”fake keyword”,因为它实际上在生成代码的时候并没有做什么。 它有点像类和结构体的可见性,是一种针对开发人员写代码的强制规则,为了让代码保持整洁的机制。

基本上 const 就是你做出承诺,某些东西是不变的,是不会改动的。但是它只是个承诺,而且你可以绕过或不遵守这个承诺,就像在现实生活中一样。

1
const int MAX_NUMBER = 100;

1. const 与 pointer

当使用const处理指针的时候,可以是指针本身,也可以是指针指向的内容,取决于const放在声明处的某处,const是在“星号”的左边还是在“星号”的右边。

  • const correctness

const类型限定符(type qualifier)是C++语言设计的一大亮点。我们围绕着这个语言特性使用“const正确性” (const correctness)的实践来防止const对象遭到改变。

1
2
3
4
const int MAX_NUM = 100;
int* a = new int;
*a = 2;
a = &MAX_NUM; // 这个写法有问题!

问题在于,a 是一个指向动态分配内存的指针,而 &MAX_NUM 是一个指向常量的指针。你不能将一个指向常量的指针赋值给一个非常量指针,因为这违反了 const-correctness 规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>

int main()
{
const int MAX_NUM = 100;
int* a = new int;
*a = 2;
a = (int*)&MAX_NUM;

std::cout << *a << std::endl;
std::cin.get();
}
// 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>

int main()
{
const int MAX_NUM = 100;
const int* a = new int;
// int const* a = new int; // 两种写法相同
*a = 2; // error! const使你不能修改a指针指向的内容
a = (int*)&MAX_NUM;

std::cout << *a << std::endl;
std::cin.get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>

int main()
{
const int MAX_NUM = 100;
int* const a = new int;
*a = 2;
a = (int*)&MAX_NUM; // error!可以改变指针指向的内容,但是不能把指针自身重新赋值,让它指向其它东西

std::cout << *a << std::endl;
std::cin.get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

int main()
{
const int MAX_NUM = 100;
const int* const a = new int;

// 既不能改变指针的内容,也不能改变指针本身让它指向别处
*a = 2; //error!
a = (int*)&MAX_NUM; //error!

std::cout << *a << std::endl;
std::cout << a << std::endl;
std::cin.get();
}

2. 类与方法中使用const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Entity
{
private:
int m_X, m_Y;
public:
int GetX() const // (这种方法只在类中有效)
{
// m_X = 1; // error! 因为方法后面加了const关键字,所有该方法不能修改类成员变量
return m_X;
}
void SetX(int x)
{
m_X = x;
}
};
1
2
3
4
5
6
7
8
9
10
class Entity
{
private:
int* m_X, m_Y;
public:
const int* const GetX() const // 指针m_X指向的内容不能变,指针的内容也不能变
{
return m_X;
}
};

ChatGPT

这段代码定义了一个名为 Entity 的类,其中包含了一个私有的指针成员 m_X 和一个私有的整数成员 m_Y。以下是对这段代码的详细解释:

1
2
3
4
5
6
7
8
9
10
cppCopy codeclass Entity
{
private:
int* m_X, m_Y;
public:
const int* const GetX() const
{
return m_X;
}
};
  • 私有成员变量:

    • int* m_X: 是一个指向整数的指针。这是一个指针,可以指向一个整数值。但请注意,只有指针本身是私有的,而不是指针指向的内容。
    • int m_Y: 是一个整数。这个成员变量是一个普通的整数。
  • 公有成员函数 GetX

    • const int* const GetX() const: 是一个公有成员函数,返回一个指向整数的常量指针。

    • const关键字在不同位置的含义:

  • const int*: 表示返回的指针指向的内容是常量,即不能通过这个指针修改对象的值

    • const int* const: 表示返回的指针本身也是常量,即不能通过这个指针修改指针的值,即不能使它指向其他内存地址。
  • const 关键字在函数的末尾表示这是一个常量成员函数,即在函数内不能修改对象的成员变量

  • GetX 函数的作用:

    • GetX 函数返回一个指向整数的常量指针,指向对象内部的 m_X 成员。
  • 由于返回的指针是常量指针,外部代码不能通过这个指针修改 m_X 指向的内容。

  • 同时,由于返回的指针本身也是常量指针,外部代码不能通过这个指针修改它指向的内存地址,即不能改变 m_X 的指向。

这种设计可以提供一定程度的数据封装和安全性,限制了外部对 m_X 的直接修改。

需要注意一个很有趣的地方!

1
2
3
4
- int* m_X, m_Y;
- 这样写的代表着m_X是int型指针变量,而m_Y仍然是int型变量
- int* m_X, *m_Y
- 而这样写才能使得两个变量都是int型指针变量

35. The Mutable Keyword in C++

mutable means something can be changed.

1. mutable and const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>

class Entity
{
private:
std::string m_Name;
mutable int m_DebugCount = 0;
public:
const std::string& GetName() const
{
m_DebugCount++;
return m_Name;
}
};
int main()
{
const Entity e;
e.GetName();

std::cin.get();
}

2. mutable and lambda

1
2
3
4
5
6
7
8
int x = 8;
auto f = [=]() mutable
{
x++;
std::cout << x << std::endl;
}

f();

36. Member Initializer Lists in C++ (Constructor Initializer List)⭐

成员初始化列表,在构造函数中初始化类成员(变量)的一种方式

1. 构造函数->初始化成员(变量)

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
#include <iostream>
#include <string>

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

const std::string& GetName() const
{
return m_Name;
}
};
int main()
{
const Entity e0;
std::cout << e0.GetName() << std::endl;

const Entity e1("Cherno");
std::cout << e1.GetName() << std::endl;

std::cin.get();
}

2. 成员初始化列表

确保成员初始化列表时,要与成员变量声明时的的顺序一致!!

为什么需要成员初始化列表?

  • 因为构造函数的功能往往不仅仅是初始化成员变量,为了使得构造函数看起来简洁易读一些,我们可以把杂乱的初始化成员变量的这一部分以成员初始化列表的形式单独写做一行,这样就简化了构造函数。-> 1. 简化构造函数
  1. 简化构造函数
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
#include <iostream>
#include <string>

class Entity
{
private:
std::string m_Name;
int m_Score;
public:
Entity()
: m_Name("Unknown"), m_Score(0)
{
}

Entity(const std::string& name, const int score)
: m_Name(name), m_Score(score)
{
m_Name = name;
}

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

const int& GetScore() const
{
return m_Score;
}
};
int main()
{
const Entity e0;
std::cout << e0.GetName() << ", " << e0.GetScore() << std::endl;

const Entity e1("Cherno", 10);
std::cout << e1.GetName() << ", " << e1.GetScore() << std::endl;

std::cin.get();
}
  1. 避免构造两次
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
#include <iostream>
#include <string>

class Example
{
public:
Example()
{
std::cout << "Created Example!" << std::endl;
}

Example(int x)
{
std::cout << "Created Example with " << x << "!" << std::endl;
}
};
class Entity
{
private:
std::string m_Name;
Example m_Example;
public:
Entity()
{
m_Name = std::string("Unknown");
m_Example = Example(100);
}
};
int main()
{
const Entity e0;

std::cin.get();
}

// 输出结果是以下两行
// Created Example!
// Created Example with 100!
// 为什么会分别调用了默认构造函数和有参数的构造函数呢? 明明在Entity类的构造函数中只使用了m_Example = Example(100);
// 这是因为Example m_Example;调用了一次Example的默认构造函数
// 因此这就相当于把Example实例化了一次然后又实例化了一次,这就造成了性能的浪费。
// 为了解决这个问题,可以使用成员列表的方式来解决。
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
#include <iostream>
#include <string>

class Example
{
public:
Example()
{
std::cout << "Created Example!" << std::endl;
}

Example(int x)
{
std::cout << "Created Example with " << x << "!" << std::endl;
}
};
class Entity
{
private:
std::string m_Name;
Example m_Example;
public:
Entity()
: m_Name("Unkonwn"), m_Example(Example(100)) // m_Example(100),写成这样的效果也是一样的。
{
}
};
int main()
{
const Entity e0;

std::cin.get();
}


// 输出结果
// Created Example with 100!

37. Ternary Operator in C++(Conditional Assignment)

Ternary Operator: 三元运算符-> 问号和冒号(本质上就是if语句的语法糖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>

static int s_Level = 1;
static int s_Speed = 2;

int main()
{
// method 1
if (s_Level > 5)
s_Speed = 10;
else
s_Speed = 5;

// method 2
s_Speed = s_Level > 5 ? 10 : 5;
std::string rank = s_Level > 10 ? "Master" : "Beginner";

s_Speed = s_Level > 5 ? s_Level > 10 ? 15 : 10 : 5;
// 尽量不要做三运运算符的嵌套,易读性可能会大大降低

std::cin.get();
}

38. How to create/instantiate object C++⭐

C++创建对象

实例化定义的类

1. 在栈上创建对象(stack)

  • 最平常是创建对象的方法

几乎所有时候。如果你可以这样创建对象的话,那就这么来创建,这是基本规则。 因为在 C++中这是初始化对象最快的方式和最受管控的方式。

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
#include <iostream>
#include <string>

class Entity
{
private:
std::string m_Name;
public:
Entity() : m_Name("Unkown") {}
Entity(const std::string name) : m_Name(name) {}

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

int main()
{
Entity entity;
std::cout << entity.GetName() << std::endl;

Entity entity1("Cherno");
// Entity entity1 = Entity("Cherno"); // 类型 实例名 = 构造函数(参数)
std::cout << entity1.GetName() << std::endl;

std::cin.get();
}
  • 需要使得创建的对象在函数生存期之外依然存在
  1. 定义在函数内的对象在函数结束之后,所占用的内存便会被释放
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
#include <iostream>
#include <string>

class Entity
{
private:
std::string m_Name;
public:
Entity() : m_Name("Unkown") {}
Entity(const std::string name) : m_Name(name) {}

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

void Function()
{
Entity entity("Cherno");
int a = 10;
// a和entity的生命周期仅存在函数function之内,函数结束了之后,变量所占用的内存也就被释放了。
}

int main()
{
Function();
// 但我们调用function的时,就为这个函数创建了一个栈结构

std::cin.get();
}

2.

image-20231227150550541

1
2
3
4
5
console输入的内容如下:
1. Cherno
2.

解释一下为什么是这样的输出。

叫做Cherno的entity实例的生命周期仅在大括号之内,跳出大括号后,这个叫 Cherno 的 entity 对象已经不存在了,它已经不存在栈结构里了,所以就没有输出了。

另一个我们不想在栈上分配的原因可能是:如果这个 entity 太大了,同时我们可能有很多的 entity,我们就可能没有足够的空间来进行分配,因为栈通常都很小,一般是一两兆,这取决于你的平台和编译器。 因此你可能不得不在heap上进行分配。

突然想到的一个内容,和本节内容相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
Entity* e;

{
Entity entity("Cherno");
e = &entity;
std::cout << entity.GetName() << std::endl;
std::cout << e->GetName() << std::endl;
std::cout << (*e).GetName() << std::endl;
}
std::cin.get();
}

/*
Cherno
Cherno
Cherno
*/
  1. std::cout << entity.GetName() << std::endl;
    • 直接通过对象 entity 调用 GetName 函数,输出实体的名称。
    • 这种方式是直接访问对象的成员函数,因为 entityEntity 类的一个实例。
  2. std::cout << e->GetName() << std::endl;
    • 通过指针 e 调用 GetName 函数,输出实体的名称。
    • 这种方式使用了指针,e 是一个指向 Entity 对象的指针,通过箭头运算符 -> 访问对象的成员函数。
  3. std::cout << (\*e).GetName() << std::endl;
    • 同样是通过指针 e 调用 GetName 函数,输出实体的名称。
    • 这种方式使用了解引用操作符 *,先解引用指针,然后再访问对象的成员函数。

在这个特定的示例中,这三种方式都会输出相同的结果,即实体的名称。选择使用哪种方式通常取决于代码的上下文和个人偏好。在一般情况下,直接通过对象调用成员函数是最直观和常见的方式。使用指针或引用通常用于处理动态分配的对象或在函数参数中传递对象,但需要小心确保指针有效且指向有效的对象。

2. 在堆上分配(heap)

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
#include <iostream>
#include <string>

class Entity
{
private:
std::string m_Name;
public:
Entity() : m_Name("Unkown") {}
Entity(const std::string name) : m_Name(name) {}

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

int main()
{
Entity* e;

{
Entity* entity = new Entity("Cherno");
e = entity; // 这里只是复制的entity对象的地址
std::cout << "1. " << entity->GetName() << std::endl;
}

std::cout << "2. " << e->GetName() << std::endl;

std::cin.get();
delete e;
}

/*
程序输出内容如下:
1. Cherno
2. Cherno
*/

Attention: 在使用了new关键字之后,不用的内存要注意使用delete关键字释放掉,防止内存泄漏。

3. 总结

两种创建对象的方法如何选择?

  • 如果要创建的对象很大-> heap
  • 显式地控制对象的生存期 -> heap
  • 其他 -> stack

39. The New keyword in C++

1
2
int* a = new int;
// 为变量a在内存中分配4 bytes大小的连续内存
  • 关于连续内存的问题,计算机并不是搜索出来的这个4 bytes的连续内存,而是存在一种叫做空闲列表(free list)的东西,它会维护那些有空闲字节的地址。
  • new的作用就是要找到一个足够大的内存块,以满足我们的需求。
  • Entity* e = new Entity(); 在这里它不仅分配了空间,还调用了构造函数。
  • 通常,调用new关键字会调用底层的C函数malloc,它是用来分配内存的。 malloc()的实际作用是,传入一个size,也就是我们需要多少个字节,然后返回一个void指针
  • new本身实际上是一个operator(操作符),操作符意味着可以操作符重载
  • 用完new之后记得使用delete
  • C++中的new和delete对应到C中就是malloc和free
1
2
3
4
5
6
7
8
int* a = new int;
delete a;

int* b = new int[50];
delete[] b;

Entity* e = new Entity();
delete e;
  • placement new

    • int* b = new int[50];
      Entity* e = new(b) Entity();
      
      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

      - 这决定了它的内存来自哪里,细节以后再讲,这里只是展示它的语法。

      - 这里将 `Entity` 对象构造在已分配的内存地址 `b` 上,而不是使用默认的内存分配器。这样可以在指定的内存位置创建对象。这行代码在 `b` 指针指向的内存位置上构造了一个 `Entity` 对象,并返回指向该对象的指针,并将其赋给了 `e` 指针。

      # 40. Implicit Conversion and the Explicit keyword in C++

      > - 隐式转换与explicit关键字
      >
      > implicit:隐式的
      > explicit:显式的
      >
      > - *implicit*(隐式)的意思是不会明确地告诉你它要做什么,它有点像在某种情况下自动的工作。实际上 C++允许编译器对代码进行一次隐式的转换。
      >
      > - 如果我开始使用一种数据类型作为另一种类型来使用,在这两种类型之间就会有类型转换,C++允许隐式地转换,不需要用*cast*等做强制转换。

      ## 1. Implicit Conversion

      ```C++
      #include <iostream>
      #include <string>

      class Entity
      {
      private:
      std::string m_Name;
      int m_Age;
      public:
      Entity(const std::string& name)
      : m_Name(name), m_Age(-1) {}
      Entity(int age)
      : m_Name("Uknown"), m_Age(age) {}
      };

      int main()
      {
      Entity a("Cherno");
      Entity b(22);

      Entity c = Entity("Cherno");
      Entity d = Entity(22);

      Entity e = std::string("Cherno"); // 隐式类型转换
      Entity f = 22; // 隐式类型转换

      std::cin.get();
      }

2. explicit keyword

  • 如果把explicit关键字放在构造函数之前,这就意味着不能使用隐式构造
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
#include <iostream>
#include <string>

class Entity
{
private:
std::string m_Name;
int m_Age;
public:
Entity(const std::string& name)
: m_Name(name), m_Age(-1) {}
explicit Entity(int age)
: m_Name("Uknown"), m_Age(age) {}
};

int main()
{
Entity a("Cherno");
Entity b(22);

Entity c = Entity("Cherno");
Entity d = Entity(22);

Entity e = std::string("Cherno");
Entity f = (Entity)22;

std::cin.get();
}

41. Operators and Operator overloading in C++⭐

1. 运算符

1
2
3
4
5
operator: 
- '+', '-', '*', '/'
- '*(dereference)', '->', '+=', '&', '<<',
- 'new', 'delete',
- ',', '()', '[]'

2. 运算符重载 + and -

  • overload重载这个术语本质就是给运算符重载赋予新的含义,或者添加参数,或者创建 允许在程序中国定义或更改运算符的行为。

  • 不过说到底,运算符就是function,就是函数。 与其写出函数名add,你只用写一个+这样的运算符就行,在很多情况下这真的有助于让你的代码更干净整洁,可读性更好。

  • 运算符重载的使用应该非常少,而且只是在完全有意义的情况下使用。

  1. 没有运算符重载的时候写的程序如下:
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
#include <iostream>
#include <string>

struct Vector2
{
float x, y;

Vector2(float x, float y)
: x(x), y(y) {}

Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}

Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
};

int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 Speed(0.5f, 1.5f);
Vector2 Powerup(1.1f, 1.1f);
Vector2 result = position.Add(Speed.Multiply(Powerup));

std::cin.get();
}
  1. 有运算符重载的时候代码如下:
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
#include <iostream>
#include <string>

struct Vector2
{
float x, y;

Vector2(float x, float y)
: x(x), y(y) {}

Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 operator+(const Vector2& other) const
{
return Add(other);
}

Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
Vector2 operator*(const Vector2& other) const
{
return Multiply(other);
}
};

int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 Speed(0.5f, 1.5f);
Vector2 Powerup(1.1f, 1.1f);

// 没有运算符重载
Vector2 result1 = position.Add(Speed.Multiply(Powerup));
// 有运算符重载
Vector2 result2 = position + Speed * Powerup;

std::cin.get();
}

3. 运算符重载 <<

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
#include <iostream>
#include <string>

struct Vector2
{
float x, y;

Vector2(float x, float y)
: x(x), y(y) {}

Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 operator+(const Vector2& other) const
{
return Add(other);
}

Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
Vector2 operator*(const Vector2& other) const
{
return Multiply(other);
}
};

std::ostream& operator<<(std::ostream& stream, const Vector2& other)
{
stream << other.x << ", " << other.y;
return stream;
}

int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 Speed(0.5f, 1.5f);
Vector2 Powerup(1.1f, 1.1f);

// 没有运算符重载
Vector2 result1 = position.Add(Speed.Multiply(Powerup));
// 有运算符重载
Vector2 result2 = position + Speed * Powerup;

std::cout << result2 << std::endl;

std::cin.get();
}

4. 运算符重载 == and !=

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <string>

struct Vector2
{
float x, y;

Vector2(float x, float y)
: x(x), y(y) {}

Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 operator+(const Vector2& other) const
{
return Add(other);
}

Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
Vector2 operator*(const Vector2& other) const
{
return Multiply(other);
}

bool operator==(const Vector2& other) const
{
return x == other.x && y == other.y;
}

bool operator!=(const Vector2& other) const
{
return !(*this == other); // this pointer 不太懂,后面会学习
}
};

std::ostream& operator<<(std::ostream& stream, const Vector2& other)
{
stream << other.x << ", " << other.y;
return stream;
}

int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 Speed(0.5f, 1.5f);
Vector2 Powerup(1.1f, 1.1f);

// 没有运算符重载
Vector2 result1 = position.Add(Speed.Multiply(Powerup));
// 有运算符重载
Vector2 result2 = position + Speed * Powerup;

std::cout << result1 << std::endl;
std::cout << result2 << std::endl;

if (result1 == result2)
{
std::cout << "equality" << std::endl;
}

if (result1 != result2)
{
std::cout << "not equality" << std::endl;
}

std::cin.get();
}

42. The “this” keyword in C++ ⭐

C++中有这样一个关键字this,通过它可以访问成员函数。 this是一个指向当前对象实例的指针,该method(方法)属于这个对象实例。

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
#include <iostream>

class Entity;
void PrintEntity(Entity* e);

class Entity
{
public:
int x, y;

Entity(int x, int y)
{
this->x = x;
this->y = y;
PrintEntity(this);
}

int GetX() const
{
// this->x = 5;
const Entity* e = this;
return this->x;
}
};


void PrintEntity(Entity* e)
{
// Print
}

int main()
{

std::cin.get();
}

43. Obeject lifetime in C++ (Stack/Scope lifetimes)⭐

scope: 作用域

1. 基于stack和基于heap的变量在对象生存期上的区别

  • 基于stack的变量在一出作用域,该变量所占用的内存空间便被释放了;
  • 基于heap的变量只要不手动释放内存空间,则该内存空间便不会被释放,知道程序的结束。
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
#include <iostream>
#include <string>

class Entity
{
public:
Entity()
{
std::cout << "Created Entity!" << std::endl;
}

~Entity()
{
std::cout << "Destoryed Entity!" << std::endl;
}

};
int main()
{
{
Entity e; // 定义来栈上的对象
}// e的作用域就在这个大括号之中,执行完大括号之后,就调用了析构函数。

std::cin.get();
}

// 输入如下:
/*
Created Entity!
Destoryed Entity!
*/
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
#include <iostream>
#include <string>

class Entity
{
public:
Entity()
{
std::cout << "Created Entity!" << std::endl;
}

~Entity()
{
std::cout << "Destoryed Entity!" << std::endl;
}

};
int main()
{
{
Entity* e = new Entity();
}//定义在heap上的对象,如果不使用delete手动释放内存空间则该内存空间就不会被释放,因此没有调用析构函数。

std::cin.get();
}

// 输入如下:
/*
Created Entity!
*/
  • 举个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //一段有问题的代码!!!

    #include <iostream>
    int* CreatedArray()
    {
    int array[50];
    return array; //array的生存期仅在这个大括号之内,跳出大括号之后,array所定义的内存空间都被释放掉了,所以返回的地址也没有什么用了。
    }

    int main()
    {
    int* a = CreatedArray();

    std::cin.get();
    }
  • 改正方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//一段有问题的代码!!!

#include <iostream>
int* CreatedArray()
{
int* array = new int[50]; // 将array定义在heap上
return array;
}

int main()
{
int* a = CreatedArray();

std::cin.get();
}

2. scope pointer

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
#include <iostream>
#include <string>

class Entity
{
public:
Entity()
{
std::cout << "Created Entity!" << std::endl;
}

~Entity()
{
std::cout << "Destoryed Entity!" << std::endl;
}

};

class ScopePtr
{
private:
Entity* m_Ptr;
public:
ScopePtr(Entity* ptr)
: m_Ptr(ptr) {}

~ScopePtr()
{
delete m_Ptr;
}
};

int main()
{
{
ScopePtr e = new Entity();
}

std::cin.get();
}

// 输入如下:
/*
Created Entity!
Destoryed Entity!
*/

// 对象定义在heap上,但是我们通过ScopePtr来实现了new-delete, 在跳出大括号的时候,自动调用delete了.

44. SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)

smart pointers使得new-delete的过程自动化

1. unique_ptr—scope pointer

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
#include <iostream>
#include <string>
#include <memory>

class Entity
{
public:
Entity()
{
std::cout << "Created Entity!" << std::endl;
}

~Entity()
{
std::cout << "Destoryed Entity!" << std::endl;
}

void Print()
{
std::cout << "Hello world!" << std::endl;
}

};

int main()
{
{
// method 1: 不可以!!!
// std::unique_ptr<Entity> entity = new Entity(); // 这种写法不可以,因为unique_ptr的构造函数有explicit关键词,只能接受显式构造

// method 2: 可以,但是因为异常安全问题,不采用这种方法!!
// std::unique_ptr<Entity> entity(new Entity());

// method 3: 最好的方法,因为这样做安全。
std::unique_ptr<Entity> entity = std::make_unique<Entity>();
entity->Print();
}

std::cin.get();
}

// 程序输入如下
/*
Created Entity!
Hello world!
Destoryed Entity!
*/

一个更好的做法是:

1
std::unique_ptr<Entity> entity = std::make_unique<Entity>();

这对于unique_ptr来说很重要,主要原因是出于exception safety (异常安全),如果构造函数碰巧抛出异常,它会稍微安全一些。你不会最终得到一个没有引用的dangling pointer(悬空指针)而造成过内泄漏。

  • 前面提到了unique_ptr不能被复制。如果你去看它的定义,你会发现它的拷贝构造函数和拷贝构造操作符实际上被删除了,这就是为什么你运行如下代码时会编译错误。

image-20231229103852279

2. shared_ptr

  • shared_ptr使用的是reference counting(引用计数).

举个例子,我刚创建了一个共享指针,又创建了另一个共享指针来复制它,此时我的引用计数是 2。第一个指针失效时,我的引用计数器减少 1,然后最后一个失效时,我的引用计数回到 0,就真的“dead”了,因此内存被释放。

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
#include <iostream>
#include <string>
#include <memory>

class Entity
{
public:
Entity()
{
std::cout << "Created Entity!" << std::endl;
}

~Entity()
{
std::cout << "Destoryed Entity!" << std::endl;
}

void Print()
{
std::cout << "Hello world!" << std::endl;
}
};

int main()
{
{
// method 1: 不可以!因为shared_ptr需要分配控制块来存储引用计数。
// std::shared_ptr<Entity> sharedEntity(new Entity());

// method 2: 使用这样方法!
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
std::shared_ptr<Entity> e0 = sharedEntity;
}

std::cin.get();
}
  • 关于shared_ptr的复制问题

image-20231229110054206

有了共享指针,你当然可以进行复制。 下图代码中有两个作用域,可以看到里面这个作用域死亡时,这个 sharedEntity 失效了,然而并没有对 Entity 析构并删除,因为 e0 仍然是有效的,并且持有对该 Entity 的引用。再按一下 F10,当所有引用都没了,当所有追踪shared_ptr的栈分配对象都死亡后,底层的 Entity 才会从内存中释放并删除。

3. weak_ptr

  • 当你将一个shared_ptr赋值给另外一个shared_ptr,引用计数会增加。

  • 当你把一个shared_ptr赋值给一个weak_ptr时,它不会增加引用计数。

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
#include <iostream>
#include <string>
#include <memory>

class Entity
{
public:
Entity()
{
std::cout << "Created Entity!" << std::endl;
}

~Entity()
{
std::cout << "Destoryed Entity!" << std::endl;
}

void Print()
{
std::cout << "Hello world!" << std::endl;
}
};

int main()
{
{
std::weak_ptr<Entity> e0;
{
std::shared_ptr<Entity> sharedEntiy = std::make_shared<Entity>();
e0 = sharedEntiy;
}
}

std::cin.get();
}

4. smart pointer and new-delete

这就是很有用的智能指针,但它们绝对没有完全取代newdelete关键字。只是当你要声明一个堆分配的对象而且不希望由自己来清理,这时候你就应该使用智能指针,尽量使用unique_ptr,因为它有较低的开销。但如果你需要在对象之间共享,不能使用unique_ptr的时候,就用shared_ptr

45. Copying and Copy constructors in C++

copy means: copy data and copy memory.

  1. 创建一个String类
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
// 注释来自ChatGPT

#include <iostream>
#include <string> // 包含 C 字符串处理函数所需的头文件

class String
{
private:
char* m_Buffer; // 存储字符串的字符数组
unsigned int m_Size; // 字符串的长度(不包括 null 终止符)
public:
// 构造函数,接受一个 C 风格字符串作为参数
String(const char* string)
{
m_Size = strlen(string); // 计算字符串的长度
m_Buffer = new char[m_Size + 1]; // 为字符串分配内存,包括 null 终止符
memcpy(m_Buffer, string, m_Size + 1); // 将传入的字符串复制到 m_Buffer
}

// 析构函数,释放动态分配的内存
~String()
{
delete[] m_Buffer;
}

// 声明友元,使得重载的输出运算符能够访问类的私有成员
friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

// 重载输出运算符
std::ostream& operator<<(std::ostream& stream, const String& string)
{
stream << string.m_Buffer; // 将字符串输出到流
return stream;
}

// 主函数
int main()
{
// 创建 String 对象并初始化为 "Cherno"
String string = "Cherno";

// 使用重载的输出运算符输出 String 对象的内容
std::cout << string << std::endl;

std::cin.get(); // 等待用户输入
}

1. shallow copy

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
#include <iostream>
#include <string>

class String
{
private:
char* m_Buffer;
unsigned int m_Size;
public:
String(const char* string)
{
m_Size = strlen(string);
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, string, m_Size + 1);
}

~String()
{
delete[] m_Buffer;
}

friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

std::ostream& operator<<(std::ostream& stream, const String& string)
{
stream << string.m_Buffer;
return stream;
}

int main()
{
String string = "Cherno";
String second = string; // 浅拷贝

std::cout << string << std::endl;
std::cout << second << std::endl;
// 程序崩溃!
std::cin.get();
}

现在问题来了,内存中有两个 String,因为它们直接进行了复制,这种复制被称为shallow copy(浅拷贝)。它所做的是复制这个 char,内存中的两个 String 对象有相同的 char的值,换句话说就是有相同的内存地址。这个 m_Buffer 的内存地址,对于这两个 String 对象来说是相同的,所以程序会崩溃的原因是当我们到达作用域的尽头时,这两个 String 都被销毁了,析构函数会被调用,然后执行delete[] m_Buffer两次,程序试图两次释放同一个内存块。这就是为什么程序会崩溃——因为内存已经释放了,不再是我们的了,我们无法再次释放它。

2. deep copy — copy constructor

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
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <string>

class String
{
private:
char* m_Buffer;
unsigned int m_Size;
public:
String(const char* string)
{
m_Size = strlen(string);
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, string, m_Size + 1);
}

~String()
{
delete[] m_Buffer;
}

String(const String& other) // copy constructor
: m_Size(other.m_Size)
{
std::cout << "Copied String!" << std::endl;
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}

char& operator[](unsigned int index)
{
return m_Buffer[index];
}

friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

std::ostream& operator<<(std::ostream& stream, const String& string)
{
stream << string.m_Buffer;
return stream;
}

void PrintString(const String& string)
{
std::cout << string << std::endl;
}

int main()
{
String string = "Cherno";
String second = string;

second[2] = 'a';

PrintString(string);
PrintString(second);

std::cin.get();
}

46. The Arrow Operator in C++

1. pointer, reference, arrow

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
#include <iostream>
#include <string>

class Entity
{
public:
void Print() const{ std::cout << "Hello!" << std::endl; }
};

int main()
{
Entity e;
e.Print();

Entity* ptr = &e;
(*ptr).Print(); // 考虑运算符优先级

Entity* ptr1 = &e;
Entity& entity = *ptr1;
entity.Print();

Entity* ptr2 = &e;
ptr2->Print();

std::cin.get();
}

2. overloading

箭头作为一种运算符,C++可以重载

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
#include <iostream>
#include <string>

class Entity
{
public:
void Print() const{ std::cout << "Hello!" << std::endl; }
};

class ScopedPtr
{
private:
Entity* m_Obj;
public:
ScopedPtr(Entity* entity)
:m_Obj(entity)
{

}

~ScopedPtr()
{
delete m_Obj;
}

Entity* operator->()
{
return m_Obj;
}

const Entity* operator->() const
{
return m_Obj;
}
};

int main()
{
ScopedPtr entity = new Entity();
entity->Print();

std::cin.get();
}

3. offset

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
#include <iostream>
#include <string>

struct Vector3
{
float x, y, z;
};

int main()
{
int offsetx = (int)&((Vector3*)nullptr)->x;
std::cout << offsetx << std::endl;

int offsety = (int)&((Vector3*)nullptr)->y;
std::cout << offsety << std::endl;

int offsetz = (int)&((Vector3*)nullptr)->z;
std::cout << offsetz << std::endl;

std::cin.get();
}

/*
0
4
8
*/

附:编程习惯

A. m_

C++中m_的含义是什么? |21xrx.com

在C++中,m_是一种命名约定,通常被用于表示一个类的成员变量。m_的含义是”member variable”或者”成员变量”,是为了区分成员变量和其他类型的变量而引入的。

使用m_的好处是可以方便地区分成员变量和其他变量,使代码变得更加可读和易于理解。此外,m_还可以避免与全局变量、局部变量或其他变量混淆,从而避免出现代码错误。

在使用m_时,需要注意以下几点:

\1. m_只是一种命名约定,不是C++的关键字或保留字,因此在使用时不要将其与其他变量名混淆。

\2. 使用m_时应该遵循统一的规范,例如将所有成员变量都以m_为前缀命名。

\3. 在构造函数和析构函数中,应该将所有成员变量的初始值或释放操作放在一起,以方便管理。

\4. 注意,在使用m_时应该尽可能使用访问器(getter和setter)而不是直接访问成员变量,这样可以使代码更加可维护和易于修改。

总的来说,m_是一种很好的命名约定,可以使代码更加清晰和易于理解。在编写C++代码时,使用m_能够提高代码的可读性和可维护性,值得开发者们好好利用。

B. 命名规则

1. 驼峰

原始:user login count

驼峰:userLoginCount

2. 帕斯卡

原始:user login count

帕斯卡:UserLoginCount

3. 蛇形

原始:user login count

蛇形:user_login_count

4. 匈牙利

int g_i32tempuratureValue:全局 32位有符号整型变量

float l_f32tempuratureValue:局部 32位有符号整型变量

unsigned char s_u8tempuratureValue:静态 无符号字符型变量

参考:内容目录 — Google 开源项目风格指南 (zh-google-styleguide.readthedocs.io)


ChernoCPP-1
https://cosmicdusty.cc/post/Knowledge/ChernoCPP_1/
作者
Murphy
发布于
2023年11月13日
许可协议