一个什么都不写的类称为空类,那么空类里真的什么成员都没有嘛,不是的,即使是空类,也有六个默认成员函数。分别是: 构造函数,析构函数,拷贝构造函数,赋值重载,取地址重载,const取地址重载。
这里会写两个类,Date(日期)类和stack(栈)来介绍这六种默认成员函数。
1. 构造函数
构造函数是用来初始化类对象的。当我们定义一个类对象时,编译器对自动调用默认构造函数来对类对象**完成初始化,构造函数可以显式调用。功能类似于Init函数。
但是构造函数有以下特征:
- 函数名和类名相同,没有返回值(不能写void,这是规定)
class Date
{
public:
//构造函数
Date()
{
//...
}
private:
int _year;
int _month;
int _day;
};
class stack
{
public:
//构造函数
stack()
{
//...
}
private:
int* arr;
int top;
int capacity;
};
- 构造函数可以重载,也就是可以有多个构造函数,比如无参构造,半缺省构造,全缺省构造。
class Date
{
public:
//无参构造
Date()
{
_year = _month = _day = 1;
}
//半缺省构造
Date(int year , int month = 10, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
class stack
{
public:
//全缺省构造
stack(int capacity = 4)
{
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == NULL)
{
return;
}
_arr = tmp;
_top = 0;
_capacity = capacity;
}
private:
int* _arr;
int _top;
int _capacity;
};
- 对象实例化时编译器自动调用对应的构造函数。
为了便于验证,我们在构造函数内加一条打印语句。
创建d1调用无参构造,创建d2调用半缺省构造,创建st调用全缺省构造。
创建对象时,不传参,就不可以加(),因为会和函数声明产生歧义。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
这个很好理解,就是我们写了构造函数,编译器不会生成;我们不写,编译器就会生成默认构造函数。
- 默认构造函数对内置类型(如int,char…)不处理(不同编译器可能不同),对自定义类型会去调用它的默认构造函数。
当我们把自己写的构造函数注释掉,在加一个print函数后,运行下面代码的结果
结果发现,编译器生成构造函数并没有做什么。这就源于第五点的特征了。我们在Date内增加一个stack对象(没有任何作用,只是为了验证对于自定义类型,会去调用它的构造函数),在运行看看结果。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class stack
{
public:
//全缺省构造
stack(int capacity = 4)
{
cout << "stack(int capacity = 4)" << endl;
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == NULL)
{
return;
}
_arr = tmp;
_top = 0;
_capacity = capacity;
}
private:
int* _arr;
int _top;
int _capacity;
};
class Date
{
public:
void print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
stack _s;
};
int main()
{
Date d1;
//Date d2(1949);
d1.print();
//stack st;
return 0;
}
运行结果如图
可以发现,对于成员_s,调用了它的构造函数。这里虽然年月日虽然被初始化为0,但仍然是没有意义的。那么对于自定义类型,该怎么处理呢?一种是我们自己显式定义构造函数,另一种方式就是使用缺省值。在成员变量声明处给成员变量一个缺省值即可。
如下这样
class A
{
public:
void print()
{
cout << a << " " << b;
}
private:
int a = 2;
int b = 3;
};
int main()
{
A a;
a.print();
return 0;
}
这段代码我们没有写构造函数,但在变量声明处给了缺省值。
运行结果为2 3。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。这两个函数可以同时存在,但是在调用时会产生歧义。
比如
//无参构造
Date()
{
cout << "Date()" << endl;
_year = _month = _day = 1;
}
//半缺省构造
Date(int year = 1949, int month = 10, int day = 1)
{
cout << "Date(int year, int month = 10, int day = 1)" << endl;
_year = year;
_month = month;
_day = day;
}
它们可以同时存在,因为它们构成函数重载。但是,当写了这样的代码时
Date d1;
会报错,原因是函数调用产生了歧义。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
结论:不传参数就可以调用的构造函数就是默认构造函数。
2. 析构函数
析构函数的作用并不是对对象本身进行一个消耗,而是完成资源的清理工作,包括释放申请的空间,关闭文件等。功能类似于Destroy();
析构函数也有以下的特征
1.函数名为:~类名,没有返回值(也不需要写void,是规定),也没有参数
2.析构函数在类里只能有一个,即不支持函数重载
3.若我们没有显式定义,编译器会自动生成默认的析构函数
4.对象生命周期结束时,C++编译系统会自动调用析构函数
5.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成内存泄漏,比如stack类
6.对内置类型不做处理(运行结束后,系统自动回收内存,不需要处理),对自定义类型会调用它的析构函数
stack的析构函数如下
//析构函数
~stack()
{
if (_arr != nullptr)
{
free(_arr);
_arr == nullptr;
_top = 0;
_capacity = 0;
}
}
3. 拷贝构造函数
拷贝构造函数是用一个已经存在的类类型的对象创建一个新的对象,它只有一个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
用一个已经存在的类对象创建一个对象的方式有两种
Date d1; // 创建一个对象
Date d2(d1); // 用d1创建d2
Date d3 = d2; //用d2创建d3
可以使用圆括号或者等号。
它有以下的特征
- 拷贝构造函数是构造函数的一个重载函数,没有返回值;参数只有一个,必须是类类型对象的引用。
比如
日期类
Date(const Date& d);
栈
stack(const stack& s);
如果使用传值的形式定义拷贝构造函数,会产生无穷递归。由于形参是实参的一份临时拷贝,如果以值的形式进行传参,那么将会产生一个临时的类类型的对象,会调用默认的拷贝构造函数。在进行传参,又会调用拷贝构造函数,从而产生无穷递归。
- 没有显式定义时,编译器默认生成的拷贝构造函数完成的是浅拷贝(以内存存储的形式按字节序依次完成拷贝)
编译器生成的默认的拷贝构造函数是不能完成所有情况的拷贝。一般如果类中没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请,则拷贝构造函数是一定要写的,比如栈。
对于栈,如果使用默认的拷贝构造会怎么样?
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class stack
{
public:
//全缺省构造
stack(int capacity = 4)
{
//cout << "stack(int capacity = 4)" << endl;
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == NULL)
{
return;
}
_arr = tmp;
_top = 0;
_capacity = capacity;
}
//析构函数
~stack()
{
if (_arr != nullptr)
{
free(_arr);
_arr = nullptr;
_top = 0;
_capacity = 0;
}
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
stack st;
stack st1(st);
return 0;
}
运行结果如下
原因是默认生成拷贝构造是浅拷贝。
通过调试也可以看到,st._arr和st1._arr是指向同一块空间的。
因此,对于有申请资源的类型,需要显式实现拷贝构造函数。
栈的拷贝构造如下
stack(const stack& s)
{
//给新对象申请资源
int* tmp = (int*)malloc(sizeof(int) * s._capacity);
if (tmp == nullptr)
{
return;
}
_arr = tmp;
_top = s._top;
_capacity = s._capacity;
//拷贝旧数据
memcpy(_arr, s._arr, sizeof(int) * s._capacity);
}
用我们写的拷贝构造在运行代码,调试观察
可以看到,st._arr和st1._arr指向不同的空间。
- 拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
4. 赋值重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名为:关键字operator后面接需要重载的运算符符号。如+ - 等
函数原型:返回值类型 operator操作符(参数列表)参数个数和操作符需要的操作数的个数相同。
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型的参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this - (.*)(::)(sizeof)(?:)(.) 注意以上5个运算符不能重载。第一个会和函数指针有联系,一般很少见,记住即可,第二个是域作用限定符,第三个为sizeof,第四个为三目运算符,最后一个为成员访问限定符。
赋值重载的特征:
- 赋值运算符需要重载成成员函数,如果重载成全局函数,会和编译器自动生成的赋值运算符重载函数冲突,产生调用歧义。
- 参数类型,const T&,T指类类型。
- 返回值类型T&,参数和返回值均使用引用是为了提高效率,减少拷贝。
- 返回值为*this,目的是为了支持连续赋值
- 用户没有显式实现时,编译器会生成一个默认的赋值运算符重载,以值的方式按字节序拷贝。内置类型的成员变量是直接赋值的,而自定义类型的成员变量需要调用对应类的赋值运算符重载完成赋值。
- operator=必须是非静态的成员函数。
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
对于日期类成员都是内置类型,我们不需要去显式实现。简单做验证即可
注:这里没有显式实现赋值重载。
将d3值给d2,可以看到d2的两次打印(第一行和第二行)结果不同。
5. 取地址重载和const取地址重载
这两个函数也是运算符重载,只是我们如果不显式实现,编译器会自动生成。它们返回的是this,即对象的地址。这两个函数一般不用我们自己实现,使用编译器生成的就可以了。
实现如下。
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
后一个函数为const Date* operator&() const。这里需要提到一个新的概念,叫const成员函数。
被const修饰的成员函数,叫做const成员函数。const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。隐含的this指针是不能显式出现在形参列表内的,因此将const加在最后(这也是规定)以便于和普通成员函数区分。那么const成员函数和普通成员函数有什么区别呢?
比如这段代码
#include<iostream>
using namespace std;
class A
{
public:
A()
{
a = 1;
b = 2;
}
void print()const
{
cout << "void print()const" << endl;
}
void print()
{
cout << "void print()" << endl;
}
private:
int a;
int b;
};
int main()
{
const A a;
a.print();
A b;
b.print();
return 0;
}
运行结果如图
const对象调用的是const成员函数,非const对象调用的是非const成员函数。
这里有几个问题
- const对象可以调用非const成员函数吗?答:不可以
- 非const对象可以调用const成员函数吗?答:可以
- const成员函数内可以调用其它的非const成员函数吗?答:不可以
- 非const成员函数内可以调用其它的const成员函数吗?答:可以
这里只需要判断对对象的读写权限的修改即可。权限可以缩小,但是不能放大。非const对象是可读可写的,const对象是只读不可写的。
6. 总结
- 以上的六个成员函数,我们不显式实现,编译器会自动生成,如果我们显式实现了,编译器就不会生成了。
- 拷贝构造函数和赋值重载需要注意浅拷贝的问题。
- 对于const成员函数,如果在函数内部没有对*this进行修改,那么建议加const。
介绍了这六个函数,我们会对日期类和栈进行完善,加深对这六个默认成员函数的理解。
本篇就到这里咯。