前言
类与对象是C++语言学习的基础,涉及的知识点较多, 本篇文章将对该部分的知识点进行一个较为详细的总结,主要是为了帮助我自己的学习,希望也可以帮助到大家,准备的时间较长,篇幅较长,如有什么问题,欢迎各位大佬在评论区,或者私信纠正,谢谢啦~
目录
[1.1 类定义的格式](#1.1 类定义的格式)
[1.2 访问限定符](#1.2 访问限定符)
[1.3 类域](#1.3 类域)
[2.1 实例化概念](#2.1 实例化概念)
[2.2 对象大小](#2.2 对象大小)
[3.1 概述](#3.1 概述)
[3.2 练习题](#3.2 练习题)
[4.1 构造函数](#4.1 构造函数)
[4.2 析构函数](#4.2 析构函数)
[4.3 拷贝构造函数](#4.3 拷贝构造函数)
[4.4 赋值运算符重载](#4.4 赋值运算符重载)
[4.4.1 运算符重载](#4.4.1 运算符重载)
[4.4.2 赋值运算符重载](#4.4.2 赋值运算符重载)
[4.4.3 日期类实现](#4.4.3 日期类实现)
[4.5 取地址运算符重载](#4.5 取地址运算符重载)
[4.5.1 const成员函数](#4.5.1 const成员函数)
[4.5.2 取地址运算符重载](#4.5.2 取地址运算符重载)
1.类的定义
1.1 类定义的格式
引出------> 类的一个示例:
cpp
class Stack
{
};
在上述代码中,class就是定义类的关键字,Stack就是类的名字,{}里面的内容为类的主体。
类中的变量为类的属性或者成员变量,类中的函数成为类的方法或者成员函数。
定义在类面的成员函数默认为inline「1」。
「1」inline是 C++ 的关键字,用于建议编译器对函数进行内联展开(即在函数调用处直接插入函数体代码,而非通过函数调用栈机制执行),以减少函数调用的开销(如压栈、跳转等),提高程序运行效率。
1.2 访问限定符
C++一种实现封装的方式,用类将对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性将其接口提供给外部的用户使用。
三种访问限定符:
1️⃣public:可以被外界访问
2️⃣protected/private:修饰的成员在类外不能被访问
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
class定义成员没有被访问限定符修饰时,默认为private,struct默认为public。
封装的本质是为了更加规范的管理。
此外,在C++中,兼容C语言的语法,则在C++中,struct也可以使用,但是在C++中更加倾向于使用class。
1.3 类域
cpp
void Stack::Init(int capacity)
{
_a = nullptr;
_top = 0;
_capacity = capacity;
}
上述代码表示:
在类外面定义成员时,需要使用**::**作用域操作符。
2.实例化
2.1 实例化概念
cpp
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;
};
在上述代码中:
private所定义的成员变量,只是声明,此时并没有开空间。
cpp
int main()
{
//此时才是定义,开空间
Stack s1;
return 0;
}
上述代码中定义的s1才为变量开了空间,因此------
用**类类型「2」**在物理内存中创建对象的过程,叫做类实例化出对象。(下面有图片比较形象地表现关系)
「2」在 C++ 中,类类型(class type) 是通过class、struct或union关键字定义的自定义数据类型,是面向对象编程的核心概念之一。它封装了数据(成员变量)和操作数据的行为(成员函数),形成一个逻辑上的整体,用于描述现实世界中的实体或抽象概念。

(该图片形象地表现了类与实例化对象的关系)
2.2 对象大小
首先要了解内存对齐规则「3」。
「3」内存对齐规则:
1、第一个成员在与结构体偏移量为0的地址处。
2、其他成员要对齐到对齐数的整数倍的地址处。
3、对齐数:编译器默认的一个对齐数与该成员大小的较小值。
4、VS中的默认对齐数为8。
5、结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
6、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(更多详细内容请参考其他优秀博主的文章,这里不再赘述)
C++类计算大小只考虑成员变量,不考虑成员函数。定义类的成员变量是不同的变量,而不同的类共用同一成员函数。
(其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址)
下面来举一个例子:
cpp
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
int main()
{
A aa1;
cout << sizeof(aa1) << endl;
return 0;
}
根据上述的规则,就可以得出大小为8。

cpp
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{
void Print()
{
}
};
class C
{
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
return 0;
}
在上述的代码中,运行之后,是下面的结果:

我们已经分析了A的大小,那么B和C的大小又如何解释?
上述B和C开一个字节,是为了占位,不存储实际数据,表示对象存在过。
3.this指针
3.1 概述
cpp
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(2025, 10, 20);
d1.Print();
d2.Init(2025, 11, 11);
d2.Print();
return 0;
}
由上述的知识点,我们已知,d1和d2共用成员函数,在运行中,编译器是怎样区别d1和d2的呢?
这就引出了隐含的this指针。
下面是隐含的this指针的示例:
cpp
// 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;
_month = month;
_day = day;
}
cpp
// d1.Init(&d1, 2025, 10, 20);
d1.Init(2025, 10, 20);
d1.Print();
编译器中,默认多了一个参数,在成员函数的变量中,第一个参数就是this指针,在引用中,也默认第一个参数就是this指针。
隐含的this指针,成员函数的参数中不允许显式表现出,但是在成员变量中是允许使用this指针来表示的(有些场景会用到this指针)。
在this指针中还有一个const
cpp
void Init(Date* const this, int year, int month, int day)
const用于保证this指针对象不能修改。
3.2 练习题
cpp
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
上述代码程序的运行结果是:正常运行
cpp
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
上述代码程序的运行结果是:运行崩溃
在上述的定义的类中,_a前面又this指针,引用的this指针是空指针,就会出现错误。
this指针是在内存的栈里面。
4.类的默认成员函数

编译器会默认生成以上6个默认成员函数。(编译器自动生成)
4.1 构造函数
构造的主要任务是:对象实例化时初始化对象。
构造函数是特殊的成员函数。
构造函数的特点:
1、函数名与类名相同。
2、无返回值。
3、对象实例化时系统会自动调用对应的构造函数。
4、构造函数可以重载。
cpp
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
这两个就是构造函数,是同一个构造函数,因为构造函数是可以重载的。
cpp
class Date
{
public:
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
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()
{
//这里是函数声明,在d1的后面不用加()
Date d1;
Date d2(2025, 10, 20);
d1.Print(); // 输出:1/1/1
d2.Print(); // 输出:2025/10/20
return 0;
}

上述代码自动调用对应的构造函数。
这里的构造函数实际上是代替了上述的Init函数,不使用Init是因为使用构造函数,不用引用就可以自动调用,不会出现对象没有初始化的现象。
5、如果类中没有显式写出构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,那么,编译器就不会生成。
6、无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。
(总结不用传参的函数就叫默认构造函数,同时,在代码展现中,三个函数只能存在一个)
(编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决)
7、总结:绝大部分情况,构造需要自己写。
无参构造函数示例:
cpp
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
全缺省构造函数示例:
cpp
//全缺省函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
4.2 析构函数
析构函数主要任务:完成对象中资源的清理释放工作。
类似于Stack中的Destroy函数。
析构函数的特点:
1、析构函数是在类名前加上字符 ~ 。
2、无参数无返回值。
3、一个类只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4、对象生命周期结束时,系统会自动调用析构函数。
5、对内置类型不作处理,自定义类型会自动调用它的析构。且自定义类型无论什么时候调用的都是默认的析构。
7、有资源申请的时候一定要写析构,否则就会造成资源泄露。
8、C++规定,后定义的先析构。
注意⚠️:后定义的先析构。
cpp
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
这里的MyQuene是自定义类型,编译器会自动调用它所对应的析构。
对比之前C语言的用法,C++的构造函数和析构函数更加方便,不会造成了初始化和销毁忘记的情况。
4.3 拷贝构造函数
拷贝构造任务:使用同类对象初始化创建对象
拷贝构造的特点:
1、拷贝构造函数是构造函数的一个重载。
2、拷贝构造的第一个参数必须是当前类类型对象的引用。
3、C++规定自定义类型对象进行拷贝行为必须调用拷贝构造。
cpp
class Date
{
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()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 10, 21);
//拷贝构造
Date d2(d1);
return 0;
}
上述的代码显示的就是一个简单的拷贝构造。
自定义类型传值传参和传值返回都会调用拷贝构造。
cpp
//自定义类型传值传参,要调用拷贝构造
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
int main()
{
Date d1(2025, 10, 21);
//拷贝构造
Date d2(d1);
Func1(d1);
return 0;
}
上述代码中,引用Func1就调用了一个拷贝构造,拷贝构造过程在调试中可以显示,这里不赘述。
当拷贝构造本身使用传值传参,就会出现以下显示的无穷递归的情况。

因此,要使用引用传参。(也可以使用指针,但通常使用引用)
cpp
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
因为期望不改变,所以最好加上const,来进行限制。
cpp
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
「4」类似于栈,浅拷贝/值拷贝时,会出现以下问题
1、一个对象修改,会影响另一个对象。
2、析构时,空间释放两次空间(同一空间释放两次)。
「5」深拷贝
不仅仅对成员拷贝,还要对指向资源空间数据进行处理。
cpp
Stack(const Stack& s)
{
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
if (_a == NULL)
{
perror("realloc fail");
return;
}
_capacity = s._capacity;
_top = s._top;
}
以上代码,就是一个深拷贝的例子(开辟了一个新的空间)。
4、未显式定义拷贝构造,编译器会自动生成拷贝构造函数(例如日期类)。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
5、如果一个类显式实现了析构并释放资源,那么他就需要显式写拷贝构造,否则不需要。
cpp
Stack& func3()
{
Stack st;
return st;
}
int main()
{
Stack ret = func3();
return 0;
}
上述代码在运行中会出现错误,st是ret的别名,当构造的函数结束后,st就会析构,而st是ret的别名,则返回的值就会出现问题,因此,在这个地方需使用传值返回,正确的代码如下:
cpp
Stack func3()
{
Stack st;
return st;
}
int main()
{
Stack ret = func3();
return 0;
}
因此:
6、传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一
样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
cpp
//两种都是拷贝构造的写法
Stack st4(st3);
Stack st5 = st4;
上述两种都是拷贝构造的写法。
4.4 赋值运算符重载
4.4.1 运算符重载
1、C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
2、运算符重载具有特殊名字的函数:operator和后面要定义的运算符共同构成。
3、具有返回类型,参数列表和函数体。
cpp
bool operator==(Date x1, Date x2)
{
return true;
}
int main()
{
Date d1(2025, 10, 1);
Date d2(2025, 10, 21);
d1 == d2;
//operator == (d1, d2);
return 0;
}
上述代码是一个简单的例子。
为了使得代码更加准确,进行了以下的修改------
cpp
bool operator==(const Date& x1, const Date& x2)
{
return true;
}
int main()
{
Date d1(2025, 10, 1);
Date d2(2025, 10, 21);
d1 == d2;
//operator == (d1, d2);
return 0;
}
4、重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。
(如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个)
因此,operator放入类中时,由于默认第一个参数为this指针,在类中应该这样写------
cpp
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
5、
cpp.* :: sizeof ?: .上面的五个运算符不能重载。
「6」 .* 运算符(详情请看下面的代码示例)
cpp
void func()
{
cout << "void func()" << endl;
}
class A
{
public:
void func2()
{
cout << "A::func()" << endl;
}
};
int main()
{
//普通函数指针
void(*pf1)() = func;
(*pf1)();
//A类型成员函数的指针
void(A:: * pf2)() = &A::func2;
A aa;
(aa.*pf2)();
return 0;
}
6、重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。
7、重载运算符定义后需有意义。
4.4.2 赋值运算符重载
赋值运算符重载:用于完成两个已经存在 的对象的拷贝辅助。
与拷贝构造不同,以下是他们的差异------
cpp
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(2025, 10, 1);
Date d2(d1);
Date d4 = d1;
//赋值运算符重载
Date d3(2025, 10, 21);
d1 = d3;
return 0;
}
特点:
1、规定必须重载为成员函数。建议写成const当前类类型引用(减少拷贝)。
2、有返回值,建议写成当前类类型的引用。
cpp
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)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//拷贝构造
Date d1(2025, 10, 1);
Date d2(d1);
Date d4 = d1;
//赋值运算符重载
Date d3(2025, 10, 21);
d1 = d3;
return 0;
}
3、没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
例如:对于日期类,不写运算符重载,也可以运行,但是像Stack类就需要自己写。
4.4.3 日期类实现
首先,给出头文件中的内容,来实现不同的函数。
cpp
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>
class Date
{
// 友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print() const;
// 直接定义类里面,他默认是inline
// 频繁调用
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };
// 365天 5h +
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year
% 400 == 0))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
bool CheckDate();
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
// d1 += 天数
Date& operator+=(int day);
Date operator+(int day);
// d1 -= 天数
Date& operator-=(int day);
Date operator-(int day);
// d1 - d2
int operator-(const Date& d);
// ++d1 -> d1.operator++()
Date& operator++();
// d1++ -> d1.operator++(0)
// 为了区分,构成重载,给后置++,强行增加了一个int形参
// 这里不需要写形参名,因为接收值是多少不重要,也不需要用
// 这个参数仅仅是为了跟前置++构成重载区分
Date operator++(int);
Date& operator--();
Date operator--(int);
// 流插入
// 不建议,因为Date* this占据了一个参数位置,使用d<<cout不符合习惯
//void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
// 重载
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
先实现一个日期加一个天数------
第一步,应该先获得某年某月的天数
cpp
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };
// 365天 5h +
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year
% 400 == 0))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
为了使得代码运行更加快速,减少拷贝,先实现"+=",再实现"+"。
cpp
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)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
前置++,后置++,他们的返回值是不一样的------
为了区分,构成重载,给后置++,强行增加了一个int形参。
cpp
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
实现"-="和"-"------
跟上述的+类似,代码如下:
cpp
Date& 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 Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
日期比较大小:
cpp
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
// d1 <= d2
bool Date::operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
日期减日期:
思路一:先分别算他们与本年的1月1号相差的天数,可以得到一个x和y,再算出来两个日期相差的年份。
思路二:用小的不断++,直到加到和大的日期相等。
这里给出思路二的代码展示:
cpp
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
min = *this;
max = d;
flag = -1;
}
int day = 0;
while (min != max)
{
min++;
++day;
}
return day * flag;
}
流插入和流提取。
重载运算符的参数个数和改运算符作用对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧对象传给第二个参数。
cpp
void operator<<(ostream& out)
{
cout << _year << "/" << _month << "/" << _day << "\n";
}
此代码运行之后,我们会发现,代码运行不通过。
在上述代码运行的时候,d1传给了this,d2传给了out,调用的时候是如下的代码显示------
cpp
d1.operator << (cout);
d1 << cout;
上述的代码运行时是正确的,但是可读性比较差,因此,需要对代码进行改进。
将重载放在全局
cpp
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << "\n";
}
会发现无法访问私有,则有多种解决的方法。
第一种,可以用友元函数「7」。
cpp
friend ostream& operator<<(ostream& out, const Date& d);
「7」友元函数在下面会进行比较详细的介绍,这里先使用。
cpp
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << "\n";
}
istream& operator>>(istream& in, Date& d)
{
cout << "请以此输入年月日:>";
in >> d._year >> d._month >> d._day;
return in;
}
当存在非法日期时,当前的函数不能准确检测到,因此我们可以写一个函数进行检查。
cpp
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << "\n";
}
istream& operator>>(istream& in, Date& d)
{
while(1)
{
cout << "请以此输入年月日:>";
in >> d._year >> d._month >> d._day;
if (d.CheckDate())
{
break;
}
else
{
cout << "输入日期非法,请重新输入" << endl;
}
}
return in;
}
完整的代码为:
cpp
#include"Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "非法日期" << *this << endl;
}
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
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)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
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)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
// d1 <= d2
bool Date::operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
min = *this;
max = d;
flag = -1;
}
int day = 0;
while (min != max)
{
min++;
++day;
}
return day * flag;
}
// 重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << "\n";
}
istream& operator>>(istream& in, Date& d)
{
while(1)
{
cout << "请以此输入年月日:>";
in >> d._year >> d._month >> d._day;
if (d.CheckDate())
{
break;
}
else
{
cout << "输入日期非法,请重新输入" << endl;
}
}
return in;
}
4.5 取地址运算符重载
4.5.1 const成员函数
cpp
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()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void test1()
{
const Date d1(2025, 9, 8);
d1.Print();
}
上述显示的代码存在问题

在这上面涉及到了权限的放大。
则应该将this指针变为const this------
cpp
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
上述的代码修饰的是this指针本身,即
cpp
//void Print(const Date* cosnt this) const
普通对象也可以调用,例如:
cpp
Date d2(2025, 9, 8);
d2.Print();
权限不可以放大,但是可以缩小。
1、const修饰的成员函数叫做const成员函数,const修饰成员函数放到成员函数参数列表的后面。
2、const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
虽然const函数很好用,但不能在任何函数后都加上const。
总:不修改成员变量的均可以加上const,其他的不能加上const。
(这样普通对象和const对象都可以调用这个const成员函数。)
4.5.2 取地址运算符重载
cpp
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
这就构成了取地址运算符的重载,在使用的时候使得引用更加准确。
cpp
Date* p1 = &d2;
const Date* p1 = &d1;
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
5.再探构造函数
1、在4.1的基础上,构造函数还有一种方式,就是初始化列表。
使用方式是以一个逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或者表达式。
cpp
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
, _t(12)
, _ref(x)
, _n(1)
上述的不完整代码就是一个比较简单的例子。
是C++规定的,不能将其形式进行改造。
2、每个成员变量在初始化列表中只能出现一次,语法上初始化列表可以认为是每个成员变量定义初始化的地方。
3、引用成员变量,const成员变量,没有默认构造[8]的类类型变量,必须放在初始化列表的位置进行初始化。
8\]默认构造:全缺省,无参,系统默认生成的(在上述的知识中有详细的讲解)。
cpp
Time _t; // 没有默认构造
int& _ref; // 引用
const int _n; // const
这三种类型必须在初始化列表中进行初始化。
共同特点:必须在定义的时候初始化。
cpp
private:
//声明
int _year;
int _month;
int _day;
Time _t; // 没有默认构造
int& _ref; // 引用
const int _n; // const
在这里仅仅是成员变量的声明,在main函数中,是对象的整体定义,因此,要对成员变量进行初始化。
建议:尽可能在初始化列表中进行初始化。
4、C++11支持在成员变量声明的位置给缺省值[9],这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
cpp
class Date
{
public:
private:
//声明,缺省值
int _year = 1;
int _month = 1;
int _day = 1;
};
9\]缺省值:有定义的数,就用定义的数,没有定义的数,用给的缺省值。 5、**尽量使用初始化列表**进行初始化,不在初始化列表进行初始化的成员也会走初始化列表。

在很多情况下,各种类型是可以混合着使用的。
cpp
class Date
{
public:
private:
int _size = 0;
Stack _st1;
Stack _st2;
};
在多数情况下,初始化列表并不能满足所有的条件,还需要进行一定的补充。
一般情况下,建议使用初始化列表进行初始化。
如果没有初始化列表初始化的值,尽量给出缺省值。
一个小练习------
cpp
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}
上述程序的运行结果是什么()
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1
6、初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表中出现的先后顺序无关。
(建议声明顺序和初始化列表顺序保持一致)

先给了一个1,a1不会先初始化,先会用a1初始化a2,又因为a1是随机值,此时的a1初始化a2,a2是随机值,因此该题选择D。
6.类型转换
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
cpp
int main()
{
int i = 1;
double d = i;
const double& ret = i;
return 0;
}
上述是两个比较简单的类型转换。
(两个类型之间有一定的关联,才能进行类型转换,例如整型和浮点型之间的转换)
cpp
class A
{
public:
// 构造函数explicit就不再支持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//构造
A a1(1);
//隐式类型转换
A a2 = 1;
return 0;
}
上述可以进行隐式类型的转换是因为有相应的构造函数。
在隐式类型转换中会出现构造一个中间变量,中间变量拷贝构造给a2。
cpp
int main()
{
//构造
A a1(1);
//2为参数,构造临时对象,临时对象拷贝构造a2
//编译器会优化为直接构造
A a2 = 2;
return 0;
}
构造函数前面加explicit就不再支持隐式类型转换。
cpp
A a4 = {1, 1};
上述是一个多参数的方法。
类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
cpp
class A
{
public:
// 构造函数explicit就不再支持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{}
private:
int _b = 0;
};
上述就是一个类类型之间的转换。
7.static成员
1、用static修饰的成员变量叫做静态成员变量。
2、静态成员变量为所有类对象锁共享,不属于某个具体的对象,不存在对象中,存放在静态区。
cpp
class A
{
public:
private:
int _a1 = 1;
int _a2 = 1;
static int _count;
};
int main()
{
A aa1;
cout << sizeof(aa1) << endl;
return 0;
}
运行的结果为:

则可得不包含静态成员变量。
3、用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
4、非静态成员变量,可以访问任意的静态成员变量和静态成员函数。
5、突破类域和访问限定,就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问惊天成员变量和静态成员函数。
cpp
cout << ptr->_count << endl;
cout << aa1._count << endl;
cout << A::_count << endl;
上面就是可以访问的几种方式。
6、静态成员也是类的成员,受到访问限定符的限制。
7、静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。(可以在类的外面进行初始化)
一个小知识:
①设已经有A,B,C,D4个类的定义,程序中他们构造函数调用顺序是什么?
②设已经有A,B,C,D4个类的定义,程序中他们析构函数调用顺序是什么?
cpp
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
①全局变量先进行构造,因此C先进行构造,局部static对象,都是在第一次运行到该位置时,才进行初始化;最终的答案是:C A B D
②析构时,B肯定在A之前,C在main函数结束之后才析构,D在静态区,main函数结束之后,局部的静态D先析构;最终的答案是: B A D C
8.友元
友元提供了一种突破类访问限定符封装的方式,友元分为:友元类和友元函数。
形式:在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
例子:
cpp
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
一个函数可以是多个类的友元。
友元不是双向的,而是单向的。因此,也不具有传递性。
cpp
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
在上述的代码中,B能访问A,但是A不能访问B。
由于代码一般是从上到下运行的,因此,在友元中,一般用声明和定义分离或者先进行声明。
1、外部友元函数可以访问类的私有和保护函数,友元函数仅仅是一种声明,他不是类的成员函数。
2、友元函数可以在类定义的任何地方声明,不受类访问限定符的限制。
3、一个函数可以是多个类的友元函数。
4、友元函数也不宜多用,会增加耦合度,破坏封装。
(代码要求高内聚,低耦合,这样代码出错不会造成太大的影响)
9.内部类
一个类定义在另一个类的内部,这个内部类就叫做内部类。
cpp
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
int _b1;
};
};
上述就是一个内部类的例子,注意,A中不包含B,测试A的大小如下:
cpp
int main()
{
cout << sizeof(A) << endl;
return 0;
}

内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
内部类是外部类的友元类。(在上述的例子中,B就默认为A的友元类,即B可以访问A中的变量)
(内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类)
10.匿名对象
用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。
例子如下------
cpp
Solution s;//有名对象
//匿名对象的生命周期只在当前这一行
Solution();//匿名对象
匿名对象的生命周期只在当前这一行。
匿名对象可以用于缺省值中,例如------
cpp
void Func(const Solution& s = Solution(), int i = 0)
{}
在这里匿名对象就作为一个缺省值来使用。
此外,在上述的代码中,const引用延长了匿名对象的生命周期,例如:
cpp
const Solution& ref = Solution();
在这个时候,匿名对象的生命周期就延长到了ref 的生命周期结束的时候(即跟const的生命周期是一样的)。
11.对象拷贝时的编译器优化
现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
C++一些更新内容,开始对部分场景进行了规定(例如C++11、C++17等)。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些编译器还会进行跨行跨表达式的合并优化。
(构造+拷贝构造------>直接就优化成了构造)
这里给出一个例子:
假设已经定义了一个A类型的类
cpp
A f2
{
A aa;
return aa;
}
int main()
{
A aa1 = f2();
return 0;
}
通过图片来感受编译器的优化------

不同的编译器对于其处理的结果是不同的,部分编译器可以省略掉中间的临时对象来进行优化。
类与对象的知识点就先进行到这里,后面会继续更新后续内容。