类中默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
1. 构造函数
1.1 背景
在C语言中,我们在创建栈,用到栈的变量是否都要先初始化,size == 0 ,capacity ,top之类都要初始化。
而在C++中,为了解决这种烦恼添加一个函数,叫构造函数。
1.2 特性
构造函数是默认成员函数,是特殊的。它的主要工作是完成对对象的初始化,而不是构造(别被名字蒙蔽了)。
特性:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
1.2.1 类中有无构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 。
这里就有个问题了
显示构造函数是自己定义的,知道怎么初始化。
但编译器自己生成的构造函数,初始化了什么?怎么初始化的呢?
看上图,变量是随机值,说明编译器没有处理内置类型。
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
以上是内置类型的初始化,那对于自定义类型,编译器又是如何处理呢?
编译器打印了num1,说明编译器自动生成的构造函数在处理自定义类型的时候,会将调用自定义类型的默认的构造函数,实现初始化。
总结:
- 编译器在有显式构造函数,会直接调用。
- 编译器在没有显式构造函数,会自动生成构造函数,对于内置类型不会处理,对于自定义类型会调用该类型的默认构造函数。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
1.2.2 类中构造函数的参数
1.2.2.1 无参数
构造函数在初始化手动赋值。
1.2.2.2 有参数
构造函数是支持带参数的,也支持缺省。
1.2.2.2.1 构造函数一些小特殊
在创建对象有参数传递,如d1直接在后面接参数。在无参数传递,如d2后面什么都带。
假如写成d3,会报错,会被编译器当做是函数声明,Data是返回值,d3是函数名,()是参数。
1.2.3 构造函数的函数重载
函数名相同,参数列表不同
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
}
2. 析构函数
2.1 背景
在C语言中,我们常常会遇到需要开辟新空间,开辟空间相对就要释放它,否则会造成内存泄漏。
在C++中,为了防止程序员忘记释放空间,增加了析构函数,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.2 特性
析构函数是特殊的成员函数,它的特征:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载(构造函数可以支持重载)
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
这里我们看反汇编,会更直观看到对象生命周期结束时,是否会调用析构函数。
他有一条是call Stack::~Stack,说明他调用了。(一般反汇编call 啥啥啥的,就是在调用该函数)
2.2.1 类中有析构函数
类中有析构函数,就是我们显式定义了析构函数,编译器就不会去调用默认的析构函数。
如果析构函数中,有自定义类型,编译器会如何执行呢?
上图,我在Stack析构函数中创建Time类的对象,最后编译器输出了~Time()。说明Time类的析构函数被调用了。
也就可以理解为在析构函数中,自定义类型也会被创建,会在其生命周期结束时,调用该类型的析构函数。
2.2.2 类中无析构函数
没有析构函数,编译器会自动生成析构函数,这是它的特性之一。
默认的析构函数,会对自定义类做如何处理?
编译器输出了~Time(),说明Time析构函数被调用了。
问题这就来了,在main方法中,只创建Data对象,并没有创建Time对象,而Data对象也没有显式的析构函数,Time对象的析构函数如何被调取的呢?
还记得若无显式析构函数,编译器会自动生成默认析构函数吗,关键就在这里了。
这个默认析构函数,对于内置成员变量是不需要销毁的,生命周期结束,系统会自动回收。
对于自定义类型,在它的外部对象(Data)要销毁的时候,顺带要销毁它的里面的自定义对象(Time)。所以类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。
3. 拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
3.1特性
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式(本质上是函数重载)。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用。
3.1.1 拷贝构造函数本质是函数重载
函数名相同,参数列表不同,符合函数重载
class Date
{
public:
//构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
3.1.2 拷贝构造函数的参数只有一个且必须是类类型对象的引用
如果使用传值方式会引发无穷递归调用 。
3.1.3 拷贝函数对于内置类型和自定义类型的处理
-
若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。 -
内置类型,生成默认拷贝是做处理的(不同于构造函数和析构函数),直接完成拷贝
-
自定义类型,会去调用它的拷贝函数,若没有显式的,会生成默认拷贝做处理,完成拷贝任务。
但是,会出现问题。
3.1.4 默认拷贝构造处理自定义类型
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
这边通过调试,发现值有正常的传输,没问题!
真的是这样吗?来看接下来的代码
class Stack
{
public:
//构造函数
Stack(size_t capacity = 3)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = NULL;
_capacity = 0;
_size = 0;
}
}
private:
int* _a;
int _capacity;
int _size;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
上面代码执行,编译器报错了,是挂在析构函数里面的。
OK,让我来一步步分析。
s1,s2都创建成功了,但是比较奇怪的是s1和s2里面的a为什么是指向同一个地址的?
指向同个地址的a,当对象销毁时,进入析构函数先将s2里面的a free掉了(对象是创建在栈里的,栈先进后出,所以会先析构后创建的对象),到现在都没问题。好了,当s1进入析构函数了,准备free掉a时,发现a已经被释放掉了,编译报错,就寄掉了。
总结:
- 自定义类型会完成值拷贝(浅拷贝)。
- 自定义函数在处理这些动态开辟的空间时,是浅拷贝是没办法很好的处理的,就必须依靠显式的拷贝构造函数来处理,不然就会出现上面的情况。这种给申请的空间拷贝构造叫深拷贝。
3.1.4 拷贝构造使用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
现实我们其实要减少拷贝构造的使用的,这样可以提高代码执行效率。
对于函数的返回值
- 出了作用域,对象还在,用引用返回
- 出了作用域,对象销毁了,就不能用引用返回了,要用拷贝构造。
4. 运算符重载
4.1 背景
对于内置类型的运算符,编译器是知道按照什么方法计算,然后有相应返回值。
对于自定义类型,编译器是不知道我们按照什么方法计算,所以就会用到运算符重载。
4.2特性
-
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表) -
不能通过连接其他符号来创建新的操作符:比如operator@
-
重载操作符必须有一个类类型参数
-
用于内置类型的运算符,其含义不能改变。(+,就是加,不管加什么)
-
作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针
-
注意有5个运算符不能重载。
.* :: sizeof ?: .
4.2.1 运算符重载的本质是转化成调用operator
class Fun
{
public:
//构造函数
Fun(int num = 0)
{
_num = num;
}
//运算符重载 >
bool operator> (const Fun& f)
{
if (_num > f._num)
{
return true;
}
else
{
return false;
}
}
private:
int _num;
};
int main()
{
Fun f1(100);
Fun f2(20);
cout << (f1 > f2) << endl;
return 0;
}
编译器调用了opeartor>
4.2.2 作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针
由于this指针的存在,所以在我们使用运算符重载的时候,参数传递的顺序是很重要的,如 f1 > f2,f1的地址会是this指针,不要搞反了。
4.3 赋值运算符
4.3.1 背景
赋值运算符是运算符重载中比较特殊的,它的特殊在于它的返回值。在一段不断赋值的指令,a1 = a2 = a3 (a1,a2,a3都是内置类型)可以看出,a2 = a3赋值完后,将它的返回值再赋值给a1。这样的特性,我们在设计赋值运算符时也得考虑。
4.3.2 特性
赋值运算符是对已经存在的两个对象之间的拷贝(区别于构造函数)。
-
参数类型:const T&,传递引用可以提高传参效率
-
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
-
检测是否自己给自己赋值
-
返回*this :要复合连续赋值的含义
-
赋值运算符只能重载成类的成员函数不能重载成全局函数
-
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
4.3.2.1 默认赋值运算符处理内置类型和自定义类型
- 内置类型,编译器会以值传递进行拷贝
- 自定义类型,编译器会调用它的赋值运算符。若没有显式,会用默认的,进行值拷贝。
简单的值拷贝(浅拷贝)会出现与运算符重载相同的问题,无法处理好申请的空间。
此时就要手动显式定义赋值运算符。
4.3.2.2 前置++,后置++
前置++与后置++,在定义的时候,注意后置++的参数是有个int的,这个int是没有实际意义的,仅仅作为占位。
它们的返回值也各不同:
- 前置++,要符合先++的原则,然后把加完后的结果返回,返回*this也就是本身,返回值是引用(返回值本来是值返回的(Fun),但考虑到值返回(Fun)会调用拷贝构造,这里优化成引用,提高效率)。
- 后置++,要符合后++的原则,利用临时变量存储++前的数据,同样返回的也是这个临时变量(对于返回的是临时变量,就无法使用引用返回,必须是值返回,调用拷贝构造)
class Fun
{
public:
//构造函数
Fun(int num = 0)
{
_num = num;
}
//前置++
Fun& operator++()
{
_num += 1;
return *this;
}
// 后置++:
Fun operator++(int)
{
Fun temp(*this);
_num += 1;
return temp;
}
private:
int _num;
};
5. 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。
class Fun
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
};
5.1 const成员
上面代码有个const写在外面了,这个const的作用就是用来修饰this指针的。
对于这个const修饰,就要考虑到权限的问题。记住,权限可以平移,可以缩小,但是不能放大。