C++11:其他特性
1 委托构造函数和继承构造函数
1.1 委托构造函数
委托构造函数允许在同一个类中一个构造函数可以调用另外一个构造函数,从而可以在初始化时简化变量的初始化:
class class_c {
public:int max;int min;int middle;class_c(){)class_c(int my_max) {max = my_max > 0 ? my_max : 10;}class_c(int my_max, int my_min){max = my_max >0 ? my_max : 10 ;min = my_min >0 & & my_min < max ? my_min : l;}class_c(int my_max,int my_min,int my_middle) {max = my_max > 0 ? my_max : 10;min = my_min > 0 & &my_min < max ? my_min : 1;middle = my_middle < max && my_middle > min ? my_middle : 5;}
};
上面的例子没有实际的含义,只是为了展示在成员变量较多、初始化比较复杂和存在多个构造函数的情况下,每个构造函数都要对成员变量赋值,这是重复且烦琐的。通过委托构造函数就可以简化这一过程,代码如下:
class class_c {
public:int max;int min;int middle;class_c(int my_max){max = my_max > 0 ? my_max : 10;}class_c(int my_max, int my_min) : class_c(my_max){min = my_min > 0 && my_min < max ? my_min : l;}class_c(int my_max, int my_min,int my_middle) : class_c(my_max,my_min){middle = my_middle < max & & my_middle > min ? my_middle : 5;}
};int main ()
{class_c c1{ 1,3,2 };
}
在上面的示例中,在构造class_c(int, int, int)时会先调用class_c(int, int),而class_c(int, int)又会调用class_c(int),即 class_c(int, int, int)递归委托了两个构造函数完成成员变量的初始化,代码简洁而优雅。需要注意的是,这种链式调用构造函数不能形成一个环,否则会在运行期抛异常。
使用委托构造函数时需要注意:使用了代理构造函数就不能用类成员初始化了。例如:
class class_a {
public:class_a() { }//member initialization here, no delegateclass_a(string str) : m_string{ str } {}//调用了委托构造函数,不能用类成员初始化了class_a(string str, double dbl) : class_a(str), m_double{ dbl } {} //error//只能通过成员赋值来初始化class_a (string st, double dbl) : class_a(str) { m_double = dbl; )double m_double{ 1.0 };string m_string;
};
1.2 继承构造函数
C++11的继承构造函数可以让派生类直接使用基类的构造函数,而无须自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。比如,下面的一个结构体有3个构造函数:
struct Base
{int x;double y;string s;Base(int i) : x(i), y(0) { }Base(int i, double j) : x(i), y(j) {}Base(int i, double j, const string& str) : x(i), y(j), s(str) {}
};
如果有一个派生类,希望这个派生类也和基类采取一样的构造方式,那么直接派生于基类是不能获取基类构造函数的,因为,C++派生类会隐藏基类同名函数。例如:
struct Derived : Base
{
};int main()
{Derived d(1, 2.5, "ok"); //编译错误,没有合适的构造函数
}
在上面的例子中,通过基类的构造函数去构造派生类对象是不合法的,因为派生类的默认构造函数隐藏了基类。如果希望使用基类的构造函数,一个做法是在派生类中定义这些构造函数:
struct Derived : Base
{Derived(int i) : Base(i){}Derived(int i, double j) : Base(i,j){}Derived(int i, double j, const string& str) : Base(i, j, str){}
};int main ()
{int i = 1;double j = 1.23;Derived d(i) ;Derived d1(i, j);Derived d2(i, j, "");return 0;
}
在派生类中重新定义和基类一致的构造函数的方法虽然可行,但是很烦琐且重复,C++11的继承构造函数特性正是用于解决派生类隐藏基类同名函数的问题的,可以通过using Base::SomeFunction来表示使用基类的同名函数,通过using Base::Base来声明使用基类构造函数,这样就可以不用定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。需要注意的是,继承构造函数不会去初始化派生类新定义的数据成员。例如:
struct Derived : Base
{using Base::Base; //声明使用基类构造函数
};int main()
{int i = 1;double j = 1.23;Derived d(i); //直接使用基类构造函数来构造派生类对象Derived d1(i, j);Derived d2(i, j, "");return 0;
}
可以看到通过using Base::Base使用基类构造函数能少写很多构造函数。这个特性不仅对构造函数适用,对其他同名函数也适用,例如:
struct Base
{void Fun(){cout << "call in Base" << endl;}
};struct Derived : Base
{void Fun(int a){cout <<"call in Derived" << endl;}
);int main ()
{Derived d;d.Fun(); //编译报错,找不到合适的函数return 0;
}
在上面的例子中,派生类希望使用基类函数,但是派生类中的同名函数隐藏了基类的函数,编译器会提示找不到合适的函数,如果希望使用基类同名函数,需要用using重新引入名字。
2 原始的字面量
原始字面量可以直接表示字符串的实际含义,因为有些字符串带有一些特殊字符,比如在转义字符串时,我们往往要专门处理。例如,打印一个文件路径:
#include <iostream>
#include <string>
using namespace std;int main()
{string str = "D:\\AlB\\test.text";cout<<str<<endl;string str1 = "D:\\\\A\\\\B\\\\test.text";cout<<str1<<endl;string str2 = R"(D:\\A\\B\\test.text) ";cout<<str2<<endl;return 0 ;
}
输出结果如下:
D:AB est.text
D:\\A\\B\\test.text
D:\\A\\B\\test.text
可以看到通过原始字符串字面量R可以直接得到其原始意义的字符串,再看一个输出html标签的例子:
string s =
"<HTML>\\
<HEAD>\\
<TITLE>this is my title</TITLE>\\
</HEAD>\\
<BODY>\\
<P>This is a paragraph</P>\\
</BODY>\\
</HTML>";
在C++11之前,如果希望能获取多行并带缩进的格式的HTML代码,不得不以这种方式来写,这种方式不但比较烦琐,还破坏了要表达的原始含义。如果通过C++11的原始字符串字面量来表示,则很简单直观,例如:
#include <iostream>
#include <string>
using namespace std;int main ()
{string s=R"(<HTML><HEAD><TITLE>This is a test</TITLE></HEAD><BODY><P>Hello,C++ HTML world!</P></BODY></HTML>) ";cout <<s <<endl;return 0;
}
3 final和override关键字
C++11中增加了final关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和Java中的final 及C#中的sealed 关键字的功能类似。如果修饰函数,final只能修饰虚函数,并且要放到类或者函数的后面。下面是final的用法:
struct A
{//A::foo is final,限定该虚函数不能被重写virtual void foo() final;//Error: non-virtual function cannot be final,只能修饰虚函数void bar() final;
};struct B final : A //struct B is final
{//Error: foo cannot be overridden as it's final in Avoid foo();
};struct C : B //Error: B is final
{
};
override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,还可以防止因疏忽把本来想重写基类的虚函数声明成重载。这样,既可以保证重写虚函数的正确性,又可以提高代码的可读性。override关键字和 final关键字一样,需要放到方法后面。
struct A
{virtual void func(){)
} ;struct D: A
{//显式重写void func() override{}
};
4 内存对齐
4.1 内存对齐介绍
内存对齐,或者说字节对齐,是一个数据类型所能存放的内存地址的属性。这个属性是一个无符号整数,并且这个整数必须是2的N次方(1、2、4、8、…、1024、…)。当我们说一个数据类型的内存对齐为8时,就是指这个数据类型所定义出来的所有变量的内存地址都是8的倍数。
当一个基本数据类型(Fundamental Types)的对齐属性和这个数据类型的大小相等时,这种对齐方式称为自然对齐(Naturally Aligned)。比如,一个4字节大小的 int型数据,在默认情况下它的字节对齐也是4。
为什么需要内存对齐?因为并不是每一个硬件平台都能够随便访问任意位置的内存的。不少平台的CPU,比如Alpha、IA-64、MIPS还有SuperH架构,若读取的数据是未对齐的(比如一个4字节的int在一个奇数内存地址上),将拒绝访问或抛出硬件异常。考虑到CPU处理内存的方式(32位的x86 CPU,一个时钟周期可以读取4个连续的内存单元,即4字节),使用字节对齐将会提高系统的性能(也就是CPU读取内存数据的效率)。比如将一个int放在奇数内存位置上,想把这4个字节读出来,32位CPU就需要两次。但按4字节对齐之后一次就可以读出来了)。
4.2 堆内存的内存对齐
在讨论内存对齐的时候很容易忽略堆内存,经常会使用malloc分配内存,却不理会这块内存的对齐方式。实际上,malloc一般使用当前平台默认的最大内存对齐数对齐内存,比如,MSVC在32位下一般是8字节对齐;64位下则是16字节。这样对常规的数据来说都是没有问题的。但是如果我们自定义的内存对齐超出了这个范围,则不能直接使用malloc来获取内存的。
当我们需要分配一块具有特定内存对齐的内存块时,在 MSVC下应当使用_aligned_malloc,而在gcc下一般使用memalign 等函数。
4.3 利用alignas指定内存对齐大小
有时我们希望不按照默认的内存对齐方式来对齐,这时,可以用alignas来指定内存对齐的大小。下面是alignas的基本用法:
alignas (32) long long a = 0;#define xx 1
struct alignas(Xx) MyStruct_1 {}; //OKtemplate <size_t YY =1>
struct alignas(YY) MyStruct_2 {}; //OKstatic const unsigned ZZ = 1;
struct alignas(ZZ) Mystruct_3 {}; //OK
注意,对MyStruct_3的编译是没问题的。在C++11中,只要是一个编译期数值(包括static const)都支持alignas。
另外,需要注意的是alignas 只能改大不能改小。如果需要改小,比如设置对齐数为1,仍然需要使用#pragma pack。或者,可以使用C++11中与#pragma等价的物_Pragma(微软暂不支持):
_Pragma("pack(1)")
struct MyStruct
{char a; //1 byteint b; //4 bytesshort c; //2 byteslong long d;//8 byteschar e; //1 byte
} ;
_Pragma("pack ()")
alignas 还可以这样用:
alignas(int) char c;
这个char就按int的方式对齐了。
4.4 利用alignof和std::alignment_of获取内存对齐大小
alignof用来获取内存对齐大小,它的用法比较简单。下面是alignof的基本用法:
Mystruct xx;
std::cout << alignof(xx) << std::endl;
std::cout << alignof(MyStruct) << std::endl;
std::alignment_of的功能是编译期计算类型的内存对齐。std中提供std::alignment_of是
为了补充alignof 的功能。alignof 只能返回一个size_t,而 alignment_of则继承自std::integral.constant,因此,拥有value_type、type和 value等成员。
我们可以通过std::alignment_of和 alignof 来获取结构体内存对齐的大小,例如:
struct MyStruct
{char a;int b;double c;
} ;int main ()
{int alignsize = std::alignment_of<Mystruct>::value; //8int sz = alignof (Mystruct) ; //8return 0;
}
std::alignment_of可以由align来实现:
template< class T>
struct alignment_of : std::integral_constant<std::size_t, alignof(T)> {};
与sizeof有点类似,alignof可以应用于变长类型,例如,alignof(Args)用来获取变参的内存对齐大小。
4.5 内存对齐的类型std::aligned_storage
std::aligned_storage可以看成一个内存对齐的缓冲区,一般和placement new结合起来使用,它的基本用法如下:
#include <iostream>
#include <type_traits>struct A
{int avg;A(int a, int b): avg ((a+b)/2) { }
};typedef std::aligned_storage<sizeof(A), alignof(A)>::type Aligned_A;int main()
{Aligned_A a, b;new (&a) A (10,20);b = a;std::cout << reinterpret_cast<A&>(b).avg << std::endl;return 0;
}
为什么要使用std:aligned_storage?我们知道,很多时候需要分配一块单纯的内存块,比如new char[32],之后再使用placement new在这块内存上构建对象:
1 char xx[32];
2 ::new (xx)MyStruct;
但是char[32]是1字节对齐的,xx很有可能并不在 MyStruct指定的对齐位置上。这时调用placement new构造内存块可能会引起效率问题或出错,所以此时应该使用std::aligned_storage来构造内存块:
1 std::aligned_storage<sizeof (Mystruct), alignof (MyStruct) >::type xx;
2 ::new ( &xx) MyStruct;
需要注意的是,当使用堆内存时可能还需要aligned_malloc,因为在现在的编译器中,new并不能在超出默认最大对齐后还保证内存的对齐是正确的。比如在 MSVC 2013中,下面的代码将会得到一个不一定能按照32位对齐的编译警告。
struct alignas (32) MyStruct
{char a; //1 byteint b; //4 bytesshort c; //2 byteslong long d;//8 byteschar e ; //1 byte
};void* p = new MyStruct;
//warning V4316:‘Mystruct' : object allocated on the heap may not be aligned 32
4.6 std::max_align_t和std::align 操作符
std::max_align_t用来返回当前平台的最大默认内存对齐类型。对于malloc返回的内存,其对齐和max_align_t类型的对齐大小应当是一致的。我们可以通过下面这个方式获得当前平台的最大默认内存对齐数:
std::cout << alignof(std::max_align_t) << std::endl;
std::align用来在一大块内存当中获取一个符合指定内存要求的地址。看下面这个例子:
char buffer[] = "---------";
void * pt = buffer;
std::size_t space = sizeof(buffer)-1;
std::align(alignof(int), sizeof(char), pt, space);
这个示例的意思是,在buffer这个大内存块中,指定内存对齐为alignof(int),找一块sizeof(char)大小的内存,并且在找到这块内存后将地址放入pt,将buffer从pt开始的长度放入space。
C++11为我们提供了不少有用的工具,可以让我们方便地操作内存对齐,但是在堆内存方面,我们很可能还是需要自己想办法。不过在平时的应用中,因为很少会手动指定内存对齐数到大于系统默认的对齐数,所以也不必每次new/delete 的时候都提心吊胆。
5 C++11新增的便利算法
C++11新增加了一些便利的算法,这些算法使代码编写起来更简洁、方便,这里仅列举一些常用的新增算法,。
5.1 all_of、any_of和none_of
算法库新增了3个用于判断的算法 all_of、any_of和 none_of:
all_of检查区间[first, last)中是否所有的元素都满足一元判断式p,所有的元素都满足条件返回true,否则返回false。
any_of检查区间[first,last)中是否至少有一个元素都满足一元判断式p,只要有一个元素满足条件就返回 true,否则返回true。
none_of检查区间[first,last)中是否所有的元素都不满足一元判断式p,所有的元素都不满足条件返回true,否则返回false。
下面是all_of、 any_of和 none_of的示例。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;int main()
{vector<int> v= {1,3,5,7,9 };auto isEven = [](int i) { return i % 2 != 0 ;}bool isallOdd = std::all_of(v.begin(), v.end(), isEven);if(isallodd)cout <<"all is odd" << endl;bool isNoneEven = std::none_of(v.begin(), v.end(), isEven);if(isNoneEven)cout << "none is even" << endl;vector<int> v1={ 1,3,5,7,8,9 };bool anyof = std::any_of(v1.begin(), vl.end(), isEven);if(anyof)cout << "at least one is even" <<endl;
}
输出结果如下:
all is odd
none is odd
at least one is even
5.2 find_if_not
算法库的查找算法新增了一个find_if_not,它的含义和 find_if是相反的,即查找不符合某个条件的元素,find_if也可以实现 find_if_not的功能,只需要将判断式改为否定的判断式即可,现在新增了find_if_not之后,就不需要再写否定的判断式了,可读性也变得更好。下面是它的基本用法。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;int main()
{vector<int> v= { 1,3,5,7,9,4 };auto isEven = [](int i){ return i % 2== 0; };auto firstEven = std::find_if(v.begin(), v.end(), isEven);if(firstEven != v.end())cout << "the first even is " << *firstEven << endl;//用find_if来查找奇数则需要重新写一个否定含义的判断式auto isNotEven = [](int i){ return i % 2 !=0;};auto firstodd = std::find_if(v.begin(), v.end(), isNotEven);if(firstodd !=v.end())cout << "the first odd is " << *firstodd << endl;//用find_if_not来查找奇数则无须新定义判断式auto odd = std::find_if_not(v.begin(), v.end(), isEven);if(odd != v.end())cout << "the first odd is " << *odd << endl;
}
输出结果如下:
the first even is 4
the first odd is 1
the first odd is 1
可以看到,使用find_if_not不需要再定义新的否定含义的判断式了,更简便了。
5.3 copy_if
算法库还增加了一个copy_if算法,它相比原来的copy算法多了一个判断式,用起来更方便了。下面是copy_if的基本用法。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;int main ()
{vector<int> v = {1,3,5,7,9,4 };std::vector<int> v1(v.size());//根据条件复制auto it = std::copy_if(v.begin(), v.end(), v1.begin(), [](int i){ return i%2!=0; });//缩减vector到合适大小v1.resize(std::distance(v1.begin(), it));for(int i:v1)cout<<i<<"";cout<<endl;
}
5.4 iota
算法库新增了iota算法,用来方便地生成有序序列。比如,需要一个定长数组,这个数组中的元素都是在某一个数值的基础之上递增的,用iota就可以很方便地生成这个数组了。下面是iota的基本用法。
#include <numeric>
#include <array>
#include <vector>
#include <iostream>
using namespace std;int main()
{vector<int> v(4); //循环遍历赋值来初始化数组std::iota(v.begin(), v.end(), 1);for(auto n: v)cout << n << ' ';cout << endl;std::array<int, 4>array;std::iota(array.begin(), array.end(), 1);for(auto n:array)cout << n << ' ';std::cout << endl;
}
输出结果如下:
1 2 3 4
1 2 3 4
可以看到使用iota 比遍历赋值来初始化数组更简洁。需要注意的是,iota初始化的序列需要指定大小,如果上述代码中的vector v(4);没有指定初始化大小为4,则输出为空。
5.5 minmax_element
算法库还新增了一个同时获取最大值和最小值的算法 minmax_element,这样在想获取最大值和最小值的时候就不用分别调用max_element和 min_element算法了,用起来会更方便,
minmax_element 会将最小值和最大值的迭代器放到一个pair中返回。下面是minmax_elemen的基本用法。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;int main()
{vector<int> v = { 1, 2, 5, 7, 9, 4 };auto result = minmax_element(v.begin(), v.end() );cout<<*result.first<<" "<<*result.second<<endl;return 0;
}
输出结果如下:
1 9
5.6 is_sorted和is_sorted_until
算法库新增了is_sorted和 is_sorted_until算法,其中 is_sort用来判断某个序列是否是排好序的,is_sort_until 则用来返回序列中前面已经排好序的部分序列。下面是is_sorted和is_sorted_until算法的基本用法。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;int main()
{vector<int> v = { 1,2,5,7,9,4 };auto pos = is_sorted_until(v.begin(), v.end());for(auto it = v.begin(); it !=pos; ++it)cout<<*it<<" ";cout<<endl;bool is_sort = is_sorted(v.begin(), v.end());cout<<is_sort<<endl;return 0;
}
输出结果如下:
1 2 5 7 9
0