0%

C++学习笔记:右值引用与移动构造函数/类特殊成员函数/using与typedef/union的使用/typeid与type_info类

  1. 右值引用(&&)与移动构造函数
  2. 类特殊成员函数与delete/default
  3. using 与 typedef 的区别
  4. union的使用
  5. typeid与type_info类

右值引用(&&)与移动构造函数

看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A {
public:
A() : _num(new int(33)) { std::cout << "A()" << std::endl; }

~A() noexcept
{
std::cout << "~A()" << std::endl;
delete _num;
}

private:
int* _num;
};

int main()
{
using namespace std;

{
A a; // A()
} // ~A()

return 0;
}

运行后先后在命令行中输出“A()”和“~A()”。这是最一般的调用,没有问题。但当调用类A的构造函数语句改成下面形式时,看输出结果:

1
2
3
4
5
6
7
{
vector<A> vec;
vec.push_back(A());
}
// A()
// ~A()
// ~A()

如果按照上面的代码其实是无法运行的,看结果知道调用了两次析构函数,然后在析构函数里面delete了两次,这很明显会报错,因此这个输出结果是把析构函数中的delete语句删掉后得到的。这也说明一个问题,当使用一个容器来存储一个未命名的类实例时,调用了一次构造函数,但总体调用了两次析构函数,这很明显不是我们想看到的。

那么首先,导致这个问题的原因是,vec.push_back(A());首先创建了一个未命名实例,接着将该实例送入容器中,送入的过程调用了拷贝构造函数,这个临时实例在拷贝构造函数执行之后就结束了生命周期,因此被销毁了,此时调用了一次析构函数;当容器被销毁时,容器中保存的实例再次调用了一次析构函数。最终调用了两次析构函数。

要解决这种临时实例的拷贝问题,C++11中有一个特别的方法,就是依赖移动构造函数,移动构造函数的参数为类类型的右值引用,表明该参数是一个右值,即将被销毁,因此可以在这个地方对右值的状态进行一次清理,以免造成不必要的提前销毁。使用移动构造函数优化上述例子的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
public:
A() : _num(new int(33)) {}

A(A&& other) noexcept
{
_num = other._num;
other._num = nullptr; // 清理指针状态
}

~A() noexcept
{
// 判断:指针有效才释放内存
if (_num)
{
delete _num;
}
}

private:
int* _num;
};

有了移动构造函数,在对临时实例进行拷贝时,优先调用移动构造函数,若不存在才调用拷贝构造函数。临时实例使用完之后会自动销毁,为了避免销毁不必要的资源需要对状态进行清理,而拷贝构造函数的参数是const常量,无法修改数据,因此只能在移动构造函数中进行修改,这就是其存在的必要性,也是右值引用一个很典型的应用场景。

类特殊成员函数与delete/default

创建一个类,如果不做任何生命,默认带有以下四种函数,称作类特殊成员函数

  • 默认构造函数
  • 拷贝构造函数
  • 析构函数
  • 拷贝赋值运算符

保证了一个类的实例具有默认行为(创建、拷贝赋值、销毁),举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {};

int main()
{
MyClass c1; // 调用默认构造函数

MyClass c2(c1); // 调用拷贝构造函数
MyClass c3 = c1;

MyClass c4;
c4 = c1; // 调用拷贝赋值运算符

return 0;
} // 退出作用域时调用析构函数

四个函数的函数参数如下所示:

1
2
3
4
5
6
7
class MyClass {
public:
MyClass() {}
MyClass(const MyClass&) {}
MyClass& operator=(const MyClass&) {}
~MyClass() {}
};

但有时候用不到这四个函数中的某一个(比如拷贝赋值),那么此时可以使用C++11中的一个新特性:将该函数赋值为delete关键字:

1
2
3
4
class MyClass {
public:
MyClass& operator=(const MyClass&) = delete;
};

此时,再调用相关功能,IDE就会报错:

1
2
3
4
5
6
int main()
{
MyClass c1, c2;
c1 = c2; // 错误 E1776 无法引用 函数 "MyClass::operator=(const MyClass &)" (已声明 所在行数:98) -- 它是已删除的函数
return 0;
}

也可以将其赋值为default,和不写没啥区别:

1
2
3
4
class MyClass {
public:
MyClass& operator=(const MyClass&) = default;
};

using 与 typedef 的区别

在类型定义中的使用没有区别:

1
2
3
4
5
using uchar = unsigned char;
typedef unsigned char uchar;

using int_vector = std::vector<int>;
typedef std::vector<int> int_vector;

但需要使用模板别名时,只能使用using

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class ClassA {
public:
ClassA() { std::cout << typeid(T).name() << std::endl; }
};

template<typename T>
using B = ClassA<T>;

template<typename T>
typedef ClassA<int> C; // 错误 E0935 此处不能指定“typedef”

union的使用

union是C语言的关键字,用法和结构体struct一样,区别在于:结构体中存放了多少变量,该结构体所占内存为所有变量字节数的总和;union中也可以存放多个变量,但所有变量共用一个内存地址(起点位置相同),且该union的大小为长度最大的那个变量所占的字节数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct block1 {
int a;
float b;
double c;
};

union block2 {
int a;
float b;
double c;
};

int main()
{
block1 b1;
block2 b2;

int s1 = sizeof(b1); // 16
int s2 = sizeof(b2); // 8

void* ptr;
ptr = &b1.a; // 0x008ffd54
ptr = &b1.b; // 0x008ffd58
ptr = &b1.c; // 0x008ffd5c
ptr = &b2.a; // 0x008ffd44
ptr = &b2.b; // 0x008ffd44
ptr = &b2.c; // 0x008ffd44

return 0;
}

typeid与type_info类

typeid是C++的关键字,用法同sizeof

1
typeid(int)

包含一个参数,返回值是type_info类。可以通过如下方式获取数据类型的输出字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {};

int main()
{
std::cout << typeid(int).name() << std::endl; // int
std::cout << typeid(double).name() << std::endl; // double
std::cout << typeid(MyClass).name() << std::endl; // class MyClass
std::cout << typeid(typeid(MyClass)).name() << std::endl; // class type_info
std::cout << typeid(int*).name() << std::endl; // int *
std::cout << typeid(MyClass*).name() << std::endl; // class MyClass *

return 0;
}