C++11的更新介绍(新的类功能、可变参数模板)

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:c++大冒险

总有光环在陨落,总有新星在闪烁


新的类功能

默认成员函数:

  •  原来C++类中有6个默认成员函数

  1. 构造函数
  2. 析构函数
  3.  拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载
没印象的朋友可以回顾一下往期内容:

  • C++11新增移动构造函数和移动赋值运算符重载

默认移动构造和默认移动赋值的拷贝方式

  1. 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  2. 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。

移动构造函数和移动赋值运算符重载生成条件

  1. 如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
  2. 如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。

 为什么设计这样的条件: 

当涉及深拷贝时,我们 从我们之前实现的移动拷贝可以得知,对于深拷贝的移动构造,是所包含的资源转移要有程序员决定,

即:深拷贝的移动构造要由程序员决定,浅拷贝不用,所以通过上面的条件确定当前类是深拷贝还是浅拷贝


类成员变量初始化

C++11 允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这
个我们在雷和对象默认就讲了,这里就不再细讲了,直接上代码。
class A
{
	int a = 0;//直接给缺省值,如果在构造函数没有处理a的值,那他就是我们所给的缺省值
	static int b;//静态成员变量不能给缺省值
	string s = "abc";//自定义类型也可以给缺省值
};

强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。
class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p)
 :_name(p._name)
 ,_age(p._age)
 {}
 Person(Person&& p) = default;
private:
 bit::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

注意: 默认成员函数都可以用default关键字强制生成,包括移动构造和移动赋值。


禁止生成默认函数的关键字delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p) = delete;
private:
 bit::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

final与override关键字

  • final修饰虚函数,表示该虚函数不能再被重写

class Person
{
	virtual int number()final
	{
		return 0;
	}
};
class Student :public Person
{
	virtual int number()
	{
		return 0;
	}
};
  • final修饰类,该类不能被继承

class Person final
{
	int a=0;
};
class Student :public Person
{

};
  • override

如果派生类在虚函数声明时使用了override关键字,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译,如下:

派生类中由于fun1没有const修饰,导致没有实现虚函数重写,此时编译器会报错

class Base
{
public:
	virtual void fun1() const
	{
		;
	}

};

class Derived : public Base
{
public:
	virtual void fun1() override
	{
		;
	}

};



可变参数模板

可变参函数模板定义:

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。
模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args,判断是否为参数包的主要关键在【省略号】

可变参函数模板使用:

template<class ...Args>
void  A(Args ...args)
{
	;
}
int main()
{
	A(0);
	A('a');
	A("aa", 'a', 0);
}

但是,我们如何解析参数包中的内容呢?

我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数。但语法不支持使用args[i]这样方式获取可变参数

template<class ...Args>
void ShowList(Args... args)
{
 
    for (int i = 0; i < sizeof...(args); i++)
    {
        cout << args[i] << " ";
    }
    cout << endl;
}

递归函数方式展开参数包

展开函数

我们每调一次ShowList函数,就会把第一个参数T val打印,然后把args参数包传给下一次调用的ShowList,接着参数包里的第一个参数会被下一个函数的T类型接受,剩下的会被args接受,如此递归下去,每次剥离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

template<class T,class ...Args>
void ShowList(T val, Args ...args)
{
	cout << val << endl;
	ShowList(args);
}

 递归结束:

根据函数匹配原则我们设计一个函数模板,只有一个参数,这样当参数报只剩一个参数时就会跳出循环进入该函数,从而结束递归

template<class T>
void ShowList(T val)
{
	cout << val << endl;
}

 逗号表达式展开参数包

这种方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式先执行 printarg(args),再得到逗号表达式的结果0。{(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了
template <class T>
void PrintArg(T t)
{
 cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
 int arr[] = { (PrintArg(args), 0)... };
 cout << endl;
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

可变参数模板的应用:emplace系列函数

emplace系列的接口,支持模板的可变参数,并且万能引用。
  • emplace与insert对比

我们先分析下面的代码:
对于emplace_back:参数为10和‘a’,根据可变参数模板的性质,他们会直接作为参数传入到list的函数中,再去构建节点中的pair<>,直接构造
对于push_back:参数50,‘e’,会先构造一个临时变量pair<>,接着调用转移构造去构建节点中的pair<>,先是构造函数,在移动拷贝
int main()
{
	std::list< std::pair<int, char> > mylist;
	mylist.emplace_back(10, 'a');
	mylist.push_back({ 50, 'e' });
	return 0;
}
即:emplace只拷贝了一次,但push_back先拷贝在移动拷贝
但是由于深拷贝中移动拷贝效率高,所以二者差距不明显
  • emplace系列接口相对insert的优势

emplace系列真正的优势在于浅拷贝的类

因为对于深拷贝的且实现了移动构造的类来说,移动构造代价很小,emplace的优势显现不出来。

浅拷贝效率低,则可以认为emplace_back节省了一次拷贝构造的时间,例如日期类

class Date
{
public:
	//构造
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	Date(Date &d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(Date&d)" << endl;
	}
	Date(Date&& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(Date&&d)" << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	list<Date> lt1;
	lt1.push_back({ 2024,3,30 });
	cout << "=============================================" << endl;
	lt1.emplace_back(2024, 3, 30);
	return 0;
}


总结

  1. emplace接口使用需要直接传入参数包才能体现其价值,因为emplace真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

  2. emplace对于深拷贝的且实现了移动构造的类意义不大,因为移动构造的代价很小,emplace系列接口对于浅拷贝的类有可观的效率提升

  3. 使用emplace应该直接传参数包,因为如果传入的是对象(不管有名还是匿名),那么emplace系列接口的效率和insert接口的效率一样(都是调用拷贝构造)


    🥰创作不易,你的支持对我最大的鼓励🥰

    🪐~ 点赞收藏+关注 ~🪐

    e3ff0dedf2ee4b4c89ba24e961db3cf4.gif

  • 21
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值