类与对象
前言
在上篇的学习中我们了解了类的概念与其中的一些全新的例如:封装,访问限定符还有this指针等类与对象篇章中基础的打底知识,而这篇中我们就会学习六大默认函数中的构造函数,析构函数,拷贝构造函数,运算符重载函数,取地址及const取地址操作符重载这六种。让我们开始吧!
一.类的六大默认函数
当一个类里什么都没有的话那就称为空类。
但是一个空类中并不是什么都没有,编译器会自动生成六个默认的成员函数。
默认成员函数:用户没有显式实现,编译器自动生成的成员函数。
二.构造函数
2.1构造函数的定义
对于Date类:
class Date
{
public:
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.DateInit(2023, 7, 30);
d2.DateInit(2023, 7, 31);
d1.DatePrint();
d2.DatePrint();
return 0;
}
按C语言的模式来,我们在定义了一个类后通常在成员函数中要定义一个初始化的函数,不仅如此日常使用中还会经常遗忘初始化从而产生错误。
而在C++中,我们有更加方便和快捷的方式来完成这个工作即构造函数。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
class Date
{
public:
Date()//构造函数
{
_year = 2023;
_month = 7;
_day = 31;
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.DatePrint();
d2.DatePrint();
return 0;
}
这只是构造函数的一种用法,我们可以运用之前的知识例如缺省值和函数重载来对构造函数的形式进行变换。
2.2构造函数的特点
在上面我们已经了解了,构造函数的用处不是用来“构造”一个新的对象,而是代替原理的Init函数对对象进行初始化!
构造函数的特性:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载
以下是构造函数的不同形式:
class Date
{
public:
Date()//无参
{
_year = 2023;
_month = 7;
_day = 31;
}
//Date(int year, int month, int day)//有参
//{
// _year = year;
// _month = month;
// _day = day;
//}
//Date(int year = 2023, int month = 7, int day = 31)//有参并全缺省
//{
// _year = year;
// _month = month;
// _day = day;
//}
//Date(int year, int month = 7, int day = 31)//有参并半缺省
//{
// _year = year;
// _month = month;
// _day = day;
//}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//无参时
//Date d1(2023, 7, 31);//有参
//Date d1();//有参并全缺省
//Date d1(2023, , );//有参并半缺省
d1.DatePrint();
return 0;
}
各类的构造函数形式都在此了
- 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。
- 全缺省的构造函数和无参的构造函数在语法上构成了函数重载但是在实际使用不要使用,因为编译器会出现不能明确调用函数的情况。
2.3默认构造函数
而在我们没有学习构造函数之前,我们没有定义构造函数但是编译器自动生成了构造函数,这种构造函数被称为默认构造函数。但默认构造函数不止如此:
默认构造函数:无参的,全缺省的,没有显式实现所以编译器自动生成的构造函数都称为默认构成函数。
class Date
{
public:
//Date()//无参
//{
// _year = 2023;
// _month = 7;
// _day = 31;
//}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.DatePrint();
return 0;
}
从结果中我们看见如果使用系统自带的默认构造函数会使对象为随机值,那么可以说系统自带的默认构造函数通常是无用的,即我们需要自己写出构造函数。
而Date类的对象并不能代表全部的类,比如:如果类里面定义的一个对象是另外一个类呢?构造函数是对进行初始化,对象为类的话系统的默认构造函数会怎么使用呢?
在这里我们需要介绍一个新的概念:内置类型和自定义类型
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,
如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认构造函数。
class Time
{
public:
Time(int hour = 23, int minute = 35, int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year, int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time t;
};
int main()
{
Date d1(2023, 7, 31);
d1.DatePrint();
return 0;
}
在我们一步步调试该代码时我们会发现在对Date类的内置类型对象进行初始化前会先对其中的自定义类型对象调用它的构造函数先进行初始化。
在C++11中对内置类型成员变量进行了优化,即内置类型成员变量可以在声明时可以给默认值,这个又关联到了构造函数的另外一个知识:初始化列表。这个知识我们会在类与对象下中学习到,现在我们只需要知道可以给默认值即可
三.析构函数
在学习了构造函数之后,我们了解了类中是如何进行初始化的即对照着C语言的Init函数而Destroy则是对应着析构函数。
3.1析构函数的定义
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
class Stack
{
public:
Stack(int capacity = 4)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
_capacity = capacity;
_size = 0;
}
void CheckCapacity()
{
if (_size == _capacity)
{
int* tmp = (int*)realloc(_array, sizeof(realloc) * _capacity * 2);
if (tmp == nullptr)
{
cout << "realloc fail" << endl;
exit(-1);
}
}
}
void Push(int x)
{
CheckCapacity();
_array[_size] = x;
_size++;
}
//void Destroy()//销毁函数
//{
// free(_array);
// _capacity = 0;
// _size = 0;
//}
~Stack()//析构函数
{
free(_array);
_capacity = 0;
_size = 0;
}
private:
int* _array;
int _size;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
//s1.Destroy();
}
3.2析构函数的特点
析构函数的很多特点和构造函数是相同的,我们可以联合记忆
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构
注意:
1.系统默认生成的析构函数会对自定义类型的成员调用它的默认析构函数进行清理,而内置类型的成员是不需要析构函数进行清理的,在程序最后系统会进行清理。
2.创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
3.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
四.拷贝构造函数
在了解了C++中的类是如何初始化和销毁的后我们遇见了新的问题。在C语言中我们知道函数的形参只是实参的一个临时拷贝,但我们那时学习的都是内置类型,在遇见如今的自定义类型时这种简单的浅拷贝已经会出现问题了,所以C++添加了拷贝构造函数这一默认函数。
4.1拷贝构造函数的定义
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的同类型的对象创建新对象时由编译器自动调用。
class Date
{
public:
Date(int year, int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 2);
Date d2(d1);//拷贝构造
d1.DatePrint();
d2.DatePrint();
return 0;
}
4.2 拷贝构造的特点
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
class Date
{
public:
Date(int year, int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date d)//错误形式,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 2);
Date d2(d1);
d1.DatePrint();
d2.DatePrint();
return 0;
}
注意:这里的无穷递归是由于Date d2(d1)时需要将值d1传给d2,但是上文我们说了这种的形参是实参的一个临时拷贝所以又会调用拷贝构造函数如此往复导致无穷递归。
3.如果没有显式实现拷贝构造函数,系统会自动生成默认的拷贝构造函数,默认的拷贝构造会将内置临时进行浅拷贝(值拷贝)而自定义类型则是调用它的拷贝构造函数。
class Time
{
public:
Time(int hour = 23, int minute = 35, int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
Time(Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year, int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
_t = d._t;//调用Time类的拷贝构造函数
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
Time _t;
};
那么什么样的类需要我们自己写拷贝构造函数呢?
例如上面的日期类,我们用浅拷贝就绰绰有余所以就不用写拷贝构造函数
而像我们之前在数据结构中学习的栈,队列等则是需要我们自己写拷贝构造函数
class Stack
{
public:
Stack(int capacity = 4)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
_capacity = capacity;
_size = 0;
}
Stack(Stack& s)//深拷贝
{
_array = (int*)malloc(sizeof(int) * s._capacity);
if (_array == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
memcpy(_array, s._array, sizeof(int) * s._size);
_capacity = s._capacity;
_size = s._size;
}
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
int* tmp = (int*)realloc(_array, sizeof(int) * newcapacity);
if (tmp == nullptr)
{
cout << "realloc fail" << endl;
exit(-1);
}
_capacity = newcapacity;
_array = tmp;
}
}
void Push(int x)
{
CheckCapacity();
_array[_size] = x;
_size++;
}
void Print()
{
for (int i = 0; i < _size; i++)
{
cout << _array[i] << ' ';
}
cout << endl;
}
~Stack()
{
free(_array);
_capacity = 0;
_size = 0;
}
private:
int* _array;
int _size;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2 = s1;
s1.Print();
s2.Print();
return 0;
}
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
这里解释为什么需要这些资源申请的类为什么要自己写构造函数,以栈举例:
因为它们开辟了动态空间,如果不进行深拷贝而是值拷贝的话,s2的_array就是s1的拷贝,它们指向的空间都是同一块,如下图
而在程序结束之后s2会先调用析构函数对_array指向的空间进行释放,这时s1的_array指向的空间已经是一块被释放的空间,而后s1调用析构函数会对这块已经释放的空间再次进行释放,这就造成了野指针问题。
所以申请了内存的类都需要我们自己进行拷贝构造函数的编写。
拷贝构造函数典型调用场景:
1.使用已存在对象创建新对象
2.函数参数类型为同类型对象
3.函数返回值类型为同类型对象
注意:在拷贝构造时如果本身是不想进行类的修改的话,最好在析构函数的参数前加const
Stack(const Stack& s)
{
_array = (int*)malloc(sizeof(int) * s._capacity);
if (_array == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
memcpy(_array, s._array, sizeof(int) * s._size);
_capacity = s._capacity;
_size = s._size;
}
五.运算符重载
在解决了初始化,清理,复制后,我们就需要考虑如果对类进行更改的工作了。例如现在的日期类我们连最基础的大小比较和加减天数都做不到,这是因为我们的运算符只考虑到了内置类型,往常我们只能定义一个专门的函数来做这种工作,这种方法不仅麻烦而且函数名花里胡哨让人琢磨意思,那我们有没有办法让自定义类型也能使用运算符呢?
5.1运算符重载的定义
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。 函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个自定义类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
5.像.* :: sizeof ?: . 这五个运算符不能重载。这个经常在笔试选择题中出现。
这里一样用Date类进行举例:
class Date
{
public:
Date(int year, int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator== (const Date& d)//重载成成员函数操作数就会减少1,因为有隐藏的this指针
//相当于operator(Date* this ,const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
void DatePrint()
{
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;
//}
//上面的代码有着明显的错误:成员函数都是私有的我们在类外怎么可以访问到它呢?
//我们其实有两种方法可以解决这种错误:1.友元函数(后面会学到)2.重载成成员函数
int main()
{
Date d1(2023, 8, 2);
Date d2(2023, 8, 1);
cout << (d1 == d2) << endl;
//cout << (d1.operator(d2) << endl;
return 0;
}
5.2赋值运算符的重载
赋值运算符重载格式
1.参数类型:const T&,传递引用可以提高传参效率
2.返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
3.检测是否自己给自己赋值
4.返回*this :要复合连续赋值的含义
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date operator=(const Date& d)//赋值运算符的重载
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void DatePrint()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 2);
Date d2(2023, 8, 1);
d1.DatePrint();
d2.DatePrint();
d1 = d2;
d1.DatePrint();
d2.DatePrint();
return 0;
}
注意:
赋值运算符只能重载成成员函数,不能重载成全局函数,原因如下:
1.赋值成全家函数后this指针消失操作符变为两个,减少了代码的可读性。
2.赋值运算符的重载函数是一个默认函数,如果用户没有自己编写那么编译器会在类中自己生成一个默认的赋值运算符重载函数这会和类外自己定义的重载产生冲突。
上面提到了赋值运算符的重载函数是一个默认函数,我们不显式定义的话编译器会自动生成而默认的默认赋值运算符重载函数,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
这里的情况和上面拷贝构造函数是相同的,对于像Date类这种没有涉及到资源管理的类是可以不写赋值运算符重载函数的但是像栈这些类是必须要写赋值运算符重载函数的。原因和拷贝构造函数相同不再过多阐述。
5.3前置++和后置++的重载
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符的重载
Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//获得某月的天数
int GetMonth(int year,int month)
{
int month_array[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return month_array[month];
}
//日期 + 天数
Date operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
//日期 += 天数
Date& operator+=(int day)
{
_day += day;
while(_day > GetMonth(_year, _month))
{
//月未满
_day -= GetMonth(_year, _month);
++_month;
//月满了
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
//日期后置++
Date operator++()
{
Date tmp = *this;
*this += 1;
return tmp;
}
//日期前置++
Date& operator++(int)
{
*this += 1;
return *this;
}
//打印
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 2);
Date d2(2023, 8, 1);
d1.Print();
d2.Print();
d1++;
++d2;
d1.Print();
d2.Print();
return 0;
}
//前置++是先将*this+1再返回*this
//后置++是创建一个临时变量tmp,然后再将*this+1后返回这个临时变量tmp。
//而这个传值返回和传引用返回的使用则是我们之前总结过的,返回的为临时对象就用传值返回,返回的对象不会被销毁就用传引用返回。
Q:为什么前置++的参数中需要加一个int呢?
A:因为当前置++和后置++重载为全局函数时就只有一个操作数,而重载成成员函数后这个操作数也被this指针顶替所以为了区分出前置和后置的区别就加上了这个int参数。但是调用函数时这个参数是不需要我们传递的,编译器会自动为我们传递。
六.const成员
在上面的学习中我们使用了很多次const来修饰参数,而const不仅可以修饰这些还可以修饰成员和成员函数。
const成员的定义:
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
而const的出现同时也会引出权限的放大的问题
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//void Print() //错误写法,传进来的d1为const成员无法修改而隐藏参数是Date* this从而引发权限的放大
void Print()const
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const Date d1(2023, 8, 2);
Date d1;
d1.Print();
d2.Print();//可以调用const成员函数
return 0;
}
注意:
1.牢记权限可以缩小和平移但是不能放大。
2. const对象不可以调用非const成员函数。
3. 非const对象可以调用const成员函数。
4. const成员函数内可以调用其它的非const成员函数。
5. 非const成员函数内不可以调用其它的const成员函数。
七.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
总结
在中篇里我们学习了六大默认函数,囊括了初始化,清理,复制,运算符等几大方面。里面的知识繁琐且复杂需要我们不止一遍的学习,而且在其中还有一些我们要进一步学习的,比如:构造函数中提到的初始化列表,运算符重载里的友元的概念。这些都需要我们在类与对象下篇中进行学习了,在学习了类与对象下后我们就应该可以将这些知识融会贯通让人惊呼"妙“字真言。尽情期待吧!