C++面向对象及其特性
一.面向对象的特点
1.特点
面向对象的思想为:一切皆对象。在解决实际问题时将问题高度抽象,通过面向对象特性解决问题。其主要特点为:封装,继承,多态。在C++中我们主要使用类这个新结构来实现面向对象。接下来的章节主要探讨类的基本格式,六大构造函数及其行为,继承与多态等多方面来说明这个主题。
2.类及其定义格式
接下来用一个简单的栈为例(栈中的数据类型为int,在此仅介绍基础概念,详细版本后面给出)
#include<iostream>
using namespace std;
class Stack {private:size_t _capacity;int*array;size_t _top;public:void Init(int n=4) {array = new int[n];_capacity = n;_top = 0;}void Push(int x) {if (_top == _capacity) {cout << "Stack Overflow" << endl;int* tmp=new int[_capacity*2];delete[] array;array = tmp;}array[_top++] = x;}void Pop() {if (_top == 0) {cout << "Stack Underflow" << endl;return;}_top--;}int Top() {if (_top == 0) {cout << "Stack Underflow" << endl;return -1;}return array[_top - 1];}void Destroy() {delete[] array;array = nullptr;_top = 0;_capacity = 0;}
};
日期类:
class Date
{ public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;private:
// 为了区分成员变量,⼀般习惯上成员变量
// 会加⼀个特殊标识,如_ 或者 m开头
int _year; // year_ m_year
int _month;
int _day;
}
由此我们不难看出,类的声明标识符为class,如以上的日期类,Date即其类名。public和private为访问限定符。
3.访问限定符
访问限定符为C++⼀种实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,通过访问权限选择性的将其接⼝提供给外部的⽤⼾使⽤。
• public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
• 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 }即类结束。
• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
• ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public
3.类域
作为一个封装的类型,类也有其独特的域。在类外定义其方法需要用::访问。类域同时还会影响编译器在编译时的查找逻辑,例如下面的Init方法,若不指定类域会直接到全局中寻找。
#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;
}
4.实例化
将一个类定义之后,并不会分配相应的内存地址空间,只有当其实例化时才会分配。打一个比方,类就像是房屋的设计图,而由类实例化出的对象就是建造出来的房屋。
以上述的Date类做例子
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:// 为了区分成员变量,⼀般习惯上成员变量// 会加⼀个特殊标识,如_ 或者 m开头int _year; // year_ m_yearint _month;int _day;};int main()
{Date date;date.Init(2025, 8, 10);
}
5.对象大小与内存对齐
我们第一次提到内存对齐这个概念是在计算struct结构体大小,而在C++的面向对象编程中,struct成为了一种特殊的类(默认成员变量和成员函数为public),在这里类的对象大小也采用内存对齐规则。
先不谈内存对齐,在计算对象大小时,成员函数并不计入其大小,原因如下:
1.主观的考虑,若对于同一个类实例出多个对象,每个对象都需要额外空间存储功能相同的成员函数,这会造成较大的内存浪费
2.客观来讲,函数被编译之后是一段指令,它被存储在代码段中,对象是无法存储函数的。调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在运⾏时找,就需要存储函数地址
3.在实际过程中,每一个对象都含有其指向对象本身的指针this*,例如当执行 x=5,其实为this->x=5,在调用成员函数时也是如此,成员函数的第一个参数默认为非显示的this*,不同的对象调用成员函数根据每个对象this指针的不同便可调用自己的成员函数,拿上面的Stack类做例子:
由此可见s,s2分别调用了Init函数和Push函数,最后都调用了Print函数,打印出的是各自独立的结果
接下来再谈内存对齐,这里的方法和计算结构体的大小如出一辙:
第⼀个成员在与结构体偏移量为0的地址处。
• 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
• 注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
• VS中默认的对⻬数为8
• 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
• 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍
拿下面的代码做例子:
#include<iostream>
using namespace std;
// 计算⼀下A/B/C实例化的对象大小
class A
{public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{
public:
void Print()
{
//...
}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}
结果如下
另外需要注意的是,若出现上述B类和C类这样没有成员变量或者仅有变量名的类,大小并非为0,为了表示其“存在”,需要1B进行占位
6.this指针
上面在说明为什么成员函数不计入对象总大小时提到了这个概念。拿上面写过的Date类做例子
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;
• C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显⽰使⽤this指针。
#include<iostream>
using namespace std;
class Date
{public:
// void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
// 编译报错:error C2106: “=”: 左操作数必须为左值
// this = nullptr;
// this->_year = year;
_year = year;
this->_month = month;
this->_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(&d1, 2025, 3, 31);
d1.Init(2025, 3, 31);
d1.Print();
d2.Init(2025, 7, 5);
d2.Print();
return 0;
}
另外有个比较有趣的问题:以下代码的运行结果为?
#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;
}
答案是正确输出A::Print(),即使p是一个空指针,但p->Print()的行为类似于A::Print(p),且Print函数内部并没有出现访问成员变量的情况,因此不算做空指针引用。如果Print函数中cout的参数再加入_a程序会发生崩溃(空指针引用),因为其本质为this->_a
二.类的默认成员函数
1.默认成员函数是什么
默认成员函数即:对于一个类,用户没有显示实现便会由编译器自动生成的函数称之为默认成员函数。其主要有6个,除了取地址重载之外均需要重点学习
2.构造函数
主要特点
构造函数并非其字面量的意思,它并非是用来“构造”一个对象的(开辟空间),而是用来“初始化”一个对象的。它的主要作用是为了替代我们上述Date类和Stack类中的Init()的函数,其主要优势就是可以在创建对象时自动调用,而非用户手动调用。主要特点如下
1. 函数名与类名相同。
2. ⽆返回值。
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,不传实参就可以调⽤的构造就叫默认构造。
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
由上面这个用两个栈实现一个队列的例子可以看出,当创建MyQueue类时,默认调用了其自定义类型成员Stack的默认构造函数。
构造函数的行为
上面提到,构造函数可以重载,因此可以有默认无参构造函数,带参构造函数和全缺省构造函数
注意下面的写法,实际中无参构造和全缺省构造函数无法同时出现,会出现歧义,我们以Date类为例观察构造函数在对象创建过程中的行为
class Date
{
public://默认无参Date() {cout << "无参构造函数" << endl;}//带参Date(int y, int m, int d){_year = y;_month = m;_day = d;cout << "有参构造函数" << endl;}//全缺省Date(int year=2025,int month=7,int day=26){_year = year;_month = month;_day = day;cout << "全构造函数" << endl;}
private:// 为了区分成员变量,⼀般习惯上成员变量// 会加⼀个特殊标识,如_ 或者 m开头int _year; // year_ m_yearint _month;int _day;};int main()
{Date d1;Date d2(2025,7,26);return 0;
}
由此可以发现构造函数确实会在创建对象时自动调用,用于初始化对象。
3.析构函数
主要特点
析构函数与构造函数类似,其功能也并非其字面量删除对象,而是用于对象销毁时对其申请的资源的释放(例如malloc等申请的空间),实际上替代了上述原始版本的Stack和Date类中的Destroy函数的功能
1. 析构函数名是在类名前加上字符 ~。
2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
析构函数的行为
析构函数会在函数栈帧销毁时将对象申请的空间进行释放
对于成员变量中存在自定义成员(如下面的MyQueue由两个stack实现),构造时调用stack的构造,析构时也会调用stack的析构。值得注意的是,这里即使显式写出MyQueue的析构函数,stack的析构函数仍然会被调用,原因如下:
遵循RAII原则:
让成员对象自己管理资源
容器类(如
myqueue
)只需确保自身析构函数被正确调用
三法则:
如果需要自定义析构函数,通常也需要自定义拷贝构造函数和拷贝赋值操作符
但在本例中,使用默认行为即可
另外,如果MyQueue本身定义了需要释放空间的成员(如int*)则需要在MyQueue的析构函数中释放该成员申请的空间,若MyQueue的析构函数释放了stack申请的空间会发生错误(多重释放)。因此只需要让成员对象自己管理资源即可
#include<iostream>
using namespace std;typedef int stdatatype;
class stack
{
public:stack(int n = 4){_a = (stdatatype*)malloc(sizeof(stdatatype) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// ...~stack(){free(_a);_a = nullptr;_top = _capacity = 0;}
private:stdatatype* _a;size_t _capacity;size_t _top;
};// 两个stack实现队列
class myqueue
{
public:// 编译器默认生成myqueue的构造函数调用了stack的构造,完成了两个成员的初始化// 编译器默认生成myqueue的析构函数调用了stack的析构,释放的stack内部的资源// 显示写析构,也会自动调用stack的析构~myqueue(){cout << "~myqueue()" << endl;}
private:stack pushst;stack popst;//int size;
};int main()
{myqueue mq;//stack st1;//stack st2;return 0;
}
4.拷贝构造函数
主要特点
用Date类举例,拷贝构造就是用已有的Date对象d1去构造一个新的Date对象d2的构造方式。
1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以传多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就
需要显⽰写拷⻉构造,否则就不需要。
6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回
拷贝构造函数的行为
用Date类做例子:这里的拷贝构造是浅拷贝的写法,因为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;}// error C2652: “Date”: 非法的复制构造函数: 第一个参数不应是“Date”// Date d2(d1)Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;
}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
用Stack做例子:由于Stack中申请了资源,拷贝构造需要做深拷贝,否则会在析构时对同一块资源析构两次造成程序崩溃
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a) {perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// st2(st1)Stack(const Stack& st){cout << "Stack(const Stack& st)" << endl;//需要对_a指向资源创建同样大的资源再拷贝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};//int main()
//{
// Stack st1;
// st1.Push(1);
// st1.Push(2);
//
// // Stack不显示实现拷贝构造,用自动生成的拷贝构造完成浅拷贝
// // 会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃
// Stack st2(st1);
//
// return 0;
//}
对于含有自定义类型成员的类,例如上面实现的MyQueue,它的拷贝构造同样会优先调用Stack的
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
Stack st2 = st1;
MyQueue mq1;
// MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst
// 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,就没问题
MyQueue mq2 = mq1;
return 0;
}
5.运算符重载
主要特点
• 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
• 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
• 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
• 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
• 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
• .* :: sizeof ?: . 注意以上5个运算符不能重载。
• 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int
operator+(int x, int y)
• ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意
义,但是重载operator+就没有意义。
• 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。
C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
• 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位
置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。
重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对
象。
运算符重载实例
为了更加深入理解运算符重载的功能,这里用实现一个Date类的判断相等,日期相减,判断大小等做例子
class Date
{
public://默认无参//全缺省Date(int year=2025,int month=7,int day=26){_year = year;_month = month;_day = day;cout << "全缺省构造函数" << endl;if (!checkDate()) {cout << "日期不合法" << endl;}}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;cout << "拷贝构造函数" << endl;}~Date(){cout << "析构函数" << endl;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}//为了得到某年某月的天数int GetMonthDay(int year, int month) {assert(month > 0 && month < 13);static int monthDay[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};//首先判断传入的月份是否为2月,如果是二月再判断是否为闰年if (month==2&&year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {return 29;}else {return monthDay[month];}}bool checkDate() { if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month)){return false;} else{return true;}}//重载==bool operator==(const Date& d) {if (_year == d._year && _month == d._month && _day == d._day) {cout << "相等" << endl;return true;}else {cout << "不相等" << endl;return false;}}//重载<bool operator<(const Date& d) const{if (_year < d._year) {return true;}else if (_year == d._year) {if (_month < d._month) {return true;}else if (_month == d._month) {if (_day < d._day) {return true;}}}return false;}//重载<=bool operator<=(const Date& d) {return *this < d || *this == d;}//重载>bool operator>(const Date& d) {return !(*this <= d);}//重载>=bool operator>=(const Date& d) {return !(*this < d);}//重载!=bool operator!=(const Date& d) {return !(*this == d);}//重载+=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){++_year;_month = 1;}} return* this;}//重载+Date& operator+(int day)const{ Date tmp = *this;tmp += day;return tmp;}//重载-=Date& operator-=(int day){if (day < 0) {return *this += -day;}_day -= day;while (_day <= 0){--_month;if (_month == 0){--_year;_month = 12;}_day += GetMonthDay(_year, _month);}return* this;}//重载-Date operator-(int day)const{Date temp = *this;temp -= day;return temp;}Date& operator++(){ *this += 1;return* this;}//注意这里是后置++Date operator++(int){Date temp = *this;*this+=1;return temp;}Date& operator--(){*this -= 1;return* this;}Date operator--(int){Date temp = *this;*this-=1;return temp;}int operator-(const Date& d) const{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;}
private:// 为了区分成员变量,⼀般习惯上成员变量// 会加⼀个特殊标识,如_ 或者 m开头int _year; // year_ m_yearint _month;int _day;};
const成员函数
值得注意的是上面有一些函数形如int operator-(const Date& d) const{...}称为const成员函数,其特点有:
在Date类中const对象和const成员函数的主要用途
class Date {
public:// const 成员函数(只读操作)void Print() const { // 不修改状态cout << _year << "年" << _month << "月" << _day << "日" << endl;}bool operator<(const Date& d) const { // 比较不修改状态return (_year != d._year) ? _year < d._year: (_month != d._month) ? _month < d._month: _day < d._day;}// 非 const 成员函数(修改状态)Date& operator+=(int day) { // ... 修改 _day, _month, _yearreturn *this;}
};// 使用场景
const Date safe(2023, 10, 1); // const 对象
holiday.Print(); // 需要 Print() 是 const
// safe += 1; // 禁止调用非 const 函数
Date unsafe(2026, 8, 15); // 非 const 对象
unsafe.Print(); // 可调用 const 版本
unsafe += 7; // 可调用非 const 版本
• 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后
⾯。
• const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。
const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
#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(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}