文章目录
- C++类和对象
- 1、类的定义
- 1.1类定义格式
- 1.2访问限定符
- 1.3类域
- 2、实例化
- 2.1实例化概念
- 3、this指针
- 4、类的默认成员函数
- 5、构造函数
- 5.1构造函数的特点
- 5.2实例分析
- 6、析构函数
- 6.1析构函数的特点
- 6.2实例分析
- 7、拷贝构造函数
- 7.1 拷贝构造函数的特点
- 7.2实例分析
- 7.3浅拷贝和深拷贝
- 8、赋值运算符重载
- 8.1运算符重载
- 8.1.1运算符重载的特点
- 8.1.2实例分析
- 8.1.3前置和后置运算的区分
- 8.2赋值运算符重载
- 8.2.1赋值运算符特点
- 8.2.2实例分析
- 9、初始化列表
- 9.1构造函数体赋值
- 9.2初始化列表
- 9.3 explicit(显示)关键字
- 10、友元
- 10.1友元函数
- 10.2友元类
C++类和对象
1、类的定义
1.1类定义格式
-
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束后面分号不能省略。类体中的内容称为类的成员;类的变量称为类的属性或成员变量;类中的函数成为类的方法或成员函数。有点类似于typedef struct,不过成员中还可以有函数。例如下文实现一个栈:
#include<iostream> using namespace std; class Stack { public: // 成员函数 void Init(int n = 4) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } void Push(int x) { // ...扩容 array[top++] = x; } int Top() { assert(top > 0); return array[top - 1]; } void Destroy() { free(array); array = nullptr; top = capacity = 0; } private: // 成员变量 int* array; size_t capacity; size_t top; }; // 分号不能省略 int main() { Stack st; st.Init(); st.Push(1); st.Push(2); cout << st.Top() << endl; st.Destroy(); return 0; }
-
C++中struct也可以定义类,C++兼容C中struct的用法,同时将struct升级成了一个类,明显的区别是struct中可以定义函数,一般情况下还是用class定义类。
struct Person{ public: void Init(string name,int age){//将传入的参数赋值给结构体中的成员 strcpy(_name,name); _age=age; } string _name; int _age; };
在主函数中使用的时候可以直接使用类型名,类名代表类型,如下:
Person p1; p1.Init("zhangsan",18);
1.2访问限定符
-
C++⼀种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
-
访问限定符一共有public、private、protected三个,public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected可以简单理解和private差不多,会在c++的继承中才体现出区别。
-
class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
-
访问限定符的作用域:从当前限定符开始,一直到下一个访问限定符出现,当后面无访问限定符则到类的 } 即类的结束时。
下面以一个类举例:
include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init(int n = 4);
private:
//成员变量
int* _array;
size_t _capacity;
size_t _top;
};
void Stack::Init(int n){
array=(int*)malloc(sizeof(int)*n);
_capacity=n;
top=0;
}
int main(){
Stack st;
st.Init();
return 0;
}
在上述代码中,Init函数是在public下的,是可以在类外被访问,因此可以在类外编辑函数内容,但private修饰下的几个变量就无法在类外访问编辑
1.3类域
-
类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用作用域操作符指明成员属于哪个类域。
-
类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
#include<iostream> using namespace std; class Stack { public: // 成员函数 void Init(int n = 4);//这里的函数时类内声明,类外定义 private: // 成员变量 int* array; size_t capacity; size_t top; }; // 声明和定义分离,需要指定类域 void Stack::Init(int n) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } int main() { Stack st; st.Init(); return 0; }
2、实例化
2.1实例化概念
-
用类类型在物理内存中创建对象的过程,成为实例化出对象。
例如将上面1.3中的stack实例化为一个对象:
Stack stk1;
-
类是对象进行⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
3、this指针
#include<iostream>
using namespace 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
Date d1;
Date d2;
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
- Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和 Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了 ⼀个隐含的this指针解决这里的问题
- 编译器编译后,类的成员函数默认都会在形参的第一个位置,增加一个当前类类型的指针,叫做this指针,比如Date类的Init的真实原型应该是
void Init(Date* const this,int year,int month,int day)
- 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值,
this->_year=year;
下面以几个例题来配合理解:
-
下面程序编译运行结果是():A 编译报错 B 运行崩溃 C 正常运行
#include<iostream> using namespace std; class A { public: void Print() { cout << "A::Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
选C
-
下面程序编译运行结果是():A 编译报错 B 运行崩溃 C 正常运行
#include<iostream> using namespace std; class A { public: void Print() { cout << "A::Print()" << endl; cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
选B
解析:在正常调用的时候,本质上调用过程应该如下:
A a1; a1.Print(&a1);
但是现在只创建了一个A*类型的空指针p,p->Print()语句中将p传递给()中的this指针,this指针为空,第一题中的Print函数没有访问类中的成员,只执行打印操作,不会报错。但是在第二题中,Print()函数访问了类下的_a成员,访问成员的本质应该是
this->_a
但是主函数中并未将A类型实例化,也就是并没有存储成员数据,而且this是个空指针,因此无法访问this->a,会报错。 -
this指针存在内存哪个区域的 () A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
选A前面有说过this指针作为一个形参传递,而形参是一种局部变量,存储在栈帧里面。
4、类的默认成员函数
- 简单来说,默认成员函数就是我们不写时,编译器默认生成的函数。
- 默认成员函数有六个,**构造函数(完成初始化工作)、析构函数(完成清理工作)、拷贝构造、复制重载、普通对象和const对象取地址。**接下来详细介绍几个函数。
5、构造函数
构造函数是特殊的成员函数,**构造函数是用来对象实例化时初始化对象。**构造函数的本质是要替代上文(1.3和3.中)的Stack和Date类中写的Init函数的功能,构造函数自动调用的特点完美的替代了Init函数。
5.1构造函数的特点
-
函数名与类名相同
-
无返回值(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
-
对象实例化时系统会自动调用对应的构造函数。
-
构造函数可以重载。
-
如果在定义类的时候没有特定写构造函数,即类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,如果写了构造函数,则编译器将不再默认生成。
-
无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。要注意很多人会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造。
-
我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,要看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。
5.2实例分析
举例1:
#include<iostream>
using namespace std;
class Date{
public:
Date(){//无参构造函数
_year=1;
_month=1;
_day=1;
}//给Date中的三个成员全部赋值为1
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 d1;
Date d2(2024,8,10);
d1.Print();
d2.Print();
return 0;
}
运行结果是1/1/1和2024/8/10,其中在主函数的Date d1;
中,在对象的实例化时自动调用构造函数,也相当于前文中的Init函数,这就免去了在实例化之后还要Init一遍。当然,这里的代码可以使用缺省参数
举例2
#include<iostream>
using namespace std;
class Date{
public:
Date(int year=1,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;
};
int main(){
Date d1;
// 注意:如果通过⽆参构造函数创建对象时,对象后面不用跟括号,否则编译器⽆法
// 区分这里是函数声明还是实例化对象。
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意⽤变量定义的?)
Date d2(2024,8,10);
d1.Print();
d2.Print();
return 0;
}
运行结果是1/1/1和2024/8/10,其中d1在实例化的时候自动调用了构造函数并且是全缺省的。
6、析构函数
析构函数与构造函数功能相反,析构函数主要用于对象生命周期结束时收回对象,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比前文1.1中Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
6.1析构函数的特点
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值。 (这里跟构造类似,也不需要加void)
-
⼀个类只能有⼀个析构函数。若没有写,系统会自动生成默认的析构函数。
-
对象生命周期结束时,系统会自动调用析构函数。
-
跟构造函数类似,我们不写时,编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
-
还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
-
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;如果默认生成的析构就够用,也就不需要自己写析构;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏。
-
⼀个局部域的多个对象,C++规定后定义的先析构。
大概结构如下图:
6.2实例分析
这里建议将代码copy,自己运行观察结果并调试观察_n的变化来感受一下
#include<iostream>
#include<string>
using namespace std;
class student {
public:
student(string name){ //有参构造函数,类内声明并定义
_n = (int*)malloc(sizeof(int));//只是为了体现效果才申请的,不用纠结有什么含义
_name = name;
}
~student() {//声明析构函数,若未声明,则自动生成默认析构函数
cout << "~Stack()" << endl;
//打印只是为了在运行代码后显示,说明执行了此析构函数
free(_n);
_n = nullptr;
}
private:
int* _n;
string _name;
};
int main()
{
student s1("ZhangSan");
return 0;
}
上述代码中,为了直观的感受析构函数的执行,在构造函数中写了一个申请空间,没有实际意义只便于展示。运行程序会发现屏幕打印了~Stack(),说明析构函数正常运行了,如果一步步调试会发现,主程序在s1的生命周期结束后执行了析构函数,即在执行完return 0后,会继续执行析构函数,释放空间。
总结:一般情况下,如果显示申请了资源(如申请空间),才需要自己实现析构,其他情况不用专门写析构函数
7、拷贝构造函数
7.1 拷贝构造函数的特点
如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
- 拷贝构造函数是构造函数的一个重载
- 拷贝构造的第一个参数必须是类类型的对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值,可见下文举例1和举例2。
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。 可见下文举例3。
- 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。这里还有⼀个小技巧,如果⼀个类显式实现了析构并释放资源,那么他就需要显式写拷贝构造,否则就不需要。
- 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。
7.2实例分析
举例1
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, 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;
};
int main()
{
Date d1(2024,8,11);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
运行结果应该是两个2024/8/11,其中在主函数的Date d2(d1);
就是一个拷贝构造,由于没有在Date类中专门写显式拷贝构造函数,所以默认生成了拷贝构造函数。如果我们自己写显式构造函数,代码可以参考如下:
举例2
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
//Date d2(d1)拷贝构造:
Date(Date &d){ //注意这里必须使用引用,可见上文7.1第二点内容
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,8,11);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
如上程序中Date类下写了拷贝构造函数,注意:拷贝构造的第一个参数必须是类类型的对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值。
举例3
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
//Date d2(d1)拷贝构造:
Date(Date &d){ //注意这里必须使用引用,可见上文7.1第二点内容
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func(Date d){
}
int main()
{
Date d1(2024,8,11);
Date d2(d1);
Func(d1);
return 0;
}
上述代码中调用了一个Func()函数,如果一步一步调试会发现,当程序运行到Func(d1)的时候,会先调用Date中的拷贝构造函数,再调用Func函数,即7.1中的第三点特点。这一特性决定了第二个特点中的“拷贝构造的第一个参数必须是类类型的对象的引用”,因为每次传值传参的时候都要先调用拷贝构造函数,如果不使用引用,那么就会一直陷入这个传参到拷贝构造函数的死循环中。
7.3浅拷贝和深拷贝
由于浅拷贝是一个字节一个字节一摸一样地复制,在浅拷贝一个构造函数中有申请空间的类的时候会出现两个对象使用一个空间的情况,例如6.2中的例子:
#include<iostream>
#include<string>
using namespace std;
class student {
public:
student(string name){ //有参构造函数,类内声明并定义
_n = (int*)malloc(sizeof(int));//只是为了体现效果才申请的,不用纠结有什么含义
_name = name;
}
~student() {//声明析构函数,若未声明,则自动生成默认析构函数
cout << "~Stack()" << endl;
//打印只是为了在运行代码后显示,说明执行了此析构函数
free(_n);
_n = nullptr;
}
private:
int* _n;
string _name;
};
int main()
{
student s1("ZhangSan");
student s2(s1);
return 0;
}
会发现运行会报错,用调试窗口查看s1和s2会发现他们中_n的地址是一样的,也就是说他们共享了一块空间,如下:
当两个对象同时共享一块空间,那么在对这块空间的内容进行操作的时候就会出现错误,并且s1和s2在生命周期结束的时候都要调用一遍析构函数,而这块空间释放一遍后再释放一遍显然是不行的,也会出现问题。我们想要达到的效果应该是两块不同的空间,但是其中的所有数据是相同的,因此我们在碰到有申请空间类似的类时需要自己写拷贝构造函数,即深拷贝,代码如下:
#include<iostream>
#include<string>
using namespace std;
class student {
public:
student(string name){ //有参构造函数,类内声明并定义
_n = (int*)malloc(sizeof(int));//只是为了体现效果才申请的,无含义
_name = name;
}
student(student& s){
_n=(int*)malloc(sizeof(int));
_name=s._name;
}
~student() {//声明析构函数,若未声明,则自动生成默认析构函数
cout << "~Stack()" << endl;
//打印只是为了在运行代码后显示,说明执行了此析构函数
free(_n);
_n = nullptr;
}
private:
int* _n;
string _name;
};
int main()
{
student s1("ZhangSan");
student s2(s1);
return 0;
}
8、赋值运算符重载
8.1运算符重载
8.1.1运算符重载的特点
-
运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
-
当运算符被用于类类型的对象时,c++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。简单来说,像内置类型如int、char等类型可以直接使用+、-、<、>等运算符,但是如果是类类型,可以类比c语言中结构体比较,无法直接使用<、>等运算符来比较,而是要自己写一个函数,给定一个比较的规则,而c++中运算符重载就类似于此 见举例1。
-
重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符(如++、– –等)有⼀个参数,二元运算符(如> ,<等)有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。 见举例1。
-
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。 见举例1。
-
不能重载的五个运算符: . (成员访问运算符) .* (成员指针访问运算符) sizeof (长度运算符) :: (域运算符) ?: (条件运算符)
-
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
8.1.2实例分析
举例1:
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
//==运算符重载:
bool operator==(const Date& x1, const Date& x2) {
return x1._year == x2._year && x1._month == x2._month && x1._day == x2._day;
}
int main()
{
Date d1(2024, 8, 11);
Date d2(2024, 8, 13);
if (d1 == d2) printf("相同");
//可以直接隐式调用使用重载后的==,
//显式调用应该为:operator==(d1.d2)
else printf("不相同");
return 0;
}
如上述代码,在类外进行了==的运算符重载,就类似一个普通的函数,但是在这里如果将Date类中的_year _month _day改为私有的话,类外的operator就无法访问,因此可以将operator重载放在类内,**注意operator重载函数中会有一个隐式的this指针,因此表面上我们只需要传一个参数即可,详见前文8.1第三、四点。**代码如下:
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
bool operator==(const Date& x1) {//由于有一个this指针当作一个参数,因此只用传一个参数就好
return x1._year == _year && x1._month == _month && x1._day == _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 8, 11);
Date d2(2024, 8, 13);
if (d1 == d2) printf("相同");
//可以直接隐式调用使用重载后的==,
//显式调用应该为:d1.operator(d2);
else printf("不相同");
return 0;
}
8.1.3前置和后置运算的区分
对于内置类型如int,我们知道可以有前置++和后置++,即对int i,有i++和++i,那么在运算符重载里,由于++会调用运算符重载函数,但是无法分辨前置++和后置++,所以他们的隐式调用虽然一样,本质的显式调用就有区别。
注意:下面代码中为方便演示,先不考虑日期满一个月月份+1等进位问题。进行两种++的重载,Date类使用++重载的意义是实现日期的前移(十一号变成十二号之类的)。
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
Date operator+=(int i) {
_day++;//先不考虑进位,主要演示前后置++
return *this;
}
//++d1,要返回++之后的值
//d1.operator++()运算符重载
Date& operator++()
{
*this += 1;
return *this;
}
//d1++
//d1.operator++(1)函数重载
Date operator++(int i)
// 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。
//C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
{
Date tmp(*this);
*this += 1;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 8, 11);
d1++;
d1.Print();
return 0;
}//运行结果2024/8/12
其中,++d1是对++函数的运算符重载,由于函数中d1(*this指针就是指向d1)在函数结束后生命周期没有结束,还是存在的,所以直接使用引用,那么返回的时候就会减少拷贝。后置++的函数中, 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 **C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。**本质上是对函数进行重载,而且由于返回的是++之前的值,因此使用tmp先拷贝构造一份,由于tmp的生命周期在这个函数结束后便会销毁,只是程序将值临时拷贝一份保留,因此不需要像之前一样使用引用返回。
8.2赋值运算符重载
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分(可见8.2.1第四点),拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
8.2.1赋值运算符特点
- 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是支持连续赋值场景。见举例2
- 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
- 拷贝构造用于一个已经存在对象拷贝初始化给另一个要创建的对象。拷贝赋值用于完成两个已经存在的对象进行直接的赋值。可见下面代码中的26-29行。 见举例1
- 一般来说,如果⼀个类显式实现了析构并释放资源,那么他就需要显式写赋值运算符重载。像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。
8.2.2实例分析
举例1
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
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(2024, 8, 11);
Date d2 = d1; //是拷贝构造
Date d3;
d3 = d1; //拷贝赋值
//显示应为d1.operator=(d3)
d3.Print();
return 0;
}
但是上面代码中的赋值重载无法实现例如d3 = d2 = d1;
的连续赋值操作,再看举例2是如何实现的。
举例2
连续赋值的操作原理应该是这样的:现在有:int x,y,z;
对它们进行赋值操作x = y = z = 1;
逻辑应该是从右往左,先将1赋值给z,然后z作为z = 1
的赋值表达式的返回值,再作为y = z
的右参数,将z赋值给y,以此类推将x,y,z全部赋值完成,因此要实现连续赋值操作,可以在赋值重载函数里增加返回值。
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {//全缺省构造函数
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
Date& operator = (const Date & d){
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
//例如执行的操作是d2.operator=(d3)则应该返回的是d2这个类,this是指向d2的指针,即d2的地址,因此应该返回*this,但是d2出了这个函数生命周期没有结束,所以可以使用引用返回减少再临时拷贝,减少资源占用
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 8, 11);
Date d2 = d1; //是拷贝构造
Date d3, d4;
d3 = d4 = d1; //拷贝赋值
//显示应为d1.operator=(d3)
d3.Print();
d4.Print();
return 0;
}
总结
- 构造一般需要自己写,自己传参定义初始化
- 析构,构造时有资源申请(比如malloc或者fopen)等,就需要显式写析构寒素
- 拷贝构造和复制重载,显式写了析构,内部管理资源就需要显式实现深拷贝
9、初始化列表
一个类的构造函数要初始化成员变量有两种方式,一种是构造函数体赋值,另一种是初始化列表。
9.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值,如下代码。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
9.2初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
例:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
//效果和函数体赋值是一样的,就是将括号里的值赋值给对应的成员变量
{}
private:
int _year;
int _month;
int _day;
};
【注意事项】
- 每个成员变量在初始化列表中只能出现一次(即只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,见下面代码。
其中,对于引用成员变量和const修饰成员变量,必须在变量定义的时候(private中是对变量的声明)进行初始化,所以无法在函数体内进行初始化,但是可以通过初始化列表进行初始化
例:
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();
}
注意代码在声明变量的时候是先_a2再_a1,因此他们的促使话顺序就是先初始化_a2再初始化_a1,这个初始化列表中的顺序无关,见下面的代码运行结果:
9.3 explicit(显示)关键字
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:
- 构造函数只有一个参数
- 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
- 全缺省构造函数
隐式类型转换
class A{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main(){
A x1(1);
A x2 = 2;//隐式类型转换,将整形转换成自定义类型。
const A& x3 = 3;
}
上面代码中的A x2 = 2;
就是一种隐式类型转化,完整过程应该是系统调用2作为构造函数的参数,创建一个A类型的临时变量,然后x2对这个临时变量进行拷贝构造。
也有编译器会对其进行优化,直接使用2来构造x2这个变量。
不过由于临时变量有常性,它是通过A的构造函数构造出来的,同类型之间可以做引用,因此直接使用引用是不行的,但是加上const修饰就可以了。
explicit关键字:
- explicit修饰构造函数时,可以防止隐式类型转化和复制初始化。
- explicit修饰转换函数时,可以防止隐式类型转换
例:
class A {
public:
explicit A(int x)
:_x (x)
{}
private:
int _x;
};
int main()
{
A a1(1);
A a2 = 2;
return 0;
}
运行结果报错:
这是因为14行中的代码发生了隐式类型转换,而我们加了explicit关键字,就无法使用隐式类型转换。
10、友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多 用。
友元分为:友元函数和友元类
10.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;
}
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
10.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;
};