1.构造函数
1.1构造函数的特性
2.析构函数
2.1概念
2.2特性
3.拷贝构造
3.1概念
3.2特性
4.赋值运算重载
4.1运算符重载
注意:
4.2赋值运算符重载
5.练习
5.1
5.2
5.2.1匿名对象
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数还可以构成重载
class Data
{
public:
Data()
{
_year=0;
_month=0;
_day=0;
}
Data(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;//这里是调用无参构造函数
Data d2(2022, 3, 8);//调用的带参构造函数
//Data d3();
}
这里的构造函数可以构成重载,因为这里满足重载函数的条件,函数参数个数不同。注意调用无参构造函数时后面是没有括号的,如果是用成了d3,那么就会返回一个日期类,里面的数据却没有定义。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定 义编译器将不再生成。
class Data
{
public:
//这里如果没有定义构造函数,编译器会自动生成一个构造函数
/*Data()
{
_year = 0;
_month = 0;
_day = 0;
}
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
//没有写构造函数也可以定义成功,因为编译器已经自动调用了默认的构造函数
Data d1;
}
6.
class Data
{
public:
Data()
{
_year=0;
_month=0;
_day=0;
}
Data(int year=2022,int month=3,int day=8)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
//Data d2(2022, 3, 8);
}
这里的d1能定义成功吗?答案是不能的因为定义的d1是无参定义,但是在类里面我们写了两个无参构造函数,这时就会出现歧义。
7.
很多人可能就会问,如果没有自己写构造函数,编译器构造的函数,调试出来看会是一堆乱码。
看起来没有什么用。
因为编译器把数据类型分成了两种,一种是内置类型,一种是自定义类型。内置类型包括:int,char,double,指针,这些类型编译器不会初始化处理。而自定义类型: 比如struct/class这些写的栈,队列这些就会初始化处理。
1.我们不写编译器默认生成构造函数,对于内置类型不做初始化处理
2. 对于自定类型成员变量会去调用它的默认构造函数初始化,如果没有默认构造函数就会报错。
3. 任何一个类的默认构造函数就是--不用参数就可以调用。
4. 任何一个类的默认构造函数有三个,全缺省、无参、我们不写编译器默认生成的。
class A
{
public:
//1.无参
//A()
//{
// cout << " A()" << endl;
// _a = 0;
//}
//2.全缺省
/*A(int a = 0)
{
cout << " A()" << endl;
_a = a;
}*/
//3.就是编译器自己生成的
private:
int _a;
};
class Date
{
public:
private:
int _year;
int _month;
int _day;
A _aa;
};
这里定义的_aa是class A这个数据类型的数据如果是编译器自己编写的,其实最后_aa的值也是乱码。
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而在对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Date
{
public:
// 推荐实现全缺省或者半缺省,因为比较好用
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
// Date类没有资源需要清理,所以Date不实现析构函数都是可以的
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2022,3,8);
}
那这里是要先析构d1还是d2呢?答案是先析构d2再析构d1。
5.其他的特性和构造函数相同。可以参考上面写的构造函数。
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用。
1.拷贝构造函数是构造函数一种特别的形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
class Date
{
public:
// 推荐实现全缺省或者半缺省,因为比较好用
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date d2(d1);
Date(const Date& d)
{
_year = d._year;
//d._year = _year;
_month = d._month;
_day = d._day;
}
~Date()
{
// Date类没有资源需要清理,所以Date不实现析构函数都是可以的
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 15);
// 拷贝复制
Date d2(d1);
}
如果是不加&,这里的Date d2(d1)相当于是调用拷贝构造,然后到了拷贝构造Date(const Date d),调用拷贝构造就要传值传参就到了const Date d,然后这里的相当于是const Date d
=d1,这里又是一次拷贝构造,然后就会无限递归下去。就像是下面的图!
所以就需要就引用&符。
3.
class Date
{
public:
// 推荐实现全缺省或者半缺省,因为比较好用
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
// Date类没有资源需要清理,所以Date不实现析构函数都是可以的
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 15);
// 拷贝复制
Date d2(d1);
}
假如像上述代码,没有写拷贝构造,编译器就会自动生成一个(需要说明的一点是如果你写了拷贝构造,那么编译器就什么构造都不会生成了,所以就需要自己写构造函数)。
如果是内置类型就会完成字节序的拷贝相当于就是把一个一个的字节拷贝到另外一个空间中。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack st1(10);
// 拷贝复制
Stack st2(st1);
return 0;
}
如果是自定义类型也是会调用拷贝构造,就是拷贝空间的位置,如果空间的内容发生改变两个定义的变量都会发生改变,而且在这个域结束后st1和st2都会调用析构函数,我们知道析构函数作用两次是不行的。
首先我们要知道写运算符重载
1.函数名 operator操作符 返回类型。
2.看操作符运算后返回值是什么参数。
3.操作符有几个操作数,他就有几个参数。
class Date
{
public:
Date(int year = 2022, int month = 3, int day = 9)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator>(const Date&d1, const Date&d2)
{
if (d1._year > d2._year)
return true;
else if (d1._year == d2._year &&d1._month > d2._month)
return true;
else if (d1._year == d2._year &&d1._month == d2._month &&d1._day > d2._day)
return true;
else
return false;
}
int main()
{
Date d1;
Date d2(2022,4,8);
d1 > d2;//第一种办法调用运算符重载
operator>(d1, d2);//第二种
cout << (d1 > d2) << endl;
}
这个就是一个日期类,然后比较日期的大小,细心的同学可以发现在Date里面的成员变量变成了共有的,因为在类外是访问不了的,那么有什么办法可以解决呢?
class Date
{
public:
Date(int year = 2022, int month = 3, int day = 9)
{
_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;
Date d2(2022, 4, 8);
d1 > d2;//第一种
d1.operator>(d2);//第二种
cout << (d1 > d2) << endl;
}
这里可以看到这里的运算符重载是写在类里面的,而且这里的第二种调用方式和上面的那个也不太一样。可以看到写在类里面的传的参数只有一个,那是因为有之前讲到的一个知识点this指针,所以这个运算符重载里面原来就有一个参数this。如果写也可以这样写。
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator>(const Date&d)
{
if (this_year > d._year)
return true;
else if (this_year == d._year &&this_month > d._month)
return true;
else if (this_year == d._year &&this_month == d._month &&this_day > d._day)
return true;
else
return false;
}
但是一般是不推荐大家写的这个知道就行。
1.不能通过连接其他符号来创建新的操作符:比如operator@(只能用c++库里的操作符)
2.重载操作符必须有一个类类型或者枚举类型的操作数(不能传 int,char类型的数据,只能是之定义类型)
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义(比如是函数功能写的是加法,但是operator后面写的减号,这样会引起歧义)
4.类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 操作符有一个默认的形参this,限定为第一个形参(因为多了一个this指针,上面有说)
5. .* 、 :: 、 sizeof 、 ?: 、 . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符重载最重要的几点
.1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
// d1 = d3;
// d1 = d1;
Date& operator=(const Date& d)
{
// 极端情况下自己给自己赋值就可以不同处理了,直接判断一下跳过
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 16);
Date d2(2022, 1, 31);
Date d3(2022, 2, 26);
// 一个已经存在的对象拷贝初始化一个马上创建实例化的对象
Date d4(d1); // 拷贝构造
Date d5 = d1; // 拷贝构造
// 两个已经存在的对象之间进行赋值拷贝
d2 = d1 = d3; // d1.operator=(d3)
//d1 = d3; // d1.operator=(d3)
d1 = d1;
int i, j, k;
i = j = k = 10;
return 0;
}
很多人可能不太理解赋值重载和拷贝构造的区别,其实可以从主函数调用哪里区分的。
拷贝构造一个已经存在的对象拷贝初始化一个马上创建实例化的对象
赋值重载是 两个已经存在的对象之间进行赋值拷贝。
而且赋值重载这里的函数名写的也是很有讲究的,也可以着重的看一下的。
那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了我们还需要自己实现吗?当然像日期类这样的类是没必要的。
总结:构造函数和析构函数的处理机制是差不多。
拷贝构造和赋值重载也是差不多的。
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class C
{
public:
C()
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
};
class D
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
};
C c;
int main()
{
//TestDate4();
A a;
B b;
static D d;
return 0;
}
这段代码可以看到有4个类,同时也是定义了4个变量,那么问题是这4个变量谁先构造,然后谁先析构呢?
首先肯定是全局变量c先析构,然后就是A,B,D,这里相信很多人都知道,就是析构的时候可能出问题,通过前面的学习,我们知道如果定义的局部变量先构造的,后析构,这个可以参考栈,所以前两个析构也可以理解,然后就是static和全局变量,这里从生命周期可以认为两个是同时销毁的,但是总要一个先后的吧(先定义的后析构),如果把static成全局的,而且写在C的前面就是D先析构,static作为静态的局部变量它的作用域是程序结束,全局变量也不用说了。
class Widget
{
public:
Widget()
{
cout << "Widget()" << endl;
}
Widget(const Widget&)
{
cout << "Widget(const Widget&)" << endl;
}
Widget& operator=(const Widget&)
{
cout << "Widget& operator=(const Widget&)" << endl;
return *this;
}
};
Widget f(Widget u)
{
Widget x; //生命周期在当前函数
Widget y = f(f(x));
return u;
}
问题是这里调用了几次拷贝构造?要解决这个问题那么首先就要知道一些其他的知识点。
这里可以看到是定义变量是不调拷贝构造,只有调用函数的时候才调用拷贝函数,因为传值传参是一次拷贝构造。
这里看到只调用了两次,但是事实上是调用三次,为什么是三次呢?因为return是传给了一个临时变量,这里是一次拷贝构造,然后临时变量又传给了y又是一次拷贝构造,但是编译器在这方面做了优化,合二为一只调了一次,相当于是u直接传给了y。
这里就可以看出来,编译器做的优化。
这里的匿名对象生命周期只在这一行,在这一行时,会调用构造和析构。
那现在再看一下这个题。袋盖过程就是这样的。