十、枚举
略
十一、构造函数
构造函数基本上是一种特殊类型的方法,它在每次实例化对象时运行。它的主要用途是初始化该类。
1、Init方法初始化
#include<iostream>
class Entity
{
public:
float X, Y;
void Print()
{
std::cout << X << Y << std::endl;
}
};
int main()
{
Entity e;
e.Print();
std::cin.get();
}
运行程序,我们得到Entity的位置,看似是随机的值。这是因为当我们实例化Entity,并为它分配内存时,实际上并没有初始化那个内存。
如果我们决定手动打印X和Y,因为是Public的,那么可以写代码打印出X,编译程序会得到错误:未初始化局部变量。因为我们试图使用没有初始化的内存。print函数虽然可以编译通过,但是显示的并不是我们所期望的。
所以我们需要一种方法,当构造一个Entity实例时,我们想要把X和Y设为0,除非已经被指定了其他值。因此创建一个叫Init的初始化方法,它是将X和Y设为0的void函数。现在当Entity对象实例创建时,我们可以调用e.Init();,然后打印值。
2、引入构造函数
当我们构造对象时,如果有办法直接运行这个初始化代码就好了,于是就出现了构造函数。构造函数是一种特殊类型的方法,这是一种每构造一个对象时都会调用的方法。我们像定义其他方法一样定义它,它没有返回类型,并且它的名称必须与类的名称相同。如下图Init方法被构造函数取代了。
如果不指定构造函数,仍然会有一个叫做默认构造函数的东西。但实际上这个默认构造函数什么都不做,Entity()函数体内完全为空,没有为变量进行初始化。像Java等语言的数据基本类型(int,float等),会自动初始化为0。但C++必须手动初始化所有基本类型。
3、带参数的构造函数
4、删除默认构造函数
class Log//只想使用我的Log类,而不希望创建实例
{
public:
static void Write()
{
}
};
int main()
{
Log::Write();//使用Log类
Log l;//创建实例
std::cin.get();
}
有两种不同的解决方案,一是我们可以通过设置private来隐藏默认构造函数,编译会得到错误,因为不能访问构造函数。不设置private编译会通过,说明C++为我们提供了一个默认构造函数。
我们也可以告诉编译器不需要那个默认构造函数,简单地写Log()=delete;。我们就不能调用Log,因为默认构造函数已经被删除。
十二、析构函数
析构函数可以说是构造函数的孪生兄弟。构造函数是在我们创建一个新的实例对象时运行,而析构函数则是在销毁对象时运行。任何时候一个对象要被销毁时,析构函数将被调用。构造函数通常是设置变量或者做任何需要的初始化,析构函数是卸载变量等东西,并清理使用过的内存,析构函数同时适用于栈和堆分配的对象。
比如使用new分配一个对象,当我们调用delete时,析构函数会被调用。而如果只是一个栈对象,当作用域结束,栈对象将被删除,此时析构函数也会被调用。
构造函数和析构函数在声明与定义时的唯一区别,就是放在析构函数前面的波浪号。
#include<iostream>
class Entity
{
public:
float X, Y;
Entity()
{
X = 0.0f;
Y = 0.0f;
std::cout << "Created Entity" << std::endl;
}
~Entity()//析构函数
{
std::cout <<"Destroyed Entity" << std::endl;
}
void Print()
{
std::cout << X<<","<< Y << std::endl;
}
};
void Function()
{
Entity e;
e.Print();
}
int main()
{
Function();
std::cin.get();
}
运行以上代码,我们可以看到先Created Entity,然后print函数运行输出X和Y,最后是Destroyed Entity。
析构函数的本质是一个特殊函数或特殊方法,在对象被销毁时调用。如果我们在构造函数中调用了特定的初始化代码,而不使用析构函数销毁这些东西,就可能会造成内存泄露。
十三、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.Move(5, 10);
player.X = 2;
std::cin.get();
}
我们在声明类型中写一个冒号,然后写public Entity,就把Player变成Entity的子类。现在Player拥有Entity的所有东西,比如说类成员X和Y。任何在Entity类中不是私有的东西,实际上都可以被Player访问。Player总是Entity的超集,意味着Player总是会拥有Entity的一切,也可能会更多。
因为有浮点数X和Y在Entity类中,我们可以打印Entity对象的大小到控制台。我们可以看到Entity的大小是8。
如果我们继续打印Player的大小,如果Player没有扩展Entity,它是一个独立的类,只有const char指针,应该是4字节的内存。但是因为Player扩展了Entity,它将继承了Entity类中的所有变量,所以大小是12。
十四、C++虚函数
1、无虚函数时的错误
虚函数允许我们在子类中重写方法。假设我们有两个类A和B,B是A派生出来的,也就是B是A的子类。如果在A类中创建一个方法,标记为virtual,则可以在B类中重新那个方法,让它做其他的事情。
#include<iostream>
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; }
};
int main()
{
Entity* e = new Entity();
std::cout << e->GetName() << std::endl;
Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;
std::cin.get();
}
Entity类唯一拥有的是一个名为GetName的公共方法,它会返回一个字符串。Player是Entity类的子类,它会存储一个名字m_Name,然后提供一个构造函数允许指定一个名字,以及一个叫GetName的方法返回这个名字m_Name,这个名字就是成员变量。
我们先创建了一个Entity,试着打印GetName()。然后创建一个Player命名为Cherno,再把Player的名字打印出来。
我们创建一个名为entity的变量,它会被赋值为p,这个p是指向Player的指针。然后打印这些,可以看到Entity被打印。但我们希望的是Player被打印,因为实际上是一个Player。
=================================
更好的例子是我们有一个PrintName函数,参数是一个Entity。PrintName函数就是调用GetName的方法,我们期望的是第6行的GetName用于Entity,第16行的GetName用于Player。但是运行代码发现打印了两次Entity。
发生错误结果的原因是,在我们通常声明函数时,我们的方法通常在类内部起作用,当调用该方法的时候,会调用属于该类型的方法。比如这个PrintName函数的参数是Entity,当我们调用GetName函数时,如果是在Entity里面,它会从Entity类中找这个叫做GetName的函数。
2、引入虚函数
若我们希望C++能意识到PrintName函数传递的Entity实际上是Player,让它调用第16行的GetName。这就是虚函数出现的地方,虚函数引入了一种叫做Dynamic Dispatch(动态联编)的东西,它通常通过v表(虚函数表)来实现编译。v表就是一个表,它包含基类中所有虚函数的映射,这样我们可以在它运行时,将他们映射到正确的覆写(override)函数。如果想要覆写一个函数,必须将基类中的函数标记为虚函数。
所以我们在基类Entity类中的GetName函数面前加上virtual。这样就可以告诉编译器生成V表,如果GetName函数被覆写(override)了,我们就可以指向正确的函数。
在C++11中引入了override,将覆写函数GetName标记为关键字override,这并不是必须的,但是它让代码更具可读性。
std::string GetName() override { return m_Name; }
3、虚函数的额外开销
但虚函数并不是免费的(无额外开销),首先创建v表需要额外的空间,这样我们就可以分配到正确的函数。包括基类中要有一个指针成员,指向v表。当我们每次调用虚函数时,需要遍历这个表,来确定要映射到哪个函数,这是额外的性能损失。
十五、C++纯虚函数
1、无纯虚函数时的错误
C++纯虚函数本质上与其他语言(如Java或C#)中的抽象方法或接口相同。纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
#include<iostream>
class Entity
{
public:
virtual std::string GetName() { return "Entity"; }//virtual--虚了
};
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);
std::cin.get();
}
Entity类中有一个虚函数GetName,然后我们重写了这个函数在Player类中。在基类中这个GetName函数有函数体,意味着在Player类中即便不重写,仍然可以调用Player.GetName,然后返回Entity字符串。
但实际上我们可能想要强制子类,为特定的函数提供自己的定义。在面向对象的编程中,创建一个类由未实现的方法组成,然后强制子类去实际实现它们是非常正常的。这通常被称为接口,因此类中的接口只包含未实现的方法作为模板,因此我们不可能实例化这个类。
2、引入纯虚函数
因此我们去掉GetName的方法体,写成等于0。注意这里依然是定义成virtual虚函数,但等于0本质上使它成为一个纯虚函数,这意味着它必须在子类中实现。我们就不在具有实例化Entity类的能力。
我们必须给它一个子类,来实现这个函数。例如Player类,当然这里要提供一些字符串,此时编译通过。
这是因为我们实际实现了GetName函数,如果我们决定注释掉第16行的实现,可以看到不能进行实例化了。我们只有在实现了所有纯虚函数以后,才能够实例化。
3、引入C++接口
假设我们想要编写一个函数来打印这些类的名字,我写上void Print,我想写???类型作为参数,obj作为例子,然后要做的是打印类名,就像obj->GetClassName()这样。
void Print(? ? ? * obj)
{
std::cout << obj->GetClassName() << std::endl;
}
这里我们需要的是一个类型来保证有GetClassName这个函数,这就是所谓的接口。我们可以把???类当做Printable,然后创建一个新的类叫做Printable,它会创建一个public的virtual字符串函数,返回一个字符串,GetClassName函数是纯虚的。
class Printable
{
public:
virtual std::string GetClassName() = 0;//纯虚
};
然后我们要让Entity去实现这个接口。此时Player已经是一个Entity了,所以不需要实现Printable接口。
class Entity :public Printable
{
public:
virtual std::string GetName() { return "Entity"; }
};
如果Player不是Entity的子类,我们需要添加一个逗号来实现这个接口。
class Player :public Entity,Printable//加一个父类
{
private:
std::string m_Name;
public:
Player(const std::string& name) :m_Name(name) {}
std::string GetName()override { return m_Name; }
};
虽然我们把Printable叫做接口,但它其实只是一个类。因为它不过有个纯虚函数而已。
我们继续在Entity类添加一个GetClassName函数,此时无法实例化的问题就解决了。
但还会出现一些问题,我们还没有为Player提供一个覆写函数,如果现在去打印,调用Print函数,分别使用e和p参数。我们会发现打印了两次Entity,因为我们还没有在Player中提供定义。
因此我们在第12行加上override,在第22行覆写函数GetClassName,就会得到正确的类名。
所有这些都来自于一个Print函数,它接受Printable作为参数。它不管具体是什么类。
十六、C++可见性
可见性是一个属于面向对象编程的概念,它指的是类的某些成员或方法实际上有多可见,这里的可见性是指谁能看到它们,谁能调用它们,谁能使用它们。可见性是对程序实际运行方式完全没有影响的东西,对程序性能类似的东西也没有影响。纯随是语言中存在的东西,帮助我们写出更好的代码。
C++中有三个基础的可见性修饰符:private、protected 和 public。
- private:私有
- protected:保护
- public:公有(全局)
例子:protected的意思是这个Entity类和层次结构中的所有子类,也可以访问这些符号。我们可以在Player类中写X=2和调用Print(),因为Player是Entity的子类。
但我们仍不能在main里面这样做,因为它是一个完全不同的函数且在类的外面。