列表初始化是C++11的一个新特性,不同于初始化列表。
列表初始化在对自定义类型时,会调用它的构造函数。
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
以前学习到的一般只有单个参数的构造函数支持隐式类型转换,现在的列表初始化实际上支持了多参数的构造函数支持隐式类型转换。
在上面的例子中就是{1,2}这个初始化列表会走多参数的构造,构造出一个Point临时对象,再进行拷贝构造,只不过这两步被编译器优化成直接构造。
在构造函数前加上explicit关键字就能禁止编译器优化了。自定义类型本质上还是调用构造函数。
所以还能这样玩:
const Point& p1 = {1,2};
这也是多参数构造支持隐式类型转换,构造的临时对象具有常性,用const引用才支持。
auto il = {1,2,3,4,5};
il就是一个intializer_list类型,等号右边是一个常量数组空间,存在常量区,intializer_list的大致结构如下:
template<class T>
class intializer_list
{
const T* _start;
const T* _start;
};
_start指向等号右边的常量数组空间的起始地址
_finush指向等号右边的常量空间的末尾元素地址的下一个
所以
auto il = {1,2,3,4,5};
的本质上还是调用il的构造函数。
有了initializer_list之后,各种容器就支持了用initializer_list来构造了。
比如说:
vector<int> v1 = {1,2,3,4,5};
本质上是先用等号右边的常量数组构造initializer_list,然后vector重载了一个用initializer_list来构造vector的函数,再调该函数完成从initializer_list到vector的构造。
还有一个容器:map
map<string,string> m1 = {{"sort","排序"},{"left","左边"}};
本质上右边的常量数组会被识别成pair,所以先调用pair的构造函数,生成一个pair
即
常量数组空间—>pair–>initializer_list < pair > -->map
decltype是一个关键字,能推出变量的类型,再定义变量
它还可以作模板参数
int a;
//decltype推导对象类型,再定义变量
//还可以作模板参数
decltype(a) a1;
cout << typeid(a1).name() << endl;
vector<decltype(a)> v;
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。
所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
结论:nullptr的本质就是 (void*)0
要知道什么是右值,先知道什么是左值。
之前学的引用中,一般都是左值引用,无论左值引用还是右值引用,都是给对象取别名。
简单区分一下左值和右值的区别:
左值可以获取地址,右值不能获取地址
下面详细讲解:
1、下面的是左值,对左值的引用
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
const int * ps = "hello c++";//字符串能取地址,也是左值,只不过是不能修改而已
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
//左值可以出现在赋值符号的右边
return 0;
}
特点:
2.下面几个都是右值
10
x+y
fmin(a,b)
特点:
结论: 左值引用是给左值取别名(&),右值引用是给右值取别名。(&&)
int main()
{
//1.右值引用,给右值取别名
int&& a = 10;
double x = 1.1, y = 2.2;
double&& rx = x + y; //虽然x,y单独都是左值,但是x+y表达式的返回值是一个右值。
//2.左值引用,给左值取别名
int b = 2;
int& rb = b;
//3.左值引用既能给左值取别名,也能给右值取别名
const int& c = 10; //const引用能给右值取别名
//4.右值引用不能给左值取别名,除非move()一下
int&& r1 = b; //false
int&& r1 = move(b); //true
return 0;
}
还有两个点:
至于什么是move(),下面会讲
在语法上,对于内置类型来说,把内置类型的右值称为纯右值
把自定义类型的右值称为将亡值。
看下面的例子:
dzt::string fun()
{
dzt::string str("xxxxxxxxxxxxxxxxxxxxxx");
return str;
}
int main()
{
dzt::string ret = fun();//连续的两次拷贝构造会被编译器优化 成一次深拷贝
//...
return 0;
}
解析:
首先调用fun函数,对str进行一次构造,在返回str时,由于返回值类型是传值返回,所以str先构造一个临时对象,然后fun函数结束后,str销毁了,临时对象再进行拷贝构造给ret,总的来说就是两次深拷贝。
(不过现在编译器会对连续的构造/拷贝构造进行优化,优化成一次构造/拷贝构造)
因为这样的情况下,如果返回string&,会造成更大的问题,在fun函数调用结束时,就已经将str的资源释放了,然而ret还是str的别名,一旦访问就会出现非法访问内存问题。
有了移动构造和移动赋值的出现,就能完美解决上面两次深拷贝造成效率下降的问题。
在编译器看来,因为str虽然是一个左值,但是str构造的临时对象后再返回给ret,是一个传值返回,且str构造临时对象后自己就会销毁,不应该多此一举,而是直接将str自己的资源直接给ret。因为str会被编译器识别成将亡值。
具体的实现:
以string为例
string(string&& s)
:_str(nullptr)
{
cout << "string(const string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(string&& s)
{
cout << "string operator=(const string&& s)-- 移动赋值" << endl;
swap(s); //现代写法,顺便在你要释放的时候把我不要的资源带走
return *this;
//返回的是s的引用,此时s已经拿到了想要的资源,且不要的资源在出了该作用域后会被将亡值释放
}
这里str会被识别成将亡值的两个前提:
所以,本质上,正是因为有编译器的优化,才会减少拷贝次数,大大提高效率。
使用场景:
1.做参数和做返回值都可以提高效率。
左值引用的价值:
右值引用的价值:
- 为了进一步减少拷贝,弥补左值引用没有解决的场景。
比如:
场景1:
自定义类型中深拷贝的类,必须传值返回的场景。(上面将的移动构造和移动赋值就是这个场景1)
场景2:
容器的插入接口,如果插入的对象是右值,可以利用移动构造将资源转移给数据结构中的对象,也可以减少拷贝。
比如:
int main()
{
list<dzt::string> lt;
dzt::string s1("111111111111111111111111111");
lt.push_back(s1);
cout << endl;
dzt::string s2("222222222222222222222222222");
lt.push_back(move(s2));
cout << endl;
lt.push_back("33333333333333333333333333333");
cout << endl;
return 0;
}
如果没有实现移动构造和移动赋值,运行后结果如下:
分析:
所以每次插入时都会产生一次深拷贝。
如果有了移动构造和移动赋值,情况截然不同:
编译器会将构造出的临时对象识别成将亡值,直接转移资源,从而每次都减少了一次深拷贝次数。
第一种情况没有走移动构造是因为,先是进行了隐式类型转换,调用构造构造了string,然后这个string再插入,再在push_back函数中调用拷贝构造,这并不是连续的行为,所以编译器不会优化,也就不会将s1识别成将亡值。
这里要区分左值和右值的原因是:
如果一个自定义类型的值不是右值,但编译器把它当成右值看待的话,那在拷贝构造/赋
值时被拷贝对象的资源就会被抢走。
move函数的返回值是一个右值属性。
比如
string ret = move(str);
move(str)的返回值是一个右值,但是str本身不会被改变成右值。
C++11前半部分就到这里,后半部分会持续更新。