【 C++ 】 类和对象的学习 (二)
😘我的主页:OMGmyhair-CSDN博客
目录
I、类的默认成员函数
一、构造函数
二、析构函数
三、拷贝构造函数
四、
运算符重载
赋值运算符重载
五、取地址重载_普通对象
六、取地址重载_const对象
I、类的默认成员函数
用户没有显示实现,编译器会自动生成的成员函数被称为默认成员函数。
接下来我们将介绍以下6个默认成员函数:
这篇文章将重点介绍前四个,稍微了解后两个,在C++11中还增加了两个默认成员函数——移动构造和移动赋值。
一、构造函数
构造函数的主要功能是对对象实例化时进行初始化,注意不是创建对象,局部对象是在栈帧创建时就一并开好了空间完成了创建的。
构造函数有以下几个点需要注意:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调用时会存在歧义。没有参数就可以调用的构造就叫默认构造。
class Person { public: Person()//无参 { cout << "初始化,无参" << endl; _age = 0; _height = 50; _weight = 20; } Person(int age=0)//全缺省 { cout << "初始化,全缺省" << endl; _age = age; _height = 50; _weight = 20; } int _age; int _height; int _weight; }; int main() { Person pete; return 0; }
此时调用产生歧义,编译器报错:
在调用默认构造函数时,不要加(),编译器会认为是函数声明,而函数中可以放函数声明,不会报错。
7. 我们不写,编译器默认⽣成的构造,对内置类型(例如int,char,double...)成员变量的初始化是不确定的,取决于编译器。对于自定义类型(自己定义的class/struct类型)成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错。
class Person { public: Person() { cout << "create person!" << endl; age = 0; height = 50; weight = 10; } int age; int height; int weight; }; class Family { public: Family() { cout << "create family!" << endl; num = 1; } Person pete; int num; }; int main() { Family one; cout << one.pete.height << endl;//输出50 return 0; }
在最后,程序输出了50,证明会调用自定义类型Person的默认构造函数对成员变量pete进行初始化。
因此当类中的成员变量全都是自定义类型,我们可以使用自动生成的默认构造。当类中成员变量有内置类型时或者需要显示传参初始化,就自己写构造函数。
在C++11中,我们还可以不用默认构造函数对内置类型进行初始化:
class Person { public: int _age = 0; int _height = 0; int _weight = 0; }; int main() { Person pete; return 0; }
二、析构函数
如同构造函数,析构函数也不是对对象进行销毁,对象的销毁是在函数结束栈帧销毁。C++规定对象在销毁时会自动调用析构函数,而析构函数的作用类似于对申请的资源进行释放。这样可以有效避免内存泄漏问题,我们因为忘记释放而造成内存泄漏,因为在析构函数是自动调用的。
析构函数的特点如下:
1.析构函数的函数名是在类名前加上~
2.没有参数没有返回值
3.一个类中只能有一个析构函数。没有显示写明,则编译器会自动生成一个默认的析构函数
4.当对象生命周期结束,系统会自动调用析构函数
5.系统自动生成的析构函数不会处理内置类型成员(int char double),对于自定义的类型成员会调用它们的析构函数。
6.即使你写明的析构函数中没有处理自定义的类型成员,编译器还是会调用这些成员自己的析构函数。即无论在什么情况下,自定义类型成员都会调用它们自己的析构函数。
7.当申请了资源时,一定要自己写析构函数。如果没申请,可以不用写。
8.先定义的对象后销毁,我们可以将这个用栈来理解,它们也遵循先进后出的规则。
我们简单写一个类,来看看析构函数:
class Room
{
public:
Room(int _height=1,int _area=1)
{
cout << "Room构造函数" << endl;
_room = (int*)malloc(sizeof(int) * _height * _area);
}
~Room()
{
cout << "Room析构函数" << endl;
free(_room);
_room = nullptr;
}
private:
int* _room;
int _height;
int _area;
};
int main()
{
Room r1;
return 0;
}
看看运行结果:
可以看到编译器自动调用了构造函数以及析构函数。
在构造函数中,我们申请了资源,在析构函数中自动释放资源。
再来看看下面这种情况:
class House
{
public:
House(int _price=1000,string _name="翻斗花园")
{
cout << "House构造函数" << endl;
}
~House()
{
cout << "House析构函数" << endl;
}
private:
Room _r;
int _price;
string _name;
};
int main()
{
House h;
return 0;
}
可以看到在House的析构函数中,我们没有对自定义Room类型的成员做任何处理,来看看运行结果:
可以看到系统自动调用了Room的析构函数,即使你在House析构函数中没有进行处理。
再来看看House的默认析构函数:
class Room
{
public:
Room(int _height = 1, int _area = 1)
{
cout << "Room构造函数" << endl;
_room = (int*)malloc(sizeof(int) * _height * _area);
}
~Room()
{
cout << "Room析构函数" << endl;
free(_room);
_room = nullptr;
}
private:
int* _room;
int _height;
int _area;
};
class House
{
public:
House(int _price = 1000, string _name = "翻斗花园")
{
cout << "House构造函数" << endl;
}
//此时我们没有写明析构函数
private:
Room _r;
int _price;
string _name;
};
int main()
{
House h;
return 0;
}
再来看看它的运行结果:
可以看到编译器默认生成的析构函数还是对自定义成员的析构函数进行了调用。
三、拷贝构造函数
当一个构造函数的第一个参数是自己类类型的引用,并且其它的参数都有默认值,那么这个构造函数叫做拷贝构造函数。
值得注意的是:C++规定传值传参调用拷贝构造
当我们想要用已存在的对象对别的对象进行初始化时,就可以使用拷贝构造函数:
class Date
{
public:
Date(int year = 2024, int month = 9, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,1,1);
Date d2(d1);
d2.Print();
return 0;
}
在c++中,d2(d1)还可以写成st2=st1这种形式:
int main()
{
Date d1(2024,1,1);
//Date d2(d1);
Date d2 = d1;
d2.Print();
return 0;
}
运行结果如下:
需要注意的是:拷贝构造函数必须的第一个参数必须写成引用的形式,不能使用传参,会造成死循环。
错误写法:
为什么会造成死循环呢?
这时因为如果使用传值的形式,每次调用拷贝构造函数都会先进行传值传参,而C++规定传值传参调用拷贝构造,因此又会形成一个新的拷贝构造函数,从而造成死循环。
如下图所示:
为了避免这种情况,我们可以使用引用来进行对另外一个对象进行拷贝。为了防止在拷贝过程中,被引用对象被误修改,我们建议加上const。
我们有时会用到使用对象进行传值传参,这时也会自动调用拷贝构造函数,这时我们也可以使用引用的形式,这样省去了调用拷贝构造函数这一步:
既然拷贝构造函数也算是默认成员函数,那么当我们不主动去写时,编译器也会生成一个默认的拷贝构造函数。默认的拷贝构造函数会做些什么呢?
我们去掉我们写的拷贝构造函数试试看:
class Date
{
public:
Date(int year = 2024, int month = 9, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
/*Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}*/
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 1);
Date d2(d1);
d2.Print();
return 0;
}
看看运行结果:
可以看到依然完成了拷贝,如果是对自定义类型呢?
class SpecialDay
{
public:
SpecialDay()
{
_date = 20240101;
}
SpecialDay(const SpecialDay& SDay)
{
cout << "SpecialDay的拷贝构造" << endl;
_date = SDay._date;
}
int _date;
};
class Date
{
public:
Date(int year = 2024, int month = 9, int day = 3)
{
_year = year;
_month = month;
_day = day;
SDay._date=20240903;
}
/*Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}*/
void Print()
{
cout << _year << " " << _month << " " << _day << " " << SDay._date << endl;
}
private:
int _year;
int _month;
int _day;
SpecialDay SDay;
};
int main()
{
Date d1(2024, 1, 1);
Date d2(d1);
d2.Print();
return 0;
}
运行结果:
可以看到调用了自定义类型成员自己的拷贝构造函数。
此时有一个问题,当我们传值返回的时候也会调用拷贝构造函数吗?
class Date
{
public:
Date(int year = 2024, int month = 9, int day = 3)
{
_year = year;
_month = month;
_day = day;
SDay._date=20240903;
}
Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
cout << "Date的拷贝构造函数" << endl;
}
void Print()
{
cout << _year << " " << _month << " " << _day << " " << SDay._date << endl;
}
private:
int _year;
int _month;
int _day;
SpecialDay SDay;
};
Date func(const Date& date)
{
return date;
}
int main()
{
Date d1(2024, 1, 1);
/*Date d2(d1);
d2.Print();*/
func(d1);
return 0;
}
在上面示例中,func函数进行了传值返回,是否会调用拷贝构造函数呢?
来看看运行结果:
可以看到它依然调用了拷贝构造函数,这是因为出了func函数体,date会赋值给一个临时对象,此时会调用拷贝构造函数。
此时我们可以使用传引用返回:
Date func(const Date& date)//传值返回,调用拷贝构造函数
{
return date;
}
Date& func1(Date& date)//传引用返回,不调用
{
return date;
}
但是传引用返回需要注意局部对象:
在下面这种情况中,temp是一个局部对象,出了函数体就被销毁,而此时返回的引用就类似于野指针了。
Date& func1(Date& date)
{
Date temp;
return temp;
}
综上所述,我们对拷贝构造函数的特点进行总结:
1.是构造函数的一个重载
2.第一个参数必须是类类型对象的引用
3.C++规定传值传参和传值返回调用拷贝构造函数
4.自动生成的拷贝构造函数会对内置类型成员变量完成值拷贝/浅拷贝,对于自定义类型成员变量会调用它们自己的拷贝构造函数。但是当我们申请了资源时,拷贝构造函数也需要自己写去进行深拷贝。
四、
运算符重载
当我想要对类的对象进行加减乘除等操作,可以直接使用运算符对对象进行操作吗?
不可以,但是C++中允许我们使用运算符重载的方式让运算符能用于对象的操作中。 * 就是一个典型的运算符重载,它能是乘号也能是解引用。
在这里我们定义一个日期类:
class Date
{
public:
Date(int y=2024,int m=9,int d=2)
{
_year = y;
_month = m;
_day = d;
}
int _year;
int _month;
int _day;
};
当我们想要比较两个日期是否相等,应该怎么做?
class Date
{
public:
Date(int y=2024,int m=9,int d=2)
{
_year = y;
_month = m;
_day = d;
}
int _year;
int _month;
int _day;
};
bool operator==(Date d1, Date d2)
{
return d1._day == d2._day
&& d1._month == d2._month
&& d1._year == d2._year;
}
int main()
{
Date date1(1949, 10, 1);
Date date2(2024, 10, 1);
if (operator==(date1, date2))
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}
return 0;
}
在C++中operator==(date1,date2)还可以写成date1==date2。
此时你可能注意到了一个问题,成员变量我们一般设为私有,但是私有不能在类外进行访问。
我们通常有三种方法进行解决。
1.将其设为公有(不推荐)
2.提供get函数
class Date
{
public:
Date(int y=2024,int m=9,int d=2)
{
_year = y;
_month = m;
_day = d;
}
int GetY()
{
int year = _year;
return year;
}
int GetM()
{
int month = _month;
return month;
}
int GetD()
{
int day = _day;
return day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(Date d1, Date d2)
{
return d1.GetD() == d2.GetD()
&& d1.GetM() == d2.GetM()
&& d1.GetY() == d2.GetY();
}
int main()
{
Date date1(1949, 10, 1);
Date date2(2024, 10, 1);
/*if (operator==(date1, date2))
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}*/
if (date1==date2)
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}
return 0;
}
3.将重载函数变为成员函数
class Date
{
public:
Date(int y = 2024, int m = 9, int d = 2)
{
_year = y;
_month = m;
_day = d;
}
bool operator==(Date d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date date1(1949, 10, 1);
Date date2(2024, 10, 1);
/*if (operator==(date1, date2))
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}*/
/*if (date1 == date2)
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}*/
if (date1.operator==(date2))
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}
return 0;
}
赋值运算符重载
赋值运算符重载是一个默认成员函数,用于两个已经存在的对象直接的拷贝赋值。C++规定必须重载为成员函数。它和拷贝构造函数的区别在于,它是两个已经存在的对象之间,而拷贝构造函数是一个对象拷贝给另外一个初始化的对象。
Date& operator=(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
return *this;
}
赋值运算符重载是有返回值的,this是调用对象的地址,返回对象是为了连续赋值这一场景。参数我们使用const,以防在赋值过程中误处理,参数使用&避免造成传值传参。
与拷贝构造函数类似,自动生成的赋值运算符重载会对内置类型进行值拷贝/浅拷贝,对于自定义类型则会调用它们自己的赋值运算符重载。
一般来说,默认生成的取地址重载函数已经足够用,除非特殊场景,比如你不希望别人通过&取得对象的地址。
五、取地址重载_普通对象
首先我们来看对普通对象进行取地址:
Date* operator&()
{
return this;
}
很简单,我们只需要返回this就行,因为this就是调用对象的地址。
六、取地址重载_const对象
再来看看对const对象进行取地址:
const Date* operator&()const
{
return this;
}
对于const对象的取地址重载,这里多了两个const,我将一一为你解释它们的意义。
首先我们来看第一个const,在这里我们需要返回对象的地址,也就是this,而this所指向的对象是const所修饰的。我们肯定不希望有人通过*this将对象进行修改,而在*的前面加const修饰的是*this也就是对象本身。
再来看看第二个const,首先我们需要知道类的成员函数的参数列表的第一个形参是隐藏的。
比如正常的你认为的成员函数长这样:
其实它长下面这样:
但是你会发现编译器对这个this进行了报错,那是因为C++规定不能在形参和实参中显式写出this,但是我们能在函数体内显式使用this:
当this指向的是const修饰的对象,那么*this也应该被const所修饰,但是我们又不能显式地写出const,所以C++规定这个const就写在成员函数参数列表的后面:
因此在任何不需要修改对象的成员函数的参数列表的后面,我们都可以加上const,以适用于被const所修饰的对象。
如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!😘😊😗💕💕😗😊😘