默认成员函数
c++ 类中有六个默认成员函数,它们是:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
其中,最重要的是前四个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。在 c++11 中新增了两个:移动构造函数和移动赋值运算符重载。
默认移动构造和移动赋值的生成条件
- 默认移动构造函数生成条件:自己没有实现移动构造函数、析构函数、拷贝构造函数、拷贝赋值函数中的任意一个,那么编译器会自动生成一个默认移动构造。
- 移动赋值重载函数生成条件:自己没有实现移动赋值重载函数、析构函数、拷贝构造函数、拷贝赋值函数中的任意一个,那么编译器会自动生成一个默认移动赋值重载。
注意:即使上面的条件都满足,编译器也可能不会生成移动构造函数或移动赋值函数,这通常是编译器发现这些函数无法正确工作或者有歧义。这种情况下,需要手动实现移动构造函数和移动赋值函数。
默认移动构造和移动赋值可以做些什么
默认生成的移动构造函数和移动赋值函数可以用于内置类型和自定义类型。对内置类型的成员会完成浅拷贝;对于自定义类型的成员,若该类实现了移动构造函数/移动赋值函数 ,就调用该函数,否则就调用它的拷贝构造。
下面对上述进行验证,这里我们需要模拟实现一个简易的 string 类,如下:
namespace hyr
{
class string
{
public:
// 构造函数
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);
}
// 拷贝构造函数
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 资源转移" << endl;
swap(s);
return *this;
}
// 赋值运算符重载函数
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
接下来再实现一个简易的 person 类,person 类中 name_ 成员变量的类型就是上面的 string 类。
class person
{
public:
person(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{}
person(const person& p)
:_name(p._name)
,_age(p._age)
{}
person& operator=(const person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
~person(){}
private:
hyr::string _name;
int _age;
};
在上面的代码中,person 类没有显示定义移动构造函数和移动赋值函数,但它提供了拷贝构造函数、拷贝赋值函数和析构函数。因此,person 类不会自动生成默认的移动构造函数和移动赋值函数,如下所示:
在上述代码中,使用 std::move() 将一个右值(s1)转移给 s2 对象时,由于 person 类没有默认的移动构造函数,编译器会选择调用拷贝构造函数来创建 s2 对象。因此,在 person 的拷贝构造函数中,会调用 string 类的拷贝构造函数来对 _name 成员进行深拷贝。
若想要让 person 类生成默认的移动构造函数,必须将其拷贝构造函数、拷贝赋值函数和析构函数注释掉,因为只有当类没有显示定义这些特殊成员函数时,编译器才会生成默认的移动构造函数。
将上面三个函数注释掉之后的运行结果:
person 默认生成的移动构造函数,对于内置类型的成员(_age)会进行值拷贝,而对于自定义类型的成员(_name),因为 string 类实现了移动构造函数,因此它会调用 string 类的移动构造函数进行资源的转移。
类成员变量初始化
c++11 引入了在类定义时给成员变量初始缺省值的特性,这使得我们可以在定义类的同时为成员变量指定默认值,从而省略了类的默认构造函数中的初始化操作。
示例:
class Person
{
public:
private:
std::string _name = "default_name"; // 给成员变量_name指定缺省值
int age = 0; // 给成员变量_age指定缺省值
};
在类定义时为成员变量 _name 和 _age 指定了缺省值,分别是 “default_name” 和 0 。这意味着,当我们创建一个 Person 对象时没有显式地为 _name 和 _age 指定值,那么它们将会被默认初始化为 “default_name” 和 0 。
强制生成默认函数的关键字 - default
在 c++11 中,可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是由于一些原有这个函数没有默认生成。就可以使用 default 关键字
来显示指定某个默认特殊的成员函数生成。
示例:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person(Person&& p) = default;
private:
hyr::string _name;
int _age;
};
测试:
使用 default 关键字
生成一个特殊成员函数,那么它需要满足以下条件:
- 函数必须是特殊成员函数,例如:构造函数、拷贝构造函数、移动构造函数、拷贝赋值函数和移动赋值函数。
- 函数必须在类内声明,不能在类外定义。
- 函数的声明和定义必须一致,不能有任何参数或函数体。
- 使用
default 关键字
生成的函数将自动获得默认的行为,例如:默认初始化或简单的拷贝操作。
禁止生成默认函数的关键字 - delete
若想要限制某些函数的生成,在 c++98 中,可以将该函数设置为 private ,并且只声明但不定义它们,这样其他人在尝试调用这些函数时编译器就会报错。
如下所示:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
Person(const Person& p); // 禁止使用拷贝函数
Person& operator=(const Person& p); // 禁止使用赋值函数
private:
hyr::string _name;
int _age;
};
在 c++11 中,有了更加简单的方法来禁用这些函数,只需要在函数声明时加上 =delete
即可,该语法指示编译器不生成对于函数的默认版本,称 =delete
修饰的函数为删除函数。
如下所示:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
hyr::string _name;
int _age;
};