面向过程和面向对象初步认识
我们在学习C语言时,听说过面向过程这个概念,学习C++与Java时,听说过面向对象这个概念,那么他们到底是怎么一回事呢?
C 语言是 面向过程 的, 关注 的是 过程 ,分析出求解问题的步骤,通过函数调用逐步解决问题。C++ 是 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完成。
我们拿外卖送餐来举一个简单的例子,面向过程关注的就是怎么点餐,怎么送餐,怎么接单这些过程,而面向对象则是关注骑手,商家,用户这三个对象,去通过对象的功能与对象之间的关系来完成过程
类的引入
C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct Student//结构体
{
void SetStudentInfo(const char* name, const char* gender, int age)//函数
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintStudentInfo()//函数
{
cout<<_name<<" "<<_gender<<" "<<_age<<endl;
}
char _name[20];//成员
char _gender[3];
int _age;
};
int main()
{
Student s;
s.SetStudentInfo("Peter", "男", 18);//调用函数
return 0; }
就像如上代码,这在C语言中是不被允许实现的,结构体中允许有函数
上面结构体的定义, 在 C++ 中更喜欢用 class 类 来代替,这也就是class的引入
类的定义
class className{// 类体:由成员函数和成员变量组成}; // 一定要注意后面的分号
class 为 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面 分号 。类中的元素称为 类的成员: 类中的 数据 称为 类的属性 或者 成员变量 ; 类中的 函数 称为 类的方法 或者 成员函数 。
类的两种定义方式:
1. 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
这种定义方式其实就是在.cpp文件中进行定义声明使用
2. 声明放在.h文件中,类的定义放在.cpp文件中
同我们在C语言中在多文件中定义声明函数一样,将类的声明放在.h文件中,类的定义,函数的实现放在.cpp文件中,一般在工程中,我们采用第二种方式进行类的定义与声明
类的访问限定符及封装
访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
我们在定义类中给他们加限定符时,public是将类暴露在外面,允许外界访问,private则是不允许外部访问,protected暂且可以认为与私有是一致的
我们可以看到,当__age权限为private时,在主函数中是无法进行访问的,
而当我们将private去掉(改为public)时,就可以进行直接访问了
【访问限定符说明】1. public 修饰的成员在类外可以直接被访问2. protected 和 private 修饰的成员在类外不能直接被访问 ( 此处 protected 和 private 是类似的 )3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. class 的默认访问权限为 private , struct 为 public( 因为 struct 要兼容 C)注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++中class与struct都可以用来定义类,class默认为私有,struct默认为共有
struct与class的区别别呢?
C++ 需要兼容 C 语言,所以 C++ 中 struct 可以当成结构体去使用。另外 C++ 中 struct 还可以用来定义类。和class 是定义类是一样的,区别是 struct 的成员默认访问方式是 public , class的成员默认访问方式是private 。
封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。封装本质上是一种管理 :我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装 起来。但是我们目的全封装起来,不让别人看。所以我们 开放了售票通 道 ,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用 protected/private 把成员 封装 起来。 开放 一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
类的作用域
类定义了一个新的作用域 ,类的所有成员都在类的作用域中 。 在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
其实也就是一个花括号限定一个作用域,不同类中的成员不能直接使用,需要加上::来进行说明
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}
在我们了解了类的相关概念了之后,我们来举个例子,一起看看与C语言的差别之处
// 定义一个数据结构栈的类
//封装
//1.将数据与方法定义到一起
//2.把想给你看的数据给你看,不想给你看的封装起来,用访问限定符来实现
class Stack
{
//1.成员函数
public:
void Init(size_t n)
{
// ...
}
void Destory()
{
//...
}
void Push(int x)
{
// ...
}
void Pop()
{
// ..
}
// ...
//2.成员变量
//protected:
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack st;
st.Init(10);
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
st.Pop();
st.Destory();
return 0;
}
当我们将栈中的初始数据进行初始化之后,我们并不想对其进行改变,所以我们就使用了protected关键字对其进行了权限的修饰,那么此时,外界就无法直接调用他们了,那么我们怎么去改变他们呢,我们通过制定了许多的public权限的函数,来在类内部,对其进行调用,完成操作
类的实例化
用类类型创建对象的过程,称为类的实例化1. 类只是 一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没有分配实际的内存空间 来存储它2. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量3. 做个比方。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
我们类内函数的声明与定义有两种形式
1.在类内直接定义
2.在类内部声明,类外部定义
class Stack
{
public:
//1.成员函数
void Push(int x)//类内定义
{
//.....
}
void Pop();//类内声明
bool Empty();
//2.成员变量
private:
int* _a;
int _size;
int _capacity;
};
void Stack::Pop()//类外的函数定义,注意要加上类名+::来对函数进行说明是类内部的函数
{
//.....
}
int main()
{
Stack s;//类的实例化,实例化出对象,相当于定义出了类的成员变量
Stack s2;
//s1. _a = nullptr;私有变量无法访问
s2.Push(1);//成员变量调用函数
system("pause");
return 0;
}
类对象模型
计算类对象的大小
我们可以看到,当我们对类进行大小的计算时,只计算了其成员变量的大小,忽略了成员函数的大小,这其实也就说明了:对象中只存储成员变量,不存储成员函数
这是为什么呢?其实,是因为一个类可以实例化出N个对象,每个对象的成员变量都可以储存不同的值,但是调用的函数确是同一个,因为如果每个对象都要重新定义一次函数,这个浪费是非常大的,所以我们的函数存放在代码区,供所有的成员变量反复调用
所以我们的计算规则就是:计算成员变量之和,并且考虑内存对齐
private:
int* _a;
char h;
};
那么我们再来看看这个类的大小呢?答案是8,因为int4个字节,char1个字节,而windows下的默认对齐数为8,而整个大小必须为最大对齐数的倍数,成员中int最大为4,一共为5,不是4的倍数,所以需要补3个字节大小,最后为8个字节
最后再补充一下,当一个类中没有成员变量时,默认的大小为1,这里开辟1大小的原因是开一个字节不是为了存储数据,而是用来占位,表示类存在,否则当大小为0时就找不到了,没办法区分是否存在
this指针
this指针的引出
#include<iostream>
#include<Windows.h>
//#include <time.h>
using namespace std;//C++库中的所有东西都是放到std命名空间中的
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1,d2;
d1.Init(2021, 8, 2);
d1.Print();
system("pause");
return 0;
}
在上述代码中,我们创建了一个Date类,并且设立了Init与Print两个函数,我们在实例化时创建了d1,与d2,但仅对d1进行了初始化,那么我们在这里就有一个问题存在疑惑
Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?
C++ 中通过引入 this 指针解决该问题,即: C++ 编译器给每个 “ 非静态的成员函数 “ 增加了一个隐藏的指针参 数,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有成员变量的操作,都是通过该 指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成 。
事实上如图所言,在C++中,编译器其实隐藏了一个名为this的指针,当我们未给函数传参时,它都一直存在,自动进行传参,而在我们编译过程中对其会进行处理,将this指针显现出来,从而对每个参数进行对象的指向
public:
// this:谁调用这个成员函数,this就指向谁
void Init(int year, int month, int day)//void Init(Date* this,int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()//void Print(Date* this)
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2021, 8, 2);//d1.Init(&d1, 2021, 8, 2);
d1.Print();//d1.Print(&d1);
system("pause");
这其实才是在编译器中程序的样子,我们在传参时,会在最前面自动加上this指针,但是我们不能在声明的地方再次加上Data* this,而在我们的函数内部是可以的
this指针的特性
1. this 指针的类型:类类型 * const2. 只能在 “ 成员函数 ” 的内部使用3. this 指针本质上其实是一个成员函数的形参 ,是对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储 this 指针 。4. this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户 传递
下面我们对this指针进行几点思考
1.this指针是存在哪里的?答:是存在栈上的,因为this也是一个形参,形参都存在栈上,(vs下是存在ecx这个寄存器中,因为this这一个指针访问的频率很高,存在内存中没有存在寄存器中效率高,vs做了优化)
2.下述代码会发生什么?
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
Date* p = NULL;
p->PrintA();
p->Show();
}
这段代码在我们调用Print函数时会发生崩溃,Show函数时则会正常输出Show(),那么我们思考一下为什么呢?
其实是因为当我们把指针置空了以后,实际在PrintA内部this->_a中,this指针被置空了,所以他就无法找到_a,所以会报错,然而在Show这个函数中,我们并没有涉及寻找对象的操作,即使将this指针置为了空,我们并没有用指针去寻找对象,所以程序会正常运行,这也说明了我们的成员函数是存在在代码段中,p->Show()这里不会去p指向的对象上找,不会进行解引用,所以不会崩溃
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数class Date {};
当我们未给类设立成员时,我们的类中会有6个默认的成员函数
构造函数
概念
构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次 。
特性
构造函数 是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象 。
1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器 自动调用 对应的构造函数。4. 构造函数可以重载。
我们在实例化对象时若没有队成员变量进行初始化,则会产生随机值,而这里的构造函数相当于一个内置的Init函数
public :
// 1.无参构造函数
Date ()
{}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2 (2021, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3();
}
注意:我们的构造函数在调用的时候不是去书写函数名带括号调用的,而是直接在实例化对象后面带括号初始化的,用法有点小奇怪,不过也很方便,而对于无参的构造函数则是直接在实例化对象时就调用了,不用带括号
5. 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
事实上,C++在这里的处理其实是:对于内置类型不做处理,系统自动随机值,而对于自定义类型进行初始化
关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是d 对象 year/ month/_day ,依旧是随机值。也就说在这里 编译器生成的默认构造函数并没有什么卵 用??解答: C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用 class/struct/union 自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t 调用的它的默认成员函数
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
当我们用户自己定义了带参的构造函数,则编译器就不会自动生成无参的了,所以当我们再想使用无参构造函数,就还需要自己去定义了,带参的则直接使用用户自己定义的
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
所以当我们用户既想定义带参构造函数以及无参构造函数时,就可以使用我们的全缺省,但定义完就不能再次出现无参或者带参构造函数了
注意:这种情况编译器是无法通过的,因为定义了两个无参构造函数,(第二个采用了全缺省的方式,是含参与无参的结合),产生了冲突,当定义的时候编译器不知道要定义哪一个,所以会报错
7.成员变量的命名风格
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
Date(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
// 所以我们一般都建议这样
class Date
{
public:
Date(int year)
{
_year = year;
}
private:
int _year;
};
// 或者这样。
class Date
{
public:
Date(int year)
{
m_year = year;
}
private:
int m_year;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。
析构函数
概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
我们在认识了构造函数之后,其主要作用是创建对象时自动初始化,那么一定就有与之对应的销毁函数,而我们的析构函数,就是在对象生命周期结束时自动销毁的函数
特性
析构函数 是特殊的成员函数。其 特征 如下:1. 析构函数名是在类名前加上字符 ~ 。2. 无参数无返回值。3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数,完成清理工作
int main()
{
Date d1;//对象生命周期结束后自动调用析构函数
Date d2
system("pause");
return 0;
}
注意:我们完成清理工作并不是针对d1的清理,d1是储存在栈中的,当程序执行完毕会自动销毁,这里的清理是指对于对象中资源的清理
我们在对象创建之后,先生成了d1,再生成d2,在对他们完成操作,结束生命周期后,清理的顺序则是先d2,再d1,因为对象是在栈中存储的,先进后出
而我们清理的资源又怎么去理解呢?
实际上,我们清理的其实是那些动态开辟的东西,就像Stack中malloc出来的东西,用于清理这些东西,若不进行清理,因为资源是有上限的,最后就有可能发生资源不够使用的问题
与构造函数一样,我们的析构函数在未自己创建时会调用编译器自动生成的,同时,面对内置类型,也是不做处理的,面对自定义类型,才会进行清理
拷贝构造
拷贝构造函数也是特殊的成员函数,其 特征 如下:1. 拷贝构造函数 是构造函数的一个重载形式 。2. 拷贝构造函数的 参数只有一个 且 必须使用引用传参 ,使用 传值方式会引发无穷递归调用 。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//传入引用
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(0,1,1)
// Date d2(0,1,1)
Date d2(d1);//调用拷贝构造
Date d3=d1;//同样调用拷贝构造
return 0;
}
我们来观察一下这个代码,当我们在实例化一个对象之后,再想实例化一个除了名字,其他一模一样的对象时,我们可以采用代码中的这种形式,将形参直接写成想要拷贝的那个对象,但是这种形式又会有些问题
如上图一样,我们想要调用Date d2(d1)这个拷贝构造函数,那么在传参的时候就要传入d1,但d1又是一个拷贝构造函数,而又在传参时传入构造函数,此时便发生了类似无穷递归的现象
所以我们在拷贝函数中采用了引用的传参方式,直接将参数传入拷贝构造中,而不会引起递归拷贝构造
而我们在传引用之前再加const的意思是,我会拷贝你,但不会修改你,避免了之后如果产生错误,改变原本的对象的可能。
赋值运算符重载
运算符重载
C++ 为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号 。函数原型: 返回值类型 operator 操作符 ( 参数列表 )注意:不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型或者枚举类型的操作数用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义作为类成员的重载函数时,其形参看起来比操作数数目少 1 成员函数的操作符有一个默认的形参this ,限定为第一个形参.* 、 :: 、 sizeof 、 ?: 、 . 注意以上 5 个运算符不能重载。
而我们的运算符重载用于解决哪些问题呢?
首先,我们的自定义类型是无法使用运算符的,要用就得实现重载函数
实际上,它可以用来解决自定义类型中的比较问题,当我们想对两个自定义不同的类中的成员大小进行比较时,我们是无法进行直接比较的,需要调用特定的函数,这里的函数就是我们的operator重载函数
就想图示的一样,我们需要比较类内成员是否相同,调用的operator==函数,不过在比较之前,需要将成员变量的权限改为public,这样才可以进行调用
事实上,我们的 opertor函数在定义了之后,也可以直接使用==来比较,编译器会自动判断为operator函数,所以我们一般采用第二种写法,可读性较强
那么这只是一种解决方式,我们的成员如果一定需要是private的呢?
此时便需要将opertor==函数放到上面,让他作为内置函数来使用,但是,在作为内置函数时,传参规则就有了变化,因为我们内置函数有一个隐藏参数,this指针,而==又只能有2个参数,所以此时,我们在对两个对象进行比较时,需要将一个对象的地址传给this指针,另一个直接进行比较,才能完成比较操作
此时,operator函数就变成了这样,d1==d2在编译器中也转化为了d1.operator==(d2)这个样子,而operator函数实际上的参数以及调用过程也如上图一样,将其中一个对象置于this指针中
赋值运算符重载
我们在类中,需要用到赋值操作,所以也需要一个赋值的函数,来完成,同运算符一样,我们的赋值运算符同样用operator来完成
Date& operator=(const Date& d)//返回值为引用,省去了拷贝构造生成新对象的操作,直接返回自己this的解引用,也就是d1
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2018,10, 1);
// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
d1 = d2;
return 0; }
赋值运算符主要有四点:1. 参数类型2. 返回值3. 检测是否自己给自己赋值4. 返回 *this5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
日期类
#include<iostream>
#include<Windows.h>
using namespace std;
class Date//日期类
{
public:
int GetMonthDay(int year, int month)//设立月份判断函数
{
static int monthDays[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
return monthDays[month];//调用哪个月,返回哪个月的最大天数
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{
return 29;//闰年2月单独处理
}
}
Date(int year = 0, int month = 1, int day = 1)//全缺省初始化日期
{
if (year >= 0
&& month >= 1 && month <= 12
&& day >= 1 && day <= GetMonthDay(year, month))//排除非法情况
{
_year = year;//对年月日赋值
_month = month;
_day = day;
}
else{
cout << "非法日期" << endl;
}
}
//Date d2(d1)
Date(const Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//d1<d2
bool operator<(const Date& d)//<运算符重载
{
if (_year < d._year)//年小时肯定小小
return true;
else if (_year == d._year&&_month < d._month)//年一样是看月
return true;
else if (_year == d._year&&_month == d._month&& _day < d._day)//年月一样时看天
return true;
else
{
return false;
}
}
bool operator==(const Date& d)//==运算符重载
{
return _year == d._year&&_month == d._month&& _day == d._day;//年月日同时相同时相同
}
bool operator!=(const Date& d)
{
return !(_year == d._year&&_month == d._month&& _day == d._day);//==运算取反
}
bool operator<=(const Date& d){//bool operator<=(Date* this,const Date& d)
return *this < d || *this == d;//复用上方代码来实现
}
bool operator>(const Date& d){//bool operator<=(Date* this,const Date& d)
return !(*this <= d);//复用上方代码来实现
}
bool operator>=(const Date& d){
return !(*this < d);//<取反
}
//是否要重载一个运算符,看是否有意义
Date operator+(int day)
{
Date ret(*this);
ret += day;//复用+=函数
return ret;
//Date ret = *this;//用d1拷贝构造一个ret
//ret._day += day;//将day加上后再赋给_day
//while (ret._day > GetMonthDay(ret._year, ret._month))//当加上后的值大于当月的最大天数
//{
// ret._day -= GetMonthDay(ret._year, ret._month);//就将天数减去当月最大月份
// ret._month++;//并将月份+1
// if (ret._month == 13)//当月份到达13时
// {
// ret._year++;//年份+1
// ret._month = 1;//月份置1
// }
//}
//return ret;//返回最后的日期
}
Date& operator+=(int day)//+=函数重载
{
if (day < 0)//当天数小于0
{
return *this -= -day;//取负值相加
}
_day += day;//加上day
while (_day > GetMonthDay(_year, _month))//当天数小于大于当月最大天数
{
_day -= GetMonthDay(_year, _month);//天数减去当月最大天数
++_month;//月份+1
if (_month == 13)//当月份到13时
{
++_year;//年份+1
_month = 1;//月份置1
}
}
return *this;//返回当前日期
}
Date& operator-=(int day)//-=运算符重载
{
if (day < 0)//若传入的天数为-则加上-值
{
return *this += -day;
}
_day -= day;//减去日期
while (_day<=0)//当日期减到负值
{
--_month;//月份-1
if (_month == 0)//当月份降到0
{
--_year;//年份减一
_month = 12;//月份置12
}
_day += GetMonthDay(_year, _month);//当前日期加上当月最大日期
}
return *this;//返回当前日期
}
Date operator=(const Date& d)//赋值运算符重载
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date operator-(int day)//减去固定天数
{
Date ret = *this;
ret -= day;
return ret;//返回+之后的
/*Date ret = *this;
ret._day -= day;
while (ret._day <= 0)
{
--ret._month;
if (ret._month == 0)
{
--ret._year;
ret._month = 12;
}
ret._day += GetMonthDay(ret._year, ret._month);
}
return ret;*/
}
Date& operator++()
{
*this += 1;
return *this;
}
Date operator++(int)//为了构成函数重载,加了int形参,为后置++
{
Date tmp(*this);
*this += 1;
return tmp;//返回+之前的
}
Date operator--()
{
*this -= 1;
return *this;
}
Date operator--(int)//后--
{
Date tmp (*this);
*this -= 1;
return tmp;
}
int operator-(const Date& d)//减去固定日期
{
int flag = 1;//
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;//日期差值为负,标志位置-1
}
int n = 0;//初始化日期差值
while(min != max)//当min与max未相遇
{
++min;//min++
++n;//差值++
}
return flag*n;//返回标志位*差值
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2(2021, 8, 2);
d2.Print();
//cout << (d1 < d2) << endl;
//cout << (d1 == d2) << endl;
//cout << (d1 > d2) << endl;
//cout << (d1 != d2) << endl;
//cout << (d1 <= d2) << endl;
//cout << (d1 >= d2) << endl;
system("pause");
return 0;
}
这里的日期类很适合我们做类与对象的练习
const成员
const修饰类的成员函数
将 const 修饰的类成员函数称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void f(const Date& d)
{
d.Print();
}
int main()
{
Date d1(2021, 8, 3);
f(d1);
system("pause");
return 0;
};
我们来看这段代码,实际上这段代码是不能编译通过的,原因是我们的f函数无法将d1传给Print函数
因为我们的f函数中的形参使用const修饰的,所以权限为只读,只能自己读取d,而在我们函数内部,需要将d传给Print,但是Print隐藏的参数为this指针,为可读可写权限,只读的无法传给可读可写的,所以为了解决这个问题就需要将Print函数中的参数改为可读,加上const,但是我们的Print函数内部参数隐藏,无法将const加在括号内,所以就加在了括号外
没加const时形参如上面的一样,加了之后,编译器就自动判定this指针指向的对象被const修饰了
不过修饰了之后就无法去改变成员变量了,因为const保护了*this
在我们可以用成员去调用成员函数之后,那么我们的成员函数调用成员函数呢?
实际上,成员函数调用成员函数也遵循权限的规则,非const可以调用const,而const无法调用非const,上图中第一种可以调用,第二种就无法调用
const使用建议:什么时候需要给成员函数加上const?当成员变量不需要改变时,建议给成员函数都加上
总结:
1. const 对象可以调用非 const 成员函数吗?不行2. 非 const 对象可以调用 const 成员函数吗?可以3. const 成员函数内可以调用其它的非 const 成员函数吗?不行4. 非 const 成员函数内可以调用其它的 const 成员函数吗?可以
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。作用是取对象地址
class Date
{
public :
Date* operator&()//服务非const成员变量
{
return this ;
}
const Date* operator&()const//服务const成员变量
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到指定的内容,将函数重载,指向nullptr,隐藏地址!
再谈构造函数
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化, 构造 函数体中的语句只能将其称作为赋初值 ,而不能称作初始化。因为 初始化只能初始化一次,而构造函数体内 可以多次赋值 。
初始化列表
初始化列表:以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟一个 放在括 号中的初始值或表达式。
class B {
public:
//可以理解为初始化列表是成员变量定义的地方
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 没有默认构造函数(不用传参就可以调那个构造函数)
int& _ref; // 引用
const int _n; // const
//成员变量的声明
int x;
};
int main()
{
B b(1, 2);//对象定义
system("pause");
return 0;
}
【注意】1. 每个成员变量在初始化列表中 只能出现一次 ( 初始化只能初始化一次 )2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量const 成员变量自定义类型成员 ( 该类没有默认构造函数 )
总的来说,对于我们的类而言,成员变量是需要进行定义的的,而对于我们的一般成员(内置类型成员)来说,在对象定义时,便会进行自动初始化,二对于我们的特殊类型(引用,const修饰,自定义类型)而言,他们只能在定义时初始化,而定以时又是只能在形参实例化时对对象整体进行定义的,无法针对成员变量来进行初始化,所以我们就引入了初始化列表,来针对特殊成员单独进行初始化
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
对于编译器而言,即使我们没有手动的去在初始化列表中初始化,也会自动地根据全缺省进行初始化
我们可以看到,上述代码中,直接对A进行实例化,赋入了一个1,实际上是对a2进行了初始化,所以初始化列表中a2的形参a1暂时还是随机值,传给a2也为随机值,而后再初始化a1,a1变为1
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
explicit关键字
构造函数不仅可以构造与初始化对象, 对于单个参数的构造函数,还具有类型转换的作用 。
我们看到Date d2 = 2是不是会感觉有些奇怪,其实这里也是编译器给我们做的小优化,这种写法也是可以的,也是拷贝构造的一种,只是编译器将它优化为直接构造了
而我们如果加上一个explicit关键字,就可以禁止隐式转化,阻止这种写法的发生
用 explicit 修饰构造函数,将会禁止单参构造函数的隐式转换 。
而对于我们的多参数构造
我们则需要将参数放在花括号中,不过这是C++11后的写法,同样的,我们如果想禁止隐式转化的发生,加上explicit就可以
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
我们来看这样一个问题,求出这个类中创建了多少个对象
事实上,我们可以看到,我们创建了a1,a2,又利用f函数进行拷贝构造了两份,但是在拷贝构造传参的过程中,还创建了两份,一共6份,同我们的static关键字修饰其他东西一样,我们需要计算对象的个数,就定义一个static的n,在每个构造与拷贝构造中队n++,最后n的结果就是最后的个数,
补充:
1.static成员变量不存在对象中,存在静态区,属于这个类的所有对象,也属于这个类
2.静态成员函数,没有this指针,不使用对象就可以调用->类名::函数名
3.static成员函数中,无法访问非静态的成员(成员变量+成员函数)
1. 静态成员 为所有 类对象所共享 ,不属于某个具体的实例2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字3. 类静态成员即可用类名 :: 静态成员或者对象 . 静态成员来访问4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员5. 静态成员和类的普通成员一样,也有 public 、 protected 、 private3 种访问级别,也可以具有返回值
这里有个练习,可以很好的练习我们的static与类
求1+2+3+....+n,不能使用乘除,for,while,if,else,switch.case等关键字及条件判断语句
class Sum{
public:
Sum()
{
_sum += _i;
++_i;
}
static void Init()
{
_i = 1;
_sum = 0;
}
static int GetSum()
{
return _sum;
}
private:
static int _i;
static int _sum;
};
int Sum::_i = 1;
int Sum::_sum = 0;
class Solution{
public:
int Sum_Solution(int n)
{
Sum::Init;
Sum*p = new Sum[n];
return Sum::GetSum();
}
};
我们利用了static对其进行静态处理,使sum与i实现了累加,再构造了一个对象数组,使可以创建n个对象,完成n次构造对象以及累加操作,而又因为我们在类外无法直接调用类内部的成员,所以使用了一个static方法取出最终结果,值得注意的是,当我们完成了一次计算之后,需要对成员进行重置,否则因为static的缘故,其值再次调用时仍会是之前的结果
C++11 的成员初始化新玩法
C++11 支持非静态成员变量在声明时进行初始化赋值, 但是要注意这里不是初始化,这里是给声明的成员变 量缺省值 。
这种在声明处通过缺省的方式进行赋值的操作,在C++11之后的版本,也是可以的,不过要是在类中已经给过值了,就不会采用这个缺省值了
友元
友元分为: 友元函数 和 友元类友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
注意,此时会发生报错,因为我们类外的函数想去调用类内的成员,这是不被允许的
而我们在类中引入了关键字friend,而后在friend后加上了f函数的声明,此时就意味着我们的f函数得到了许可,成为了类的朋友,所以就可以突破类的限制,进行访问了
友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 的 普通函数 ,不属于任何类,但需要在类的内部声明,声明时需要加friend 关键字。
但是此时就有人会说了,这样麻烦,还不如直接定义类内部函数,多麻烦,但是我们再来看看这种情况
此时我们可以发现,当我们对<<输出操作符进行运算符重载时,我们的常规写法不能用了,只能写成d1.operator<<(cout),或者d1<<cout这种反着来的写法才能调用,原因是我们的运算符重载函数的第一个隐藏形参这是this指针,在这里指向的是d1,所以调用形参的规则也是先d1的,但是我们cout的写法又不同,这违背了我们的习惯
问题:现在我们尝试去重载 operator<< ,然后发现我们没办法将 operator<< 重载成成员函数。 因为 cout 的 输出流对象和隐含的 this 指针在抢占第一个参数的位置 。 this 指针默认是第一个参数也就是左操作数了。但是实际使用中cout 需要是第一个形参对象,才能正常使用。所以我们要将 operator<< 重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>> 同理。
那么如何解决这种问题呢
此时便用上了我们的友元函数,将这个函数放在类的外面,此时便没有了this指针的干扰,设立两个形参,正常调用,在类中利用friend关键字将这个函数引入即可,此时便恢复了我们的习惯写法
最后为了我们可以连续进行输出,我们将返回值改为out
友元函数 可访问类的私有和保护成员,但 不是类的成员函数友元函数 不能用 const 修饰友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。友元关系是单向的,不具有交换性。如果 B 是 A 的友元, C 是 B 的友元,则不能说明 C 时 A 的友元。
总的来说,同友元函数一样,起到的都是越过类的封锁去访问的作用,只是友元类是这个类可以去访问另一个类的成员
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成
员变量
public:
Time(int hour, int minute, int second)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t.second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time类的私有成员变量,但想在Time 类中访问 Date 类中私有的成员变量则不行。
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。特性:1. 内部类可以定义在外部类的 public 、 protected 、 private 都是可以的。2. 注意内部类可以直接访问外部类中的 static 、枚举成员,不需要外部类的对象 / 类名。3. sizeof( 外部类 )= 外部类,和内部类没有任何关系。
class A {
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
总的来说,就是在类的内部再定义类,而且这个内部类,天生是外部类的友元类,值得注意的一点是调用时用A::B b这种方式,告诉编译器去A中找到B,再实例化B的对象
匿名对象
我们来看这种情况,我们创建了3个对象,后面两个为匿名对象,匿名对象调用方法为函数名()的方式,不去规定特定的对象,就可以实现匿名对象,匿名对象的生命周期仅在其所在的一行,在执行完自己的一行之后便会调用自己的析构函数析构
再次理解封装
高内聚,低耦合,模块化处理,正如各个国家一样,中国用自己的方式抗疫,别国感染多,对中国影响较低
再次理解面向对象
更好的模拟这个世界,以对象,方法为导向,弱化过程