✨ 链接1:C++类和对象(上)
✨ 链接2:C++基础知识 (命名空间、输入输出、函数的缺省参数、函数重载)
✨ 链接3:C++基础知识 (引用)
✨ 链接4:C++基础知识(inline、auto、nullptr)
如果一个类中什么成员都没有,简称空类。当类中什么都不写的时候,编译器会自动生成6个默认成员函数。
const
对象取地址构造函数是一个特殊的成员函数,名字与类名相同,创建类的时候编译器会自动调用类的构造函数,来做类的初始化操作,在对象整个生命周期内只会调用一次。
构造函数的特征:
example1:
#include
using namespace std;
class Date {
public:
// 无参的构造函数
Date() {
cout << "Date()" << endl;
}
// 带参的构造函数
Date(int year , int month , int day) {
cout << "Date(int year , int month , int day)" << endl;
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用无参的构造函数
Date d2(2023 , 8 , 6); // 调用带参的构造函数
return 0;
}
example2:
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() {
Date d1; // 调用编译器自动生成的默认构造函数
return 0;
}
ps:
如果当创建类的时候没有合适的默认构造函数会报错。
在C++中把类型分为内置类型和自定义类型,内置类型就是语言本身自带的类型,如:int
、double
、char
等等。自定义类型就是 class
、struct
、union
等等。默认构造函数并不会对内置类型进行处理,比如用日期类来举例:_year
、_month
、_day
依然是随机值,但是默认的构造函数会对自定义类型的成员处理并调用它们的默认构造函数。
example3:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
这是C++当中比较不好理解的一个点,因为默认的构造函数只会处理自定义类型而不会处理内置类型,所以在C++11中,针对内置类型成员不初识化的缺陷给出了解决方案。内置类型成员变量在类中声明时可以给默认值。
example4:
class Date {
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};
ps:
这里的 =
并不是初始化值的意思,而是声明。这样当调用构造函数的时候如果在构造函数中初始化则会使用构造函数的初始化的结果,若没有则使用当前的默认值。总结一句话就是这样的操作也可以为内置类型初始化了。
默认构造函数的含义: 无参的构造函数 和 全缺省的构造函数 还有 没写编译器自动生成的构造函数 都称为默认构造函数,并且默认构造函数只能有一个。
析构函数与构造函数的功能相反,析构函数不是完成对象本身的销毁,这些是由生命周期结束时编译器做的。而在对象销毁时会自动调用析构函数,完成对象当中资源的清理工作。 比如类中的成员有动态开辟的内存,当类销毁时使用析构函数释放内存还给操作系统。
析构函数的特征:
~
example5:
#include
using namespace std;
class Date {
public:
// 无参的构造函数
Date () {
cout << "Date" << endl;
}
// 析构函数
~Date() {
cout << "~Date()" << endl;
}
private:
int _year = 2023;
int _month = 8;
int _day = 6;
};
int main()
{
Date d;
return 0;
}
编译器自动生成的析构函数会做什么事情呢?
构造函数和析构函数都是只对自定义类型处理。内置类型的成员,销毁时不需要资源的清理,最后系统直接回收即可。对于自定义类型,当前类的默认析构函数会调用这个自定义类型的析构函数来销毁。
example6:
#include
using namespace std;
class Test {
public:
Test(int order) {
_order = order;
cout << _order << " Date" << endl;
}
~Test() {
cout << _order << " ~Date()" << endl;
}
private:
int _order;
};
Test t1(1);
/*
构造顺序:1 2 3 4 5
析构顺序:3 2 5 4 1
*/
int main() {
Test t2(2);
Test t3(3);
static Test t4(4);
static Test t5(5);
return 0;
}
ps:
构造函数的顺序比较简单,因为在对象实例化时就会调用构造函数完成初始化。所以是从上往下依次构造。而析构函数在是对象的生命周期结束的时候调用,这里可以用栈来理解,先创建的就先压栈,所以析构的时候是先析构栈顶的对象,所以这里是创建的早就析构的晚,创建的晚就析构的早,而 static
在静态区中所以当出了 main
函数作用域中,局部对象优先被析构,其次是 static
对象再是全局对象(栈帧和栈里面的对象都要符合后进先出,也就是后定义先析构)。
拷贝构造的含义是:使用已经存在的类对象创建新对象时编译器会自动调用类的拷贝构造。
拷贝构造函数的特征:
ps:
因为如果是传值调用,那么当前类对象会拷贝一份给形参,这里又会调用形参的拷贝构造函数。而拷贝构造函数又要先传参,传参就调用拷贝构造,所以就引发了无穷的递归调用。example7:
#include
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 最好使用 const 修饰
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
// 拷贝构造
Date d2(d1);
// 拷贝构造
Date d3 = d2;
return 0;
}
ps:
当成员中有指向动态开辟内存的指针,如果使用浅拷贝,那么两个指针将会指向同一块内存空间,当修改其中一个指针里的数据时,另一个指针里数据也会被影响。当对象声明周期结束用析构函数的时候,会造成 free
两次的情况导致运行崩溃。拷贝构造函数的调用场景:
运算符重载是为了让一些自定义类型也可以使用运算符,有较强的可读性。
语法:返回值 operator操作符(参数列表)
特性:
this
.*
、::
、sizeof
、?:
、.
以上操作符不能重载ps:
前置++和后置++运算符重载区分方法,带一个形参 int
的为后置++。前置++和后置++是运算符重载,而这两个运算符重载函数又构成了函数重载。
type operator++();
type operator++(int);
将一个对象赋值给另一个对象时,就需要赋值重载。
const Type&
传递引用可以减少拷贝构造提升效率Type&
返回引用也可以减少拷贝构造的次数,而且有返回值可以支持连续赋值example8:
#include
using namespace std;
class Date1 {
public:
// 构造函数
Date1(int year = 2023 , int month = 8 , int day = 12) {
_year = year;
_month = month;
_day = day;
}
// 赋值拷贝
Date1& operator=(const Date1& d) {
// 若当前两个对象地址不同则赋值
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date1 d1;
Date1 d2(2024 , 8 , 12);
// 赋值重载
d1 = d2;
return 0;
}
ps:
赋值运算符只能重载成类的成员函数不能重载成全局函数,因为赋值运算符如果不显示实现,编译器会生成一个默认的,如果在类外实现一个全局的运算符重载,就会和编译器默认生成的赋值重载冲突。
若用户没有显示定义,编译器会生成一个默认的赋值运算符重载,是以浅拷贝的方式拷贝。内置类型是直接按照逐字节的拷贝方式,而自定义类型会调用对应类的赋值运算符的重载完成赋值。
将 const
修饰的成员函数叫做 const
成员函数,const
修饰类成员函数,实际是修饰该成员函数的 this
指针,意思是 this
指向的内容不能被修改。
example9:
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() const {
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
return 0;
}
ps:
由于 d2
是 const
修饰的对象,当 d2.Print()
实际上是 d2.Print(&d2)
,第一个参数会有一个隐含的 this
指针,而this
指针的类型是 Date * const this
,所以一个 const
的地址传给非 const
修饰的指针时,这是权限的放大所以会 error
,解决办法是在成员函数后加上 const
修饰。这里实际上是 const Date * const this
。
example10:
class Date {
public:
Date* operator&() {
return this;
}
const Date* operator&()const {
return this;
}
};
ps:
这两个运算符重载函数构成函数重载,这两个函数都要存在才可以,因为如果没有 const
修饰的取地址重载,虽然普通对象和 const
对象都可以接收,但是当返回的时候就有问题了,如果接收返回值的是普通对象,那你返回一个 const
的地址这里造成了权限的放大 error
,所以这里针对 const
和非 const
要采取两种不同的方式。
但是这两个函数一般情况不用自己定义,编译器会自动生成。