欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 手游 > 【C++】C++11新特性(一)

【C++】C++11新特性(一)

2025/4/28 7:30:19 来源:https://blog.csdn.net/2401_84170223/article/details/147569260  浏览:    关键词:【C++】C++11新特性(一)

文章目录

  • 列表初始化
  • initializer_list
  • 左值引用和右值引用

列表初始化

在 C++98 中可以使用{}对数组或者结构体元素进行统一的列表初始值设定

struct Point
{int _x;int _y;
};
int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0; 
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用列表初始化时,可添加等号(=),也可不添加

列表初始化可以防止窄化转换。窄化转换是指可能导致数据丢失或精度降低的转换。例如,将一个浮点数转换为整数时,如果浮点数的小数部分非零,就会丢失小数部分的数据。如果尝试进行窄化转换,编译器会报错

一切皆可用列表初始化,可以省略等号

class A
{
public:A(int a):_a(a){}A(int a,int b):_a(a),_b(b){}
private:int _a;int _b;
}
A a1{1};
A a2{11,45};
//本质上就是多参数的隐式类型转换
//在C++98单参数隐式类型转换的基础上引入了多参数的隐式类型转换
int arr[]{12,34,56,78,90};

创建对象时也可以使用列表初始化方式调用构造函数初始化

本质是生成一个 A 的临时对象,然后将这个临时对象赋给变量,编译器优化后就变成了隐式类型转换,可以通过在类前面加上关键字 explicit 来阻止隐式类型转换

A& a1{12};//临时对象具有常性,A&对象不能引用
const A& a2{12};

构造生成一个临时对象,临时对象具有常性,可以赋值给const变量

A a1=11;//单参数的隐式类型转换
A a2={11,12};//多参数的隐式类型转换,实质上是构造了一个A对象,然后调用了拷贝构造传给a2对象

initializer_list

本质是个常量数组,里面只存有指向first和last的指针,因此32位下只有8字节,支持迭代器,因此可以遍历

统一容器初始化方式:标准容器(如std::vectorstd::list等)都支持使用std::initializer_list进行初始化。这使得容器的初始化方式更加统一和直观。也可以作为operator=的参数,这样就可以用大括号赋值

如果想要自定义类型支持列表初始化,就可以在自定义类型的构造函数中使用std::initializer_list

std::initializer_list是一个不可变的类型,即一旦创建,它的元素不能被修改。它提供了beginend函数来访问其中的元素,就像访问数组一样,但不提供修改元素的接口

vector(const T& x1);
vector(const T& x1,const T&x2);
...
vector(initializer_list<T> il);
//这个构造一劳永逸的解决了问题,不用一个个写构造
vector(initializer_list<T>& il)
{vector(std::initializer_list<T> il)//可以不用加引用,和const原因:这里的initializer_list本来要的就是浅拷贝//实质上是il的指针分别指向常量数组的第一个元素和常量数组的最后一个元素,不用拷贝,直接就构造了,因此加上const和引用对性能提升没有影响//{1,2,3,4,5,6,78,75}
{reserve(il.size());for (auto& e : il){push_back(e);}
}
}
vector<int> v1={1,5,6,8,6,4}//隐式类型转换
vector<int> v2({1,5,5,6,9,1})//构造函数
map<int,int> m1={{1,2},{2,3},{3,4}};
//实际上是生成了隐式类型转换成了pair对象,然后再使用initializer_list<pair>进行构造

中间实际生成了临时对象,然后拷贝构造,实质也是隐式类型转换

实质是pair多参数的隐式类型转换和initializer_list

decltype

与typeid类似,但decltype推断出的类型可以定义变量,也可以用来模板传参,而typeid不行,typeid 只是一个字符串,不能用于定义对象

list<int>::iterator it1;
cout << typeid(it1).name();
//结果:class std::_List_iterator<class std::_List_val<struct std::_List_simple_types<int> > >
typeid(it1).name() it2;//错误,typeid推出的类型不能用于定义变量,只是一个字符串
decltype(it1) it2;
cout << typeid(it2).name();
//结果:class std::_List_iterator<class std::_List_val<struct std::_List_simple_types<int> > >
//用于赋值的类型和原类型一致

decltype 的通常用于推导 auto 的类型

class A
{
public:T* func(){return new T;}private:int _a=10;
};
auto func()
{list<int> l;return l.begin();
}
auto i = func();
A<decltype(i)> a;

decltype 和 auto 的使用会增加代码的阅读难度

左值引用和右值引用

左值和右值的区分:

左值可以取地址,右值不能取地址

匿名对象不能取地址,是右值

表示数据的表达式,如字面常量,表达式返回值,函数返回值等都是右值

右值引用:给右值取别名,常见右值: 字面常量、表达式返回值,函数返回值

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值但是可以取它的地址

左值引用和右值引用都是给对象起别名

纯右值(内置类型的)

将亡值(自定义类型的)

string&& ref1 = string("123");
string&& ref2 = to_string(123);
int&& ref3 = 10;
int&& ref4=(x+y);

左值引用不可以给右值起别名,但const修饰的左值可以

右值引用不可以给左值起别名,但是可以给move以后的左值起别名

const int& leftref = 10;
int a = 10;
int&& rightref = move(a);

move 只是会将左值强转为右值,但并不会涉及到资源的分配,只是告诉编译器可以进行右值操作,允许将这个变量的资源进行分配

被 move 之后的左值,一旦被分配,则自己就不再拥有原来的资源,而是分配出去了,就像真正的右值一样

引用的意义是减小拷贝,提高效率

左值引用没有彻底解决这个问题,因为局部变量无法引用

移动构造:传入右值引用的构造,将传入的右值的资源剥夺后分配给要构造的对象,避免了拷贝造成的资源占用

如果是右值,那么直接把资源转移

可以直接返回局部本来要销毁的变量,不用拷贝构造

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

string func()
{string s="12456";return ret;//return move(ret);编译器自动优化成右值
}
string ret=func();
//这里由于编译器优化,会将原本ret需要拷贝构造一个临时对象
//然后ret通过移动构造将这个临时对象的资源拿到,优化为直接进行移动构造
//相当于将ret强行识别为右值

纯右值: 内置类型,返回类型为非引用类型的函数调用或运算符表达式属于纯右值,lambda 表达式为纯右值, 因为表达式本身没有名字,本质是临时值,例如 42、a + b 或 func()(函数返回非引用类型)
将亡值: 自定义类型,返回类型为对象右值引用的表达式为将亡值,右值类对象的成员为将亡值,右值数组的成员为将亡值,标识一个具名对象,但该对象即将被销毁(如通过 std::move 转换)。它允许安全地移动资源,而非拷贝

移动语义允许高效地转移资源,避免不必要的复制,特别是对于大型或资源密集型的对象,当一个变量或者右值被移动后,资源将会被分配到其他位置,原来的变量或右值不能够访问

**移动赋值:**移动赋值运算符(operator=的移动赋值版本)用于将一个对象的资源转移到另一个已经存在的对象中。与移动构造函数类似,它的主要目的是高效地处理资源的转移,避免不必要的资源复制。

移动赋值运算符通常也期望右值引用作为参数。当有右值(如临时对象)出现在赋值表达式的右侧时,编译器会优先选择移动赋值运算符(如果定义了的话)来进行资源的转移。

如果想对左值进行移动赋值操作,可以像移动构造函数一样,使用std::move函数将左值转换为右值引用

不能被移动的左值

  1. 常量对象(const 左值)

移动操作需要修改源对象,常量禁止对象修改,尝试移动常量对象会调用拷贝而非移动

const std::string s = "Hello";
std::string s2 = std::move(s);
// 调用拷贝构造函数,s中的值还是Hello,说明没有移动,只是进行了拷贝
  1. 基本数据类型的对象(int,float,double)

没有动态资源,移动等同于拷贝

int a = 10;
int&& rightref = move(a);//a中的值还是10,说明只是拷贝,并没有移动
  1. 无移动操作的类对象

若类未定义移动构造函数/赋值运算符,或编译器未隐式生成,则移动会退化为拷贝

class NoMove {
public:NoMove(){cout << "constructor function" << endl;}NoMove(const NoMove&){cout << "copy constructor" << endl;} // 只有拷贝构造函数
};
int main()
{NoMove obj1;NoMove obj2 = std::move(obj); // 调用拷贝构造函数
}
  1. 移动操作被显式删除的类对象

若移动构造函数/赋值运算符被标记为= delete,尝试移动会引发编译错误

class DeletedMove {
public:DeletedMove(){}DeletedMove(DeletedMove&&) = delete;
};
DeletedMove dm1;
DeletedMove dm2 = std::move(dm); // 编译错误
  1. 包含不可移动成员的类对象

若类的成员或基类不可移动,隐式移动操作会被删除,导致只能拷贝

struct NonMovableMember {NonMovableMember(NonMovableMember&&) = delete;
};
class Wrapper {NonMovableMember m;
};
Wrapper w;
Wrapper w2 = std::move(w); // 隐式移动被删除,尝试调用拷贝构造函数
struct S { int i : 4; };
S s;
int x = std::move(s.i); // 实际拷贝位域的值

右值引用可以引用字面常量int&& a=5,这里的 a 实际上是 const int&& 类型,因此不能通过 a 修改这个值

右值引用还能引用表达式产生的临时对象,如,在函数调用返回一个临时对象时,这个临时对象是右值,可以用右值引用绑定它

  • 假设存在一个函数std::vector<int> createVector(),它返回一个std::vector<int>对象。可以这样使用右值引用:std::vector<int>&& v = createVector();。这里createVector返回的临时vector对象被右值引用v绑定
  • 另一个例子是算术表达式的结果,如int&& result = (3 + 5);。表达式(3 + 5)产生一个临时的int8,右值引用result绑定了这个临时值

在移动语义的场景下,右值引用用于引用那些即将被移动资源的对象,当一个对象要将自己的资源(如动态分配的内存)转移给另一个对象时,会使用右值引用

右值引用本身是左值,只有右值本身处理成左值,才能实现移动构造,因此右值引用的函数要将接收到的右值传给下一个函数时,要进行 move,才能调用下一个函数的 move 形式

如果右值引用的属性是右值,那么移动构造和移动赋值,要转移的语法逻辑是矛盾的,因为右值无法被改变(可以理解为右值默认自带 const 属性)因此右值引用本身要被处理成左值

std::vector<int> createVector() {std::vector<int> v = {1, 2, 3};return v;
}
std::vector<int> v2 = std::move(createVector());

这里使用 move 的原因,由于不确定编译器是否会优化,此时,即使返回值在理论上是右值,仍然可能会调用拷贝构造函数进行复制,因此需要使用 move 告诉编译器需要使用,移动语义来传递对象,而不是依赖可能会发生也可能不会发生的返回值优化,函数返回值虽然在语义上是右值,但在语法形式上可以被当作左值处理

移动拷贝对于效率的提升是针对于自定义类型的深拷贝的类,因为只有深拷贝的类才有移动函数

对于内置类型和浅拷贝自定义类型,没有移动函数

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词