目录
前言
成员函数是C++ 的类和结构体的一个重要特性。这些数据类型可以包含作为其成员的函数。成员函数分为静态成员函数与非静态成员函数。静态成员函数只能访问该数据类型的对象的静态成员。而非静态成员函数能够访问对象的所有成员。在非静态成员函数的函数体内,关键词
this
指向了调用该函数的对象。这通常是通过thiscall调用协议,将对象的地址作为隐含的第一个参数传递给成员函数。
一、构造函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。
构造函数是一种特殊的成员函数,构造函数的主要任务是初始化对象,而不是开辟空间来创建对象。
创建对象主要是由操作系统来完成,以普通的局部变量为例。
创建一个对象要在栈上建立栈帧,用ebp和esp两个与栈帧的创建和维护的寄存器来维护栈帧。
编译器预先计算栈空间大小,提前预留足够的空间,ebp和esp声明这块空间已经被使用。当对象销毁时,esp和ebp向上移动。所谓的空间销毁并不是真的销毁这块空间,而是声明这块空间不能再被使用。
而初始化对象是对象已经创建好了,我们给它赋值。就好比int a = 10;给a初始化赋值为10。
1.构造函数的函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。4.构造函数可以重载。5.如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成
C++中,数据类型的分类:
内置类型/基本类型:int / double / char /long / 各种指针(包括自定义类型的指针)
自定义类型:struct / class / union / enum ……
C++的默认构造函数对内置类型是不会进行处理的,自定义类型回去调用它的默认构造函数
#include<iostream>
using namespace std;
class Date
{
public:
void Show()
{
cout << _year << "/ " << _month << "/ " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1;
d1.Show();
}
int main()
{
TestDate();
return 0;
}
将日期类的数据打印出来是随机值。
既然说到了默认构造函数,那么什么是默认构造函数?
我们自己不写,编译器帮我们自动生成的。还有无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个。
因为编译器自动生成的和我们写好的必然只能有一个,无参的和全缺省的构造函数虽然构成函数重载,并且语法上是支持这种写法的。但是当我们实例化对象时,不传参数时,编译器无法确定到底要调用哪个函数会出现
代码如下:
#include<iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 1970;
_month = 1;
_day = 1;
}
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Show()
{
cout << _year << "/ " << _month << "/ " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1;
d1.Show();
}
int main()
{
TestDate();
return 0;
}
一般情况下,对象初始化惯例分为两种,默认值初始化,给定值初始化
我们合二为一,给一个全缺省的构造函数。
我们会发现C++中的构造函数及其诡异,在C++11中,打了一个补丁,使我们能够方便一点来定义构造函数
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Show()
{
cout << _year << "/ " << _month << "/ " << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
注意这里面不是初始化,这里是我们给它缺省值,因为这是类的声明部分,声明没有开辟空间,自然不可能是定义。
总结:默认构造函数,不传参就可以调用
我们自己不写 :编译器自动生成
我们自己写的 :无参构造函数
我们自己写的 :全缺省构造函数
二、析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁是由编译器完成的。对象在销毁时自动调用析构函数,完成对象中的资源清理工作。
1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
析构函数与构造函数类似,对于内置类型并不处理,对于自定义类型去调用自定义类型的析构函数。
一些类需要显示的写析构函数。比如Stack Queue……
一些类并不需要显示的写出析构函数。比如:没有动态开辟的类。或者是类中的成员变量是自定义类型。
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Show()
{
cout << _year << "/ " << _month << "/ " << _day << endl;
}
~Date()
{
cout << "this is ~Date" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
因为Date并不需要我们显示的写出析构函数,我们只是打印出一句话来证明已经调用了析构函数。
三、构造函数和析构函数的调用顺序
我们知道一个对象的生成要调用构造函数,销毁调用析构函数。但是多个对象创建时的构造函数和析构函数的顺序呢?
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
{
_a = a;
cout << "A(int a)->" << _a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
A aa3(3);
void test()
{
static A aa0(0);
A aa1(1);
A aa2(2);
static A aa4(4);
}
int main()
{
test();
return 0;
}
我们以这个例子为例:
先来说明构造函数的顺序。第一个创建的是aa3,因为aa3是全局对象,在编译阶段,地址就以确定,就已经成功创建了。而其它几个对象需要test函数建立栈帧,然后创建。
所以接下来是aa0,aa1,aa2,aa3。因为我们只是创建了对象,并没有使用它,所以很快对象就销毁了,我们还是要明确一点,具有相同生命周期的对象,构造函数和析构函数调用的顺序相反。
又因为aa4定义为static,它没有存储在test函数的栈帧,而是储存在静态区,所以它不是第一个析构。aa2和aa1析构之后,test函数栈帧销毁,就来到了main函数,因为static修饰的变量生命周期变长,所以和aa3这个全局对象的声明周期一样长aa4是后构造的所以先析构。
四、拷贝构造函数
复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。
复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。
如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。
拷贝构造函数也是构造函数,它是构造函数的一个重载形式,拷贝构造函数的参数只能有一个且必须是类类型对象的引用,使用传值的方式编译器会直接报错,因为会引发无穷递归。
A(A aa)
{
_a = A._a;
}
至于为什么会出现无穷递归也是比较好理解的;
我们要想先调用拷贝构造函数,首先要传参,而传参又要调用拷贝构造函数。它们两者相互调用构成递归。当我们传引用时,传递的是对象的别名,对象已经调用过构造函数了,所以不会出现,无穷递归。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
所谓的浅拷贝是指如果类中有指针类型,编译器会自动生成一个与要拷贝的类的指针指向同一块空间的指针。如果前一个对象修改内容,拷贝之后的对象也会跟着修改,这样就不叫做拷贝了。并且如果前一个对象销毁了,拷贝对象接着使用已经销毁的空间,出现野指针问题。
浅拷贝也就是一个对象修改会影响另一个对象,前一个对象销毁,会造成同一块空间析构两次,程序崩溃。解决办法是自己实现深拷贝。
拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
五、运算符重载函数
函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)注意:不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。第一个运算符不是 . 而是.* 用来矩阵运算的
1、=运算符重载
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
总结:
参数类型:const T&,传递引用可以提高传参效率返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this :要复合连续赋值的含义
同时赋值操作符重载要在类内部,不能重载成全局函数
2、==运算符重载
bool Date::operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
3、>运算符重载
一个日期大于另一个日期的条件是 年大的就大,年相等的,月大的就大,月相等的看天数
bool Date::operator>(const Date& d)
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
{
return true;
}
else
{
return false;
}
}
4、<运算符重载
为了增强代码复用性,小于实际上就是,不大于和等于。而对于日期类的大于和等于我们已经实现好了,直接使用即可。
bool Date::operator<(const Date& d)
{
return !((*this > d) || (*this == d));
}
5、>=运算符重载
大于等于就是小于的对立面
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
6、!=运算符重载
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
7、+=运算符重载
对于日期类来说,日期加日期是没有意义的,只有日期加天数有意义,而日期加天数并不是直接相加的,还要考虑每个月的天数,是否是平年闰年。我们将获取每个月的天数封装成一个函数
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
同时,日期加上一个负数就相当与减去一个日期
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
8、+操作符重载
+操作符就是返回相加之后的对象,而原对象不发生改变
这时我们需要利用拷贝构造函数来创建一个临时变量,将临时变量利用+=操作符改变之后,将其返回。
Date Date::operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
9、-=操作符重载
-=的重载思路与+=类似,都是先让day减去天数,然后改变月份和年份
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
这里进入循环就进行--month是因为我们要向上一个月进行借位。循环条件是_day <= 0是因为,一个月不可能有0天。
10、++操作符重载
++分为前置++和后置++,前置++比较容易实现,直接天数相加然后返回就可以,而后置++返回的是++之前的值,然后它在加一,后置++需要进行拷贝,返回拷贝即可
又因为前置++后置++的操作符都是++,编译器无法区分,因此规定后置++的形参列表有一个int标记。
前置++与后置++构成了函数重载
//前置++
Date& Date::operator++()
{
return *this += 1;
}
//后置++
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
11、--操作符重载
--与++类似
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
Date& Date::operator--()
{
return *this -= 1;
}
12、日期相减返回天数
我们可以铆钉一个基准值例如1月1日,然后分别从1月1日到两个日期所间隔的天数。
但是在我们已经实现了多个运算符重载的条件下,我们只要让小的日期加到大的日期,计算加的次数就可以了。
//日期相减返回天数
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
int day = 0;
while (min < max)
{
++min;
++day;
}
return flag * day;
}
13、友元
友元函数问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字 。说明:友元函数可访问类的私有和保护成员,但不是类的成员函数友元函数不能用const修饰友元函数可以在类定义的任何地方声明,不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同
14、>> <<操作符重载
我们想让Date类像内置类型一样可以使用cout直接打印出,我们想到了将<<操作符重载
要想重载<<我们要了解一下cout和cin是什么
cin使istream的全局对象,cout是ostream的全局对象
cin和cout能够对内置类型直接使用是因为:库里面写好了运算符重载,自动识别类型是因为它们构成了函数重载。
运算符重载:让自定义类型对象可以用运算符,转换成调用这个重载函数
函数重载:支持函数名相同的函数同时存在
运算符重载和函数重载虽然都用了重载这个词,但是它们之间并没有什么必然联系。
我们在类内实现
ostream& operator<<(ostream& out)
{
out << _year << "/ " << _month << "/ " << _day;
return out;
}
这是我们在类中实现的<<重载,乍一看是没有什么问题,当我们调用时就会出现这种情况
Date d(2022, 7, 25);
cout << d << endl;
这是什么情况?
我们以它的原生的方式调用
Date d(2022, 7, 25);
d.operator<<(cout);
发现什么错误也没有。
我们反向调用试一下
Date d(2022, 7, 25);
d << cout;
以这种诡异的方式<<重载竟然调用成功了
流提取运算符和流插入运算符都要在全局实现
因为操作符重载,第一个参数是左操作数,第二个参数是右操作数。而<< 和>>操作符都是作为左操作数使用的,在类中它们被当作右操作数使用所以出现了这种情况。
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/ " << d._month << "/ " << d._day;
return out;
}
inline istream& operator>>(istream& in, Date& d)
{
cin >> d._year >> d._month >> d._day;
return in;
}
使用inline来修饰<<和>>重载,因为这两个重载函数调用次数比较频繁
六、七、取地址重载函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
它们是默认成员函数,我们不写编译器会自动生成,自动生成的就够用了,所以一般不需要我们自己写。
我们知道this指针是 Date* const this 它是为了防止将this的指向改变,而我们在函数后面加入的const是为了防止this指针解引用之后的值修改,也就是为了防止成员变量的值被修改而设立的。
总结
以上就是类和对象中篇的内容。