前言:本文章主要用于个人复习,追求简洁,感谢大家的参考、交流和搬运,后续可能会继续修改和完善。
因为是个人复习,会有部分压缩和省略。
一、默认成员函数
当类里面成员函数什么都不写的时候,编译器会自动生成6个默认成员函数
六个成员函数包括:
构造函数(主要完成初始化工作)
析构函数(主要完成清理工作)
拷贝构造(视同同类对象初始化创建对象)
赋值重载(主要是把一个对象赋值给另一个对象)
取地址重载(主要是普通对象和const对象取地址,这两个很少需要自己实现)
默认成员函数对于内置类型成员不处理,对于自定义类型成员,它会去调用它的构造函数、析构函数。我们主要探讨前四个
1.构造函数(主要完成初始化工作)
因为我们有时候可能会忘记调用初始化函数,C++为了解决这个问题,引入了构造函数来初始化,构造函数不可能没有调用,它会在对象实例化时自动调用,保证对象一定有初始化流程。
构造函数的特点:
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数(保证了对象一定会初始化)
4.构造函数可以重载(我们就可以有多种初始化方式)
给一个全缺省的初始化是最好的
注意:调试的时候,只能看当前作用域里的变量
当全缺省构造函数和无任何参数的构造函数同时存在时,编译会出错,因为无法判断是要调用哪一个构造函数。语法上是可以的,但是实际使用时是不可以的。
我们不写,编译器会生成一个无参的构造函数,我们写了编译器就不会生成了。所以说构造函数是默认构造函数。虽然构造函数默认生成了,但是其初始化时,不会把值初始为0,而是随机值
对于内置类型(基本类型)语言原生定义的类型,如char、int、double、指针等等编译器不会初始化为0。对于自定义类型:class、struct等定义的类型,编译器会去调用它们的默认构造函数初始化为0。构造函数还是自己写靠谱,绝大多数情况下,编译器生成的默认构造函数并不好
默认构造函数:我们不写,编译器自动生成,我们写了,编译器自动生成。这个理解有一些地方不对。我们在写构造函数时,最好写不用传参就可以调用的构造函数,全缺省的构造函数是最好的,它可以适应各种场景。
1.我们不写,编译器默认生成的
2.我们自己写的无参的
编译器默认生成的并不是什么都不做,而是有区分的。
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,5,15);//可以
Date d2;//可以
Date d3();//不可以,未调用原型函数。没有调用到构造函数,对象没有被构造出来
return 0;
}
那么我们如何验证呢?如下:
class Date
{
public:
void Print()
{
cout << "cout" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,5,15);//可以
Date d2;//可以
Date d3();//不可以,未调用原型函数。没有调用到构造函数,对象没有被构造出来
d1.Print();
d2.Print();
d3.Print();//这里会出错,会显示"Print"的左边必须有类/结构/联合
return 0;
}
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
虽然有些构造函数调用之后,对象已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,不能称作初始化,因为初始化只能初始化一次,而构造函数体内可以多次赋值。
time*
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后跟一个放在括号中的初始值或表达式
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量、const成员变量、自定义类型成员(该类没有默认构造函数)
3.尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
4.成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
}
int main() {
A aa(1);
aa.Print();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
选D
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换地作用
class Date
{
public:
Date(int year)
:_year(year)
{}
explicit Date(int year)
:_year(year)
{}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2018);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2019;
}
上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换
2.析构函数(主要完成清理工作)
对象的构造和销毁是编译器干的。构函数的作用是对于资源的清理,会在对象销毁时(生命周期到时)自动调用
析构函数的特性:
1.析构函数需要在类名前加上~
2.没有参数没有返回值(无法重载)
3.一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
4.对象生命周期结束时,C++编译系统自动调用析构函数
有些类的析构函数才有意义,例如类中有malloc的。
关于调用顺序
因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出
3.拷贝构造函数(使用同类对象初始化创建对象)
拷贝构造函数在我们不写时会自动生成,会对内置类型完成浅拷贝或者值拷贝
Date d4(d1);
这样之后d4和d1的值就是一样的。即使用d1的值初始化创建出来的d4.
拷贝构造也是一种构造函数
其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
也就是说这样写是不对的:
Date(Date d)
{
_year = year;
_month = month;
_day = day;
}
非法的拷贝构造函数,要把传参括号里的Date d写成Date& d,不然传值会一直进行拷贝构造,这里先省一个图。为了避免这个,我们有两种解决方式:1.传引用2.用指针
这里用指针比较麻烦,推荐使用传引用
拷贝构造建议加const,这样写反了才能报错
Date(const Date& d)
{
_year = year;
_month = month;
_day = day;
}
Date d4(d1);
如果拷贝构造了一块空间,会出很多错误
那么拷贝构造函数对于自定义类型呢?
自定义类型会调用它自己的拷贝构造
对于浅拷贝,默认生成的拷贝构造就够用了,像Stack这样的类,需要的是深拷贝,需要自己写
4.赋值运算符重载(主要是把一个对象赋值给另一个对象)
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类
型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型或者枚举类型的操作数用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参
.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
// 全局的operator==
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;
};
// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year;
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
}
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
}
让自定义类型可以像内置类型一样使用运算符需要哪个运算符,就重载哪个运算符
运算符重载跟函数重载,都有用了重载这个词,但是两个地方之间没有关联。
1.函数重载时支持定义同名函数
2.运算符重载时为了让自定义类型可以像内置类型一样取使用运算符
赋值是把值赋给变量,拷贝构造是创建一个对象时,拿同类对象初始化它。赋值拷贝时两个对象都已经存在
赋值重载:两个已经存在的对象拷贝
拷贝构造:拿一个已经存在的对象取构造初始化另一个要创建的对象
赋值运算符重载
class Date
{
public :
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
private:
int _year ;
int _month ;
int _day ;
};
赋值运算符主要有四点:
1.参数类型
2.返回值
3.检测是否自己给自己赋值
4.返回*this
5.一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象俺字节序的值拷贝
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;
Date d2(2018,10, 1);
// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
d1 = d2;
return 0;
}
那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String
{
public:
String(const char* str = "")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
s1 = s2;
}