C++11中引入了右值引用和移动语义,可以避免无谓的复制,提高了程序性能
左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象
int&& x = 42;
)std::move
返回的右值引用(例如:int x = 42; int&& y = std::move(x);
)std::forward
返回的右值引用(例如:template void func(T&& t) { f(std::forward(t)); }
)C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值
如何区分将亡值和纯右值:
std::move
函数将一个对象转换为将亡值。在某些情况下,编译器也会自动将一个对象识别为将亡值,例如返回一个std::vector
对象时。而纯右值则通常是字面量、临时对象或者返回值表达式。区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”,最常见的情况有:
右值引用允许我们将一个右值(即临时对象或表达式的结果)绑定到一个可以修改的引用上,从而避免了不必要的内存拷贝和对象构造。下面是一个右值引用的例子:
#include
#include
using namespace std;
void print(vector<int>&& v)
{
for (auto i : v)
cout << i << " ";
cout << endl;
}
int main()
{
vector<int> v1{1, 2, 3};
print(move(v1)); // 将v1作为右值引用传递给print函数
return 0;
}
右值引用就是对一个右值进行引用的类型,因为右值没有名字,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
总结:
auto&&
或函数参数类型自动推导的T&&
是一个未定的引用类型,被称为universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型T&&
为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用左值引用和右值引用的主要区别在于它们绑定到不同类型的值上。左值引用绑定到左值上,而右值引用绑定到右值上。
左值是指可以取地址的表达式,通常是已命名的变量或对象。例如:
int x = 10;
int& lref = x; // 左值引用绑定到 x 上
右值是指不能取地址的表达式,通常是临时创建的对象或字面量。例如:
int&& rref = 5; // 右值引用绑定到字面量 5 上
左值引用和右值引用的另一个区别是它们对于移动语义和完美转发的支持。右值引用通常用于实现移动语义和完美转发,而左值引用则用于传递和修改已命名的对象。如下是一个简单的示例:
#include
#include
using namespace std;
void printVector(vector<int>& v) // 左值引用参数
{
for (auto i : v)
cout << i << " ";
cout << endl;
}
void printVector(vector<int>&& v) // 右值引用参数
{
for (auto i : v)
cout << i << " ";
cout << endl;
}
int main()
{
vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2 = {6, 7, 8, 9, 10};
// 左值引用参数
printVector(v1);
// 右值引用参数
printVector(move(v2));
return 0;
}
函数printVector()
接受一个右值引用参数,第一次调用该函数时,它将接受一个左值 v1,因为 v1 是已命名的对象;第二次调用该函数时,它将接受一个右值 move(v2)
,因为move()
返回一个右值引用,将 v2 转换为右值。
第一个例子是在函数中传递一个大型对象,可以使用右值引用来避免深拷贝和提高性能。例如:
#include
#include
// 接受一个 std::vector 的右值引用
void process(std::vector<int>&& vec)
{
// 对 vec 进行一些操作
std::cout << "The vector has " << vec.size() << " elements.\n";
// ...
}
int main()
{
std::vector<int> v = {1, 2, 3, 4, 5};
// 调用 process 时将 v 转换为右值引用
process(std::move(v));
// 此时 v 已经被移动,无法再使用
// std::cout << v[0] << std::endl; // 会导致编译错误
return 0;
}
process
函数接受一个std::vector
的右值引用,这意味着传递给函数的参数是一个临时对象或者可以被移动的对象,而不是一个具有所有权的对象。在函数内部,我们可以对这个对象进行一些操作,而无需进行深拷贝。此外,我们使用了 std::move
函数将 v 转换为右值引用,这意味着 v 的所有权被移动到了process
函数内部,因此在函数结束后,我们无法再使用 v。
使用右值引用可以避免不必要的深拷贝,从而提高性能。但是,需要注意的是,右值引用只适用于可以被移动的对象,如果一个对象不支持移动语义,则不能使用右值引用。
不支持移动语义的对象:
第二个关于类的例子
假设有一个类MyString,它表示一个字符串,并且有一个成员变量char* str
指向字符串的内存空间。为了避免深拷贝,我们可以使用右值引用来优化它的移动构造函数。
移动构造函数是一种特殊的构造函数,它接受一个右值引用参数,并将资源从一个对象转移到另一个对象,而不是进行深拷贝。在这个例子中,我们可以使用std::move
来将指针从一个对象移动到另一个对象:
#include
class MyString
{
public:
MyString() : str(nullptr) {}
MyString(const char* s)
{
str = new char[std::strlen(s) + 1];
std::strcpy(str, s);
}
MyString(const MyString& other)
{
str = new char[std::strlen(other.str) + 1];
std::strcpy(str, other.str);
}
MyString(MyString&& other) noexcept
{
str = other.str;
other.str = nullptr;
}
~MyString()
{
delete[] str;
}
private:
char* str;
};
在上面的代码中,我们定义了一个移动构造函数MyString(MyString&& other) noexcept
,它接受一个右值引用参数other,并将其 str 成员变量的指针移动到当前对象的 str 成员变量中。由于我们已经将 other.str 设置为 nullptr,因此在 other 对象的析构函数中将不会释放内存。
现在,我们可以使用右值引用来避免深拷贝,例如:
MyString s1("hello");
MyString s2(std::move(s1)); // 移动构造函数被调用,避免了深拷贝
在上面的代码中,我们首先创建一个 MyString 对象 s1,然后使用std::move
将其转换为一个右值引用,并将其传递给 MyString 对象 s2 的构造函数。由于我们已经定义了移动构造函数,因此在这个过程中会调用移动构造函数,而不是进行深拷贝。
引用是一种别名,是一个已经存在的对象的另一个名字。而右值引用则是对临时对象(rvalue)的引用,通常用于移动语义和完美转发。
使用意义上,引用主要用于函数参数传递和返回值,可以避免复制大对象,提高性能。而右值引用则主要用于实现移动语义,可以将对象的资源所有权转移给另一个对象,避免不必要的复制和销毁,提高效率。
总的来说,引用和右值引用都可以用于传递参数和返回值,但是使用场景和意义不同。引用主要用于避免复制大对象,而右值引用则主要用于实现移动语义,提高效率。
C++11为了让普通的左值也能借组移动语义来优化性能,提供了std::move()
方法来将左值转换为右值,从而方便应用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
#include
#include
// 定义一个简单的类,包含一个动态分配的数组
class MyClass
{
public:
MyClass() : data(nullptr), size(0) {}
MyClass(int n) : data(new int[n]), size(n)
{
std::cout << "Constructing MyClass of size " << size << std::endl;
}
~MyClass()
{
delete[] data;
std::cout << "Destructing MyClass of size " << size << std::endl;
}
// 移动构造函数,实现移动语义
MyClass(MyClass &&other) : data(other.data), size(other.size)
{
other.data = nullptr;
other.size = 0;
std::cout << "Moving MyClass of size " << size << std::endl;
}
// 移动赋值运算符,实现移动语义
MyClass &operator=(MyClass &&other)
{
if (this != &other)
{
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
std::cout << "Moving MyClass of size " << size << std::endl;
}
return *this;
}
private:
int *data;
int size;
};
int main()
{
std::vector<MyClass> vec;
vec.reserve(10);
// 向vector中添加10个MyClass对象
for (int i = 0; i < 10; i++)
vec.emplace_back(i + 1);
std::cout << "Resizing vector\n";
// 将vector的大小减半
vec.resize(5);
std::cout << "Done\n";
return 0;
}
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。
forward是一个模板函数,主要用于将参数进行完美转发,即将参数以原本的类型和引用类型传递给另一个函数,避免因为多次拷贝而引起的性能问题。
forward完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
具体来说,forward可以用于以下场景:
#include
using namespace std;
void bar(int& x)
{
cout << "bar&" << endl;
}
void bar(int&& x)
{
cout << "bar&&" << endl;
}
template<typename T>
void foo(T&& t)
{
bar(forward<T>(t));
}
int main()
{
int a = 10;
foo(a);
foo(10);
return 0;
}
上述代码中,foo 函数接受一个参数T&& t
,并将其转发给另一个函数bar。使用std::forward
可以保持参数类型和引用类型的一致性,从而避免不必要的拷贝和类型转换。
这里的 foo 函数的参数 t 是一个“转发引用”,也称为“万能引用”。在使用这个函数模板时,t 的类型将根据函数参数的类型来确定,可能是一个左值引用或者是一个右值引用。
因此,在这个函数模板中,t 既可以是左值,也可以是右值。它的左值或右值属性取决于传递给函数的实参类型。如果传递给函数的实参是左值,则 t 也将是一个左值引用;如果传递给函数的实参是右值,则 t 将是一个右值引用。
需要注意的是,即使 t 是一个右值引用,它仍然可以绑定到左值或右值,因为它是一个“转发引用”,能够接受任何类型的参数。
#include
using namespace std;
class MyClass
{
public:
MyClass() : m_data(nullptr) {}
~MyClass()
{
delete[] m_data;
}
MyClass(MyClass &&other) noexcept
{
cout << "Moving MyClass" << endl;
m_data = other.m_data;
other.m_data = nullptr;
}
MyClass &operator=(MyClass &&other) noexcept
{
cout << "Moving Operator=" << endl;
if (this != &other)
{
delete m_data;
m_data = other.m_data;
other.m_data = nullptr;
}
return *this;
}
private:
int *m_data;
};
int main()
{
MyClass t1;
MyClass t2;
MyClass t3(forward<MyClass>(t1));
MyClass t4 = forward<MyClass>(t2);
return 0;
}
上述代码中,移动构造函数和移动赋值运算符需要将参数以右值引用的方式传递,并将其移动到新的对象中。使用std::forward
可以避免不必要的拷贝和析构操作,提高性能。
template<typename... Args>
void foo(Args&&... args)
{
bar(std::forward<Args>(args)...);
}
上述代码中,foo函数接受可变参数模板,可以接受任意类型和数量的参数,并将其转发给另一个函数bar。使用std::forward可以将参数以原本的类型和引用类型传递给bar函数,从而实现通用代码。
例如:
#include
#include
template<typename... Args>
void bar(Args&&... args)
{
std::cout << "this is bar function" << std::endl;
}
template<typename... Args>
void foo(Args&&... args)
{
bar(std::forward<Args>(args)...);
}
int main()
{
int x = 42;
std::string s = "hello";
foo(x, s);
return 0;
}
当我们调用foo函数时,可以将任意类型和数量的参数转发给bar函数
总结:
T&& t
类型的变量,则会将左值变为右值;是的话,则原来是什么值就转发成什么值emplace_back是C++中vector容器的一个成员函数,用于在vector的末尾插入一个新元素。与push_back不同的是,emplace_back可以直接在vector中构造一个新元素,而不需要先创建一个对象再插入。这个新元素是通过参数列表中的参数来构造的,而不是通过复制或移动现有对象来构造的。因此,emplace_back比push_back更高效,因为它可以避免不必要的对象构造和复制。也就是说,emplace_back是就地构造,不用构造后再次复制到容器中,因此效率更高。
例如像下面的语句:
vector<string> vec;
vec.push_back(string(16, 'a'));
string(16, ‘a’)
会创建一个string类型的临时对象,这涉及到一次string构造过程push_back
结束时,最开始的临时对象会被析构c++11可以用emplace_back代替push_back,因为emplace_back在将元素添加到容器中时,不需要进行复制或移动操作。相反,它直接在容器的末尾构造元素对象,避免了复制或移动操作的开销,可以省略一次构建和一次析构,从而达到优化的目的。这意味着emplace_back比push_back更快,特别是当元素类型较大或不可复制时,例如包含大量数据成员或具有唯一所有权的对象。
如下的例子,使用push_back和emplace_back分别向向量中添加100万个整数,并测量添加的时间:
#include
#include
#include
int main()
{
std::vector<int> vec;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; i++)
vec.push_back(i);
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "push_back duration: " << duration.count() << " microseconds" << std::endl;
vec.clear();
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; i++)
vec.emplace_back(i);
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "emplace_back duration: " << duration.count() << " microseconds" << std::endl;
return 0;
}
stdout:
push_back duration: 5001 microseconds
emplace_back duration: 4000 microseconds
C++11增加了无序容器 unordered_map/unordered_multimap 和unordered_set/unordered_multiset,由于这些容器中的元素是不排序的,因此,比有序容器map/multimap 和 set/multiset 效率更高。 map 和 set 内部是红黑树,在插入元素时会自动排序,而无序容器内部是散列表(Hash Table),通过哈希(Hash),而不是排序来快速操作元素,使得效率更高。由于无序容器内部是散列表,因此无序容器的 key 需要提供 hash_value 函数,其他用法和map/set 的用法是一样的。不过对于自定义的 key,需要提供 Hash 函数和比较函数。
map和unordered_map都是关联容器,存储的元素都是键值对(key-value)。
map是基于红黑树实现的,它能够自动根据键值对的键排序,因此可以快速地查找、插入和删除元素,时间复杂度为O(log n)。但是,红黑树的构建和维护需要耗费一定的时间和空间,因此在元素数量较少时,使用map可能会比较浪费资源。
unordered_map是基于哈希表实现的,它不会对键进行排序,而是根据哈希函数将键映射到哈希表的某个位置上,因此能够快速地查找、插入和删除元素,时间复杂度为O(1)。但是,哈希表的构建和维护需要耗费一定的时间和空间,而且哈希函数的质量和哈希表的装载因子会影响其性能。
map的优点是能够自动排序,支持快速查找、插入和删除元素,并且不受哈希函数的影响。缺点是构建和维护红黑树需要耗费较多的时间和空间,而且在元素数量较少时可能会比较浪费资源。
unordered_map的优点是能够快速查找、插入和删除元素,时间复杂度为O(1),并且不受元素顺序的影响。缺点是构建和维护哈希表需要耗费较多的时间和空间,而且哈希函数的质量和哈希表的装载因子会影响其性能。
map适用于需要对键进行排序的场景,例如需要按照字典序或者数字大小对元素进行排序。同时,当元素数量较少时,使用map可以避免哈希函数的影响,提高性能。
unordered_map适用于需要快速查找、插入和删除元素的场景,例如需要进行散列表操作。同时,当元素数量较多时,使用unordered_map可以避免红黑树的影响,提高性能。但是,需要注意哈希函数的质量和哈希表的装载因子,以避免影响性能。
假设有一个字符串数组,需要统计每个单词出现的次数。可以使用map和unordered_map来实现。
map:
#include
#include
using namespace std;
int main()
{
string words[] = {"hello", "world", "map", "world", "example", "map", "example", "hello"};
map<string, int> wordCount;
for (string word : words)
wordCount[word]++;
for (auto it : wordCount)
cout << it.first << " : " << it.second << endl;
return 0;
}
stdout:
example : 2
hello : 2
map : 2
world : 2
unordered_map:
#include
#include
using namespace std;
int main()
{
string words[] = {"hello", "world", "map", "world", "example", "map", "example", "hello"};
unordered_map<string, int> wordCount;
for (string word : words)
wordCount[word]++;
for (auto it : wordCount)
cout << it.first << " : " << it.second << endl;
return 0;
}
stdout:
map : 2
world : 2
hello : 2
example : 2
可以看到,使用map和unordered_map实现的结果是一样的,但是unordered_map的输出结果没有按照字典序排列。这是因为unordered_map使用哈希表实现,不保证元素的顺序。而map使用红黑树实现,可以保证元素按照字典序排列。因此,在需要按照顺序遍历元素的情况下,应该使用map;在不需要保证顺序的情况下,可以使用unordered_map,因为它的查找速度更快。
set是一种基于红黑树实现的关联式容器,它的元素按照一定的顺序排列,每个元素都唯一且不可修改。红黑树是一种自平衡的二叉查找树,在插入和删除元素时能够保持树的平衡性,使得查找、插入、删除等操作的时间复杂度都为O(log n)。
unordered_set是一种基于哈希表实现的关联式容器,它的元素不按照任何顺序排列,每个元素都唯一且不可修改。哈希表是一种通过哈希函数将元素映射到桶中的数据结构,具有查找速度快的优点,但是在插入和删除元素时需要处理哈希冲突,这会增加一定的开销。
set的优点在于能够保持元素的有序性,可以方便地进行范围查找和二分查找等操作。同时,红黑树的自平衡性能够保证操作的时间复杂度比较稳定,不会受到数据分布的影响。缺点在于插入和删除元素时需要维护红黑树的平衡性,这会增加一定的开销。
unordered_set的优点在于查找元素的速度比set快,因为它是通过哈希函数进行查找的。同时,在插入和删除元素时不需要维护任何平衡性,因此操作的开销比较小。缺点在于元素的顺序是不确定的,不支持范围查找和二分查找等操作。同时,哈希函数的设计和哈希冲突的处理会影响到查找的性能。
set适用于需要保持元素有序的情况,比如需要进行范围查找和二分查找等操作的场景。同时,由于红黑树的平衡性能够保证操作的时间复杂度比较稳定,因此在数据量比较大,对时间复杂度有要求的场景下也比较适用。
unordered_set适用于对查找速度有要求的场景,比如需要进行大量的查找操作但不需要保持元素有序的情况。同时,由于哈希表不需要维护任何平衡性,因此在插入和删除元素频繁的场景下也比较适用。但是,由于哈希函数的设计和哈希冲突的处理会影响到查找的性能,因此在数据量比较小或者哈希函数的设计不合理的情况下可能会出现性能问题。
假设我们有一个字符串列表,我们想要找到其中的重复项。
set:
#include
#include
#include
using namespace std;
int main()
{
vector<string> strs = {"hello", "world", "hello", "world", "set", "unordered_set"};
set<string> s;
for (auto &str : strs)
{
if (s.count(str))
cout << "Found duplicate: " << str << endl;
else
s.insert(str);
}
return 0;
}
stdout:
Found duplicate: hello
Found duplicate: world
unordered_set:
#include
#include
#include
using namespace std;
int main()
{
vector<string> strs = {"hello", "world", "hello", "world", "set", "unordered_set"};
unordered_set<string> s;
for (auto &str : strs)
{
if (s.count(str))
cout << "Found duplicate: " << str << endl;
else
s.insert(str);
}
return 0;
}
stdout:
Found duplicate: world
Found duplicate: hello
可以看到,set 和 unordered_set 都能找到重复项,但是它们输出的顺序不同。set 输出的顺序是按照字典序排列的,而 unordered_set 没有任何顺序保证。