1. 程式人生 > >C++ 11新特性的用法之auto

C++ 11新特性的用法之auto

一、靜態型別,動態型別和型別推導

        在程式語言分類中,C/C++C常常被認為是靜態型別的語言。而有的程式語言則號稱是“動態型別”的,比如python。通常情況下,“靜”和“動”的區別是非常直觀的。我們看看下面這段簡單的python程式碼:    

  name=‘world\n’
  print 'hello, ' %name

       這段程式碼中python中的一個hellowworld的實現。這就是程式語言中的“動態型別”,在執行時來進行型別檢查,而C++中型別檢查是在編譯階段。動態型別語言能做到在執行時決定型別,主要歸功於一技術,這技術是型別推導。

       事實上,型別推導也可以用於靜態型別語言中。比如上面的python程式碼中,如果按照C/C++程式設計師的思考方式,world\n表示式應該可以返回一個臨時的字串,所以即使name沒有進行宣告,我們也能輕鬆低推匯出name的型別應該是一個字串型別。在C++11中,這個想法得到了實現。C++11中型別推導的實現之一就是重定義auto關鍵字,另一個實現是decltype。

       我們可以使用C++11方式來書寫剛才的python的程式碼

#include <iostream>
using namespace std;
int main()
{
  auto name=‘world\n’
  cout<<"hello   "<<name<<endl;

}
      這裡使用auto關鍵字來要求編譯器對變數name的型別進行了自動推導。這裡編譯器根據它的初始化表示式的型別,推匯出name的型別為char*。事實上,atuo關鍵字在早期的C/C++標準中有著完全不同的含義。宣告時使用auto修飾的變數,按照早期C/C++標準的解釋,是具有自動儲存期的區域性變數。不過系那是情況是該關鍵字幾乎無人使用,因為一般函式內沒有宣告為static的變數總是具有自動儲存期的區域性變數。auto宣告變數的型別必須又編譯器在編譯時期推導而得。

      通過以下例子來了解以下auto型別推導的基本用法

#include <iostream>
using namespace std;
int main()
{
     double foo();
	 auto x=1;
	 auto y=foo();
	 struct m
	 {
	     int i;
	 }str;
	 auto str1=str;
	 auto z;
	 z=x;
}

        以上變數x被初始化為1,因為字面變數1的型別是const int,所以編譯器推匯出x的型別應該為int(這裡const型別限制符被去掉了,後面會解釋)。同理在變數y的定義中,auto型別的y被推導為double型別;而在auto str1的定義中,其型別被推導為struct m。這裡的z,使用auto關鍵字來宣告,但是不立即對其進行定義,此時編譯器則會報錯。這跟通過其他關鍵字(除去引用型別的關鍵字)先聲明後定義的變數的使用規則是不同的。auto宣告的變數必須被初始化,以使編譯器能夠從其初始化表示式中推匯出其型別。這個意義上,auto並非一種型別宣告,而是一個型別宣告時的“佔位符”,編譯器在便已是親會將suto替代為變數實際的型別。

二、auto的優勢

1.直觀地,auto推導的一個最大的優勢在於擁有初始化表示式的複雜型別變數宣告時簡化程式碼。由於C++的發展,變數型別變得越來越複雜。但是很多時候,名字空間、模板成為型別的一部分,導致了程式設計師在使用庫的時候如履薄冰。

#include <string>
#include <vector>
void loopover(std::vector<std::string>&vs)
{
    std::vector<std::string>::iterator i=vs.begin();
	for(;i<vs.end();i++)
	{
	
	}

}
<pre name="code" class="cpp">#include <string>
#include <vector>
void loopover(std::vector<std::string>&vs)
{
	for(  auto i=vs.begin();;i<vs.end();i++)
	{
	
	}

}

使用std::vector<std::string>::iterator來定義i是C++常用的良好的習慣,但是這樣長的宣告帶來了程式碼可讀性的困難,因此引入auto,使程式碼可讀性增加。並且使用STL將會變得更加容易

2.可以避免型別宣告時的麻煩而且避免型別宣告時的錯誤。事實上,在C/C++中,存在著很多隱式或者是使用者自定義型別的轉換規則(比如整型與字元型進行加法運算後,表示式返回整型,這是一條隱式規則)。這些規則並非容易記憶,尤其是在使用者自定義很多操作符以後,這個時候auto就有使用者之地了。看一下例子

class PI
{
   public :
          double operator*(float v)
		  {
		     return (double)val*v;
		  }
          const float val=3.1415927f;
}
int main()
{
          float radius=1.7e10;
	  PI pi;
	  auto circumference =2*(pi*radius);
}
     上面定義了一個float型別的變數radius(半徑)以及一個自定義型別PI的變數pi,在計算周長的時候,使用auto型別來定義變數circumference。這裡PI在於float型別資料相乘時,其返回值為double。而PI得定義可能是在其他的地方(標頭檔案裡),main函式的程式可能就不知道PI的作者為了避免資料上溢或者是精度上的降低而返回了double型別的浮點數。因此main函式程式設計師如果使用float型別宣告circumference,就可能會享受不了PI作者細心設計帶來的好處。反之,將circumference宣告為auto,則毫無問題,因為編譯器已經做了最好的選擇。

     但是auto不能解決所有的精度問題。下面例子

#include <iostream>
using namespace std;
int main()
{
   unsigned int a=4294967295;//最大的unsigned int值
   unsigned int b=1;
   auto c=a+b;
   cout<<"a="<<a<<endl;
   cout<<"b="<<b<<endl;
   cout<<"c="<<c<<endl;
}
    上面程式碼中,程式設計師希望通過宣告變數c為auto就能解決a+b溢位的問題。而實際上由於a+b返回的依然是unsigned int的值,姑且c的型別依然被推導為unsigned int,auto並不能幫上忙。這個跟動態型別語言中資料hi自動進行拓展的特性還是不一樣的。

3.在C++中其“自適應”效能夠在一定程度上支援泛型的程式設計。

     回到上面class PI的例子,這裡假設PI的作者改動了PI的定義,比如講operator*返回值變為long  double,此時,main函式並不需要修改,因為auto會“自適應”新的型別。同理,對於不同平臺上的二代馬維護,auto也會帶來一些“泛型”的好處。這裡我們一strlen函式為例,在32位編譯環境下,strlen返回的為一個4位元組的整型,在64位的編譯環境下,strlen會返回一個8位元組的整型。即使系統庫中<cstring>為其提供了size_t型別來支援多平臺間的程式碼共享支援,但是使用auto關鍵字我們同樣可以達到程式碼跨平臺的效果。

     auto var=strlen("hello world").

     由於size_t的適用性範圍往往侷限於<cstring>中定義的函式,auto的適用範圍明顯更為廣泛。

     當auto應用於模板的定義中,其"自適應"性會得到更加充分的體現。我們可以看看以下例子

template<typename T1,typename T2>
double Sum(T1&t1,T2&t2)
{
        auto a=t1+t2;
	return a;
}
int main()
{
        int a=3;
	long b=5;
	float c=1.0f;
	float d=2.3f;
	auto e=Sum<int,long>(a,b); //e的型別被推導為long
	auto f=Sum<float,float>(c,d);//s的型別被推導為float
}
       上面中Sum模板函式接受兩個引數。由於T1,T2要在模板例項化時才能確定,所以Sum中將變數s的型別宣告為auto的。在函式main中我們將模板例項化時。Sum<int,long>中的s變數會被推導為long型別,而Sum<float,float>中的s變數則會被推導為float。可以看到,auto與模板一起使用時,其“自適應”特效能夠加強C++中泛型的能力。

三、auto的使用注意細節

①我們可以使用valatile,pointer(*),reference(&),rvalue reference(&&) 來修飾auto

auto k = 5;
auto* pK = new auto(k);
auto** ppK = new auto(&k);
const auto n = 6;

②用auto宣告的變數必須初始化

auto m; // m should be intialized  

③auto不能與其他型別組合連用

auto int p; // 這是舊auto的做法。

④函式和模板引數不能被宣告為auto

void MyFunction(auto parameter){} // no auto as method argument
template<auto T> // utter nonsense - not allowed
void Fun(T t){}

⑤定義在堆上的變數,使用了auto的表示式必須被初始化

int* p = new auto(0); //fine
int* pp = new auto(); // should be initialized
auto x = new auto(); // Hmmm ... no intializer
auto* y = new auto(9); // Fine. Here y is a int*
auto z = new auto(9); //Fine. Here z is a int* (It is not just an int)

⑥以為auto是一個佔位符,並不是一個他自己的型別,因此不能用於型別轉換或其他一些操作,如sizeof和typeid

int value = 123;
auto x2 = (auto)value; // no casting using auto
auto x3 = static_cast<auto>(value); // same as above 

⑦定義在一個auto序列的變數必須始終推導成同一型別

auto x1 = 5, x2 = 5.0, x3='r';  // This is too much....we cannot combine like this

⑧auto不能自動推導成CV-qualifiers(constant & volatile qualifiers),除非被宣告為引用型別

const int i = 99;
auto j = i;       // j is int, rather than const int
j = 100           // Fine. As j is not constant
// Now let us try to have reference
auto& k = i;      // Now k is const int&
k = 100;          // Error. k is constant
// Similarly with volatile qualifer

⑨auto會退化成指向陣列的指標,除非被宣告為引用

int a[9];
auto j = a;
cout<<typeid(j).name()<<endl; // This will print int*
auto& k = a;
cout<<typeid(k).name()<<endl; // This will print int [9]