类的默认成员函数
任何一个类,在我们没有显式的定义的情况下,编译器都会自动生成6个默认成员函数。
构造函数、析构函数、拷贝构造和赋值重载是我们学习的重点。
构造函数
很多时候创建一个对象都需要初始化。在C语言中,我们要专门写一个初始化函数并调用它,有点麻烦,而且还很容易忘记去调用。C++的构造函数则弥补了这个坑。
构造函数是特殊的成员函数,它的任务是初始化对象,对象实例化时编译器会自动调用对应的构造函数。
它的特征如下:
- 函数名与类名相同。
- 无返回值。
- 构造函数可以重载。
对于以下日期类,这里写了两个构造函数
class Date
{
public:
Date() //构造函数
{
_year = 1;
_month = 1;
_day = 1;
}
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;
};
void test1()
{
Date d1;//构造函数在此时自动调用
d1.Print();//结果:1-1-1
Date d2(1919, 8, 10);//调用了第二个构造函数
d2.Print();//结果:1919-8-10
}
👆:注意:要调用无参的构造函数,必须像上面一样,写成Date d1;
,而不能写成Date d1();
,这么写就变成了函数声明。
也可以使用缺省参数,有了这个构造函数,上面两个都不需要了。
注意:全缺省构造函数和无参构造函数构成函数重载可以同时存在,但是不能无参调用,因为会产生歧义。
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
- 概念:无参构造函数和全缺省构造函数都称为默认构造函数,因为它们的调用都可以不传参。并且默认构造函数只能有一个。默认构造函数都是默认成员函数。
编译器生成的默认构造函数:
如果我们没有显式的定义构造函数,编译器就会自动生成一个无参的默认构造函数,一旦显式定义,编译器便不再生成。
- 注意:
-
默认生成的构造函数对于内置类型成员不做处理,对于自定义类型的成员变量才会处理
-
内置类型/基本类型:比如int/char/double/指针…
-
自定义类型:class/struct定义的类型
-
下面定义一个A
类,Date
类中增加一个A
类型的成员变量,Date
类中我们不写构造函数,看看结果如何。
class A
{
public:
A()
{
cout << "A()" << endl;
_a = 0;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
void test1()
{
Date d1;
d1.Print();
//结果:
//A()
//-858993460--858993460--858993460
}
最后我们发现,A
类的默认构造函数被调用了,_a
被初始化为0,Date
类的其他三个内置类型成员变量都是随机值。
对于自定义类型成员变量的处理方式就是再去调用它的默认构造函数。
总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的构造函数,如果有内置类型的成员,或者需要显式传参初始化,那么都要自己实现构造函数。
这其实是C++设计的一个缺陷,为了弥补,C++11支持对内置类型成员变量给缺省值,供编译器生成的默认构造函数使用。
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1; //缺省值
int _month = 1;
int _day = 1;
A _aa;
};
void test1()
{
Date d1;
d1.Print();
}
//结果:
//A()
//1-1-1
析构函数
析构函数也是特殊的成员函数,它的功能与构造函数相反,它是在对象销毁时自动调用,完成类的一些资源清理工作。
特性:
- 析构函数名是
~
+类名。 - 无参数无返回值。
- 一个类有且只有一个析构函数,若未显式定义,系统会自动生成默认的析构函数
class Date
{
public:
~Date() //析构函数
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
void test1()
{
Date d1;
}
//结果:~Date()
其实日期类没有资源需要清理。内部有动态分配空间的需要清理资源,否则会造成内存泄漏,比如我们之前学的栈
在C++中的构造函数和析构函数写法如下:
class Stack
{
public:
Stack(int capacity = 10) //构造函数
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
~Stack() //析构函数
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
- 小知识:析构函数的调用顺序和构造函数相反,因为先定义的对象后销毁,后定义的对象先销毁。
下面我们来验证一下,使用这个Stack
类分别定义两个对象,初始化传入不同的_capacity
进行区分,调用析构函数时打印_capacity
来查看调用的顺序。
class Stack
{
public:
Stack(int capacity = 10) //构造函数
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
~Stack() //析构函数
{
cout << _capacity << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
void test2()
{
Stack s1(1);
Stack s2(2);
}
//结果:
//2
//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;
};
void test3()
{
Date d1(1919, 8, 10);//构造并初始化d1
Date d2(d1);//用d1去拷贝d2
d2.Print();//打印d2
}
//结果:1919-8-10
特性:
- 拷贝构造是构造函数的一个重载形式。
- 拷贝构造只有一个引用传参,使用传值调用会引发无穷递归调用。
显式的写法,注意一定要使用引用传参:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
👆:如果你不会对d
进行修改,那么建议加上const
。因为权限可以缩小,不能放大,这样可以保证const
修饰的变量也能传参。
为什么使用传值调用会引发无穷递归呢?
答:对于内置类型,我们知道它的传值调用会产生额外拷贝。那么对于自定义类型,它的传值调用就需要拷贝构造,而拷贝构造本身又需要传值调用,所以产生无穷递归。使用引用传参不需要额外拷贝,也就不会出现这个问题。
注意:对与下面的Stack类,直接使用默认的拷贝构造会出问题。
class Stack
{
public:
Stack(int capacity = 10) //构造函数
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
~Stack() //析构函数
{
cout << _capacity << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
void test2()
{
Stack s1(1);
Stack s2(s1);
}
这就涉及到深浅拷贝的问题了,默认的拷贝构造函数是浅拷贝,类似于使用memcpy直接把这个对象的值复制给了另一个对象,导致两个对象的_a
指针指向的是同一块空间,接着调用的两个析构函数使_a
指向的空间被释放了两次,进而导致程序崩溃。
这就要求我们必须自己实现一个深拷贝构造函数。
- 对于内置类型完成浅拷贝,对于自定义类型的成员,则会去这个成员的拷贝构造。
运算符重载
特性
内置类型可以直接使用<
,>
,==
之类的运算符进行比较,自定义类型也想用这些怎么办呢?C++的运算符重载解决了这个问题。
运算符重载是一种特殊的函数,它的实现需要用到关键字operator
函数原型:返回值类型
operator
+要重载的运算符(参数列表);
operator
+要重载的运算符就是该函数的函数名。参数的个数由运算符的操作数决定。
返回值就是运算符运算后的结果。
如对Date类重载==
:
如果运算符重载写在类外面,就像这样:
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
那么问题来了,因为要访问类里面的私有成员,就必须把类里面的数据设成公有,或者提供访问私有成员的函数接口,或者设置友元,这些方法要么太繁要么破坏封装,都不合适。
我们先把成员变量设置成公有试一下:
void test4()
{
Date d1(1919, 8, 10);
Date d2(1919, 8, 10);
if (operator==(d1, d2))//①
{
cout << "operator==(d1, d2)" << endl;
}
if (d1 == d2)//②
{
cout << "d1 == d2" << endl;
}
}
//结果:
//operator==(d1, d2)
//d1 == d2
👆:以上两种形式的调用是等价的,不过为了可读性我们肯定是选②。
其实把运算符重载直接写在类里面才是最优的:
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
这样写依然是两个参数,因为还有一个隐含的this指针。
调用时d1 == d2
等价于d1.operator==(d2)
注意:如果上面两个重载函数同时存在,编译器会优先调用类里面的。
重载<
:
bool operator<(const Date& d)
{
return ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day));
}
注意:
- 不能通过连接其他符号来创建新的运算符:如operator@
- 重载运算符至少有一个自定义类型参数,我们不能直接对内置类型重载。
- 运算符重载作为成员函数时看起来会少一个参数,其实还有一个隐含的this指针。
.*
::
sizeof
?:
.
这5个运算符不能重载👈:面试常考
赋值重载
拷贝构造是以一个已经存在的对象去初始化另一个要创建的对象,赋值重载则是两个已经存在的对象之间赋值。
赋值重载是类的默认成员函数之一。
Date类里面我们显式的去写:
注意:
- 内置类型的赋值是有返回值的,因为要支持连续赋值,如
i = j = k
,k
赋值给j
后应该返回j
,然后j
才能赋值给i
- 要防止自己给自己赋值
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
我们不写,编译器自动生成的赋值重载会完成浅拷贝。
const成员
特性
先来看下面这个程序
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;
};
void func(const Date& d)
{
d.Print(); //此处报错:对象含有与成员 函数 "Date::Print" 不兼容的类型限定符
}
void test5()
{
Date d(1919, 8, 10);
func(d);
}
为什么会报错呢?
原来是因为this指针出现了权限放大,因为d
是由const
修饰,&d
的类型就是const Date*
而this指针的类型默认是Date* const
,指针指向的内容变成了可以改变,是权限的放大。
上篇说过,参数里隐含的this指针我们不能显式地去写,那么在哪加这个const呢?
我们可以直接加在成员函数的后面:
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
这样this指针的类型就变成了const Date* const
注意:
- 建议成员函数内不修改成员变量的都加上
const
,这样普通对象和const
对象都可以调用。 - 如果声明和定义分离,声明和定义都要加
const
const
成员函数不能调用其他非const
成员函数
取地址运算符重载
显式地去写:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
这两个我们一般不会去自己写,编译器自己生成的就够了。