目录
前言
对于 C++的初学者而言,可能会心生困惑:明明结构体就能搞定的事情,为何还要弄出个"类"来?而且要是数据能像在结构体中那样随时随地随意使用,岂不是方便至极?进而得出"C++不如 C 语言"的结论。
在此,我必须为 C++叫屈!咱们 C++的祖师爷绝不可能闲来无事创造一门语言。况且,既然C++能被广大程序员广泛学习和运用,必有其过人之处!C 语言在应对大规模、复杂的程序时存在一定局限性。而类和对象这一概念的引入,为程序员提供了更为强大的工具,用于组织和管理代码,能更出色地模拟现实世界中的问题与实体,实现更高程度的代码复用、封装、继承和多态等特性。(这里并非要表明 C++比 C 语言更出色,二者各有千秋。C 语言面向过程,运行速度更快,对内存有着至高权限;C++面向对象,在处理复杂对象时,优势尽显。)
C++的学习之路必然是坎坷的,它的概念和语法相较于其他语言可能会显得更为复杂和抽象。但请相信,每一次攻克难题后的收获都将是无比宝贵的。当你深入理解并熟练运用类和对象的概念时,你会发现它为编程带来的巨大便利和效率提升。
所以,希望大家不要轻言放弃。接下来,我会带领大家从结构体逐渐过渡到类,尽可能让大家慢慢体悟二者的差异。闲话少叙,咱们即刻进入正题。
类的定义
1.类定义格式
**class为定义类的关键字,Point为类的名字(以本文为例,名字可自定义),{}中为类的主体,注意类定义结束时后面分号不能省略。**类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或者成员函数。
我们来看一个例子:
cpp
class Point
{
private:
// 私有成员变量 x,用于存储点的 x 坐标
int x;
// 私有成员变量 y,用于存储点的 y 坐标
int y;
public:
// 公共成员函数,用于设置点的坐标
void set(int x, int y)
{
// 使用 this 指针区分成员变量和参数
this->x = x;
this->y = y;
}
// 公共成员函数,用于打印点的坐标
void print()
{
std::cout << "Point: (" << x << ", " << y << ")" << std::endl;
}
};
2.从结构体到类的跨越
呀~这不就是C语言里的结构体嘛,老朋友了!
**错!**这是今天要带你认识的新大哥!也是C和C++在本质上一个很大的不同之处。可以说C++被创造出来,有很大一部分原因就是因为类。
因为C 语言在处理大规模、复杂的程序时存在一些局限性。而类和对象的概念引入为程序员提供了更强大的工具来组织和管理代码,更好地模拟现实世界中的问题和实体,实现更高程度的代码复用、封装、继承和多态等特性。
下面我们来介绍C++中的类和C语言中的结构体的异同:
C++ 中的类和 C 语言中的结构体有以下相同点:
都可以包含不同数据类型的成员:可以有整型、浮点型、字符型、指针等各种类型的成员变量。
成员的存储方式相同:按照声明的顺序依次存储在内存中。
都可以作为函数的参数进行传递:可以按值传递或者通过指针传递。
它们的不同点主要有:
成员的默认访问权限不同:在 C 语言的结构体中,成员的默认访问权限是公共的(
public
);而在 C++ 的类中,成员的默认访问权限是私有的(private
)。继承特性:C++ 的类支持继承机制,可以从一个已有的类派生出新的类,实现代码的复用和扩展;C 语言的结构体没有继承的概念。
多态性:C++ 的类支持多态性,通过虚函数可以实现运行时的动态绑定;C 语言的结构体不支持多态。
函数作为成员:在 C 语言的结构体中,一般不能直接包含函数;而在 C++ 的类中,可以将函数作为成员函数来定义和使用。
封装性:C++ 的类提供了更好的封装性,可以将成员变量和成员函数进行访问权限的控制,隐藏内部实现细节;C 语言的结构体封装性较弱,成员都是公开可访问的。
我们来上代码体会:
这是C语言版的,用来实现坐标设置的代码:
cpp
#include <stdio.h>
// 定义结构体
typedef struct Point {
int x;
int y;
} Point;
// 函数来设置点的坐标
void setPoint(Point *p, int x, int y) {
p->x = x;
p->y = y;
}
int main() {
Point p;
setPoint(&p, 10, 20);
printf("Point: (%d, %d)\n", p.x, p.y);
return 0;
}
下面是C++版本:
private和public看不懂的同学不要急,我们下面就讲。
cpp
#include <iostream>
class Point {
//私有
private:
int x;
int y;
//公有
public:
void set(int x, int y) {
this->x = x;
this->y = y;
}
void print() {
std::cout << "Point: (" << x << ", " << y << ")" << std::endl;
}
};
int main() {
Point p;
p.set(10, 20);
p.print();
return 0;
}
屏幕前的你也许正在心里吐槽:明明就是C语言的代码简洁明了。没错,你还真讲对了!(我前面就说了,各有所长)面对对象编程,顾名思义,对象越复杂就越能体现它的奥妙。我们来举个形象的例子:
在做小项目时,用到的成员变量也不多,就像是给了你图中的方块。你脑海中自然就构建出了类似村民住所的小房子,甚至你自己盖的更漂亮(代码更简洁),给你图纸(类)可谓是多此一举,但如果项目过大呢?
若是给你了一堆变量(方块),却没有给你图纸(类)。一定是有人能做出来,但可能不如给你图纸那般顺畅罢了。当然这只是个比喻,你可不能因为喜欢MC中自己开动想象力创造,就不喜欢C++哦!
C++中数据和函数都放到了类⾥⾯,通过访问限定符进⾏了限制,不能再随意通过对象直接修改数 据,这是C++封装的⼀种体现,这个是最重要的变化。这⾥的封装的本质是⼀种更严格规范的管 理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后⾯还需要不断的去学习。
值得一提的是,struct在C++中升级成类了,也可以包含成员函数 。但这并不意味着struct和class就完全一样,他们成员的默认访问权限不同:在struct中,成员的默认访问权限是public(公有);在class中,成员的默认访问权限是private(私有)。这里还是推荐大家去用class。
3.访问限定符
C++⼀种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使⽤。
- public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访 问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
- 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有 访问限定符,作⽤域就到 }即类结束。
- class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
- ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
4.类域
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作 ⽤域操作符指明成员属于哪个类域。
类域影响的是编译的查找规则,下⾯程序中Init如果不指定类域Stack,那么编译器就把Init当成全 局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
例如:
cpp
#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;
}
5.类的实例化
用类的类型在物理内存中创建对象的过程,称为类实例化出对象。
类是对象进行一种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
⼀个类可以实例化出多个对象,实例化出的对象占⽤实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使⽤建筑设计图建造出房子,类就像是设计图,设计图规划了有多 少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房 子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。
cpp
#include<iostream>
using namespace std;
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(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
类的默认成员函数
1.默认成员函数的定义和学习方向
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数**(但并不是编译器会自动生成我们就可以不学习,反之这恰恰代表了这些函数是必不可少的,很多时候编译器并不能了解和满足我们的意图,此时就需要我们自己动手去写)。**
一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
- 第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
- 第⼆:编译器默认生成的函数不满足我们的需求时,我们需要自己实现,那么如何自己实现?
2.构造函数
构造函数是特殊的成员函数,用于在创建对象时进行初始化操作。
需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象 。构造函数的本质是要替代我们以前写的Init函数(初始化函数)的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点:
- 函数名与类名相同。
- 无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
- 对象实例化时系统会自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造。
- 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。(说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。)
我们来举个例子。在这个例子中,好好感受我在特点处标黄的三个特点和构造函数的初始化作用。
cpp
#include <iostream>
class Rectangle
{
private:
int length;
int width;
public:
// 构造函数,用于初始化矩形的长和宽
//注意和类名相同,无返回值(什么都没有,连void都没有)
Rectangle(int l, int w)
{
length = l;
width = w;
}
int area()
{
return length * width;
}
};
int main()
{
Rectangle r(5, 3); // 使用构造函数创建对象并初始化
std::cout << "Area: " << r.area() << std::endl;
return 0;
}
3.析构函数
析构函数是类的一种特殊成员函数,其作用和目的主要是资源释放。当对象被销毁时,析构函数负责释放对象在其生命周期内获取的资源,如动态分配的内存、打开的文件、网络连接等。如果不及时释放这些资源,可能会导致内存泄漏或其他资源浪费的问题。
所以呢,析构函数一般在类中包含动态分配的内存和类使用了其他系统资源,如文件、网络连接、数据库连接等情况下才需要我们专门去写。其他时候使用编译器自动生成的析构函数即可。
析构函数的特点:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。 (这里跟构造类似,也不需要加void)
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,系统会自动调用析构函数。
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
- 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如;如果默认生成的析构就可以用,也就不需要显示写析构;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏。
- 一个局部域的多个对象,C++规定后定义的先析构
下面我们来看一下析构函数在不同场景下的运用:
1.当类中包含动态分配的内存:如果在类中分配了内存,那么需要在析构函数中释放,以避免内存泄漏。
cpp
class example
{
private:
int* arr;
public:
stack(int size)
{
arr = new int[size];
}
~stack()
{
delete[] arr;
}
};
2.当类使用了其他系统资源,如文件、网络连接、数据库连接等:在析构函数中关闭或释放这些资源,以确保资源的正确释放和程序的稳定性。
cpp
//FileHandler 类用于处理文件操作
class FileHandler
{
private:
// 用于文件操作的流对象
std::fstream f;
public:
//构造函数,根据给定的文件名打开文件
//filename 要打开的文件名
FileHandler(const std::string& filename)
{
f.open(filename);
}
//析构函数,关闭已打开的文件
~FileHandler()
{
f.close();
}
};
4.拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数是构造函数的一个重载。
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。(疑难解答有详讲)
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用其拷贝构造。
- 像某些类成员变量全是内置类型且没有指向资源,编译器自动生成的拷贝构造就可以完成所需的拷贝,所以无需我们显式实现拷贝构造。但如果类中有成员指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合需求,就需要我们自己实现深拷贝(对指向的资源也进行拷贝)。这里还有一个小技巧,如果一个类显式实现了析构并释放资源,那么就需要显式写拷贝构造,否则就不需要。
举个例子:
cpp
#include <iostream>
class MyClass
{
private:
int num;
public:
// 自定义构造函数
MyClass(int n)
{
num=n;
}
// 拷贝构造函数
MyClass(const MyClass& other)
{
num = other.num;
//拷贝构造函数无返回
}
void printNum()
{
std::cout << "当前数字: " << num << std::endl;
}
};
int main() {
MyClass num1(798);
MyClass num2(num1); // 调用拷贝构造函数
num1.printNum();
num2.printNum();
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放到第一个形参位置就可以了,第二个形参位置当类类型对象。
下面我们来写一个日期类的重载运算符'-'。思路很简单,将两个日期分别变成天,然后相减取绝对值(写的时候没预料到日期转换成天的代码还挺长,没有封装,大家将就一下吧):
cpp
#include<bits/stdc++.h>
using namespace std;
int daymaxp[13]={-1,31,28,31,30,31,30,31,31,30,31,30,31};//平年每个月的天数
int daymaxr[13]={-1,31,28,31,30,31,30,31,31,30,31,30,31};//闰年每个月的天数
class Date
{
public:
Date(int a,int b,int c)//构造函数
{
_year=a;
_month=b;
_day=c;
}
int operator -(Date date)
{
int day1=0;//将第一个日期换算为天
int day2=0;//将第二个日期换算为天
for(int i=1;i<=_year;i++)
{
if((_year%4==0&&_year%100!=0)||(_year%400==0))//闰年
{
day1+=366;
}
else//平年
{
day1+=365;
}
}
for(int i=1;i<_month;i++)
{
if((_year%4==0&&_year%100!=0)||(_year%400==0))//闰年
{
day1+=daymaxr[i];
}
else
{
day1+=daymaxp[i];
}
}
day1+=_day;
for(int i=1;i<=date._year;i++)
{
if((date._year%4==0&&date._year%100!=0)||(date._year%400==0))//闰年
{
day2+=366;
}
else//平年
{
day2+=365;
}
}
for(int i=1;i<date._month;i++)
{
if((date._year%4==0&&date._year%100!=0)||(date._year%400==0))//闰年
{
day2+=daymaxr[i];
}
else
{
day2+=daymaxp[i];
}
}
day2+=date._day;
return (day1-day2)>0?day1-day2:day2-day1;
}
void print()
{
cout<<_year<<"年"<<_month<<"月"<<_day<<"日";
}
private:
int _year;
int _month;
int _day;
};
main()
{
Date day1(2024,8,14);
Date day2(2024,7,13);
day2.print();
cout<<"和";
day1.print();
cout<<"相差"<<day2-day1<<"天";//this指针指向'-'左边的数据
}
运行结果:
总结
默认成员函数 :
默认成员函数是在类中,如果没有显式定义某些特定函数时,编译器会自动生成的函数。包括默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。理解它们的默认行为以及何时和如何自定义这些函数对于正确管理类的对象的创建、复制、销毁等操作至关重要。
构造函数 :
构造函数用于在创建对象时进行初始化工作。可以有多个重载形式,以适应不同的初始化需求。它确保对象在创建时处于一个有效的初始状态。
析构函数 :
析构函数在对象销毁时被自动调用,用于释放对象在其生命周期中获取的资源,进行必要的清理工作。
拷贝构造函数 :
拷贝构造函数用于以一个已存在的同类型对象来创建并初始化新对象,实现对象的复制。需要注意正确处理资源的复制,避免浅拷贝导致的问题。
重载运算符 :
通过重载运算符,可以为自定义类赋予与内置类型类似的运算行为,使代码更具可读性和自然性。重载时需要遵循一定的规则,如参数数量与对应内置运算符的运算对象数量一致,不能创建新的运算符等。同时,要根据类的实际需求和语义来决定哪些运算符需要重载以及如何重载。
疑难解答
1.this指针的用法
this
指针指向的就是当前正在操作的对象的地址。通过this
指针,成员函数可以访问和操作当前对象的成员变量和其他成员函数。
this
指针是 C++ 类中一个非常重要且特殊的概念,以下为您更详细地解释其作用和用法:作用:
解决成员变量和局部变量同名冲突:在类的成员函数内部,如果存在与成员变量同名的局部变量,使用
this
指针可以明确地指定要操作的是成员变量。作为函数返回值:使得成员函数能够返回调用它的对象本身,从而支持链式编程风格,让代码更加简洁和直观。
在成员函数中传递对象的地址:某些情况下,需要在成员函数内部将当前对象的地址传递给其他函数或进行一些与对象地址相关的操作,这时可以使用
this
指针。
用法:
(1)访问成员变量:
当成员函数内部需要访问类的成员变量时,可以使用 this->成员变量名
的方式来明确指定要操作的是类中的成员变量,而非同名的局部变量。
例如:
cpp
class MyClass
{
private:
int value;
public:
void setValue(int value)
{
this->value = value; // 这里使用 this 指针明确是成员变量 value
}
};
(2)作为返回值:
cpp
class Counter
{
private:
int count;
public:
Counter& increment()
{
count++;
return *this; // 返回当前对象的引用
}
};
Counter c;
c.increment().increment(); // 链式调用,count每次+1
(3)传递对象地址:
cpp
class SomeClass
{
public:
void process()
{
anotherFunction(this); // 将当前对象的地址传递给另一个函数
}
};
值得一提的是,C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
2.为什么拷贝构造函数的第一个参数必须是类类型对象的引用?
当我们需要创建一个新的对象,并使用现有的对象来初始化这个新对象时,就会调用拷贝构造函数。我们先来看个例子:
cpp
//拷贝构造函数
Date (Date d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
int main()
{
Date d1(2030,1,1);
Date d2(d1);
d2.Print();
}
在main函数中执行Date d2(d1);时,就会出现无限递归的情况。
这是因为在拷贝构造函数的参数传递过程中,默认是按值传递。也就是说,当您传递 d
到拷贝构造函数时,实际上会调用拷贝构造函数来创建这个参数 d
,而在创建这个参数 d
的过程中又会调用拷贝构造函数,如此就形成了无限递归。
如图:
实在不理解也没关系,记住自己写拷贝构造函数时,参数为类类型的引用即可。
3.函数返回引用的注意事项
传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象在当前函数结束后还存在,才能用引用返回。
这个理解起来不难,下面我给你举个例子你就懂了。
特别注意!!!
给出的这三个函数只是普通的函数,不是拷贝构造函数!
当使用拷贝构造函数创建新对象时,系统会自动在内部处理对象的创建和成员的复制,不需要通过返回值来获取结果。 所以在定义拷贝构造函数时,不需要也不能指定返回值。
它们分别以传值和传引用的方式返回一个 Number
类型的对象。只是通过这两个函数的示例,来说明传值返回和传引用返回在对象复制方面的不同行为和可能出现的问题。
假设我们有一个类 Number
,用于表示数字。
cpp
class Number {
public:
int value;
Number(int val)
{
value=val;
}
};
然后我们有两个函数,一个使用传值返回,一个使用传引用返回:
cpp
Number createNumber() {
Number num(798);
return num; // 传值返回,会创建临时对象并调用拷贝构造函数
}
Number& createNumber() {
Number num(798);
return num; // 错误!返回局部对象的引用,函数结束后 num 被销毁,产生野引用
}
一般情况下我们直接返回值就行了。不过,当需要避免不必要的对象拷贝以提高性能,或者需要在多个函数调用之间共享和修改同一个对象时,返回引用也是有用的,那就是让这个局部变量变为静态对象,代码如下:
cpp
Number& CreatNumber() {
static Number num(798); // 使用静态对象,函数结束后不会销毁
return num;
}
4.关于'++''--'运算符的前置后置重载
在 C++ 中,当重载 ++
运算符时,存在前置 ++
和后置 ++
两种情况。
前置 ++
表示先增加对象的值,然后返回增加后的值。例如,如果有一个对象 obj
,执行 ++obj
就是前置 ++
操作。后置 ++
则是先返回对象当前的值,然后再增加对象的值。比如,对同一个对象 obj
执行 obj++
就是后置 ++
操作。
由于这两种操作的逻辑不同,所以需要通过不同的重载函数来实现。但问题是,它们的函数名都只能是 operator++
,这就导致无法直接通过函数名来区分。为了解决这个问题,C++ 规定,后置 ++
重载时,增加一个 int
类型的形参。这个形参实际上不会被使用,只是用于和前置 ++
重载函数进行区分。
模板如下:
cpp
class MyClass {
public:
MyClass& operator++() { // 前置 ++ 重载
// 增加对象内部状态的相关操作
return *this;
}
MyClass operator++(int) { // 后置 ++ 重载,通过 int 形参与前置 ++ 区分
MyClass temp = *this;
// 增加对象内部状态的相关操作
return temp;
}
};
学会了?那我们来实现一个对于日期类的++吧,下面是作者的代码。什么?为什么没有写后置++?当然是留给你练手了!差别不大,注意一下参数和返回值就行。
cpp
#include<bits/stdc++.h>
using namespace std;
int daymaxp[13]={-1,31,28,31,30,31,30,31,31,30,31,30,31};//平年每个月的天数
int daymaxr[13]={-1,31,28,31,30,31,30,31,31,30,31,30,31};//闰年每个月的天数
class Date
{
public:
Date(int a,int b,int c)//构造函数
{
_year=a;
_month=b;
_day=c;
}
Date& operator++()//前置++重载
{
_day+=1;
if((_year%4==0&&_year%100!=0)||(_year%400==0))//是闰年
{
if(_day>daymaxr[_month])//超出当前月份最大天数
{
_month+=1;
_day=1;
if(_month>12)//超过12月
{
_year+=1;//年份加一
_month=1;//月份变为1月
}
}
}
else
{
if(_day>daymaxp[_month])
{
_month+=1;
if(_month>12)
{
_year+=1;
_month=1;
}
}
}
return *this;
}
void print()//打印函数
{
cout<<_year<<"年"<<_month<<"月"<<_day<<"日"<<endl;
}
private:
int _year;
int _month;
int _day;
};
main()
{
Date today(2024,12,31);
Date tomorrow=++today;
tomorrow.print();//打印明天
today.print();//由于是前置++,此时today也变大了一天
}