1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情 况下,都会自动生成下面6个默认成员函数。
2. 构造函数
2.1 概念
对于以下的日期类
class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.SetDate(2018, 5, 1);
d1.Display();
Date d2;
d2.SetDate(2018, 7, 1);
d2.Display();
return 0;
}
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信 息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员 都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主 要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
class Date
{
public:
/*void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//对象实例化一定保证调用构造函数
//Date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
Date(int year=2022 ,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//d1.SetDate(2018, 5, 1);
Date d1;
d1.Display();
Date d2(2011);
//d2.SetDate(2018, 7, 1);
d2.Display();
return 0;
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定 义编译器将不再生成。
class Date
{
public:
/*void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//对象实例化一定保证调用构造函数
//Date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
/*Date(int year=2022 ,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}*/
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class stack
{
public:
stack(int capicity = 4)
{
_array = (int*)malloc(sizeof(int) * capicity);
if (_array == NULL)
{
perror("malloc");
exit(-1);
}
_capicity = capicity;
}
private:
int* _array;
int _size;
int _capicity;
};
class MyQueue
{
private:
stack _st1;
stack _st2;
};
int main()
{
Date d1;
stack s1;
MyQueue q1;
return 0;
}
//C++类型分类:
//内置类型/基本类型:int/double/char/指针等等
//自定义类型:struct/class
//默认生成构造函数,内置类型成员不做处理,对自定义类型成员会去调用他的默认构造函数
默认构造函数:(特点就是不传参数就可以调用)
1.我们不写,编译器自动生成的那个
2.我们自己写的,全缺省构造函数
3.我们自己写的,无参的构造函数
这里的报错跟上面两个date没有关系,上面的两个date函数构成函数重载 ,是因为无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
这三个往往不能同时存在,其中的两个可以同时存在,d1这里调用的时候存在歧义不知道是调用第一个date还是第二个。(无参跟全缺省语法上两个存在是可以的,但是调用会出现歧义)最好的写的方式是提供全缺省函数,初始化的时候很容易。
如果在日期类之前在创造一个time类设置一个Time _t这属于自定义类型,在date类不需要写time的构造函数,编译器会自己调用
如果这里改成Time *_t的话也不会处理,不会初始化。它属于内置类型,任意类型的指针都是内置类型。
对于自定义函数一定要是默认构造函数
总结:一般的类都不会让编译器默认生成构造函数,自己写写一个全缺省,非常好用。
特殊情况下才会默认生成,比如我们已经写了一个栈,栈的构造初始化已经写好了,如果下面要用栈实现一个队列这种数据结构,我们栈已经有了初始化(自己写的构造函数)这时候的队列创造的对象就不需要初始化。
3.析构函数
3.1 概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
3.2 特性
析构函数是特殊的成员函数。 其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
全局先初始化,因为全局在main之前就要初始化,全局和静态都在静态区,在main函数之前就要初始化,所以它是最先进行的,而局部的静态的存储是在静态区的但是它第一次运行进来初始化。
析构的话由于他们三个都在静态区里面,程序结束的时候才会被销毁,而aa1和aa2在栈帧中也就是main函数被销毁的时候他们两个一次被销毁,顺序满足跟构造函数相反,剩下在静态区的也要有个顺序也符合谁先进去后销毁的顺序。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a=0)->" <<_a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
A aa3(3);
void f()
{
static A aa0(0);
A aa1(1);
A aa2(2);
static A aa4(4);
}
//构造顺序:由于在常量区的变量程序没结束之前只会建立一次,所以只有在栈区的变量会因为函数多次调用多次建立
// 顺序:f第一次调用:3 0 1 2 4,之后在调用f第二次 1 2 所以:3 0 1 2 4 1 2
//析构顺序:跟构造相反,先销毁一次2 1,之后还有第一次的2 1,之后依次是常量区的4 0 3所以:2 1 2 1 4 0 3
int main()
{
f();
f();
return 0;
}
4. 拷贝构造函数
4.1 概念
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
class date
{
public:
/*date()
{}*/
date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
date(const date &d)//date(date d)
{
//如果这里是传值来写就会无限循环,因为传入date d2(d1)这里的函数参数本身会开辟一个空间
//把d1拷贝过去,但是这个就是拷贝构造函数,相当于一直自己调自己陷入循环(传参就要拷贝构造,拷贝构造
//要传参)
cout << "拷贝构造函数" << endl;
_year = d._year;//相当于(d2)this->_year=d1.year
_month = d._month;
_day = d._day;
//如果不注意写反了,这里语法也不会报错,只不过传值的时候会传反,这里加个const限定一下
//缩小权限,把可读可写变成可读,就能报错了,避免这些问题,大部分传参的时候不需要改变就加入const修饰
/*
d._year = _year;
d._month = _month;
d._day = _day;
*/
}
/*date(date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}*/
//用指针也可以,但是就不是拷贝构造函数了,这种形式不太好,大多数不被采用,拷贝构造更符合场景。99
~date()
{
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
//Time _t;
};
void get1(date d)
{
//这个函数是d1传入的时不是直接传入,而是date d临时创造一个空间拷贝构造函数把get1(d)的值给拷贝过来
//类似函数中如果传值,是int类型接收是建立一个临时变量,将传入的值拷贝过去并非同一块地址
}
void get2(date& d)
{
//这个函数的传入是别名,d1直接开辟的空间起别名为d直接再次使用
}
int main()
{
//对象实例化的时候都要调用构造
date d1(2022,1,1);//这里的调用是调用了能传三个参数的构造函数
date d2(d1);
get1(d1);//这里的d1传入是调用了拷贝构造函数
get2(d1);
//d1.print();
// d2.print();
//date d2(2021, 1, 1);
//d2.print();
return 0;
}
3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
这里就跟前面有些区别了,构造和析构是对内置类型都不做处理,对自定义类型才会处理,内置类型除非给了缺省值否则不会初始化是随机值,析构函数内置类型不会处理,自定义的会自动调用
我们把拷贝函数屏蔽之后发现依旧可以正常打印,. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像 日期类这样的类是没必要的。这里就涉及深浅拷贝的问题了 ,日期类的拷贝是属于浅拷贝,只是赋值了。如果是栈这种类进行拷贝,它将拷贝部分省略之后进行拷贝,在析构(具体是free部分就会崩溃,free的崩溃往往不是free部分出现了问题,而是指针出现了问题)本质的问题这个是浅拷贝,st1和st2这两个栈都指向同一块空间,这里调用了两次析构函数,把同一块空间释放了两次。
无论是自己写的拷贝构造函数还是系统默认的拷贝构造函数都是值拷贝st1上这个类有(*array指针,有capicity,有size)把这些参数都进行了值拷贝,相当于array指的同一块空间,但是free调用两次,相当于同一块空间被释放了两次而产生了错误。这就类似于memcopy,对于日期类这种拷贝就需要浅拷贝,栈的size和capicity就需要这种,但是指针不需要。
浅拷贝有两大问题:
1.一个对象修改会影响另外一个
2.会析构两次,程序崩溃
如何解决,自己实现深拷贝。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a=0)->" << _a << endl;
}
A(const A& aa)
{
_a = aa._a;//这里访问的_a不是下面private的_a是this的_a下面的只是声明
cout << "A(const A& aa)->" << _a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
void func1(A aa)
{
}
void func2(A& aa)
{
}
A func3()
{
static A aa(3);
return aa;//传值返回不会返回aa,会返回aa的拷贝,之后拷贝对象会被销毁。
}
A& func4()
{
static A aa(4);
return aa;
}
A func5()
{
A aa(5);
return aa;
}
A& func6()
{
A aa(6);
return aa;
}
//传引用返回肯定比传值返回更高效,
int main()
{
func3();
cout << endl;
func4();
cout << endl;
func5();
cout << endl;
func6();
//如果aa出了func的作用域就销毁,那么引用是有问题的,func销毁了aa,main还要访问就会越界了。
return 0;
}
5.赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
内置类型可以直接使用运算符运算,编译器知道要如何运算,自定义类型无非直接用运算法,编译器也不知道要如何运算,想支持,自己实现运算符重载
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
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;
}
bool operator==(const date& x)
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
private:
int _year;
int _month;
int _day;
};
//有几个操作数就有几个参数(第一个参数代表左操作数,第二个参数代表右操作数)
//运算符重载的返回值不固定,要根据你的需求来设计。
//bool operator==(const date &x1, const date &x2)
//{
// return x1._year== x2._year
// && x1._month == x2._month
// && x1._day == x2._day;
//}
//内置类型有没有引用影响不大,但是自定义类型有没有引用影响就很大了,传值传参就要调用拷贝构造,就要有
//消耗,加了引用传参,栈的消耗减少了,后期如果使用深拷贝不用引用消耗会更大
//加入const防止写入x1._month=x2._month这种代码,这种代码语法检查不出来,但是会执行出错
//int operator-(date x1, date x2)
//{
//
//}
int main()
{
date d1, d2;
cout << (d1 == d2) << endl;//不带括号这里因为算法的优先级可能会报错,认为重载的是<<
//类外的函数调用:
//cout << operator==(d1, d2) << endl;
//相当于转换成 cout<<operator==(d1,d2)<<endl;
//类内的函数调用:
// cout << operator==(d1, d2) << endl;
// 相当于cout<<d1.operator==(&d1,d2)<<endl;
//这里的&d1我们不能写出来,因为他是由this指针接收,this指针我们不能显示传,不能显示作为参数,但是类里面
//可以用
//写成一般函数也可以,但是调用的时候就像正常调用函数一样没有意义,如果使用operator来写
//就能像内置类型一样正常使用,增强代码可读性
return 0;
}
5.2 赋值运算符重载
这里的错误我们可以拿内置int来说明:k=i=j=10;10作为右操作数赋值给了j,j是返回值,j作为右操作数赋值给了i,i作为返回值又给了k,上面的自定义类型d3赋值给了d2,但是d2我们写函数的时候返回值写成了void导致没有返回值,所以连续赋值会有错误,这里的返回值应该是d1,
赋值运算符主要有四点:
1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。