列表初始化
文章目录
在
C++98
中,允许使用花括号对数组元素进行统一的列表初始值设定。比如
int array1[] = {1,2,3,4,5,6};
int array[2] = {0};
在C++98
中可以用花括号对数组等类型进行 初始化,可是对于自定义的数据类型却不能进行初始化,比如:
// 这样的语法在c++98中是无法初始化的
vector<int> v{1,2,3,4};
这样的语法导致我们在定义vector时,都需要把vector定义出来,然后使用循环对其赋值,非常的不方便。C++11扩大了用大括号括起来的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表的时候,可以添加等号也可以不加等号
内置类型的列表初始化
int main()
{
// 内置类型变量
int a = {10};
int b{10};
int c = {1+2};
int d{1+2};
// 数组
int arr1[5]{1,2,3,4,5}; // 省略了等号
int arr2[]{1,2,3};
// 同样的动态数组在c++11中也是支持列表初始化的
int* arr3 = new int[5]{1,2,3,4,5}; // 直接申请空间后就初始化
// 标准容器
vector<int> varr{1,2,3,4}; // 用花括号初始化
map<int, int> m{{1,2},{2,2},{3,3}};
return 0;
}
PS:列表初始化可以在{}之前使用等号,其效果与不使用等号没有什么区别
自定义类型的列表初始化
-
标准库支持单个对象的列表初始化
class point { public: point(int x = 0, int y = 0):_x(x),_y(y) {} private: int _x; int _y; }; int main() { point p{1,2}; // 传入的就是构造函数需要的两个参数 return 0; }
-
多个对象的列表初始化
多个对象想要支持列表初始化,需要给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
该类型用于访问c++初始化列表中的值,该初始化列表是一个const T类型的元素列表。
这种类型的对象由编译器根据初始化列表声明自动构造,初始化列表是用大括号括起来的逗号分隔的元素列表:
Auto il = {10,20,30};// il的类型是initializer_list
请注意,这个模板类没有隐式定义,即使隐式使用类型,也要包含头文件<initializer_list>来访问它。
Initializer_list对象被自动构造为
#include<initializer_list>
template<class T>
class Vector
{
public:
Vector(initializer_list<T> l):_capacity(l.size(), _size(0))
{
_array = new T[_capacity];
for(auto e:l)
{
_array[_size++] = e;
}
}
// 模拟实现的带有initializer_list类型参数的构造函数
Vector<T>& operator=(initializer_list<T> l)
{
delete[] _array;
size_t i = 0;
for(auto e : l)
{
_array[i++] = e;
}
return *this;
}
private:
T* _array;
size_t _capacity;
size_t _size;
}
变量类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来比较复杂,例如:
#include<map>
#include<string>
int main()
{
short a = 12340;
short b = 43210;
// 如果c给成short,会造成数据丢失,如果能让编译器根据a+b的结果推导c的实际类型,就不会存在问题
short c = a+b;
std::map<std::string,std::string> m{{"apple","苹果"},{"banana","香蕉"}};
//如果要使用迭代器遍历,迭代器类型会很繁琐
std::map<std::string, std::string>::iterator it = m.begin();
}
所以在c++11中,可以使用auto类根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。
decltype类型推导
在C++98中也支持获取变量数据类型操作,typeid关键字,但是typeid只能支持查看数据类型,并不能用其结果定义类型
#include<iostream>
#include<typeinfo>
using namespace std;
int main()
{
int c = 0;
printf("%s",typeid(c).name());
return 0;
}
decltype是根据表达式的实际类型推演出定义变量时所用的类型
根据表达式推导数据类型
#include<iostream>
#include<typeinfo>
using namespace std;
int main()
{
// 推演表达式类型作为变量的定义类型
char a = 100;
char b = 100;
decltype(a + b) d; // 用a+b的结果推导数据类型定义c
cout << typeid(d).name() << endl;
return 0;
}
推演函数返回值的类型
#include<iostream>
#include<typeinfo>
using namespace std;
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导函数返回值类型,这里只是推演不会执行
cout << typeid(decltype(GetMemory(1))).name() << endl;
return 0;
}
默认成员函数控制
在C++中对于空类编译器会生成一些默认的成员函数,比如;构造函数、拷贝构造函数、运算符重载、析构函数、移动构造、移动拷贝构造等函数。如果在类中显示定义了,编译器就不会重新生成默认版本。于是C++11让程序员可以控制是否需要编译器生成。
显示缺省函数
在C++11中,可以在默认函数定义或声明时加上
=default
,从而显示的只是编译器生成该函数的默认版,用=default
修饰的函数称为显式缺省函数
class A
{
public:
A(int a):_a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1;
A a2;
a2 = a1;
return 0;
}
删除默认函数
如果想要限制某些默认函数的生成,在C++98中,是将该函数设置成private,并且 不给定义,这样 只要其他人想要调用就会报错 。在C++11中更简单,只需要在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
class A
{
public:
A(int a):_a(a){};
// 禁止编译器生成默认的拷贝构造函数以及运算符重载
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a;
}
int main()
{
A a1(10);
A a2(a1); // 这里会编译失败,因为没有生成默认的拷贝构造函数
return 0;
}
右值引用
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体共同使用一块 内存空间,而引用的底层时通过指针来实现的,因此使用引用,可以提高程序的可读性。为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
// 左值引用
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int add(int a, int b)
{
return a+b;
}
int main()
{
int a= 1;
int b = 2;
swap(a,b);
// 引用函数返回值,返回值是一个临时变量,为右值
int&& ret = add(a,b);
return 0;
}
左值与右值
- 普通类型的变量,因为有名字,可以取地址,都认为是左值
- const修饰的常量,不可以修改,只读类型,理应按照右值对待,但因为其可以取地址,c++11认为是左值
- 如果表达式的运算结果是一个临时变量,认为是右值
- 如果表达式运算结果或单个变量是一个引用则认为是左值
引用与右值引用的比较
在C++98中的普通引用与const引用在引用实体上的区别:
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
// int& ra2 = 10; // 编译失败,因为10是右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
注意:普通引用只能引用左值,不能引用右值,const引用既可以引用左值,也可以引用右值
C++中的右值引用:只能引用右值,一般不能直接引用左值
值的形式返回对象的缺陷
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
namespace tpm
{
class String
{
public:
// 构造函数
String(const char* str = "")
{
if (nullptr == str) str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造
String(const String& s)
:_str(new char[strlen(s._str)])
{
strcpy(_str, s._str);
}
// 赋值重载
String& operator=(const String s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
// 重载加法运算符
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
tpm::String strRet(pTemp);
return strRet;
}
~String()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
}
int main()
{
tpm::String s1("hello");
tpm::String s2("world");
tpm::String s3(s1 + s2);
}
上面的程序看上去没有什么问题,但有些地方是令人不满意的。比如我们重载的加法运算符。
在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察就会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大。
从运行结果我们可以看出,有四次析构,这是因为临时对象在离开作用域的时候就会调用析构函数来释放掉自己的空间
移动构造语义
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题
在C++11中如果要实现移动语义,必须使用右值引用
String(String&& s)
:_str(s._str) // 直接让新的对象指向临时对象,这样就少了拷贝和析构的步骤
{
s._str = nullptr;
}
- 因为strRet对象的生命周期在创建好临时对象之后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程只需创建一块堆内存即可
- 移动构造函数的参数不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,需显示定义自己的移动构造
右值引用引用左值
- 按照语法,右值引用只能引用右值
- 当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值
- 在C++11中,std::move()函数位于头文件中,他并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
注意:
- 被转化的左值,其生命周期并没有随着左值的转变而改变
- STL中也有一个move函数,就是将一个范围中的元素搬移到另一个位置
int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}
class person
{
public:
person() = default;
person(const person& p)
{
cout << "拷贝构造" << endl;
}
person(person&& p)
{
cout << "移动构造" << endl;
}
};
int main()
{
person p;
//person p1(p);
person p2(move(p1));
}
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
void Fun(int& x){cout << "左值引用" << endl;}
void Fun(const int& x){cout << "const左值引用" << endl;}
void Fun(int&& x){cout << "右值引用" << endl;}
void Fun(const int&& x){cout << "const右值引用" << endl;}
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
我们看结果惊奇的发现,如果不使用完美转发,即是我们传一个常量进去它也是调用的左值引用,这是为什么呢?因为右值引用本身是个左值,所以它传递过去的时候被识别为左值了
- 完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销
- 所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,他就应该被转发为右值