c++98是C++标准委员会成立第一年的C++标准,C++的第一次更新是C++03,但由于C++03基本上是对C++98缺陷的修正,所以一般把C++98与C++03合并起来,叫做C++98/03;
后来原本C++委员会更新的速度预计是5年更新一次,但由于C++标准委员会的进度,吃吃未更新,直到2011年C++11的出现使得C++的功能出现了大的提升,所以我们需要进行学习,提高我们的编码效率;
{}初始化
在c++11中,我们的初始化有更多的方式,{}这个符号也可以进行数据的初始化,这是在C++98中没有的;
// 测试C++11{}符号初始化
int main()
{
int a = 10;
int b = { 10 };
int c{ 10 };
vector<int> v{ 1,2,3,4,5 };
int* pa = { &a };
int arr[]{ 1,2,3,4 };
return 0;
}
这些都是可以使用{}花括号来进行初始化的;
我们自己实现的类使用{}也会自动调用构造函数来进行初始化:
class A {
public:
A(string str)//单参数构造参数可以自动进行类型转化
:_a(str)
{
cout << "A() 构造函数" << endl;
}
private:
string _a;
};
int main()
{
A a{ "123" };
return 0;
}
看自动调用了构造函数:
我们使用的时候用起来非常方便但是出现这样的使用情况的时候是不是会有点奇怪,这是怎么构造的:
list<int> l{1,2,3,4,5,6,7};
奇怪了,为什么这样也可以进行构造,如果是调用普通的构造函数,我们传递这么多参数,难道构造函数有这么多参数吗,而且参数的个数还是不确定的;
所以为了解决这个疑惑引入一个新的概念:
initializer_list 模板类
这是一个C++的模板类,这个类可以用来接收{}中的数据,构造出一个装载了{}中类型数据的list;所以说我们在使用{}进行初始化容器时,如果容器无法直接调用构造函数初始化,就会尝试先将数据转换为initializer_list然后再将initializer_list中的数据交给容器的构造函数来进行构造;
我们看容器的构造函数就可以看到容器是适配了一个参数是initializer_list构造函数的
基本上在C++11中所有的容器都是实现了这样的构造来适配{}进行初始化的;
map<int,string> m{ { 123 , "abc" } , { 456 , "def" } };
它们都是适配了initializer_list的;
我们自己实现的list也需要再多实现一个构造这样我们也能支持{}的初始化了:
list(initializer_list<int> lt)
{
list_emptyinit();
for (auto& e: lt)
{
push_back(e);
}
}
list<int> l{ 1,2,3,4,5,6 };
decltype
这个关键字可以用来获取我们的表达式结果类型并返回此类型:
自动推导出了类型vector中的数据类型为double;
C++11增加的容器:
unordered_map ,unordered_set ,array, forward_list; (记之以忘)
右值引用
左右值简介
在c++中有右值与左值之分,一开始就是是用在赋值符号的作用来作为区分的;但是现在对于左右值不能这么区分;现在应该如此区分:可以取地址的就是左值,不可取地址的为右值;
右值一般为将亡值,就是即将被释放的数据,(但有时候我们用户强制在转换的右值可能不一定是将亡值)
左右值可以相互转换(但有条件)
// 右值引用
int main()
{
string a = "hello";
string b = " world!";
//左值引用
string& refa = a;
const string& refab = a + b;//可以使用const来接收右值(因为临时对象具有常性)
//右值引用
string&& rrefb = move(b);//可以使用move函数将左值转换为右值来接收接收
string&& rrefab = a + b;
list<int> l{ 1,2,3,4,5,6 };
list<int> l1 = move(l);//拷贝构造时右值的数据会被转移走
return 0;
}
这里的l不但不是将亡值而且还是左值,但是因为move将它转换为了右值,使得他的数据被转移走了;
所以右值的转换要慎用;
左值:就是我们常见的普通数据(不过多讲解)
右值:
1. 将亡值
2.(我们不需要再使用的数据)可以将它转换为右值提供给某个接口或者资源,进行资源转移;
移动构造函数:
在容器中,为了减少拷贝带来的消耗,如果传递给容器的值是右值则会调用移动构造来进行拷贝,依此来提高效率
// 我发现编译器对于拷贝构造进行了很多的优化,有时候并不会调用拷贝构造
struct A
{
string _str;
A(const string& str)
:_str(str)
{
cout << "构造函数A(const string& str)" << endl;
}
A()
:_str("")
{
cout << "构造函数A()" << endl;
}
A(const A& a)
:_str(a._str)
{
cout << "深拷贝A(const A& a)" << endl;
}
A(A&& a)
{
swap(_str, a._str);
cout << "移动构造A(A&& a)" << endl;
(void)a;
}
~A()
{}
};
int main()
{
string s("hello");
A a(s);//普通构造函数
A c(a);//深拷贝
//A b(A());//这样会被编译器翻译为一个函数的声明,声明了一个叫b的函数,返回值类型为A,参数为返回值为A的无参函数
//A b(A{});//本来要调用移动构造的但是,由于编译器优化,直接进行了一次构造
A b(move(a));//移动构造
return 0;
}
这里由于a是右值,所以a的资源会被转移给,b使得b不用进行深拷贝(再开辟一片空间出来复制一份a的数据);
这样可以大大的提高拷贝的效率;这就是右值的运用;
运用场景
编译器未优化前:
左值拷贝时的场景:
看这就是深拷贝的场景,因为这样的情况,为了解决这样低效的拷贝,右值引用应运而生;
右值拷贝的场景:
如果我们使用右值引用来接收这个返回值v
编译器优化后:
左值引用:
右值引用:
移动语义
为什么说这里的&和&&标志可以省略呢,因为,前面我们说了拷贝构造和移动构造的出现使得我们的拷贝有了两种方式,1.是进行深拷贝,2.是进行移动构造;而这两种构造的方式是通过不同的参数来调用的,编译器会匹配最适合的函数让参数进入;而我们将&和&&标志省略后,编译器判断出这个返回值是局部变量他是右值,匹配最合适的移动构造来进行拷贝,这就是移动语义;当然如果没有移动构造,那么就只能匹配普通的构造函数来来构造了,移动语义也没办法去调用不存在的移动构造函数;
下面是我将移动构造加入我自己实现的strin类中,从而实现移动构造的场景的代码:
模拟实现stl容器/模拟实现string类 · future/my_road - 码云 - 开源中国 (gitee.com)
完美转发
有的时候,我们的右值因为多次的传递它不再是右值了,这个时候我们的移动语义就无法被触发,从而无法调用移动构造;
// 完美转发
void func(const string& a)
{
cout << "左值" << endl;
}
void func(string && a)
{
cout<<"右值" << endl;
}
template<class T>
void test1(T&&a)
{
//func(a);//a引用接收右值之后,他自己便成为了左值
func(forward<T>(a));//完美转发,保持a右值的状态
}
void test()
{
string a;
test1(a);
test1(move(a));
}
int main()
{
test();
return 0;
}
在模板中肯定会有这种多层传递的出现,一个右值传递给了右值引用,此时的右值引用是左值,因为它将右值存储器起来了,这个右值引用可以取地址了;
所以为了避免多次传递时右值被转换为了左值,我们需要使用forward<T>()这个函数来保持原有类型状态,保持它的右值的状态;我们需要在每一层都使用forward<T>来保证右值的传递,直到最后一层的右值传递给移动构造即成功完成传递;
模板中的&&
在模板中&&可以即代表左值又代表右值叫做(万能引用),编译器会根据传递给接口的参数类型来进行推到的;
push_back(T&&data)
data为左值时->T&
data为右值时->T&&
对于匿名对象的说明
// 测试临时对象返回
struct A
{
int _a ;
int _b ;
A(const A& a)
{
_a = a._a;
_b = a._b;
//noting
}
//A &()(int a,int b)
A(int a,int b)
:_a(a)
,_b(b)
{
}
};
A getA()
{
A a(10, 20);
return a;
}
int main()
{
const A& b = getA();
string("11111111111");
}
我们可以使用引用来接收匿名对象/临时对象时,匿名/临时对象会延长生命周期至b的生命周期,直到b死亡这份空间中的资源才会被释放;
如果我们修改返回值为引用:
A &()(int a,int b)
{
return A;
}
那么这个时候返回的值指向的时被释放的内存的局部变量A;如果我们在继续增加栈帧就会覆盖掉原来的数据从而会影响b引用的数据:
类和对象的补充
在C++98中有6个默认函数,到了c++11后增加移动构造和移动赋值两个默认成员函数;
默认生成的移动构造和移动赋值:
当我们没有实现析构,拷贝构造,赋值构造三个中的任意一个默认函数时,编译器会自动生成一个移动构造函数,这个移动构造函数会自动的调用成员的移动构造,如果成员没有移动构造则调用拷贝构造函数(移动赋值构造是同样的原理);
我们可以看下面的代码:
// 证明移动构造的默认生成
class B {
public:
B()
{}
B(const char* str)
:_str(str)
{
}
B(const B& b)
:_str(b._str)
{
cout << "(const B& b)深拷贝" << endl;
}
B(B&& b)
{
_str.swap(b._str);
cout << "B(B&& b)移动构造" << endl;
}
private:
string _str;
};
class A {
public:
A(const B& data)
:_data(data)
{
}
//~A()//只要写了一个除构造外的默认函数就会导致默认移动构造的删除
//{}
private:
B _data;
};
int main()
{
B b("hello");
A a(b);
A a1(a);//深拷贝
A a2(move(a));//移动构造我们并没有实现,但是还是调用了B的移动构造,说明默认生成了
A a3 = move(a2);//移动赋值也是默认生成了的(b没有实现赋值调用了)
return 0;
}
现象:
当写了析构函数的时候:
内置类型的初始化(缺省参数)
在C++11中补齐了前面C++98中,只有内置类型会自动调用自己的构造函数缺陷,在下面函数定义的位置可以设置缺省参数来初始化成员变量;
default关键字
这个关键字可以生成默认的拷贝构造,赋值构造,移动构造,移动赋值函数(暂时我只知道这四个函数默认生成的情况,其他如果还有我们再慢慢补充)
生成场景,就拿上面的默认移动构造的生成来举例,假设我们自己实现了一个析构函数(一般自己实现说明是深拷贝,但我们这里只是为了产生现象所以写一个,但是内部不写任何代码)
class B {
public:
B()
{}
B(const char* str)
:_str(str)
{
}
B(const B& b)
:_str(b._str)
{
cout << "(const B& b)深拷贝" << endl;
}
B(B&& b)
{
_str.swap(b._str);
cout << "B(B&& b)移动构造" << endl;
}
private:
string _str = "hello";
};
class A {
public:
A(const B& data)
:_data(data)
{
}
~A()//只要写了一个除构造外的默认函数就会导致默认移动构造的删除
{}
A(const A& a) = default;//让编译器默认生成
A(A&& a) = default;//让编译器默认生成
private:
B _data;
};
int main()
{
B b("hello");
A a(b);
A a1(a);//深拷贝
A a2(move(a));//移动构造我们并没有实现,但是还是调用了B的移动构造,说明默认生成了
A a3 = move(a2);//移动赋值也是默认生成了的(b没有实现赋值调用了)
return 0;
}
看编译器删除了默认构造后又生成了,这就是default的作用;
delete关键字
这是用来禁止生成默认函数的,C++98中是将那个不许生成的默认函数作为private成员这样一旦调用此函数就会报错,但是在C++11中,直接使用=delete,即可完成禁止生成的操作;
class B {
public:
B()
{}
B(const char* str)
:_str(str)
{
}
B(const B& b)
:_str(b._str)
{
cout << "(const B& b)深拷贝" << endl;
}
B(B&& b)
{
_str.swap(b._str);
cout << "B(B&& b)移动构造" << endl;
}
private:
string _str = "hello";
};
class A {
public:
A(const B& data)
:_data(data)
{
}
//~A()//只要写了一个除构造外的默认函数就会导致默认移动构造的删除
//{}
//A(const A& a) = default;//让编译器默认生成
//A(A&& a) = default;//让编译器默认生成
A(const A& a) = delete;//阻止编译器默认生成
A(A&& a) = delete;//阻止编译器默认生成
private:
B _data;
};
int main()
{
B b("hello");
A a(b);
A a1(a);//深拷贝
A a2(move(a));//移动构造我们并没有实现,但是还是调用了B的移动构造,说明默认生成了
A a3 = move(a2);//移动赋值也是默认生成了的(b没有实现赋值调用了)
return 0;
}
委托构造
在C++11中,我们还可以在一个构造函数中去调用另一个构造函数;这之前只有在继承的时候,子类需要显示的调用父类的带参构造去初始化父类的变量;而C++11可以通过在初始化列表中显示的委托构造调用自己的构造函数;
class A {
public:
A()
{
cout << "A()" << endl;
}
A(int a)
:A()//委托构造
{
cout << "A(int a)"<<endl;
_a = a;
}
private:
int _a;
};
int main()
{
A a(10);
return 0;
}
现象: