🎁个人主页:工藤新一¹
🔍系列专栏:C++面向对象(类和对象篇)
🌟心中的天空之城,终会照亮我前方的路
🎉欢迎大家点赞👍评论📝收藏⭐文章
类和对象(中)
一、类中的默认成员函数
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我 们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是 C++11 以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯ 去学习:
二、构造函数
构造函数 是特殊的成员函数,需要注意的是,构造函数 虽然名称叫构造,但是 构造函数 的主要任务并 不是开空间创建对象(我们常使⽤的 局部对象是栈帧创建时,空间就开好了 ),⽽是对象实例化时初始化 对象。构造函数 的本质是要替代我们以前 Stack
和 Date
类中写的 Init函数
的功能,构造函数
⾃动调⽤的 特点就完美的替代的了 Init
构造函数特点:
1、对象实例化时系统会自动调用对应的构造函数
2、构造函数可以发生重载,默认构造不仅仅局限于默认构造函数,默认构造的特点:不传参即可调用
3、如果类中没有显式定义构造函数,则 C++ 编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成
C++
//调用默认构造函数时
注意区分:函数声明
Date func();//--->函数声明
Date d();//--->编译器会无法区分(认为这也是函数声明)
Date d1;//--->默认构造的正确调用方式
/*
与 Java 中的调用默认构造不同
Date d = new Date();
*/
4、无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的 默认构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存 在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造
C++
//1、无参构造
Date()
{
year = 1;
month = 1;
day = 1;
}
//2、全缺省构造
Date(int year = 1, int month = 1, int day = 1)
{
this->year = year;
this->month = month;
this->day = day;
}
//在语法上 无参构造与全缺省构造可以构成函数重载
相较于无参构造,全缺省构造的优点:
- 实例对象可无参调用
- 实例对象可有参调用
5、我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表,我们下个章节再细细讲解
typedef int STDataType;
class Stack
{
public:
//全缺省构造(作用:初始化)
Stack(int n = 4)
{
arr = (STDataType*)malloc(sizeof(STDataType) * n);
if (arr = nullptr)
{
perror("malloc fail!");
exit(1);
}
capacity = n;
top = 0;
}
private:
STDataType* arr;//维护堆区数据
int capacity;
int top;
};
//两个栈实现队列
class MyQueue
{
public:
//编译器默认生成 MyQueue 的构造函数调用了 Stack 的构造,完成了对两个成员的初始化
private:
//类对象
Stack pushst;
Stack popst;
};
int main()
{
//实例化的同时调用了 MyQueue 的构造函数
MyQueue mq;
return 0;
}
构造函数的调用顺序:
三、析构函数
- 析构函数并非是释放对象,具有 “先构造后析构” 的特性(有构造一定有析构)
- 对于自定义数据类型不显示析构,会导致内存泄漏
析构函数 与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++ 规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。 析构函数的功能类⽐我们之前 Stack
实现的 Destroy
功能,⽽像 Date
没有 Destroy
,其实就是没有资源需要释放,所以严格说 Date
是不需要 析构函数 的
析构函数的特点:
析构函数名是在类名前加上字符
~
(按位取反,!
:逻辑取反)⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数,且对象⽣命周期结束时,系统会⾃动调⽤析构函数
跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数
还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数
如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如
Date
;如 果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue
;但是有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏,如Stack
:
C++
class Stack
{
public:
Stack(int n = 4)
{
//动态申请资源 - 需要我们手动释放数据
arr = (STDataType*)malloc(sizeof(STDataType) * n);
top = 0;
capacity = n;
}
~Stack()
{
free(arr);
arr = nullptr;
top = capacity = 0;
}
};
- ⼀个局部域的多个对象,C++规定后定义的先析构
3.1默认构造初始内置数据类型的问题
四、拷贝构造函数
如果⼀个 构造函数 的第⼀个参数是⾃⾝类的类型引⽤,且任何额外的参数都有默认值,则此构造函数也叫做 拷贝构造函数,也就是说 拷贝构造是⼀个特殊的构造函数
同时,存在一个小技巧:如果代码中需要 显示实现析构函数并释放资源,那么往往会伴随着 显示拷贝构造的实现(写 深拷贝),因此 析构与拷贝构造往往会同时出现
拷贝构造的特点:
- 指针 指向 动态开辟的数据时,需要显示实现拷贝构造函数,因此 指针 并非一定需要 深拷贝,如 迭代器 、文件指针(指向打开的文件),这些只需 浅拷贝 即可
4.1无穷递归(链式)调用
- 拷⻉构造函数是构造函数的⼀个重载
- 拷⻉构造函数的第⼀个参数必须是类的类型对象的引⽤,使⽤ 传值⽅式 编译器直接
报错
,因为语法逻辑上会引发 无穷递归调用(链式调用)。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
值传递带来的链式调用问题:
- C++ 规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
举例描述:
函数调用特点:要想调用当前有参函数要先传参,需要优先调用拷贝构造函数进行参数的功能运算
**
**
4.2返回值、返回引用
传值返回(返回值) 会产⽣⼀个 临时对象调用拷贝构造,而 传值引用返回(返回引用),返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但如果返回对象是⼀个当前函数局部作用域下的局部对象
,函数结束就销毁了,那么使⽤ 值返回 是有问题的,这时的引⽤相当于⼀个 “ 野引用(悬垂引用)
”,类似 野指针
。**返回引用** 可以减少拷⻉,但是⼀定要确保返回对象,在当前函数执行结束后依然存在
引发的问题(内置类型):
更严重的问题(自定义类型):
对比 Stack 的值返回:
4.3默认拷贝构造
- 对于内置数据类型,进行浅拷贝(值拷贝)
编译器生成默认拷贝构造函数,也可完成拷贝工作,这个⾃动⽣成的拷⻉构造 对内置类型成员变量(与析构、构造的区别) 会进行 值拷贝/浅拷贝 (⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造
4.4浅拷贝 - 堆区数据重复释放
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
arr = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == arr)
{
perror("malloc fail!");
exit(1);
}
_capacity = n;
_top = 0;
}
void StackPush(STDataType x)
{
if (_top == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
STDataType* temp = (STDataType*)realloc(arr, newCapacity * sizeof(STDataType));
if (temp == nullptr)
{
perror("realloc fail!");
exit(1);
}
arr = temp;
_capacity = newCapacity;
}
arr[_top++] = x;
}
~Stack()
{
cout << "调用 Stack 析构函数" << endl;
if (arr != nullptr)
{
//delete arr;--->写法错误,malloc 动态开辟的空间需要 free 释放
free(arr);
arr = nullptr;
}
}
private:
STDataType* arr;
size_t _capacity;
size_t _top;
};
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st1;
st1.StackPush(1);
st1.StackPush(2);
st1.StackPush(3);
Stack st2(st1);
return 0;
}
于是,我们再一次见到了我们的老朋友:
浅拷贝所带来的问题:堆区数据重复释放
浅拷贝带来的问题是堆区内存重复释放,为了解决这个问题,引入了深拷贝在堆区开辟一块新空间,从而使拷贝构造出的指针指向这块新的堆区数据,再通过析构函数分别释放这两块堆区数据
C++
Stack(const Stack& s)
{
_top = s._top;
_capacity = s._capacity;
//分配足够的空间(与数组容量相同的空间)
arr = (STDataType*)malloc(sizeof(STDataType) * _capacity);
memcpy(arr, s.arr, sizeof(STDataType) * s.top);
}
五、运算符重载
- 当运算符被⽤于自定义类型的对象时,C++ 语⾔允许我们通过运算符重载的形式指定新的含义。C++ 规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载
另外,函数重载与运算符重载,都描述了 “重载” 这个词汇,但他们的意义是不同的:
- 函数重载:返回值类型、函数名相同,参数不同
- 运算符重载:重新定义(自定义数据类型)运算符的行为
- 两个基于相同运算符的重载,又可以形成函数重载
//形成函数重载
d1 - 100 ---> d1.operator(100);
d1 - d2 ---> d1.operator(d2);
5.1浅识汇编层
C++
class Date
{
public:
Date(int year, int month, int day)
: year(year), month(month), day(day) {
}
private:
int year;
int month;
int day;
};
int main()
{
int a = 10, b = 20;
bool ret = a < b;
return 0;
}
转入 汇编码:
由于内置类型是一个比较简单的数据类型,因此系统库中在底层内化了一些指令(比较指令:cmp
),进行一些功能(如:解引用、比较、随机访问 []
等运算操作符)
但系统并不会给自定义数据类型提供指令,因为自定义数据类型可以很复杂,可以使用各种各样的符号操作,系统无法判断
5.2重载运算符
5.2.1重载 ==
- 运算符重载 也具有返回值、参数列表、函数体
- 操作符也包括一元运算符(*p、&p…),二元运算符(x < y),那么重载运算符函数时,运算符函数的参数个数与运算对象数相同,如上述比较两个自定义数据类型大小时:
class Date
{
public:
//声明友元函数
friend bool operator== (const Date& d1, const Date& d2);
Date(int year, int month, int day)
: year(year), month(month), day(day) { }
private:
int year;
int month;
int day;
};
//加入引用避免拷贝,const的引用使函数体中d1、d2数据不会被修改
bool operator== (const Date& d1, const Date& d2)
{
return d1.year == d2.year
&& d1.month == d2.month
&& d1.day == d2.day;
}
/*方法二:
或者类内提供 Get 方法(Java 常用)
d1.GetYear() == d2.GetYear();
*/
/*方法三:
直接放到类中,重载为成员函数 operator==();
运算符重载为成员函数
*/
int main()
{
/*
如果const Date d1;
形参接收时也需要加入 const 修饰,避免权限放大(const (转为)---> 非const)
*/
Date d1(2025, 5, 6);
Date d2(2025, 5, 6);
//两种写法
d1 == d2;
operator==(d1, d2);
return 0;
}
将运算符重载为成员函数:
原因:如果⼀个 重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的 this指针
,因此运算符重载作为成员函数时,其函数参数⽐运算对象少⼀个(因为被隐藏的 this指针
本身就算一个参数)
因此编译器会将代码隐式转换为:
class Date
{
public:
Date(int year, int month, int day)
: year(year), month(month), day(day) { }
bool operator== (const Date& d) const
{
return year == d.year
&& month == d.month
&& day == d.day;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1(2025, 5, 6);
Date d2(2025, 5, 6);
cout << (d1 == d2) << endl;
cout << d1.operator== (d2);
return 0;
}
这就是二元运算符,通常左对象传入 this指针
,右对象作为形参
5.2.2重载 << 流插入运算符
- 在C++中,
<<
是 流插入运算符(Stream Insertion Operator),它是标准库中重载的一个运算符,用于向输出流(如std::cout
、文件流等)插入数据。
C++
cout << d1 == d2 << endl;
另外,对于内置类型的直接比较也同样需要注意优先级的问题:
5.2.3运算符重载注意事项
- 运算符重载无法根据 C / C++ 语法中不存在的符号创建新的操作符,比如:operator@ ();
- 重载操作符至少有一个内置类型(即至少存在一个隐式的
this指针
),无法通过运算符重载改变内置类型对象的含义,如:int operator+ (int x, int y);
有五个运算符不能发生重载(选择题常考):
- “ : : ” , 作用域限定符
- “ sizeof ”
- “ ? : ” , 三目运算符
- “ . ” , 调用操作符
- “ .* ” , 成员指针运算符
5.2.4成员指针运算符
5.2.4.1函数指针(回调函数)与成员函数指针
- C++ 几乎抛弃了 函数指针 这个触发 回调函数 的方式,由更简洁的
lambda
表达式进行了高效替代,因此对于 函数指针,我们目前只需浅尝辄止即可
C++
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
void f()
{
cout << "f()" << endl;
}
int main()
{
//函数指针 - 作为回调函数
void(*func1)() = f;
(*func1)();//函数指针调用
return 0;
}
C++
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
void f()
{
cout << "f()" << endl;
}
int main()
{
//普通函数
//函数指针 - 作为回调函数
void(*func1)() = f;//函数名就是函数的地址
(*func1)();//函数指针调用
//成员函数的指针
//指定成员函数指针的类域
void(A::*func2)() = &A::func;//获取成员函数的地址,需要加入取地址符号
//成员函数的调用需要通过实例对象进行调用
A aa;
(aa.*func2)();//调用成员函数指针
return 0;
}
5.2.5成员对象与类对象
5.3项目 - 时间计时器
Date.h
文件
#include<cassert>
class Date
{
public:
//可以将频繁调用的小函数变为内联函数
int get_month_day(int year, int month);
//内置数据类型拷贝构造时逐字节进行浅拷贝
Date(int year = 1, int month = 1, int day = 1);
//(日期加天数)返回值依然是日期
Date operator+ (int day);
Date operator- (int day);
private:
int year;
int month;
int day;
};
Date.cpp
文件
#include"Date.h"
Date::Date(int year, int month, int day)
{
this->year = year;
this->month = month;
this->day = day;
}
int Date::get_month_day(int year, int month)
{
}
Date Date::operator+ (int day)
{
}
Date Date::operator- (int day)
{
}
static
所修饰的变量、函数,不存储在对象上(存储在静态区)
Test.cpp
文件
#include"Date.h"
int main()
{
Date d1;
Date d2(d1 + 100);//拷贝构造,也可以写出
Date d2 = d1 + 100;
return 0;
}
5.3.1重载 += 复合运算符(经典误区)
对于 “ + ” 运算符的重载,我们尤其需要注意 “ + ”(二元运算符) 与 “ += ”(复合赋值运算符) 的区别,前者不会改变 this
对象自身,而后者则回改变 this
对象自身的属性
经典错误案例:
举例阐述:
5.3.2重载 + 运算符
- 返回运算结果(临时对象)
5.3.3重载 “ + ” 与重载 “ += “
d1.operator(d2); 或 d1 + d2(d1 += d2)
- “ + ”:不能改变 d1(*this)(前面内置数据类型的运算有详细说明,因此自定义数据类型也需遵循)
- “ += ”:可以改变d1
5.4辨别深、浅拷贝
5.5赋值运算符重载
赋值运算符重载的特点:
规定必须重载为成员函数(建议写成:const + &)
赋值运算符支持 链式赋值(连续赋值)的方式
没有显示实现时,编译器会生成默认赋值运算符重载,其默认函数与默认拷贝构造函数类似,对内置数据进行 浅拷贝(逐字节拷贝),对自定义变量会调用其赋值重载函数(因此,存在深、浅拷贝问题)
遵循 **C++三大原则:**类中显示实现 析构函数,那么则需显示实现 拷贝构造、赋值运算符重载函数
this->day = day;
:会导致赋值无法达到理想的效果,因为 this->day == day
所以,改错:this->day = d.day;
5.5.1C++三大原则
🌟 各位看官好,我是工藤新一¹呀~
🌈 愿各位心中所想,终有所致!