b站Cherno的课[86]-[90]
一、C++持续集成
Jenkins 商业软件
二、C++静态分析
静态分析器会检查你的代码,并尝试检测各种错误,这些错误
可能是你无意中编写的,有点像代码复查,但不是由人来做
PVS-Studio 商业软件
三、C++的参数计算顺序
(参数求值顺序)
argument evaluation order
未定义的行为
也就是说它会根据编译器的不同而变化,完全依赖于C++编译器将代码转换成机器码的实际实现
cpp
#include <iostream>
void PrintSum(int a, int b)
{
std::cout << a << "+" << b << "=" << (a + b) << std::endl;
}
int main()
{
int value = 0;
PrintSum(value++, value++);
std::cin.get();
}

release模式下的常数折叠
C++标准添加了一个从C++17开始的新规则
后缀表达式必须在别的表达式之前被计算
wandbox.org 在线编译网站
C++的参数计算顺序:没有定义
因为c++实际上并没有提供c++规范,并没有提供一个定义,来说明在这种情况下应该发生什么,参数(形参)或实参应该按照什么顺序求值
但如果你提到c++17说了这两件事不能同时做(同时计算)
那就加分了
也就是说,
它们必须一个接一个地完成
但是再说一次,,这个顺序并没有在规范中定义
这意味着你在技术上无法知道计算顺序是什么
四、C++移动语义
移动语义本质上允许我们移动对象
cpp
#include <iostream>
class String
{
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
~String()
{
delete m_Data;
}
void Print()
{
for (uint32_t i = 0; i < m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
:m_Name(name)
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity(String("wm"));
entity.PrintName();
std::cin.get();
}

cpp
#include <iostream>
class String
{
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other) noexcept
{
printf("Moved!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
~String()
{
printf("Destroyed!\n");
delete m_Data;
}
void Print()
{
for (uint32_t i = 0; i < m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
:m_Name(name)
{
}
Entity(String&& name)
:m_Name(name)
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity(String("wm"));
entity.PrintName();
std::cin.get();
}

cpp
#include <iostream>
class String
{
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other) noexcept
{
printf("Moved!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
~String()
{
printf("Destroyed!\n");
delete m_Data;
}
void Print()
{
for (uint32_t i = 0; i < m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
:m_Name(name)
{
}
Entity(String&& name)
// 只修改了此处,输出Moved
//:m_Name((String&&)name)
:m_Name((std::move)name)
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity(String("wm"));
entity.PrintName();
std::cin.get();
}

Entity(String&& name)
:m_Name(name)
{
}
这里的问题是,此构造函数的参数name应该是一个右值(String&&),所以m_Name(name)应该是调用的String的移动构造函数,为啥要m_Name(std::move(name)) 才行
因为右值引用在进入函数体内的之后,参数类型会变为左值
也就是在函数体内你可以对 String&& val中的 val取&
所以要触发移动语义必须让他变为右值引用,std::move就是干这个转换的
你看std::move的代码 就是干了一个找到源参数类型的static_cast转换
把参数换成了&&类型
所以就能触发后面的移动构造函数
每个表达式都有两种特征:一是类型二是值类别。
很多人迷惑的右值引用为啥是个左值,那是因为右值引用是它的类型,左值是它的值类别。
想理解右值首先要先知道类型和值类别的区别;其次是各个值类别的定义是满足了某种形式它就是那个类别,经常说的能取地址就是左值,否则就是右值,这是定义之上的不严谨经验总结,换句话说,是左值还是右值是强行规定好的,你只需要对照标准看这个表达式满足什么形式就知道它是什么值类别了。
为什么要有这个分类,是为了语义,当一个表达式出现的形式表示它是一个右值,就是告诉编译器,我以后不会再用到这个资源,放心大胆的转移销毁,这就可以做优化,比如节省拷贝之类的。
move的作用是无条件的把表达式转成右值,也就是rvalue_cast,虽然编译器可以推断出左右值,但人有时比编译器"聪明",人知道这个表达式的值以后我不会用到,所以可以在正常情况下会推成左值的地方强行告诉编译器,我这是个右值,请你按右值的语义来做事。
五、stdmove与移动赋值操作符
移动语义能够将一个对象移动到另一个对象上
move assignment operator(移动赋值运算符)
cpp
#include <iostream>
class String
{
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other) noexcept
{
printf("Moved!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
String& operator = (String&& other) noexcept
{
printf("Moved!\n");
if (this != &other)
{
delete[] m_Data;
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
return *this;
}
~String()
{
printf("Destroyed!\n");
delete m_Data;
}
void Print()
{
for (uint32_t i = 0; i < m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
:m_Name(name)
{
}
Entity(String&& name)
:m_Name(std::move(name))
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity(String("wm"));
entity.PrintName();
String apple = "Apple";
String dest;
//String string = "Hello";
//String dest((String&&)string);
//String dest = (String&&)string;
//String dest(std::move(string));
//String dest = std::move(string);
//dest.assign(std::move(string));
//dest = std::move(dest);
apple.Print();
dest.Print();
dest = std::move(apple);
apple.Print();
dest.Print();
std::cin.get();
}

cpp
int main()
{
String apple = "Apple";
String dest;
std::cout << "Apple: ";
apple.Print();
std::cout << "Dest: ";
dest.Print();
dest = std::move(apple);
std::cout << "Apple: ";
apple.Print();
std::cout << "Dest: ";
dest.Print();
std::cin.get();
}

总而言之
移动赋值操作符是你想要包含在类中的东西,当你包含一个移动构造函数时,因为它当然是想要将一个对象移动到一个现有的变量中
它基本上是五法则的一部分,还有三法则,五法则包含了所有的新移动语义
注:
C++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符C++五法则:为了支持移动语义,又增加了移动构造函数和移动赋值运算符
赋值操作符与使用构造函数的区别:
通过使用这个运算符,它就像我们写.operator=,并像调用函数一样调用它
std:move是你想要将一个对象转换为临时对象时要做的
换句话说,如果你需要把一个已经存在的变量变成临时变量
你可以标记它,表示你可以从这个特定的变量中窃取资源
这使我们能够在现有的变量上执行移动操作
如果你在创建一个新变量,如果是在函数参数中或在移动构造函数中(创建),那么它已经是一个临时变量,这是可以的
但如果你有一个已经存在的变量,比如这个apple的例子
这是一个已经存在的变量,你需要确保使用std:move来将它转换成一个临时变量
这样你就可以使用移动构造函数或移动赋值操作符,从那个变量中获取资源,并进行移动