05.How C++ Works
preprocessor statement:预处理指令
#include 在计算机编译过程中其实就是把头文件里的东西粘贴过来了,就这么简单
declaration:声明
IDEA : operator as function
06.How the C++ Compiler Works
compiler:编译器
abstract syntax tree:抽象语法树
translation unity:编译单元
register:寄存器
07.How the C++ linking works
程序被分为多个C++文件,编译后要将他们link起来
static 来防止函数多次定义的问题,他将函数定义为当前翻译单元的内部函数,对其他object不可见。
08.Variable in C++
09.Function in C++
10.Header File in C++
#pragma once 使同一个文件不会被包含多次,比如头文件A.h和B.h里都包含了struct Player,但struct只能被定义一次(不能重名),这样会造成错误。
另一种处理方法就是#ifndef,即
#ifndef _LOG_H
#define _LOG_H
/*
.
其他函数与程序
.
*/
#endif
11.How to use VS to debug the code
断点调试(逐语句:一行一行跳转、逐过程:直接跳出此函数进入下一个函数)、监视窗口、直接查看内存
12.Condition:if / if else / else if
13.Some good settings
14.Loop:for / while
15.Control Flow:continue / break / return
continue 跳到for的下一次迭代
16.Pointer
注意一下
int* a, b; // a是指针,b是int型
int* a, *b; // ab都是指针
17.Reference
引用就是个别名,并不是一个真正的变量
int& ref = a;
用例:
void Increment(int value)
{
value++;
}
int main()
{
int a = 5;
Increment(a);
std::cont << a << std::endl;
std::cin.get();
}
这样a的值并没有改变,因为Increment创造了一个新的变量value。这个其实就是实参和形参的区别了。
void Increment(int* value)
{
(*value)++;
}
int main()
{
int a = 5;
Increment(&a);
std::cout << a << std::endl;
std::cin.get();
用指针可以解决这个问题
void Increment(int& value)
{
value++;
}
int main()
{
int a = 5;
Increment(a);
std::cont << a << std::endl;
std::cin.get();
引用可以更简单的实现。
对于指针,他可以实现引用能实现的功能,但引用使代码更加简单干净。
18.19.20.Class
类可以理解为把数据和功能组合在一起的东西
类内的函数被称为方法,方法针对的对象就是那个实例,所以定义的时候就不用要求输入实例名了;相反,如果函数定义到类外,那使用此函数改变类内的变量时就要输入确切的实例名(一般用引用),同时需要改变的变量必须是public的。
class Player
{
public:
int x = 0, y = 0;
int speed = 1;
};
void Move(Player& player, int xa, int ya)
{
player.x += xa * player.speed;
player.y += ya * player.speed;
}
int main()
{
Player player;
Move(player, 1, -1);
}
class Player
{
public:
int x = 0, y = 0;
int speed = 1;
void Move(int xa, int ya)
{
x += xa * speed;
y += ya * speed;
}
};
int main()
{
Player player;
player.Move(1, -1);
}
类与结构体:
只在技术上来看:默认情况下,类是private的,结构体是public的。
C++中保留结构体的原因就是向后兼容C的代码。
好的编程风格:struct去把简单有关联的多变量放一起;class使用在更加复杂的情景里。
C++类实例化的两种方式:new和不new的区别
A a; // a存在栈上
A* a = new A(); // a存在堆中
Others:堆与栈
栈由操作系统自动分配释放,函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量,内存地址的生长方向由高到低,后定义的变量地址低于先定义的变量。出了作用范围就会被销毁
堆由开发人员分配和释放,堆的内存地址生长方向与栈相反,由低到高,但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,即 p2 指向的地址并不一定大于 p1 所指向的内存地址,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。堆中存储的数据若未释放,则其生命周期等同于程序的生命周期。
操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入空闲链表。
动态内存分配会使对象的可控性增强,所以大程序用new,小程序不加new直接申请。
new了记得用delete删除。
21.22.static
static将函数定义为当前翻译单元的内部函数(有点像类里的private),这个变量只会在这个翻译单元内部链接,对其他object不可见,这样可以有效的防止变量重复定义。
或者可以用extern,表示从外部翻译单元寻找这个变量的定义
int Variable; // 在obj1中定义Variable
extern int Variable // 在obj2中寻找obj1定义的Variable
为什么要使用static和private?
因为全局变量容易产生bug(会被链接到各个翻译单元),如果不需要变量是全局变量,那就要尽可能多的使用静态变量
class中的static:如果在一个class中定义一个变量为static的,意味着在类的所有实例中,这个变量只有一个实例,这样的话无论创造多少个实例,他们的static都一样,且一个改变全都改变,所以通过类实例来引用静态变量是没有意义的。静态可以被调用,不需要通过类的实例,而在静态方法内部,即在名为XXX(类名)的命名空间中创建了两个变量,他们实际上并不属于类。
当你想要跨类使用变量时,可以创建一个全局变量,也可以使用一个类的静态全局变量,他是内部进行链接的,不会在整个项目中是全局的,但可以达到同样的效果。
所以为什么要这么做呢?
如果你有需要在XXX类的所有实例中共享某个数据,或者它本身与XXX类有关,把他这样存储是有意义的,那就可以这样做,而不是把一些全局的东西到处乱放。
类中的静态函数同理,但要注意静态方法不能访问非静态变量,原因就是静态方法没有类实例。本质上,你在类中写的每一个非静态方法总能获得当前类的一个实例作为参数,但静态方法不会得到那个隐藏参数。
23.Local Static
声明一个变量要考虑两部分:
① 生存周期:变量实际存在的时间
② 作用域
静态局部变量允许我们声明一个变量,他的生存周期相当于整个程序的生存期,但作用范围被限制,这使我们可以用它来代替全局变量。
class Singleton // ingleton class 单例类 一般是只存在一个实例的类
{
private:
static Singleton* s_Instance;
public:
static Singleton& Get()
{
return *s_Instance;
}
void Hello()
{}
};
Singleton* Singleton::s_Instance = nullptr;
int main()
{
Singleton::Get().Hello();
}
理解:就是设置了一个地址,然后这个地址变量是只能在类内访问,类外就只能通过调用Get来得到它。
这样就得到了一个可以使用的类实例,可以用静态地使用它。
或者可以写成
class Singleton
{
public:
static Singleton& Get()
{
static Singleton instance
return Instance;
}
void Hello()
{}
};
int main()
{
Singleton::Get().Hello();
}
24.ENUM
枚举就是一个数值集合,是给值命名的一种方法,帮助我们将一组数值集合作为类型,而不是仅仅用整型作为类型,可以给他赋值任何整数,或者指定哪些值可以赋值。
用处就是不让你的代码里整数到处乱飞。
enum LogLevel : const int
{
LoglevelError = 0,
Loglevelwarning = 1,
LoglevelInfo = 2
};
25.Constructor
C++构造函数的作用是对函数进行初始化
class Entity
{
public:
float x;
float y;
public:
Entity()
{
x = 0.0f;
y = 0.0f;
}
void print()
{
std::cout << x << ", " << y << std::endl;
}
};
int main()
{
Entity e1;
std::cout << e1.x << std::endl;
e1.print();
std::cin.get();
}
还有一种直接跟冒号的写法,有兴趣可以掌握一下。
26.Destructor
析构函数与构造函数功能相反,在销毁对象时进行。一个对象要被销毁时,就会调用析构函数。析构函数同时适用于栈和堆分配的对象。析构函数的作用是防止内存泄露,其次,堆上的内存需要手动清理。
~Entity(float X, float Y)
{
x = 0.0f;
y = 0.0f;
}
27.Inheritance
继承允许我们有一个相互关联的类的层次结构,它允许我们有一个包含公共功能的基类,然后从在派生有特殊功能的子类,能够有效地避免代码重复。
class Entity
{
public:
float X, Y;
public:
void Move(float xa, float ya)
{
X += xa;
Y += ya;
}
void print()
{
std::cout << X << ", " << Y << std::endl;
}
};
class Player : public Entity
{
public:
const char* Name;
};
int main()
{
Player player1;
player1.Move(3.0f, 2.0f);
std::cin.get();
}
Player可以在任何地方代替Entity,即使是一些被要求传入Entity的地方,因为它拥有Entity的一切参数。
28.Virtual Function**
虚函数引入了一种叫做动态联编的东西,通常通过虚函数表来实现编译,虚函数表包含基类中所有虚函数的映射,这样我们可以在他运行的时候,将他们正确的覆写函数。例如有两个类AB,B是A的子类,如果我们在A中创建一个方法,再在B中重写他让他去做其他事。
如果你想覆写一个函数,必须将基类中的基函数标记为虚函数。
class Entity
{
public:
【virtual】std::string GetName() {return "Entity";} //对比在开头加不加vritual的输出结果
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string& name)
: m_Name(name) {}
/*
这里我怎么理解的:
Player函数需要一个const字符串的引用name,同时初始化时m_Name = name
*/
【virtual】std::string GetName() {return m_Name;}; // 加了vritual后,在GetName()和{}中间要加override,表明覆写函数
};
0
void PrintName(Entity* entity)
{
std::cout << entity->GetName() << std::endl;
}
int main()
{
Entity* e1 = new Entity();
PrintName(e1);
Player* e2 = new Player("Cherno");
std::cout << e2->GetName() << std::endl;
Entity* entity = e2;
PrintName(e2);
std::cin.get();
}
左为未加vritual,右为加vritual
29.Pure Virtual Function
纯虚函数允许我们在基类里定义一个没有实现的函数,然后强制子类去实现该函数。
在上节虚函数中,我们重写一个函数让他做与基类同名函数不同的事情,而当我们不重写时,我们依然可以调用【基类.函数】。然而在一些情况下,提供这种默认实现是没有意义的,只需要定义此函数,然后要求子类去实现就可以了。这通常被称为接口。
因此,类中的接口只包含未实现的方法作为模板,这个类其实也不能实例化。
class Entity
{
public:
virtual std::string GetName() = 0; // 定义成纯虚函数,意味着他必须要在一个子类中实现,如果你想要实例化这个子类的话
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string& name)
: m_Name(name) {}
virtual std::string GetName() { return m_Name; }; // 这里对GetName进行了实现,如果注释掉这个实现,main里也不能对Player进行实例化了
};
void PrintName(Entity * entity)
{
std::cout << entity->GetName() << std::endl;
}
int main()
{
Entity* e1 = new Player("Player1"); // 以实现了虚函数的子类进行实例化
// Entity* e1 = new Entity(); 不能直接实例化,必须给他一个子类来实现这个函数
PrintName(e1);
Player* e2 = new Player("Cherno");
std::cout << e2->GetName() << std::endl;
Entity* entity = e2;
PrintName(e2);
std::cin.get();
}
例2:
class Printable
{
public:
virtual std::string GetClassName() = 0;
};
class Entity : public Printable
{
public:
virtual 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() override {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* e1 = new Entity();
//PrintName(e1);
Player* e2 = new Player("Cherno");
//std::cout << e2->GetName() << std::endl;
Entity* entity = e2;
//PrintName(e2);
Print(e1);
Print(e2);
std::cin.get();
}
运行结果:
30.Visibility
可见性是一个面向对象编程的概念。C++种有三级基础的可见性修饰符:
- private:只有类内可见,但C++中还有个关键字叫friend,它可以让类或者函数称为友元,可以从类中访问私有成员。另外,子类也不能访问父类中的private
- protected:比private更可见,比public更不可见。这个类和层次结构中的所有子类可以访问
- public:全局可见
为什么要设置可见性:与性能无关,是为了让代码容易理解利于维护,确保不调用不该调用的东西。例如UI界面,游戏内移动XY,将X设为私有,将SetX函数设为公共,保证人们不直接给X赋值而是用SetX之类的方法。
31.Arrays
数组是元素的集合。
C++11中的数组:标准数组std::array这是一个内置数据结构,具有边界检查什么的。
32.Strings
Cherno系列中常将字符串称为 const char*
记录一个发现的问题:
这里第二个地址输出乱码了
在网上搜了搜出现的原因:
c是靠%s,%x,%p来区分指针表达式&a[0]的输出形式的;c++没有这个格式控制,只能按一种形式输出,对char* 类型的指针值就理解为串输出,所以必须对这个指针表达式做类型转换处理。
实际上,C++标准库中I/O类对输出操作符<<重载,在遇到字符型指针时会将其当做字符串名来处理,输出指针所指的字符串。既然这样,我们就别让他知道那是字符型指针,所以得进行类型转换,即:希望任何字符型的指针变量输出为地址的话,都要作一个转换,即强制char* 转换成void*
这里其实还有一个问题,我name输出的地址其实指针的指针,因为name本身就是一个指针变量,我又取了地址。再做一点点修改:
这里由于VS2017中使用这种char*的表达方式会造成程序崩溃,所以VS2017对其进行了控件管理。具体为:链接
用其他编译器是可以执行的,输出的就是"Cherno"的地址。
补:好像可以手动转换类型
char* name = (char*)"Cherno"
但这么写终究是不规范的,所以我们需要改变的时候写成数组,不需要改变的时候写成const char* 就好了,要么是数组要么是个指针。
OK我们继续听课
注意char* 用双引号,字符用单引号
不是字符串而是char*,不过很快就要变成字符串了
char* 自己后面会加\0,单个字符组成的数组不会。所以如果申请五位的数组放五个字符,cout就不知道该在哪里结束打印,就会得到乱码
多申请一位写个0、\0、不写都可以正确打印了。字符串就是个字符的集合。
std::string是怎么工作的呢?其实就是一个char数组
注意是const char,也就是说默认的string构造的是const char数组而不是char数组,但通过char*的隐式转换你想改的时候也能改。
另一件常见的事就是追加字符串。
不能写成
std::string name = "Cherno" + "hello!";
我们不能把两个数组或者两个指针直接相加
要写成
std::string name = "Cherno";
name += "hello!";
+=这个操作在string类会被重载。
操作符重载:把已经定义的、有一定功能的操作符进行重新定义,来完成更为细致具体的运算等功能
举个例子:操作符“+”完成float和int两种类型的加法计算,就是默认的操作符重载了。
有时候我们自己写的类也要实现某种运算,比如两个点的横纵坐标分别相加,那就需要自己写一个操作符重载函数
或者
std::string name = std::string("Cherno") + "hello!";
显式调用一个string构造函数,相当于创建了一个字符串,然后附加这个字符数组给他。
显式调用隐式调用这个东西我没太搞明白,但是好像不是很影响编程?
摸了
33.String literal
字符串字面量是指双引号引住的字符,也可以没有字符。
针对字符串字面量也有一些自己的函数,类如上面的字符串追加,直接在字符串字面量后面加个 s:
他其实是一个操作符函数,他返回标准字符串对象。
还有一种更简便的写法:C++源码转义技巧 R"()"
using namespace std::string_literals;
const char* example = R"(
Line1
Line2
Lin3
)";
再简便一点就是
const char* example = "Line1"
"Line2"
"Line3";
字符串字面量永远保存在内存的只读区域内,即使是用数组方法创建的也是将其复制到别的地方再进行改变(另一种创建方式const char*的改不了)
34.Const
const基本上就像你做出的承诺,承诺某些东西将是不变的。但你也可以绕过、你也可以打破承诺(笑
常量首字母大写,这是好的代码风格。
const int* ptr; // 不能修改该指针指向的内容,但是可以去指别人
int* const ptr; // 可以改变该指针指向的内容,但不能把指针本身重新赋值去指别的东西
int const* ptr; // 和第一种是一样的
const int* const ptr; // 啥都不能改变了
类内方法右侧的const:
class Entity
{
private:
int m_X, m_Y;
public:
int GetX() const
{
return m_X;
}
};
这个方法不会修改任何实际的类,是一个只读方法。所以要记得标记你的一些不修改类或者不应该修改类的方法为const
另一个关键字:mutable
意味着可以被改变的。
把变量设计为mutable的就可以在const方法中修改了
class Entity
{
private:
int m_X, m_Y;
mutable int var;
public:
int GetX() const
{
var = 2;
return m_X;
}
};
35、Mutable
关键字mutable一般有两种不同的用途:
一个是跟const一起使用