文章目录
前言
这次,我们继续上次的学习,开始类与对象(中)的学习,这次的内容有可能第一次无法很好的理解,请大家耐下性子去学,加油呀!大家!
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
让我们来看看这6个默认成员函数分别是什么吧。
好了,了解了这6个默认成员函数,让我们来详细了解它们吧。
一、构造函数
例子如下:
class Date
{
public:
void SetDate(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year;
int _month;
int _day;
};
像上面的例子,在之前,我们创造对象都需要通过公用的方法给对象设置对象,少量还好:但是一旦要要创造的对象多了,每次都这么搞会很麻烦,特别会容易忘记初始化。
为了避免这种情况,C++引入了构造函数。
1.概念
构造函数是特殊的成员函数,虽然构造函数的名称叫构造,但是注意:构造函数的主要任务并不是开空间,而是初始化对象。
2.特征
- 函数名与类名相同
- 无返回值
- 对象实例化时,编译器自动调用对应的构造函数
- 构造函数可以重载(这就说明了一个类中,可以有无数个构造函数)
例子如下:
class Data
{
public:
//无参构造函数
Date()
{}
//带参构成函数
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造函数
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就变成了函数声明
Date d2(2015,1,1);//调用带参的构造函数
return 0;
}
注意:这边是为了更好的给同学们展示构造函数,在一个类中无参构造函数和带参构造函数是不能放在一起的,如果像上面那样写的话,编译器就分不清所要弄的是对象还是函数名了。
我们知道构造函数是6个默认成员函数中的一个,如果我们不写它,编译器就会自动生成一个构造函数
例子如下:
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;
//没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
}
那编译器自动生成有没有什么需要注意的呢?我们现在来看下:
就在这个时候,编译器就会区别对待:
C++把类型分成内置类型(基本类型)和自定义类型
内置(基本)类型:语言原先定义的类型,如:int,char,double…还有指针
自定义类型:我们使用class/struct自己定义的类型
对于内置(基本)类型:不初始化,随机值
对于自定义类型:初始化
这时想,我们不写,编译器会默认生成,我们自己写无参和全缺省构造函数,总结一下:大多数情况下,都要我们自己写构造函数完成初始化,并且建议一下尽量写全缺省的构造函数,以方便适应各种场景。
二、析构函数
1.概念
析构函数不是完成对象的销毁,局部对象销毁工作是由编译器来完成的。对象在销毁时,会自动调用析构函数,完成类的一些资源清理工作。
2.特征
- 析构函数名是在类名前加上字符~
- 无参数无返回值
- ** 一个类有且只有一个析构函数**。若未显示,系统会自动生成默认的析构函数
- 对象生命周期结束时,C++编译系统自动调用析构函数
注意:析构函数自动调用时,规定和构造函数相同,对内置类型成员不处理,对自定义类型成员,会调用。
代码如下(示例):
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity
}
~SeqList()
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
那我们想想,当创建了多个对象,构造和析构的顺序是什么呢?
例子如下:
class Strack
{
public:
void Push(int i=1)
{...}
~Strack()
{...}
private:
int _i;
}
int main()
{
Strack st1;
st1.Push(1);
Strack st2;
st2.Push(1);
return 0;
}
因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也符合后进先出。
这里插个问题,有些同学有个疑问,数据结构的栈和我们讲的内存分段区域也有一个叫栈和堆,它们之间有什么区别和联系?
首先,我们讲的内存分段区域是操作系统。
联系: 数据结构和分段栈(函数栈帧)中的对象都符合后进先出。
区别:它们之间没有绝对的联系,以为它们属于两个两个学科的各自的一些命名,一个是数据结构,一个是 分段(一段 内存的命名)。
三.拷贝构造函数
1.概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰)
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
我们看拷贝构造函数特征的第二点,说必须使用引用传参,使用传值方式会引起无穷递归调用,让我们来看下,为什么会引起无穷递归调用吧。
Date(Date d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
//其它和上面的例子相同
int main()
{
Date d1;
Date d4(d1);
return 0;
}
这个时候要调用拷贝构造函数,就要先传参,传参传值,又是对象拷贝构造函数,循环往复的过程。
过程如下:
拷贝构造函数是默认成员函数。我们不写,编译器会自动生成拷贝构造函数。这个拷贝构造函数和之前的构造函数和析构函数不同,这个拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。
例子如下:
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 d4(d1);
cout<<d1<<endl;
cout<<d4<<end1;
return 0;
}
输出的答案都是1900 1 1,那浅拷贝是什么呢?我们现在先大概了解下。
浅拷贝
浅拷贝:拷贝类的对象时,将拷贝其指针成员,但是没有复制指针指向的缓冲区,这样的话,结果就是:两个对象指向同一块动态分配的内存。
这样的话,像之前的Date类就还行,但是如果要是碰上一下类,我们不写就会遇上问题。
例子如下:
class String
{
public:
String(const char* str="jack")
{
_str=(char*)malloc(strlen(str)+1);
}
~String()
{
cout<<"~String()"<<endl;
free(_str);
}
private:
char* _str;
};
int main()
{
Stirng s1("hello");
String s2(s2);
retrun 0;
}
如果有人去试了这个程序的话,会发现这个程序会崩溃,原因有两点:
- 调用析构函数时,这块空间被free了两次。
- 其中一个对象插入删除数据时,都会导致另一个对象也插入删除数据。
像String这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,不满足我们的需求,需要自己实现深拷贝。(深拷贝会在以后的讲解中了解的)
四.赋值运算符重载
1.运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类
型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表) 。
注意:
1. 不能通过连接其他符号来创建新的操作符:比如operator@
2. 重载操作符必须有一个类类型或者枚举类型的操作数
void operator(int i,int j)错误
3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5. .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
6.运算符可以被多次重载
例子如下:
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;
}
这时候,有些同学看到上面的代码有个不认识的关键字bool,我们现在来了解一下这个关键字吧
布尔数据类型(bool)
作用:表示真或假的值,真——true(本质为1),假——false(本质为0),bool类型的数据占用一个字节
运算符重载和函数重载
运算符重载和函数重载,都用了重载这个词,但是这两个词之间没有关联
- 函数重载支持定义同名函数
- 运算符重载时为了让自定义类型可以像内置类型一样去使用用运算符
2.赋值运算符重载
赋值运算符重载也是拷贝行为,但是不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝。这里是赋值拷贝时连个对象已经都存在了,都初始化了,现在想把一个对象,赋值拷贝给另一个对象。
例子如下:
//其它相关代码参考上面
Date& operator(const Date& d)
{
//检查如果不是自己给自己赋值,才需要拷贝
if(this!=&d)
{
_year=d.-year;
-month=d.-month;
-day=d.-day;
}
return *this;
}
要注意的点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
赋值构成函数也是默认成员函数,和拷贝构造函数一样,针对内置类型,会完成浅拷贝,也就是像Date这样的类不需要我们自己写赋值运算符重载;针对自定义类型也一样,它会调用它的赋值运算符重载完成拷贝。
现在让我们来看一个代码,来看看同学们是否能区分出来拷贝构造函数和赋值重载函数
例子如下:
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;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d5;
Date d2;
Date d5(d1);
d1=d2;
Date d6=d1;
return 0;
}
对于Date d5(d1); 和d1=d2;,大家能很快地想出来,前一个时拷贝构造函数,后一个是赋值符重载,那Date d6=d1;是什么呢?
这时候我们就要看拷贝构造和赋值重载的含义了,拷贝构造,是拿一个已经存在的对象去构造初始化另一个要创建的对象;赋值重载,是两个已经存在的对象进行拷贝。
然后,我们看d6是要创建的对象,d1是已经存在的对象,和拷贝构造函数一样,所以 Date d6=d1;是拷贝构造函数。
总结
今天的类与对象的学习就到这里,过几天,我(有可能)就会更新中,都看到这里,不关注一下嘛?只要动动你的小手指。