零、 基础知识
NULL 和 nullptr
NULL 定义,可知C++中NULL是宏定义;
#ifdef __cplusplus
# if !defined(__MINGW32__) && !defined(_MSC_VER)
# define NULL __null
# else
# define NULL 0
# endif
#else
# define NULL ((void*)0)
nullptr是c++11引入的一个关键字,为了解决NULL的二义性,建议在使用空指针的时候使用。
lamda 表达式
// values pointed to by anything pointer-like
auto derefLess = [](const auto& p1, const auto& p2){
return *p1 < *p2;
}
一、std::pair & std::make_pair
注意:使用pair与make_pair要包含头文件 #include < utility >
- pair实质上是一个结构体(类),make_pair是函数。
- pair主要的两个成员变量是first和second,这两个变量可以直接使用。
- 初始化一个pair可以使用构造函数,也可以使用std::make_pair函数
// 构造函数初始化一个pair对象
pair <string,double> product1 ("tomatoes",3.25);
// 其他初始化
pair <string,double> product2;
product2.first = "lightbulbs"; // type of first is string
product2.second = 0.99; // type of second is double
// make_pair初始化一个pair对象
pair <string,double> product3;
product3 = make_pair ("shoes",20.0);
二、动态/静态数组的创建和回收
静态数组:定义时就已经在栈上分配了空间,运行时大小不改变。
动态数组:运行时在堆上分配一定的存储空间,另外在运行时还可以改变其大小。虽然在栈上分配空间效率较高,但是栈空间有限,对于大型数据应使用new和delete在堆区分配空间。另外在很多情况下预编译过程阶段数组的长度是不能预先知道的,必须在程序运行的过程中动态给出。
一维数组
静态 int array[100]; 定义了数组array,并未对数组进行初始化
静态 int array[100] = {1,2}; 定义并初始化了数组array
动态 int* array = new int[100]; delete []array; 分配了长度为100的数组array
动态 int* array = new int[100]{1,2}; delete []array; 为长度为100的数组array初始化前两个元素
二维数组
静态 int array[10][10]; 定义了数组,并未初始化
静态 int array[10][10] = { {1,1} , {2,2} }; 数组初始化了array[0][0,1]及array[1][0,1]
动态 int (*array)[n] = new int[m][n]; delete []array; // in c++11: auto array = new int[m][n]
动态 int** array = new int*[m]; for(i) array[i] = new int[n]; for(i) delete []array[i]; delete []array; 多次析构
注意这种方式不可用: int **array = new int[m][n];
// 创建
int **array = new int* [m]; // 数组元素类型为int*
for (int i=0; i<m; i++) {
array[i] = new int [n];
}
// 回收
for (int i=0; i<m; i++) {
delete [] array[i];
}
delete [] array;
三、左值 & 右值
https://www.jianshu.com/p/94b0221f64a5
1. 定义
左值:
- 一个左值是指向一个指定内存的东西
- 左值活的很久,因为他们以变量的形式(variable)存在
- 一个变量有着具体(specific)的内存位置
右值: - 右值就是不指向任何地方的东西
- 右值是暂时和短命的
- 没有指定的内存地址,当然在程序运行时一些临时的寄存器除外。
// C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。
int x = 1;
// 在这里我通过取地址操作符&获取了x的内存地址并且把它放进了p。&操作符需要一个左值并且产生了一个右值,这也是另一个完全合法的操作:
int* p = &x;
// 1是一个字面常量也就是一个右值,它没有一个具体的内存位置(memory location),所以我们会把y分配到一个不存在的地方。
// 赋值的左操作数需要一个左值,这里我们使用了一个右值1。
int x;
1 = x;
// &操作符需要一个左值作为操作数,因为只有一个左值才拥有地址,右值1没有地址。
int* p = &1;
2. 返回左值和右值的函数
setValue返回一个右值,不能作为一个赋值的左操作数,也就是不能将3赋给setValue的返回值。
int setValue()
{
return 6;
}
// ... somewhere in main() ...
setValue() = 3; // error!
setGlobal返回global的引用,global是左值,可以作为赋值的做操作数,被赋值。
int global = 100;
int& setGlobal()
{
return global;
}
// ... somewhere in main() ...
setGlobal() = 400; // OK
3. 左值到右值的转换
x和y经历了一个隐式(implicit)的左值到右值(lvalue-to-rvalue)的转换。许多其他的操作符也有同样的转换——减法、加法、除法等等。
int x = 1;
int y = 3;
int z = x + y; // ok
4. 左值引用
这里将yref声明为类型int&:一个对y的引用,它被称作左值引用(lvalue reference)。现在你可以开心地通过该引用改变y的值了。
int y = 10;
int& yref = y;
yref++; // y is now 11
在右边我们有一个临时值,一个需要被存储在一个左值中的右值。在左边我们有一个引用(一个左值),他应该指向一个已经存在的对象。但是10 是一个数字常量(numeric constant),也就是一个左值,将它赋给引用与引用所表述的精神冲突。右值无法转换成左值。
int& yref = 10; // will it work?
我将一个临时值10传入了一个需要引用作为参数的函数中,产生了将右值转换为左值的错误。这里有一个解决方法(workaround),创造一个临时的变量来存储右值,然后将变量传入函数中(就像注释中写的那样)。将一个数字传入一个函数确实不太方便。
void fnc(int& x)
{
}
int main()
{
fnc(10); // Nope!
// This works instead:
// int x = 10;
// fnc(x);
}
4. 常量左值引用
C++中经常通过常量引用来将值传入函数中,这避免了不必要的临时对象的创建和拷贝。
const int& ref = 10; // OK!
// 或者
void fnc(const int& x)
{
}
int main()
{
fnc(10); // OK!
}
编译器会为你创建一个隐藏的变量(即一个左值)来存储初始的字面常量,然后将隐藏的变量绑定到你的引用上去。那跟我之前的一组代码片段中手动完成的是一码事,例如:
// the following...
const int& ref = 10;
// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;
四、右值引用和移动
https://www.jianshu.com/p/31cea1b6ee24
1. 右值引用
传统的C++规则规定:只有存储在const变量(immutable)中的右值才能获取它的地址。从技术上来说,你可以将一个const lvalue绑定(bind)到一个rvalue上。如上面所示的代码。
C++ 0x引入了一个新的类型——右值引用(rvalue reference),通过在类型名后放置&&来表示右值引用。这些右值引用让你可以改变一个临时对象的值,看上去好像他去掉了上面第二行中的const了一样。
std::string s1 = "Hello ";
std::string s2 = "world";
std::string&& s_rref = s1 + s2; // the result of s1 + s2 is an rvalue
s_rref += ", my friend"; // I can change the temporary string!
std::cout << s_rref << '\n'; // prints "Hello world, my friend"
现在s_rref是一个对于临时对象的一个引用,或者称之为右值引用。这个引用没有const修饰,所以我可以根据需求随意修改他而不需要付出任何代价。如果没有右值引用和&&符号,想要完成这一步是不可能的。为了更好地区分右值引用和一般引用,我们将传统的C++引用称作左值引用(lvalue reference)。
乍一看右值引用毫无用处,然而它为移动语义(move semantics)的实现做了铺垫,移动语义可以先出提升你的应用的表现。
2. 移动语义move sementic
class Holder
{
public:
Holder(int size) // Constructor
{
m_data = new int[size];
m_size = size;
}
~Holder() // Destructor
{
delete[] m_data;
}
private:
int* m_data;
size_t m_size;
};
这是一个处理动态内存块的类,除了动态内存分配(allocation)部分之外没什么特别的。当你选择自己管理内存时你需要遵守所谓的rule of three。规则如下:如果你的类定义了下面所说的方法中的一个或者多个,它最好显式定义所有的三个方法:
- 析构函数(destructor)
- 拷贝构造函数(copy constructor)
- 拷贝复制运算符(copy assignment operator)
(如果你不定义这些函数)C++的编译器会以默认的方式生成这些函数以及构造函数和其他我们现在没有考虑的函数。不幸的是,默认的函数对于处理动态资源是完全不够的。实际上,编译器无法生成向上面那样的构造函数,因为它不知道我们的类的逻辑。
1)实现拷贝构造函数
拷贝构造函数会创造新的类对象
Holder h1(10000); // regular constructor
Holder h2 = h1; // copy constructor
Holder h3(h1); // copy constructor (alternate syntax)
拷贝构造函数如下
Holder(const Holder& other)
{
m_data = new int[other.m_size]; // (1)
std::copy(other.m_data, other.m_data + other.m_size, m_data); // (2)
m_size = other.m_size;
}
2)实现赋值拷贝预算符
将一个已经存在的对象替换为另一个已经存在的对象
Holder h1(10000); // regular constructor
Holder h2(60000); // regular constructor
h1 = h2; // assignment operator
赋值拷贝预算符的实现如下
Holder& operator=(const Holder& other)
{
if(this == &other) return *this; // (1)
delete[] m_data; // (2)
m_data = new int[other.m_size];
std::copy(other.m_data, other.m_data + other.m_size, m_data);
m_size = other.m_size;
return *this; // (3)
}
两个函数的参数都是一个常量左值引用。
3. 现有类设计的限制
我们的类类很好,但是它缺少一些优化。考虑下面的函数:
Holder createHolder(int size)
{
return Holder(size);
}
它用传值的方式返回了一个Holder对象。我们知道,当函数返回一个值时,编译器会创建一个临时且完整的对象(右值)。现在,我们的Holder是一个重量级(heavy-weight)的对象,因为它有着内部的内存分配,这是一个相当费事的任务——以现有的类设计返回这些东西的值会导致多次内存分配,这并不是一个好主意。如何得出这个结论?让我们看下面的代码:
int main()
{
Holder h = createHolder(1000); // 调用Holder类的拷贝构造函数
}
由createHolder()创建的临时对象被传入拷贝构造函数中,根据我们现有的设计,拷贝构造函数通过拷贝临时对象的数据分配了它自己的m_data指针。这里有两次内存分配:
- 创建临时对象:createHolder(1000)的返回值,会创建一个临时对象;
- 拷贝构造函数调用:Holder类拷贝构造函数调用
同样的拷贝过程发生在赋值操作符中:
int main()
{
Holder h = createHolder(1000); // Copy constructor
h = createHolder(500); // Assignment operator
}
我们的赋值运算符清除了对象的内存,然后通过从临时对象中拷贝数据,为赋值的对象从头开始分配新的内存。在这里也有两次内存分配:
- 临时对象创建
- 调用赋值运算符
拷贝的次数太多了!我们已经有了一个完整的(fully-fledged)临时对象,它由createHolder()函数创建。它是一个右值,如果在下一个指令前不被使用将会消失。所以为什么在构造或者复制时我们不使用move而是选择重复的拷贝呢?
在上古C++中,我们没办法做这样的优化,返回一个重量级对象的值是无用的。幸运的是,在C++11后,我们可以(并且鼓励)使用move来优化我们的类。简而言之,我们将从现有的对象处偷取他们的数据而不是做一些毫无意义的克隆。不要拷贝,总是使用move,因为移动的代价更加的低。
4. 用右值引用实现移动语义
让我们用move(将一个左值变成右值)来为我们的类增光添彩!我们的想法就是增加新的版本的拷贝构造函数和赋值运算符,这样我们就可以将临时对象的数据直接偷过来。“偷”的意思是改变对象中数据的拥有者,我们怎么修改一个临时变量呢?当然是使用右值引用!
在这里我们通常遵守另一个C++规则——Rule of Five。它是Rule of Three的扩展,额外声明了一个规则:任何需要move的类都要声明两个额外的成员函数:
- 移动构造函数(move constructor):通过从临时对象偷取数据来构建一个新的对象
- 移动赋值运算符(move assignment operator):通过从临时对象偷取数据来替换已有对象的数据
1)实现移动构造函数
一个典型的移动构造函数:
Holder(Holder&& other) // <-- rvalue reference in input
{
m_data = other.m_data; // (1)
m_size = other.m_size;
other.m_data = nullptr; // (2)
other.m_size = 0;
}
它使用一个右值引用来构造Holder对象,关键部分:作为一个右值引用,我们可以修改它,所以让我们先偷他的数据(1),然后将它设置为nullptr(2)。这里没有深层次的拷贝,我们仅仅移动了这些资源。将右值引用的数据设置为nullptr是很重要的,因为一旦临时对象走出作用域,它就会调用析构函数中的delete[] m_data,记住了吗?通常来说,为了让代码看上去更加的整洁,最好让被偷取的对象的数据处于一个良好定义的状态。
- 实现移动赋值运算符
移动赋值运算符有着同样的逻辑:
Holder& operator=(Holder&& other) // <-- rvalue reference in input
{
if (this == &other) return *this;
delete[] m_data; // (1)
m_data = other.m_data; // (2)
m_size = other.m_size;
other.m_data = nullptr; // (3)
other.m_size = 0;
return *this;
}
我们先清理已有对象的数据(1),再从其它对象处偷取数据(2)。别忘了把临时对象的数据设置为正确的状态!剩下的就是常规的赋值运算所做的操作。
既然我们有了新的方法,编译器就会检测你到底是在使用临时对象(右值)创建一个对象还是使用常规的对象(左值),并且它会根据检测的结果触发更加合适的构造函数(或者运算符)。例如:
int main()
{
Holder h1(1000); // regular constructor
Holder h2(h1); // copy constructor (lvalue in input)
Holder h3 = createHolder(2000); // move constructor (rvalue in input) (1),直接接收右值,用移动构造
h2 = h3; // assignment operator (lvalue in input)
h2 = createHolder(500); // move assignment operator (rvalue in input),直接接收右值,用移动构造
}
5. 可以移动左值吗
是的,通过标准库中的工具函数std::move,你可以移动左值。它被用来将左值转化为右值,假设我们想要从一个左值盗取数据:
int main()
{
Holder h1(1000); // h1 is an lvalue
Holder h2(h1); // copy-constructor invoked (because of lvalue in input)
}
由于h2接收了一个左值,拷贝构造函数被调用。我们需要强制调用移动构造函数从而避免无意义的拷贝,所以我们这样做:
int main()
{
Holder h1(1000); // h1 is an lvalue
Holder h2(std::move(h1)); // move-constructor invoked (because of rvalue in input)
}
在这里,std::move将左值h1转化为一个右值:编译器看见输入变成了右值,所以调用了移动构造函数。h2将会在构造时从h1处偷取数据。