目录
前言:
C语言是面向过程的,关注的是过程,逐步解决问题的过程。
C++是基于面向对象的,关注的是对象,将一件事情分解成不同的对象,靠对象之间的交互完成。
一、 类的引入
C语言中的结构体里面只能定义变量,而C++的结构体里面不仅可以定义变量,也可以定义函数。
struct Node
{
//成员变量
int a;
int b;
//成员函数
void Print()
{
cout << "C++ class" << endl;
}
};
1. 类的定义
class className //className 是类名
{
//类的内容 由 成员变量和成员函数组成
//...
}; //注意这里的分号
在成员函数(类的方法)中,根据人为的一些规则来区分形参和成员变量
例如: 定义日期类的时候
class Date
{
void Init(int year, int month, int day)
{
year = year; //这里我们就会比较容易搞混
month = month;
day = day;
}
int year;
int month;
int day;
};
所以为方便区分参数和成员变量,我们通常会给成员变量前加上一个下划线 _成员变量 (其他方式也可以) 来区分
class Date
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year; //通过 _成员变量 来区分
int _month;
int _day;
};
2. 类的访问限定符及封装
(1) 访问限定符
- public 修饰的成员可以在类的外面直接被访问
- protect 和 private 修饰的成员在类外面不能直接被访问
- 访问权限的作用域:从访问限定符开始直到下一个访问限定符出现为止
- 如果后面没有访问限定符,作用域就到 } (即类的结束)
- 在没有写访问限定符时,class的默认访问为private,struct 的默认访问为public
例如:
class Date
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year; //通过 _成员变量 来区分
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024,1,31); //error class的默认访问为private
return 0;
}
改正:加上public
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year; //通过 _成员变量 来区分
int _month;
int _day;
};
上述的成员变量和成员函数都可以访问到了。
C++ 中 struck 也是可以上访问限定符的
int main()
{
// struct Date d;
// class Date d; 这样写也可以
Date d1; //但是一般为了方便会把这里的class给省略
return 0;
}
关于类的定义两种方式:
- 声明和定义全部放在类体中,当成员函数如果在类中定义,编译器可能会将其当成内联函数处理
- 类的声明放在 . h 文件按中,成员函数定义放在 . cpp 文件中
在第二种方法中 涉及到 (命名空间)类域
类的作用域: 类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域
//test.h
// 声明
class Date
{
private:
//成员变量
int _year; //通过 _成员变量 来区分
int _month;
int _day;
public:
//成员函数
void Init(int year, int month, int day);
void Print_Date();
};
//test.cpp
//函数定义
void Date::Init(int year, int month, int day) //注意这里的Date类域,说明是Date类中的成员函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print_Date() //加上Date类域
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 上述 先去局部去找,再去全局找,最后去Date类中找
C++中struct 和 class 的区别:
相同点:类的成员都可以有成员函数
不同点:struct 定义类的默认访问为 public (兼容C语言) ;class 定义类的默认访问为 private。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
(2) 封装
面向对象的三大特性:封装、继承、多态
封装:将数据和操作数据的方法进行有机的结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
3. 类的实例化
用类的类型创建对象的过程,称为类的实例化。通过实例化,我们可以得到一个具有类定义属性和方法的对象,并且可以对这个对象进行操作。
- 声明出一个类并没有分配实际的内存空间
- 一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量
- 设计出类但没有进行实例化并不占内存空间
Date._day; // error Date类是没有空间,只有实例化出对象才有具体年龄
//实例化对象的正确方法
Date d1; //定义对象,实例化
( 定义的一个标志 是:开空间 )
4. 类对象模型
(1) 计算类对象的大小
class A
{
public:
void fu1() {}
private:
int _a;
};
class B
{
public:
void fu1() {}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl; // 4
cout << sizeof(b) << endl; // 1
cout << sizeof(c) << endl; // 1
return 0;
}
通过计算类的对象,一个类的大小,就是该类成员变量之和,前提在内存对齐下
空类的大小是1,编译器给空类一个字节来标识这个类的对象
结构体内存对齐规则:
- 首先第一个成员在 结构体偏移量为0的地址处
- 其他成员变量要对齐到(对齐数)的整数倍处,对齐数 = 编译器默认对齐数大小 与 该成员大小 之中的较小值
- 结构体的总体大小为:最大对齐数( 就是所有变量类型最大者 与 默认默认对齐数中较小的一方) 的整数倍
- 如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数 的整数倍,结构体的整体大小就是所有最大对齐数(包括嵌套结构体的对齐数) 的整数倍
内存对齐:会导致空间浪费,为什么要内存对齐呢?请猛击
(2) 类对象的存储方式
Date d1;
Date d2;
d1._day;
d2._day;
//定义出的变量地址不同
d1.Print_Date();
d2.Print_Date();
// d1 与 d2 的 Print_Date地址相同
存储方式
例题:思考下方运行结果
Date a1;
Date* p1 = &a1;
p1->f1();
Date* p2 = nullptr;
p2->f1();
//A、编译错误
//B、运行错误
//C、运行正常
答案是 运行正常,因为 虽然有箭头,但没有进行解引用( f1并没有在p2指向的内存空间里面,而是在公共代码区)
5. this指针
先看下方问题:
这里我们首先要知道
所以去访问实例对象的,那么C++又是怎样分出d1和d2的呢?
C++中通过引入this指针解决该问题,C++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),函数体中所有"成员变量"的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要传递,编译器自动完成。
编译器处理
所以是通过隐藏的this指针来进行区分d1和d2的
那this指针存在哪里? 我们发现this指针出现在函数的形参中,又形参和局部变量 存在栈帧里面,所以this指针存在栈里面
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
//答案是 C 正常运行
原因是:Print函数在公共代码区,不在p指向的内存空间中,所以并没有触发p指针解引用去内存找,Print函数地址在编译的时候就有了。
如果 (p->_a++; 这里会造成 B、运行崩溃)
如果(*p).Print(); 答案是 正常运行和p->Print();一样
// 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
//答案是 B
this指针的传递是没有问题的,但是空指针的访问会导致运行崩溃
小总结
-
Date this 其实是 Date const this** ,即this指针指向的内容可以进行修改,但是this指针本身是不可以改变的,不能给this指针赋值
-
隐藏的this指针我们不能显示写出来,但是在函数内部可以用
void Print_Date() { cout << this->_year << "/" << this->_month << "/" << this->_day << endl; } //包括 void Init(int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; }
-
this指真本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
-
this指针是"成员函数"第一个隐含的指针形参,
-
一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递
-
隐藏的this指针我们不能显示写出来,但是在函数内部可以用
void Print_Date() { cout << this->_year << "/" << this->_month << "/" << this->_day << endl; } //包括 void Init(int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; }
-
this指真本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
-
this指针是"成员函数"第一个隐含的指针形参,
-
一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递
二、 类的6个默认成员函数
1. 构造函数
当一个类什么成员都没有的时候称为空类
实际上空类并不是什么的都没有,当类在什么都没有写时,编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date{};
构造函数是一个特殊的成员函数,构造函数的函数名和类名相同,创建类对象时,由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在该对象的整个生命周期内只调用一次。
构造函数虽然名字叫构造,但实际上主要任务并不是开空间创建对象,而是初始化对象。
注意:
- 函数名与类名相同
- 没有返回值(返回类型不用写)
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
#include<iostream>
using namespace std;
class Date
{
public:
//无参的构造函数
Date() //特殊的成员函数
{
_year = 1;
_month = 2;
_day = 3;
}
//带参数的构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print_Date()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //对象实例化的时候 编译器自动调用无参构造函数
d1.Print_Date();
Date d2(1,1,1); //调用带参的构造函数
d2.Print_Date();
return 0;
}
无参构造Date d1(); 不能这样的的原因是:不能与函数声明区分开 Date func(); 这样无参数的函数声明区分开,而带参是可以区分开的 因为Date func(int…);这里是有类型的
注意
C++规定对象实例化的时候必须调用构造函数,如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义了,编译器将不再生成。
对象实例化需要调用相应的构造函数吗,对于上述的两个构造函数其实也是可以通过缺省函数写成一个
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
当没有显示定义构造函数的时候
Date d1;
d1.Print_Date();
打印
发现打印结果竟然是随机值(没有初始化)
原因是:
C++把类型分为 内置类型(基本类型) 和 自定义类型。内置类型就是语言提供的数据类型,如:int,float…;自定义类型 使用 class/struct/union 等自己定义的类型。C++98规定默认生成的构造函数对内置类型不做处理,自定义类型回去调用自己的默认构造
class Time
{
public:
Time()
{
cout << "Time" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print_Date()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
//内置类型(基本类型)
int _year = 1; //类声明时给值
int _month = 1;
int _day = 1 ;
//自定义类型
Time _t;
};
int main()
{
Date d1;
d1.Print_Date();
return 0;
}
打印:
即 默认生成的构造函数对内置类型不做处理,自定义类型回去调用自己的默认构造
然而,内置类型没有初始化
所以 在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值
分析一个类型成员和初始化需求:
- 需要写构造函数我们就自己写;不需要时就是用编译器自己生成的
- 对于绝大多数情况下都需要我们自己实现构造函数
严格上讲默认构造函数有三种:
第一个是编译器默认生成的,第二种是无参构造函数,第三种是全缺省构造函数
class Date
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print_Date()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print_Date();
return 0;
}
当编译上述代码时:
因为写了构造函数,编译器就不会自动生成相应的默认构造函数,因为对于无参和全缺省只能存在一个,一般情况下,建议提供全缺省构造函数
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
2. 析构函数
(1) 析构函数的概念
析构函数:析构函数不是对对象本身销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
(2) 析构函数时特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符~
~Date(){} //析构函数
- 析构函数没有参数,没有返回值类型
- 一个类只能有一个析构函数;若没有显示定义,系统会自动生成默认的析构函数;注意析构函数不能重载
- 当对象生命周期结束时,C++编译时系统会自动调用析构函数。
调用析构函数的小细节:
因为局部变量和函数一般是在函数栈帧里面的,即析构函数,后定义的先析构
①
class Date
{
public:
Date(int year = 1)
{
_year = year;
}
~Date()
{
cout << "~Date()->" <<_year<< endl; //调用析构的时候
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1); //析构函数 后 定义的 先 析构
Date d2(2);
return 0;
}
打印结果:
根据打印结果也发现,后定义的先进行析构
②
Date d1(1); //析构函数 后 定义的 先 析构
Date d2(2);
static Date d3(3);
打印结果:
上述增加了一个 static Date d3(3); 却没有按照后定义的先进行析构,以为它们存放的区域是不同的,static Date d3(3);(存放在静态区的)其生命周期是全局的,所以main函数销毁的时候会调用d3的析构函数,当然在调用d3的析构前,需要先去销毁局部的
再次修改一下:
void func()
{
Date d3(3); //局部的
static Date d4(4); //全局的
}
int main()
{
Date d1(1); //析构函数 后 定义的 先 析构
Date d2(2);
func();
return 0;
}
打印结果:
析构,先局部再全局
小总结:
析构函数的调用顺序:
先 局部对象(后定义的先析构) --> 局部的静态对象 --> 全局对象(后定义的先析构)
- 默认生成的析构函数和构造函数类似
内置类型不做处理,自定义类型的成员去调用它的析构函数 - 局部成员,即函数需要进入函数栈帧,所以析构函数是后定义的先析构( 满足后进先出 )
分析下方代码:
#include<iostream>
using namespace std;
class Time
{
public:
~Time()
{
cout << "Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 10, int month = 10, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
void Print_Date()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
//内置类型
int _year = 1;
int _month = 1;
int _day = 1;
//自定义类型
Time _t;
};
int main()
{
Date d1;
return 0;
}
程序运行结果:
Time()
发现main方法中没有直接创建Time类对象,最后却调用了Time类的析构函数
原因是:在main函数中创建了Date对象 d1,而d1中包含4个成员变量,其中_year,_month,_day是内置类型成员,而_t是Time类对象,所以在d1销毁时,要将的d1中包含的Time类的_t对象进行销毁,所以要调用Time类的析构函数。
所以不能直接调用Time类的析构函数,首先是编译器先生成一个Date类的默认的析构函数,进而在其内部调用Time类的析构函数。
要保证其内部每个定义对象都可以正确销毁
3. 拷贝构造函数
在类和对象中,我们可能会想在创建对象时,可否创建一个 与 已经存在对象一模一样的新对象呢?
拷贝构造函数:在C++中,拷贝构造函数是一种特殊的构造函数,用于创建并初始化一个对象,作为另一个已经存在对象的副本。当创建对象需要基于已存在对象的内容时,拷贝构造函数就会被调用。
例如:
#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_Date()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//将d1拷贝构造给d2
Date(Date & d) //拷贝构造,这里必须使用引用,Date(Date d)会导致无穷递归的
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(1970,1,1);
Date d2(d1); //拷贝构造用一个相同类型的其他对象(即,日期类 类型对象)进行构造
return 0;
}
调试查看结果发现;
完成了拷贝构造
上述的拷贝构造函数形参为什么使用引用呢?
因为,C++中,对于自定义的类型传值传参时都会调用拷贝构造
首先先看 传值 传参 和 传引用 传参
关于传值传参调用拷贝构造:
传引用传参就不用调用拷贝构造函数
当使用传值传参的时候,再去调用拷贝构造引起的无穷递归的原因是:
调用拷贝构造,要先传参,这里因为是传值传参,会形成一个新的拷贝构造。当按值传递对象到函数(包括拷贝构造函数)时,C++默认会调用拷贝构造函数来创建函数参数的副本。如果拷贝构造函数的实现本身又试图使用按值传递的方式来复制其参数,这就会导致无穷递归。
如图:
所以 使用引用可以解决无穷递归问题
当我们使用引用后,但是不想因为引用取别名去修改原来的值,所以使用了const进行常量修饰,相当于缩小了权限
Date(const Date &d) //const进行修饰,让指向的内容只能读,不能修改
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
拷贝构造与普通构造函数的一个区别:
先看下方代码:
//拷贝构造与构造的一个区别
#include<iostream>
using namespace std;
class Date
{
public:
void Print_Date()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//构造函数
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1970,1,1);
Date d2(d1);
d1.Print_Date();
d2.Print_Date();
return 0;
}
打印结果;
上述代码中并没有写拷贝构造,调用d2的Print_Date时候发现,打印结果和d1的一样
我们知道,拷贝构造也是默认成员函数,当我们没有手动写时,编译器会生成一个默认成员函数。
当没有写任何的构造函数,编译器会默认生成;写了任何构造函数,编译器不在生成构造函数。
构造函数:编译自动生成的,对于内置类型不做处理,自定义类型去调用它的默认构造函数。C++11的补丁 内置类型成员变量在类中声明时可以给默认值
拷贝构造函数:
对默认生成的内置类型进行处理
对于自定义类型,当写了拷贝构造函数,没有写构造函数发现;
我们可以使用
//强制生成构造函数
Time() = default;
class Time
{
public:
~Time()
{
cout << "~Time() " << endl;
};
//强制生成构造函数
Time() = default;
//拷贝构造函数
Time(const Time& d)
{
cout << "Time(const Time& d)" << endl;
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
private:
int _hour;
int _minute;
int _second;
};
小总结:
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个,必须是类 类型对象的引用,如果使用传值的方式编译器直接报错(引发了无穷递归)
- 当没有显示定义,编译器会生成默认的拷贝构造函数,内置类型是按照字节序方式直接拷贝的,而自定义类型是调用相应的拷贝构造函数完成的。
- 像上述日期类是没必要显示实现拷贝构造函数的,但是像malloc动态开辟空间(在堆上)需要我们显示实现拷贝构造函数。
注意:在(堆空间)不能浅拷贝(会造成二次free,容易出错),需要进行深拷贝。
例如:
浅拷贝:
深拷贝:
//深拷贝
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(s._capacity*(sizeof(DataType)));
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp,s._a,s.size*(sizeof(DataType)));
_a = tmp;
_size = s.size;
_capacity = s._capacity;
}
4. 赋值运算符重载
前言:
先看一段代码:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year; //注意内置类型是公有的
int _month;
int _day;
};
bool DateEqual(const Date& x,const Date& y)
{
return x._year == y._year
&& x._month == y._month
&& x._day == y._day;
}
bool DateLess(const Date& x,const Date& y)
{
if (x._year < y._year)
{
return true;
}
else if (x._year == y._year)
{
if (x._month < y._month)
{
return true;
}
else if (x._month == y._month)
{
//if (x._day < y._day)
//{
// return true;
//}
return x._day < y._day;
}
}
return false;
}
int main()
{
Date d1(1970,2,1);
Date d2(1970,2,8);
DateEqual(d1,d2); //比较日期是否相等
DateLess(d1,d2); //判断d1对象的日期是否小
cout << DateEqual(d1, d2) << endl << DateLess(d1, d2) << endl;
return 0;
}
上述代码,我们发现,对于自定义类型的比较,我们需要函数来实现,但是实现这些函数存在一些问题,给函数取名的问题,会造成代码的可读性变差。
C++为了增加代码的可读性,增加了运算符重载,运算符重载是具有特殊函数名的函数。有和普通函数类似的返回值类型,函数名字,参数列表
// 返回值类型 operator操作符(参数列表...){}
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year; //注意内置类型是公有的
int _month;
int _day;
};
bool operator==(const Date& x, const Date& y)
{
return x._year == y._year
&& x._month == y._month
&& x._day == y._day;
}
bool operator<(const Date& x, const Date& y)
{
if (x._year < y._year)
{
return true;
}
else if (x._year == y._year)
{
if (x._month < y._month)
{
return true;
}
else if (x._month == y._month)
{
return x._day < y._day;
}
}
return false;
}
int main()
{
Date d1(1970, 2, 1);
Date d2(1970, 2, 8);
//运算符重载 返回值类型 operator操作符(参数列表...){}
//参数顺序最好保持一致
cout << operator==(d1,d2) << endl; //cout << ( d1 == d2 )<< endl;
cout << operator<(d1, d2) << endl; //cout << ( d1 < d2 ) << endl;
cout << ( d1 == d2 )<< endl; //注意要加上括号,因为<<优先级比较高
cout << ( d1 < d2 ) << endl;
//发现上述自定义类型也是可以使用运算符的
return 0;
}
注意:上述的内置类型变成了公有,因为函数实现在类的外面
当然运算符重载函数可以放到类里面
看下方代码
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& y)
{
return _year == y._year
&& _month == y._month
&& _day == y._day;
}
bool operator<(const Date& y)
{
if (_year < y._year)
{
return true;
}
else if (_year == y._year)
{
if (_month < y._month)
{
return true;
}
else if (_month == y._month)
{
return _day < y._day;
}
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1970,1,1);
Date d2(2000,1,1);
cout << d1.operator==(d2) << endl;
cout << d1.operator<(d2) << endl;
cout << (d1 == d2) << endl; //cout << d1.operator==(d2) << endl;
cout << (d1 < d2) << endl; //cout << d1.operator<(d2) << endl;
return 0;
}
注意:
- 重载操作符必须有一个类 类型参数
- 作为类成员函数重载时,形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: .
以上五个运算符不能重载
上述的 .* 操作符的一个使用介绍:
class ob
{
public:
void func()
{
cout << "void func()" << endl;
}
};
//我们平时 typedef void(*)() pobfunc;
//函数指针和数组指针需要特殊定义
typedef void (ob::*pobfunc)(); //函数指针(成员函数指针)
int main()
{
pobfunc p = &ob::func; //注意这里的取地址&
ob temp;
//对于普通的函数指针我们可以(*p)();去调用函数
(temp.*p)(); //这里使用 .*是去调用成员函数 (目的是把this传过去)
return 0;
}
赋值运算符重载
d1 = d2; //已经存在的对象,一个拷贝赋值给另一个,重载运算符
① 赋值运算符重载格式
Ⅰ 参数类型: const T&
对于内置类型的连续赋值每次赋值都会有返回值
Ⅱ 赋值运算符重载有返回值类型: T& ,有返回值类型的目的是为了支持连续赋值
//...
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this; //注意这里返回的是*this,不是this(因为this是指针)
}
如果上述使用 operator=返回Date,编译器可能需要创建一个临时对象来返回结果,这样可能会导致不必要的拷贝操作,尤其对于在大型对象的情况下,甚至有可能会影响到程序的运行。
所以使用引用Date&
返回,可以避免复制这些对象,避免不必要的拷贝,支持连续赋值操作
Ⅲ 检测是否自己个自己赋值:
//可能会出现自己给自己赋值的情况
//d1 = d1;
所以为了避免自己给自己赋值的情况:
Date& operator=(const Date& d)
{
if (this != &d) //防止自己给自己赋值,注意这里是取地址
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; //注意这里返回的是*this,不是this(因为this是指针)
}
Ⅳ 返回*this:要进行链式赋值
② 当没有显示实现赋值运算符重载时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注:内置类型 成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。与拷贝构造类似
③ 赋值运算符只能重载成类的成员函数,而 不能 重载 成 全局函数
因为:赋值运算符重载,当类中没有手动写的时候,编译器会生成一个默认的,此时类外手动生成的赋值重载全局函数,编译器调用的时候,不知道去调用哪一个。所以故赋值运算符重载只能是类的成员函数。
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
日期类里面;
这里报错的原因是 const 的权限被放大了
如图:
改正:权限平移,权限缩小是允许的,权限放大是不允许的
下图是允许的情况
const int i = 0;
int j = i; //注意这里是拷贝,j的改变不影响i
int& r = i; //error 权限放大
const int* p1 = &i;
int* p2 = p1; //error 权限放大
// 注意: 指针和引用存在权限放大
小总结:
成员函数,如果是一个对成员变量只读访问的 函数 -> 建议加上const 这样const 对象和非const对象都可以使用
成员函数,如果是一个对成员变量 读写访问的 函数 -> 不能加const,否则不能修改成员变量
非const对象可以调用const成员函数 (权限的缩小)
const对象不可以调用非const成员函数(权限的放大)
临时变量具有常性:类型传换和返回值 都会产生临时变量,所产生的临时变量也有常性,
链式的时候也有临时变量(表达式返回值i+10) ,传值返回,函数调用,类型转换
重载操作符,复用的时候 注意:
例如:
建议:写+的时候,去复用+=
5. 取地址重载
这个默认成员函数一般不用手动实现,由编译器默认生成
手动生成:
//取地址重载
//一般情况不需要我们手动实现,由编译器默认生成
class A
{
public:
A* operator&()
{
return this;//返回隐藏this指针
}
};
int main()
{
A a1;
cout << &a1 << endl;
}
特殊情况:不想要别人拿到我的地址
class A
{
public:
A* operator&()
{
//return this;//返回隐藏this指针
return nullptr; //注意这里返回空指针
}
const A* operator&() const
{
return this;
}
};
int main()
{
A a1;
const A a2;
cout << &a1 << endl;
cout << &a2 << endl;
}
上述的普通地址就获取不到了
6. const取地址重载
这个默认成员函数一般不用手动实现,由编译器默认生成
那么 手动生成:
//取地址重载
//一般情况不需要我们手动实现,由编译器默认生成
class A
{
public:
const A* operator&() const //第二个const修饰的是this
{
return this;
}
};
int main()
{
const A a2;
cout << &a2 << endl;
}
特殊情况:
让const取地址重载返回假的地址
class A
{
public:
A* operator&()
{
return this;//返回隐藏this指针
//return nullptr;
}
const A* operator&() const
{
return (const A*)(0x0012ff40);
}
};
int main()
{
A a1;
const A a2;
cout << &a1 << endl;
cout << &a2 << endl;
}
const A* operator&() const
{
int a = 1;
return (const A*)&a;
//return (const A*)(0x0012ff40);
}