C++ Primer学习 《Classes》

来源:互联网 发布:免费下载visio软件 编辑:程序博客网 时间:2024/05/18 15:52

Classes(基础)

1. 类的基础介绍

定义在class内部的函数默认是inline的。

1.1 this指针介绍

当我们通过object调用类成员函数时,有一个指向这个object的指针被传递给这个函数,这个指针叫做this指针。

例如当我们调用:

total.price();
实际上相当于:

price(&total);//pseudo-code
因为this永远指向当前object,因此this是一个const指针,我们不能改变this的地址。但是this指向的object当然是可以改变的。因此this是一个top-level const pointer,不一定是low-level const pointer。

1.2 const 成员函数介绍

类成员函数有时被声明为const类型,这意味着该类成员函数不会修改this指向的object。而没有被声明为const类型的类成员函数,则自然可以修改这个object。因此,如果object是const,那么我们就不能调用nonconst类成员函数。

class testConst{public:int constMemberFunction(){ return 0; }        int constMemberFunction2() const { return 0; }//const 类成员函数};testConst a;a.constMemberFunction();//okconst testConst b;b.constMemberFunction();//error!b.constMemberFunction2();//ok!

具体说来,在上面的例子中,第一个类成员函数constMemberFunction将this指针定义为int* const类型,而在第二个类成员函数constMemberFunction2中将this指针定义为const int * const类型。大家可以自行体会。

1.3 类内顺序(class scope)

在类的定义中,先出现的函数可以在函数体内调用后出现的变量。即:

class scopeTest{    int print(){cout << answer << endl;}//ok!尽管answer在后面定义    int answer;};
这是因为,C++编译器在编译类时分两步,第一步:对所有变量和类成员函数的声明进行编译;第二步:处理函数体。

Return “this” object

直接返回this指向的object,一般的写法是:

class thisTest{...};thisTest& thisTest::add(const thisTest&){...return *this;}

1.4 构造函数(Constructors)

和其他函数不同的是,构造函数没有返回类型。一个类可以有多个构造函数,只要满足函数重载的规则即可。

构造函数不能定义为const函数,这很容易理解。即使我们要创建一个const object,在创建的过程中我们无疑需要对这个object进行一些赋值。

默认构造函数(default constructor)

默认构造函数没有任何参数。默认构造函数可以显式声明,只要没有参数就属于默认构造函数。

编译器自动生成的构造函数通常被称作:synthesized default constructor。在这个函数中,编译器遵循如下法则初始化类内各个成员:
1.如果有in-class initializer(在类内直接赋初值),则使用这个值。

2.否则,对成员进行default initialize。(int等被赋为0等)

有时不能依赖synthesized default constructor

1.只有当类没有定义任何构造函数时,编译器才会为我们定义一个synthesized default constructor

2.当成员是built-in type 或 compound type时(arrays , pointers等),default initialize会产生undefined value,这是我们绝对不希望的。

3.有时,编译器无法完成synthesized default constructor的工作。比如,一个类内部有一个成员是另一个类,而那个类没有构造函数,等等情况。

=default 

有时,我们在定义各个类内成员时,已经写好了in-class initializer,那么可能会觉得再写一个构造函数比较麻烦,这时可以用=default 语句。

class defaultConstructorClass{...defaultConstructorClass() = default;};
一般来说,只有当我们同时需要定义其他的构造函数时,才会这么写,否则,直接不定义不就好了嘛。

constructor initializer list

在定义构造函数时,有一个比较与众不同的东西叫做constructor initializer list。

形式如下:

className(string &s) : bookNo(s){}className(string &s,unsigned n,double p) : bookNo(s),units_sold(n),revenue(p*n)  {}

及时我们需要将构造函数定义在类外,这种constructor initializer list也依然是可以使用的。

一般来说,最好能使用in-class initializer,但如果编译器不支持,那我们必须这样声明。


2. 控制Class以及包裹Class

2.1 class和struct关键词

struct和class几乎是可以互换的,只是一种风格的不同。

唯一的区别是,默认情况下(没有声明public,private等),struct下的成员是public的,而class下的成员是private的。

至于编程风格上,如果我们的类中所有成员都是public的,那么我们用struct。如果有private成员,那我们用class。

2.2 友元(Friends)

将其他类或函数声明为友元(friends)后,该类或函数就能调用本类中的非public成员。

例如:

class friendClass{friend friendClass add(...);//一个类外函数friend anotherClass;//另一个类};
friends函数不受private或public影响,但是作为好的习惯,我们应该将他们放在一起,放在class中的最开始,或者最后。

另外,class内的友元函数的声明,只是表明一个权限(access),而不是一般意义上的声明。如果在类的外部我们需要使用这个友元函数,那么我们还需要再次声明这个函数!

一般来说,我们会将这些友元函数的声明和这个类放在同一个头文件中。

而如果是将一个类声明为友元,则可以在两个头文件中都包含一下另一个类的简单声明。(这个是我自己认为的)


3.类的其他特性

3.1 类成员

Type Member

在类的定义中,我们还可以定义type。type也会受到public和private的限制。

class testConst{public:using publicint = int;private:using privateint = int;};testConst::publicint a = 0;//ok!testConst::privateint b = 0;//error!

值得注意的是,和其他类成员不同,type member必须先定义,再使用!因此,一般讲typedef / type alias声明在类的最前面。

Inline Members

前面提到过,在类的内部完全定义好的函数默认是inline的。同时我们也可以显式要求某个函数是inline的。

我们可以在类的内部声明inline标示符,可以在类外部定义时声明inline标示符,也可以在两个地方都声明,效果是一样的。但是,C++ Primer建议,只在类的外部声明inline标示符。(猜测这是为了避免类的内部东西太多,毕竟inline对理解函数和类的作用不提供任何帮助)

基于和之前在《函数》一章我们提到过的同样的原因,inline函数应该和类的定义放在同一个头文件中,而不是放在一个源文件内。

Mutable Data Members

有时候我们希望类中的某个成员,即使在一个const object中,也能够被改变,这个成员就是mutable data member。我们使用mutable关键词来表明。

class testConst{public:mutable int a = 0;int b = 0;void testMutable() const{ a++; }//ok!void testMutable2() const{ b++; }//error!};

Initializers for Data Members of Class Type

我们提到了C++ 11中可以使用in-class initializer,之前我们只用了赋值操作符(=),我们还可以使用大括号({}),事实上,也只能用这两种方式。

一个使用大括号的例子:

class c{public:vector<int> a { 10 };};

事实上,我在这里遇到了很奇怪的现象。

首先,这个构造函数不允许接收多于两个参数,会提示重载函数不存在。例如:vector<int> a{1,2,3,4,5}似乎应该是很可行的,但是编译器会报错。
其次,构造函数将a理解为一个具有10个0的vector,而不是在一般情况下a是一个vector只有一个元素10.

我还在困惑之中。

另外值得注意的是,{}的初始化,其内部的变量类型必须和声明的变量类型完全符合,不能有任何转换,这个之前已经提过很多次了。

Returning *this from a const Member Function

如果一个类成员函数是const的,且我们希望他返回object本身,即返回*this,那么函数的返回值也必须是const!

class d{public:const d& get() const//ok!{return *this;}d& get2() const//error!{return *this;}};

Overloading Based on Const

和其他的overload函数一样,有时我们希望const和nonconst object获得不同的函数来执行。在类中,如果我们希望const object和nonconst object调用一个重载函数,可以有两个相应的函数来执行,那么应该如下:

class c{c& get(){}//nonconst 版本const c& get() const {}//const 版本};

3.2再看看友元函数

友元函数可以直接在类的内部直接定义,这样的函数默认是inline的。当然,你还需要在类的外部声明一下,才能在类的外部使用。

Friendship between classes

在A类中声明B类是它的友元,那么B类就能访问A类中任何成员。

class b{//可以访问a内任何成员};class a{friend class b;};
值得注意的是,友元不具有传递性!即如果B是A的友元,C是B的友元,那么C并不是A的友元!

Making A Member Function a Friend

我们也可以只将B类中的一个成员函数声明为A类的友元。例如:

class a{friend void b::get(a);};
然而,这样做需要我们仔细安排我们的程序结构,使得声明和定义之间的依赖关系得以确保。完成上面例子中的友元声明需要这么做:

1.声明a类,但不定义a。

2.首先定义b类,在b类中要声明,但不能定义get()函数。a必须在get()函数使用其内部的成员之前被定义。

3.定义a类,包括将get()声明为友元。

4.定义get()函数,这个函数现在可以使用a类中的成员了

感觉还是蛮麻烦的!!

Overloaded Functions and Friendship

对于重载函数,如果希望将它们都声明为友元,则需要将每一个函数都声明为友元,即不能只声明一个函数为友元,而期望其他重载函数都自动成为友元。

Friend Declarations and Scope

这个指的是之前提过很多次的概念,一个友元函数在类中被声明并不表示在类的外部我们可以使用这个友元函数。在类的外部,即使是在类成员函数的定义中,如果我们没有在类外声明这个友元函数,而直接调用它,那么就会出错。

简言之,在类的外部,我们也必须声明这个友元函数!


4. Class Scope

4.1 Scope and Members Defined outside the Class

这部分主要说的是,如果我们的类成员函数在类的外部被声明,那么在“总的类scope”(我自己起的名字哈哈)出现之前,如果出现了其他的类内的成员,那么必须特别声明,如果出现在其后,那么无需再次声明。

用例子来说明:

class c{private:using cint = int;void get1(cint);cint get2();};//下面这些类成员函数的定义中,get1和get2前的那个"c::"就是我说的"总的类scope",因为无论是哪一个类成员函数都得有这个c::void c::get1(cint){}//这里参数列表中的cint无需再次声明是c的成员,因为它在总的类scope之后,编译器能找到它c::cint c::get2(){ return 0; }//这里返回类型的cint需要声明是c的成员,因为在总的类scope之前,编译器不知道

4.2 Name Lookup Class Scope

这一节,从这一部分开始,我觉得是一些比较诡异,或者比较tricky的内容,是我们一般不会注意的,但是如果想打好C++基础的话则应该掌握。

一般来说,name lookup(就是我们使用一个变量,编译器会去寻找这个变量对应的是之前声明的哪一个)的过程是很直观的,如下:

1.在这个name被使用的block中查找,当然之查找name出现前的代码。

2.如果这个block没找到,就到enclosing scopes中查找。这个意思是,比如一个while中又有一个while,就会有两层大括号,如果最内层是我们的block,那么外层就是enclosing scopes(嵌套域)。

3.如果一直找不到,那么报错。

而在类的内部,这个过程有一些不一样,正如刚刚已经提到过的:

1.所有的成员的声明会被编译。

2.成员函数的定义只有在所有成员声明结束后,即整个类是可见的(entire class has been seen)后,才会被编译。

4.3 Name Lookup For Class Member Declarations

class内部的和外部同名的变量,内部会覆盖外部。

string a;class c{double get(){return a;}//ok!string get2(){return a;}//error!double a;};

4.4 Type Names Are Special

如果在类的外面已经typedef了一个类型,那么在类的内部不能对这个名称重定义同一个类型或其他类型。

但是我在VS2013中测试时,发现重定义不会编译出错。

尽管如此,这么做有很大问题。这是Primer上的一个例题:

typedef string TYPE;TYPE initVal();class ex{public:typedef double TYPE;TYPE setVal(TYPE);TYPE initVal();private:int val;};TYPE ex::setVal(TYPE parm)//在这里,第一个返回类型的TYPE是string!!而parm的类型是double!!所以编译报错!!{val = parm + initVal();return val;}
基于以上这个例子,我们应该尽量避免类内typedef和类外typedef重名。

4.5 Normal Block-Scope Name Lookup inside Member Definitions

在类成员函数内部:

1.在类成员函数的定义内寻找。

2.如果成员函数定义中没有找到,就在类中寻找,所有的类成员都会被考虑到。

3.如果依旧没有找到,就在类成员函数的定义前的所有范围内寻找。

4.6 三种类成员引用方法

在类成员函数中,如果我们想要使用类成员,可以用this,也可以用::。

void get(){int a = this->someint;int b = className::someint;//equivalent!}
另外,如果我们希望显式表明使用global的变量,可以这样:

void get(){int a = ::someint;//someint是global one!}

5.深入了解构造函数

5.1构造初始表(constructor initializer list)

初始化与赋值
回顾第一节我们介绍了用构造初始表来初始化类成员:
className(string &s,unsigned n,double p) : bookNo(s),units_sold(n),revenue(p*n)  {}
事实上我们当然也可以用赋值的方法来实现:
className (string &s,unsigned n,double p)  {    bookNo = s;    units_sold = n;    revenue = p*n;}
这种做法完全是合法的,但却显得臃肿。

有时你只能使用构造初始表(constructor initializer list)
有些情况下(其实很常见),你必须使用构造初始表,这也证明了为什么你应该尽可能使用构造初始表。
应该记住的是:使用构造初始表时,你做的操作是初始化(initialization),而使用赋值语句时,你是在赋值(assignment)。这里的初始化和赋值的操作和所有其他地方的C++代码一样。
因此,当成员是const或reference必须对他们初始化!而如果一个类并没有提供默认构造函数,那么它也只能被初始化!
class a{public:    int i;    const int ci;    int &ri;}a::a(int ii){    i=ii;//ok!    ci=ii;//error! const类型不能赋值    ri = i;//error! ri未被初始化};
而如果我们改用构造初始表则可以完成:
a::a(int ii):i(ii),ci(ii),ri(i){} //ok!
另外,如果两种情况都可以使用的话,使用构造初始表效率相对较高。因为赋值语句做了两件事:首先initialize,然后赋值;而构造初始表通过一步initialize完成。

成员初始化的顺序
值得注意的是,构造初始表的顺序并没有决定类成员被初始化的顺序!事实上,类成员初始化的顺序和类中这些成员定义的顺序相同,与构造初始表中的顺序无关!
一般情况下,这不是问题。但如果你的成员初始化之间存在着依赖关系,那就相当重要了。
最好的办法,就是让构造初始表和类中成员的定义顺序完全一样,这样你能一目了然。更重要的是,如果可能,尽量避免用一个类成员去初始化另一个类成员。

拥有默认参数的构造函数
记住,一个类只能拥有一个默认构造函数(显而易见),而默认构造函数的本质是:可以不接受任何参数的构造函数,就是默认构造函数。
因此,如果我们定义了如下构造函数:
a(int ii = 0){}
那么就不能再定义:
a() = default;
当然,也不能定义其他同类的构造函数。

5.2 委托构造函数(delegating constructor)

委托构造函数使用另一个和它在同一个类中的构造函数,来完成它自己的初始化。
class a{public:    a(int i,double j){}    a() : a(1,2.0){}    a(int i) : a(i,0){}    a(istream &is) : a(){read(is, *this);}};
对于委托构造函数,如果一个函数A委托了函数B来构造自己,那么调用函数A时,函数B的初始化列表以及函数B的函数主体都会被执行(虽然上面例子中函数主体都是空的)。

委托构造函数调用的顺序
class a{public:int i;a(){ cout << "1" << endl; }a(int i):a(){ cout << "2" << endl; }a(string s) :a(1){ cout << "3" << endl; }};
这是如果我们调用 a aa(s);
那么将输出: 1 2 3
这是很显而易见的。

避免自己委托自己
如果有:
a(double i) :a(i){}
而我们调用 a aa(1);
将会毫无疑问发生堆栈溢出,因为这是一个无穷自己调用自己的过程,需要避免。

5.3 默认构造函数的作用

这里讲的默认构造函数不仅仅指类中的,而是整个C++中很多我们甚至没有完全意识到的存在的默认构造函数。
默认构造函数在以下情况被自动执行:一个object是default状态,或者这个object被value initialized

default initialization发生在:
1.我们定义nonstatic变量或者array时,并且没有给予初始化。
2.一个类中使用synthesized default initializer(即类中的默认构造函数)
3.在初始构造表中,没有把所有类成员都包括进去。

value initialization发生在:
1.初始化array但提供的初始化元素少于array的长度。
2.定义一个local static object而没有提供初始化。
3.通过T()形式来显式调用value initialization。这里T是type的名字。(例如vector通过获得一个int来表明vector的长度,以此来初始化它的内部元素)

你总应该提供一个默认构造函数
如果你在定义类时,定义了非默认构造函数,那么你最好同时总提供一个默认构造函数!
class a{public:    a(string s){}};int main(){    a aa;//error!不存在a的默认构造函数!}
记住!当一个类已经有了至少一个构造函数以后,编译器就不会再自动为你补足默认构造函数!(VS2013测试)

默认构造函数的使用
下面这个例子很有意思:
class a{public:    int i=0;    a() = default;};<pre name="code" class="cpp">int main(){    a aa();    aa.i = 1;//error!!}
这里为什么会报错?原因在于我们自以为定义了一个名为aa的object,其实我们定义了一个名为aa的函数!它不需要任何参数,返回一个class a!
正确的方法是:
int main(){    a aa;    aa.i = 1;//ok!}
注意这里有一个括号的区别!

5.4 Implicit Class-Type Conversions

如果一个类存在一些构造函数,这些构造函数只有一个形参。那么当需要这个类时,我们可以使用这种类型的变量来替代这个类,这种构造函数也被叫做converting constructors。这么说很抽象,看个例子就一目了然:
class a{public:    int i;    a(int ii):i(ii){}};void testA(a aa){...}int main(){    testA(1);//ok!}
这里testA本该接收一个类型为a的变量,但是因为存在int到a的转换!所以调用int类型参数也可以!

只能经过一次class-type conversion
这种implicit class-type conversion只能经过一次class-type conversion。
例如:
class a{public:    a(string s){}};void testA(a aa){...}int main(){    string s = "1";    testA(s);//ok!    testA("1");//error!不存在const char [1]到a的转换}

再举一例:
class a{public:    a(vector<int> v){}};void testA(a aa){...}int main(){    vector<int> v = {1,2,3};    testA(v);//ok!    testA({1,2,3});//error!不存在initializer_list到a的转换}

但是,如果是double,char,int这类转换,或者const转换,并不是class-type转换,则可以有多次,例如:
class a{public:    int i;    a(int ii):i(ii){}};void testA(a aa){...}int main(){    double d = 1.0;    testA(d);//ok!}


implicit class-type conversion并不总那么好用
虽然这种转换看似方便,但有一点值得注意,那就是我们在使用了这个方法后,我们无法获得生成的那个类!因此这个类在被使用后就被丢弃了。
另外,这种类型转换也可能引起误操作,即我们并不真的想调用这个类,而是希望使用原来的内容。

使用explicit关键字来阻值implicit conversion
通过explicit关键字,我们可以阻止implicit conversion
class a{public:    int i;    explicit a(int ii):i(ii){}};void testA(a aa){...}int main(){    double d = 1.0;    testA(d);//error!}
explicit只对接受一个参数的构造函数有效!当一个构造函数需要多个参数时,一个类型转换是绝对不存在的,因此一个explicit也是没有必要的。
另外,explicit只在类的内部定义时可用,在类的外部定义构造函数时,explicit关键词会引起报错。

explicit构造函数只能被用于直接初始化
这点其实是很显而易见的,如果一个构造函数是explicit,那么我必须使用初始化的方式来定义一个类对象,而不能使用赋值的方式。
class a{public:    int i;    explicit a(int ii):i(ii){}};a aa(1);//ok!a aa = 1;//error!如果不是explicit,那么可以

显式调用构造函数来完成类型转换
上面介绍了一些implicit conversion,其实更好的做法是explicit conversion
例如:
class a{public:    int i;    explicit a(int ii):i(ii){}};void testA(a aa){...}testA(a(1));//ok!testA(static_cast<a>(1));//ok!
无论a的构造函数是explicit还是不是explicit,使用这种显式转换都是可行的,而且代码更加清晰易懂。

Library Classes with explicit Constructors
C++Primer中给出两例:
1.string constructor存在构造函数,只接受一个const char*类型,不是explicit的。
2.vector构造函数,只接受一个int来表示size,是explicit的。

5.5 Aggregate Class

一个aggregate class使得用户可以直接获取他的成员,并且有特殊的初始化语法。
一个aggregate class定义为:
1.所有的成员都是public
2.没有定义任何构造函数
3.没有in-class initializers
4.没有基类或虚函数(将在之后介绍)

我们可以通过braced list来初始化aggregate class的成员。
struct Data{    int ival;    string s;};<pre name="code" class="cpp">Data d1 {0,"1"};//ok
Data d1 = {0,"1"};//ok
甚至结构中有数组也可以:
struct Data{    int ival;    int a[5];};Data d1 {0,{1,2,3,4,5}};//okData d1 {0};//ok,数组被value initialized
另外要注意的是,初始化的顺序必须和aggregate class中定义的顺序一致。

显然,aggregate class并不那么常用,它至少有三个缺点:
1.所有成员必须都是public(你的class将会更加危险)
2.它把工作交给了用户,而不是class的创作者。当class稍微大一点后,这种初始化将会非常冗长。
3.一旦有一个成员增加或删除,整个初始化都要重新改变(超级大的缺点)

5.6 Literal Classes

之前在function一章中,我们提到,如果一个function的参数和return type都是constexpr类型,那么function也是一个literal type。
同样,一些class,也可以是literal type。一个literal type的class,可以拥有constexpr的成员函数。
一个aggregate class,如果所有的成员都是literal type,那么它就是literal type。
如果不是aggregate class,当满足以下条件时,也是literal type:
1.数据成员都是literal type
2.至少有一个constexpr构造函数
3.如果数据成员有in-class initializer,那么这些initializer必须是const expression。如果数据成员是其他的类,那么必须使用其他类的constexpr构造函数。
4.类的析构函数必须使用default difinition。

constexpr构造函数
尽管构造函数不能是const的,但是literal class中的构造函数可以是constexpr函数。
一个constexpr构造函数可以被声明为=default,或者被声明为deleted function(以后会介绍)。否则,constexpr构造函数必须同时满足构造函数(没有返回值)和constexpr函数(唯一可执行的(executable)语句是返回语句)的所有条件。因此,constexpr构造函数的函数主体一般是空的。
一个constexpr构造函数必须初始化所有的数据成员。这种初始化要么通过构造初始表完成,要么通过in-class initializer完成。
一个例子:
class Debug{public:    constexpr Debug(bool b=ture):hw(b),io(b),other(b){}    constexpr Debug(bool h,bool i,bool o):hw(h),io(i),other(o){}    constexpr bool any(){return hw||io||other;}private:    bool hw;    bool io;    bool other;};
constexpr构造函数一般用来省城一些objects,这些object常作为常量参数或constexpr函数的返回类型。
constexpr Debug io_sub(false,true,false);if(io_sub.any())//equivalent to if(true)...constexpr Debug prod(false);if(prod.any())//equivalent to if(false)...

6.静态类成员(static class members)

静态类成员的作用是:有时我们需要一个成员,和类的所有的object都有关系,而不仅仅是之和一个object有关系。

6.1定义static member

一个static member除了在关联程度上和其他成员不同,其他都一样。可以是public或private,可以是const,reference,array,class type,甚至可以是一个function。
class a{string s;double amout;static double rate;static double initRate();};
因为static member是独立于所有objects之外的,因此所有的class a的object都只有两个成员:s和amount。只存在一个rate,被所有class a的objects共享。
另外,static成员函数也是独立于所有objects之外的,这就导致它不能调用this指针!因此static 成员函数不能声明为const,不能调用nonstatic member,即不能显示或隐式调用任何会设计this指针的内容。

6.2 使用static member

我们可以直接用scope operater来调用

double r = a::rate;double r2 = a::initRate();
或者,虽然static member不属于任何object,我们还是可以通过object来调用它们。

a aa;double r = aa.rate;double r2 = aa.initRate();
类的内部,成员函数可以直接调用static member,而不需要scope operator

double a::calculate(){return 2*rate;}

6.3定义static member

当定义一个static成员函数时,我们既可以在类的内部定义,也可以在外部定义,形式和其他的函数定义完全一样。

不过注意的是,在类的内部我们要有static关键词,但是在类的外部不能出现。

另外,static member并不在哦我们创建类的时候被定义,因此类的构造函数无法初始化static member。我们也不能在类的内部对static member进行in-class initialize。相反的,我们必须在类的外部初始化static member。

正确的定义方法是:
double a::rate = 1.0;//okdouble a::rate = initRate();//ok
这里,即使initRate()是private也没关系,毕竟static member是类内成员,有权利调用类内任何成员。
不过我还是有一些困惑,这要是针对第二种通过static 成员函数来初始化的方法,我在VS2013中做了如下测试:
class a{    static int ii;    static int f(){return ii};};int a::ii = f();
完全可以运行,而且ii的输出的结果为0,总觉得非常诡异,还有待进一步了解。

6.4 in-class initialization of static data members

虽然刚刚说了static member不能有in-class initialization,但是如果这个static member是const integral type(int,char,short等,不包括double,string等),那么我们可以使用in-class initialization。而如果static member是constexpr的literal type,那我们必须使用in-class initialization!
这里C++ Primer讲了很多,我没有完全理解,对于上面的constexpr是否必须要in-class initialization,我也没法验证(VS2013不支持constexpr)。
但是有一点能够理解的是,C++ Primer建议即使提供了in-class initialization,最好在类的外部再次做声明。
class a{    static const int ii = 1;//const int可以有in-class initialization};const int a::ii;//在外部再次声明,不过不要重定义

6.5 static member可以有ordinary member不能操作的操作

static member可以拥有incomplete type(不完整的类型,比如做了声明,但没有定义的类)。
class a{static a a1;//ok,static member can have incomplete typea *a2;//ok,pointer member can have incomplete typea a3;//error! data members must have complete type!};

另一点是,static member可以作为类内函数的默认参数。
class a{static const int ii;int get(int i = ii){...}//ok!};
一般的数据成员不能作为默认参数,因为函数不知道从哪个object来获取那个数据成员。但是static member是一致的,因此可以作为默认参数。

0 0
原创粉丝点击