C++ 学习
哪块不会学哪块!!!
76.c++如何处理optional数据(std::optional)
1.C++17在STL中引入了std::optional,就像std::variant一样,std::optional是一个“和类型(sum type)”,也就是说,std::optional类型的变量要么是一个T类型的变量,要么是一个表示“什么都没有”的状态。
2.基本用法:
首先要包含
#include <optional>
3.has_value()
可以通过has_value()来判断对应的optional是否处于已经设置的状态,代码如下所示:
int main()
{
std::string text = /*...*/;
std::optional<unsigned> opt = firstEvenNumberIn(text);
if (opt.has_value()) //直接if(opt)即可,代码更简洁
{
std::cout << "The first even number is "
<< opt.value()
<< ".\n";
}
}
4.访问optional对象中的数据
1.opt.value()
2.(*opt)
3.value_or() //value_or()可以允许传入一个默认值, 如果optional为std::nullopt,
//则直接返回传入的默认值.(如果数据确实存在于std::optional中,
//它将返回给我们那个字符串。如果不存在,它会返回我们传入的任何值)
std::optional是C++17的新东西,用于检测数据是否存在or是否是我们期盼的形式,用于处理那些可能存在,也可能不存在的数据or一种我们不确定的类型 。
比如在读取文件内容的时候,往往需要判断是否读取成功,常用方法是传入一个引用变量或者判断返回的std::string是否为空,C++17引入了更好的方法,std::optional
老方法:传入一个引用变量或者判断返回的std::string是否为空
#include <iostream>
#include <fstream>
#include <string>
std::string ReadFile(const std::string &fileapath, bool &outSuccess) {
std::ifstream stream(filepath);
//如果成功读取文件
if (stream) {
std::string result;
getline(stream,result);
stream.close();
outSuccess = true; //读取成功,修改bool
return result;
}
outSuccess = false; //反之
}
int main() {
bool flag;
auto data = ReadFile("data.txt", flag);
//如果文件有效,则接着操作
if (flag) {
}
}
使用std::optional
// 用g++编译时需要加上‘-std=c++17’ or ‘-std=gnu++17’
// std::optional同样是C++17的新特性,可以用来处理可能存在、也可能不存在的数据
//data.txt在项目目录中存在,且其中的内容为"data!"
#include <iostream>
#include <fstream>
#include <optional>
#include <string>
std::optional<std::string> ReadFileAsString(const std::string& filepath)
{
std::ifstream stream(filepath);
if (stream)
{
std::string result;
getline(stream, result);
stream.close();
return result;
}
return {};
//如果文本存在的话,它会返回所有文本的字符串。如果不存在或者不能读取;则返回optional {}
}
int main()
{
std::optional<std::string> data = ReadFileAsString("data.txt");
//auto data = ReadFileAsString("data.txt"); //可用auto关键字
if (data)
{
// std::string& str = *data;
// std::cout << "File read successfully!" << str<< std::endl;
std::cout << "File read successfully!" << data.value() << std::endl;
}
else
{
std::cout << "File could not be opened!" << std::endl;
}
std::cin.get();
}
//输出
File read successfully!"data!"
如果文件无法打开,或者文件的特定部分没有被设置或读取,也许我们有一个默认值,这很常见。此时就可以使用value_or()函数。其作用就是:如果数据确实存在于std::optional中,它将返回给我们那个字符串。如果不存在,它会返回我们传入的任何值。
删除data.txt,此时文件不存在打不开,则被设置为默认值
#include <iostream>
#include <fstream>
#include <optional>
#include <string>
std::optional<std::string> ReadFileAsString(const std::string& filepath)
{
std::ifstream stream(filepath);
if (stream)
{
std::string result;
//getline(stream, result);
stream.close();
return result;
}
return {}; //返回空
}
int main()
{
std::optional<std::string> data = ReadFileAsString("data.txt");
std::string value = data.value_or("Not present");
std::cout << value << std::endl;
if (data)
{
std::cout << "File read successfully!" << std::endl;
}
else
{
std::cout << "File could not be opened!" << std::endl;
}
}
//输出
Not present
File could not be opened!
85.c++ 左值和右值(lvalue and rvalue)
-
左值:
有地址,有值,有储存空间的值,可以出现在赋值运算的左边或是右边。
-
右值:
临时量,没有地址,没有存储空间、短暂存在的值
-
左值引用:
仅仅接受左值,但是用了const后可以兼容(非const的左值引用只接受左值),加const后,实际上会创建一个变量,一般看到函数都是const T&,常量引用。
-
右值引用:
不能绑定左值,可以通过常引用或者右值引用延长右值生命周期。
-
右值引用的优势:优化
如果我们知道传入的是一个临时对象的话,那么我们就不需要担心它们是否活着,是否完整,是否拷贝。我们可以简单地偷它的资源,给到特定的对象,或者其他地方使用它们。因为我们知道它是暂时的,它不会存在很长时间 而如果如上使用const string& str,虽然可以兼容右值,但是却不能从这个字符串中窃取任何东西!因为这个str可能会在很多函数中使用,不可乱修改!(所以才加了const)
e.g.
#include<iostream>
void PrintName(std::string name) // 可接受左值和右值
{
std::cout<<name<<std::endl;
}
void PrintName(std::string& name) // 只接受左值引用,不接受右值
{
std::cout << name << std::endl;
}
void PrintName(const std::string& name) // 接受左值和右值,把右值当作const lvalue&
{
std::cout << name << std::endl;
}
void PrintName(std::string&& name) // 接受右值引用
{
std::cout << name << std::endl;
}
int main()
{
std::string firstName = "Leee";
std::string lastName = "yyee";
std::string fullName = firstName + lastName; //右边的表达式是个右值。
PrintName(fullName);
PrintName(firstName+lastName);
std::cin.get();
}
#include <iostream>
int &GetValue()
{ // 左值引用
static int value = 10;
return value;
}
void SetValue(int value) {}
void PrintName(std::string &name)
{ // 非const的左值引用只接受左值
std::cout << "[lvalue]" << name << std::endl;
}
void PrintName(const std::string &&name)
{ // 右值引用不能绑定到左值
std::cout << "[rvalue]" << name << std::endl;
}
int main()
{
int i = GetValue();
GetValue() = 5;
SetValue(i); // 左值参数调用
SetValue(10); // 右值参数调用,当函数被调时,这个右值会被用来创建一个左值
// 关于const,const引用可以同时接受左值和右值
// int& a = 10; // 不能用左值作为右值的引用
const int &a = 10; // 通过创建一个左值实现
std::string firstName = "Yan";
std::string lastName = "Chernikov";
std::string fullname = firstName + lastName;
PrintName(fullname); // 接受左值
PrintName(firstName + lastName); // 接受右值
return 0;
}
89.c++移动语义
当理解左值和右值,左值引用和右值引用后,就可以了解最大的一个用处:移动语义
为什么要使用移动语义?
很多时候我们只是单纯创建一些右值,然后赋给某个对象用作构造函数。这时候会出现的情况是:首先需要在main函数里创建这个右值对象,然后复制给这个对象相应的成员变量。 如果我们可以直接把这个右值变量移动到这个成员变量而不需要做一个额外的复制行为,程序性能就能提高。
e.g. 没有使用移动语义
#include <iostream>
#include <cstring>
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(int argc, const char* argv[]) {
Entity entity(String("Cherno")); // 首先创建String类型对象然后Entity初始化列表触发拷贝构造,把之前的对象中的资源进行了复制,然后析构
entity.PrintName();
return 0;
}
//输出结果:
Created!
Copied!
Cherno
可以看到中间发生了一次copy,实际上这次copy发生在Entity的初始化列表里。 从String的拷贝构造函数可以看到,复制过程中还申请了新的内存空间!这会带来很大的消耗。
使用移动构造函数
#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 // 右值引用拷贝,相当于移动,就是把复制一次指针,原来的指针给nullptr //noexcept 指定函数是否抛出异常 (不抛出异常)
{
//让新对象的指针指向指定内存,然后将旧对象的指针移开
//所以这里做的其实是接管了原来旧的内存,而不是将这片内存复制粘贴!
printf("Moved\n");
m_Size = other.m_Size;
m_Data = other.m_Data; // 窃取地址
//这里便完成了数据的转移,将other里的数据偷走了
other.m_Size = 0;
other.m_Data = nullptr; // 为什么要进行nullptr,防止析构时重复进行delete,这会出问题
}
~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:
uint32_t m_Size;
char* m_Data;
};
class Entity
{
public:
Entity(const String& name) : m_Name(name)
{
}
void PrintName() {
m_Name.Print();
}
Entity(String&& name) : m_Name(std::move(name)) // std::move(name)也可以换成(String&&)name
{
}
private:
String m_Name;
};
int main()
{
Entity entity(String("Cherno")); //注意到String("Cherno")其实是零食的右值,所以这里构造函数是Entity(String&& name)
entity.PrintName();
std::cin.get();
}
//输出:
Created!
Moved!
Destroyed!
Cherno
Destroyed
表达式都有两种特征:一是类型二是值类别。很多人迷惑的右值引用为啥是个左值,那是因为右值引用是它的类型,左值是它的值类别。 想理解右值首先要先知道类型和值类别的区别;其次是各个值类别的定义是满足了某种形式它就是那个类别,经常说的能取地址就是左值,否则就是右值,这是定义之上的不严谨经验总结,换句话说,是左值还是右值是强行规定好的,你只需要对照标准看这个表达式满足什么形式就知道它是什么值类别了。 为什么要有这个分类,是为了语义,当一个表达式出现的形式表示它是一个右值,就是告诉编译器,我以后不会再用到这个资源,放心大胆的转移销毁,这就可以做优化,比如节省拷贝之类的。 move的作用是无条件的把表达式转成右值,也就是rvalue_cast,虽然编译器可以推断出左右值,但人有时比编译器“聪明”,人知道这个表达式的值以后我不会用到,所以可以在正常情况下会推成左值的地方强行告诉编译器,我这是个右值,请你按右值的语义来做事。
90.std::move与移动赋值操作符
-
std::move,返回一个右值引用,将本来的copy操作变为窃取资源的move操作
-
想要将一个已经存在的对象移动给另一个已经存在的对象,就像下面这样。
移动赋值相当于把别的对象的资源都偷走,那如果移动到自己头上了就没必要自己偷自己 。 更重要的是原来自己的资源一定要释放掉,否则指向自己原来内容内存的指针就没了,这一片内存就泄露了!
#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");
}
String& operator=(const String& other) // 拷贝赋值
{
printf("Copied\n");
if(this != &other)
{
delete[] m_Data;
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
return *this;
}
}
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_Data = nullptr;
other.m_Size = 0;
}
return *this;
}
private:
uint32_t m_Size;
char* m_Data;
};
class Entity
{
public:
Entity(const String& name) : m_Name(name)
{
}
void PrintName() {
m_Name.Print();
}
Entity(String&& name) : m_Name(std::move(name)) // std::move(name)也可以换成(String&&)name
{
}
private:
String m_Name;
};
int main()
{
String apple = "apple";
String orange = "orange";
printf("apple: ");
apple.Print();
printf("orange: ");
orange.Print();
apple = std::move(orange);
printf("apple: ");
apple.Print();
printf("orange: ");
orange.Print();
std::cin.get();
}
//输出:
Created
Created
apple: apple
orange: orange
Moved
apple: orange
orange:
这里需要注意区分和移动构造的区别。
- 移动构造:初始化新对象 → 无旧资源,无需释放;
- 移动赋值:赋值已有对象 → 有旧资源,必须先释放。
这里做个总结
| 运算符类型 | 参数类型 | 核心行为 | 对应构造函数 | 适用场景 |
|---|---|---|---|---|
| 拷贝赋值(左值) | String& operator=(const String& other) |
深拷贝:开辟新内存,复制对方的资源 | 拷贝构造String(const String&) |
给左值对象赋值(a=b,b是有名字的左值) |
| 移动赋值(右值) | string& operator=(String&& other) |
浅拷贝(资源窃取):直接接管对方的内存,无需新开辟 | 移动构造String(String&&) |
给对象赋右值(比如a=std::move(b)、a=String("hello")) |
- 拷贝构造(String(const String&)) --- 需要用到另一个对象,需要有名字的左值;然后对其资源进行复制,会开辟内存,是深拷贝;无需清理自身资源(对象刚创建,无旧资源)。
- 移动构造(String(String&&)) --- 接收右值引用(临时右值/
std::move转换的左值);是进行资源的窃取操作,无需进行资源复制,只是接管对方内存;当然接管后需要设为空指针nullptr,因为对方被掏空了,这样更规范,避免析构重复释放;必须加上noexcept。 - 拷贝赋值(String& operator=(const String&)) --- 接收const左值引用;区别于拷贝构造就是需要先清理自己的资源,防止内存泄漏;然后开辟内存复制资源,深拷贝;当然需要保证不能赋值自己,因为你先清空了自己的资源,防止自赋值导致野指针访问(m_Data = other.m_Data,other.m_Data已释放了)。
- 移动赋值(String& operator=(String&&)) --- 接受的是右值引用(临时右值/
std::move转换的左值);区别于移动构造也是要先清理自己的资源;然后夺取对方的资源,接管对方内存,无需开辟新内存;值得注意的是也要有置空指针nullptr的操作;需要加上noexcept还有不能自赋值。
构造 vs 赋值的核心区别:构造是 “创建新对象”(无旧资源,无需清理),赋值是 “修改已存在的对象”(有旧资源,必须先清理);
拷贝 vs 移动的核心区别:拷贝是 “深拷贝(新内存 + 复制数据)”,移动是 “资源窃取(接管旧内存 + 置空源对象)”;
移动语义的关键约束:移动构造 / 赋值必须加 noexcept,自赋值判断是所有赋值运算符的必备防护。

浙公网安备 33010602011771号