9346 字
47 分钟
C++11基础知识
2023-08-29

NULL和nullptr#

1. nullptr的出现#

nullptr是()中新加入的,它的出现是为了解决()的问题 1 1 1 1 1 nullptr是C++ 11版本中新加入的,它的出现是为了解决NULL表示空指针在C++ 中具有二义性的问题

2. C程序中的NULL#

在C语言中,NULL通常被定义为() 所以说NULL实际上是一个(),如果在C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了(),把void指针转换成了相应类型的指针。 1 1 1 1 1 在C语言中,NULL通常被定义为:#define NULL ((void *)0)

所以说NULL实际上是一个空指针,如果在C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。

// 注意得是c语言中这么写,C++不可以这么写
int *pi = NULL;
char *pc = NULL;

3. C++程序中的NULL#

C++ 中void* 不能()转换成(),为了解决空指针的表示问题,C++ 引入()来表示空指针,C++中,NULL实际上是()

但是用NULL代替0表示空指针在()时会出现()问题 当函数重载时,将NULL作为参数会调用到int类型参数的函数,而我们本意是将NULL作为空指针调用void*类型的函数,这样就出现了二义性

1

1

1

1

1

C++ 中void* 不能隐式转换成其他类型的指针,所以实际上编译器提供的头文件做了相应的处理: 在C++ 中,NULL实际上是0.因为C++ 中不能把void*类型的指针隐式转换成其他类型的指针,所以为了解决空指针的表示问题,C++ 引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。 但是用NULL代替0表示空指针在函数重载时会出现二义性问题

#include <iostream>
using namespace std;
void func(void* i)
{
cout << "调用了参数为void*类型的函数" << endl;
}
void func(int i)
{
cout << "调用了参数为int类型的函数" << endl;
}
void main(int argc,char* argv[])
{
func(NULL);
func(nullptr);
}
// 结果为
// 调用了参数为int类型的函数
// 调用了参数为void*类型的函数

我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本,所以是有问题的,这就是用NULL代替空指针在C++程序中的==二义性==。

C++中的nullptr#

为解决NULL代指空指针存在的二义性问题,在C++11版本(2011年发布)中特意引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。

4. 总结#

NULL在C++ 中就是0,因为在C++ 中()类型不允许(),所以之前C++ 中用()来代表空指针,但是在()的情况下,会出现上述的问题。所以,C++ 11加入了(),可以保证在任何情况下都代表(),而不会出现上述的情况,因此建议空指针用(),0用()。

auto#

5. auto的概念#

c++ 11中auto并不代表一种(),它只是一个(),auto也并不是在所有场景下都能推导出变量的(),==使用auto必须要进行(),让编译器推导出它的(),在编译阶段将()替换为()== 1 1 1 1 1 c++ 11中auto并不代表一种实际的数据类型,它只是一个类型声明的占位符,auto也并不是在所有场景下都能推导出变量的实际类型,==使用auto必须要进行初始化,让编译器推导出它的实际类型,在编译阶段将auto占位符替换为实际类型==

auto temp = 2;

6. auto的特殊推导规则#

(1)==当不声明为()或()时==,auto的推导结果和初始化表达式==抛弃()和()== 限定符后的类型一致,图上面的e和f (2)==当声明为()或()时== ,auto的推导结果将==保持()属性==

1 1 1 1 1 (1)==当不声明为指针或是引用时==,auto的推导结果和初始化表达式==抛弃引用和const属性==限定符后的类型一致,图上面的e和f (2)==当声明为指针或是引用时==,auto的推导结果将==保持初始化表达式的const属性==

int x = 0;
auto *a = &x; //a->int*,auto被推导为int
auto b = &x; //b->int*,auto被推导为int*
auto &c = x; //c->int&,auto被推导为int
const auto d = x; //d->const int,auto被推导为int
auto e = c; //e->int,auto被推导为int,丢弃引用
auto f = d; //f->int,auto被推导为int,丢弃const
const auto& g = x; //g->const int&,auto被推导为int
auto& h = g; //h->const int&,auto被推导为const int,保留const
auto i = g; //i->int,auto被推导为int,丢弃引用、丢弃const

不懂的:C++种引用和指针互相赋值?

已经理解了,它们赋值的写法不一样:

// 引用赋值给指针和普通变量赋值给指针都要用auto* a = &x;的写法
int x = 1;
auto& a = x;// 引用可以直接用普通变量初始化
auto* b = &a;// 引用赋值给指针要加&
auto* c = &x;// 普通变量赋值给指针要加&
auto& e = c;// 而指针赋值给引用可以直接赋值

增强for循环#

7.增强for循环的使用规则#

在增强for循环中不需要传递容器需要(),循环会自动以()为范围(),并且循环屏蔽掉了(),直接抽取()进行运算。

如果想要修改(),则需要使用()的方式遍历

如果(),使用()比()的效率高一些

注意遍历()时获取的是对象不是()所以要用 ()或者()

1

1

1

1

1

在增强for循环中不需要传递容器需要遍历的范围,循环会自动以容器为范围展开,并且循环屏蔽掉了迭代器的遍历细节,直接抽取容器中的元素进行运算。

如果想要修改遍历的容器,则需要使用引用的方式遍历

如果只读数据,不修改元素的值,使用for(const auto & it : vec) 比非引用的效率高一些

注意遍历哈希表时获取的是对象不是迭代器所以要用 it.first 或者 it.second

noexcept#

8.noexcpte修饰符的用法#

noexcept表示其修饰的()不会()

从语法上讲,noexcept 修饰符有两种形式:

1.()后加上 noexcept 关键字

2.可以接受一个()作为参数,值为()的效果与原noexcept相同

1

1

1

1

1

noexcept表示其修饰的函数不会抛出异常 。

从语法上讲,noexcept 修饰符有两种形式:

1.简单地在函数声明后加上 noexcept 关键字

double divisionMethod(int a, int b) noexcept
{
if (b == 0)
{
cout << "division by zero!!!" << endl;
return -1;
}
return a / b;
}

2.可以接受一个常量表达式作为参数,值为true的效果与原noexcept相同

double divisionMethod(int a, int b) noexcept(常量表达式);
void fun(int a)noexcept(0)//值为 false,表示有可能抛出异常这里
{
if (a == 0) throw("抛出异常");
}
void fun1(int a)noexcept("0")//值为 true,表示函数不会抛出异常
{
if (a == 0) throw("抛出异常");
}

静态断言#

9.静态断言的使用#

()状态下使用,不需要(),静态断言会保证()参数正确,错误就会报错()参数

如果我们想知道当前是32 位还是64位平台,显然如果写完程序之后再运行测试是不合理的,所以此时我们应该使用静态断言。(),所谓静态就是在()时就能进行检查的断言,使用时不需要引用头文件。

静态断言的表达式是在() 进行检测,所以它的表达式中不能出现变量,必须是()

1

1

1

1

1

非运行状态下使用,不需要头文件,静态断言会保证第一个参数正确,错误就会报错第二个参数

如果我们想知道当前是32 位还是64位平台,显然如果写完程序之后再运行测试是不合理的,所以此时我们应该使用静态断言。static_assert ,所谓静态就是在编译时就能进行检查的断言,使用时不需要引用头文件。

静态断言的表达式是在==编译阶段==进行检测,所以它的表达式中不能出现变量,必须是==常量表达式==。

int main()
{
//如果我们的环境是32位平台那么在编译阶段就会提示我们自己定义的错误 no 32
static_assert(sizeof(int*) == 4, "no 32 "); // 正确的
static_assert(sizeof(int*) == 8, "no 64");//报错 no64
//错误,a是变量,表达式要求是常量表达式
int a = 4;
static_assert(a == 4, "no 32 ");
return 0;
}

动态断言#

10.动态断言的使用#

要引入头文件()或者()

用于帮助(),对()里的语句进行判断,如果错误就(),并提示()

1

1

1

1

1

要引入头文件 #include或者#include<assert.h>

用于帮助锁定错误,对assert()小括号里的语句进行判断,如果错误就断到这了,并提示问题出现再哪个文件中

// 创建一个指定大小的 char 类型数组
char* createArray(int size)
{
// 通过断言判断数组大小是否大于0
assert(size > 0); // 必须大于0, 否则程序中断
char* array = new char[size];
return array;
}
int main()
{
char* buf = createArray(0);
return 0;
}

lambda表达式#

11.lambda表达式的优点#

1.可以就地()定义()或(),不需要()

2.lambda是一个()

1

1

1

1

1

lambda是C++11非常重要也是最常用的特性之一,他有以下优点:

1.可以就地==匿名== 定义==目标函数== 或==函数对象== ,不需要==额外写一个函数==

2.lambda是一个==匿名的内联函数==

12.lambda表达式的语法#

lambda表达式定义了一个匿名函数,语法()

其中capture是(),params是(),ret是(),body是()。

捕获列表[]:捕获() 的()

参数列表(): 和普通函数的参数列表一样,如果没有参数列表可以()

1

1

1

1

1

lambda表达式定义了一个匿名函数,语法如下:

[ capture ]( params ) -> ret {body;};

其中capture是==捕获列表== ,params是==参数列表== ,ret是==返回值类型== ,body是==函数体== 。

捕获列表[]:捕获==一定范围内== 的==变量==

参数列表(): 和普通函数的参数列表一样,如果没有参数,参数列表可以==省略不写==

auto fun = [](){return 0;};
auto fun = []{return 0;};

13.捕获列表中捕获的变量内容#

[ ] ()

[&] 捕获外部作用域中的(),并且按照() 捕获

[=]捕获外部作用域的() ,按照() 捕获,拷贝过来的() 在函数体内是()的

[= ,&a] 按值捕获外部作用域中的(),并且按照() 捕获外部变量()

[bar] 按()捕获(),不捕获其他变量

[this] 捕获(),让lambda表达式拥有和() 同样的()

1

1

1

1

1

[ ] ==不捕获任何变量==

[&] 捕获外部作用域中的==所有变量== ,并且按照==引用== 捕获

[=]捕获外部作用域的==所有变量== ,按照==值== 捕获,拷贝过来的==副本== 在函数体内是==只读== 的

[= ,&a] 按值捕获外部作用域中的==所有变量== ,并且按照==引用== 捕获外部变量==a==

[bar] 按==值== 捕获==bar变量== ,不捕获其他变量

[this] 捕获==当前类中的this指针== ,让lambda表达式拥有和==当前类成员函数== 同样的==访问权限==

int main()
{
int a = 10, b = 20;
auto f1 = [] {return a; }; // 错误,没有捕获外部变量,因此无法访问变量 a
auto f2 = [&] {return a++; }; // 正确,使用引用的方式捕获外部变量,可读写
auto f3 = [=] {return a; }; // 正确,使用值拷贝的方式捕获外部变量,可读
auto f4 = [=] {return a++; }; // 错误,使用值拷贝的方式捕获外部变量,可读不能写
auto f5 = [a] {return a + b; }; // 错误,使用拷贝的方式捕获了外部变量 a,没有捕获外部变量 b,因此无法访问变量 b
auto f6 = [a, &b] {return a + (b++); }; // 正确,使用拷贝的方式捕获了外部变量 a,只读,使用引用的方式捕获外部变量 b,可读写
auto f7 = [=, &b] {return a + (b++); }; // 正确,使用值拷贝的方式捕获所有外部变量以及 b 的引用,b 可读写,其他只读
return 0;
}
class Test
{
public:
void output(int x, int y)
{
auto x1 = [] {return m_number; }; // 错误,没有捕获外部变量,不能使用类成员 m_number
auto x2 = [=] {return m_number + x + y; }; // 正确,以值拷贝的方式捕获所有外部变量
auto x3 = [&] {return m_number + x + y; }; // 正确,以引用的方式捕获所有外部变量
auto x4 = [this] {return m_number; }; // 正确,捕获 this 指针,可访问对象内部成员
auto x5 = [this] {return m_number + x + y; }; // 错误,捕获 this 指针,可访问类内部成员,没有捕获到变量 x,y,因此不能访问。
auto x6 = [this, x, y] {return m_number + x + y; }; // 正确,捕获 this 指针,x,y
auto x7 = [this] {return m_number++; }; // 正确,捕获 this 指针,并且可以修改对象内部变量的值
}
int m_number = 100;
};

14.lambda表达式的返回值#

一般情况下,不指定lambda表达式的返回值,编译器会根据() ,()推导返回值类型,但是需要注意的是lambda表达式不能通过()自动推导出返回值类型。

1

1

1

1

1

一般情况下,不指定lambda表达式的返回值,编译器会根据==return语句== ,==自动== 推导返回值类型,但是需要注意的是lambda表达式不能通过==列表初始化== 自动推导出返回值类型。

// 可以自动推导出返回值类型
auto f = [](int i)
{
return i;
}
// 不能推导出返回值类型
auto f1 = []()
{
return { 1, 2 }; // 基于列表初始化推导返回值,错误
}
// 正确显示声明了函数的返回值类型
auto f1 = []()-> vector<int>
{
return { 1, 2 }; // 基于列表初始化推导返回值,错误
};

15.lambda表达式与函数指针#

那么我们再比较一下函数指针方式以及lambda方式。函数指针的方式看似简洁,不过却有很大的缺陷。

第一点是函数定义在别的地方,比如很多行以前(后)或者别的文件中,这样()

第二点则是出于()考虑,使用函数指针很可能导致()不对其进行(),在()的时候,内联的 lambda和没有能够内联的函数指针可能存在着巨大的()差别。因此,相比于函数指针,lambda拥有无可替代的优势。

1

1

1

1

1

那么我们再比较一下函数指针方式以及lambda方式。函数指针的方式看似简洁,不过却有很大的缺陷。

第一点是函数定义在别的地方,比如很多行以前(后)或者别的文件中,这样==代码阅读起来并不方便==

第二点则是出于==性能== 考虑,使用函数指针很可能导致==编译器== 不对其进行==inline优化( inline对编译器而言并非强制)== ,在==循环次数较多== 的时候,内联的 lambda和没有能够内联的函数指针可能存在着巨大的==性能== 差别。因此,相比于函数指针,lambda拥有无可替代的优势。

lambda与STL搭配使用#

#include <iostream>
using namespace std;
#include<vector>
#include<algorithm>
int main()
{
vector<int> vec = {1,2,3,4,5,6};
sort(vec.begin(), vec.end(), [](int a, int b)
{
return a > b;
});
for (auto it : vec)
{
cout << it << " ";
}
}
#include <vector>
#include <algorithm>
using namespace std;
#include<iostream>
vector<int> nums;
vector<int> largeNums;
const int ubound = 10;
inline void LargeNumsFunc(int i) {
if (i > ubound)
largeNums.push_back(i);
}
void Above() {
//传统的for循环
for (auto itr = nums.begin(); itr != nums.end(); ++itr) {
if (*itr >= ubound)
largeNums.push_back(*itr);
}
//使用函数指针
for_each(nums.begin(), nums.end(), LargeNumsFunc);
//使用lambda和算法for_each
for_each(nums.begin(), nums.end(), [=](int i) {
if (i > ubound)
largeNums.push_back(i); });
}

final#

16.final的作用#

c++增加了final 关键字来限制() 或者() ,如果final修饰函数只能() ,并且要把final关键字放到()

1.修饰函数,只能修饰() ,这样就可以组织() 重写()

2.修饰类,该类不允许() ,也就是说这个类不能有()

1

1

1

1

1

c++增加了final 关键字来限制==某个类不能被继承== 或者==某个虚函数不能被重写== ,如果final修饰函数只能==修饰虚函数== ,并且要把final关键字放到==类或者函数的后面==

1.修饰函数,只能修饰==虚函数== ,这样就可以组织==子类== 重写==父类这个函数==

class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
void test() final
{
cout << "Child class...";
}
};
class GrandChild : public Child
{
public:
// 语法错误, 不允许重写
void test()
{
cout << "GrandChild class...";
}
};

2.修饰类,该类不允许==被继承== ,也就是说这个类不能有==子类==

class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child final : public Base
{
public:
void test()
{
cout << "Child class...";
}
};
// 语法错误
class GrandChild : public Child
{
public:
};

override#

17.override的作用#

override关键字明确的表明将会() ,和final的用法相同,放在() 。提高了() ,降低了()

1

1

1

1

1

override关键字明确的表明将会==重写父类的虚函数== ,和final的用法相同,放在==函数后面== 。提高了==程序的正确性== ,降低了==出错概率== 。

class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
//正确,重写了父类的虚函数
void test() override
{
cout << "Child class...";
}
};
class GrandChild : public Child
{
public:
//报错,父类中没有相同的虚函数可以被重写
void test(int a) override
{
cout << "Child class...";
}
};

=default#

18.default的作用#

可以在()修饰() 为() ,也可以在() 修饰()为()

默认函数除了在类定义的(),也可以在类的()

1

1

1

1

1

可以在==类内部== 修饰==满足条件的类函数== 为==显示默认函数== ,也可以在==类定义之外== 修饰==成员函数== 为==默认函数== 。

class Base
{
public:
//指定无参构造为默认函数
Base() = default;
//指定拷贝构造函数为默认函数
Base(const Base& obj) = default;
//指定移动构造函数为默认函数
Base(Base&& obj) = default;
//指定复制赋值操作符重载函数为默认函数
Base& operator= (const Base& obj) = default;
//指定移动赋值操作符重载函数为默认函数
Base& operator= (Base&& obj) = default;
//指定析构函数为默认函数
~Base() = default;
};

默认函数除了在类定义的==内部指定== ,也可以在类的==外部指定==

// 类定义
class Base
{
public:
Base();
Base(const Base& obj);
Base(Base&& obj);
Base& operator= (const Base& obj);
Base& operator= (Base&& obj);
~Base();
};
// 在类定义之外指定成员函数为默认函数
Base::Base() = default;
Base::Base(const Base& obj) = default;
Base::Base(Base&& obj) = default;
Base& Base::operator= (const Base& obj) = default;
Base& Base::operator= (Base&& obj) = default;
Base::~Base() = default;
class Base
{
public:
Base() = default;
Base(const Base& obj) = default;
Base(Base&& obj) = default;
Base& operator= (const Base& obj) = default;
Base& operator= (Base&& obj) = default;
~Base() = default;
// 以下写法全部都是错误的
Base(int a = 0) = default; //自定义带参构造,不允许使用 =default 修饰(即使有默认参数也不行)
Base(int a, int b) = default; //自定义带参构造,不允许使用 =default 修饰
void print() = default; //自定义函数,不允许使用 =default 修饰
//下面两行不是移动、复制赋值运算符重载,不允许使用 =default 修饰
bool operator== (const Base& obj) = default;
bool operator>=(const Base& obj) = default;
};

=delete#

19.=delete的作用#

delete关键字标识() ,显示删除可以避免用户使用一些不应该使用的()

1

1

1

1

1

delete关键字标识==显示删除== ,显示删除可以避免用户使用一些不应该使用的==类的成员函数==

下面是=delete使用的例子

如果想通过=delete禁止使用默认生成的函数

class Base
{
public:
Base() = default;
Base(const Base& obj) = delete; //禁用拷贝构造函数
Base& operator= (const Base& obj) = delete; //禁用 = 进行对象复制/
};
int main()
{
Base b;
Base tmp1(b); // 报错 拷贝构造函数已被显示删除,无法拷贝对象
Base tmp = b; // 报错
return 0;
}

如果想通过=delete禁止使用自定义函数

class Base
{
public:
Base(int num) : m_num(num) {}
Base(char c) = delete; //禁用带 char 类型参数的构造函数,防止隐式类型转换(char 转 int)
void print(char c) = delete; //禁止使用带 char 类型的自定义函数,防止隐式类型转换(char 转 int)
void print()
{
cout << "num: " << m_num << endl;
}
void print(int num)
{
cout << "num: " << num << endl;
}
private:
int m_num;
};
int main()
{
Base b(97);
Base b1('a'); // 'a' 对应的 acscii 值为97 报错 对应的构造函数被禁用,因此无法使用该构造函数构造对象
b.print();
b.print(97);
b.print('a'); // 报错 对应的打印函数被禁用,因此无法给函数传递 char 类型参数
return 0;
}

委托构造#

20.委托构造的作用?使用时需要注意哪些地方?#

委托构造函数允许()

但是在使用委托构造的时候要注意两点:

1.链式的调用委托构造不能()

2.在初始化列表调用了委托构造,就不能在()中()了

1

1

1

1

1

委托构造函数允许==使用同一个类中的一个构造函数调用其他的构造函数,从而简化相关变量的初始化==

但是在使用委托构造的时候要注意两点:

1.链式的调用委托构造不能==形成一个闭环==

2.在初始化列表调用了委托构造,就不能在==初始化列表== 中==初始化其他变量== 了

class Test
{
public:
int min;
int mid;
int max;
Test(int min)
{
this->min = min;
}
Test(int min, int max):Test(min)
{
this->max = max;
}
Test(int min, int mid, int max):Test(min,max)
{
this->mid = mid;
}
};
int main()
{
Test t(1,2,3);
cout << t.min << " " << t.mid << " " << t.max << endl;
return 0;
}

继承构造#

21.继承构造的作用#

c++11提供的继承构造函数可以让()直接使用()的(),而不需要(),尤其是在()的情况下,可以极大的()

在子类中初始化父类中的私有成员变量,需要在()的()中加入()

继承构造语法:()

注意:继承构造函数只能初始化()中的数据成员,对于()中的数据成员,仍然需要自行处理

在子类中不添加任何构造函数,只添加using 类名::构造函数名,这样就可以在子类中(),通过他们去构造子类对象

继承构造函数可能遇到的问题:

1.如果父类的构造函数是()的,那么子类()

2.如果子类是是以()的方式继承父类,在子类中也无法声明父类的继承构造函数

1

1

1

1

1

c++11提供的继承构造函数可以让==子类== 直接使用==父类== 的==构造函数== ,而不需要==自己再写构造函数== ,尤其是在==父类有很多构造函数== 的情况下,可以极大的==简化子类构造函数的编写==

下面是代码示例:

没有继承构造的处理方式

#include <iostream>
using namespace std;
class father
{
int min;
int mid;
int max;
public:
father(int i) :min(i) {}
father(int i, int j) :min(i), mid(j) {}
father(int i, int j, int k) :min(i), mid(j), max(k) {}
};
class Child : public father
{
public:
Child(int i):father(i){ }
Child(int i, int j, int k) :father(i, j, k) {}
};
int main()
{
Child c(1,2,3);
return 0;
}

在子类中初始化父类中的私有成员变量,需要在==子类== 的==初始化列表== 中加入==父类的构造函数==

继承构造语法:==using 类名::构造函数名==

注意:继承构造函数只能初始化==父类== 中的数据成员,对于==子类== 中的数据成员,仍然需要自行处理

在子类中不添加任何构造函数,只添加using 类名::构造函数名,这样就可以在子类中==直接继承父类的所有构造函数== ,通过他们去构造子类对象

继承构造函数可能遇到的问题:

1.如果父类的构造函数是==私有==的,那么子类==无法声明父类的继承构造函数==

class T
{
private: //基类构造函数声明为private
T() = default;
T(const int &a){}
T(const int &a, float &b){}
};
class U : public T{
public:
using T::T; //报错
int m_b{1};
};

2.如果子类是是以==虚继承== 的方式继承父类,在子类中也无法声明父类的继承构造函数

右值引用#

22.左值与右值#

c++11增加了一个新的类型,右值引用,记作:&& 。

左值是指(),我们可以找到这块地址的数据(可取地址)。

右值是只提供数据,()(不可取地址)。

有名字的变量都是左值,而右值是()的。

一般情况下位于等号左边的是(),位于等号右边的是(),但是也可以出现()给左值赋值的情况。

1

1

1

1

1

c++11增加了一个新的类型,右值引用,记作:&& 。

左值是指==在内存中有明确的地址==,我们可以找到这块地址的数据(可取地址)。

右值是只提供数据,==无法找到地址==(不可取地址)。

有名字的变量都是左值,而右值是==匿名== 的。

一般情况下位于等号左边的是==左值== ,位于等号右边的是==右值== ,但是也可以出现==左值== 给左值赋值的情况。

23.两种右值的区分#

c++11中右值分为两种情况:一个是(),另一个是()。

纯右值:()

将亡值:(),比如:T&& 类型函数的返回值,std::move()的返回值等。

在C++之中,使用左值去初始化对象或为对象赋值时,会调用()或()。而使用一个右值来初始化或赋值时,会调用()或()来移动资源,从而避免拷贝,提高效率。

而将亡值可以理解为通过()的方式获取到的值。

例如test是一个对象,我们将std::move(test)作为参数实例化一个Time类的test2对象,Time类会根据这个参数选择通过移动构造函数来初始化成员变量,通过移动构造的方式可以减少不必要的内存操作,但是之后我们也无法再访问 test 对象的内容了,因为都在移动构造函数之中置为了空指针。将亡值通过移动构造函数“借尸还魂”,通过 test2 变量延续了自己的生命周期。

所以,在确保其他变量不再被使用、或即将被销毁时,来() 。而实际上(),所以称之为:将亡值

1

1

1

1

1

c++11中右值分为两种情况:一个是==将亡值==,另一个是==纯右值==。

纯右值:==非引用返回的临时变量,运算表达式产生的临时变量,原始字面量,lambda表达式等==

将亡值:==与右值引用相关的表达式== ,比如:T&& 类型函数的返回值,std::move()的返回值等。

在C++之中,使用左值去初始化对象或为对象赋值时,会调用==拷贝构造函数== 或==赋值构造函数== 。而使用一个右值来初始化或赋值时,会调用==移动构造函数== 或==移动赋值运算符== 来移动资源,从而避免拷贝,提高效率。

而将亡值可以理解为通过==移动构造其他变量内存空间== 的方式获取到的值。

例如test是一个对象,我们将std::move(test)作为参数实例化一个Time类的test2对象,Time类会根据这个参数选择通过移动构造函数来初始化成员变量,通过移动构造的方式可以减少不必要的内存操作,但是之后我们也无法再访问 test 对象的内容了,因为都在移动构造函数之中置为了空指针。将亡值通过移动构造函数“借尸还魂”,通过 test2 变量延续了自己的生命周期。

所以,在确保其他变量不再被使用、或即将被销毁时,来==延长变量值的生命期== 。而实际上==该右值会马上被销毁== ,所以称之为:将亡值

24.右值引用的概念#

右值引用就是()。因为右值是()的,所以我们只能通过()的方式找到它。无论是左值引用还是右值引用都必须被(),因为引用类型本身并不拥有(),只是该对象的一个()。通过右值引用,该右值所占的()又可以被使用。

1

1

1

1

1

右值引用就是==对右值引用的类型==。因为右值是==匿名==的,所以我们只能通过==引用==的方式找到它。无论是左值引用还是右值引用都必须被==初始化==,因为引用类型本身并不拥有==所绑定对象的内存==,只是该对象的一个==别名==。通过右值引用,该右值所占的==内存==又可以被使用。

int&& value = 520; // 右值引用,520是字面量,是右值
class Test
{
public:
Test()
{
cout << "构造函数" << endl;
}
Test(const Test& a)
{
cout << "拷贝构造函数" << endl;
}
};
Test getObj()
{
return Test();
}
int main()
{
int a1;
//int &&a2 = a1; // 报错 右值引用不能被左值初始化
//Test& t1 = getObj(); // 右值不能初始化左值引用
Test && t2 = getObj(); //函数返回的临时对象是右值,可以被引用
const Test& t3 = getObj();// 常量左值引用是万能可以接收左值,右值,常量左值,常量右值
const int& t3 = a1; //被左值初始化
return 0;
}

25.右值引用的用处#

在c++用对象初始化对象时会调用(),如果这个对象占用(),那么这个拷贝的代价就是非常大的,在某些情况,如果想要避免对象的深拷贝,就可以使用()进行性能的优化。

右值引用具有移动语义,移动语义可以将(),通过()从一个对象转移到另一个对象这样就能(),大幅度提高性能。

需要()的类,应该设计移动构造,提高程序的效率。需要注意的是在提供移动构造的同时,一般也会提供(),左值初始化新对象时会走()

1

1

1

1

1

在c++用对象初始化对象时会调用==拷贝构造==,如果这个对象占用==堆内存很大==,那么这个拷贝的代价就是非常大的,在某些情况,如果想要避免对象的深拷贝,就可以使用==右值引用==进行性能的优化。

class Test
{
public:
Test() : m_num(new int(100))
{
cout << "构造函数" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "拷贝构造函数" << endl;
}
~Test()
{
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};

这段代码在调用Test t = getObj(); 的时候调用了拷贝构造函数,对返回的临时对象进行了深拷贝得到了对象t,在getObj函数中创建的对象虽然进行了内存申请操作,但是没有使用就被释放掉了。如果我们在函数结束后仍然可以利用在函数里面申请的空间就极大的节省了创建对象和释放对象的时间。这个操作就需要我们的右值引用来完成。

右值的移动语义

右值引用具有移动语义,移动语义可以将==堆区资源==,通过==浅拷贝==从一个对象转移到另一个对象这样就能==减少不必要的临时对象的创建、拷贝、销毁==,大幅度提高性能。

class Test
{
public:
Test() : m_num(new int(100))
{
cout << "构造函数" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "拷贝构造函数" << endl;
}
// 添加移动构造函数,参数是右值引用
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;
cout << "移动构造函数" << endl;
}
~Test()
{
delete m_num;
cout << "析构函数" << endl;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj(); // 因为getObj 返回的是右值,所以调用移动构造函数
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};

在上面的代码中添加了 移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj();并没有调用构造函数进行深拷贝,而是调用的(浅拷贝)移动构造,提高了性能。

本例子中,getObj()返回值是一个右值,在进行赋值操作的时候如果 等号 右边是一个右值,那么移动构造函数就会被调用。

结论:需要==动态申请大量的资源== 的类,应该设计移动构造,提高程序的效率。需要注意的是在提供移动构造的同时,一般也会提供==左值引用拷贝构造函数== ,左值初始化新对象时会走==拷贝构造==。

26.move#

c++11添加了右值引用,却不能(),在一些特定的情况下免不了需要左值初始化右值引用(用左值调用移动构造),如果想要用左值初始化一个右值引用需要借助() 函数。move() 函数可以()

1

1

1

1

1

c++11添加了右值引用,却不能==左值初始化右值引用==,在一些特定的情况下免不了需要左值初始化右值引用(用左值调用移动构造),如果想要用左值初始化一个右值引用需要借助==std::move()== 函数。move() 函数可以==将左值转换为右值==。

#include<iostream>
using namespace std;
class Test
{
public:
int a = 3;
int* m_num = &a;
Test() : m_num(new int(100))
{
cout << "构造函数" << endl;
}
Test(const Test& other) : m_num(new int(*other.m_num))
{
cout << "拷贝构造函数" << endl;
}
// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;
cout << "移动构造函数" << endl;
}
~Test()
{
delete m_num;
cout << "析构函数" << endl;
}
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
Test t2 = move(t); //此处调用移动构造,因为move返回一个右值
return 0;
};

27.引用折叠#

c++中,并不是所有情况下&&都代表右值引用,在模板和自动类型推导(auto)中,如果是模板参数需要指定为(),如果是自动类型推导需要指定为(),这两种情况下&&被称作()。另外 ()表示 一个右值引用,不是未定引用类型。

因为()或者()这种()作为参数时,有可能被推导成右值引用,也有可能被推导为左值引用,在进行()时()会发生变化,这种变化被称作()。折叠规则如下:

通过()推导()或者()得到的是一个()类型,()表示 一个右值引用

通过()推导()或者()得到的是一个()类型

1

1

1

1

1

c++中,并不是所有情况下&&都代表右值引用,在模板和自动类型推导(auto)中,如果是模板参数需要指定为==T&&==,如果是自动类型推导需要指定为==auto &&==,这两种情况下&&被称作==未定的引用类型==。另外== const T &&==表示 一个右值引用,不是未定引用类型。

template<typename T>
void fun(T&& param)
{
work(forward<T>(param))
}
int main()
{
fun(10); //对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
int x = 1;
fun(x);//对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
return 0;
}

因为==T&& ==或者==auto&&==这种==未定引用类型==作为参数时,有可能被推导成右值引用,也有可能被推导为左值引用,在进行==类型推导== 时==右值引用== 会发生变化,这种变化被称作==引用折叠== 。折叠规则如下:

通过==右值== 推导==T&&==或者==auto&& ==得到的是一个==右值引用==类型,==const T &&==表示 一个右值引用

通过==非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)==推导==T&& ==或者==auto&&==得到的是一个==左值引用==类型

int main()
{
int&& a1 = 1; //右值推导为 右值引用
auto&& bb = a1; //右值引用推导为 左值引用
auto&& bb1 = 2; //右值推导为 右值引用
int a2 = 1;
int &a3 = a2; //左值推导为 左值引用
auto&& cc = a3; //左值引用推导为 左值引用
auto&& cc1 = a2; //左值推导为 左值引用
const int& s1 = 1; // 常量左值引用
const int&& s2 = 1;// 常量右值引用
auto&& dd = s1; // 常量左值引用推导为 左值引用
auto&& ee = s2; // 常量右值引用推导为 左值引用
return 0;
}
C++11基础知识
https://fuwari.cbba.top/posts/c11基础知识/
作者
Chen_Feng
发布于
2023-08-29
许可协议
CC BY-NC-SA 4.0