目录
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/23a79964073067a53f028553f7d6f093.jpeg#pic_center)
1.类的默认成员函数
当一个类中什么不显式存在任何成员时,我们将之称为“空类”。但是空类真的什么都没有吗?其实不然,空类中会自动生成下面6个默认的成员函数
默认成员函数:
默认一般是指在缺省时,提供的预先指定的参数或函数。
例如缺省参数函数会使用默认值作为函数参数一样,默认成员函数就是在调用时无需显式调用的函数,而且可以不用传参。
2.构造函数
2.1 构造函数引入
首先我们定义一个日期类:
class Date
{
public:
SetDate(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;
};
int main()
{
Date d1,d2;
d1.SetDate(2021,12,5);
d2.print();
return 0;
}
对于类对象d1,我们需要调用成员函数SetDate为其设定日期,但每次创建变量后都需要调用一次该函数,过于麻烦。那是否能在创建变量时将这些信息初始化进去呢?
构造函数是一个特殊的成员函数,它能够在类实例化成对象时自动调用,我们可以将这些信息按自己的赋值规则将其初始化给该对象的成员变量,保证每个成员都有自己合适的初始值。并且构造函数在对象的生命周期内只调用一次。
2.2 构造函数概念及特点
构造函数虽名为构造,但是构造函数的任务并不是去开辟空间创建对象,而是初始化对象。
其特征概括如下:
1. C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
2. 构造函数在定义时可以有参数;
3. 没有任何返回类型的声明;
4. 对象实例化时编译器自动调用对应的构造函数;
5. 构造函数可以重载;
class Date
{
public:
//构造函数
//用户给定值初始化
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//重载构造函数,保证多种初始化方式
//默认值初始化
Date()
{
_year = 2000;
_month = 1;
_day = 1;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021,12,4);//直接在对象实例化时添加括号,会自动调用带参数的构造函数
d1.print();
Date d2;//调用无参的构造函数
d2.print();
//调用无参的构造函数时,对象后面不用跟括号,否则编译器会当成函数声明
//Date d3();//错误
return 0;
}
大多数情况下我们会选择全缺省参数作为构造函数
//将两种初始化的方式合并——全缺省初始化
Date(int year = 2020, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
6. 如果没有显式定义的构造函数,那编译器会自动生成一个无参的默认构造函数(称为合成的默认构造函数
),一旦用户显式定义则编译器将不会生成;
合成的默认构造函数将按如下规则对成员变量进行初始化:
- 如果存在类内初始值,用它来初始化类内成员;
- 否则,默认初始化(成员为内置类型将不被初始化,成员是自定义类型class等,将会调用其自身的构造函数(如果有的话)/默认构造函数)
利用=default
,我们也可以显式的定义“合成的默认构造函数”
这么做的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。
- 无参的构造函数,全缺省的构造函数,编译器生成的合成默认构造函数都是默认构造函数,因为这三个函数都可以不用参数就初始化一个对象——
Date d1
。
又因为这三个默认函数不能构成重载(无参调用时不明确),所以默认构造函数只能存在一个。
虽然在我们不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,所以大多数情况下都需要我们自己写构造函数。
3. 析构函数
3.1 析构函数引入
析构函数和构造函数执行的相反地操作,析构函数不是完成对象的销毁,局部对象的销毁由编译器负责完成,而在对象销毁时会自动调用析构函数,目的是完成类的一些资源清理工作,以及还可以用来执行用户希望在最后一次使用对象之后执行的任何操作
3.2 析构函数的概念
- 析构函数与构造函数名字相同,但它前面必须加一个波浪号
(~)
; - 析构函数没有参数,也没有返回值,而且不能重载。因此在一个类中只能有一个析构函数;
- 当撤消对象时,编译系统会自动地调用析构函数。 如果程序员没有定义析构函数,系统将自动生成和调用一个默认析构函数,默认析构函数只能释放对象的数据成员所占用的空间,但不包括堆内存空间。
不管是默认析构函数还是自定义析构函数,它永远不应该也不允许被你调用(没有任何参数,连this指针也没有),它是在对象的生命周期结束时由系统调用的。如果一个类不含有指针类型的数据成员(并且这个指针是在类的构造函数里分配的内存空间)的话,可以无视析构函数。但如果类是下面这种样子的话,就应该自定义析构函数:
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
~Stack()//这样的析构函数具有重大意义
{
free(_a);
_a = NULL;
_top = _capacity = 0;
cout << "~Stack" << endl;
}
private:
int* _a;
int _top;
int _capacity;
};
这种情况下不写自定义析构函数,程序也可能正常运行,但它却造成了内存泄露!
3.3 在哪些情况下会程序会执行析构函数?
- 如果一个函数中定义了一个对象,当这个函数被调用结束时,对象应该释放,在这个对象释放前自动执行析构函数
- 静态(static)局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只有在main函数结束或者调用exit函数结束程序时,才调用static局部对象的析构函数
- 如果定义了一个全局对象,则在程序的流程离开其作用域时(main函数结束或者调用exit函数结束程序),调用其全局对象的析构函数
- 如果用malloc(/new)运算符动态的建立了一个对象,当用free(/delete)运算符释放对象时,先调用该对象的析构函数
3.4调用构造函数和析构函数的顺序
最先调用的构造函数,与之对应的对象的析构函数最后调用。
最后调用的构造函数与之对应的对象的析构函数先调用
简记为:先构造后调用,后构造先调用
类似于栈:先进后出
3.5 编译器默认生成的析构函数
自动生成的默认析构函数针对类的内置类型(int,char,double…)不处理,但若成员中含有自定义变量(class,struct,union…),会调用其自身的析构函数。
针对申请堆空间的成员,一定要自定义析构函数去释放动态申请的空间,编译器会自动调用。(释放空间这个动作要准备好,然后编译器在对象生命周期结束时会自动调用)
4. 拷贝构造函数
4.1 拷贝构造函数引入
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。
他是特殊的构造函数,只有一个形参,该形参是对本类类型对象的引用(常用const修饰)。
在为已存在的类类型对象创建新对象执行拷贝时,编译器会自动调用。同样,你可以自己定义拷贝函数,如果没有则编译器会自动生成默认的拷贝构造函数,这两者是存在区别的,会在下文中讨论。
4.2 拷贝构造函数概念
拷贝构造函数的特征如下:
1. 拷贝构造函数是构造函数的一个重载形式
所以当我们自定义了一个拷贝构造函数时,编译器就不会再生成合成的默认构造函数了,但是反过来,如果我们自定义了其他的构造函数,编译器也会为我们合成一个拷贝构造函数。因为构造函数不一定是拷贝构造函数 但是拷贝构造函数是构造函数的一种特殊形式。
当然这种情况一般我们还会自己再定义一个默认构造函数,否则无法创建对象:
2. 拷贝构造函数的参数只有一个,且必须使用引用传参,使用传值方式会引发无穷递归调用
所以需要传引用(没有拷贝的过程)就不会再触发拷贝构造,又因为传引用,有修改函数实参的可能,所以记得添加const缩小权限。
- 即使是传参给类外函数,我们也选择传引用。自定义类型的对象,推荐用引用传参,如果使用传值传参也行,但每次都会使用拷贝构造绕个弯路。
3. 若没有显式地定义拷贝构造函数,那么编译器会生成默认的拷贝构造函数,默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
我们通过代码来认识默认拷贝函数所实现的浅拷贝
案例①
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
Date d2(d1);
return 0;
}
通过调试可以发现d1的值已经全部复制到d2中了,既然默认的拷贝构造完全能应付copy的工作,那我们就高枕无忧了吗,其实不然,我们说过默认只能完成浅拷贝,而无法完成深拷贝。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源(堆内存),当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
默认拷贝构造函数一旦拷贝的是带有资源的成员变量就会出错,见下面案例
案例②
class Stack
{
public:
Stack(int capacity = 4)//默认构造 创建一个容量为4字节的栈
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
~Stack()//析构 (释放空间——这一点默认的析构函数可做不到)
{
free(_a);
_a = NULL;
_top = _capacity = 0;
cout << "~Stack" << endl;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
我们用类想要实现一个栈。
在我们调试时报错来了:
发现了其中的隐患:
- 我们将 _a,_top,_capacity赋值给了st2,
- 但是!!_a是指向一块内存空间的指针,在使用空间时st1和st2会造成冲突,
- 而且!!在析构时这块_a指向的内存空间被st2先给free了(栈帧的后进先出原则),st1析构时同一块空间又被free了一次,发生报错!
- 像stack这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,不满足需求,需要自己实现深拷贝
如果一个类中包含指向动态分配存储空间的指针类型的成员变量时,就应该为这个类设计一个拷贝构造函数,除了需要设计一个拷贝构造函数之外,还需要为它添加一个赋值操作符重载函数(即重载“=”操作符,这将会在下一节加以介绍)。
由于类会自动生成拷贝构造函数,因此有些时候为了不让对象发生拷贝行为,我们可以显示声明一个拷贝构造函数,并将其设置为 private 属性。这跟通过将默认构造函数设置成 private 属性限制对象的创建时一样的道理。当然,禁止对象发生拷贝的需求较少,如果有这样的需求的话,知道还可以这么做就足够了,这是一个类设计的技巧。
5.赋值运算符重载
5.1 运算符重载
1. 学会使用运算符重载
-
函数的名字为:关键字
operator
接需要重载的运算符
-
函数原型:返回类型
operator
运算符(形参列表) -
调用方式:1). operator操作符(参数),2).直接使用操作符
注意 :
①重载运算符必须有一个操作数是类类型或枚举类型
②不能自己新创建一个操作符:比如operator@
③操作符的重载是针对某一个类或多种类的对象而言的:
比如:两个日期的相减得到的天数,我们需要重新建立两个对象相减的规则,
然而对于已有的内置类型的运算规则:如int的相加的规则->operator+(int,int)是不被允许的。
④类成员的运算符重载,看起来少一个函数参数,
是因为默认this指针为函数第一个形参,
对于需要两个以上的操作数的运算符,this将作为最左边的参数。
如果我们要将this对象后置,那就需要将运算符重载设为全局函数,
然后在类中添加友元函数声明来使用(为了访问私有的成员变量),这个之后会说。
2. 运算符重载的特性
运算符重载指的是将 C++ 提供的操作符进行重新定义,使之满足我们所需要的一些功能。运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名以及参数列表,其返回值类型与参数列表与普通函数类似。
运算符重载和函数重载都用了重载这个词,但这两个地方没有关联:
- 函数重载支持定义同名的函数;
- 运算符重载是为了让自定义类型可以像内置类型一样去使用运算符。
在 C++ 中可以重载的运算符有:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |=
<< >> <<= >>= == != <= >= && || ++ -- , ->* -> () []
new new[] delete delete[]
上述运算符中,[ ]
操作符是下标操作符,( )
操作符是函数调用操作符。自增自减操作符的前置和后置
形式都可以重载。
不能被重载的操作符:长度运算符sizeof
、条件运算符:?
、成员选择符.
、对象选择符.*
和域解析操作符::
。
3. 以日期类为例,实现==
的运算符重载
运算符重载作为成员函数
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//大部分情况下,成员变量还是为private,所以运算符重载函数尽量作为成员函数。
//这时已有默认this,所以参数只需一个,尽量使用const+类引用作为参数,减少拷贝构造的次数
bool operator==(const Date& x2)
{
return _year == x2._year && _month == x2._month &&_day == x2._day;
}
//当运算符是两个操作数时,第一个参数是左操作数,第二个参数是右操作数
//第一个操作数是默认的this指针
//尽量使用常引用作为函数参数,这样能减少拷贝的次数以及防止篡改
//d1<d2
bool operator<(const Date& x2)//bool operator(Date* this,const Date& x2)
{
if (_year > x2._year)
{
return false;
}
else
{
if (_month > x2._month)
{
return false;
}
else
{
if (_day >= x2._day)
{
return false;
}
}
}
return true;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 12, 5);
Date d2(2021, 12, 6);
//调用方式1
cout << d1.operator==(d2) << endl;//d1.operator(&d1,d2 )
//调用方式2
cout << (d1 == d2) << endl;
cout << (d1 < d2) << endl;
return 0;
}
运算符重载作为全局函数
class Date
{
//如果成员变量为私有,添加下面的友元声明,则函数即可访问成员
friend bool operator==(Date x1, Date x2);
public:
Date(int year = 2020, 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;
};
//当成员变量可以访问时(public),便可以将成员函数外置为全局函数,因为访问成员变量也不受限制
bool operator==(const Date& x1,const Date& x2)
{
return x1._year == x2._year && x1._month == x2._month && x1._day == x2._day;
}
int main()
{
Date d1(2021,12,5);
Date d2(2021,12,6);
//调用方式1
cout<<operator==(d1, d2)<<endl;
//调用方式2
cout<<(d1==d2)<<endl;
return 0;
}
4.一个妙用——[]
运算符重载
class Arr
{
public:
Arr()
{
for (int i = 0; i < 10; i++)
{
_a[i] = 10 * i;
}
}
//将方括号重载可以通过运算符来直接访问成员变量数组,如果返回引用还可以修改
int& operator[](int pos)
{
return _a[pos];
}
private:
int _a[10];
};
int main()
{
Arr a;
cout << a[0] << endl;//a.operator[](&a,0)
cout << a[1] << endl;//a.operator[](&a,1)
cout << a[2] << endl;//a.operator[](&a,2)
//赋值给了返回对象的引用
a[0] = 100;
a[1] = 200;
a[2] = 300;
cout << a[0] << endl;
cout << a[1] << endl;
cout << a[2] << endl;
return 0;
}
5.2 赋值运算符的重载
1. 我们可以根据上述的运算符重载,我们可以先自己写一个赋值运算符重载
class Date
{
public:
//赋值运算符重载
//d1=d2; d1.operator=(&d1,d2)
void operator=(const Date& d)//void operator=(Date* this,const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
这个赋值重载函数可以完成单次的赋值,但是由于没有返回值,该函数无法胜任连续赋值(a=b=c),因为连续赋值是右结合的,b=c之后应该需要返回b的值再赋值给a。于是我们做出改进:
//改进1.0
Date operator=(const Date& d)//void operator=(Data* this,const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
返回值是最简单的方式,它的操作主要在栈上,变量 *this 在函数结束后会删除,
为了返回 *this
的值,系统会在内部建立一个临时变量*保存 *this
的值,以返回给调用该函数的表达式,调用结束后变量便不再存在。
把*this
交给临时变量的过程是会调用拷贝构造函数的! 如果我们自己写了拷贝构造函数,可通过调试发现在return *this
时会自动调用拷贝构造函数。
如果*this是简单的数据类型也无所谓,不是很占用内存,如果是大的自定义类型的数据,那么对Date的复制将会占用比较大的内存。而且函数返回值是右值,不能进行运算符操作。
赋值重载一般都返回引用:
//改进2.0 返回引用
Date& operator=(const Date& d)//void operator=(Date* this,const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
为了防止自己给自己赋值,我们还可以再优化一下
//d1=d1;
//改进3.0 完整版
Date& operator=(const Date& d)//void operator=(Date* this,const Date& d)
{
if (this != &d)//检查不是自己给自己赋值,才需要拷贝
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
2.编译器默认生成的赋值运算符重载函数
当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。
见下例:
class Data
{
public:
Data(int val=0)
{
_val=val;
}
Data& operator=(int val)
{
_val=val;
return *this;
}
void Print()
{
cout<<_val<<endl;
}
private:
int _val;
};
int main()
{
Data x1(10);
Data x2,x3;
x2=10;
x2.Print();
x3=x2;
x3.Print();
return 0;
}
运行结果
在我们自定义了带int型参数的赋值重载函数后,编译器还是针对本类的引用生成了默认的赋值运算符重载,x3=x2编译通过了。
赋值运算符的重载与拷贝构造的特性相同:
-
针对内置类型会完成浅拷贝,如果类中的成员变量带有指针,则赋值的是相同的指针值(浅拷贝),那么同一块空间将会被两个指针同时指向,如果其中一个对象已析构,那么另一个对象中的指针将会称为野指针,不能对该指针指向的空间进行访问的同时也不能析构。
-
针对自定义类型,会调用该类型自身的赋值运算符重载。
-
一个误区
Date d4=d2;
是拷贝还是赋值?
Date d3(d1);//拷贝构造——用一个已知对象去构造初始化另一个要创建的对象
d1=d2;//赋值重载 ——两个已知的对象进行赋值
Date d4=d2;//用已知的对象去初始化刚创建的对象——>拷贝构造 等同于 Date d4(d2)
对于编译器默认生成的函数的总结
//1.构造和析构特性是类似的,我们不写则编译器对内置类型不进行处理,自定义类型则会调用其自身的构造和析构处理
//2.拷贝构造和赋值重载特性是类似的,我们不写编译器对内置类型会完成浅拷贝,自定义类型则会调用其自身的拷贝构造和赋值重载
6. const成员
6.1 const 修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。
关于添加const后能否与原函数构成重载,见下例代码:
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1, int value = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(const Date& d) -> bool operator==(Date* this,const Date& d)
//为防止this指针被篡改,可在最后添加const 可修饰this指向的值,一旦函数修改了成员变量,编译时就可以被检查出来
bool operator==(const Date& d)const // -> bool operator==(const Date* this,const Date& d)
{
return (_year == d._year) && (_month == d._month) && (_day == d._day);
}
//建议 如果成员函数中不需要修改函数变量,建议都加上const
//注意下方的两个print函数是能构成重载的
void print()
{
cout << "print()" << endl;
cout << _year << "_" << _month << "_" << _day << endl;
}
void print()const
{
cout << "print()const" << endl;
cout << _year << "_" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 12, 9);
const Date d2(2021, 12, 9);
d1.print();
d2.print();
//d1也可以进入print()const 理由是权限缩小
//但是d2不能进入print(),理由 是权限放大
return 0;
}
由此可得出结论:
- const对象可以调用非const成员函数吗? 不可以(权限放大)
- 非const对象可以调用const成员函数吗? 可以(权限缩小)
- const成员函数内可以调用其它的非const成员函数吗? 不可以(权限放大)
- 非const成员函数内可以调用其它的const成员函数吗? 可以(权限缩小)
7. 取地址及const取地址操作符重载
6个默认成员函数的最后两个,编译器会默认重载&运算符来返回对象和const对象的地址,用的比较少,除非开发者不想让用户获得对象的地址,可以自定义运算符&,或者将自定义出来的放在private中。
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1, int value = 1)
{
_year = year;
_month = month;
_day = day;
}
//取地址运算符重载
//用的较少 通常不需要自己编写,使用编译器默认生成的就够用了
//除非你不想别人获取该类对象的地址,才有必要自己实现(放在private中)
Date* operator&()
{
return this;
}
//针对const对象的取地址运算符重载
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
const Date d2;
&d1;
&d2;
return 0;
}
青山不改 绿水长流