目录
一、类的6个默认成员函数
空类中什么都没有吗?在什么都没写的情况下,编译器会自动生成6个默认成员函数。
默认成员函数如果我们写了,编译器就不生成。没写,编译器自动生成。
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值重载
- const成员函数
- 取地址及const取地址操作符重载
二、构造函数
在c语言中,经常会忘记初始化和destroy。
- 不初始化对象就插入值,编译器会报错。因为不初始化,对象里面存的都是随机值。
- 忘记destroy不报错,但可能会造成内存泄露。
那能不能在对象定义的时候就自动初始化呢?
有滴,这个函数叫构造函数。
构造函数是一个特殊的成员函数,名字和类相同,无返回值,创建类的对象时编译器自动调用,并且在对象的整个生命周期内只调用一次。
构造函数的任务不是为对象开辟空间,而是初始化对象。
特性:
- 函数名和类名相同
- 无返回值(void也不用写)
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载(可以提供多个初始化函数,有参/无参时调用不同函数)
1.有参构造函数
class Date
{
public:
Date (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(2022,10,15);
d1.Print();
return 0;
}
2.无参构造函数
class Date
{
public:
Date ()
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//无参不要带括号:Date d1();
d1.Print();
return 0;
}
来个更优版的缺省参数
class Date
{
public:
Date(int year=1, 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;
};
int main()
{
Date d1(2022, 10, 15);
Date d2;
d1.Print();
d2.Print();
return 0;
}
全缺省参数和无参的的构造函数可以共存(函数重载),但是在不传参调用的时候存在歧义,编译器不知道调用哪个就会报错。
应用:栈的初始化
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int *)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
private:
int *_a;
int _top;
int _capacity;
};
5. c++把类型分为内置类型(基本类型)和自定义类型。内置类型由语言提供,如 int/char/double.....指针。自定义类型:class/struct/Stack/Queue/Person
6. 如果类中没有定义构造函数,编译器会自动生成一个无参的默认构造函数。
如果我们自己定义了,编译器就不定义了。
(自动生成的构造函数对内置类型数据不处理,自定义类型会调用它的默认构造函数。)/ /默认构造函数定义在下面
例:自动生成的构造函数对内置类型不处理
class A
{
public:
A()
{
_a == 0;
cout << "A()的构造函数"<<endl;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;//增加一个自定义类型
};
int main()
{
Date d1;
d1.Print();
return 0;
}
想用自动生成的构造函数,但是类里面自定义类型和内置类型都有,内置类型怎么初始化?
自定义类型不用我们处理,编译器会自动调用他们的构造函数。内置类型我们可以给缺省值。默认生成的构造函数就会用这个缺省值初始化。
缺省值这里建议看了初始化列表后再来回味一遍,就能理解的更加透彻。
如果在自己定义了构造函数,并且给所有成员变量都初始化了,就不用缺省值。
例:
class Date
{
public:
Date (int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year=0;
int _month=0;
int _day=0;
};
int main()
{
Date d;
d.Print();
return 0;
}
如果自己写了构造函数,但是没有在构造函数里初始化变量,还是用缺省值初始化。
7.无参的构造函数和全缺省的构造函数都称为默认构造函数(这两个有且只能存在一个,因为不传参调用的时候存在二义性),编译器自动生成的那个也叫默认构造函数。
不传参数就能调用的构造函数,就叫默认构造函数。
例:读代码,为什么没找到默认构造函数
class Stack
{
public:
Stack(int capacity)
{
_a = (int *)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
_capacity = capacity;
_top = 0;
cout << "Stack(int capacity = 4)" << endl;
}
private:
int *_a;
int _top;
int _capacity;
};
class MyQueue
{
public:
private:
Stack _pushST;
Stack _popST;
size_t _size = 0;//这里不是初始化,而是缺省值。
};
int main()
{
MyQueue q;
return 0;
}
_size给了缺省值,没问题。问题出在_pushST和_popST。
MyQueue类没有自己写构造函数,编译器自动生成它的默认构造。构造函数对内置类型不处理,可以在定义变量的时候给缺省值。自定义类型去找它的默认构造函数,Stck没有默认构造。
Stack的构造函数我们自己写了,就没有编译器定义的构造函数。
也没有无参或全缺省。所以找不到Stack的默认构造函数。
解决方法:
1.在stack里定义无参或全缺省的默认构造
2.stack用自动生成的默认构造
如果Stack也不写构造函数:
方法一:
方法二:
都能编译过去
三、析构函数
和构造函数功能相反,不是销毁对象本身(局部变量的空间释放由编译器完成),而是完成对象中的资源清理(清理对象里动态开辟的空间,和Destroy功能相似)。生命周期结束后,对象会自动调用构造函数。
特性:
- 函数名:~类名
- 没有参数,也没有返回值类型
- 不能重载
- 对象生命周期结束的时候自动调用
- 自动生成的析构函数对内置类型不处理,对自定义类型调用它的析构函数
生命周期的影响因素(作用域不一定影响生命周期,比如命名空间域不影响生命周期)
1.局部对象,函数结束就销毁
2.全局对象,main函数结束就销毁
3.malloc出来的,要手动free才销毁,或程序正常运行结束会自动销毁
4.类对象生命周期结束时,自动调用析构函数,会清理资源。
出生命周期自动调用析构:
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int *)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
_capacity = capacity;
_top = 0;
cout << "Stack(int capacity = 4)" << endl;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity=0;
cout << "~Stack" << endl;
}
void Push(int x)
{
//扩容
_a[_top++] = x;
}
private:
int *_a;
int _top;
int _capacity;
};
class MyQueue
{
public:
private:
Stack _pushST;
Stack _popST;
};
void Func()
{
MyQueue q;
}
int main()
{
Func();
return 0;
}
什么时候需要自己写析构函数?
日期类都是内置类型,没动态开辟空间,也不用资源清理,生命周期到了由编译器销毁回收,所以不用我们自己写析构函数。
而栈需要释放动态开辟的空间,且是内置类型的,它的析构函数需要我们自己写。因为编译器生成的默认析构函数对内置类型不处理(比如栈里面malloc出来的空间,用int*接收地址)。
MyQueue的析构函数也要释放资源,但是因为都是自定义类型,调用自定义类型:栈 的析构函数,不用自己写析构函数。
成员是指针类型或迭代器,无论是内置类型还是自定义类型的,默认生成的析构函数不会自动处理。
生命周期相同的类对象谁先析构?
后定义的,先析构。
例:
****设已经有A,B,C,D,4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
分析:1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
2、全局对象先于局部对象进行构造
3、局部对象按照出现的顺序进行构造,无论是否为static
4、所以构造的顺序为 c a b d
5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生命周期之后,会放在局部对象之后进行析构
6、因此析构顺序为B A D C
四、拷贝构造函数
特性
1.拷贝构造是构造函数的重载,没有返回值,函数名是类名
2.拷贝构造的参数只有一个,必须是类对象的引用,使用传值编译器会报错(引发无穷递归)
以Date类的拷贝构造为例:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//【const哦】防止写反,把d里面的成员给改了 比如:d._day=_day;
{
_year = d._year;
_month = d._month;
_day= d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,10,16);//构造
//拷贝一份d1
Date d2(d1);//拷贝构造——拷贝初始化
d1.Print();
d2.Print();
return 0;
}
为什么会引发无穷递归?
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day= d._day;
cout << "调用拷贝构造" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)//形参是实参的拷贝
{
cout << "func1 传值" << endl;
}
void Func2(Date& d)//形参是实参的引用
{
cout << "func2 传引用" << endl;
}
int main()
{
Date d1(2022,10,16);//构造
Func1(d1);
Func2(d1);
return 0;
}
因为传 类的值 的时候会调用拷贝构造。
自定义类型的拷贝比较复杂,我们需要自己写一个函数来完成拷贝,这个函数就是拷贝构造。
3.如果没有自己定义拷贝构造,编译器会生成默认的拷贝构造函数。
注意:编译器生成的拷贝构造 内置类型是按字节拷贝的(浅拷贝),自定义类型调用它的拷贝构造函数。
编译器自动生成对内置类型的浅拷贝,对有的类来说并不好用,比如浅拷贝指针就不行。
比如拷贝栈:都是内置类型,自动生成的浅拷贝连指针存储地址都给拷过去了。
其他的拷贝过来了,但是指针存储地址也拷贝的一模一样,指向同一块空间。在生命周期结束时会调用析构函数,第一次释放成功,第二次释放出错,因为指针还指向已经释放的空间,同一块空间不能释放两次,越界了。
还有一个问题就是,拷贝过来的插入删除会影响原来的,因为俩指针指向同一块空间。按理说拷贝只是拷贝,是可以另外操作而不影响原数据的。
默认生成的拷贝构造不能完成我们的要求。还得自己写,深拷贝。
代码:
//拷贝构造函数
Stack(const Stack& st)
{
_a = (int *)malloc(sizeof(int)*st._capacity);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
memcpy(_a, st._a, sizeof(st._top));
_top = st._top;
_capacity = st._capacity;
}
总结:
需要自己写析构函数的类,都需要写深拷贝的拷贝构造。
不需要自己写析构函数的类,用默认生成的浅拷贝拷贝构造就够用。
五、运算符重载
为了增强代码可读性和让自定义对象也能用运算符,c++引入运算符重载。(函数重载和运算符重载不是一个东西)。在学习赋值重载之前要先学一下运算符重载。
例:
这两个函数的效果是一样的。运算符重载真正的意义是代码可读性。别人也能读懂符号是什么意思,调用函数的时候也简单。
函数名:关键字operator后加需要重载的运算符符号。
函数原型:返回值类型 operator 操作符(参数列表)
参数:运算符重载函数是针对自定义类型的,所以必须有一个类类型的参数。(看函数定义在哪,定义在类里面自动就有一个隐藏的this指针,定义在类外,就一定要有一个类类型的参数)
注意:
不能创建一个新的运算符(比如:operator @)
以下五个运算符不能重载: .*(很少用) 、 :: 、 sizeof 、 ? : 、.
练习:
1.公有成员变量,在类外面定义运算符重载
class Date
{
public:
Date(int year = 1, 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;
};
//公共数据可以在外面访问
bool operator==(const Date& d1, const Date& d2)//传引用。传值会调用拷贝构造
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2022, 10, 16);//构造
Date d2(2022, 10, 19);
cout << (d1 == d2) << endl;//<< 的优先级高于==。所以带括号哦。
operator==(d1, d2);//也可以显示调用,但一般不这么用。上一种可读性很好。
return 0;
}
2.私有成员变量的运算符重载:
方法一:在类里面写get函数,在类外面定义运算符重载。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//构造
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "调用拷贝构造" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==( Date& d1, Date& d2)//传引用
{
return d1.GetYear()== d2.GetYear()
&& d1.GetMonth() == d2.GetMonth()
&& d1.GetDay() == d2.GetDay();
}
int main()
{
Date d1(2022, 10, 16);//构造
Date d2(2022, 10, 19);
cout << (d1 == d2) << endl;//<< 的优先级高于==。所以带括号哦。
//
//operator==(d1, d2);//也可以显示调用,但一般不这么用。上一种可读性很好。
return 0;
}
方法二:把运算符重载函数写到类里面。
运算符有几个操作数,就有几个参数。
两个操作数:显示写一个参数。实际是两个参数,因为有一个隐藏的this指针。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//构造
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "调用拷贝构造" << endl;
}
bool operator== (const Date& d)//运算符有几个操作数,就有几个参数,包含this指针。
{
return _year == d._day
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 10, 16);//构造
Date d2(2022, 10, 19);
cout << (d1 == d2) << endl;//<< 的优先级高于==。所以带括号哦。
d1.operator==(d2);//也可以这样,但没必要
return 0;
}
练习:Date类
1.判段d1==d2(等于重载)
bool operator== (const Date& d)
{
return _year == d._day
&& _month == d._month
&& _day == d._day;
}
2.d1>d2
//判断d1是否大于d2
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;
}
}
3.">=" "<" "<="
//判断d1是否大于等于d2
bool operator>=(const Date& d)
{
return *this > d || *this == d;
}
bool operator<=(const Date& d)
{
return !(*this > d);
}
bool operator<(const Date& d)
{
return !(*this >= d);
}
4.+和+=
//+=
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;//对象出了函数不销毁,引用做返回值,可以省去调用拷贝构造函数
}
//+
Date operator+(int day)
{
Date ret(*this);
ret += (day);
return ret;//只能传值返回,ret出了函数会销毁
}
5.-和-=
Date& operator -= (int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day<=0)//这个月不够减,往前借。记得一定要有=,10.23减23是10.0。没有十月零号。
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date operator - (int day)
{
Date ret(*this);//拷贝构造
ret -= day;
return ret;
}
d2-1000,d2本身不变,想要打印出结果有两种方法
int main()
{
Date d1(2022, 10, 23);
d1 -= 1000;
d1.Print();
Date d2(d1);//拷贝构造
//d2-1000,d2本身不变,想要打印出结果有两种方法
//1.创建新对象接收返回值
Date d3=d2 - 1000;
Date d4 (d2 - 1000);//都是拷贝构造。这两种方法一样。
d3.Print();
d4.Print();
//2.返回值是一个对象
(d2 - 1000).Print();
return 0;
}
6.完善构造函数,检查日期是否合法
Date(int year = 1, int month = 1, int day = 1)//构造
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (!(year >= 1)
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month)))
{
cout << "非法日期" << endl;
}
}
7.前置++和后置++
对于内置类型,前置后置差不多,而对于自定义类型用前置比较好。
自定义类型的前置没有拷贝的发生,后置调用了两次拷贝构造。
//前置
Date& operator++()
{
*this += 1;
return *this;
}
//后置
Date operator++(int)//多一个参数是为了跟前置区分,构成函数重载
{
Date tmp(*this);//拷贝构造
*this += 1;
return tmp;//对象传值返回的时候调拷贝构造
}
注意单目运算符重载需要注意是前缀和后缀。
前缀单目运算符,操作数在运算符左边。后缀单目运算符,操作数在运算符右边。
前置++是前缀单目运算符,后置++是后缀单目运算符。
调用前置++: ++d 或 d.operator++()
调用后置++:d++ 或 d.operator++(0)
六、赋值重载
注意运算符重载可以声明定义在类外或者类内,但是赋值运算符的重载只能定义在类里。
因为赋值运算符在类中不显式实现时,编译器会生成一份默认的,此时用户在类外再将赋值运算符重载为全局的,就和编译器生成的默认赋值运算符冲突了,故赋值运算符只能重载成成员函数。
区分赋值重载和拷贝构造
都在拷贝,赋值重载也是一个默认成员函数。
//= 赋值重载
//d1=d2 赋值重载需要注意参数顺序(谁赋值给谁),operator第一个参数是操作符左边的,第二个参数是右边的。这里第一个参数是d1的this指针,第二个参数是d2
//d1=d2=d3,连续赋值时,d2=d3后函数需要一个返回值,再赋值给d1
Date& operator =(const Date& d)//也可以直接传 Date d,只是会调用拷贝构造。返回值用引用返回,避免拷贝构造。
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
//能不能返回d? return d;//权限会放大。除非把返回值变成const Date&。但这样就不能修改返回值了
}
int main()
{
Date d1;
Date d2(2022, 10, 22);
Date d3(d1);//拷贝构造,一个已经初始化的 拷贝初始化一个马上要创建的对象
Date d4 = d2;//还是拷贝构造,d4是一个刚创建的对象,由d2拷贝初始化
d1 = d2;//赋值重载。已经初始化的两个对象间的拷贝。
return 0;
}
构造函数和析构函数类似,对内置类型不处理,自定义类型调用它们的构造函数和析构函数。
拷贝构造和赋值重载类似,内置类型处理(值拷贝),自定义类型调用他们的拷贝构造和赋值重载。
一般情况编译器默认生成的就够用。
对于要写析构函数的类,就要自己写赋值重载。因为拷贝的时候指针不能直接值拷贝。详细原因和拷贝构造那里一样。
用栈举例:存在内存泄漏(Stack1 和 Stack2都是已经初始化的,malloc开辟了两块空间,直接拷贝指针,两个指针指向同一空间,另一块空间没有释放,造成了内存泄漏),同一块空间析构两次就崩溃了。
Stack& operator=(const Stack& st)
{
if (this != &st)//取地址
//例如:st1=st;//调用赋值重载
//地址必须不相等的原因:深拷贝要先free释放掉st1开辟的空间,如何新开空间,再复制内容。如果&st1==&st说明两个地址指向同一个Stack对象,释放掉st1的资源后,从st里拿到内容进行拷贝会形成越界访问。
{
free(_a);
_a = (int*)malloc(sizeof(int)* st._capacity);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
memcpy(_a, st._a, sizeof(st._a));
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
七、取地址重载
这两个平时我们不用自己写,编译器自动生成的就能用。
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
八、const成员
指针和引用的传递不能放大权限。
const对象的this指针类型为(以日期类为例): const Date*const this
例1:void Print(){}函数中的this指针是Date* const this类型。
const对象调非const成员函数,this指针会权限放大,编译不通过。
例2:
int operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;//d2大,d1小,d1-d2就是负的。一会++到等于max的时候,给n乘个-
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * flag;
}
if条件如果改成d>*this编译不通过。因为显示调用的话是d.operator>(*this)
d是const对象,d的this指针也是const的,>的运算符重载没用const修饰,就是this指针的权限放大。
用const修饰的成员函数就是const成员函数。
如:void Print ()const
这个const修饰的是隐含的this指针,加上后不能对this指针指向对象的成员变量进行修改。
this指针的类型为const Date*const this
注意:如果声明和定义分离编译,const在声明和定义都要加。
const对象不能调用非const成员函数(this指针,权限放大)。
非const对象可以调用const成员函数。this指针权限缩小。凡是成员函数内部不改变*this对象成员变量的,都应该用const修饰成员函数。