C++是一门OOP(面向对象)的语言。
而C语言只是一门面向过程的语言。
里面有些思路需要重新改变。
前情提要:文章较长,请根据目录自行选择
为一个场守望先锋街机先锋统计数据
如果是面向过程的程序员,可能会考虑:
记录每个玩家所选的英雄,击杀人数,对对面伤害量,治疗量,承受伤害量,命中率等等。
同时还要记录每个英雄的移动,释放技能,技能是否命中,技能造成的伤害的计算,甚至还要考虑技能的运动等等。
甚至还要考虑对方的技能对自己的影响,以及队友的技能对自己的影响。
用一个主函数 main 来获取所有数据。
调用另外一些函数来分别计算英雄的移动,技能伤害,技能影响,收到的伤害等等。
再调用另外一部分函数来显示结果。玩过守望先锋的玩家都知道,对于这些数据的 计算,统计,显示,都是瞬时性的,会随着玩家的操作不断变化。而且,一次组队并不是只打一场,可能会由两场左右。
守望先锋并不是传统的FPS游戏,而是FPS与Moba游戏的结合
而且,对于不同的英雄都要根据英雄的技能去计算,统计不同的数据。
如:安娜要分别统计开镜和不开镜的命中率,以及睡针的命中人数。
而法拉要统计火箭弹直接命中人数等等。
而不同模式,又会有不同的变化,对英雄的移动,技能的影响也是不同的。
如:战斗狂欢,让所有英雄的血量翻倍,技能CD减半,这就会造成很多不一样的“化学反应”。
像我有一次战斗狂欢打了 1.5h 才打完(路霸一直卡车旁边,都在卡加时),对于普通快速10分钟左右可是相当长的。对于比赛的各个数据上限又该怎么设置?
而不一样的地图对角色的移动,技能的影响又是不一样的。比如:地形杀。
但对于数据怎么办?又要重新统计。这相对于计算机而言都太复杂。(如果是这样,我相信网易的服务器早就崩了)。
总之,对于过程性编程,首先要考虑遵循的步骤,然后要考虑如何表示这些数据。
如果是一位OOP程序员,其不仅要考虑如何表示数据,还要考虑如何使用数据 。
我要跟踪什么呢?当然是每个玩家,因此要有一个对象来表示每个玩家的各个方面的数据。可以选择为各个不同的英雄定义不同的类,在通过类来为玩家创建对象,让计算机执行计算玩家之间的数据交互,可以自动计算。我们要做的仅仅是研究如何跟踪或表示,每个玩家之间的数据交互。
对于OOP程序员,只需为每个不同英雄定义属于他们的类,每次游戏开始,再为每个玩家根据其选择创建对象。再利用算法跟踪玩家之间的数据交互即可
这不比面向过程编程简单?(不知道我理解的对不对)
ps:Dv.A爱你呦~
这个世界太复杂,处理复杂性的方法之一是简化和抽象。
守望先锋游戏中,通过为每个英雄定义一个类,为每个玩家创建一个对象来统筹数据。
在C++中,用户定义类型指点是实现抽象接口的类的设计。
(说人话就是按照需求,定义类。)
我们都知道建一栋房子首先需要什么?
需要图纸,房子的结构图纸。
有了图纸,我们就可以根据图纸见很多大同小异的房子(一张图纸建出来的么)。
房子可以建很多,而图纸始终是那一张。
这样可以帮助我们更好理解。
对像是一个实体,而类只是一个自定义的类型。
就像int a;
a 是一个实体变量,在内存中有空间,而 int 只是一个类型,表明 a 的身份。
这样应该理解了吧。
基本类型(内置类型)定义变量完成了三项操作
对内置类型而言,有关信息全部被内置到编译器中。但是C++在自定义类型是,必须要自己提供所有信息。
类的实例化:就是根据类创建一个对象
C语言也是存在”类“。我们常用的结构体。
比如我们用C语言去是实现一个栈
#define CAP 4
typedef int STData;
typedef struct Stack//结构体用于维护栈
{
int top;//栈顶标记
STData* arr;//栈的指针
int capacity;//栈的容量
}STstack;
这是对于栈的数据的定义,栈就是我们用结构体定义的一个”自定义类型“–”栈类型“。(因为它并不具有自定义类型的全部信息)
void InitStack(STstack* st);//栈的初始化
void StackPush(STstack* st, STData n);//元素入栈
STData StackPop(STstack* st);//元素退栈
void StackExpansion(STstack* st);//扩容
int StackEmpty(STstack* st);//判断栈是否为空
void StackDestory(STstack* st);//销毁栈,防止内存泄漏
void StackPrint(STstack* st);//打印栈的元素,但前提是要退栈才能得到元素
这些函数是我们能够对栈执行的操作。
但是数据和执行方法都是分离的。
这就导致我们重点关注的是对栈操作的整个过程。
就是,我们这一步选择入栈,下一步选择弹栈。
我们会将栈看为一个类,也就是一个类型,一个自定义类型。
而一个类型,我们需要两个部分
1,类的成员变量(类的属性)
2,类的成员函数(类的行为/能够执行的操作)
这个也满足上面关于类型的三项操作。
对于C++而言,就不再用结构体这个概念了,该叫类。
C++将struct
从C语言的结构体 - 升级到 - 类
你目前可以认为C++中的类就是C语言的结构体除了定义结构体成员变量还有结构体成员函数。
比如:
#define CAP 4
typedef int STData;
struct Stack//结构体用于维护栈
{
//结构体成员变量
int top;//栈顶标记
STData* arr;//栈的指针
int capacity;//栈的容量
//结构体成员函数
void InitStack(STData capacity=4);//栈的初始化//缺省
void StackPush(STData n);//元素入栈
STData StackPop();//元素退栈
void StackExpansion();//扩容
int StackEmpty();//判断栈是否为空
void StackDestory();//销毁栈,防止内存泄漏
void StackPrint();//打印栈的元素,但前提是要退栈才能得到元素
};
你目前可以理解成长这样。
而且,其类名就可以是其自定义类型的类名。
直接Stack a1;
。这就定义了一个对象。
而且也不用传上面指针过去了,可以a1.Init()
,就可以调用那些成员函数。
由于C++兼容C语言,这可以是一个类。但C++会使用class
(其中文有类的意思)。
class ClassName//class是定义类的关键字 后面接一个类的名字
{
//类体:由成员变量和成员函数组成
};
与定义结构体很像。
对于类而言,其中的成员函数。你可以直接在类中定义。也可以在.h文件
类中声明,去.c文件
中定义。但是,在类中的成员变量,那只是声明,并不是定义。
声明:告诉编译器,我有一个这样的东西在这,但并未实现。
定义:根据声明,去实现这个对象的需求。
形象来说就是:
游戏公司建了一个新游戏的文件夹,并向外发布公告,说未来会发布一款新游戏,也可能文件夹都还没建好。
这就是声明,只是告诉你有,但你不知道游戏剧情、内容、玩法是什么。只知道有这个游戏。
当游戏发售后,你买了。你就可以知道游戏的所有内容。
这就是定义。
声明是对的是一个属性,而定义对的是一个实体。
C++对于类中的成员提供了访问限定符
private(私有),public(公有),protected(保护)
。
他们描述了对类成员的访问控制
根据类创建的对象,都可以访问到对象类的公有部分,并且只能通过公有函数来访问对象类的私有成员
访问控制,也是对对象类的数据的保护。
OOP编程的主要目标之一是隐藏数据,因此这类数据通常放在私有部分,
而组成类接口的成员函数放在公有部分,否则就无法调用这些函数。
由于C++兼容C语言,且C++对C语言的结构进行了拓展,如
struct A
{
};
class A
{
};
在C++中,都是类。
两者区别
对于struct
创建的类,里面的成员默认公有。
对于class
创建的类,里面成员默认私有。
class Ana
{
public:
void ShowData();//展示数据
void Teletherapy(bool input);//远程治疗
void SleepyNeedle(bool input);//睡针
private:
char Name[20];//名字
int Weight;//体重
int Height;//身高
double Speed;//移动速度
double Blood;//血量
//睡针类 睡针
};
私有数据,我们是不能修改或访问的,
只能通过公有函数去操作,或通过公有函数来了解私有数据的状态。
如果我们能直接访问到私有数据,那么我们也能对私有数据进行修改,但那样不就成开挂了吗?(正常人谁打竞技游戏开挂)。
所以,一般情况下,成员变量都是私有的,想给你用的成员函数是公有的,不希望被调用的函数是私有的。
根据规则:计算类的大小,是不考虑类中成员函数的,只计算类中成员变量,同时还要考虑结构体内存对齐规则,也就是计算结构体大小的规则。
结构体内存对齐规则
那就计算一下
#include
class Ana
{
public:
void ShowData();//展示数据
void Teletherapy(bool input);//远程治疗
void SleepyNeedle(bool input);//睡针
private:
char Name[20];//名字
int Weight;//体重
int Height;//身高
double Speed;//移动速度
double Blood;//血量
};
int main(void)
{
Ana a1;
Ana a2;
std::cout << "a1 = "<<sizeof(a1)<<std::endl;
std::cout << "a2 = "<<sizeof(a2)<< std::endl;
return 0;
}
来看看其内存的概念图
对于类中的成员函数是不会进行计算大小的。
这也是类中一个比较重要的知识点
通过一个日期类来分析(因为日期类比较简单)
#include
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)//缺省参数
{
_year = year;
_month = month;
_day = day;
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day<<"日"<<std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;//类的实例化
Date d2;
d1.Init(2002,2,5);//对象d1初始化
d2.Init(2019,4,1);//对象d2初始化
d1.print();//打印
d2.print();//打印
}
在调用对象类的成员函数初始化对象 d1,d2 。
在成员函数中可以访问到对象封装的成员变量。
再介绍一下
对于类的实例化,创建对象。
Date d1;//类的实例化
Date d2;
系统会为对象d1 d2
在栈中分配空间。
但是只是对象中的成员变量分配在该对象的空间内。
但是对于成员函数,其是在一个代码公共区段,而不是在每个对象空间的内部。不会每调用一次就分配一块空间。
每个对象都可以访问那个区段。
我们就该想,当调用函数时,进入公共区段。如何确定调用的是那个对象的成员变量呢?
其实,传递过去的参数,不仅仅有显示声明定义的参数,还有一个隐藏的this
指针。
即,C++编译器会为每一个非静态成员函数配备一个隐藏的指针参数。
该指针指向当前对象(函数运行时调用该函数的对象)。
当然,你也可以这样写
void Init(int year = 1, int month = 1, int day = 1)//缺省参数
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
但是我们不能自己在参数列表中加一个this指针,因为编译器自己会传递,会处理。要是自己加了,就会造成参数缺失。
这样也是允许的,不容易搞混。
成员函数调用的真实样子
d1.Init(2002, 2, 5);//->Init(&d1,2002, 2, 5);
d2.Init(2019, 4, 1);//->Init(&d2,2019, 4, 1);
d1.print();//->print(&d1)
d2.print();//->print(&d2)
注意
而且,对于类的成员变量,最好命名独特一点,不然,如果和缺省参数名字一样,编译器会无法识别,
this
指针指向不明,从而报错
就我目前微薄的知识,this
指针是储存在栈中的。
因为,this
指针毕竟是一个形参。而形参和局部变量都是储存在栈中的。
可以随着成员函数的调用而创建,函数结束就销毁。但是不同编译器是按照不同的规则的,比如VS就是通过ecx寄存器自动传递。
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
p->Show();
}
创建一个对象指针,并把它初始化为空。
p->PrintA();
如果this->_a
是行不通的,本身就是空指针,指向空,根本就不会有指向有权限的空间。算是非法访问。
但是p->Show();
,并未访问对象内,而是访问到类的公共区段。不会造成非法访问。
类的默认成员函数即使我们
不自己声明定义,编译器也会自动创建定义
对于一个什么成员函数都没有的类,是一个空类。但是,当编译器处理时,会自动生成6个默认成员函数来防止出错。
事实上,真正用处大的是前4个,后面两个,基本上没多大用处。
该函数可以完成对对象的初始化。
C++提供这个默认成员函数。是为了解决,没有初始化就使用对象的问题。
对于日期类
class Date
{
public:
//void Init(int year = 1, int month = 1, int day = 1)//缺省参数
//{
// this->_year = year;
// this->_month = month;
// this->_day = day;
//}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
如果没有Init()
函数,如果是C语言的话,就会报错,因为没有初始化。
就比如这样
int main(void)
{
Date d1;
Date d2;
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
我们没有初始化,却直接打印。事实上,编译器输出
虽然没有报错,但是却输出了随机值。这样也是防止了程序直接崩溃的问题。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员
都有 一个合适的初始值,并且在对象的生命周期内只调用一次
这是一个默认成员函数,意思就是如果我们自己不定义,系统就会自动生成。
加上构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
如果我们自己不定义,系统会自动生成,我们定义,系统就调用我们定义的函数。
那我就加一个打印,来看看结果
Date(int year = 1, int month = 1, int day = 1)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
构造函数虽然名字感觉像是负责为对象创建分配空间,
但实际上,起作用只是完成对对象的成员变量的初始化,防止未初始化就使用而造成的程序崩溃。
特性
void
是返回一个空,实际上还是有返回值,而构造函数其根本就没有返回类型)虽然可以重载
但是
Date()
{
std::cout << "Date()" << std::endl;
_year = 10;
_month = 1;
_day = 1;
}
Date(int year=1, int month = 1, int day = 1)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
却不受欢迎的,因为编译器也不知道如何处理。会报错。
但可以这样
Date(int year,int month = 1, int day = 1)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
在初始化的时候,自己无论如何都要提供一个值。
或者
Date()
{
std::cout << "Date()" << std::endl;
_year = 10;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
std::cout << "Date()" << std::endl;
_year = year;
_month = month;
_day = day;
}
可以运行,但还不如全缺省的构造函数。
内置类型:内置类型就是语法已经定义好的类型:如int/char…,
自定义类型:是我们使用class/struct/union自己定义的类型
对于普通的成员变量而言,其默认的构造函数,感觉效果并不怎样。但是,如果有一个成员变量也是类?(自定义类型)。
#include
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
private:
int a;
};
class Date
{
public:
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
A a1;
};
int main(void)
{
Date d1;
Date d2;
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
其输出结果为
像上面那段代码,对于类Date
,我们自己没有定义构造函数,而编译器会生成默认构造函数,取对成员变量初始化。
而对于内置类型,直接初始化一个随机值,防止程序崩溃。
但是对于自定义类型,编译去会取调用它的构造函数对自定义类型的成员变量进行初始化
我定义了A的构造函数是为了可以更好的看到自定义类型的构造函数被调用。
对于内置类型,编译器基本不会处理,
对于自定义类型,编译器会去调用自定义类型的构造函数
大多数情况下,默认构造函数都不太顶用,最好还是自己写一个满足要求的。
一般而言,构造函数有三种(不需要传递参数就可以调用的函数)
Date()
{
_year = 10;
_month = 1;
_day = 1;
}
3.我们自己写的全缺省的
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
不过,就我而言,还是推荐使用全缺省的构造函数。
因为,这个完全可以兼容前面两种,
既可以带参,
也可以不带参,
还可以带部分参。
这个和名字其实不太一样,析构函数并不是负责销毁对象的空间,只是做一些,资源清理的工作。
就跟内置类型的对象一样,类对象的销毁都是由编译器完成的,其本身不具备这种操作。
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。
而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特征:
对于日期类,额,这个析构函数基本没什么作用。但是对于栈、队列等数据结构的类还是相当有意义的。
清理资源可以防止内存泄漏等等。
比如数组栈调用结束后,析构函数就会自动调用,去销毁其数组,就不会因为我们忘记调用free()
而造成内存泄漏
对于内置类型,析构函数不会起什么作用
对于自定义类型,析构函数会清理对象的资源,进行析构。
对于日期类
class Date
{
public:
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2002);
Date d2;
Date d3;
Date d4;
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
对象d1 d2 d3 d4
都是通过函数调用在栈上为对象分配空间,当其生命周期结束后,编译器就会销毁对象在栈上的空间。
但是,对象销毁的顺序是什么呢?数据结构-栈
要知道,这几个对象都是存放在栈上的。
对于操作系统中的栈与数据结构中的栈,基本没啥关系。但是两者都符合后进先出
的条件
我们知道数据结构栈
对于数据元素的处理规则是后进先出
。则,对于在栈上的对象而言,也会符合这个要求。
在生命周期结束后,
先创建的对象,后调用析构函数;
后创建的对象,先调用析构函数。
则
析构函数调用顺序为
要注意,当生命周期到的时候。
回声开大:人格复制
就是在一定时间内(好像最近时间又削短了),回声可以复制敌方任意一个英雄,并拥有其所有技能(充能时间也大大缩短)。
就相当于,在这一定时间内,你所操作的英雄换了一个类对象。
或者说,
对面一个被你选中的英雄的所有对象数据被你Ctrl C + Ctrl V
。完全被你拷贝过来了。
换句话说,在这一定时间内,你根据他的类,创建了一个一模一样的对象。
特性如下:
正常的拷贝构造-传引用调用
对于引用的介绍请参考C++入门语法
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date d1;
Date d2(d1);
如果是传值调用
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
因为,
对于形参要开辟一个对象空间,再将实参对象传递过来,将实参拷贝到这个形参空间,但,又需要调用拷贝构造函数,又需要开辟一个形参空间。。。。就这样不断拷贝,不断开辟空间,无限递归。
而对于传引用调用 :
传递的是要拷贝的对象的别名。只是拿到了目标对象的别名,可以对其进行访问,并且,这个过程并没有再开辟空间。
其实,也可以传址调用
Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
Date d1;
Date d2(&d1);
虽然也可以达到拷贝的效果,但是,其语法上并不是拷贝构造函数,而且比较麻烦,毕竟指针这容易出错。
如果是对于一个对象而言,
void test1(Date d)
{
}
void test2(Date& d)
{
}
test1(d1);
test2(d1);
对于test1()
而言,是传值调用,要先开辟一个空间,将对象d1拷贝过去,就需要调用拷贝构造函数,
但是test2()
,是传引用,直接就可以访问到源空间,根本不需要调用拷贝构造函数。这样对于效率也要快好多。
而且,拷贝构造毕竟是传引用,如果不小心会改变源空间的值,因为拷贝构造也是在类的公共区段,是在类内,不会被访问限定。
那么最好加一个const
去防止源对象的成员变量被修改。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这样,哪怕写反了复制顺序,编译器自己也会报错,而不会影响程序。
拷贝构造也是一个默认成员函数
如果我们不自己定义,编译器也会自动生成。
如果是一个日期类,类的成员变量全部都是内置类型的那种。编译器会自动生成一个拷贝构造函数,一个个直接拷贝过去,简称:值拷贝或浅拷贝
#include
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
private:
int a;
};
class Date
{
public:
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2002);
Date d2(d1);
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
但是对于一些自定义类型而言会出大问题。
比如:数组栈类
class Stack
{
private:
int* arr;
int size;
int capacoty;
};
当其对于栈对象进行拷贝构造,对这三个内置类型进行浅拷贝
这样浅拷贝完全不符合我们的要求,我们是希望通过拷贝,可以得到另一个与原对象一模一样的对象,
而这样的浅拷贝,当对对象 s1 进行改变时,对象 s2 也会受到改变。
就像
int a=10;
int b=a;
这是我们希望达到的效果。
而对,像栈、队列这一类的类,浅拷贝都无法达到正确效果,只有依靠深拷贝去完成。(嘿嘿,这个下次再完成)
而如果日期类中有自定义类型的成员变量,其会去调用自定义类型的拷贝构造函数,来完成对自定义类型的拷贝。
这也是类的默认成员函数
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类
型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
C++为了让自定义类型的对象也能像内置类型的对象一样进行例如+、-、*、/、=、==
的一系列操作。提供了运算符重载这种特性。
特性如下:
operator
后面接需要重载的运算符符号。注意:.* 、:: 、sizeof 、?: 、.
注意以上5个运算符不能重载。
像这类函数,都是定义在类中,不然,访问不到成员变量。
继续拿日期类举例
#include
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Date& d)
{
return (_year == d._year) && (_month == d._month) && (_day = d._day);
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2020,1,3);
Date d2(d1);
std::cout << (d1 == d2) << std::endl;
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
在类中定义运算符重载函数,使用时,就像内置类型一样使用运算符即可。
同时,
要注意,在自定义类型使用运算符时,第一个参数会是this
指向的对象。
第一个参数
this指针
是左操作数,第二个参数是右操作数
再来看一个数组类,有点神奇。
#include
class Array
{
public:
Array()
{
for (int i = 0; i < 10; i++)
{
_arr[i] = i;
}
_size = 10;
}
int& operator[](int pos)
{
return _arr[pos];
}
int GetSize(void)
{
return _size;
}
private:
int _arr[10];
int _size;
};
int main(void)
{
Array arr;
for (int i = 0; i < arr.GetSize(); i++)
{
std::cout << arr[i] << " ";
}
return 0;
}
这个[]
运算符重载,让对象可以像数组一样访问,如果是返回引用的话
甚至可以对数组的值进行修改,就像普通数组那样操作
内置类型
int a=10;
int b=a;//拷贝构造
//
int a=10;
int b;
b=a;//复制拷贝
自定义类型
Date d1(2021,6,5);
Date d2(d1);//拷贝构造
//
Date d1(2021,6,5);
Date d2;
d2=d1;//复制拷贝
复制拷贝:
对象已经初始化好后,再把一个对象拷贝给另一个对象
拷贝构造:
在对象还在创建时,拿另一个同类对象去初始化这一个对象
对于自定义类型,
拷贝构造调用拷贝构造函数,
而复制拷贝调用=
运算符重载函数
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
为了让自定义类型能够像内置类型一样,连续赋值,与防止自己给自己赋值。
a=b=c;
毕竟,赋值运算符重载函数是默认成员函数,即使我们不写,编译器也会自动生成。
#include
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Date& d)
{
return (_year == d._year) && (_month == d._month) && (_day = d._day);
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2020,1,3);
Date d2;
d2=d1;
d1.print();//->print(&d1)
d2.print();//->print(&d2)
}
对于内置类型的复制拷贝,编译器会通过默认的赋值运算符重载完成浅拷贝。
对于自定义类型的复制拷贝,编译器会调用自定义类型的赋值运算符重载完成拷贝。
对于构造和析构的特性是类似的,
对于内置类型,我们不写,编译器基本不会处理,
而自定义类型,编译器会调用自定义类型的析构和构造
对于拷贝构造和赋值运算符重载的特性是类似的,
对于内置类型,编译器会进行浅拷贝(直接赋值),
对于自定义类型,编译器要调用自定义类型的拷贝构造和赋值运算符重载,进行深拷贝。
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,
实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
注意:只有成员函数才能加const
。如构造,析构等等都不能加
就是当我们确幸该成员函数中不会,也不会造成对象的任何成员变量被修改
就使用const
去修饰成员函数,这样也是为了保险,防止错误操作。
比如:
bool operator==(const Date& d)const
{
return (_year == d._year) && (_month == d._month) && (_day = d._day);
}
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
被const
修饰后,对成员函数或者对象所能操作的权限就被缩小,而且,权限不能放大,且不能有放大权限的可能。
对于题目
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
}
这个操作符基本没什么那啥价值,根本不需要我们去实现它,默认的成员函数就完全够用了。
但是,如果你不希望对象的地址被读出来,要对其进行保护
class Date
{
public :
Date* operator&()
{
return nullptr ;
}
const Date* operator&()const
{
return nullptr ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
}
这样,当别人想读对象的地址时,就只能读出00000000
。可以对你的对象进行保护。
直接看代码,这里面是学会对运算符重载的应用,本身没什么算法知识。
#include "date.h"
void test(void)
{
Date d1(-11,5,26);
Date d2(2025,1,1);
d1.print();
//date d3= d1 + 4;//->operator(&d2,4)
//d3 = d1 + 3;//->operator(&d2,4)
d3.print();
//date d3 = d1 - 10;
//d1.print();
//d2.print();
d3.print();
//int days = d1 - d2;
//std::cout << days;
Date d3=d2++;
d3.print();
d2.print();
if (d3 >= d2)
{
std::cout << "d3<=d2" << std::endl;
}
//d1 += 4;//->operator+=(&d1,4)
//d1 += 1;
/*d1.print();
d2.print();
d3.print();*/
}
int main(void)
{
test();
}
#include "Date.h"
inline int MonthDays(int year, int month)
{
static int DayArrary[] = {
31,28,31,30,31,30,31,31,30,31,30,31 };
int day = DayArrary[month - 1];
if (month == 2 && (((year % 4 == 0) && (year % 100 != 0)) || year % 400 == 0))
{
day = 29;
}
return day;
}
Date::Date(int year, int month, int day)
{
if (year != 0 && month <= 12 && month > 0 && day <= MonthDays(year, month) && day > 0)
{
_year = year;
_month = month;
_day = day;
}
else
{
std::cout << "date illegal" << std::endl;
assert(false);
}
}
void Date::print(void)
{
if (_year >= 1)
std::cout << "公元 ";
else
std::cout << "公元前 ";
std::cout << _year << "年" << _month << "月" << _day<<"日"<<std::endl;
}
Date::Date(const Date& d)//拷贝构造
{
_year =d._year;
_month = d._month;
_day = d._day;
}
Date& Date::operator+=(int day)//+=运算赋重载
{
_day += day;
while (_day > MonthDays(_year, _month))
{
_day -= MonthDays(_year, _month);
_month++;
if (_month > 12)
{
_year++;
if (_year == 0)
_year = 1;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)//+运算符重载
{
//建立一个临时对象
Date tmp(_year,_month,_day);//
tmp += day;//->operator+=(&tmp,day);//对这个临时对象的操作
return tmp;
}
Date& Date::operator-=(int day)//-运算符重载
{
_day -= day;
while (_day<=0)//终止条件->>_day > 0 && _day <= MonthDays(_year, _month)
{
_month--;
if (_month < 1)
{
_year--;
if (_year == 0)
_year = -1;
_month = 12;
}
_day += MonthDays(_year, _month);
}
return *this;
}
Date Date::operator-(int day)//-运算符重载
{
Date tmp = (*this);
tmp -= day;
return tmp;
}
//日期之间的天数
int YearDays(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || year % 400 == 0)
return 366;
return 365;
}
int Date::operator-(Date& d)//日期减日期
{
int Days = 0;
Date bigDate;
Date smallDate;
if (_year > d._year)
{
bigDate = *this;
smallDate = d;
}
else if (_year < d._year)
{
bigDate = d;
smallDate = *this;
}
else//同一年
{
//小月的剩余天数加上中间月份的天数,再加上大月的天数
if (_month > d._month)
{
bigDate = *this;
smallDate = d;
}
else if (_month < d._month)
{
bigDate = d;
smallDate = *this;
}
else//同一个月
{
int tmp = abs(_day - d._day);
return tmp;
}
}
int yearTmp = bigDate._year - smallDate._year - 1;
int monthTmp = bigDate._month - smallDate._month - 1;
for (int i = 1; i <= yearTmp; ++i)
{
Days += YearDays(smallDate._year + i);
}
Days += MonthDays(smallDate._year, smallDate._month) - smallDate._day;
for (int i = smallDate._month + 1; i <= 12; i++)
Days += MonthDays(smallDate._year, i);
for(int i=1;i<bigDate._month;i++)
Days+= MonthDays(smallDate._year, i);
Days += bigDate._day;
return Days;
}
Date& Date::operator++()//前置++
{
*this += 1;
return *this;
}
Date Date::operator++(int) 后置++
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date Date::operator--(int)// 后置--
{
Date tmp(*this);
--* this;
return tmp;
}
Date& Date::operator--()// 前置--
{
*this -= 1;
return *this;
}
// >=运算符重载
bool Date::operator>=(const Date& d)
{
if (_year > d._year)
return 1;
else if(_year<d._year)
return 0;
else
{
if (_month > d._month)
return 1;
else if (_month < d._month)
return 0;
else
{
if (_day > d._day)
return 1;
else if (_day < d._day)
return 0;
else
return 1;
}
}
}
// <运算符重载
bool Date::operator<(const Date& d)
{
if (_year < d._year)
return 1;
else if (_year > d._year)
return 0;
else
{
if (_month < d._month)
return 1;
else if (_month > d._month)
return 0;
else
{
if (_day < d._day)
return 1;
else if (_day > d._day)
return 0;
else
return 0;
}
}
}
// <=运算符重载
bool Date::operator<=(const Date& d)
{
if (_year < d._year)
return 1;
else if (_year > d._year)
return 0;
else
{
if (_month < d._month)
return 1;
else if (_month > d._month)
return 0;
else
{
if (_day < d._day)
return 1;
else if (_day > d._day)
return 0;
else
return 1;
}
}
}
// !=运算符重载
bool Date::operator!=(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
return 0;
return 1;
}
这么长?源氏看来都要拔刀了
#pragma once
#include
#include
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);//拷贝构造
Date& operator+=(int day);//+=运算赋重载
Date operator+(int day);//+运算符重载
Date& operator-=(int day);//-=运算符重载
Date operator-(int day);//-运算符重载
int operator-(Date& d);//日期减日期
Date& operator++();//前置++
Date operator++(int); 后置++
Date operator--(int);// 后置--
Date& operator--();// 前置--
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator>=(const Date& d);
// <运算符重载
bool operator<(const Date& d);
// <=运算符重载
bool operator<=(const Date& d);
// !=运算符重载
bool operator!=(const Date& d);
void print(void);
private:
int _year;
int _month;
int _day;
};
实现>,<
之类的运算符重载过于琐碎,大家小心观看,因为我当时的顺序不一样,写多了。
因为,在库中早已经重载好了,并且可以自动识别类型,各个函数之间构成重载。
我以我LZ的英文水平翻译看看。cout是一个ostream
类的对象。可以通过操作符<<
将格式化或未格式化的数据通过成员函数写入到。。。。我就不知道咋翻了。
很明显,cin是一个istream
的类的对象。通过<<与成员函数完成对输入流的写入。
这些都不重要。大概了解是啥就行。
cout
是根据ostream
创建的一个对象
cin
是根据istream
创建的一个对象
这样,我们可以完成对自定义类型简化为内置类型.
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << d._month << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year >> d._month >> d._day;
return _cin;
}
按照这样的形式,我们可以做到像内置类型一样直接输入。
如果,要定义在类中,我们的cout
与cin
都是操作符的左边,导致,this
指针就无法指向类中的成员变量。
所以定义在类中并不合适。只能定义在类外。
但是,在类外,我们就无法访问到类中的成员变量。
所以,引入一个友元
提供友元函数。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
class Date
{
public:
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Date& d)
{
return (_year == d._year) && (_month == d._month) && (_day = d._day);
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << d._month << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year >> d._month >> d._day;
return _cin;
}
int main()
{
Date d;
cin>>d;
cout<<d<<endl;
return 0;
}
这样,友元可以突破封装,相当于开了一个后门。
注意:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
例如:
class Time;//时间类前置声明
class Date
{
public:
friend class Time;//声明时间类是日期类的友元
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Date& d)
{
return (_year == d._year) && (_month == d._month) && (_day = d._day);
}
void print(void)
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
class Time
{
public:
Time(int second = 0, int minute = 0, int hour = 1)
{
_second = second;
_minute = minute;
_hour = hour;
}
private:
int _second;
int _minute;
int _hour;
};
时间类是日期类的友元,则,时间类中的所有成员函数都可以说日期类的友元函数。
则,在时间类中可以访问到日期类在的私有成员变量。
但是,日期类依旧不能访问到时间类中的私有成员变量。
总结:
在创建对象的时候,对象通过调用构造函数来初始化对象。
C++在构造函数上提供了两种方案来初始化成员变量。
就是我们常见的初始化的模式
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
此模式的初始化规则:
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
事实上,虽然函数体内调用构造函数后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
例如:
初始化列表两次初始化,编译器直接报错,因为在列表中初始化只能初始化一次。但是,
编译器却可以允许,
来看看结果
这已经不叫初始化了,而是叫赋值,初始化毕竟只能初始化一次。
这也再一次证明了,函数体内“初始化”,准确来叫应该是赋值,而非初始化。
初始化列表才是真正的初始化成员变量。
从而可以推出,创建一个对象,对于对象而言,其中的成员变量是在初始化列表的时候定义的。
而且,对于有些成员变量,只有在初始化的时候才能赋初值,且之后就不能修改了。
例如
对于没有要求一定要在定义的时候初始化的成员变量,可以在函数体内”初始化“。
例如这样
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
,i(10)
,ret(_year)
{
_year = 100;
_year = 200;
}
private:
int _year;
int _month;
int _day;
const int i;
int& ret;
};
对于第三种,没有默认构造函数的自定义成员变量
也就是说,自定义类中没有不需要传参就能初始化的构造函数。也就是说,忘记传参就会导致报错。
这样将自定义成员变量,显示定义到初始化列表中,可以我i自定义成员变量初始化,还不会报错。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
,i(10)
,ret(_year)
,t(0)
{
_year = 100;
_year = 200;
}
private:
int _year;
int _month;
int _day;
const int i;
int& ret;
Time t;
};
class Time
{
public:
Time(int t)
{
_t = t;
}
private:
int _t;
};
如果不显示初始化列表,编译器就会报错没有默认的构造函数。
还有就是注意,初始化列表是成员函数定义的阶段,无论是哪种“初始化”模式,最终都会经历初始化列表这个阶段,
所以还是推荐使用初始化列表,保险一点
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
这个知识点有点绕,但还蛮重要的。
文章关于类与对象的知识比较零碎,又比较多,还比较重要。其实更多是对我自己的总结。