【八股】【C++】11新特性

news/2024/7/24 5:58:19

这里写目录标题

  • auto
  • decltype
  • decltype(auto)
  • NULL与nullptr
  • RAII机制
  • 智能指针
    • auto_ptr
    • unique_ptr
    • shared_ptr
    • weak_ptr
  • 手写实现智能指针类需要实现哪些函数
  • Lambda
  • trivial destructor
  • ++i 与 i++
  • 左值与右值
  • 左值引用和右值引用
  • move

auto

C++11引入了auto关键字,也叫类型说明符,用它来让编译器替我们去分析表达式所属的类型,那这样我们就可以在声明变量的时候使用auto,让它根据变量初始值的类型自动为我们选择匹配的类型。我对auto的理解就是它会根据后面的值,来自己推测前面的类型是什么。auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代
1.减少冗长复杂的变量声明,使程序更清晰易读

list<int> l1;
list<int>::iterator i = l1.begin();
auto i = l1.begin();

2.定义模板参数时,用于声明依赖模板参数的变量
auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。

1.auto声明的变量必须要初始化,否则编译器不能判断变量的类型。这类似于const关键字。
auto 不能在函数参数和模板参数中使用,因为我们在定义函数的时候只是对参数进行了声明,指明了参数的类型,但并没有给它赋值,只有在实际调用函数的时候才会给参数赋值;而 auto 要求必须对变量进行初始化,所以这是矛盾的。

decltype

decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。
因为使用auto关键字进行类型推断的时候有一些限制,比如定义变量的时候必须要初始化,在某些特定环境下auto使用起来非常的不方便,所以C++11新特性才会引出decltype这个关键字。

auto serven_1 = 10;
decltype (exp) varname = value;
auto是根据=右边的初始值来自动推导变量的类型,所以auto在定义的时候必须初始化;
decltype是根据表达式exp来自动推导出变量的类型,跟=右边的value没有关系,所以decltype在定义的时候可以不用初始化

类的静态成员可以使用auto, 对于类的非静态成员无法使用auto,如果想推导类的非静态成员的类型,只能使用decltype。
decltype可以作用于变量、表达式及函数名。①作用于变量直接得到变量的类型;②作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;③作用于函数名会得到函数返回值类型,函数的返回值不能是void。

decltype(auto)

decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号右边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:

int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

NULL与nullptr

首先NULL和nullptr都可以用来表示空指针,其中NULL属于C 语言中的宏,而 nullptr 是C++11 中引入了的关键字。
在 C语言 中,NULL被定义为没有类型的指针常量 (void*) 0
在 C++中,NULL 被定义为整形常量 0

也就是说在 C++中,下面的这两行代码是完全等价的,没有区别。
int* p1 = NULL;  
int* p2 = 0;

在C++里,如果我们有时候我们使用 NULL 作为函数重载的参数,可能运行的结果和我们的期望不符合:

void test(int)

void test(int*)

int main()
{
	// 两次都会调test(int)。
	test(0);
	test(NULL);
	return 0;
	//因为在 C++中,字面常量 0 既可以表示一个整形常量 0,也可以表示无类型指针常量 (void*) 0,但是编译器默认把它看成是一个整形常量 0 
	如果把 0 当指针使用,就必须对其进行强转 (void*) 0 。
	为了解决这个问题,C++11标准增加了新的关键字 nullptr,保证在任何情况下都表示空指针。
}

在C++11中,sizeof(nullptr)sizeof((void*)0)所占字节相同,都为4。
为了提高代码的健壮性,使用表示空指针的时候最好使用nullptr。

RAII机制

RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,是C++的一种管理资源、避免泄漏的惯用方法。
RAII的做法是使用一个对象,在构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

智能指针

所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
简而言之,智能指针就是帮管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏!
一共有四种智能指针:C++98里的auto_ptr、C++11里的unique_ptrshared_ptrweak_ptr

auto_ptr

auto_ptr比较重要的作用就是解决"有异常抛出时发生内存泄漏"的问题;因为如果抛出异常的时候,有可能导致指针所指向的空间得不到释放从而导致内存泄漏;

// 定义智能指针
auto_ptr<Test> test(new Test);

三个常用函数
(1)get() 获取智能指针托管的指针地址

// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp = test.get();		// 获取指针返回
cout << "tmp->debug:" << tmp->getDebug() << endl;

(2)release() 取消智能指针对动态内存的托管

// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管
delete tmp2;	// 之前分配的内存需要自己手动释放
也就是智能指针不再对该指针进行管理,改由管理员进行管理!

(3)reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

// 定义智能指针
auto_ptr<Test> test(new Test);
test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULL
test.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之

reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
C++11后不建议使用auto_ptr,使用unique_ptr替代!
主要原因:复制或者赋值都会改变资源的所有权

auto_ptr<string> p1(new string("I'm Li Ming!"));
auto_ptr<string> p2(new string("I'm age 22."));
// p1: 012A8750   P2:012A8510
// p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
// 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
p1 = p2;	
// P1: 012A8510	 P2:00000000

unique_ptr

unique_ptr 和 auto_ptr用法几乎一样,只不过更严谨一些,它直接把拷贝构造函数和赋值重载函数给禁用掉了,不允许进行拷贝和赋值,所以也就直接禁止两个指针指向同一个资源,提高了代码的严谨性和安全性。

shared_ptr

auto_ptr和unique_ptr中有个问题就是它们都具有排他性,也就是都不支持多个智能指针指向同一块资源,比较局限。而shared_ptr就是为了解决这个问题,shared_ptr会允许多个智能指针指向同一块资源,并且能够保证共享的资源只会被释放一次。
shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:

shared_ptr会维护一份引用计数,用来记录当前资源被几个对象共享。
当复制或者拷贝的时候,引用计数加+1,当一个shared_ptr对象被销毁的时候,会调用析构函数把这个计数-1。
当计数为零的时候,代表已经没有指针指向这块内存,那么我们就释放它。

缺点:
假设我们要使用定义一个双向链表,把链表上节点都定义成shared_ptr智能指针,当其中两个节点互相引用的时候,就会出现循环引用的现象。

最开始node1和node2的引用计数都是1.
node1的next指向node2所指向的资源时,node2的引用计数+1,变成 2.
node2的pre指向noede1所指向的资源时,node1的引用计数+1,变成 2.
当这两个智能指针使用完后,调用析构函数,引用计数都-1,都变成1。
但是由于引用计数不为0,所以node1和node2所指向的对象都不会被释放,造成循环引用。

weak_ptr

weak_ptr也叫弱指针,引入weak_ptr的初衷就是为了解决刚才说的shared_ptr可能出现的循环引用问题,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会影响到引用数。
所以在刚才的例子里,我们可以用weak_ptr声明链表节点的pre和next两个指针,那么节点在指向前一个或后一个节点后并且不会改变shared_ptr的引用计数,当node1计数为0时,node1指向的空间就会被销毁掉。node2计数为0时,node2指向的空间也会被销毁掉,所以weak_ptr指针搭配shared_ptr指针可以很好解决循环引用的问题。

手写实现智能指针类需要实现哪些函数

我们需要一个指针对象,需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。
通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1
一个构造函数、拷贝构造函数、复制构造函数、析构函数、移动函数;

Lambda

Lambda函数是C++ 11的新特性之一,使用Lambda我们可以编写内嵌的匿名函数,以此来简化编程工作。我们平时调用函数的时候,都是需要被调用函数的函数名,但是使用lambda函数就不需要函数名,直接写在需要调用的地方就可以。

[capture](parameters) mutable throw() -> return_type { /* ... */ }

[capture] :也称为Lambda导入器,编译器根据导入器判断接下来的代码是否是Lambda函数[]内为捕捉外部变量的传递方式:值、引用等
mutable: 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)
-> 后为lambda函数的返回类型。一般情况下,编译器能够推出lambda函数的返回值,所以这部分可以省略。

// lambda函数定义后返回的是函数指针类型
auto addFunction= [](int a,int b) ->int {  return a + b;   };
int result  = addFunction(1,2);

实际上编译器会把Lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符,实现了一个operator()方法。
Lambda可以忽略参数列表和返回值,但必须包含捕获列表和函数体;
优点:
可以直接在需要调用函数的位置定义代码量不多的函数,而不需要预先定义好函数再使用,相比于普通函数它的结构层次更加明显、代码可读性更好。
缺点:
语法灵活增加了阅读代码的难度,不能进行函数复用。

trivial destructor

“trivial destructor"可以翻译为"无关痛痒的析构函数”,指的是用户并没有自定义析构函数,使用的析构是由系统生成的。

相反,如果用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露。

++i 与 i++

(1)前置返回一个引用,后置返回一个对象
(2)前置不会产生临时对象,后置会产生临时对象,所以前置比后置效率高一些

// ++i实现代码为:
int& operator++()
{

  *this += 1;
  return *this;

} 
//i++实现代码为:                 
int operator++(int)                 
{
int temp = *this;                   

   ++*this;                       

   return temp;                  
} 

左值与右值

一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
简单说,左值以变量的形式存在,指向内存,生命周期比较长,我们可以对左值进行各种操作;而右值通常以常量的形式存在,是一个临时值,不能被程序的其它部分访问,生命周期很短。

int x = 8;
x是左值,8是右值;

判断某个值是左值还是右值:
① 可以在赋值号(=)左侧的表达式是左值;只能位于赋值号(=)右侧的表达式就是右值;
② 有名称的,能取到地址的表达式是左值,反之是右值;

左值引用和右值引用

概念
简单来说:传统的C++中引用就是左值引用。
右值引用关联到右值时,右值会被存储到特定位置,右值引用指向这个特定位置,虽然右值虽然无法获取地址,但是右值引用是可以获取地址的,这个地址表示临时对象的存储位置。
无论左值引用还是右值引用,都是给对象取别名

左值引用
左值引用只能引用左值,不能直接引用右值。
但是const左值引用既可以引用左值,也可以引用右值

// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;

int& rt2 = 8;  // 编译报错,因为10是右值,不能直接引用右值
const int& rt3 = t;	// 但是const左值引用既可以引用左值

右值引用
右值引用只能引用右值,不能直接引用左值。
但是右值引用可以引用被move的左值

// 右值引用只能引用右值
int t = 10;
int&& rrt = t;  // 编译报错,不能直接引用左值

// 但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
const int&& rr6 = std::move(b);

实际意义
左值引用:我们知道函数参数传值和传值返回都会产生拷贝,代价很大,左值引用有两个使用场景:函数传参、函数返回值,相比于值传递减少了拷贝次数从而提高效率。
问题:当返回局部对象时,出了函数局部对象被析构,会调用拷贝构造出临时对象再返回。

string operator+(const string& s, char ch)
{
	string ret(s);
	ret.push_back(ch);
	return ret;
}
如果用右值引用就可以解决这个问题

右值引用的应用场景场景主要有两个:移动语义、完美转发
移动语义:如果我们把赋值操作看作资源转移,那传统的资源转让是通过拷贝实现的,需要两份空间。而移动语义是通过移动来实现资源转让,只使用一个空间。移动语义具体实现是基于移动构造和移动赋值。

拷贝构造函数的参数是 const左值引用,接收左值或右值;左值做参数,那么就会调用拷贝构造函数,做一次拷贝
移动构造函数的参数是右值引用,接收右值或被 move 的左值。右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝

完美转发

void notPerfectForward(int &&i) {
	printValue(i);  i会被当作左值处理
}

这个转发过程中,i最开始是右值引用,但再次传递时却变成了左值。失去了右值引用的特性,不是我们的预期。这种情况适合使用完美转发。
  完美转发指函数模板转发给内部调用的其他函数时转发参数的左右值属性不变。也就是说参数是左值引用,转发给下一个函数还是左值引用;参数是右值引用,转发给下一个函数还是右值引用。
  完美转发的实现基于std::forward

template<typename T>
void PerfectForward(T &&i) {
	printValue(std::forward<T>(i)); 这个i会被当作右值处理
}

move

在C++11中,move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,以实现移动语义,然后就可以通过右值引用使用该值,以用于移动语义。
从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能。

#include <utility>
int main()
{
    string str = "Hello";
    vector<string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    cout << "After move, str is \"" << str << "\"\n";
    cout << "The contents of the vector are \"" << v[0]
                                    << "\", \"" << v[1] << "\"\n";
}

After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
string s("Hello World11111111111111111");
string s1 = s;  // s是左值,所以调用拷贝构造函数
string s2 = move(s);  // s被move后变为右值,所以调用移动构造函数,s的资源会被转移用来构造s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了

http://www.niftyadmin.cn/n/1003674.html

相关文章

将目录下的所有文件输出为树结构

工具向&#xff0c;用python编写。 以下是代码&#xff1a; import osdef generate_tree(path, level0):tree ""folder_name os.path.basename(path)if level 0:tree f"{folder_name}\n"else:tree f"{ * level}├── {folder_name}/\n"…

在 JavaScript 中获取选中或突出显示的文本

本文将展示我们如何使用 DOM API 来让用户在屏幕上突出显示或选择文本。 DOM API 为我们提供了 getSelection() 方法&#xff0c;该方法允许我们获取用户选择的文本。 窗口对象可以直接访问这个方法。 让我们看看如何实际实现此功能。 使用 JavaScript 中的 window.getSelecti…

MAYA动力学曲线带动骨骼

例子 2 自由下落了 对比测试 尖端 太麻烦&#xff0c;使用风 nucleus1.windDirectionZ10*sin(time) 把球合成一个 删除一个解算器&#xff0c;就不动了

parcel运行终端报错Uncaught ReferenceError: parcelRequire is not defined解决方案

我们通过指令 npm install parcel-bundler安装的parcel 运行起来会有一个报错 换个版本就好了 打开项目终端 我们先执行 npm uninstall parcel-bundler将错误的版本给他干掉 然后执行 npm install parcel安装正确的版本 然后运行项目 在浏览器中访问地址就一切正常了

US-P1-R-S单路控制比例阀放大器

US-P1-R-S、US-P1-R-C、US-P2-R-S、US-P2-R-C、US-P1-M-C、US-P2-M-S M12插头端子号 &#xff08;US-P1-…-C&#xff09; 线缆颜色 &#xff08;US-P1-…-S&#xff09; 端子定义 &#xff08;US-P1-R&#xff09; 端子定义 &#xff08;US-P1-M&#xff09; 1 红 2 …

sql中的主键和索引

在 SQL 中&#xff0c;PRIMARY KEY、UNIQUE、INDEX 和 FULLTEXT 是用于定义表中字段约束和创建索引的关键字。它们具有不同的含义和功能&#xff0c;下面是它们的区别以及具体的例子说明&#xff1a; PRIMARY KEY&#xff08;主键&#xff09;&#xff1a; 主键是用于唯一标识…

Pytorch ----注意力机制与自注意力机制的代码详解与使用

注意力机制的核心重点就是让网络关注到它更需要关注的地方 。 当我们使用卷积神经网络去处理图片的时候&#xff0c; 我们会更希望卷积神经网络去注意应该注意的地方&#xff0c;而不是什么都关注 &#xff0c;我们不可能手动去调节需要注意的地方&#xff0c;这个时候&#x…

pgsql 数据类型为jsonb的字段如何去掉json中的某个属性值

在 PostgreSQL 中&#xff0c;要从 JSONB 类型的字段中删除某个属性值&#xff0c;可以使用 jsonb_set() 函数。以下是一个示例&#xff1a; UPDATE your_table SET your_column jsonb_set(your_column, {property_name}, null, false) WHERE your_condition;请按照以下步骤进…