目录
1.类的6个默认成员函数
如果一个类中什么都没有,也就是我们创建一个空类,我们除了知道它的内存空间大小为一个字节以外,还有没有别的新知识呢。
本篇博客主要介绍的就是,任何一个类,在我们不写的情况下,都会自动生成的6个默认成员函数。
2.构造函数
构造函数是一个特殊的成员函数,函数名和类名相同,创建类类型对象由编译器自动调用,用来保证每个成员函数都有一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数并非是创建对象,而是进行初始化对象。
2.1 构造函数的特点:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时,编译器自动调用对应的构造函数。
4. 构造函数支持重载。
2.2 尝试实现构造函数
举个例子
class Date
{
public:
// 1.无参构造函数
Date()
{
// 具体实现初始化
}
//2.带参实现构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1; // 调用无参构造函数 注意后不用加() ,否则成为函数的声明了。
Date d2(2019, 9, 1); // 调用带参函数的构造函数
Date d3();
}
这里我们就自己实现了带参和不带参的构造函数。相信这个理解起来并没有难度。但是大家需要注意一些细节 就比如:
Date d1 ;
Date d3();
前者 ,是调用无参构造函数
后者 ,是声明一个 返回值是 Date 类型的 ,形参为空的 函数 。
2.3 构造函数值得注意的点
1. 如果用户自己定义了构造函数,编译器就不再生成。
如果我们在一个类时,并没有定义构造函数,然后成功创建对象,此处调用的就是编译器自己生成的构造函数。
2. 无参的构造函数和全缺省的构造函数都称为默认构造函数,默认构造函数只有一个(虽然支持函数重载,但是调用时容易引起歧义)
2.4 默认构造函数有什么用?
我们调试查看使用编译器默认生成的构造函数创建的对象 , 发现 _year,_month ,_day 都是随机值,那么默认构造函数好像啥也没做啊。
C++把类型分成了内置类型(基本类型)和自定义类型。
内置类型 就是语法已经定义好的 ,比如 int , char ,double, 等等。
自定义类型就是我们使用class ,struct ,union 自己定义的类型。
我们不写构造函数,编译器默认生成默认构造函数,对内置类型不做初始化处理。
对于自定义类型的成员变量,会调用它的默认构造函数初始化,如果没有默认构造函数就会报错。
任何一个类的默认构造函数,就是不用参数就可以调用。
任何一个类的默认构造函数有三种 ,全缺省 , 无参 , 我们不写编译器默认生成。
总结 :
C++的构造函数设计的很奇怪,对内置类型和自定义类型区别对待。不处理内置类型的成员变量,只处理自定义类型的成员变量。
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值。
举个例子:
class A
{
public:
A()
{
cout << "A()" << endl;
}
};
int main()
{
A _a;
return 0;
}
对于我们使用class 定义的自定义类型的变量,自动调用默认构造函数(但其实这个默认构造函数还不是我们自己实现的)
我们将我们自己定义的构造函数注释掉以后再加上个函数再运行。
class A
{
public:
/*A()
{
cout << "A()" << endl;
}*/
A(int a) // 编译器检查到这段代码,就不会生成默认构造函数
{
}
};
int main()
{
A _a;
return 0;
}
3. 析构函数
析构函数:
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.1 析构函数的特性
1.析构函数的函数名 是在类名前 加上 ~
2. 无参数 无返回值
3. 一个类有且只有一个析构函数。若我们不主动定义,系统会自动生成析构函数
4. 对象生命周期结束时,C++ 编译系统自动调用析构函数。
3.2 析构函数的具体作用
析构函数和构造函数一样,对内置类型和自定义类型区别对待。
默认生成的析构函数对内置类型不做处理,对于自定义类型调用它的默认析构函数。
那什么时候区分,需不需要我们自己实现析构函数?
情况1 :
class Date
{
int _year;
int _month;
int _day;
};
情况2:使用类构造一个动态的栈。
class Stack
{
int* a;
int size;
int capacity;
};
这两种情况,如果我们都不定义析构函数,直接使用编译器默认生成的析构函数,可行吗?
情况1是可行的,情况2不可行。
第一种情况 ,虽然析构函数对内置类型成员变量不做处理。但是它是开辟在栈区,函数结束会自动释放。
第二种情况,由于是我们主动申请在堆上的空间 ,堆上的资源清理 需要我们自己定义析构函数,防止出现内存泄漏等问题。
总结: 在我们定义类的默认析构函数时 ,需要考虑资源是不是需要我们自己来清理就比如开辟在堆上的空间 ,如果需要我们自己定义默认析构函数,那么我们就必须定义,不可偷懒。
4. 拷贝构造
使用同类型的对象来初始化实例对象。在创建新对象时,由编译器自动调用。
同样,如果我们不实现,编译器会生成一个默认拷贝构造函数。
4.1 特点:
1.拷贝构造是 构造函数的一种重载形式。
2. 拷贝构造函数的参数只有一个,且必须使用引用传参,使用传值传参会引发无穷递归。
举个例子
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_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(2023, 9, 9);
Date d2(d1);
}
我们着重解释一下,为什么必须用传引用传参 , 如果 我们使用的是,传值传参,那么
我们需要将值给形参,这不就又是一个拷贝构造 (用一个同类型的对象初始化我),这样会一直,反复套娃。
事实上,在C++ 中,所以的传值传参,都是拷贝构造 (即用一个同类型的对象初始化我自己)。
拷贝构造函数典型调用场景:
使用已存在对象创建新对象 (拷贝构造)
函数参数类型为类类型对象(传值传参)
函数返回值类型为类类型对象 (作返回值)
除此以外,当我们注释掉我们定义的拷贝构造函数后运行程序发现,程序一样运行成功。
这是因为编译器调用了它生成的默认拷贝构造。
对于编译器生成的默认拷贝构造:
1. 内置类型的成员变量,会按照字节序完成拷贝。(浅拷贝)
2. 自定义类型成员,调用拷贝构造,这里就需要我们自己实现拷贝构造函数了。
注意:拷贝构造是用一个已经存在的对象来拷贝初始化一个即将创建的对象,而如果是两个已经存在的对象,则是我们下面介绍的情况——赋值重载
5.运算符重载
由于C++不支持自定义对象使用运算符。
所以C++为了增强代码可读性,引入了运算符重载。运算符重载是具有特殊函数名的函数。
具有返回值类型,函数名,参数,返回类型。
函数名为 关键字operator后加需要重载的运算符符号。
函数原型为: 返回值类型 operator操作符 (参数列表)
5.1 实现一个> 的运算符重载。
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
bool operator>(const Date& d)
{
if (_year > d._year)
return true;
else if (_year > d._year && _month > d._month)
return true;
else if (_year > d._year && _month > d._month && _day > d._day)
return true;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 9, 9);
Date d2(2022, 7, 1);
cout << (d1 > d2) << endl;
}
注意:
1. 为了顺利拿到 private的对象的同时 ,不破坏它的封装 , 我们选择在类中实现运算符重载。
2.由于是在类中定义的运算符重载,我们需要注意 隐含的this指针 。在比较两个对象大小时,this指针已经接收了第一个对象,我们只需要将第二个对象传引用即可。(C++中只要符合需求,优先选择传引用)
3. 由于C++为了代码的可读性 ,我们可以直接使用运算符重载后的 + ,直接 判断
d1 > d2;
但是实际应该是 : d1.operator+(d2); 只是由于代码的可读性,我们可以写的简单。
5.2 赋值运算符重载的实现
Date& operator= (const Date& d) // 传引用返回 ,减少了拷贝构造。
{
if (this != &d) // 判断是否自己给自己赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // *this 拿到的 其实是 对象本身 。
}
我们需要注意的是 ,
赋值重载是 俩个已经实例化的对象间的赋值拷贝。
拷贝构造 是使用同类型的对象来初始化实例对象。
5.3 赋值运算符重载的特点
当我们注释掉自己定义的赋值重载后再运行程序会发现 ,一样可以运行通过。
这是由于
内置类型的成员变量 ,编译器生成的默认赋值重载会完成字节序的复制。
自定义类型的成员变量,则会调用我们自己定义的深拷贝的赋值重载。(需要我们自己定义)
5.4.前置++和后置++重载
前置++和后置++都是一元运算符,为了让前置++和后置++可以进行正确的重载。
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
例如:
class A
{
public:
A(int a = 10)
{
_a = a;
}
// 前置++
A& operator++()
{
_a += 1;
return *this;
}
//后置++
A operator++(int)
{
A temp(*this);
_a += 1;
return temp;
}
private:
int _a;
};
前置++,使用引用做返回值,这是因为返回的是++之后的值,*this 仍然存在,使用引用返回。
后置++,使用传值返回,返回的是之前拷贝的一份临时变量,出了函数就被销毁。
5.5 取地址运算符以及const取地址运算符
由编译器默认帮我们实现,如果有特殊情况,比如让别人获取到我们指定的地址,我们也可以自己实现。
6.const 成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
例:const Date d1 , d1这个对象调用函数时,传的参数是 const Date * const this,而函数的参数类型是 Date const * this ,属于权限的放大,无法正确的调用函数。
我们可以在函数名后加 const 来解决上述情况
void Date::Print() const // const Date* const this
{
}
成员函数加上 const 是有利的,普通对象和const对象都可以调用,保护对象不会被错误的修改。