C++类和对象
😤警醒自我
✍一、类和对象(上篇)
📃1.面向过程和面向对象的初步认识
C语言面向的是过程,注重的是过程,看到问题分析问题拆解问题求出问题的步骤,通过函数调用来实现。
可以通过下图手洗衣服来理解
C++面向的是对象,注重的是对象,将一件事情拆分成几个对象,将问题封装在对象中,通过对象之间的交互来实现。
可以通过下面图来理解,洗衣机与人的交互
📃2.类的引入
C语言中的结构体只能定义变量,但是C++中的结构体不仅可以定义变量又可以定义函数。
C++的结构体喜欢用class来代替struct.
//之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,
//会发现struct中也可以定义函数。
//成员函数直接定义到类里面
struct Stack
{ //Stack类截取部分
//成员函数
void Init(int n= 4 ) //缺省参数
{//初始化
a = (int*)malloc(n * sizeof(int));
if (a == NULL)
{
perror("malloc fail");
exit(-1);
}
capacity =n;
size = 0;
}
void Push(int x)
{//尾插
if (size == capacity)
{//扩容
int* tmp = (int*)realloc(a, 2 * capacity * sizeof(int));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
a = tmp;
capacity *= 2;
}
a[size] = x;
size++;
}
//成员变量
int* a;
int capacity;
int size;
};
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
return 0;
}
📃3.类的定义
class className
{
//类体又成员函数和成员变量组成
}; //注意这里分号不能缺
class为定义类的关键字,className为类的名字,{}中为类的主体(由成员函数和成员变量组成),{}后面的分号;不能缺。
类体中内容称为类的成员:类的变量称为类的属性或成员变量,类的函数称为类的方法或成员函数。
类的两种定义方式:
1.第一种
声明和定义全部放在类体中,需要注意的是:如果成员函数在类中定义,编译器可能将其当作内联函数处理。
2.第二种
类的声明放在一个.h文件中,类的定义放在一个.cpp文件中。注意成员函数前要加–>类名::
一般情况下都用第二种方式定义类
类的命名规则
成员变量前加_,是为了区分成员变量和参数。以后去了公司,公司会有规则,到时候入乡随俗就行。
//class Data
//{
//public:
// void Init(int year, int month, int data)
// {
// _year = year;
// _month = month;
// _data = data;
// }
//
private:
// //成员变量前加_,是为了区分成员变量和参数
// //声明
// int _year;
// int _month;
// int _data;
//};
📃4.类的访问限定符及封装
🗞4.1访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一起,使对象更加完善,通过权限选择性的将其接口提供给外部用户使用。
【访问限定符的说明】
1. public修饰的成员在类外可以直接访问。
2. private和protected修饰的成员在类外不可以直接访问(此处protected和private使类似的)。
3. 访问权限的作用域从(从上往下)第一个访问限定符开始到下一个访问限定符结束。
4. 从一个访问限定符开始,后面没有下一个访问限定符,则到}结束。
5. class的默认访问权限是private,struct是public(因为struct要兼容C)。
注意:访问限定符只在编译时有用,当数据映射到内存上,没有访问限定符上的区别。
【面试题】
问题:C++中class和struct的区别?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。
🗞4.2封装
【面试题】
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法有机结合在一起,隐藏类的属性和操作细节,仅对外提供接口与对象进行交互。
封装的本质上是一种管理,方便用户使用类。
比如: 对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。
因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
📃 5.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用**::作用域操作符**来指明这个成员属于那个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo() //类名+::(作用域操作符)
{
cout << _name << " "<< _gender << " " << _age << endl;
}
📃 6.类的实例化
用类类型创建对象的过程,就是类的实例化。
1.类是对对象进行描述,类似于模型一样的东西,限制了类体中有哪些成员变量和成员函数,并没有分配实际的内存空间。
2.一个类可实例化多个对象,实例出的对象分配了物理空间,每个对象都存储了成员变量。
3.总的来说,类如同一张别墅设计图,而对象是根据类设计出来的一栋栋别墅,实例化出的对象才能存储实际数据,占用物理空间,对象被分配了实际内存空间,而类没有。
请结合下面的代码来加深理解
//Date类--》日期类设计图
class Data
{
public:
void Init(int year, int month, int data)
{
_year = year;
_month = month;
_data = data;
}
//private:
//成员变量前加_,是为了区分成员变量和参数
//声明
int _year;
int _month;
int _data;
};
int main()
{
//类对象实例化--开空间
Data d1; //实例化--用设计图建一栋栋别墅
Data d2; //实例化--用设计图建一栋栋别墅
//为什么成员变量在对象,而成员函数不在对象中
//成员变量就像一个小区里面的一间间房间,它是独立的;每个对象的成员变量是不一样,需要独立的存储空间
// 而成员函数就像小区的公共场所(超市、篮球场等等),大家都可以用;每个对象的成员函数可以共享
//目的:节约空间
d1.Init(2022, 2, 23);
d1._year++;
d2.Init(2023, 2, 23);
d2._year++;
return 0;
}
📃 7.类对象
🗞 7.1如何计算类对象的大小
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
请往下看👇
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
🗞7.2结构体内存对齐规则
🤔我感觉大家应该对内存对齐规则比较陌生了吧,所以我在进行科普一下。
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意: 对齐数 = (编译器默认的一个对齐数 与 该成员大小)的较小值。
VS中默认的对齐数为8
注意: 自身对齐值该 (成员大小) :结构体变量里每个成员的自身大小
注意: 指定对齐值(编译器默认的一个对齐数):有宏 #pragma pack(N) 指定的值,这里面的 N一定是2的幂次方.如1,2,4,8,16等.如果没有通过宏那么在32位Linux主机上默认指定对齐值为4,64位的默认对齐值为8,AMR CPU默认指定对齐值为8;
注意: 有效对齐值(对齐数):(结构体成员自身对齐时有效对齐值为自身对齐值与指定对齐值中)较小的一个.
- 结构体总大小为:最大对齐数([所有变量类型最大者与默认对齐参数]取最小)的整数倍。min[所有变量类型最大者,默认对齐参数]。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举例子👇有代码和图片解释
结构体内存对齐例题1🤔
//编译器X86,编译器默认对齐数8字节
//其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
//结构体总大小为:最大对齐数([所有变量类型最大者与默认对齐参数]取最小)的整数倍。
// min(所有变量类型最大者,默认对齐参数)。
typedef struct _st_struct1
{
char a; //1 对齐数=min(1,8)=1 [0] 第一个成员在与结构体偏移量为0的地址处。
short b; //2 对齐数=min(2,8)=2 偏移量为1 2*0=0已被占用 2*1=2未被占用 ([0]、1、[2、3])
int c; //4 对齐数=min(4,8)=4 偏移量为4 4*0=0已被占用 4*1=4未被占用 ([0]、1、[2、3]、[4、5、6、7])
}st_struct1; //1号空间被浪费 最大对齐数=min(max(1,2,4),8)=4 0-7等于8 8为4的整数倍
//所以st_struct1结构体的总大小为8
printf("%ld\n",sizeof(st_struct1));
结构体内存对齐例题2🤔
//在X86编译器下编写的
#pragma pack(1) //这条指令改变了默认对齐数,对齐数变为1
typedef struct _st_struct2
{
char a; //1 对齐数=min(1,1)=1 [0] 第一个成员在与结构体偏移量为0的地址处。
int c; //4 对齐数=min(4,1)=1 偏移量为1 1*0=0已被占用 1*1=1未被占用 ([0]、[1、2、3、4])
short b; //2 对齐数=min(2,1)=1 偏移量为5 1*5未被占用 ([0]、[1、2、3、4]、[5、6])
}st_struct2; //最大对齐数=min(max(1,4,2),1)=1 0-6等于7 7为1的整数倍
//所以st_struct2结构体的总大小为7
printf("%ld\n",sizeof(st_struct2));
结构体内存对齐例题3🤔
//在X86编译器下编写的
//如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
//体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct s3
{
double d0; //8 对齐数=min(8,8)=8 [0-7] 第一个成员在与结构体偏移量为0的地址处。
char c; //1 对齐数=min(1,8)=1 偏移量为8 1*8=8未被占用 ([0-7]、[8])
int i; //4 对齐数=min(4,8)=4 偏移量为9 4*3=12未被占用 ([0-7]、[8]、9、10、11、[12-15])
}; //9-11号空间被浪费 最大对齐数=min(max(8,1,4),8)=8 0-15等于16 16为8的整数倍
//所以struct s3结构体的总大小为16
struct s4
{
char c3; //1 对齐数=min(1,8)=1 [0] 第一个成员在与结构体偏移量为0的地址处。
struct s3 s3; //对齐数=min(struct s3最大对齐数=8,8)=8 偏移量为1 8*1=8未被占用 ([0]、1-7、[8-23])
double d; //8 对齐数=min(8,8)=8 偏移量为24 8*3=24未被占用 ([0]、1-7、[8-23]、[24-31])
}; //1-7和9-11号空间被浪费 最大对齐数=min(max(1,8,8),8)=8 0-31等于32 32为8的整数倍
//所以struct s4结构体的总大小为32
printf("%ld\n",sizeof(struct s4));
🗞 7.3结构体内存对齐的意义
内存是以字节为基本单位,cpu在存取数据时,是以块为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数,这种以空间换时间的做法目的降低cpu的开销。 cpu存取是以块为单位,存取未对齐的数据可能开始在上一个内存块,结束在另一个内存块。这样中间可能要经过复杂运算在合并在一起,降低了效率。字节对齐后,提高了cpu的访问速率。
作者:秧歌Star
链接:https://juejin.cn/post/6973176851480969253
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
📃8.this指针
🗞 8.1this指针的引出
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(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
Date类的类体中包括Init和Print函数,类体中的函数并没有区分这些函数是那些对象的,当d1调用Init函数时,该函数是如何知道是设置d1对象,还是d2对象?
C++通过引入this指针来解决这个问题:C++编译器给“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都通过这个指针进行访问操作。只不过所有的操作对对象是透明的,用户不用来传递,编译器自动完成。
🗞8.2this指针的特性
1.this指针的类型:类型*const,即在成员函数中,this是不可以修改的
2.只能在"成员函数"的内部使用
3.this指针本质上是"成员函数"的形参,当对象调用成员函数时,实际上是将实参对象地址传给形参this。 所以对象中不存储this指针。
4.this指针是"成员函数"第一个指针形参,一般情况下通过编译器exc寄存器自动传递,不需要用户手动传递。
代码解释👇
class Data
{
public:
//void Init(int year, int month, int data)
void Init(int this->year, int this->month, int this->data)
{
//不同的对象调用不同的函数成员,编译器都进行了处理,对不同的对象进行了标记
/*cout << this << endl;
this->_year = year;
this->_month = month;
this->_data = data;*/
_year = year;
_month = month;
_data = data;
}
void func()
{
cout << this << endl;
cout << "func()" << endl;
}
private:
//声明
int _year;
int _month;
int _data;
};
//1.this存在哪里?--> 栈,因为它是隐含形参 / vs下存在ecx寄存器中
//2.空指针问题
int main()
{
//类对象实例化--开空间
//实例化--用设计图建一栋栋别墅
Data d1;
Data d2;
d1.Init(2022, 2, 23);
d2.Init(2023, 2, 23);
Data* ptr = nullptr;
ptr->func(); //正常运行
//ptr->Init(2023, 3, 5); //运行崩溃 ptr传递给this,this解引用程序崩溃
(*ptr).func(); //正常运行;
return 0;
}
🗞8.3C语言和C++实现Stack的对比
C语言:
可以看到,在用C语言实现时,Stack相关操作函数有以下共性:
每个函数的第一个参数都是Stack*
函数中必须要对第一个参数检测,因为该参数可能会为NULL
函数中都是通过Stack参数操作栈的
调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
C++:
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。
而且每个方法不需要传递Stack的参数了,编译器编译之后该参数会自动还原,即C++中 Stack *参数是编译器维护的,C语言中需用用户自己维护。
✍二、类与对象(中篇)
📑1.类的六个默认成员函数
如果一个类中一个成员函数都没有,简称空类。
空类中并不是一个成员函数都没有,编译器会自动生成六个默认成员函数。
默认函数:用户没有显示实现,编译器会自动生成。
下面我来一一介绍这些成员函数。
📑2.构造函数
🗞 2.1概念
构造函数是一种特殊的函数,名字和类名相同,创建类类型对象时由编译器自动调用完成,构造函数虽然名称叫构造,但它的主要任务并不是开空间创建对象,而是对对象进行初始化。
class Stack
{
public:
Stack()//构造函数
{
_a = nullptr;
_capacity = _size = 0;
}
Stack(int n)//构造函数
{
_a = (int*)malloc(n * sizeof(int));
if (_a == NULL)
{
perror("malloc fail");
exit(-1);
}
_capacity = n;
_size = 0;
}
private:
//成员变量
int* _a;
int _capacity;
int _size;
};
🗞 2.2特性
1.无返回值
2.函数名和类名相同
3.对象实例化时编译器会自动调用相应的构造函数
4.构造函数可以重载
📑3.析构函数
🗞 3.1概念
与构造函数的功能相反,不是完成对对象的本身销毁,局部对象的销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,对对象的资源进行清理工作。
class Stack
{
public:
~Stack() //析构函数
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
//void Destroy() //有了析构函数就不用Destroy函数
{//销毁
/*free(_a);
_a = nullptr;
_size = _capacity = 0;*/
//}
private:
//成员变量
int* _a;
int _capacity;
int _size;
};
🗞 3.2特性
1.析构函数是在类名前加符号~
2.无参无返回值函数
3.一个类只能有一个析构函数。若未显示定义,系统会自动生成析构函数。注意: 析构函数不能重载。
4.对象生命周期结束时,C++编译器系统会自动调用析构函数
📑4.拷贝构造函数
🗞 4.1概念
只有一个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时编译器会自动调用。
拷贝构造需要注意的事情:
①传值传参拷贝 需要调用拷贝构造。
②内置类型,编译器可以直接拷贝。
③自定义类型的拷贝,需要调用拷贝构造 。
④自定义类型用传值传参拷贝,会出现不同对象的不同指针共用一个空间,析构时会出现问题。
class Data
{
public:
Data(int year=1900, int month=1, int data=1)
{
_year = year;
_month = month;
_data = data;
}
Data(const Data& d)
{
cout << "(Data& d)" << endl;
_year = d._year;
_month = d._month;
_data = d._data;
//加const避免出现下面这种情况
/*d._year = _year;
d._month = _month;
d._data = _data;*/
}
void Print()
{
cout << _year << "/" << _month << "/" << _data << endl;
cout << _year << "年" << _month << "月" << _data <<"日"<< endl;
}
private:
int _year;
int _month;
int _data;
};
//传值传参拷贝 需要调用拷贝构造
//内置类型,编译器可以直接拷贝
//自定义类型的拷贝,需要调用拷贝构造 //自定义类型用传值传参拷贝,会出现不同对象的不同指针共用一个空间,析构时会出现问题
void Func1(Data d)
{
}
//传引用传参
void Func2(Data& d)
{
}
int main()
{
Data d1(2023, 2, 3);
Data d2(d1);
Func1(d2);
return 0;
}
🗞 4.2特性
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类对象的引用,使用传值方式编译器会报错,因为会产生无穷递归。
3.若未显示定义,系统会自动生成拷贝函数,拷贝方式是内存存储按字节拷贝,这种拷贝方式称为值拷贝,或称浅拷贝。
什么情况下需要进行拷贝构造?
自己实现了析构函数释放了空间,就需要拷贝构造。
拷贝构造
默认生成拷贝构造和赋值重载:
a、内置类型完成浅拷贝/值拷贝–按字节一个一个拷贝
b、自定义类型,去调用这个成员拷贝构造/赋值重载
class Date
{
public:
Date(int year=1 , int month = 1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
/*Date(Date d) //错误写法,拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
{
}*/
Date(const Date& d) //正确的写法
{ //加const的意义: ①防止被赋值变量与赋值变量写反,导致赋值变量被改变
// ②防止d的权限被放大
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
typedef int Datatype;
class Stack
{
public:
//构造函数
//错误构造,tmp出了栈帧就销毁了,
Stack(int capacity = 10)
{
cout << "Stack(int capacity = 10)" << endl;
int* tmp = (int*)malloc(sizeof(Datatype) * capacity);
if (tmp == NULL)
{
perror("malloc失败\n");
exit(-1);
}
_array = tmp;
_size = 0;
_capacity = capacity;
}
void Push(const Datatype& data)
{
//Checkcapacity();
_array[_size] = data;
++_size;
}
//什么情况下需要进行拷贝构造?
//自己实现了析构函数释放了空间,就需要拷贝构造
//拷贝构造
//默认生成拷贝构造和赋值重载:
//a、内置类型完成浅拷贝/值拷贝--按字节一个一个拷贝
//b、自定义类型,去调用这个成员拷贝构造/赋值重载
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
//Datatype* _array = (Datatype*)malloc(sizeof(Datatype) * st._capacity);
//这样创建的_array并非是*this._array, Datatype* _array代表你创建了一个新的_array
_array = (Datatype*)malloc(sizeof(Datatype) * st._capacity);
if (_array == nullptr)
{
perror("malloc失败\n");
exit(-1);
}
memcpy(_array, st._array, sizeof(Datatype) * st._size);
_capacity = st._capacity;
_size = st._size;
}
//析构函数
~Stack()
{
cout << "~Stack()" << endl;
assert(_array);
if (_array)
{
free(_array);
_array = nullptr;
_size = _capacity = 0;
}
}
private:
Datatype* _array;
int _capacity;
int _size;
};
class MyQueue
{
public:
//默认生成构造
//默认生成析构
//默认生成拷贝构造
private:
Stack _Pushstack;
Stack _Popstack;
int _size = 0;
};
int main()
{
Date d1(2023,3,8);
d1.Print();
//Date d2 = d1;
Date d2(d1);
d2.Print();
Stack st1;
//Stack st2(st1);
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2 = st1;
cout << "================================" << endl;
MyQueue q1;
MyQueue q2(q1);
return 0;
}
📑 5.赋值运算符重载
🗞 5.1运算符重载
意义:增加代码的可读性,是自定义类型能够直接使用+、-、==等等运算符。
函数名字:operator后面接需要重载的运算符
函数模型:返回值类型 operator操作符(参数列表)
注意: (这里可做公司考点)
①不能通过连接其他符号来创建新的操作符:比如operator@
②重载操作符必须有一个类类型参数
③用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
④作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
⑤ .* :: sizeof ? : . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。(考点)
🗞 5.2赋值运算符重载
//赋值重载
//d1=d2;
//为了进行连续赋值i=j=k
//返回Date&类型,不需要临时拷贝
Date& operator=(Date& d) //(Date d) 传值调用,不会出现无限递归
{
if (this != &d)
{//d!=d,防止深拷贝出现的问题
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
🗞 5.3前置++和后置++重载
前置++和后置++重载方式是不一样的,后置++需要带一个参数。即它们的形参不一样,而且它们返回的结果也不同。
//前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
📑 6.日期类的实现
Date.h👇
一点小知识🤓(后面文章将会提到):
在C++中,如果您希望能够直接打印自定义类型(例如,自定义类或结构体)或从流中提取自定义类型的数据,您需要自定义输入(插入)和输出(提取)运算符,分别为operator<<和operator>>。
原因如下:
1. C++的输入/输出库采用运算符重载的方式:C++的输入和输出操作是通过运算符重载实现的,这意味着对于内置类型,C++标准库已经提供了<<和>>运算符的重载,但对于自定义类型,您需要定义它们的行为。
2. 提供了类型特定的输出和输入格式:通过自定义operator<<和operator>>,您可以控制如何格式化自定义类型的输出和输入。这允许您以适当的方式呈现和解析自定义类型的数据。
3. 使代码更可读和可维护:自定义运算符使您的代码更具可读性和可维护性。当您或其他人阅读代码时,直接打印和解析自定义类型的代码将更容易理解,因为它们会遵循C++的标准输入和输出模式。
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
#include <string.h>
#include <math.h>
class Date
{
//友元函数
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date (int year = 1, 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;
}
void Print() const;
int GetMonthDay(int year, int month) 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;
Date& operator=(const Date& d);
Date& operator+=(int day);
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
int operator-(const Date& d) const;
//前置++
Date& operator++();
//后置++
Date operator++(int);
//前置--
Date& operator--();
//后置--
Date operator--(int);
//流插入
//void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
//想要自己直接打印自定义类型需要自己写流插入和流提取
//全局的流插入
//void operator<<(ostream& out,const Date& d);
ostream& operator<<(ostream& out, const Date& d);
//全局的流提取
istream& operator>>(istream& in, Date& d);
Date.cpp👇
#include "Date.h"
int Date::GetMonthDay(int year, int month) const
{
assert(month > 0 && month < 13);
int Month[] = { 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 Month[month];
}
}
void Date::Print() const
{
if(_year>0&&
_month>0&&_month<13&&
_day <= GetMonthDay(_year, _month)&&
_day>0)
{
cout << _year << "/" << _month << "/" << _day << endl;
}
else
{
cout << "非法日期:";
cout << _year << "/" << _month << "/" << _day << endl;
}
}
bool Date::operator==(const Date& d) const
{
return this->_year == d._year &&
_month == d._month &&
_day == d._day;
}
//小于
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;
}
else
{
return false;
}
}
//小于等于
bool Date::operator<=(const Date& d) const
{
return (*this < d || *this == d);
}
//大于
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);
}
//赋值重载
//d1=d2;
//为了进行连续赋值i=j=k
//返回Date&类型,不需要临时拷贝
Date& Date::operator=(const Date& d) //(Date d) 传值调用,不会出现无限递归
{
if (this != &d)
{//d!=d,防止深拷贝出现的问题
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//加等
//d=d+100
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= -day;
return *this;
}
_day += day;
//进位
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
//加
//d+100
//复用Date& Date::operator+=(int day)
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-=(int day)
{
if (day < 0)
{
*this += -day;
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//减
//d-100
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;
}
//两个日期相减
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (max < min)
{
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)
{
in >> d._year >> d._month >> d._day;
return in;
}
📑7. const成员
将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
不可以。当您创建一个const对象时,它表示对象的状态不会被修改,因此只能调用const成员函数。尝试调用非const成员函数将导致编译错误。这是C++语言中的一个重要安全特性,它确保const对象的不可变性。- 非const对象可以调用const成员函数吗?
可以,非const对象可以调用const成员函数。const成员函数表示它们不会修改对象的状态,因此它们可以在非const对象上安全地调用。这是因为在非const对象上调用const成员函数不会引发任何副作用。- const成员函数内可以调用其它的非const成员函数吗?
不可以,需要强转。- 非const成员函数内可以调用其它的const成员函数吗?
可以,非const成员函数内可以调用其他的const成员函数。非const成员函数可以读取和修改对象的状态,但它们也可以调用const成员函数来访问对象的状态,因为const成员函数的调用不会导致不可变性冲突。这使得代码更具灵活性,因为非const成员函数可以共享一些逻辑,而不需要重复编写。
📑 8.取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
✍三、类和对象(下篇)
📜1.再谈构造函数
🗞1.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
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初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔开的成员变量列表, 每一个"成员变量"跟一个放在括号中的初始值或表达式。
注意:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class B
{
public:
B(int i)
{
cout << "B()" << endl;
}
private:
int b;
};
class A
{
public:
//每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
A()
:_a2(1),
_x(1),
_ref(_a1),
_bb(0)
{
_a1++;
_a2--;
}
private:
int _a1 = 1;
int _a2 = 2;
//1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
//2.类中包含以下成员,必须放在初始化列表位置进行初始化:
//引用成员变量
//const成员变量
//自定义类型成员(且该类没有默认构造函数时)
const int _x;
int& _ref;
B _bb;
};
int main()
{
A aa;
return 0;
}
🗞1.3explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。用explicit修饰构造函数,将会禁止构造函数的隐式转换。
class A
{
public:
//explicit A(int a) //如果不想普通类型转换为A类型(隐式转换),在构造函数面前添加关键值
//单参数构造函数
A(int a)
//初始化列表
:_a1(a)
{
cout << "A(int a)" << endl;
}
//多参数构造函数
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1),
_a2(a2)
{
}
//拷贝构造
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
*******************************************************
单参数构造函数 C++98
A aa(1);
//拷贝构造会有临时变量生成
A bb = 1; //赋值,隐式类型转换 //按理论出牌,应该是先构造生成bb,再进行拷贝构造 //实际--》 构造+拷贝构造+优化-->构造
//A& ret = 10; //这样编译不过 左边是非常量引用,右边是常量
const A& ret = 10; //为啥加了const又能编过了呢? 赋值进行拷贝构造,拷贝构造会产生临时变量,而引用&不会进行优化,
//ret是临时变量的别名 --》间接说明此过程没进行优化,产生了拷贝构造,且有临时变量
//********************************************************
//多参数构造函数 C++11
A aa1(1, 1);
//A aa2 = 2 = 2; //不支持这样赋值
A aa3 = { 2,2 }; //支持这样赋值
const A& ref = { 2,2 };
return 0;
}
📜 2.static成员
🗞2.1概念
声明为static的类成员为类的静态成员函数,用static修饰的成员变量为静态成员变量,用static修饰的成员函数为静态成员函数。静态成员函数一定要在类外进行初始化。
//实现一个类,计算程序中创建了多少个类对象
class A
{
public:
//构造函数
A(int a = 0)
{
count++;
}
//拷贝函数
A(const A& aa)
{
count++;
}
/*int Getcount()
{
return count;
}*/
//成员函数加static--静态成员函数--没有this指针修饰
static int Getcount()
{
//_a++; //静态成员函数不能直接访问非静态成员
return count;
}
//静态成员也是类的成员,受public、protected、private 访问限定符的限制
static int count ; //声明
private:
//类中的静态变量 //静态成员为所有类对象所共享,不属于某个具体的类
int _a = 0;
};
//静态变量成员必须在类外定义
int A::count = 0; //定义
void func(A a)
{
}
int main()
{
A aa1(1);
A aa2(aa1);
func(aa1);
A aa3 = 1;
//const A& ref = 1;
//******************************************************
//count为公有
cout << A::count << endl;
cout << aa1.count << endl;
cout << aa2.count << endl;
cout << aa3.count << endl;
A* ptr = nullptr;
cout << ptr->count << endl; //即使是空指针(未进行解引用),只要能进入类,也能调用count
//*******************************************************
//***********************
//私有时 count在访问限定符private中
cout << aa1.Getcount() << endl;
//************************
return 0;
}
🗞2.2特性
1.静态成员为所有类所共享,不属于某个具体的对象,存放在静态区。
2.静态成员变量定义必须在类外定义,定义时不需要加stastic,类中只是声明。
3.类静态成员可以用类名::静态成员或者对象.静态成员来访问。
4.静态成员没有隐藏的this指针,所以不能访问任何非静态成员。
5.静态成员也是类的成员,也受public、protected、private访问限定符的限制。
【问题】
- 静态成员函数可以调用非静态成员函数吗?
答:不能访问任何非静态成员- 非静态成员函数可以调用类的静态成员函数吗?
答:可以,非静态成员函数可以调用类的静态成员函数。静态成员函数是与类关联的函数,而不与类的具体实例相关。它们可以通过类名来调用,因此可以从非静态成员函数内部调用静态成员函数。
📜3.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
🗞3.1友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
#include <iostream>
class MyClass {
private:
int privateData;
public:
MyClass(int data) : privateData(data) {}
// 声明友元函数
friend void FriendFunction(const MyClass& obj);
void DisplayData() {
std::cout << "Data: " << privateData << std::endl;
}
};
// 定义友元函数,可以访问私有成员
void FriendFunction(const MyClass& obj) {
std::cout << "Friend Function: " << obj.privateData << std::endl;
}
int main() {
MyClass myObj(42);
myObj.DisplayData(); // 调用成员函数
FriendFunction(myObj); // 调用友元函数
return 0;
}
🗞 3.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
①友元关系是单向的,不具有交换性。
②友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
③友元关系不能继承,在继承位置再给大家详细介绍。
#include <iostream>
class MyClass {
private:
int privateData;
// 声明友元类
friend class FriendClass;
public:
MyClass(int data) : privateData(data) {}
void DisplayData() {
std::cout << "Data: " << privateData << std::endl;
}
};
class FriendClass {
public:
void AccessPrivateData(const MyClass& obj) {
std::cout << "FriendClass accessing private data: " << obj.privateData << std::endl;
}
};
int main() {
MyClass myObj(42);
FriendClass friendObj;
myObj.DisplayData(); // 调用成员函数
friendObj.AccessPrivateData(myObj); // 通过友元类访问私有成员
return 0;
}
📜4.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
//内部类
class A
{
private:
int _h = 1;
static int k;
public:
//内部类,跟A是独立,受类域的影响
class B //内部类天生就是外部类的友元
{
public:
void func(const A& a)
{
cout << a.k << endl;
cout << a._h << endl;
}
private:
int _b = 2;
};
};
int A::k = 0;
int main()
{
A aa;
cout << sizeof(aa) << endl;
//B为A的公有时,可以这样定义
A::B bb;
bb.func(aa);
return 0;
}
📜5.匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
📜6.拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
对象返回总结:
1.接收返回值尽量拷贝构造接收,不要赋值接收
2.函数返回对象时,尽量匿名对象函数传参的总结: 尽量使用const &接收 传参
//编译器的优化
class A
{
public:
//构造函数
//列表初始化
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
//拷贝函数
//列表初始化
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A a)
{
}
void func2(const A& aa)
{
}
//int main()
//{
// A aa1 = 1; // 构造+拷贝构造+优化--》优化为直接构造
// cout << "********************" << endl;
// func1(aa1); //无优化
// cout << "********************" << endl;
// func1(2); // 构造+拷贝构造+优化--》优化为直接构造
// cout << "********************" << endl;
// func1(A(3)); // 构造+拷贝构造+优化--》优化为直接构造
// cout << "********************" << endl;
// cout << "--------------------------------------------------------------" << endl;
//
// func2(aa1); //无优化 aa1与a是同一个对象,a是aa1的别名无需拷贝构造
// //cout << "********************" << endl;
// func2(2); //无优化
// //cout << "********************" << endl;
// func2(A(3)); // 无优化
// //cout << "********************" << endl;
// return 0;
//}
A func3()
{
A aa;
return aa;
}
A func4()
{
return A();
}
int main()
{
func3(); //构造+拷贝构造 -- 无优化
A aa1 = func3(); //拷贝构造+拷贝构造--》优化为一个拷贝构造
cout << "******" << endl;
A aa2; //干扰优化
aa2 = func3(); //不能优化
cout << "---------------------" << endl;
func4(); //构造+拷贝构造--》优化直接构造
A aa3 = func4(); //构造+拷贝构造+拷贝构造 --》优化为直接构造
return 0;
}
//对象返回总结:
//1.接收返回值尽量拷贝构造接收,不要赋值接收
//2.函数返回对象时,尽量匿名对象
//函数传参的总结:
//尽量使用const & 传参
📜7.再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗
衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什 么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清 楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、 Java、Python等)将洗衣机用类来进行描述,并输入到计算机中。
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣 机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才 能洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。 在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化 具体的对象。
🐷四、结语
小生不才,文章必有诸多缺点,希望大家能够指正,感谢您的阅读!