C++学习笔记(3)

十、枚举

十一、构造函数

构造函数基本上是一种特殊类型的方法,它在每次实例化对象时运行。它的主要用途是初始化该类。

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++中有三个基础的可见性修饰符:privateprotectedpublic

  • private:私有
  • protected:保护
  • public:公有(全局)

例子:protected的意思是这个Entity类和层次结构中的所有子类,也可以访问这些符号。我们可以在Player类中写X=2和调用Print(),因为Player是Entity的子类。
在这里插入图片描述
但我们仍不能在main里面这样做,因为它是一个完全不同的函数且在类的外面。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值