C++类和对象(中)
Hello,大家好,今天,我们来继续学习类和对象部分的知识。
Hello,大家好,今天,我们来继续学习类和对象部分的知识。
目录
1.类中的默认成员函数
默认成员函数就是用户没有显示实现,编译器会自动生成的成员函数,我们将其称为是默认成员函数。一般来说,类在不写的情况下会默认生成6个默认成员函数(构造函数,析构函数,拷贝复制,赋值重载,普通对象取地址,const对象取地址),其中前4个极为重要,要深入学习,其次,C++11中又增加了2个默认成员函数(移动构造,移动赋值),后面会说到。
2.构造函数
构造函数时特殊的成员函数,它虽然名字是叫构造,但它其实上不是开创空间,而是对象实例化时的初始化对象,换句话说,就是构造函数的本质就是替代我们之前所写的 Init 函数的功能,它的自动调用完美地替代了Init。
2.1构造函数的特点
(1).函数名和类名相同。
(2).无返回值。(返回值啥都不用写,包括void,不用纠结,C++规定)
(3).对象实例化时编译器会自动调用对应的构造函数。
(4).构造函数可以重载。(同时定义两个同名的构造函数)
#include<iostream>
using namespace std;
class date1
{
public:
// 1.无参构造函数
date1()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
date1(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;
};
class date2//这里之所以再次写一个类,是因为无参构造函数和全缺省构造函数这两个函数不能同时出现,因此,定义两个类,将这两个函数分隔开
{
public:
// 3.全缺省构造函数
date2(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()
{
date1 d1;// 当我们的程序走到这里的时候,系统会自动调用对应的构造函数,也就是说,当我们实例化一个对象以后,系统系统会自动调用对应的构造函数(这里指的是无参构造函数),注:我们在调用无参构造函数的时候d1后面不能加(),否则,系统会报错,因为分不清这是调用某个函数还是初始化
d1.print();// 1 1 1
date1 d2(2024, 8, 7);// 这里调用的是带参构造函数
d2.print();// 2024 8 7
date2 d3(2024, 8);// 这里调用的是全缺省构造函数
d3.print();// 2024 8 1
return 0;
}
(5).如果类中没有显示定义构造函数,那么C++编译器会自动生成一个无参的默认构造函数,一旦用户显示构造函数则编译器就不会再生成默认构造函数。
(6).无参的构造函数,全缺省构造函数,我们不写构造函数时编译器默认生成的函数,都叫默认构造函数,这三个函数只能有一个存在,不能同时存在,要注意很多同学会认为默认构造函数就只是编译器默认生成的那个函数叫默认构造函数,这个认为不全面,总结一下:不传实参就可以调用的构造函数叫做默认构造函数。
补充:C++把类型分为内置类型(基本类型)和自定义类型。
1).内置类型:语言提供的原生数据类型,如:int / char / double / float / 指针等。
2).自定义类型:我们平时使用的 class / struct等关键字自己定义的类型。
(7).我们不写,编译器默认生成的构造函数对内置类型成员变量的初始化没有要求,也就是是否初始化是不确定的,这个得看编译器,对于自定义类型,编译器会自动调用这个自定义类型成员变量的默认构造函数初始化,如果有默认构造函数,则调用。
#include<iostream>
using namespace std;
class stack2
{
public:
stack2()
{
_year = 2024;
_month = 8;
_day = 28;
}
private:
int _year = 2;
int _month = 2;
int _day = 2;
};
class stack1
{
stack2 st2;
};
int main()
{
stack1 st;//由于我们在这里没有写satck1的构造函数,所以,系统在这里就会自动生成一个tsack1的默认构造函数,而生成的这个默认构造函数会直接调用stack2的默认构造函数。
return 0;
}
如果这个成员变量(这里指的是stack2类型的对象st2,紧接上述代码),没有默认构造函数,那么就会报错,我们要想初始化这个成员变量,就要使用初始化列表才可以解决这个问题,这个初始化列表下一节会讲。
总结:大多数情况下,构造函数都需要我们自己去实现,少数情况类似stack1且stack2有默认构造函数时,stack1自动生成的就可以使用。
3.析构函数
析构函数与构造函数功能相反,但析构函数不是完成对对象本身的销毁,实际上,它是完成对象中资源的清理释放工作。(资源目前指的是堆区的资源)
3.1析构函数的特点
(1).析构函数名是在类前面加上字符~。
(2).无参无返回值。(这里和构造函数类似,也不需要加void)
(3).一个类只能有一个析构函数,若未显示定义,那么系统会自动生成默认的析构函数。
(4).对象生命周期结束时,系统会自动调用析构函数。
#include<iostream>
using namespace std;
class date
{
public:
date()//构造函数
{
_year = 2024;
_month = 8;
_day = 21;
}
~date()//析构函数
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1;
return 0;//当程序运行到这里时,也就是d1对象生命周期结束的时候,编译器就会自动去调用date的析构函数
}
(5). 跟构造函数类似,我们不写,编译器自动生成的那个析构函数不做处理,自定义类型成员会调用它的析构函数。
#include<iostream>
using namespace std;
class stack
{
public:
stack(int n = 4)
{
_arr = (int*)malloc(sizeof(int) * 4);
if (_arr = nullptr)
{
return;
}
_top = 0;
_capacity = n;
}
~stack()
{
free(_arr);
_arr = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _arr;
int _top = 0;
int _capacity = 0;
};
class queue
{
stack st1;
};
int main()
{
queue q1;//当q1生命周期结束的时候,系统会自动为q1生成一个析构函数,这个析构函数会自动调用st1的析构函数
stack s1;
stack s2;
//这里再来给大家普及一个知识,就是s1和s2这两个对象在进行析构的时候是先对s2进行析构操作,然后再对s1进行析构操作,简单来说:就是先创建的后析构。(C++规定)
return 0;
}
(6).如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的那个默认析构函数即可,就比如date类;如果默认生成的析构函数可以使用的话,也就不需要显示的去写析构函数了,如queue类;但是有资源申请时,一定要自己写析构函数,否则,会造成资源泄露,如stack类。
(7).还需要注意的是,如果我们显示写析构函数,对于自定义类型成员也会调用它的析构函数,也就是说,自定义类型成员无论什么情况都会自动调用它的析构函数。
#include <iostream>
using namespace std;
class stack
{
public:
stack(int n = 4)
{
_arr = (int*)malloc(sizeof(int) * 4);
if (_arr = nullptr)
{
return;
}
_top = 0;
_capacity = n;
}
~stack()
{
cout << "~stack()" << endl;
free(_arr);
_arr = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _arr = nullptr;
int _top = 0;
int _capacity = 0;
};
class queue
{
public:
~queue()
{
cout << "~queue()" << endl;
}
private:
stack st;
};
int main()
{
queue q;
return 0;//当程序执行到这里的时候,也就是说明q对象的生命周期到期了,编译器就会自动去调用q的析构函数,会输出~queue()这个字符串,按照我们前面讲的逻辑,接下来,就是应该结束程序的运行了,但是实际上并没有结束,由于q对象中有一个自定义成员变量,因此根据我们上面的讲解,会去调用st的析构函数,因此,还会输出~stack()这个字符串。
}
通过上述编译器所打印出来的结果,我们可以得知自定义类型成员无论什么情况都会自动调用它的析构函数。
(8).一个局部域的多个对象,在执行析构操作的时候,C++规定后定义的先析构。
4.拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值(缺省值),则此构造函数也叫拷贝构造函数,也就是说拷贝构造函数就是一个特殊的构造函数。
4.1拷贝构造函数的特点
(1).拷贝构造函数是构造函数的一个重载。
(2).拷贝构造函数的参数只有一个并且这个参数必须是当前类类型的引用,若使用传值方式的话,则编译器就会直接报错,因为在语法逻辑上就会引发无穷递归调用(如果传值传参的话,就会调用拷贝构造函数,逻辑上会无穷递归下去,结合(3)中的知识点)。
#include <iostream>
using namespace std;
class date
{
public:
date(int year = 1, int month = 1, int day = 1)//默认构造函数
{
_year = year;
_month = month;
_day = day;
}
date(date& d)//拷贝构造函数,注意这里一定要使用引用去进行传值传参操作
{
_year = d._year;
_month = d._month;
_day = d._day;
}//拷贝构造函数其实就是有一个已经初始化好的类类型的对象,将这个类类型的对象中的各个元素全部拷贝到另一个同类型的类类型的对象中。
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024,8,27);
date d2(d1);//将d1中的各个成员变量的值全部拷贝到d2中
return 0;
}
(3).C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
#include <iostream>
using namespace std;
class date
{
public:
date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
date(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;
};
void func(date d2)
{
d2.print();
}
int main()
{
date d1(2024,8,28);
func(d1);//2024 8 28
return 0;
}
接下来,就给大家讲一下这个代码,也就是传值传参会先调用拷贝构造,我们这里首先跟着程序走,先创建一个d1对象,调用func函数,这里我们传d1过去,我们使用的是传值调用,就会自动调用拷贝构造函数,这样的话,d1就会先传给d(这一步是引用传参,因此地址是一样的),这个时候我们先记下this指针的地址0x0000001aa04ffd08,执行完拷贝构造后,就会再次回到主函数刚刚调用func函数的位置,我们再往下进行一步的话,此时才算是真正的进入到func函数中,此时我们再次看d2的地址,会发现d2的地址就是刚刚this指针的地址,这足以说明刚刚进行的拷贝构造函数操作是将d1对象中的数据全部拷贝到d2中,要完成这种效果,就只能重新构造一个同类型的对象,而那个拷贝构造就是创造一个同类型的对象,而创造的这个新的对象就是d2。
(4).若未显示拷贝构造函数,编译器会自动生成拷贝构造函数。自动生成的拷贝构造函数对内置类型成员会完成值拷贝/浅拷贝(一个字节一个字节地去拷贝),对自定义类型成员变量会调用它的拷贝构造函数。
#include <iostream>
using namespace std;
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(2024,8,28);
date d2(d1);
d2.print();//2024 8 28
return 0;
}
(5).像date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造函数就可以完成我们所需要的拷贝操作,所以这种情况下不需要我们显示实现拷贝构造函数。但是像stack这样的类,它虽然也是内置类型,但是_arr指向了资源(堆区资源),编译器自动生成的拷贝构造函数所完成的浅拷贝(一个字节一个字节的去拷贝),这样的拷贝方式很显然不符合我们要求,所以就需要我们自己去实现深拷贝(对指向的资源也进行拷贝操作);像queue这样的类型内部主要是自定义类型stack成员,编译器自动生成的拷贝构造会调用stack的拷贝构造,也不需要我们去显示实现queue的拷贝构造函数。
我们这里使用stack类来展示一下为什么对于有资源的对象,要进行深拷贝,而不能进行浅拷贝。
我们在这里先创建一个stack类类型的对象st1,然后再拷贝构造一个st2对象,将st1拷贝到st2中,
OK,通过上面的这一幅图,我们可以得知将st1中的数据全部拷贝到st2中,就会使st1和st2中的_arr指针都指向同一块空间,这样的话,就会出现许多问题, 编译器再编译的时候就会崩溃,因为这样的话,在生命周期到期的时候,这一块空间总共析构了两次,就会出现越界访问的问题,在第一次析构之后,_arr就是一个野指针,此时在对这一块空间进行析构,就会造成问题。
stack(stack& st)
{
_arr = (int*)malloc(sizeof(int) * st._capacity);//创建一块新的空间
if (_arr == nullptr)
{
return;
}
memcpy(_arr, st._arr, sizeof(int) * st._capacity);//将原来的那块空间中的数据全部拷贝到刚刚开创的这块空间中
_top = st._top;
_capacity = st._capacity;
}
这里再给大家讲一个小技巧:如果一个类显示实现了析构函数并且释放了资源,那么他就需要显示写拷贝构造,否则,就不需要显示的去写拷贝构造函数。
(6).传值返回会产生一个临时对象调用拷贝构造函数,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是,我们如果返回的是一个当前函数局部域的局部对象,函数在结束时就被销毁了,那么我们这里还是使用传引用返回的话,就是有问题的,这时的引用就相当于是一个野引用,类似与我们前面说的野指针一样,传引用返回可以减少拷贝,但是一定要确保返回对象在当前函数结束后还存在,这有这种情况才可以使用引用返回。
stack func1()
{
stack st1;
return st1;//函数结束时返回st1,st1会被拷贝一份,将拷贝的那一份放在一个临时对象中,这个临时对象不在func1()这个局部函数的空间中,这种情况下不可以使用引用返回。
}
stack& func2(stack& st2)
{
return st2;//函数结束时返回st2,st2是传过来的形参,在func2()局部函数到期时,st2不会被销毁,st2的声明周期是全局。因此,这种情况下我们就可以使用传引用返回。
}
stack& func3()
{
static stack st3;
return st3;//st3这个对象被static修饰,因此st3的生命周期就变成了全局变量,它在func3()函数结束时不会被销毁,因此,这种情况下我们也可以使用传引用返回。
}
这里再给大家补充一个东西:就是拷贝构造的写法:
stack st1(2024 8 29);
stack st2(st1);//satck st2=st1;这样写也是调用拷贝构造函数
注意:拷贝构造函数指的是将一个初始化好的对象拷贝给另一个未初始化的对象,它针对的对象是一个已经初始化的和一个未进行初始化的,这个需要我们去注意一下。
5.赋值运算符重载
5.1运算符重载函数
(1).当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用相应的运算符重载,若没有对应的运算符重载,则会编译报错。
(2).运算符重载函数是具有特殊名字的函数,它的名字是由operator这个关键字和后面要定义的运算符共同构成。和其他的函数一样,它也具有其返回类型和参数列表以及函数体。
int operator+(date& d)//operator+是函数名,int是这个函数的返回类型。
{}
(3).运算符重载的函数的参数个数和该运算符作用的运算对象一样多。一元运算符有一个参数(++,--),二元运算符(==,<,>,<=,>=)有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
(4).如果一个重载运算符函数是成员变量,那么它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员变量时,参数比运算符少一个。
#include<iostream>
using namespace std;
class date
{
public:
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int operator+(date& d)
{
return (*this)._year + d._year;//this指针指向的是d1,而d指的是d2的那块空间。
}
int operator++()//这里指的是前置++
{
return ++(*this)._year;//this指向的就是d1。
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
date d1(2024,8,24);
date d2(2018,10,30);
cout << d1.operator+(d2) << endl;//4024,调用运算符重载函数的方法和调用普通函数的方法没有什么区别。
cout << d1.operator++() << endl;//2025
return 0;
}
除此之外,再来给大家讲解另一种调用运算符重载函数的写法:
date d1(2024,6,23);
date d2(2012,8,30);
d1.operator=(d2);//d1=d2,这种写法也是调用运算操作符重载函数operator=,这样的写法更简介。
(5).运算符重载函数以后,其优先级和结合性与对应的内置类型运算符保持一致。
(6).不能通过连接语法中没有的运算符符号来创建新的操作符,如:operator@,语法的运算符符号中没有@这个符号。
(7).".*"、"."、"::"、"sizeof"、"?:"、注意这5个运算符不能重载(面试选择题里可能会有,大家注意一下)。
(8).重载操作符至少要有一个类类型的参数,不能通过运算符重载而改变内置类型对象的含义。
(9).一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如date类重载operator-就有意义,重载operator+就没有意义。
(10).我们在重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,我们无法很好的去区分它们。C++规定,后置++在重载时,增加一个int形参,跟前置++区分开来。
date& operator++()//前置++
{
(*this)._year += 1;
return *this;
}
date& operator++(int)//后置++,传值的时候随便传一个int类型的值就可以。
{
date tmp = *this;
(*this)._year += 1;
return tmp;
}
//前置++和后置++的返回值结果不一样,前置++返回的是++之后的结果,而后置++返回的是++之前的结果
(11).我们在重载<<和>>操作符时,需要重载为全局函数,因为如果重载成成员函数,this指针默认抢占了第一个形参的位置,第一个是左侧运算对象,调用时就要变成对象<<cout;这样很显然是不符合我们的使用的习惯的,同时,也会降低这个代码的可读性,重载为全局函数将ostream/istream放到第一个形参位置就可以了,第二个形参的位置为类类型的对象(这里大家需要注意一下,就是如果我们在这里将<<和>>操作符重载成全局变量了以后,这样就无法访问到类中的私有变量了,此时就需要我们用到我们的友元函数声明一下,这里我们先浅浅的提一下,后面我们会细讲)。
class date
{
public:
//友元函数声明一下
friend ostream& operator<<(ostream& out,const date& d);
friend istream& operator>>(istream& in, date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
istream& operator>>(istream& in, date& d)//传过来的date类型的d不能使用const修饰。
{
in >> d._year >> d._month >> d._day;
return in;
}
//这里需要给大家说明一点,就是istream和ostream类型的对象不能传值,只能传引用(C++规定)。
5.2赋值运算符重载函数
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造函数区分开来,拷贝构造函数用于一个对象拷贝初始化给另一个要创建的对象(关于区分赋值运算符和拷贝构造,后面会再次强调一下)。
5.2.1赋值运算符重载的特点
(1).赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型的引用,否则传值传参会有拷贝,有拷贝就会有性能的降低。
#include<iostream>
using namespace std;
class date
{
public:
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
date& operator=(const date& d)//由于这个是赋值操作符的重载,它的作用就是赋值,根据重载的这个操作符的作用我们就可以判断不会改变传过来的这个对象的中的各个成员变量的值,因此,这里就可以使用const修饰。
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2024,6,23);
date d2(2018,4,28);//要想完成这个赋值操作符这个操作,我们首先就要构造两个已经存在的date类型的变量。
d1.operator=(d2);//d1=d2,写法不同
return 0;
}
(2).有返回值,且建议写成当前类型的引用,引用返回可以减少拷贝,从而提高程序运行的效率,有返回值的目的是为了支持连续赋值的场景。
date d1(2012,4,12);
date d2(2018,5,19);
date d3(2014,8,28);
d1=d2=d3;//有返回值是为了支持像这一步代码一样的连续赋值,首先执行d2=d3这一步操作,返回值是d2,然后再执行d1=d2这一步操作。
(3).没有显示实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝,对自定义类型成员变量会调用他的赋值重载函数。
(4).如果一个类显示实现了析构函数并释放了资源,那么他就需要写赋值运算符重载(实现深拷贝),否则就不需要。
这里我们再来强调一下区分赋值运算符和拷贝构造。
date d1(2012,3,24);
date d2(2024,5,28);
d1=d2;//d1和d2都存在,这里调用的是赋值运算符重载。
date d3(d2);//一个对象(d2)拷贝初始化给另一个要创建的对象(d3),这里调用的是拷贝构造。
6.取地址运算符重载
6.1const成员函数
前情提要:
我们这里定义了一个date类,创建了一个const修饰的date类型的变量d1,调用print函数的时候,编译器报错了,是访问权限的问题,为了防止这种问题的产生,就需要const修饰成员函数。
#include<iostream>
using namespace std;
class date
{
public:
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const date d1(2024,5,13);
d1.print();//2024 5 13
return 0;
}
(2).const虽然是在成员函数参数列表的后面放着,但是const实际上修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
void print(date* const this) const;
void print(const date* const this);
6.2取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们使用了,不需要去显示实现。
class date
{
public:
date* operator&()
{
return this;
}
const date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
OK,以上内容就是我们这一节所要给大家讲述的内容,谢谢大家的支持,你们的支持就是我创作的最大动力。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)