继上篇我们初步认识了c++中的类和对象,这篇文章我们就来深入探讨一下c++在类于对象的基础上做出了什么改变。
这里也给大家放一下链接,以便回顾重温。
目录
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。但是事实如此吗?并不是,类有自己的成员。
默认成员函数就是用户没有显示实现 , 编译器会自动生成的成员函数称为默认成员函数 。 一个类 , 在不写的情况下 , 编译器会默认生成以下的 6 个 默认成员函数 , 需要注意的是这 6个中最重要的是前 4 个 , 最后两个取地址重载不重要 , 稍微了解就好 。 其次 C++11 以后还会增加两个默认成员函数 , 移动构造 和 移动赋值

1.构造函数定义及其特征
构造函数是特殊的成员函数 需要注意的是 构造函数虽然名称为 构造 , 但 是构造函数的主要任务并不是开辟空间,创造对象(我们常使用的局部对象是栈帧创建时 , 空间就开好了) , 而是对象实例化时 初始化对象 。
构造函数的本质是要替代我们以前 Stack 和 Date 类中写的 Init 函数的功能 , 构造函数自动调用的特点就完美的替代了 Init 。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。5.如果类中没有显示定义构造函数 , 则C++编译器会自动生成一个无参的默认构造函数 ,一旦用户显示定义 ----> 编译器将不再生成 。
6.无参构造函数 , 全缺省构造函数 , 在不写构造函数时编译器默认生成的构造函数都叫做默认构造函数 。 但是这三个函数有且只有一个存在 , 不能同时存在 。无参构造函数和全缺省构造函数虽然构成函数重载 , 但是调用时会存在歧义 。默认构造函数并不只有编译器默认生成的那个叫默认构造 , 实际上无参构造函数 , 全缺省构造函数也是默认构造函数 ,也就是说不传实参就可以调用的构造就叫默认构造 。
7.不写构造函数时 , 编译器默认生成的构造 , 对内置类型成员变量的初始化没有要求 , 也就是是否初始化是不确定,看编译器 。 对于自定义类型成员变量 ,要求调用这个成员变量的默认构造函数初始化 。如果这个成员变量 ,没有默认构造函数 ,那么就会报错 , 需要初始化这个成员变量 , 需要使用初始化列表才能解决 , 初始化列表是什么,我们后续在讲。
1.1构造函数的使用
假如我们这里来定义一个日期类,同时使用构造函数。
#include <iostream>
using namespace std;
class Data
{
public:
Data()
{ }
Data(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
};
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
};
int main()
{
Data d1;
Data d2(2023, 1, 1);
d1.Print();
d2.Print();
return 0;
}
这里就是一个无参的构造函数:
Data();
相反的,这里是一个带参的构造函数:
Data(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
};
他们的调用各自如何呢:
Data d1;
Data d2(2023, 1, 1);
那我们现在就来看看,构造函数的赋值。
如果类里面没有显示的定义构造函数,那么 C++编译器就会自动生成一个无参的默认构造函数,一旦用户显示的定义,那么编译器将不再生成。所以的话,如果我们不写构造函数的话那也是可以的,但是初始化就可能会麻烦一点。
在创建对象的时候,编译器通过调用构造函数,给每一个对象中的各个成员给一个合适的初始值。有两种方法,一种是初始化列表,另一种就是函数体内赋值,函数体内赋值不讲了,这里讲初始化列表。
初始化列表就是以一个冒号开始,接着是一个逗号分隔的数据成员列表,每一个成员变量后面跟一个放在括号中的初始值或者表达式。
那么,什么时候用初始化链表呢?
以下三种成员变量必须用列表来进行初始化:
1、引用成员变量
2、const成员变量3、自定义类型成员(且该类没有默认构造函数时)
这里注意,每一个成员变量在初始化列表中只能出现一次,也就是初始化只能有一次。
我们下面来一一举例看一下:
class A
{
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10)
, _x() //不写的话,缺省值就有用,如果显示的给了值,那么缺省值就没有用了
{
}
private:
//声明
A _aobj; // 没有默认构造函数,不传参和全缺省,不单单指编译器自动生成的
//必须在定义的时候进行初始化
int& _ref; // 引用
const int _n; // const
int _x = 1;//这里的1是缺省值,缺省值是给初始化列表的
};
还有一个问题,成员声明顺序的事,我们定义成员数据的时候究竟是按什么来赋值的?
实际初始化顺序由成员在类中的声明顺序决定,而非初始化列表中的顺序。
如果 _aobj、_ref、_n、_x 的声明顺序与初始化列表不一致,可能导致未定义行为(尤其是依赖其他成员初始化时)。
所以日常使用需要保持初始化列表顺序与成员声明顺序一致。
最后我们再来思考一下,什么情况下我们需要使用构造函数?

2.析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
举个栗子,比如局部对象是存在栈帧的 , 函数结束栈帧销毁 , 它就释放了 , 不需要我们管 , C++规定对象再销毁时会自动调用析构函数 , 完成对对象中的资源清理释放工作 。
析构函数的功能类比我们之前Stack 实现的Destory 的功能 , 而像Date 没有 Destory , 其实就是没有资源需要释放 , 所以严格说Date日期类 , 是不需要析构函数的 。
析构函数的特性:
1.析构函数名是在类名前加上字符~ 。
2.无参数无返回值 。 (这里与构造类似 , 也不需要加void)
3.一个类只能有一个析构函数 。 若未显式定义,系统会自动生成默认的析构函数 。注意,析构函数不能被重载。
4. 对象生命周期结束时,C++编译系统系统会自动调用析构函数。
5. 与构造函数类似,我们不写构造函数时,编译器会自动生成析构函数,但是对内置类型成员不做处理,自定类型成员会调用他的析构函数。(因为程序结束操作系统会自动回收这些内置类型成员变量的空间的)类对象在实例化之后,对象的变量也是开辟在栈等空间上的,这些空间都是操作系统开辟的,回收也是由操作系统回收。
仍需注意的是显示写析构函数时 ,对于自定义类型成员也会调用自定义类型中的析构函数 , 也就是说自定义类型成员无论什么情况都会自动调用析构函数 。
如果类中没有申请资源时,析构函数可以不写 ,直接使用编译器生成的默认析构函数 ,如 Date类 ; 如果是默认生成的析构就可以用 ,也就不需要显示写析构函数 , 如MyQueue ; 但是有资源申请时 , 一定要自己写析构 , 否则会造成资源泄漏 , 如Stack 。这就是著名的RAII(资源获取即初始化)原则:
在构造函数中获取资源,在析构函数中释放资源。
一个局部域的多个对象 , C++规定后定义的先析构 。
3.拷贝构造函数
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?这就是拷贝构造函数。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
拷贝构造的特征:
1.拷贝构造函数是构造函数的一个重载形式,一般得加const,以免拷贝错了
什么意思呢?我们之前在学引用的时候已经注意到,引用是被引用对象的别名,本质上是同种东西
这里就涉及到一个权限的放大的问题。
举个例子 : 如果我给了一个张三的蓝本 , 给你去造一个张三出来 , 但是因为某一个不小心的错误 , 把张三 变成了 李四了 , 给我造了个李四出来 , 还把我原先给的张三的蓝图 改成了 李四的蓝图 , 这就和本意不符合了。
所以我们就可以得出,为什么加const:
避免意外修改原对象(拷贝时不应改变源对象)。
允许拷贝 const 对象(提高灵活性)。
符合语义(拷贝构造函数应该是“只读”的)。
2.拷贝构造函数的参数只有1个,且必须是该类类型对象的引用(指针也行,但c++规定是引用),如果用传值传参,这个时候,拷贝构造函数的形参是接受来自实参的值传递,又相当于调用了一次新的拷贝构造函数,如此,无穷反复,编译器会直接报错。
这就要我们来看看,拷贝构造函数到底干了什么,动了谁的蛋糕。

传值返回会产生一个临时对象调用拷贝构造
传值引用返回,返回的是返回对象的别名(引用) , 没有产生拷贝!
但是如果返回对象是一个当前函数局部域的局部对象 ,函数结束就销毁了 ,那么使用引用返回是有问题的,这时的引用相当于野引用,类似一个野指针一样 。
传引用返回可以减少拷贝 , 但是一定要确保返回对象 , 在当前函数结束后还在 ,才能用 引用返回 。
class MyClass {
public:
MyClass(MyClass other) { // ❌ 错误!传值方式会导致递归
this->data = other.data;
}
};
MyClass obj1;
MyClass obj2(obj1); // 调用拷贝构造函数
发生了什么?
1.obj2(obj1) 调用拷贝构造函数 MyClass(MyClass other)。
2.由于 other 是传值(而不是引用),所以编译器需要先复制 obj1 来构造 other。
3.为了复制 obj1,又需要调用 MyClass(MyClass other),这时又需要再复制 obj1 来构造新的 other。
所以这个逻辑会无限循环下去,直到栈溢出。
注意 : 拷贝构造的第一个参数必须是类类型对象的引用,可以再后面加参数,但此时的参数必须是缺省的!
Date(const Date& d,int x= 1)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
那么我们来看,什么是浅拷贝?什么是深拷贝?
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。


那么,什么时候用浅拷贝,什么时候用深拷贝呢?
在c++中如果用值拷贝自定义类型,会出事,假如是栈(数据结构),那么用传值调用,由于析构函数在对象生命周期结束后,会自动调用,这个时候,形参和实参都会对空间进行一次释放。相当于一块空间被连续释放了两次。
因此对于涉及资源申请(例如空间开辟)的类,我们应当自己写拷贝构造函数,对于不涉及的,可以让编译器自己生成。(注意,还有一种类型,就是我们进行嵌套,比如用两个栈实现队列,这个时候队列的拷贝构造我们没必要写,因为对于自定义类型,会调用它的拷贝构造,所以我们只需要把栈的拷贝构造写好就行)

对于拷贝构造一般用在:用已经存在的同类的对象初始化或拷贝。
函数参数为类类型对象或返回值为类类型对象。
下面给大家一段代码演示一下在vs2022里面,构造函数的调用
int main() {
Date d1(2022, 1, 13); // 调用构造函数创建d1
Test(d1); // Test函数以值方式传递,传参时调用拷贝构造函数创建d
return 0;
}
Date Test(Date d) { // 函数以值方式返回,返回时使用temp拷贝构造临时对象用来返回
Date temp(d); // 调用拷贝构造函数创建temp
return temp; // 返回temp
}

4.赋值运算符重载
4.1运算符重载
C++为了增强代码的可读性引入了运算符重载.
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
bool operator> (NN x, NN y)
{
if (x._c > y._c)return true;
else return false;
}
int main()
{
NN B1, B2;
cout << (B1 > B2);
//这个等同于cout<<(operator>(B1,B2));
return 0;
}
为什么要有运算符重载呢?因为我们有了类,这时候可能会需要比较,但问题是,编译器也不知道我们比较的规则等,所以编译器对于自定义类型,干脆是不提供比较方法,要我们自己想办法,对于内置类型,编译器最开始就知道怎么比,所以有运算符直接提供给我们用。
这些运算符可以直接转换为机器能识别的指令。
但是,这样我们就忽略了封装了,如果不采用全局的operator,我们可以封装成成员函数。因为 operator> 直接访问了 NN 类的私有成员 _c,而通常情况下,类的私有成员(private)不应该被外部函数直接访问。
class NN
{
public:
bool operator> (NN y)//可以是引用也可以传值
{
if (_c > y._c)return true;
else return false;
}
//对于成员函数来说,可以随意调用同类的成员,不受限制
//对于成员函数来说,一直有个默认的this指针,作为第一参数,
//所以只需要一个参数即可.
private:
int _c = 1;
int _k;
};
int main()
{
NN B1, B2;
cout << (B1 > B2);
//B1>B2在编译器视角下,是NN::operator>(&B1,B2);
return 0;
}
4.2 赋值运算符重载
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
#include<iostream>
using namespace std;
class NN
{
public:
NN(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
NN& operator=(const NN& a)
{
if (this != &a)
{
_year = a._year;
_month = a._month;
_day = a._day;
}
return *this;
}
void print()
{
cout << _year << '/' << _month << '/' << _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
NN d1(2024, 1, 4);
NN d2 =d1 + 100;
d2.print();
d1.print();
return 0;
}
1.赋值运算符只能重载成类的成员函数,不能重载成全局函数,因为编译器会自动生产一个赋值运算符的重载在类里面(如果你不显示定义),这样全局和类里面会冲突,所以不能重载全局。

2.编译器默认生成的赋值运算符,跟拷贝构造很像,对内置类型的变量,直接采用逐字节值覆盖,自定义类型采用该类型的赋值运算符。赋值运算符重载是一个默认成员函数 , 用于完成两个 已经存在 的对象直接拷贝赋值 , 这里要注意跟拷贝构造区分 , 拷贝构造用于一个对象拷贝初始化给另一个对象 。
3.没有显式实现时 , 编译器会自动生成一个默认赋值运算符重载 , 默认运算符重载行为跟默认构造函数类似 , 对内置类型成员变量会完成 浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造 。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
4.3 前置++和后置++重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
一份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}
5.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,const 修饰成员函数放在成员函数参数后面 。实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
我们来看这段代码思考几个问题:
1. const对象可以调用非const成员函数吗?
❌ 不可以!const 对象保证其成员变量不会被修改,而非 const 成员函数可能修改成员变量。这里还是涉及到一个可更改的权限的问题。
2. 非const对象可以调用const成员函数吗?
✅ 可以!const 成员函数承诺不会修改对象状态,对非 const 对象是安全的。
3. const成员函数内可以调用其它的非const成员函数吗?❌ 不可以!同1
4. 非const成员函数内可以调用其它的const成员函数吗?✅ 可以!这是一种常见做法,例如在非 const 函数中复用 const 函数的逻辑
6.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
7.日期类的实现
7.1流插入 << 和 流提取 >>
注意,我们打印自定义类型不能直接用cout,为什么(我们之前知道cout和cin是自动识别类型,这是因为c++自己写了内置类型的函数重载,从而识别)cout是ostream类里面的,cin是在istream类里面的。如果我们要用cout和cin输入输出指定的自定义类型,我们要运算符重载。
通常我们要输出自定义类型有两种方法:
1.全局重载,需要用到友元函数,下一篇会讲。
class Date {
int year, month, day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}
friend ostream& operator<<(ostream& os, const Date& dt); // 声明友元函数
};
// 实现 `<<` 运算符
ostream& operator<<(ostream& os, const Date& dt) {
os << dt.year << "-" << dt.month << "-" << dt.day;
return os; // 返回流以便链式调用(如 `cout << d1 << d2;`)
}
int main() {
Date d1(2023, 1, 1);
cout << d1; // ✅ 正确,输出:2023-1-1
return 0;
}
2.函数重载,调用比较奇怪。cout在右边
class Date {
int year, month, day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}
ostream& operator<<(ostream& os) { // ❌ 不推荐,因为调用方式奇怪
os << year << "-" << month << "-" << day;
return os;
}
};
int main() {
Date d1(2023, 1, 1);
d1 << cout; // ❌ 语法奇怪,不符合常规 `cout << d1;` 的习惯
return 0;
}
但是这里我们也可以让>>和<<重载.
class Date {
int year, month, day;
public:
Date() = default; // 默认构造函数(cin 需要)
friend istream& operator>>(istream& is, Date& dt); // 声明友元函数
};
// 实现 `>>` 运算符
istream& operator>>(istream& is, Date& dt) {
is >> dt.year >> dt.month >> dt.day;
return is; // 返回流以便链式调用(如 `cin >> d1 >> d2;`)
}
int main() {
Date d1;
cin >> d1; // ✅ 正确,输入:2023 1 1
cout << d1; // 输出:2023-1-1
return 0;
}
7.2日期类代码
date.h
#pragma once
#include<iostream>
using namespace std;
class NN
{
public:
NN(int year=1, int month=1, int day=1);
int getday(int year, int month1);
NN& operator+= (int day);
NN operator+ (int day);
NN& operator=(const NN& a);
NN& operator-= (int day);
NN operator- (int day);
NN& operator++();
NN operator++(int);
NN& operator--();
NN operator--(int);
bool operator==(const NN& a);
bool operator>(const NN& a);
bool operator<(const NN& a);
bool operator>=(const NN& a);
bool operator<=(const NN& a);
bool operator!=(const NN& a);
int operator-(const NN& a);
void print();
private:
int _year;
int _month;
int _day;
};
date.cpp
#include"date.h"
NN::NN(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (_year < 1 || _month>12 || _day < 1 || _day>getday(_year, _month))
{
this->print();
cout << "日期非法" << endl;
}
}
int NN::getday(int year, int month1)
{
int month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month1 == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0))
{
return 29;
}
return month[month1];
}
NN& NN::operator+= (int day)
{
if (day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > getday(_year, _month))
{
_day -= getday(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
//* this = *this + day;
return *this;
}
NN NN::operator+ (int day)
{
NN T(*this);
T += day;
//T._day += day;
//while (T._day > getday(T._year, T._month))
//{
// T._day -= getday(T._year, T._month);
// T._month++;
// if (T._month > 12)
// {
// T._year++;
// T._month = 1;
// }
//}
return T;
}
NN& NN::operator=(const NN& a)
{
if (this != &a)
{
_year = a._year;
_month = a._month;
_day = a._day;
}
return *this;
}
NN& NN::operator++()
{
*this += 1;
return *this;
}
NN NN::operator++(int)
{
NN tmp(*this);
*this += 1;
return tmp;
}
bool NN::operator==(const NN& a)
{
if (_year == a._year && _month == a._month && _day == a._day)
{
return true;
}
else
{
return false;
}
}
bool NN::operator>(const NN& a)
{
if (_year > a._year)
{
return true;
}
else if (_year == a._year && _month > a._month)
{
return true;
}
else if (_year == a._year && _month == a._month && _day > a._day)
{
return true;
}
else return false;
}
void NN::print()
{
cout << _year << '/' << _month << '/' << _day;
}
bool NN::operator>=(const NN& a)
{
return *this > a || *this == a;
}
bool NN::operator<(const NN& a)
{
return !(*this >= a);
}
bool NN::operator<=(const NN& a)
{
return !(*this > a);
}
bool NN::operator!=(const NN& a)
{
return !(*this == a);
}
NN& NN::operator-= (int day)
{
if (day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += getday(_year, _month);
}
return *this;
}
NN NN::operator- (int day)
{
NN a(*this);
a -= day;
return a;
}
int NN::operator-(const NN& a)
{
int flag = 1;
NN max(*this);
NN min(a);
if (*this < a)
{
max = a;
min = *this;
flag = -1;
}
int d = 0;
while (min != max)
{
++min;
d++;
}
return d * flag;
}
NN& NN::operator--()
{
*this -= 1;
return *this;
}
NN NN::operator--(int)
{
NN tmp(*this);
*this -= 1;
return tmp;
}

被折叠的 条评论
为什么被折叠?



