C11簡潔之道:初始化改進


1、  C++98/03初始化

  我們先來總結一下C++98/03的各種不同的初始化情況:

//普通數組
int i_arr[3] = {1, 2, 3};

//POD(plain old data)
struct A
{
    int x;

    struct B
    {
        int i;
        int j;
    }b;
}a = {1, {2, 3}};

//拷貝初始化
int i = 0;

class Foo
{
public:
    Foo(int){};
}Foo = 123;

//直接初始化
int j(0);

  這些不同的初始化方法都有各自的適用范圍和方法,但是種類繁多的方法卻沒有一種可以通用的。所以C++11提出了初始化列表的方法來解決通用問題。

2、  統一初始化方法

  其實C++98/03中已經存在初始化列表的方法,只是范圍比較窄,只適用於常規POD類型。

int i_arr[3] = {1, 2, 3};
int i_arr2[] = {1, 2, 3, 4};

struct B
{
     int i;
     int j;
}b = {1, 2};

  而C++11將這種初始化方法適用於所有類型的初始化。我們先來看一組例子:

class Foo
{
public:
    Foo(int){};
private:
    Foo(const Foo &){};
};
 
void testFunc(void)
{
    Foo val1(123);
    //Foo val2 = 123; // error:Foo::Foo(const Foo &) is private.
    Foo val3 = {123};
    Foo val4{123};
    int a5 = {2};
    int a6{3};
}

  val3、val4使用了初始化列表來初始化對象,a3雖然使用等號,但是並不影響到私有拷貝,仍然是初始化列表的方式,等統一val1的直接初始化,而val2則調用私有拷貝函數會報錯。a5、a6則是一般類型的初始化,val4和a6都是C++11特有的,而C++98/03並不支持。

  新的初始化方法是變量名后面加{}來進行初始化,{}內則是初始化的內容,等號是否存在並不影響。

type val {};

  C++11的新方式同樣支持new操作符:

int *a = new int{5};
double b = double {12.34};
int *arr = new int[3]{1,2,3};

  a指向了new操作符分配的一塊內存,通過初始化列表將內存的初始值指定為了5;

  b是對匿名對象進行初始化之后然后進行拷貝初始化;

  arr則是通過new動態申請一個數組,並通過初始化列表進行初始化。

  初始化列表還有一個特殊的地方,就是作為函數的返回值。

struct Foo
{
    Foo(int, double){};
};

Foo testFunc(void)
{
    return {12,  12.3};
}

  在C++11中,初始化列表是非常方便的,不僅統一了對象的初始化方式,還使代碼更加簡潔清晰。

3、  使用細節

3.1 自定義類型初始化

  當我們在C++11中使用初始化列表時,可能有以下情況:

struct A
{
    int x;
    int y;
}a = {123,123};   //a.x = 123, a.y = 123

struct B
{
    int x;
    int y;
    B(int, int) : x(0), y(0) {};
}b = {123,123};   //b.x = 0, b.y = 0

  這個例子說明什么問題呢,a是以C++98/03的聚合類型來初始化的,用拷貝的方式初始化a中的成員,而b呢,由於自定義了構造函數,所以初始化是以構造函數來初始化的。所以有以下結論:

  當使用初始化列表時,如果是聚合類型,則以拷貝的方式來初始化成員,如果是非聚合類型,則是以構造函數來初始化成員。

3.2 聚合類型

  提了這么多的聚合類型,那么到底什么是聚合類型呢?我們來看聚合類型的定義:

  1)  類型是普通數組(int[10],char[],long[2][3]等)。

  2)  類型是一個類,且:

  • 無用戶自定義構造函數;
  • 無私有或者保護的非靜態成員;
  • 無基類;
  • 無虛函數;
  • 無{}和=直接初始化的非靜態數據成員。

3.2.1 數組

  對於數組而言,就很簡單了,只要該類型是一個普通的數組,如果數組的元素並不是聚合類型,那么這個數組也是一個聚合類型:

int [] = {1,2,3};
std::string s_arr[3] = {“hello”,  “C++”,  “11”};

3.2.2 存在自定義構造函數

struct A
{
    int x;
    int y;
    int z;
    A(int, int){};
}; 

A a = {123, 123, 12};

  當一個自定義類擁有自己的構造函數使,無法將該類看作一個聚合類型,必須通過自定義的構造函數才能構造對象。

3.2.3 存在私有或者非靜態成員

struct A
{
    int x;
    int y;
protected:
    int z;
}; 

A a = {123, 123, 12}; //error

struct B
{
    int x;
    int y;
protected:
    static int z;
}; 

B b = {123, 123}; //ok

  例子中,A的實例化是失敗的,因為z是一個受保護的非靜態成員。而b是成功的,因為z是一個受保護的靜態數據成員,所以,類成員里面的靜態數據成員是不能通過初始化列表來初始化的,靜態數據成員的初始化遵循靜態成員的初始化方式。

3.2.4 有基類或者虛函數

  有基類或者虛函數同樣不適用於使用初始化列表。

struct A
{
    int x;
    int y;
    virtual void fun(){};
}; 

A a = {123, 123}; //error

class B {};

struct C : public B
{
    int x;
    int y;
}; 

B b = {123, 123}; //error

3.2.5 {}和=初始化的非靜態數據成員

struct A
{
    int x;
    int y = 2;
}; 

A a = {123, 123}; //error

  在類型A中,y在聲明時即被=初始化為2,所以A不再是一個聚合類型。

  這個例子中需要注意的是,C++11中放寬了類型申明的初始化操作,即在非靜態數據成員的聲明時調用{}或者=來對成員進行初始化,但是造成的影響是該類型不再是聚合類型,所以不能直接使用初始化列表。所以,如果要使用初始化列表就必須自己定義一個構造函數。

3.2.6 聚合類型並非遞歸

struct A
{
    int x;
    int y;
private:
    int z;
}; 

A a{1, 2, 3}; // error

A a1{}; //ok

struct B
{
    A a;
    int x;
    double y;
};

B b{{}, 1, 2.5};

  A有一個私有化的非靜態成員,所以使用A a{1, 2, 3}是錯誤的,但是可以調用他的無參構造函數,所以在B中,即使成員a是一個非聚合類型,但是B仍然是一個聚合類型,可以直接使用初始化列表。

3.2.6 小結

  根據這么多例子,我們得到以下結論:

  對於一個聚合類型,使用初始化列表相當於對其中每個元素分別賦值;而對於非集合類型,則需要先定義一個合適的構造函數,此時使用初始化列表將調用它對應的構造函數。

4、  初始化列表

4.1 任意長度的初始化列表

  在c++中,對於stl容器和未顯示數組長度的數組可以進行任意長度的初始化,在初始化的時候可以書寫任意長度的內容。

int i_arr[] = {1,2,3,4};

std::vector<int> veci_t = {1,2,3,4};

std::map<std::string, int> mapsi_t = {{"1", 1}, {"2", 2}, {"3", 3}};

  但是對於自定義類型不具備這種能力,但是C++11解決了這個問題,C++11中可以通過輕量級模板std::initalizer_list來解決這個問題。我們只需要添加一個std::initializer_list的構造函數,這個自定義類型即可擁有這種任意長度初始化列表來初始化的能力。

class Foo
{
public:
    Foo( std::initializer_list<int> list ) {};
};

Foo foo = {1,2,3,4,5};

  std::initializer_list負責接收初始化列表,可以通過for循環來讀取其中的元素,並將元素做操作。不僅可以作為類型的初始化,同樣的,可以作為函數參數傳遞同類型的數據集合。在任何需要的時候,都可以使用std::initializer_list來一次性傳遞多個參數。

// code1
class FooVector
{
public:
    FooVector(std::initializer_list<int> list)
    {
        for(auto it = list.begin(); it != list.end(); ++it)
        {
            mveci_content.push_back(*it);
        }
    }
private:
    std::vector<int> mveci_content;
};

FooVector foo1 = {1,2,3,4,5};

//code2
using pair_t = std::map<int, int>::value_type;
class FooMap
{
public:
    FooMap(std::initializer_list<pair_t> list)
    {
        for(auto it = list.begin(); it != list.end(); ++it)
        {
            mmapii_content.insert(*it);
        }
    }
private:
    std::map<int, int> mmapii_content;
};

FooMap foo2 = {{1,2}, {3,4}, {5,6}};
 
//code3
void vFunc(std::initializer_list<int> list)
{
    for(auto it = list.begin(); it != list.end(); ++it)
    {
        std::cout << *it << std::endl;
    }
}

void vCallFunc(void)
{
    vFunc({});
    vFunc({1,2,3,4});
}

4.2 std::initializer_list使用細節

  std::initializer_list的特點如下:

  • 它是一個輕量級的容器類型,內部定義了iterator等容器等必須的概念;
  • 可以接收任意長度的初始化列表,但是要求元素必須都是同種類型;
  • 有三個成員接口,size(),begin(),end();
  • 只能被整體初始化或者賦值。

  //獲取長度

std::initializer_list<int> list = {1,2,3};  //初始化
size_t len = list.size();       //len = 3

  std::initializer_list的訪問只能通過begin()和end()來進行循環遍歷,遍歷取得的迭代器是只讀的,所以無法修改其中元素的值,但是可以整體賦值來修改其中的元素。

std::initializer_list<int> list;
size_t len = list.size();  //len = 0
list = {1,2,3,4,5};
len = list.size();   //len = 5
list = {1,2};
len = list.size();    //len = 2

  在研究了std::initializer_list的用法之后,我們來看std::initializer_list的效率。很多時候,如果容器內部是自定義類型或者數量較大,那么是不是就像vector之類的容器一樣,把每個元素都賦值一遍呢?答案是不是!std::initializer_list是非常高效的,它的內部並不保存初始化列表元素中的拷貝,僅僅保存初始化列表中的引用。

  如果我們按照下面的代碼來使用std::initializer_list是錯誤的,雖然可以正常通過編譯,但是可能無法得到我們希望的結果,因為a,b在函數結束時生存周期也結束了,返回的是不確定的內容。

std::initializer_list<int> func1(void)
{
    int a = 1, b = 2;
    return {a, b};  //a,b在返回時並沒有拷貝
}

  正確的用法應該是這樣,通過真正的容器或者具有轉移拷貝語意的物件來替代std::initializer_list返回的結果。

std::vector<int> func2(void)
{
    int a = 1, b = 2;

    return {a, b};  //ok
}

  我們應該將std::initializer_list看作保存對象的引用來使用,在它持有的對象的生命周期結束之前來完成傳遞。

5、  防止類型收窄

5.1 類型收窄的情況

  我們先來看一段代碼:

struct Foo
{
    Foo(int i) { std::cout << i << std::endl;}
};

Foo foo(1.2);

  這個例子就是類型收窄的情況,雖然說能夠正常通過編譯,但是在傳遞i之后不能完整的保存浮點數的數據。

  我們來看C++中有哪些情況會有類型收窄的情況:

  • 從一個浮點數隱式轉換為一個整數,如int I = 2.2;
  • 從高精度浮點數隱式轉換為低精度浮點數,如long doule隱式轉換為double或者float;
  • 從一個整數隱式轉換為一個浮點數,並且超出了浮點數的范圍,如float f = (unsigned long long ) – 1;
  • 從一個整型隱式轉換為一個長度較短的整型數,並且超出了長度較短的整型數范圍,如char x = 65536;

  這些類型收窄的情況,在編譯器並不會報錯,但是可能存在潛在的錯誤。

5.2 C++11的改善

  C++11中可以通過初始化列表來檢查,防止類型的收窄。我們來看一組例子:

int a = 1.1;            //ok
int b = {1.1};          //error

float fa = 1e40;          //ok
float fb = {1e40};        //error

float fc = (unsigned long long) -1;         //ok
float fd = { (unsigned long long) -1 };     //error
 
float fe = (unsigned long long)1;           //ok
float ff = {(unsigned long long)1};         //ok

const int x = 1024, y = 1;
char c = x;                       //ok
char d = {x};                     //error
char e = y;                       //ok
char f = {y};                     //ok

  在C++11中,遇到各種類型收窄的情況,初始化列表是不允許這種轉換的,上述例子中,如果x,y去掉const限定符,最后的f也會因為類型收窄而報錯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM