c++11的特性-新语法
前言
本文是记录个人对c++11新特性的学习,文中所用可能会采用其他大神的图、思路、例子等,请大神们见谅。本博客文章是在学习过程的一些总结,整理出来,分享给大家,希望对各位读者有帮助,文章中的总结可能存在很多不完整或有错误的地方,也希望读者指出。
1、Range-Based-For (for循环)
c++11版本之前的for语句写法:
//遍历数组
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (int i = 0; i < 10; i++)
cout << arr[i];
//遍历容器
std::vector<int> vec {1,2,3,4,5,6,7,8,9,10};
for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); itr++)
std::cout << *itr;
C++11中引入了这种方法也就是基于范围的For(Range-Based-For),用基于范围的For 改写上面两个例子:使用for+auto关键字
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto n : arr)
std::cout << n;
//对容器的遍历是只读的
std::vector<int> vec {1,2,3,4,5,6,7,8,9,10};
for (auto n :vec)
std::cout << n;
//对容器的遍历是读/写都可以
std::vector<int> vec {1,2,3,4,5,6,7,8,9,10};
cout << "修改前" << endl;
for (auto& n :vec)
std::cout << n++;
注: 基于范围的FOR循环的遍历是只读的遍历,除非将变量变量的类型声明为引用类型
1.1、使用细节
1)在遍历容器的时候,auto自动推导的类型是容器的value_type类型,而不是迭代器,而map中的value_type是std::pair,因此需要使用val.first,val.second来访问数据,例如map
std::map<string, int> map = { { "a", 1 }, { "b", 2 }, { "c", 3 } };
for (auto &val : map)
cout << val.first << "->" << val.second << endl;
2)基于范围的for循环还要注意容器类本身的约束,set容器元素本身有容器的特性就决定了其元素是只读的,即使使用了引用类型来遍历set元素,也不能修改器元素,再比如map的first值;
set<int> ss = { 1, 2, 3, 4, 5, 6 };
for (auto& n : ss)
cout << n++ << endl; //编译报错
3)基于范围的for循环,如果冒号后面的表达式是一个函数调用时,函数仅会被调用一次
set<int> ss = { 1, 2, 3, 4, 5, 6 };
const set<int>& getSet()
{
cout << "GetSet" << endl;
return ss;
}
int main()
{
for (auto &n : getSet()) //getSet函数仅仅会被调用一次
cout << n << endl;
}
4)基于范围的For循环的内部实现机制还是依赖于迭代器的相关实现
//以下代码运行会导致崩溃
vector<int> vec = { 1, 2, 3, 4, 5, 6 };
int main()
{
for (auto &n : vec)
{
cout << n << endl;
vec.push_back(7); //在遍历容器的时候,在容器中插入一个元素导致迭代器失效了
}
}
1.2、自定义基于范围的for实现
实现步骤:
1)定义自定义类的迭代器 (这里的迭代器是广义的迭代器,指针也属于该范畴)
2)该类型拥有begin() 和 end() 成员方法,返回值为迭代器(或者重载全局的begin() 和 end() 函数也可以)
3)自定义迭代器的 != 比较操作
4)自定义迭代器的++ 前置自增操作,显然该操作要是迭代器对象指向该容器的下一个元素
5)自定义迭代器* 解引用操作,显然解引用操作必须容器对应元素的引用,否则引用遍历时将会出错
template <typename T>
class Myiterator
{
public:
Myiterator(T val) :_value(val){}
bool operator!=(const Myiterator<T>& other) const
{
return this->_value != other._value;
}
const T & operator*()
{
return _value;
}
T operator++()
{
return ++_value;
}
private:
T _value;
};
template<typename T>
class Range
{
public:
Range(T begin, T end) :__begin(begin), __end(end){}
Myiterator<T>& begin()
{
return Myiterator<T>(__begin);
}
Myiterator<T>& end()
{
return Myiterator<T>(__end);
}
private:
T __begin;
T __end;
};
int main()
{
for (auto i : Range<int>(1, 10))
cout << i << " ";
}
1.3、std::for_each用法
使用for_each语法来使用STL遍历容器
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
vector<int> vec;
for(int i=0;i<10;++i)
{
vec.push_back(i);
}
for_each(vec.begin(),vec.end() ,[](int i)->void{ cout << i <<" "; });
cout<<endl;
return 0;
}
2、列表初始化
2.1、变量类型列表初始化
C++11扩大了用初始化列表的使用范围,让其适用于所有的内置类型和自定义类型,而且使用时,=可以不写
// 内置类型
int x1 = {10};
int x2{10}
// 数组
int arr1[5] {1,2,3,4,5}
int arr2[]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3}
map<int,int> m{{1,1},{2,2}}
// 自定义类型
class Point
{
int x;
int y;
}
Power p{1,2};
2.2、多个对象的列表初始化
给类(模板类)添加一个带有initializer_list类型参数的构造函数即可支持多个对象的,列表初始化
#include<initializer_list>
template<class T>
class Vector{
public:
Vecto(initializer_list<T> l)
:_capacity(l.size())
,_size(0){
_array = new T[_capacity];
for(auto e : l)
_array[_size++] = 3;
}
private;
T* _array;
size_t _capacity;
size_t _size;
};
3、委派构造函数和继承构造函数
3.1.1 委派构造函数
通过委派其它构造函数,使多构造函数的类编写更加容易,减少了冗余的代码
/*委派构造函数的使用,类似与构造基类 */
class Info
{
public:
Info() { init(); }
//Info(int _age) : Info(), mName("qwe") //无法编译通过,不能同时 委派 和 初始化成员,如果需要初始化成员,必须放在构造函数体中
Info(int _age) : Info()
{
mAge = _age;
printf("--- one param mAge:%d\n", mAge);
}
Info(int _age, std::string _name) : Info(_age)
{
mName = _name;
printf("--- two param mName:%s\n", mName.c_str());
}
private:
void init(){ mHeight = 1.23f; printf("--- init mheight:%f\n", mHeight); }
int mAge;
std::string mName;
float mHeight;
};
void testDelegatingConstructor()
{
Info info(77, "yang");
/*
--- init mheight:1.230000
--- one param mAge:77
--- two param mName:yang
请按任意键继续. . .
*/
}
委派构造函数和泛型编程:它使得构造函数的泛型编程变得更加容易
template<typename T> class A
{
public:
A(int i): A(i, 0)
{
}
A(double d): A(d, 0.0)
{
}
// 函数模板
A(T i, T j)
{
num1=i;
num2=j;
average=(num1+num2)/2;
cout << "average=" << average << endl;
}
private:
T num1;
T num2;
T average;
};
int main()
{
A<int> a_int(1);
A<double> a_double(1.0);
}
3.1.2 委派构造函数使用注意事项
1)构造函数时,不能使用委派构造函数和使用初始化列表一起使用,会编译报错
2)委派构造函数时,不能调用构造函数,形成闭环调用情况(委托环)
3.2.1 继承构造函数
可以让派生类直接使用基类的构造函数,无须在写(c++11之前,派生需类要使用基类构造函数需要在写,有多少个写多少个)。使用 using 来声明继承基类的构造函数,是隐式声明
class Base {
public:
Base(int v) :_value(v), _c('0'){}
Base(char c): _value(0), _c(c){}
private:
int _value;
char _c;
};
class Derived :public Base {
public:
// 使用继承构造函数
using Base::Base;
// 新增派生类构造函数,初始化派生类新增成员变量
Derived(int a,double b):Base(a), _double(b){}
// 假设派生类只是添加了一个普通的函数
void display() {
//dosomething
}
private:
// 派生类新增数据成员,快速初始化
double _double{0.0};
};
3.2.2 继承构造函数使用注意事项
1)继承构造函数只初始化基类成员变量,不会初始化派生类成员变量:
第一:通过 =、{} 对非静态成员快速地就地初始化;
第二:新增派生类构造函数,使用构造函数初始化列表;
2)构造函数拥有默认值会产生多个构造函数版本,且继承构造函数无法继承基类构造函数的默认参数,参数默认值会导致多个构造函数版本的产生,所以我们在使用有默认参数构造函数的基类时就必须要小心
class A {
public:
A(int a = 3, double b = 4): _a(a), _b(b){} //有默认值,容易导致多个构造函数版本的产生
void display() {
cout<<_a<<" "<<_b<<endl;
}
private:
int _a;
double _b;
};
class B:public A {
public:
using A::A;
};
/*
类A的构造函数可能版本
A()
A(int)
A(int,double)
A(const A&)
*/
/*
类B的构造函数可能版本
B()
B(int)
B(int,double)
B(const B&)
*/
3)多继承的情况下,继承构造函数会出现“冲突”的情况,因为多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名与参数相同,即函数签名。通过显示定义来阻止隐式生成的继承构造函数
class A {
public:
A(int i){}
};
class B {
public:
B(int i){}
};
class C : public A,public B {
public:
using A::A;
using B::B; //编译出错,重复定义C(int)
// 显示定义继承构造函数 C(int)
C(int i):A(i),B(i){}
};
4)使用继承构造函数时,如果基类构造函数被申明为私有成员函数,或者派生类是从虚基类继承而来 ,那么就不能在派生类中申明继承构造函数
5)如果派生类使用继承构造函数,则编译器不会为派生类生成默认构造函数
4、默认函数的控制
自定义的类之后,编译器会默认生成一些成员函数,这些函数被称为默认函数,包括以下:构造函数、拷贝构造函数、拷贝赋值构造函数、移动构造函数、移动拷贝函数、析构函数;此外编译器默认生成一些操作符函数,包括operator ,、operator &、operator &&、operator 、operator ->、operator ->、operator new、operator delete。
4.1 定义概述
1)显式缺省函数 (= default)
如果实现了这些函数的自定义版本后,编译器就不会去生成默认的版本在声明时在函数末尾加上”= default”来显式地指示编译器去生成该函数的默认版本,这样就保证了类还是POD类型
2)显式删除函数 (= delete)
有时候可能需要限制一些默认函数的生成,例如需要禁止拷贝构造函数的使用。原来,通过把拷贝构造函数声明为private成员,这样一旦使用编译器就会报错。而在C++11中,在显式的定义或者声明后面加上”= delete”就能实现这个效果
class Example {
public:
Example() = default; // 显示缺省函数
Example(int i) : data(i) {}
Example(const Example& ex) = delete; //限制默认函数的生成
private:
int data;
};
int main()
{
std::cout << std::is_pod <Example>::value << std::endl; //显示缺省调用
Example ex;
Example ex1(ex); //静止拷贝构造的使用导致的编译失败
return 0;
}
3)禁止赋值构造(operator delete)
//C++11之前通过operator delete方式实现
class Thing
{
public:
private:
Thing(const Thing&);
Thing& operator=(const Thing&);//禁止赋值构造
};
//C++11通过operator delete方式实现
class NonCopyable
{
protected:
NonCopyable() = default;
~NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; //禁止复制构造
NonCopyable & operator = (const NonCopyable&) = delete; //禁止赋值构造
};
4.2 其他用法
1)= default同样也能在类外的定义中修饰成员函数
class Example {
public:
Example() = default;
Example(const Example&);
private:
int data;
};
Example::Example(const Example& ex) = default; //类外修饰成员函数
2)= delete,还能避免编译器做一些不必要的隐式数据转换,也作用于普通函数
class Example {
public:
Example(int i) {}
};
class ExampleB {
public:
ExampleB(int i) {}
ExampleB(char c) = delete;//避免编译器做隐式转换
};
void func(int i) {}
void func(char c) = delete;
int main()
{
Example ex(1);
Example ex1('a'); // 编译成功,char会隐式装换成int型
ExampleB ex(1);
ExampleB ex1('a'); // 编译失败
func(1);
func('a'); // 编译失败
return 0;
}
让对象只能生成在栈内存上:
struct NonNewable {
void *operator new(std::size_t) = delete;
};
3)显示删除new操作符来避免在堆上分配对象、显示删除析构函数用于构建单例模式等等
5、模板细节的改进
1)模板的右尖括号
C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误
int main() {
std::vector<std::vector<int>> a; // c++11 之前会error,c++11之后可以
std::vector<std::vector<int> > b; //c++11 之前写成此格式ok
}
template<int N>
class foo
{ //....
};
int main(void)
{
foo< 100 >> 2> xx; //会编译报错,产生了二义性
foo< (100 >> 2)> xx; //编译成功,加了括号,防止二义性
return 0;
}
2)模板的别名
C++11引入了using,可以轻松的定义别名,而不是使用繁琐的typedef
typedef std::vector<std::vector<int>> vvi; // before c++11
using vvi = std::vector<std::vector<int>>; // c++11
//使用usingd定义函数与typedef的区别
typedef void (*func)(int, int);
using func = void (*)(int, int);
//定义模板别名对比
template <typename T>
class func_t
{
typedef void (*type)(T, T);
}
func_t<int>::type xx_1; //使用模板
template <typename T>
using func_t = void (*)(T, T);
func_t<int> xx_2; //使用模板
3)函数模板的默认模板参数
C++11之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11后都支持
template<typename T, typename U = int, U N = 0>
struct Foo{
....
};
template< typename T = int> //在c++98/03中不被支持 c++11支持
void func(void){
....
};
函数模板的默认参数在使用规则上也和其他的默认参数有所区别,例如,没有必须写在参数表最后的位置。同时,没有默认值或者类型的参数,可以自动推导
template<typename R = int, typename U> //默认模板参数没有必须写在参数表最后的位置
R func(U val){
return val;
}
int main(void){
func(123); //参数U使用自动推导,推导为int
func<long> (123); //参数从右向左填充,则U被视为long类型,则返回的123为long类型
return 0;
}
调用函数模板时,若显示指定模板的参数,参数填充顺序从右往左
此外当函数模板参数类型自动推导,当默认模板参数和模板参数自动推导同时使用时,若函数模板无法自动推导出参数类型,则编译器将使用默认模板参数,否则将使用自动推导出的参数类型。即自动推导类型优先
template<typename T>
void f(T val){
cout << val << endl;
}
tempalte<typename T>
struct identity{
typedef T type;
};
template<typename T = int>
void func(typename identity<T>::type val, T = 0){
..
};
int main(){
f("hello world"); //模板参数自动推导,T 为 const char*
func(123); //T 为int
func(12,12.0); //T 为 double,因为func中的第二个参数为12.0,这样参数模板T就被优先自动推导为double
return 0;
}
注意:模板参数自动推导是根据实参推导而来,但自动推导生效时,默认模板参数会被忽略