目录
面向过程和面向对象的初步认识,C语言是面向过程,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题,C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
1.类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数,因为结构体在C++中被升级成为了类,且类中的函数定义可以不分先后顺序。。
//在C++中,每一对花括号都属于域
struct Stack
{
//成员函数
void Init(size_t capaticy = 4)
{
a = (int*)malloc(sizeof(int) * capaticy);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
}
void Push(int x)
{
//扩容
//...
a[top++] = x;
}
void destroy()
{
free(a);
a = nullptr;
top = capacity = 0;
}
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
struct Stack s1;//C++兼容大部分C语言语法
s1.Init(10);
Stack s2;//C++的写法
s2.Init();
s2.Push(1);
s2.Push(2);
s2.Push(3);
s2.Push(4);
s2.Push(5);
s2.destroy();
return 0;
}
上面结构体的定义,在C++中更喜欢用class来代替
//在C++中,每一对花括号都属于域
class Stack
{
//.....
};
不过替换为class之后类里面的成员变量和成员函数会变成无法访问,这里有涉及到类的访问权限问题。限定符修改之后:
class Stack
{
public:
//成员函数
//.....
private:
//成员变量
//.....
};
2.类的访问限定符及封装
2.1访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。访问限定符有三种:
1.public(公有属性)
2.protected(保护属性)
3.private(私有属性)
访问限定符说明:
1. public修饰的成员在类外可以直接通过类创建的对象来被访问。
2. protected和private修饰的成员在类外不能直接被访问和类创建的对象也不能访问(此处protected和private是类似的),可以在类内访问。
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
2.2封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件,对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
3.类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; //和结构体一样,后面一要有分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1、声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2、类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作
符指明成员属于哪个类域。
一般情况下,更期望采用第二种方式。在练习的时候可以使用方式一定义类,在工作中尽量使用第二种。
成员变量命名规则的建议:
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
为了分清楚函数体的里面year到底那一个是成员变量,那一个是函数参数,建议在成员变量面前加上下划线
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
_year = year;
}
private:
int _year;
};
4.类的实例化
用类类型创建对象的过程,称为类的实例化
1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;所以类里面的成员变量和成员函数都是属于声明不是定义。
2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,只存储类成员变量。所以可以知道类创建的对象(定义的变量),只有成员变量占用空间,成员函数的地址不存类创建的对象里面。
如果需要访问成员变量需要先创建对象,再把权限更改为公有权限,即可在类外访问成员变量。
5.类对象模型
计算类对象的大小
using namespace std;
class Stack
{
public:
//成员函数
void Init(size_t capaticy = 4);
void Push(int x);
void destroy();
private:
//成员变量
int* a;
int top;
int capacity;
};
通过上面我们知道实例化出的对象占用实际的物理空间,只存储类成员变量。所以类创建的对象(定义的变量),只有成员变量占用空间,可以使用sizeof验证:
使用上面的类创建了两个对象分别都是12,在x86环境下指针占用四个字节,int占用四个字节所以是12。为什么创建的对象只存储成员变量而不存储成员函数?因为如果每一个对象都去存储一份成员函数就会导致空间的浪费,因为大家虽然存储的数据不同,但是调用的成员函数功能是相同的,既然如此干脆把这些成员函数放在类作用域的公共代码区让大家一起调用不就行了吗,只需要存储成员变量就行,还能减少空间的浪费。
那么空类或者仅有成员函数的类占用空间吗?
没有成员变量的类对象,需要1byte,是为了占位(或者是为了划分给类的起始地址),表示对象的类存在,不存储有效数据。
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐和C语言的内存对齐是一样的,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
6.this指针
6.1 this指针的引出
class Stack
{
public:
//成员函数
void Init(size_t capaticy = 4)
{
a = (int*)malloc(sizeof(int) * capaticy);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
}
void Push(int x)
{
//扩容
//...
a[top++] = x;
}
void destroy()
{
free(a);
a = nullptr;
top = capacity = 0;
}
private:
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
Stack s2;
s1.Init();
s2.Init();
return 0;
}
对于上述类,有这样的一个问题:
Stack类中有Init与Push、destroy三个成员函数,函数体中没有关于不同对象的区分并且创建的对象也不存储成员函数的地址,那当s1调用Init函数初始化时,该函数是如何知道应该设置s1对象进行初始化,而不是设置s2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数,让该指针指向当前对象(该对象调用的函数运行时会去调用执行函数的该对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针不能在形参和实参显示传递,但是可以在函数内部显示使用。
通过打印s1的地址和this指针维护的地址是一样的。this是形参,所以this指针是跟普通参数一样存在函数调用的栈帧里面,vs下面对this指针传递,进行优化,对象地址是放在ecx,ecx存储this指针的值。
6.2 this指针的特性
1. this指针的类型:类类型* const(Stack* const 变量名),即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。
所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用
户传递
5.在通过类创建的对象调用非静态成员函数时,一定包含有this指针,在类外调用成员函数时只能通过对象去调用,因为调用非静态函数时,this指针即是调用对象,没有调用对象就会报错,如果是静态成员函数是没有this指针的,因为this指针创建在栈上,在类外使用静态成员函数时,需要指明其所属类域。
7.类的六个默认成员函数
class Stack {};
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数,默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数,对内置类型不做处理,对自定义类会去调用他自定义的显示(写)函数,如果也没显示一样是调用编译器默认生成的默认成员函数。
注:这几个默认成员函数不可以定义为全局的,因为会和编译器默认生成的进行冲突。
6个默认成员函数分别是:
初始化函数和清理:
1.构造函数:构造函数主要完成初始化工作
2.析构函数:析构函数主要完成清理工作
拷贝和赋值:
1.拷贝构造函数:拷贝构造是使用同类型对象初始化创建对象
2.(运算符重载)赋值运算符重载函数:赋值重载主要是把一个对象赋值给另一个对象
取地址和重载:
1.取地址操作符重载函数:
2.const修饰的取地址操作符重载函数:
8.构造函数
8.1概念
class Stack
{
public:
//成员函数
Stack(size_t capaticy = 4)
{
a = (int*)malloc(sizeof(int) * capaticy);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
}
// void Init(size_t capaticy = 4)
// {
// a = (int*)malloc(sizeof(int) * capaticy);
// if (a == NULL)
// {
// perror("malloc fail\n");
// return;
// }
// }
void Push(int x)
{
//扩容
//...
a[top++] = x;
}
void destroy()
{
free(a);
a = nullptr;
top = capacity = 0;
}
private:
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
Stack s2;
s1.Init(10);
s2.Init(20);
return 0;
}
对于Stack类,可以通过 Init 公有方法(公有函数)给对象设置空间大小(初始化),但如果每次创建对象时都要调用该方法去设置,未免有点麻烦,那能否在对象创建时,就将其设置好呢?
构造函数是一个特殊的成员函数主要就是完成自动初始化对象,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
class Stack
{
public:
//构造函数
Stack(size_t capaticy = 4)
{
a = (int*)malloc(sizeof(int) * capaticy);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
}
void Push(int x)
{
//扩容
//...
a[top++] = x;
}
void destroy()
{
free(a);
a = nullptr;
top = capacity = 0;
}
private:
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
Stack s2;
return 0;
}
8.2特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
//构造函数的重载
//构造函数也是可以重载的,列如一开始就需要使用一组数据用来进行初始化开辟的空间
Stack(int *pa, size_t n)
{
a = (int*)malloc(sizeof(int) * n);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
memcpy(a, pa, sizeof(int) * n);
top = n;
capacity = n;
}
Stack(size_t capaticy = 4)
{
a = (int*)malloc(sizeof(int) * capaticy);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
注:
1.一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成。
2.全部都是自定义类型成员的可以考虑让编译器自己生成。(前提是自定义类型成员的构造函数自己有没有写)。
ps:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
6.构造函数的调用根普通函数调用的区别
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
结论:
1.一般情况下,构造函数都需要我们自己写
2.内置类型成员都有缺省值,且初始化符合我们的要求
3.全是自定义类型的构造,且这些类型都定义有默认构造
9.析构函数
9.1概念
通过前面的构造函数我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?(指释放动态函数申请开辟的空间)
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,而是完成自动对局部对象的销毁,这也工作是由编译器完成的。对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
9.2特性
析构函数也是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Stack
{
public:
//构造函数
Stack(size_t capaticy = 4)
{
printf("构造函数\n");
a = (int*)malloc(sizeof(int) * capaticy);
if (a == NULL)
{
perror("malloc fail\n");
return;
}
}
void Push(int x)
{
//扩容
//...
a[top++] = x;
}
// //销毁函数
// void destroy()
// {
// printf("销毁函数\n");
// free(a);
// a = nullptr;
// top = capacity = 0;
// }
//析构函数
~Stack()
{
printf("析构函数\n");
free(a);
a = nullptr;
top = capacity = 0;
}
private:
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
s1.Init();
return 0;
}
注:
1、一般情况下,有动态申请资源,就需要显示写析构函数释放资源
2、没有动态申请的资源,不需要写析构
3、需要释放资源的成员都是自定义类型,不需要写析构函数
目前已知的三种方法
10.拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
10.1特性
拷贝构造函数也是特殊的成员函数,其特性如下:
1. 拷贝构造函数是构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。(也可以使用指针来解决这一个问题)
ps:由此可知在实现拷贝构造函数时,形参一定要加上引用,就算不加编译器也会报错无法通过编译,并且最好加上const去修饰形参,以免误操作改变了实参的值。
//正确写法
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
Date(const Date& d)//使用const去修饰形参
{
_year = d._year;
_month = d._month;
_day = d._day;
//错误写法
//d._year = _year;
// d._month = _month;
// d._day = _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2000, 10, 10);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。(1.内置类型成员完成值拷贝/浅拷贝。2.自定义类型成员会调用他的拷贝构造。)
这个类去掉了显示的拷贝构造,使用了编译器默认生成的拷贝构造依然拷贝成功了,这个叫做浅拷贝,如果换一个Stack类呢?能不能拷贝成功?
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
对比一下s2拷贝成功了吗?很明显是失败了,s2的a成员和s1的a成员指向了同一块的地址空间,存在两个问题:
1.在调用析构时会对同一块空间多次释放,根据栈先进后出的特性s2的a成员会先释放该空间,后面s1的a成员又会释放一次该空间。
2.在修该其中一个a成员时另外一个a也会跟着改变。
在有动态类型开辟的空间时我们就需要手动去写拷贝构造函数,编译器生成默认的拷贝构造函数只能完成值拷贝(浅拷贝),需要自己写的拷贝构造函数就是深拷贝。
修改之后两个类的a成员不再是指向同一块空间的地址。
结论:在没有动态类型开辟空间的情况下我们可以不用去显示的写拷贝构造函数,调用编译器自动生成的就好,如果有动态类型函数开辟的空间就一定需要自己去写的显示拷贝构造函数。
11.赋值运算符重载
11.1运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注:
1、不能通过连接其他符号来创建新的操作符:比如operator@
2、重载操作符必须有一个类类型参数
3、用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5、.* :: sizeof ?: . 注意以上5个运算符不能重载。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator<(const Date& x)//重载小于号
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
//bool operator<(const Date& x1, const Date& x2)
//{
// if (x1._year < x2._year)
// {
// return true;
// }
// else if (x1._year == x2._year && x1._month < x2._month)
// {
// return true;
// }
// else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
// {
// return true;
// }
// return false;
//}
int main()
{
Date d1(2022, 4, 30);
Date d2(2022, 4, 15);
//两种不同的写法
printf("%d\n", d1 < d2);//这个编译时会被转换为d1.operator<(d2),下面的写法
printf("%d\n", d2.operator<(d1));
/*printf("%d\n", d1 < d2);//转换成operator<(d1,d2)
printf("%d\n", operator<(d1,d2));*/
return 0;
}
11.2赋值运算符重载
1. 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
/*Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}*/
/*void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 4, 30);
Date d2(2022, 4, 15);
Date d4;
//使用已经存在的两个对象之间赋值拷贝 --运算符重载函数
d1 = d4 = d2;
//使用一个已经存在的对象初始化另外一个对象 --构造函数
//ps: 在创建一个对象使用另一个对象来进行初始化。
//Date d3(d1);
return 0;
}
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)//left表示左操作数,right表示右操作数
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
Date d3;
d1 = d2 = d3;
return 0;
}
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?虽然像这种不存在动态开辟空间这样的类是没必要的。但是存在动态开辟的空间类就需要自己去写,需要使用到深度拷贝去解决。
注:运算符重载和赋值运算符重载其实就是写类的运算方法,因为一个类对象里面包含了多个内置类型(自定义类型成员会调用它自己定义的运算符重载函数和赋值运算符重载函数,)成员,编译器不知道如何进行计算,需要程序员自己去写一个函数进行成员之间的运算。
12.日期类的实现
//Date.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include <assert.h>
#include <iostream>
using namespace std;
class Date
{
//友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;//cout禁止使用用来输出单引号引起来的字符
}
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 展开之后 d2.operator=(&d2, d3),所有函数都可以像这样展开
Date& operator=(const Date& d);
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day)const;
// 日期-=天数
Date& operator-=(int day);
// 日期-天数
Date operator-(int day)const;
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// 日期-日期 返回天数
int operator-(const Date& d) const;
// >运算符重载
bool operator>(const Date& d)const;
// ==运算符重载
bool operator==(const Date& d)const;
// >=运算符重载
bool operator >= (const Date& d)const;
// <运算符重载
bool operator < (const Date& d)const;
// <=运算符重载
bool operator <= (const Date& d)const;
// !=运算符重载
bool operator != (const Date& d)const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);//流输出运算符重载
istream& operator>>(istream& in, Date& d);//流输入运算符重载
//Date.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
int Date::GetMonthDay(int year, int month)
{
int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{
return 29;
}
else
{
return arr[month];
}
}
//拷贝构造
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 全缺省的构造函数
Date::Date(int year, int month, int day)
{
if (month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
printf("非法日期\n");
assert(false);
}
}
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day) const
{
Date tmp = *this;
tmp += day;
return tmp;//虽然有返回值但是可以不接受
}
// 日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp = *this;
tmp -= day;
return tmp;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// 后置--
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// <运算符重载
bool Date::operator< (const Date& d) const
{
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;
return false;
}
// ==运算符重载
bool Date::operator==(const Date& d) const
{
return _year == d._year && _month == d._month && _day == d._day;
}
// >=运算符重载
bool Date::operator >= (const Date& d) const
{
return !(*this < d) || *this == d;
}
// >运算符重载
bool Date::operator>(const Date& d) const
{
return !(*this <= d);
}
// <=运算符重载
bool Date::operator <= (const Date& d) const
{
return *this < d || *this == d;
}
// !=运算符重载
bool Date::operator != (const Date& d) const
{
return !(*this == d);
}
//日期-日期 返回天数
int Date::operator-(const Date& d) const
{
//第一种方法
Date max = *this;
Date min = d;
int flag = 1;
int n = 0;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
while (min._year != max._year || min._month != max._month)
{
int tmp = min.GetMonthDay(min._year, min._month);
min._day += tmp;
while (min._day > tmp)
{
n += tmp;
min._day -= tmp;
++min._month;
if (min._month == 13)
{
min._month = 1;
++min._year;
}
}
}
n = max._day - min._day + n;
return n * flag;
//第二种方法
/*Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;*/
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13 && day > 0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
printf("非法日期\n");
assert(false);
}
return in;
}
//main.c
#include "Date.h"
void test1()
{
Date d1(2023, 5, 6);
/*d1.Print();
d1 += 1000;
d1.Print();*/
/*Date d2(d1);
d2.Print();
d2 -= 1000;
d2.Print();*/
Date d3(2023, 5, 6);
d3.Print();
d3 = d1 - 100;
d3.Print();
d3 = d1 + 100;
d3.Print();
}
void test2()
{
Date d1(2023, 5, 6);
Date d2(2023, 5, 6);
printf("%d\n", d1 - 100 < d2 + 200);
printf("%d\n", d1 + 100 > d2);
printf("%d\n", d1 == d2);
printf("%d\n", d1 <= d2);
printf("%d\n", d1 >= d2);
printf("%d\n", d1 != d2);
}
void test3()
{
Date d1(2023, 5, 6);
Date d2(2023, 5, 6);
int ret = d2 - d1;
printf("%d\n", ret);
}
int main()
{
test3();
return 0;
}
通过这个日期类的实现,可以知道类就是把所有相关的函数和变量给封装起来,只能提供给这个类的成员变量使用,防止在各种各样的场景下避免函数冲突的情况发生,并且一个类里面包含了许多的成员变量,只靠编译器的的内置类型计算方法无法给自定义类型进行计算(指类里面的成员如何进行运算要和谁(那个变量)进行运算),所以就有了运算符重载和函数重载,统统由程序员自己来实现,因为一个类如何进行加减、需要什么功能、这个程序要做什么、只有程序员自己知道。(但是函数里面终究是使用的是编译器内置类型自带运算符进行运算(列如+=重载函数和-=重载函数里面的while循环的运算符号运用的就是编译器自带的内置类型运算符),只不过是方法由我们来实现)。
13.const成员
将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数实际上是修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
1. const对象可以调用非const成员函数吗?不可以,权限的放大。
2. 非const对象可以调用const成员函数吗?可以,权限的平移。
3. const成员函数内可以调用其它的非const成员函数吗?不可以,因为在类内const成员函数的this指针不可以修改直接调用非const成员函数是默认以this指针作为参数去调用,不过函数参数里面有其他非const对象参数,可以通过该对象来调用非const成员函数。
4. 非const成员函数内可以调用其它的const成员函数吗?可以,权限不可以放大但是可以缩小。
14.取地址及const取地址操作符重载
这两个默认成员函数一般不要重新定义,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this;
//return nullptr;
}
const Date* operator&()const
{
return this;
}
private :
int _year = 1; // 年
int _month = 1; // 月
int _day = 1; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容或者不需要别人获取到地址。
15.再谈构造函数
15.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
15.2初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
1. 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类内没有显示默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class A
{
public:
A(int a)//改为 int a = 0,就是存在默认构造函数
:_a(a)
{}
private:
int _a;
};
class B
{
public:
//初始化列表:对象的成员定义的位置,初始化列表是构造函数的一部分
B(int a, int& ref)
:_aobj(a) //如果这里没有显示的写并且在没有默认构造函数的情况下就会报错
//显示的写了就会去调用该自定义类型对应的构造函数
, _ref(ref) //ref 是 n的别名, _ref又是ref的别名,所以_ref是n的别名
//如果ref不是引用类型,就会也野指针一样形成野引用。
, _n(10)
,_x(2) //如果显示的写了就使用它,否则使用缺省值
{}
private:
//声明
A _aobj; // 没有默认构造函数,如果有默认构造函数并且没有在初始化列表显示的写
// 一样会去最大最少都会在初始化列表出现一次,再去调用它的默认构造函数
//特征:必须在定义的时候进行初始化,所以没有初始化列表的情况下会报错
int& _ref; // 引用
const int _n; // const
int x = 1;//这里的1是缺省值,这里的缺省值是给定义在初始化列表的成员变量的,和构造函数的缺省值是不一样的
//构造函数的缺省值给的是形参在没有传参的情况下使用默认缺省值,是两码事。
};
int main()
{
//对象整体定义,在定义的时候会去调用构造函数,构造函数再去定义成员
int n = 10;
B b1(10, n);
B b2(20, 2);
return 0;
}
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值
//选D,按照声明的顺序进行初始化,先初始化_a2再依次向下初始化。
//_a2先初始化,_a1是随机值,_a1赋值给_a2,所以也是随机值。
注:拷贝构造函数也可以使用初始化列表来进行初始化。
15.3explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
explicit修饰构造函数,禁止类型转换。
16.static成员
16.1概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
class A
{
public:
A() {
++_scount;
}
A(const A& t) {
++_scount;
}
~A() {
--_scount;
}
//静态成员函数没有this指针,指定类域和访问限定符就可以访问。
static int GetACount()
{
return _scount;
}
private:
//成员变量 -- 属于每一个类对象,存储在对象里面
int _a1 = 0;
int _a2 = 0;
//静态成员变量 -- 属于类,属于类的每一个对象共享,储存在静态区
static int _scount;//这里不能给缺省值,因为这里的缺省值给的是初始化列表的。
};
//全局位置,只能在类外面定义,不可以在类内定义
int A::_scount = 0;
类的静态成员其实就是对全局的变量或者函数进行封装,如果是全局的就可以随意的去使用以及改变,相对之下使用类进行封装之后你只可以对它进行访问却不可以随便的去改变它。
16.2特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
问题:
1.静态成员函数可以调用非静态成员函数吗?
不可以。非静态的成员函数调用需要this指针,我没有this指针。
2.非静态成员函数可以调用类的静态成员函数吗?
可以,因为静态成员函数没有this指针,指定类域和通过访问限定符就可以访问。
17.友元函数
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元关键字:friend
友元分为:友元函数和友元类
17.1友元函数
根据之前写的日期的类实现重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
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;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
说明:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰,因为没有this指针
3.友元函数可以在类定义(类域内)的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
4.友元函数的调用与普通函数的调用原理相同
17.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。友元关系是单向的,不具有交换性。
比如以下代码Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C时A的友元,友元关系不能继承。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _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;//声明一个时间类
};
18.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参考友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的,受访问限定符的影响。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系,内部类和外部类各自占用独立的空间。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
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;
}
19.匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void Print() const
{
printf("%d\n", this->_a);
printf("%p\n", this);
}
private:
int _a;
};
void Printaddress(const A& a)
{
a.Print();
}
int main()
{
//printf("有名对象\n");
//A aa(1); //有名对象 -- 生命周期在当前域所在的范围
//aa.Print();
A bb(); //这种写法会被编译器当成无参函数
因为A可以当作类型,bb可以当作函数名,所以在定义一个对象要么传参,要么去掉圆括号。
//printf("---------------------\n");
//printf("匿名对象\n");
A(1);
//A(1).Print();//匿名对象 -- 生命周期在当前所在行,当执行当前行的下一行时就自动销毁。 -- 即用即销毁
//A(1).Print();//上一个和这一个不是同一个,因为即用即销毁,匿名对象之间不会进行冲突。
A.Print(); -- 错误写法,类型无法直接调用函数,需要加上圆括号。
//printf("const引用匿名对象\n");
A& pA = A(1); //匿名对象具有常性
//const A& pA = A(1); //const引用延长匿名对象的生命周期,生命周期也是在当前所在域
//printf("隐式转换产生的临时变量是局部变量\n");
//A b(1);
//b.Print();
//Printaddress(1);
//Printaddress(2);//和局部变量的地址是在同一个区域
return 0;
}
20.拷贝对象时的一些编译器优化
在传参和返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的,可以大大减少一些函数的调用,比如:拷贝函数等。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(cout A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
_a = aa._a;
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
//void Func1(A a) 和 void Func1(const A& a)虽然构成重载但是存在调用不明确的问题
//因为没有被const修饰的普通对象两个都可以进行调用
void Func1(A a)
{
}
void Func2(const A& a)
{
}
A Func3()
{
A aa;
return aa;
}
A& Func4()
{
static A aa;
return aa;
}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra1 = Func5();//拷贝构造 + 拷贝构造 -> 优化为拷贝构造
printf("-------分隔符------\n");
A ra2;
ra2 = Func5();
//A a1;
//Func1(a1);//不会优化
//Func1(A(1));//构造+拷贝构造 ->优化为构造
//A a2 = 1;// 构造+拷贝构造 ->优化为构造
//Func5();//这两个写法的调用结果是一样的
//A a1;
//Func1(a1);//调用拷贝构造
//printf("---------分隔符--------\n");
//Func2(a1);//没有调用拷贝构造
//Func3();
//Func4();
return 0;
}
21.再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,
那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
注:换而言之,类就是描述一个物体有什么功能是用来干什么的(它的功能就相当于数函数,把它们用东西围起来就是一个类,这个类取名叫洗衣机),以及它的功能是对什么东西进行做功,对的这个东西进行做功就称之为这个类的对象,函数的实现就是它的过程。