由于本人是個蒟蒻,并且語文不好,所以如果本文有任何錯誤,請指出,我將盡快修正。
子函數(shù)的編寫(必需)
C++的類型
強制類型轉(zhuǎn)換
指針/迭代器
auto
模板
假如wenge問你:
如何不用for,求出2個數(shù)的最大值?
你說,這還不簡單,
ans=max(a,b);
wenge又問你:
如何不用for,求出3個數(shù)的最大值?
你說,這還不簡單,
ans=max(a,max(b,c));
wenge又問你:
如何不用for,求出10個數(shù)的最大值?
你說,這還不簡單,
ans=max(a[1],max(a[2],max(a[3],max(a[4],max(a[5],max(a[6],max(a[7],max(a[8],max(a[9],a[10])))))))));
wenge說這太亂了。
你說,這個總行了吧,
ans=max(ans,a[1]);
ans=max(ans,a[2]);
ans=max(ans,a[3]);
ans=max(ans,a[4]);
ans=max(ans,a[5]);
ans=max(ans,a[6]);
ans=max(ans,a[7]);
ans=max(ans,a[8]);
ans=max(ans,a[9]);
ans=max(ans,a[10]);
能不能一行寫完呢?
能。
ans=max({a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9],a[10]});//C++11
但是庫函數(shù)max
為什么可以為什么可以這樣使用呢?
好了,現(xiàn)在讓我們進入正題——C++的兩種可變參數(shù)。
注意:此節(jié)為C++11的新特性,編譯時請增加命令行選項-std=c++11(這個貌似大家都懂)
首先讓我們看看max的函數(shù)模板聲明。
注意到用紅色框起來的函數(shù)模板聲明。
這個函數(shù)以initializer_list
為參數(shù)。initializer_list
是啥?
就是一個初始化表。初始化表是啥?讓我們看一個例子。
{1,2,3,4,5}
什么?你告訴我這個就是initializer_list
?讓我們再看一個例子。
vector<int> a={1,2,3,4,5};
如果你還不知道initializer_list
是啥,那就再看一個例子。
int a[5]={1,2,3,4,5};
你可能會問了,這不是數(shù)組賦值嗎?
但是,這個數(shù)組賦值用了初始化表,所以{1,2,3,4,5}
就是一個initializer_list
。
initializer_list
是C++11的一個新特性。這玩意除了能給數(shù)組賦值以外,還可以給自己定義的結(jié)構(gòu)體賦值。比如
#include <iostream>
#include <initializer_list>
struct point{
int x,y;
};
int main(){
point a={1,2};
cout<<a.x<<' '<<a.y;
return 0;
}
程序會輸出1 2
。然而我們要講的是initializer_list
實現(xiàn)可變參數(shù)。
用initializer_list
實現(xiàn)可變參數(shù)的方式,就是把initializer_list
作為函數(shù)的參數(shù)。
現(xiàn)在首先看一看本人實現(xiàn)的max
函數(shù):
#include <iostream>
#include <algorithm>
#include <initializer_list>
using namespace std;
//C++ STL的可變參數(shù)
int mymax(initializer_list<int> a){
int ans=-2147483648;//int的最小值
for(auto i:a){
ans=max(i,ans);
}
return ans;
}
int main(){
int a=1,b=2,c=3,d=4,e=5;
cout<<mymax({a,b,c,d,e});
return 0;
}
程序輸出了5,因為a,b,c,d,e
的最大值就是5。
現(xiàn)在讓我們看看mymax
這個函數(shù)是如何實現(xiàn)的。
首先定義了一個臨時變量ans
存儲答案。
現(xiàn)在來看for
。也許可能有人不明白這個for
是什么意思。實際上這個for
的用途是遍歷整個initializer_list
。
比較麻煩的一點是,簡單的遍歷一個initializer_list
只有兩種方式,一種是使用C++11的auto
新特性(也就是代碼里的for(auto i:a)
),另外一個是使用迭代器。使用迭代器的方法是這樣的:
int mymax(initializer_list<int> a){
int ans=-2147483648;//int的最小值
for(initializer_list<int>::iterator i=a.begin();i!=a.end();i++){
ans=max(*i,ans);
}
return ans;
}
看上去很麻煩。但是initializer_list<int>::iterator
這東西可以換成auto
。但是即使initializer_list<int>::iterator
可以換成auto
,個人還是推薦使用for(auto i:a)
。
但是上述方法只能遍歷整個initializer_list
容器。如何遍歷一個initializer_list
容器的一部分?一個簡單的方法是建立一個數(shù)組,把initializer_list
中所有的數(shù)據(jù)拷貝進這個數(shù)組里。
一個initializer_list
的大小可以用initializer_list.size()
函數(shù)來獲取。initializer_list
類型的size()
函數(shù)與string
和vector
的size()
的作用完全一致,即返回initializer_list
中的元素個數(shù)??纯聪旅娴睦樱?/p>
#include <iostream>
#include <algorithm>
#include <initializer_list>
using namespace std;
int b[10];
int average(initializer_list<int> a){
int ans=0;
int j=1;
for(auto i:a){
b[j]=i;
j++;
}
sort(b+1,b+a.size()+1);
for(int i=2;i<a.size();i++){
ans+=b[i];
}
ans/=(a.size()-2);
return ans;
}
struct point{
int x,y;
};
int main(){
int a=80,b=10,c=40,d=40,e=70;
cout<<average({a,b,c,d,e});
return 0;
}
這個例子是求多個數(shù)的去除一個最大值和一個最小值的平均值??梢钥吹?,我們將initializer_list
類型的a拷貝進了預(yù)先定義的數(shù)組b,再在數(shù)組b上執(zhí)行sort()
。
你可以在單個函數(shù)使用多個initializer_list
作為參數(shù)。比如
void print(initializer_list<int> a,initializer_list<char> b);//print()的具體定義略
int main(){
print({1,2,3},{'a','b','c'});
return 0;
}
這樣使用是合法的。
并且你可以增加一個模板,使一個initializer_list
可以支持同種不同類型的數(shù)據(jù):
template<typename T>
void print(initializer_list<T> a);//print()的具體定義略
int main(){
print({114514,1919,810});
print({'c','h','a','r'});
print({'xyzzy','plugh','abracadabra'});
return 0;
}
首先,因為initializer_list
是拿大括號括起來的,所以傳入一個initializer_list
參數(shù)的時候要拿大括號括起來。
C++標(biāo)準(zhǔn)要求“initializer_list
的元素類型都必須相同,但編譯器將進行必要的轉(zhuǎn)換”、“但不能進行隱式的窄化轉(zhuǎn)換”(C++ Primer)。比如下面的代碼:
double a[5]={1.14514,2.33,-3.456789,4.0,5};
//int類型的5被編譯器隱式轉(zhuǎn)換成double類型的5.0
long long b[5]={114514ll,233ll,-3456789ll,4ll,5};
//int類型的5被編譯器隱式轉(zhuǎn)換成long long類型的5
int c[5]={114514,233,-3456789,4,5.0};
//double類型的5.0被編譯器轉(zhuǎn)換成int,屬于隱式窄化轉(zhuǎn)換,錯誤
但是在C++ Primer里所說的這個錯誤并不一定導(dǎo)致CE,比如本人在gcc4.9.2編譯,只出現(xiàn)了一個warning。但是為了避免潛在的CE,應(yīng)該盡量避免類型轉(zhuǎn)換問題。
除了類型轉(zhuǎn)換問題,initializer_list
的元素類型都必須相同,所以不能將不同類型的元素裝進initializer_list
里。例如下列代碼將會導(dǎo)致CE:
void print(initializer_list<int> a);//print()的具體定義略
int main(){
print('string');
return 0;
}
另外,即使你使用了多個initializer_list
,由于每個initializer_list
的元素類型都必須相同,每個initializer_list
只能處理一種類型的數(shù)據(jù)。這導(dǎo)致了一般的initializer_list
實現(xiàn)的可變參數(shù)只能支持單種類型的數(shù)據(jù)。
另外,說起可變參數(shù),我們可能想到最多的例子就是scanf
和printf
。實際上,它們的確是真真正正的可變參數(shù)函數(shù)。而scanf
和printf
是C的原生函數(shù),其誕生早在initializer_list
之前。那么scanf
和printf
是如何實現(xiàn)的呢?
還是看一下函數(shù)聲明。
那個...
是什么?
沒錯,那就是可變參數(shù)的標(biāo)志。
但是那些參數(shù)叫什么呢?
沒有名字。
那怎么讀取它們呢?
用va_list
。
C/C++函數(shù)可以通過在其普通參數(shù)后添加逗號和三個點(,...
)的方式來接受數(shù)量不定的附加參數(shù),而無需相應(yīng)的參數(shù)聲明。
特別的,雖然直接使用三個點作為函數(shù)的參數(shù)是合法的,但是這些參數(shù)并不能被讀?。ê竺鏁f明原因)。不推薦使用這樣的函數(shù)。
注意三個點必須加在參數(shù)列表的最后。
例如:
void print(int count,...);//合法
void print(...);//合法
void print(int count,...,int count2);//非法,CE
C/C++頭文件<stdarg.h>
或<cstdarg>
提供了對可變參數(shù)的支持。va_list
是一個類型,定義在頭文件<stdarg.h>
或<cstdarg>
中。<stdarg.h>
中定義了3個宏,與va_list
配套使用,分別是va_list
,va_start
,va_arg
和va_end
。在C++中,C++11標(biāo)準(zhǔn)又新增了宏va_copy
。所以,va_list
,va_start
,va_arg
和va_end
是C與C++通用的。
讓我們看一個使用va_list
實現(xiàn)可變參數(shù)的例子:
#include <iostream>
#include <cstdarg>
using namespace std;
void printint(int count,...){
va_list a;
va_start(a,count);
for(int i=1;i<=count;i++){
int b=va_arg(a,int);
cout<<b<<' ';
}
va_end(a);
}
int main(){
printint(5,114514,233,-3456789,4,5);
return 0;
}
這個函數(shù)從參數(shù)讀取數(shù)量等同于參數(shù)count
的整數(shù)并輸出這個程序?qū)敵?code>114514 233 -3456789 4 5。
在函數(shù)體內(nèi),我們首先定義了一個va_list
類型的a
。a
即是printint()
的參數(shù)表,即其包含了包括count
在內(nèi)的所有參數(shù)。你可以把va_list
看做一個棧(實際上也是這么存儲的),參數(shù)從右至左入棧。
然后,我們調(diào)用了va_start
宏。va_start
宏有兩個參數(shù),第一個需要寫我們之前創(chuàng)建的va_list
的名字(在這個例子里是a
),第二個參數(shù)則寫...
之前的上一個參數(shù)(在這個例子里是count
)。你可以想象有一個指針,一開始什么都不指向(即指向NULL),我們調(diào)用va_start
宏,則這個指針就指向了棧頂,并且一直彈棧,直到指針指向了...
之前的上一個參數(shù)(在這個例子里是count
)的位置。
然后,我們使用va_arg
宏讀取函數(shù)的參數(shù)。va_arg
宏有兩個參數(shù),第一個需要寫我們之前創(chuàng)建的va_list
的名字,第二個填寫當(dāng)前所要讀取的參數(shù)的類型(這也是scanf
為什么有那么多占位符的原因,因為它必須獲取參數(shù)的類型)。你可以認(rèn)為這個宏先進行彈棧,然后讀取棧頂內(nèi)容并返回。注意,va_arg
的第二個參數(shù)中,char,char_16t,wchar_t,short
以及其signed,unsigned
版本要寫成int
,float
要寫成double
,char_32t
要寫成unsigned long
。原因是C/C++的默認(rèn)類型轉(zhuǎn)換。否則必定RE。這個東西gcc會給出warning。除此之外,如果第二個參數(shù)的實際類型不同于va_arg
的第二個參數(shù),這個參數(shù)會被吃掉。
最后,使用va_end
宏結(jié)束函數(shù)參數(shù)的讀取。你可以認(rèn)為這個宏使指針重新指向NULL,并且刪除賦予va_list的內(nèi)存,使可變參數(shù)函數(shù)能正確返回。有助于代碼的健壯。
同時這個函數(shù)也可以使用while
完成:
#include <iostream>
#include <cstdarg>
using namespace std;
void printint(int first,...){
va_list a;
int b=first;
va_start(a,first);
while(b!=-1){
cout<<b<<' ';
b=va_arg(a,int);
}
va_end(a);
}
int main(){
printint(114514,233,-3456789,4,5,-1);
return 0;
}
實際上,va_list
的優(yōu)點在于其可以接受不同類型的參數(shù),前提是你知道這些參數(shù)的類型。下面是一個例子,是本人實現(xiàn)的printf
,只實現(xiàn)了%d
和%c
:
#include <iostream>
#include <cstdarg>
using namespace std;
void myprintf(string f,...){
va_list a;
va_start(a,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(a,int);
}
if(f[i]=='c'){
cout<<char(va_arg(a,int));
}
}
else cout<<f[i];
}
va_end(a);
}
int main(){
myprintf('test\n%d%c',114514,'A');
return 0;
}
這東西也可以有模板。但是由于類型轉(zhuǎn)換方面的問題,要想支持int以下的類型,恐怕是得寫一大堆的特化了。(想一想,怎么寫)由于作者太懶,就不寫了
那么va_copy
呢?
實際上,如果你定義多個va_list
,只有你定義的第一個va_list
里面有那些參數(shù)的數(shù)據(jù)。va_copy
有兩個參數(shù),類型都是va_list
,用途是把第二個va_list
的數(shù)據(jù)拷貝進第一個va_list
里。
如果參數(shù)類型都相同的話誰會用這東西呢?還不如把這些參數(shù)拷貝進數(shù)組里(滑稽)所以這個東西用來重復(fù)處理具有多個數(shù)據(jù)類型的可變參數(shù)時會很方便。比如下面這個輸出兩遍的printf
:
#include <iostream>
#include <cstdarg>
using namespace std;
void myprintf(string f,...){
va_list a,b;
va_copy(b,a);
va_start(a,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(a,int);
}
if(f[i]=='c'){
cout<<char(va_arg(a,int));
}
}
else cout<<f[i];
}
va_end(a);
va_start(b,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(b,int);
}
if(f[i]=='c'){
cout<<char(va_arg(b,int));
}
}
else cout<<f[i];
}
va_end(b);
}
int main(){
myprintf('test\n%d%c',114514,'A');
return 0;
}
之前我們已經(jīng)知道了,直接使用三個點作為函數(shù)的參數(shù)是合法的,但是這些參數(shù)并不能被讀取。為什么呢?因為如果有一個直接使用三個點作為參數(shù)的函數(shù),則沒有...
之前的上一個參數(shù),va_start
將無法使用。然而親測不使用va_start
會RE,所以這種函數(shù)的參數(shù)并不能被讀取。
之前我們已經(jīng)知道了,va_arg
的第二個參數(shù)中,char,char_16t,wchar_t,short
以及其signed,unsigned
版本要寫成int
,float
要寫成double
,char_32t
要寫成unsigned long
。原因是C/C++的默認(rèn)類型轉(zhuǎn)換。否則必定RE。這個東西gcc會給出warning。除此之外,如果第二個參數(shù)的實際類型不同于va_arg
的第二個參數(shù),這個參數(shù)會被吃掉。然而,寫成int
,va_arg
讀取的也是int
。比如下面的手寫printf
的錯誤例子,不會輸出char
類型的A
,而會輸出int
類型的65
(A
的ASCII碼)。 所以想出默認(rèn)類型轉(zhuǎn)換這個餿主意的人真是個大銻。
#include <iostream>
#include <cstdarg>
using namespace std;
void myprintf(string f,...){
va_list a;
va_start(a,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(a,int);
}
if(f[i]=='c'){
cout<<va_arg(a,int);
//沒有轉(zhuǎn)換,正確寫法實際上是用強制類型轉(zhuǎn)換
//cout<<char(va_arg(a,int));
}
}
else cout<<f[i];
}
va_end(a);
}
int main(){
myprintf('%c','A');
return 0;
}
所以,如果有使用更低等的類型(比如char
)的必要,使用強制類型轉(zhuǎn)換。
在函數(shù)最后,一定要調(diào)用va_end
。
不要越界!不要越界?。〔灰浇纾。?!
你們的程序里有千萬塊內(nèi)存。只要不越界,這個系統(tǒng)就無法檢測到非法讀寫。
如果越界,非法讀寫將被檢測到,系統(tǒng)的保護模塊將會觸發(fā),你們的程序?qū)E!
不要越界!不要越界??!不要越界?。?!
然后就沒了。
http://www.cplusplus.com/
C++ Primer
洛谷日報接受投稿,采用后有薄禮奉送,請戳
https://www.luogu.org/discuss/show/47327 .