
上节补充,定义在类中的成员函数默认为inline。
一、类的默认成员函数
程序员不实现,编译器在某种条件下自己在类中生成的函数。程序员一旦手动实现了某个默认函数,编译器就会停止生成该默认函数。C++11之前有6个默认成员函数:构造函数(初始化)、析构函数(清理)、拷贝构造函数(用已创建的对象初始化新的对象)、赋值运算符重载(把一个已经创建的对象赋值给另一个已经创建的对象)、普通取地址运算符(取完地址后可读可写)、const取地址运算符(取完地址后只读)。
二、构造函数
(1)概念
构造函数是类中默认成员函数的一种。作用是完成初始化工作。(例如给对象的属性赋值,申请内存)
(2)注意
①语法
函数名与类名相同。无返回值。(连void都不用写)
cpp
#include<iostream>
using namespace std;
class Data {
private:
int _year;
int _month;
int _day;
public:
Data();
void Print();
};
Data::Data() {//实现无参的构造函数
_year = 1946;
_month = 2;
_day = 14;
}
void Data::Print() {
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
int main() {
Data data1;//无参的对象实例化
data1.Print();
}
②实例化
cpp
int main() {
Data data1(2026, 3, 20);//找不到对应的构造函数就会报错
}
对象实例化会自动调用对应的构造函数,假如说我并没有手动实现带参的构造函数。那么我偏要带参实例化,自动调用找不到就会报错。
③默认
如果你没有手动定义一个构造函数,编译器就会自动生成一个无参的默认构造函数。你定义了,编译器就不会再生成了。
cpp
#include<iostream>
using namespace std;
class Data {
private:
int _year;
int _month;
int _day;
};
int main() {
Data data1;//无参实例化,编译器自动生成无参的构造函数
Data data2(2026, 3, 20);//编译报错,有参实例化,编译器不会自动生成有参的构造函数
}
④辨析构造函数和默认构造函数
也就是说只要满足函数名是类名,没有返回值的就是构造函数。默认构造函数是它的子集,指的是不需要传递实参的构造函数。(不管你是编译器默认生成的无参默认构造函数还是你自己写的无参构造函数、全缺省构造函数都是默认构造函数)
辨析:
不需要传递实参" ≠ "没有形参
不需要传递实参:全缺省函数 + 无参函数
没有形参:无参函数
注:大多数情况下默认构造函数都要我们自己实现。
⑤重载
构造函数支持函数重载。
三、析构函数
(1)概念
构造函数是类中默认成员函数的一种。作用是完成清理工作------归还资源(比如释放堆上申请的内存,关闭打开的文件等)。
(2)注意
①语法
析构函数的函数名是在类名前面加上字符~;无参数无返回值。(不需要写void)
②函数重载
析构函数在一个类中只能存在一个,不支持函数重载。(析构函数语法规定它无参就注定不支持函数重载)
③调用
你不实现析构函数。编译器会在对象的生命周期结束后,自动调用默认生成的析构函数。你实现了,就自动调用你写的。(相比C语言的一大优点,不再需要手动释放内存,只需要在类中写好实现)
④要不要手动实现析构函数
没有手动申请资源就不需要手动写析构函数。
⑤析构顺序
C++中规定,一个局部域中的多个对象。后定义的先析构。
四、拷贝构造函数
(1)概念
拷贝构造函数是一个特殊的构造函数(是构造函数的重载)。在构造函数的基础上要求第一个参数必须是自身类类型的引用,若存在其它参数。则必须有缺省值。
(2)作用
拷贝构造函数的作用是复制已有对象的属性,没有对象就没办法使用。可以复制的一模一样,(只有一个引用参数)也可以在复制的过程中改变值(拷贝构造函数中存在额外的参数)。
cpp
#include<iostream>
using namespace std;
class Num {
private:
int _num;
public:
Num(int num);
Num(const Num& num,int add = 2);
};
Num::Num(int num) {//构造函数
_num = num;
cout << "拷贝前_num = " << _num << endl;
}
Num::Num(const Num& num,int add) {//拷贝构造函数
_num = num._num + add;
cout << "拷贝后_num = " << _num << endl;
}
int main() {
Num n1(10);//n1._num = 10
Num n2 = n1;//n2._num = 10(这里是传0为参数的意思)
Num n3 = Num(n1, 5);//n3._num = 15(这里是传5为参的意思)
}
(3)注意
①无限递归
拷贝构造函数的第一个参数必须是自身类类型的引用。如果传值就会引发无限递归。
cpp
Num::Num(Num num,int add) {//拷贝构造函数
_num = num._num + add;
cout << "拷贝后_num = " << _num << endl;
}
形参是实参的一份临时拷贝。当你传值时会进行拷贝,又会再次调用拷贝构造函数,就会一直这样调用下去。造成无限递归。
核心原因 :C++规定自定义类型进行拷贝行为必须调用拷贝构造。
②默认构造函数
程序员未定义拷贝构造函数,编译器会自动生成一个只有引用参的默认构造拷贝函数(没有其它参数)。这个默认拷贝构造函数只能完成浅拷贝(一字节一字节的拷贝,对内置类型可以成功拷贝)。
cpp
using namespace std;
class Num {
private:
int _num;
public:
Num();
void Print();
};
Num::Num() {//构造函数
_num = 10;
}
void Num::Print() {
cout << _num << endl;
}
int main() {
Num n1;
n1.Print();
Num n2 = n1;//没有写构造拷贝函数,依旧会调用默认拷贝函数
n2.Print();
}
如果是自定义类型,以指针为例,就只能完成地址的拷贝。最后在析构时,会造成对同一块内存连续释放两次。造成程序崩溃。
③什么时候写
如果只存在内置类型的类属性,就不需要写构造拷贝函数;存在自定义类型或者在堆区上申请空间就需要写构造拷贝函数。
一般来说一个类实现了析构就需要写拷贝构造函数。
④普通函数传参
对于我们所写的一个函数只要是值返回,就会调用拷贝构造函数把函数里的返回值复制一份给main函数。
而引用返回就不会存在拷贝,如果是函数内部的引用返回就会造成非法访问,函数运行结束后栈帧就销毁了。所以在使用引用返回时必须保证它返回的不是内部引用
五、赋值运算符重载
(1)运算符重载
①基本概念
运算符重载是具有特殊名字的函数。指当运算符被用于对象属性的运算时,我们可以通过自定义运算符的方式进行正常的运算。如何没有定义就会编译报错。
cpp
#include<iostream>
using namespace std;
class Calc {
private:
int _num;
public:
Calc(int num);
void Print();
};
Calc::Calc(int num) {
_num = num;
}
void Calc::Print() {
cout << "_num" << endl;
}
int main() {
Calc a(2);
Calc b(3);
Calc c = a + b;//编译报错,对象属性的运算必须用运算符重载
}
②语法
cpp
返回值类型 operator运算符(参数列表){函数体}
cpp
双目运算符的参数列表:(const 类型& 右侧对象)const;
运算符重载作为类属性的运算时,默认第一个参数是当前对象的this指针(左侧对象)。括号后面的const就是用来修饰这个this指针的,因为我不期望我运算的数字在运算之前改变。右侧的对象也是同理,这里传引用,是因为避免了传值产生的临时拷贝,提高效率。
cpp
int Calc::operator+(const Calc& right)const {
return _num + right._num;
}
③注意
Ⅰ.运算符重载后,其优先级和结合性和原运算符一致
Ⅱ.不能通过原来没有的运算符进行运算符重载:¥
Ⅲ.5个不能被重载的运算符:.* :: sizeof ? : .
Ⅳ.为了区分前置++和后置++;前置++重载时无参数;后置++重载时默认传一个int
(2)赋值运算符重载
①概念
默认成员函数之一,作用是完成对于两个已经初始化的对象进行拷贝工作。核心是对运算符重载 = 的实现,只能作为类的成员函数。区别于构造拷贝函数,构造拷贝函数是用已经初始化后的对象对新的刚创建的对象进行初始化拷贝。
②返回值和const修饰
赋值运算符重载有返回值。核心是为了保证可以对对象进行连续赋值。返回值类型为引用,核心是为了减少拷贝次数,提高效率。
const修饰右值,保证右值不能在函数内部改变。
cpp
#include<iostream>
using namespace std;
class Calc {
private:
int _num;
public:
Calc(int num);//构造函数
void Print();//打印
Calc& operator=(const Calc& right);
};
Calc::Calc(int num) {
_num = num;
}
void Calc::Print() {
cout << _num << endl;
}
Calc& Calc::operator=(const Calc& right) {//有返回值,满足连续赋值
_num = right._num;
return *this;
}
int main() {
Calc num1(1);
Calc num2(2);
Calc num3(3);
//num2.Print();
num3 = num2 = num1;
num2.Print();
num3.Print();
}
③什么时候需要实现
程序员不实现赋值运算符重载编译器会自动生成一个。但是只能完成浅拷贝,对于有资源申请的往往需要我们自己写。可以看如果写了析构,一般就要写构造拷贝函数和赋值运算符重载。
六、取地址运算符重载
(1)const成员函数
用const修饰的成员函数。作用是保证成员函数内部不能修改类的属性。const放到参数列表的后面。
(2)取地址运算符重载
默认成员函数之一。作用是对这个类进行取地址。分为普通取地址运算符重载和const运算符重载。
对于普通的取地址运算符重载函数,再取完地址后我们可以改变其对象属性的值(这个对象是可读可写的)。
对于const修饰的取地址运算符重载函数,再取完地址后我们不能改变其对象属性的值(这个对象是只读的)。
一般这两个函数用编译器默认生成的就够了。除非你不想得到当前类的地址,就可以自定义任意的地址。
普通地址运算符重载
cpp
#include<iostream>
using namespace std;
class Data {
public:
Data(int x);
void Print();
void revise(int val);
private:
int _val;
};
Data::Data(int val) {
_val = val;
}
void Data::Print() {
cout << _val << endl;
}
void Data::revise(int val) {
_val = val;
}
int main() {
Data val(10);
val.Print();
Data* p = &val;
p->revise(20);//合法
p->Print();
}
const地址运算符重载
cpp
#include<iostream>
using namespace std;
class Data {
public:
Data(int x);
void Print()const;//想要用const取地址符访问,这里必须写const,是向编译器承诺我只读不写
void revise(int val);
private:
int _val;
};
Data::Data(int val) {
_val = val;
}
void Data::Print()const {
cout << _val << endl;
}
void Data::revise(int val) {
_val = val;
}
int main() {
Data val(10);
val.Print();
const Data* p = &val;
p->revise(20);//不允许修改数据
p->Print();
}