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