【C++11新特性】知识点总结(1)

本文介绍了C++11的新特性,重点讲解了Variadic Templates的用法,包括它的基本概念、泛化与特化,以及如何通过递归调用来处理不定数量的参数。此外,还讨论了nullptr、auto关键字、一致性初始化和initializer_list的作用与实现。这些新特性让C++编程更加灵活高效。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Variaidc Templates(数量不定的模板参数)

释义

这个特性可以使得函数接收数量不定的参数进行处理。

先看示例,比如我们常见的print函数,我们把它写成函数模板

template<typename T,typename...Types>
void print(const T& firstArg,const Types&... args){
	cout<<firstArg<<endl;
	print(args...);
}

这里相当于把参数分为T类型的firstArg和剩下的Types类型的args。

首先要注意的是template<>的写法,…出现在typename之后

再来看函数内参数的写法,…出现在类型Types的后面。

根据调用我们来理解上述的过程

print(7.5,"hello",bitset<16>(377),42);

这里传入了四个参数。

print会把他们分为第一个和剩下的其他所有。然后输出7.5

当再次调用print(args…)时,又会把剩余的参数分解,再输出hello。

整个过程是一个递归的函数调用。

泛化与特化

如果还有一个print

template<typename...Types>
void print(const Types&... args){
	...
}

与上面的print可以并存么?会不会出现歧义?答案是可以的。

来看这样一个例子:

class CustomerHash{
public:
	std::size_t operator()(const Customer& c)const{
		return hash_val(c.fname,c.iname,c.no);
	}
	
};	

下面则是hash_val的三个版本。

版本1:

template<typename T,typename...Types>
inline void hash_val(size_t& seed,const T& val,const Types&... args){
	hash_combine(seed,val);
	hash_val(seed,args...)
}

版本2:

template<typename...Types>
inline size_t hash_val(const Types&... args){
	size_t seed=0;
	hash_val(seed,args...);
	return seed;
}

版本3:

template<typename T>
inline void hash_val(size_t&& seed,const T& val){
	hash_combine(seed,val);
}

我们根据调用中的return hash_val(c.fname,c.iname,c.no);来解释究竟调用了哪个版本。

  1. 第一次调用时,由于c.fname不是size_t类型的,所以调用版本2来接受所有参数,这是定义size_t seed =0

  2. 第二次调用时为hash_val(seed,args…),这时候版本1和版本2其实都是符合的,但版本1算作特化,也就是更加符合(有seed),所以调用版本1,把原来的一包参数分解为val和剩下的args

  3. 多次调用之后,当参数里只剩下2个参数时,也就是seed和val,这时候会调用版本3收尾。

  4. 可以看出整个过程是对于不定量参数的不断拆分,通过递归调用来实现的。


nullptr

c++11之后允许使用nullptr代替0或者NULL。之前使用NULL代表指针为空,但其实NULL本身还是0。

而现在的nullptr是指针类型的空,更好地表示指针为空。

auto关键字

auto关键字可以由编译器自动推导出该变量的类型。

double f();
auto d=f();//d是double类型

但不建议用于所有类型推导,因为知道具体的类型更有益于理解程序。

一般用于类型名比较长或者比较复杂的情况。

list<string> c;
list<string>::iterator ite;
ite=c.begin();
auto ite=c.begin();

可以省去较长的类型名。

一致性初始化

在C++11之前,初始化可以使用大括号,中括号或者等于号来操作。

而现在可以统一使用大括号,也就是在变量后面加{}进行初始化。

int value[]{1,2,3};
vector<int>v{2,3,5,7,11,13,17};
complex<double>c{4.0,3.0};

这里要谈谈背后的实现。

编译器在看到这个{}时,会做出一个initializer_list(初始化列表),会将其关联至array<T,n>,这里的T就是数据的类型,n就是个数。

然后调用构造函数时,这一堆数据可以被编译器分解为单个数据传给构造函数。比如上面的complex,编译器把4.0和3.0单独传给complex的构造函数来实现初始化。

但也有一些区别:比如vector的构造函数有多个版本,其中的一个版本可以直接接受整个initializer_list,不需要分解。

而complex就没有直接接受initializer_list的构造函数。

initializer_list

作用

上面在初始化提到的initializer_list在很多地方还有大用处。

这个列表主要实现了可以传入任意数量的某类型的参数的功能。

来看一个例子

class P{
	public:
		P(int a,int b)
		{
			cout<<"P(int,int),a="<<a<<",b="<<b<<endl;
		}
		P(initializer_list<int> initlist)
		{
			cout<<"P(initializer_list<int>),value=";
			for(i:initlist)
				cout<<i<<'';
			cout<<endl;
		}
};

P有两个版本的构造函数,第一个版本是我们熟悉的,第二个版本是initializer_list的,可以看出第二个版本可以将传入的参数进行循环处理。

当我们调用时

P p(77,5);//1
P p{77,5};//2
P r{77,5,42};//3
P s={77,5};//4

1会直接调用第一个版本的构造函数(也是上面complex的情况)

而2,3,4因为{}的存在,都会调用initializer_list的版本。

回到上面的疑问,如果我们没有initializer_list版本的构造函数会发生什么呢?

编译器会把{}中的参数逐一传给普通版本的构造函数,所以1,2,4还是可以实现的,但3就会出现错误。

内部实现

class initializer_list{
...
private:
	iterator _M_array;
	size_type _M_len;
...
}

initializer_list的内部其实是由array和其长度组成的,和我们前面说的关联至array<T,n>一致。

需要注意这里的array其实是一个迭代器,指向这个数组空间,并不是数组本身。

array是标准库中的容器,这样设计便于调用接口。

所以在拷贝initializer_list时需要明白其实只是拷贝了迭代器,属于浅拷贝(link),是很危险的操作。

新的用法

initializer_list在标准库中被大量的使用。主要用于接受不定数量的参数。

这也意味着我们在使用一些容器或者算法时可以更加灵活。

vector<int>v1{2,3,4,5,6,7};
vector<int>v2({2,3,4,5,6,7});
vector<int>v3;
v3={2,3,4,5,6.7};
max({string("ace"),string("bds")});
min({43,32,451,3254});

类似这样对于initializer_list{}的使用都可以让程序变得更加简洁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值