C++学习(3)11新特性
1 类型推导
1.1 auto
auto可以让编译器在编译期推导出变量的类型
- auto的使⽤必须马上初始化,否则⽆法推导出类型;
- auto在⼀⾏定义多个变量时,各变量的推导不能产⽣⼆义性,否则编译失败;
- auto不能⽤作函数参数;
- 在类中auto不能⽤作⾮静态成员变量;
- auto不能定义数组,可以定义指针;
- auto⽆法推导出模板参数;
- 在不声明为引⽤或指针时,auto会略等号右边的引⽤类型和cv限定;
- 在声明为引⽤或者指针时,auto会保留等号右边的引⽤和cv属性;
1.1 decltype
decltype则⽤于推导表达式类型,这里只⽤于编译器分ຉ表达式的类型,表达式实际不会进⾏运算;不会像auto⼀样忽略引⽤和cv属性,decltype会保留表达式的引⽤和cv属性;
cv属性
CV限定符即cv-qualifier,C++语言中指const和volatile限定符。通常来说,C++语言中有两种情况不能使用CV限定符进行限定:
A、非成员函数不能使用CV限定
B、静态成员函数不能使用CV限定
对于decltype(exp)有:
- exp是表达式,decltype(exp)和exp类型相同
- exp是函数调⽤,decltype(exp)和函数返回值类型相同
- 其它情况,若exp是左值,decltype(exp)是exp类型的左值引⽤
auto和decltype的配合使⽤:
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
2 右值引⽤
左值右值:
左值: 可以放在等号左边,可以取地址并由名字;
右值: 不可以放在 i 等号左边,不能取地址,没有名字;
字符串字⾯值"abcd"也是左值,不是右值;
++i、--i是左值,i++、i--是右值;
2.1 将亡值
将亡值是指C++11新增的和右值引⽤相关的表达式
将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间⽅式获取的值,在确保其它变量不再被使⽤或者即将被销毁时,可以避免内存空间的释放和分配,延⻓变量值的⽣命周期,常⽤来完成移动构造或者移动赋值的特殊任务;
2.2 左值引⽤
左值引⽤就是对左值进⾏引⽤的类型,是对象的⼀个别名并不拥有所绑定对象的堆存,所以必须⽴即初始化。 对于左值引⽤,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使⽤const引⽤形式;
2.3 右值引⽤
表达式等号右边的值需要是右值,可以使⽤std::move
函数强制把左值转换为右值;
2.4 移动语义
可以理解为转移所有权,对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为⾃⼰所拥有,别⼈不再拥有也不会再使⽤。通过移动构造函数使⽤移动语义,也就是std::move
;移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作⽤,还是会拷⻉,因为它们实现没有对应的移动构造函数;
浅拷⻉:
- a和b的指针指向了同⼀块内存,就是浅拷⻉,只是数据的ᓌ单赋值;
深拷⻉:
- 深拷⻉就是再拷⻉对象时,如果被拷⻉对象内部还有指针引⽤指向其它资源,⾃⼰需要重新开⼀块新内存存储资源;
2.5 完美转发
写⼀个接受任意实参的函数模板,并转发到其它函数,⽬标函数会收到与转发函数完全相同的实参,通过std::forward()
实现;
想详细了解可以参见:
https://blog.csdn.net/zhizhengguan/article/details/115833949
3 nullptr
nullptr
是⽤来代替NULL
,⼀般C++会把NULL
、0
视为同⼀种东西,这取决去编译器如何定义NULL
,有的定义为((void*)0)
,有的定义为0
;C++不允许直接将void*
隐式转换到其他类型,在进⾏C++重载时会发⽣混乱;
如:
void foo(char *);
void foo(int );
如果NULL
被定义为 ((void*)0)
,那么当编译char *ch = NULL
时,NULL被定义为 0,当foo(NULL)
时,此时NULL
为0,会去调⽤foo(int )
,从⽽发⽣混乱;
为解决这个问题,从⽽需要使⽤NULL
时,⽤nullptr
代替;
C++11引⼊nullptr
关键字来区分空指针和0。nullptr
的类型为 nullptr_t
,能够转换为任何指针或成员指针的类型,也可以进⾏相等或不等的⽐较。
4 范围for循环
基于范围的迭代写法,for(变量:对象)表达式
对string对象的每个字符做⼀些操作:
std::string str("some thing");
for(char c : str) cout << c << endl;//对字符串的每个字符进行cout操作
对vector进行遍历:
std::vector<int> arr(5, 100);
for(auto it : arr) cout << *it << endl;//对字符串的每个字符进行cout操作
5 初始化列表
C++定义了⼏种初始化⽅式,例如对⼀个int变量 x初始化为0:
int x = 0; // method1
int x = {0}; // method2
int x{0}; // method3
int x(0); // method4
采⽤花括号来进⾏初始化称为列表初始化,⽆论是初始化对象还是为对象赋新值。
⽤于对内置类型变量时,如果使⽤列表初始化,且初始值存在丢失信息⻛险时,编译器会报错。
long double d = 3.1415926536;
int a = {d}; //存在丢失信息⻛险,转换未执⾏。
int a = d; //确实丢失信息,转换执⾏。
6 lambda表达式
lambda表达式表示⼀个可调⽤的代码单元,没有命名的内联函数,不需要函数名因为我们直接(⼀次性的)⽤它,不需要其他地⽅调⽤它。
6.1 lambda 表达式的语法
[capture list] (parameter list) -> return type {function body }
// [捕获列表] (参数列表) -> 返回类型 {函数体 }
// 只有 [capture list] 捕获列表和 {function body } 函数体是必选的
auto lam =[]() -> int { cout << "Hello, World!"; return 88; };
auto ret = lam();
cout<<ret<<endl; // 输出88
-> int :代表此匿名函数返回int,⼤多数情况下lambda表达式的返回值可由编译器猜测得出,因此不需要我们指定返回值类型。
6.2 lambda 表达式的特点
变量捕获:
1. [] 不捕获任何变量,这种情况下lambda表达式内部不能访问外部的变量;
2. [&] 以引⽤⽅式捕获所有变量(保证lambda执⾏时变量存在);
3. [=] ⽤值的⽅式捕获所有变量(创建时拷⻉,修改对lambda内对象⽆响);
4. [=, &foo] 以引⽤捕获变量foo, 但其余变量都ᶌ值捕获;
5. [&, foo] 以值捕获foo, 但其余变量都ᶌ引⽤捕获;
6. [bar] 以值⽅式捕获bar; 不捕获其它变量;
7. [this] 捕获所在类的this指针;
int a = 1, b = 2, c = 3;
auto lam2 = [&,a](){//b,c以引用捕获,a以值捕获。
b = 5; c = 6;//a=1;a不能赋值
cout << a << b << c << endl;//输出 1 5 6
};
1am2();
void fcn(//值捕获
size_t v1 = 42;
autof = [v1] {returnv1;};
v1 = 0;
auto j = f(); //j=42;创建时拷贝,修改对1ambda内对象无影响
void fcn(){//可变1ambda
size_t v1 = 42;
auto f = [v1] () mutable{return++v1;};//修改值捕获可以加mutable
v1 = 0;
auto j = f(); //j = 43
}
void fcn(){//引用捕获
size_t v1 = 42;//非const
autof = [&v1](){return ++v1;};
v1 = 0;
autoj=f();//注意此时j=1;
}
lambda最⼤的⼀个优势是在使⽤STL中的算法(algorithms)库
int arr[] = {6, 4, 3, 2, 1, 5};
bool compare(int& a, int& b){//谓词函数
return a>b;
}
std::sort(arr, arr+6, compare);
//1ambda形式:
std::sort(arr, arr+6, [](const int& a, const int& b){return a>b;}};
//降序排序
std::for_each(begin(arr), end(arr),[](const int& e){cout << "After:" << e << endl;});
//6,5,4,3,2,1
7 并发
7.1 std::thread
default (1) | thread() noexcept; |
---|---|
initialization (2) | template <class Fn, class... Args> explicit thread (Fn& fn, Args&... arg33); |
copy [deleted] (3) | thread(const thread&)=delete; |
move (4) | thread(thread&&x)noexcept; |
- 默认构造函数,创建⼀个空的 thread 执⾏对象。
- 初始化构造函数,创建⼀个 thread对象,该 thread对象可被 joinable,新产⽣的线程会调⽤ fn 函数,该函数的参数由 args 给出。
- 拷⻉构造函数(被禁⽤),意ޱ着 thread 不可被拷⻉构造
- move 构造函数,move 构造函数,调⽤成功之后 x 不代表任何 thread 执⾏对象。
注意:
可被 joinable
的 thread
对象必须在他们销毁之前被主线程 join
或者将其设置为 detached
。
std::thread
在使⽤上容易出错,即std::thread
对象在线程函数运⾏期间必须是有效的。
#include <iostream>
#include <thread>
void threadproc() {
while(true) {
std::cout << "I am New Thread!" << std::endl;
}
}
void func() {
std::thread t(threadproc);
}
int main() {
func();
while(true) {} //让主线程不要退出
return 0;
}
以上代码再main函数中调⽤了func函数,在func函数中创建了⼀个线程,看起来好像没有什么问题,但在实际运⾏是会崩溃。
因为在func函数调⽤结束后,func中局部变量t(线程对象)销毁,而线程函数仍然在运⾏。所以在使⽤std::thread
类时,必须保证线程函数运⾏期间其线程对象有效。
std::thread
对象提供了⼀个detach
⽅法,通过这个⽅法可以让线程对象与线程函数脱离关系,这样即使线程对象被销毁,也不影响线程函数的运⾏。只需要在func函数中调⽤detach
⽅法即可,代码如下:
// 其他代码保持不变
void func() {
std::thread t(threadproc);
t.detach();
}
7.2 lock_guard
lock_guard
是⼀个互斥量包装程序,它提供了⼀种⽅便的RAII(Resource acquisition is initialization )⻛格的机制来在作⽤域块的持续时间内拥有⼀个互斥量。
创建lockguard
对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lockguard对象的作⽤域时,lock_guard
析构并释放互斥量。
ਙ的特点如下:
- 创建即加锁,作⽤域结束⾃动析构并解锁,⽆需⼿⼯解锁;
- 不能中途解锁,必须等作⽤域结束才解锁;
- 不能复制
7.3 unique_lock
unique_lock是
⼀个通⽤的互斥量锁定包装器,它允许延迟锁定,限时Ⴎ度锁定,递归锁定,锁定所有权的转移以及与条件变量⼀起使⽤。
简单来说,uniquelock
是 lockguard
的升级加强版,它具有 lock_guard
的所有功能,同时⼜具有其他很多⽅法,使⽤起来更强灵活⽅便,能够应对更复的锁定需要。
特点如下:
- 创建时可以不锁定(通过指定第⼆个参数为std::defer_lock),⽽在需要时再锁定;
- 可以随时加锁解锁;
- 作⽤域规则同 lock_grard,析构时⾃动释放锁;
- 不可复制,可移动;
- 条件变量需要该类型的锁作为参数(此时必须使⽤unique_lock;
8、C++11之后的新特性概述
c++11后续又有11,14,17,20等众多新版本,C++14在11的基础上查缺补漏,并未加入许多新特性,C++17为C++11后的第一个大版本,在此暂不做过多介绍,有兴趣可以参考一下博客:
https://blog.csdn.net/yyz_1987/article/details/124877714。
8.1 14新特性概述
c++14并没有太大的改动,就连官方说明中也指出,c++14相对于c++11来说是一个比较小的改动,但是在很大程度上完善了c++11,所以可以说c++14就是在c++11标准上的查漏补缺。
8.1.1语言新特性
- 变量模板
c++14可以定义变量模板
template<class T>
constexpr T pi = T(3.1415926535897932385L); // variable template
int main()
{
std::cout << pi<double> << std::endl;//3.14159
std::cout << pi<float> << std::endl;//3.14159
std::cout << pi<int> << std::endl;//3
return 0;
}
如果想在类内部定义变量模板,那么需要定义静态变量,同时也可以对模板变量进行特化。
- lambda表达式的改动
支持lambda泛型参数;
支持初始化捕获;
// x通过初始化捕获
// y通过类型推导获取类型
auto func = [x = 3](auto y) {return x + y; };
std::cout << func(4.5) << std::endl;
std::cout << func(7) << std::endl;
std::unique_ptr<Item> item(new Item());
auto func = [m = std::move(item)] { /* do something */ };
泛型参数这个比较好理解,初始化捕获有什么好处呢。在c++11中,lambda表示捕获变量只能通过值捕获或者引用捕获,支持了初始化表达式之后,我们就可以更灵活的捕获了,比如移动捕获。
- constexpr限制放宽
constexpr是在c++11标准中被引入的,旨在让编译器可以在编译器就做好一些工作,而不必等到运行期,这个在很多时候可以提高程序在运行时的效率。 在c++11中,constexpr要求非常严格,这就导致了constexpr的并不是那么易用。在c++14中就可以使用if、局部变量和循环了(c++14可以,c++11报错)
constexpr int FuncNew(int n)
{
if (n <= 0){
return 0;
}
int sum = 0;
for (int i = 0; i < n; ++i){
sum += i;
}
return sum;
}
- 二进制字面量
可以使用0b或者0B开头直接定义二进制变量。
{
int bit1 = 0b1001;
int bit2 = 0B1011;
std::cout << bit1 << " " << bit2 << std::endl;// 输出结果:9 11
}
- 数字分隔符
我们定义一个比较大的值的时候,有时候会很难一眼看出来他是多少,但是c++14之后,就可以对数字添加分隔符了,这使得大数字的可读性变得更高了。比如下面两个定义1亿的方式,第二个明显会比第一个可读性高很多。
int main(){
// 一亿
int val1 = 100000000;
int val2 = 100'000'000;
std::cout << val1 << " " << val2 << std::endl;
return 0;
}
注意:数字分隔符并不会对数字本身有任何的影响,只是对数字可读性的增强。
- 函数返回值推导
c++14新增了函数返回值的推导,当返回值声明为auto时,编译器会根据你的return语句推导出你的返回值类型。
template<typename T>
auto Func(T x, T y){
return x + y;
}
int main(){
std::cout << Func(3, 4) << std::endl; // 返回值推导为int
std::cout << Func(3.1, 4.2) << std::endl; // 返回值推导为double
return 0;
}
返回值的推导有几个限制条件:
- 如果有多个推导语句,那么多个推导的结果必须一致
// 编译报错,第一个return推导为int,第二个return推导为double,两次推导结果不一致
auto Func(int flag){
if (flag < 0){
return 1;
}
else{
return 3.14;
}
}
- 如果没有return或者return为void类型,那么auto会被推导为void。
auto f() {} // returns void
auto g() { return f(); } // returns void
auto* x() {} // error: cannot deduce auto* from void
- 一旦在函数中看到return语句,从该语句推导出的返回类型就可以在函数的其余部分中使用,包括在其他return语句中。
auto Sum(int i){
if (i <= 1){
return i; // 返回值被推导为int
}
else{
return Sum(i - 1) + i; // sum的返回值已经被推导出来了,所以这里是没有问题的
}
}
但是如果还没被推导出来,那就不能使用。
auto Sum(int i){
if (i > 1){
return Sum(i - 1) + i;
}
else{
return i;
}
}
// 编译报错,因为Sum的返回值还没有被推导出来,所以还不能使用
//error: use of ‘auto Sum(int)’ before deduction of ‘auto’
- 不能推导初始化列表。
auto func () { return {1, 2, 3}; }
// 编译报错
//error: returning initializer list
- 虚函数不能使用返回值推导
struct Item{
virtual auto Func();
};
// 编译报错
//error: virtual function cannot have deduced return type
- [[deprecated]]标记
而c++14之后,c++引入了deprecated标记,可以在编译期间发出警告。
[[deprecated]]
void Func(){
std::cout << "hello world!" << std::endl;
}
int main(){
Func();
return 0;
}
// 编译的时候会有警告信息
warning: ‘void Func()’ is deprecated [-Wdeprecated-declarations]
8.1.2 库的新特性
- 新增std::make_unique
std::unique_ptr<Item> pt = std::make_unique<Item>();
- 新增读写锁std::shared_timed_mutex与std::shared_lock
c++11引入了多线程线程的一些库,但是是没有读写锁的,因此在c++14引入了读写锁的相关实现(头文件shared_mutex),其实c++14读写锁也还不够完善,知道c++17读写锁这块才算是完备起来,后面写c++17的时候计划把这块再完整的梳理一下。
std::shared_timed_mutex是带超时的读写锁对象
std::shared_lock是加锁的RAII实现,即构造时加锁,析构时解锁。
std::shared_timed_mutex mtx;
void Func(){
// 加读锁,离开作用域时自动解锁
std::shared_lock<std::shared_timed_mutex> lck(mtx);
// do something
}
- std::exchange
c++14新增了一个接口std::exchange(头文件utility),其实这个也并不算是新增的,因为这个接口其实在c++11的时候就有了,只不过在c++11中作为一个内部函数,不暴露给用户使用,在c++14中才把它暴露出来给用户使用。使用方法也很简单。
int main(){
std::string s1 = "hello";
std::string s2 = "world";
std::exchange(s1, s2);
std::cout << s1 << " " << s2 << std::endl;// 输出结果:world world
return 0;
}
可以看到,exchange会把第二个值赋值给第一个值,但是不会改变第二个值。除此之外,我们这里说明一个关键的点。exchange的第二个值是万能引用,所以说他是既可以接收左值,也可以接收右值的,所以我们可以这样来使用。
- std::quoted
建议看下官方介绍,我也没看懂。。。
https://en.cppreference.com/w/cpp/14
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理