【笔记】Cherno C++ Tutorial note 01

@[TOC]

指针Pointers

指针是一个整数,储存一个内存地址。

类型没有意义,只是在这个地址的数据是这个类型

1
2
3
4
5
6
7
8
9
int var = 8;
void* ptr = &var;
*ptr = 10;// 指针解引用,访问对应地址储存的值
=========================
char* buffer = new char[8];
// 一个char占用1个字节,分配了8个字节的内存,并返回一个指向这块内存的开始地址的指针
memset(buffer, 0, 8 );// 为指定地址填充值
delete[] buffer;// 删除值
buffer = nullptr;// 回收指针

指针本身也只是变量,这些变量也储存在内存中(double pointers,triple pointers-指针的指针)

1
2
3
char* buffer = new char[8]; 
memset(buffer, 0, 8 );
char** ptr = &buffer;// 在32位程序里,一个内存地址是32位的

引用Reference

指针和引用基本是一回事,引用是对某个存在变量的引用。引用本身不是一个新的变量,并不真正占用内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int a = 5;
int* b = &a;
int& ref = a;//这里引用的&符号不同于指针,它是变量类型的一部分,紧跟着变量类型
//在这里创建了一个a的别名,ref其实不是一个实际的变量,只是a所在地址的数据的别称
void increment(int value) { // 在这里是传值调用(passing by value)
value++;
}
increment(a);//将a拷贝到了函数中,创造一个全新的变量value,a并不会发生改变

void increment(int* value) {//传递地址,调用a的内存地址而不是a本身给函数
(*value)++;//解引用,改变地址储存的数值而不是地址本身.
//递增符号会优先执行,所以添括号保证先解引用再递增
}
increment(&a)//传递变量地址

void increment(int& value) {//引用传递,passing by reference
//和传递地址是一样的,但是看起来更简单了
value++;
}
increment(a)

引用只是让代码更好看了,没有什么事情是引用能做但指针不能的

一旦声明一个引用,就不能更改它所引用的对象

类Class

C++支持面向过程,基于对象,面向对象,泛型编程

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

由类类型制成的变量叫对象,新创建对象的过程叫做实例化

默认情况下,类成员的访问控制都是私有的,只有类内部的函数才能访问——提供公有函数访问接口

类内的函数称为方法(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
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)
std::cout << "[ERROR]: " << message << std::endl;
}
void Warn(const char* message){
if (m_LogLevel >= LogLevelWarning)
std::cout << "[WARNING]: " << message << std::endl;
}
void Info(const char* message){
if (m_LogLevel >= LogLevelInfo)
std::cout << "[Info]: " << message << std::endl;
}
}
int main(){
Log log;
log.SetLevel(log.LogLevelWarning);
Log.Error("Hello!");
Log.warn("Hello!");
Log.Info("Hello!");
std::cin,get();

}

Static静态关键字

类外或结构体外的static修饰符在link阶段是局部的,它只对定义它的编译单元(.obj)可见

类内或结构体里面的static表示这部分内存是这个类的所有实例共享的,即就算实例化了很多次这个类或结构体,这个静态static变量只会有一个实例,并被共享。

静态方法也是如此,没有该实例的指针(this)

类外的static。

static.cpp:

1
2
static int s_Variable = 5; // 这个变量在link的时候只对这个编译单元(.obj)里的东西可见
// static变量或函数表示在link到它实际的定义时,linker 不会再这个编译单元.obj外面寻找它的定义

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
int s_Variable = 10;
int main() {
std::cout << s_Variable << std::endl;
//输出10 不会与static.cpp中的static变量发生重复定义
std::cin.get();
}
=========================
#include <iostream>
extern int s_Variable;//外部链接,会在其他编译单元里寻找定义,但如果static.cpp中是静态声明,就找不到了,静态声明使其他编译单元不能访问s_Variable,相当于私有变量
int main() {
std::cout << s_Variable << std::endl;
std::cin.get();
}

类内Static

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>
struct Entity{
int x,y;
void Print() {
std::cout << x << ", " << y <<std:endl;
}
}

int main() {
Entity e;
e,x = 2;
e.y = 3;
Entity e1 = { 5, 8 };
e.Print();
e1.Print();//2, 3 /n 5, 8
std::cin.get();
}
============================================
#include <iostream>
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 = 2;
e.y = 3;
Entity e1// = { 5, 8 };//x和y不再是类的成员, 这种写法会报错
e1.x = 5;
//等同于Entity::x = 5,虽然写了e1.x,但其实并不属于类成员
e1.y = 8;
e.Print();
e1.Print();//5, 8 /n 5, 8
std::cin.get();
//static 修饰的struct全局会共用一块内存
//无法通过静态方法访问非静态的成员变量
}

局部静态Local Static

变量的

  • 生命周期:变量被删除前在内存中停留多久

  • 作用域:在哪里能访问这个变量

一个静态局部变量允许我们定义一个变量,生命周期是整个程序,但作用于被限制在这个函数里或任何作用域。和类内作用域的静态变量是一样的。

1
2
3
4
5
6
7
#include <iostream>
void Function() {
static int i = 0;
i++;//如果没有静态,则每次调用函数时,i的值被设为0,自增后输出1
//局部静态变量和在函数外定义全局变量的区别在于作用域
std::cout << i << std::endl;
}

ENUMS枚举

1
2
3
4
5
6
7
8
9
10
11
12
enum Example {
A,B,C
//第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。
// 可以指定任意数组,默认顺序
};
int main() {
Example value = B
}
============
enum Example : unsigned char {// 可以指定任意类型

}
1
2
3
4
5
6
7
8
9
class Log{
public:
enum Level{
Error, Warning, Info
};
// const int LogLevelError = 0;
// const int LogLevelWarning = 1;
// const int LogLevelInfo = 2;
}

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
class Entity{
public:
float X, Y;
void Init() {//需要一种方式,每当构造Entity实例时,需要将X,Y初始化
X = 0.0f;
Y = 0.0f;
}
void Print() {
std::cout<< X<< ", " << Y << std::endl;
}
}
int main() {
Entity e;
e.Init();
e.Print();
}
=================================================
class Entity{//构造函数就是每当构建一个对象时都会调用的方法
public:
float X, Y;
// Entity() = delete;删除默认的构造函数
Entity(){
X = 0.0f;
Y = 0.0f;
}// 可以写许多构造函数(重载)
Entity(float x, float y){
X = x;
Y = y;
}
}
int main() {
Entity e;
Entity e2(10.0f, 5.0f);
e.Print();
e2.Print()
}

当使用static声明类中元素时,并不会调用构造函数(因为static是所有实例共有的)

特殊类型的构造函数:拷贝构造函数Copy Constructor, Move Constructor

Destructors析构函数

constructor 在创建一个对象实例时运行,而析构函数在摧毁一个对象时运行

构造函数通常用来设置变量或者进行某些需要的初始化

当对象即将结束生命周期时,需要清理原本占用的内存,

析构函数同时使用堆和栈来给对象分配空间,所以如果使用new给对象分配空间,而对应的需要使用delete调用析构函数释放空间

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
class Entity{//构造函数就是每当构建一个对象时都会调用的方法
public:
float X, Y;
Entity(){
std::cout << "Created Entity!" << std::endl;
X = 0.0f;
Y = 0.0f;
}
Entity(float x, float y){
X = x;
Y = y;
}
~Entity(){
std::cout << "Destroyed Entity!" << std::endl;
}
}
void Function(){
Entity e;
e.Print();
}
int main() {
Function();
/*
Created Entity!
0, 0
Destroyed Entity!
实例e的生命周期只在Function函数中,因此Function结束后,就调用析构函数释放了空间
*/
std::cin.get();
}

Inheritance继承

(Code Duplication)

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
class Entity{
public:
float X,Y;
void Move(float xa, float ya) {
X += xa;
Y += ya;
}
};
class Player : public Entity{// Entity中任何非私有的变量或函数,Player都可用
public:
const char* Name;
/*float X,Y;
void Move(float xa, float ya) {
X += xa;
Y += ya;
}*/
void PrintName() {
std::cout<< Name <<std::endl;
}
};
int main() {
Player palyer;
player.PrintName();
palyer.Move(5, 10);
}

Virtual Functions虚函数

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

比如B是A的派生类(子类),如果指定A中某虚函数方法,则可以在B中覆盖该方法做别的事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Entity {
public:
std::string GetName() { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_Name;
public:
Player(const std::string& name)//这里使用了初始化列表,在构造时先于函数体执行,代表将name赋值给m_Name
: m_Name(name) {}
std::string Getname() { retrun m_Name; }
};

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

Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;//Cherno
PrintName(p)//Entity
}

虚函数引入了动态分派Dynamic Dispatch,通常使用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
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() { retrun m_Name; }
std::string Getname() override { retrun m_Name; }
//在C++11中,可以将覆写的函数用关键词override标记
};

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

Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;//Cherno
PrintName(p)//Cherno
}

虚函数的性能代价,是需要遍历整个虚函数表查看映射

Pure virtual function纯虚函数

纯虚函数允许我们定义一个在基类中没有实现的函数,然后迫使在子类中实际实现

在接口中,类仅仅包含未实现的方法并充当一种勉强的模板,这种类无法实例化,必须在子类中实现

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
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) {}
//std::string Getname() { retrun m_Name; }
std::string Getname() override { retrun m_Name; }
//在C++11中,可以将覆写的函数用关键词override标记
};

void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main() {
// Entity* e = new Entity();
// PrintName(e)//Entity无法再实例化Entity类
Entity* e = new Player("");
PrintName(e);

Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;//Cherno
PrintName(p)//Cherno
}

只有当一个类提供所有纯虚函数的实现,才能实例化。

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
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 { retrun 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->GetClassNmae <<std::endl;
}
int main() {
Entity* e = new Entity();
// PrintName(e)//Entity
Print(e)//Entity
Player* p = new Player("Cherno");
// std::cout << p->GetName() << std::endl;//Cherno
// PrintName(p)//Cherno
Print(p)// Player,如果没有在Player中定义,调用的是Entity中的override,输出Entity
}

Visibility访问控制

本质指类中的成员数据及函数的可访问性

基本的访问控制修饰符:public公有, protected保护, private私有

1
2
3
4
5
6
7
8
9
10
class Entity {
int X,Y;// 默认私有,意味着只有Entity类内才能访问这些变量,包括读写(除非是friend)
public:
Entity() {
X = 0;
}
};
class Player : public Entity {//Player的构造函数也不能访问Entity的私有变量

};

Array数组

1
2
3
4
5
6
7
int example[5]; //声明并分配空间
example[0] = 0;

std::cout << example[0] << std::endl;
std::cout << example << std::endl;//输出example的地址,这是一个指针类型
int* ptr = example;
*(ptr+2) = 6;//example[2] = 6,example地址后移两个int的空间并解引用,得到example[2]的值赋值为6

new 运算符动态分配内存空间,

delete 运算符释放,否则即便局部作用域的程序运行完毕,内存也不会被回收。

为class分配内存空间时,还会同时运行构造函数。

实例的地址所指向的就是成员变量的地址;但如果用new关键字来分配,为成员变量建立了一个指针,实例的地址指向的是指针的地址。

Strings字符串

1
2
3
4
5
6
7
8
const char* name = "Cherno";
// 字符串在内存中末尾需要有0 填充作终止符
const char name2[6] = {'C','h','e','r','n','o'}
std::cout<<name2<<std::endl;//输出的Cherno后面会跟随后面内存所对应的无意义的ASCII码,如锟斤拷
const char name2[7] = {'C','h','e','r','n','o'0}
std::cout<<name2<<std::endl;// 加上终止符,能正常输出Cherno
name[2] = 'a';

standard string

1
2
3
4
5
6
7
8
9
10
11
#include <string>
int main() {
std::string name = "Cherno";
//std string 有一个构造函数,接受一个char ptr或const char ptr
//这里是(const char[7])
//std string 还有一些成员函数可以使用
//std::string name = "Cherno" + "hello!";这是不可以的,这相当于把两个数组相加
name += "hello!";//这是可以的,将一个指针和name相加,利用string的重载+ 拼接字符串
std::string name2 = std::string("Cherno") + “hello!";
//这也是可以的
}

string literal

字符串是不可变的。默认为const,在内存中只读

1
2
3
//正如上面所说,字符串的末尾默认有\0的填充,作为终止符
//"Che\0rno" ="Che\0rno\0"
//实际的字符长度因为中间的终止符,只有3
1
char name[] = "Cherno"// 将它定义为数组,而不是字符串或字符串指针,就可以修改了
1
2
3
4
const char* name = u8"Cherno";//每个字符一个字节
const wchar_t* name2 = L"Cherno";//宽指针
const char16_t* name3 = u"Cherno";//每个字符两个字节的16位字符串
const char32_t* name4 = U"Cherno";//32位字符或每个字符四个字节

在C++14中,还可以

1
2
3
4
5
6
7
8
9
10
using namespace std::string_literals;
std::string name0 = "Cherno"s + "hello";
std::wstring name0 = L"Cherno"s + L"hello";
std::u32string name0 = U"Cherno"s + U"hello";
//R前缀
const char* example = R"(Line1
Line2
Line3
Line4)"

Const常量

就像一个承诺,认为一个变量不改变,但是否信守承诺还是取决于你自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int MAX_AGE = 90
const int a =5;
int* a = new int;
const int* a = new int;// 意味着不能再更改指针的内容,但是可以读取,也可以更改指针。
*a = 2//不可操作
a = (int*)&MAX_AGE; //可以操作
================================
//也等同于:
int const* a = new int;
=================================
int* const a = new int;// 完全相反,不能更改指针,但可以更改指针的内容
*a = 2;// 可以操作
a = (int*)&MAX_AGE;// 不可操作
================================
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
class Entity{
private:
int* m_X,m_Y;
mutable int var;
public:
int GetX() const {// 此方法不会修改任何类成员变量,是一种只读方法
return m_X;
}
int GetX() {
m_X = 0
return m_X;
}
void SetX(int x) {
m_X = x;
}
const int* const GetX() const {// 此方法的指针和此方法都不能更改,且不会修改任何成员变量。
retrun m_X;
}
};

void PrintEntity(const Entity& e){
std::cout<<e.GetX()<<std::end;;
//因为这里是引用了常量,因此不能对类有任何修改,在调用类方法时,也只能调用只读方法,如果没有类方法没有const,则不能调用,因为无法保证没有更改类成员。
//有时同名的类方法会有const版本和无const版本,在这里调用时会自动使用const版本
}

Mutable关键字

与const共同使用

1
2
3
4
5
6
7
8
9
10
11
class Entity
{
private:
std::string m_Name;
mutable int m_DebugCount = 0;
public:
const std::string& GetName() const {
m_DebugCount++;//在这里,本来该方法不能修改类成员变量,但mutable是例外,可以修改了,mutable 将其标记为可变的。
return m_Name;
}
};

lambda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int x = 8;
auto f = []() {
std::cout<<"Hello"<<std::endl;
}//lambada函数
auto f = [&]() {//传地址
std::cout<<x<<std::endl;
}
auto f = [=]() {//传值
x++;//在这里因为是值传递,相当于用新的变量储存,x不会发生改变
std::cout<<x<<std::endl;
}
auto f = [=]() mutable {
x++;//外部的x变量仍然不会改变
std::cout<<x<<std::endl;//9
};
f();//x = 8

Member Initializer Lists成员初始化列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Entity
{
private:
std::string m_Name;
int m_Score;
public:
Entity() {
m_Name = "Unknown";
}//可以使用初始化列表来完成:
Entity() : m_Name("Unknown"),m_Score(0)//需要按照声明顺序定义
{
}
Entity(const std::string& name)
m_Name(name)
{
//m_Name = name;
}
}
int main() {
Entiyt e0;
}

此外,实际上在不同的构造函数中,如果对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Entity
{
private:
std::string m_Name;
Example m_Example;
public:
Entity() {
m_Name = "Unknown";
}
Entity()
{
m_Name = "Unknown";
m_Example = Example(8)////调用了构造函数,不带参数,来源于m_Example声明。也调用了参数为8的构造函数。
}
Entity() : m_Name("Unknown"),m_Example(Example(8)),m_Example(8)//只调用一次参数为8的构造函数,节约性能。
{
}
Entity(const std::string& name)
m_Name(name)
{
//m_Name = name;
}
}

Ternary Operators三元操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (s_Level>5){
s_Speed = 10;
} else {
s_Speed = 5;
}
s_Speed = s_Level>5 ? 10 : 5;

std::string rank = s_Level > 10 ?"Master": "Beginner";//三元操作符能做到更快
std::string rank;//需要先声明,并调用构造函数
if(s_Level>10)
rank = "Master";//还要进行重载
else
rank = "Beginner";

s_Speed = s_Level > 5 ? s_Level > 10 ? 15 : 10 : 5;
s_Speed = s_Level > 5 ? (s_Level > 10 ? 15 : 10 ) : 5;
s_Speed = s_Level > 5 && s_Level < 100 ? s_Level > 10 ? 15 : 10 : 5;