c++ primer 第五版学习笔记-第七章 类

本文为转载,出处:https://blog.csdn.net/libin1105/article/details/48664019

                                https://blog.csdn.net/sunhero2010/article/details/49798749

                                https://blog.csdn.net/fnoi2014xtx/article/details/78152884


7.1 定义抽象数据类型

 

1.类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。类的接口包括用户所能执行的操作:类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的借口和实现的分离。

    成员函数的声明必须在类的内部,它的定义可以在类的内部也可以在类的外部,作为接口组成部分的非成员函数,例如:add,read,print等,声明和定义都在类的外部。

    定义在类内部的函数,是隐式的inline函数。

    在类的外部定义成员函数时候,成员函数的定义必须与它的声明匹配,如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名。

    double Sales_data::avg_price() const{...}

 

类由类成员组成。类成员包括属性,字段,成员函数,构造函数,析构函数等组成。

    类设计应该遵从抽象封装性。

    类抽象性指对于类的使用者来说只需知道类接口即可使用类功能。类的具体实现由设计者负责。即使某个功能发生了变更但由于使用者是以接口方式调用类所以用户代码无需做任何修改。

    类封装性指类用户只需知道类的功能无需了解具体实现。实现代码对用户来说不可见。

    C++类没有访问级别限限制,定义类时不能用public 或 private 做修饰。类成员有访问级别,可以定义 public protect private

 {
    public:      // 类成员只能声明不允许定义 替换成string name("tom") 会产生编译错误, 数据成员初始化工作在构造函数中执行
      string  name;            
      // 给类定义别名类型成员 index 由于别名要在外部访问所以一定要定义在 public
      typedef std::string::size_type index;
      // 内部定义的函数,等价于inline
      char get() const { return contents[cursor]; }
      // 内部声明一个成员函数(无定义),且函数是内联的inline表示在编译时该声明会被替换成定义语句
      inline char get(index ht, index wd) const;
      // 内部声明一个成员函数(无定义)
     index get_cursor() const;
            // ...
};

// 定义类 Screen 的成员函数 get 具体实现 
char Screen::get(index r, index c) const 
{
     index row = r * width;    // compute the row location
 
     return contents[row + c]; // offset by c to fetch specified character
} 
// 定义类 Screen 的成员函数 get_cursor 具体实现,且是内联的
inline Screen::index Screen::get_cursor() const 
{ 
     return cursor;
}

注意:类的inline修饰符可以放在类内部申明也可以放在外部定义。一般放在内部声明便于理解。  

    类定义完毕后一定要加上封号结束符 ;。

    类数据成员只允许声明不允许定义;

    可以声明类而不定义它。成为前向声明又叫不完全类,这样的类无法定义实例也无法使用成员。一般用来处理类相互依赖的情况。定义了类就能定义类对象:myclass obj; 一定要注意不能是 myclass obj() ; 类对象定义时会分配内存空间,每个类都有自己的空间相互间不受影响。

2.成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this

this是一个类的常量指针,在成员函数内部,this存储该对象的地址,我们不能改变this存储的地址

struct book
{
    string no;
    string isbn()const{return this->no;}//等价于 return no;
}Book1;

Book1.isbn()
//当我们调用这个函数时,this限定为Book1的地址了
//当我们需要使用当前this对应的元素时,用*this返回当前元素的引用

 

3.C++允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。

对于成员函数,若定义在类的内部,默认内联,否则默认不内联 
在机器运行中,对于Book1.isbn()的调用,我们隐式的传递了一个this指针,伪代码表示如下 
string isbn(book *const this) 
若我们需要限定this为底层const,即函数内不修改成员的值,我们在使用const成员函数,伪代码表示如下 
string isbn(const book *const this)

    类对象包含一个 this 指针指向自身(当前的实例对象)且无法更改指针指向。在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的 const 指针。既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。

    基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const可以重载一个函数。

class mycls
{
    public:
        mycls(){}; // 想要定义 const mycls a; 必须要显示定义默认构造函数
        mycls &Get(){ return *this; };
        const mycls &Get() const { return *this; }; // 想如果const函数返回this引用或指针; 必须要返回const指针或引用,因为无法用const对象(this)初始化非const对象。
 
};
 
const mycls b;
mycls b1 = c.Get(); // 调用const版Get函数
const mycls b2 = c.Get(); // 调用const版Get函数
// b1 b2 定义时会调用类的拷贝函数。b1,b2是Get返回值的副本,b1还会将常量副本转变成变量
 
mycls &b3 = c.Get(); // 错误,不能用 const &mycls 初始化 &mycls (指针或者引用类型不能用常量初始化变量)
const mycls &b4 = c.Get();
 
 
mycls a;
mycls a1 = a.Get(); // 调用非const版Get函数
const mycls a2 = a.Get(); // 调用非const版Get函数

由此可见调用那个版本和调用对象是否const有关系,const对象会调用const版本,非const对象会调用非const版本。

引用网上的总结:

     成员函数具有const重载时,类的const对象将调用类的const版本成员函数,类的非const对象将调用非const版本成员函数。

     如果只有const成员函数,类的非const对象也可以调用const成员函数。                          ——这个思路来描述很囧。下同。

     如果只有非const成员函数,类的const对象…额,不能调用非const成员函数。                ——其实跟上一句的意思是一样的:const对象只能调用它的const成员函数。

    总的来说,就是当我们调用一个成员函数时,编译器会先检查函数是否有const重载,如果有,将根据对象的const属性来决定应该调用哪一个函数。如果没有const重载,只此一家,那当然就调用这一个了。这时编译器亦要检查函数是不是没有const属性而调用函数的对象又有const属性,若如此,亦无法通过编译。   

   还有一点非常重要,想要定义类的const对象必须显示定义对应构造函数,无法依赖系统自动分配的构造函数。

istream &read(istream &is,book &rhs)
{
    is>>rhs.no;
    return is;//可以返回读入是否成功的值
}
ostream &print(ostream &os,book &rhs)
{
    os<

4.每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。构造函数的名字和类名相同,构造函数没有返回类型,不同构造函数必须在参数类型和参数数量上有差别。构造函数不能声明为const。在const对象的构造过程中可以向其写值。

struct book
{
    book() = default;//显式默认构造函数
    book(const string &s):No(s){}//构造函数初始值列表中未出现变量将以默认构造函数方式初始化
    book(const string &s,int p):No(s),price(p){}
    book(istream &);
    string No;
    int price=0;
    //支持先出现构造函数,后出现变量定义
}
book::book(istream &is)//定义book类的成员,名字是book,同名,所以是构造函数。
{
    read(is,*this);//从is中读取一条交易,存入this对象中
}

5.如果我们的类没有显式的定义构造函数,那么编译器会为我们隐式的定义一个默认构造函数。默认构造函数无需实参,编译器创建的构造函数又被称为合成的默认构造函数。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:

如果存在类的初始值,用它来初始化成员

否则,默认初始化该成员

 

构造函数是特殊的成员函数。在类对象定义时被调用。 不能通过定义的类对象调用构造函数,构造函数可以定义多个或者说构造函数允许重载。

    如果没有定义任何构造函数,系统就会给类分配一个无参的默认构造函数,类只要定义了一个构造函数,编译器也不会再生成默认构造函数。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数

    定义类对象时不能写成 Sales_item myobj(); 编译器会理解成:一个返回 Sales_item 类型叫 myobj的函数声明。 正确写法是去掉后面的括号。

    构造函数后面不允许定义成 const,这样定义会产生语法错误: Sales_item() const {};

    构造函数在执行时会做类数据成员的初始化工作。从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

    不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。

class mycls
{
    public:
        mycls()
        {
           age = 12; name = "tom";
         };
 
        mycls(int i):age(i) 
        {
           age = 12 + i; name = "tom";
         };
 
    private:
        int age;
        string name;
};

    mycls obj1 ;使用无参构造函数,虽然构造函数并没有显示初始化数据成员但类类型name还是会被初始化成默认值name初始化为"" age未初始化(其值是个随机数),初始化后构造函数重新赋值,最终age=12, ame = "tom" ;

    mycls obj2(4) ; 用构造函数参数初始化 age = 4, name = "",构造函数重新赋值,最终age=16, name = "tom" ;

    如果数据成员是自定义类类型,如果不显示初始化则类一定要有默认构造函数否则编译错误,成员被初始化的次序就是定义成员的次序。第一个成员首先被初始化,然后是第二个,依次类推。

    默认情况下可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。

class mycls
{
    public:
        int i;
        mycls(int i){ };
        explicit mycls(string s){ };
 
};
    mycls obj(2) ; 也可以这样使用这个构造函数 mycls obj = 2; 这里做了一个类型转换,但是这样的写法很不直观。
    可以通过将构造函数声明为 explicit,(可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。)来防止在需要隐式转换的上下文中使用构造函数:mycls obj("tom"), 无法用 mycls obj = "tom" 因为转换被禁止,通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。 
    explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。


6.C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中=default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。

book() = default;

7.2 访问控制与封装

 

1.C++语言中,我们使用访问说明符加强类的封装性:

定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口

定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

 作为接口的一部分,构造函数和部分成员函数(isbn(),combine)紧跟在public后面,而数据成员和作为实现部分的函数紧跟在private后面。

struct和class可以定义类,区别是默认访问权限不一样。如果是struct,顺序是先Public再private。class相反。

2.类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。友元声明只能出现在类定义的内部。


std::istream &read(std::istream &,book &);
//友元仅仅指定访问权限,并未声明,需在之前进行声明
class book
{
    friend std::istream &read(std::istream& ,book &);
    friend class BOOK;//友元类,注意友元关系不具有传递性
    friend void BOK::clear(int);//只声明另一个类中的某个成员函数为友元
    public:

    private:
}

对于B,若想让A的成员函数f为友元,应当使用以下顺序

class B;//声明B
class A
{
    public:
        void f(B &);//声明f
};
class B
{
    friend void A::f(B &);//声明友元
    private: 
        int cnt;
};
void A::f(B &b){printf("%d\n",b.cnt);}//定义f

注意,我们可以在友元函数声明时定义该函数,但是,此时定义的函数并没有声明,对调用不可见

class X
{
    friend void f(){/*定义*/}
    X(){f();}//错误,未声明
}
f();//错误,未声明
void f();//声明f()
f();//正确

// 友元类
class me
{
    friend class he;
    
    private:
        int i;
        string s;
};
 
class he
{
    public:
        void show(me &it)
        {
            cout << it.i << it.s << endl;
        };
};

类he是me的友元类,所以he中可以访问me的私有成员i和s;

 将类成员作为另一个类的友元函数情况比较复杂,需要用到前面讲过的前向声明(两个类之间有互相依赖关系)

// 类成员友元
class me; // 先要前向声明类
 
class he  // 友元类需要目标类做参数由于目标类已声明所以可以使用类引用或者指针--show(me &it)方法中的参数
{
    public:
        void show(me &it);
};
 
class me // 目标类需要声明类的的成员show作为自己的友元函数,he在上面做了成员声明所以成员show(me &it)可用
{
    friend void he::show(me &it);
    
    private:
        int i;
        string s;
};
 
void he::show(me &it) // 友元方法中使用目标类私有成员,目标类上一步定义了私有成员因此这里成员可用
{
    cout << it.i << it.s << endl;
};


 

7.3 类的其他特性

 

1.mutable修改一个可变数据成员,永远不会是const,即使它是const对象的成员。因此,一个const成员函数可以改变一个可变成员的值。

 

2.一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

 

(1)定义类型成员

class screen
{
    public:
        typedef std::string::size_type pos;
    //using pos=std::string::size_type;
    //要求:先定义,后使用    
    private:
        pos cur=0;//当前光标位置
        pos height=0,width=0;
        std::string contents;
}

(2)可变数据成员 
const成员函数中也可以对它进行修改

class screen
{
    public:
        void some_member()const;
    private
        mutable int cnt;
};
void screen::some_member()const{++cnt;}//cnt是可变的,即使some_member是const的,也可以修改它

(3)this的使用方法

class screen
{
    public:
        screen &set(char c){contents[cur]=c;return *this;}
        screen &move(int pos){cur=pos;return *this;}
        //若为const成员函数,则返回的是const screen &类型
        //即我们不能使用以下的调用方式
    ...
}MyScreen;

MyScreen.move(10).set('*');

解决方法之一是重载const函数

(4)类的声明

class screen;//这是前向声明,我们已知它是类类型,但是不知道它有什么成员,所以它是不完全类型

我们可以定义指向不完全类型的指针,但是不能创建不完全类型的对象。screen *x//对   screen x//错

不完全类型只能在以下情况使用:定义该类的指针或引用,声明(非定义)以该类作为参数或返回类型的函数


7.4 类的作用域

 

1.名字查找的过程:

首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明

如果没有找到,继续查找外层作用域

如果最终没有找到匹配的声明,则程序报错

typedef double Money
string bal;
class Account
{
    public:
        Money balance(){return bal;}
    private:
        Money bal;
}
当编译器看到声明 balance 时,首先在 Account 中找 Money 的声明,找不到去外层,然后对于 return bal; 由于类的成员函数在所有定义后被处理,所以返回的是 Money bal  

我们可以重定义变量,但我们不能重定义类型


2.类的定义分两步处理

首先,编译成员的声明

直到类全部可见后才编译函数体

 

3.成员函数中使用的名字按照如下方式解析

首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。

如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。

如果类内也没有找到该名字的声明,在成员函数定义之前的作用域内继续查找。

 

7.5 构造函数再探

 

1.成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数的初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

成员类型初始化

class DY
{
    const int x;
    const int &z;   
    //对于必须初始化的引用与常量类型,我们支持这样初始化
    DY(int y):x(y),z(x){}
};

初始化顺序由定义顺序决定,即

class DY
{
    int i;
    int j;
    public: 
        DY(int val):j(val),i(j){}//错误,i的值未定义
};

2.一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些职责委托给了其他构造函数。

struct book
{
    int x,y;
    book(int _x,int _y):x(_x),y(_y){/*代码1*/}
    book(int _x):book(_x,0){/*代码2*/}
    //调用第二个构造函数,先执行代码1,后执行代码2
};
//非委托构造函数 
Sales_data(std::string s,unsigned cnt,double price):bookNo(s),units_sold(cnt),revenue(cnt*price){}
//委托构造函数 
Sales_data():Sales_data("",0,0){}
Sales_data(std::string s):Sales_data(s,0,0){}
Sales_data(std::istream &is):Sales_data(){read(is,*this);}

如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名后的空括号对:

sala_data  obj;//obj是一个默认初始化的对象。

默认构造函数的作用:

struct B
{
    int y;
    B(int x):y(x){}
};
struct A
{
    B b;
};
A a;//错误,建立A的默认构造函数,缺少B::B()

3. 在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为 explicit 加以阻止。explicit只能用于直接初始化,不能用于拷贝初始化。

关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit的。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。

struct book
{
    string isbn;
    book()=default;
    book(string &s):isbn(s){}
    void add(book &rhs)
    {
        ...
    }
}Bk;

Bk.add(string("123456789"));//正确
Bk.add(book("123456789"));//正确
Bk.add("123456789");//错误,不允许两步隐式转化

//抑制隐式转化的方法如下
struct book
{
    string isbn;
    book()=default;
    explicit book(string &s):isbn(s){}
    //explicit函数只能用于直接初始化,且只能用于一个实参的函数
    void add(book &rhs)
    {
        ...
    }
}Bk;

4.聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:

所有成员都是public

没有定义任何构造函数

没有类内初始值

没有基类,没有virtual函数

struct data{int a,b,c;};

可以如下方式初始化

data D{1,2,3};//初始值与声明顺序一样,若初始值列表较少靠后的值初始化

5.判断字面值常量类:

1.是聚合类

2.若不是聚合类,符合以下要求也可以:

    1.数据成员都必须是字面值类型

            2.类必须至少含有一个constexpr函数

            3.如果一个数据成员含有类内初始值,则内置成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。

            4.类必须使用析构函数的默认定义,该成员负责销毁类的对象。

7.6 类的静态成员

 

1.静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const,而且我们也不能在static函数体内使用this指针。这一限制既使用于this的显式使用,也对调用非静态成员的隐式使用有效。

有时需要类的一些成员与类本身相关,而不是与类的各对象相关,且对于该成员值的变动需要所有对象共享

class Account
{
    public:
        static double rate(){return interest;}
        static void rate(double);
    private:
        static double interest;
        static double initRate();
};

类的静态成员存在于所有对象之外,被所有对象共享,对象内不包含静态成员的数据 
静态成员函数类似,不包含this指针,不能声明成const函数

//使用方法
Account::rate();//1.使用作用域运算符直接访问

Account usr1,*usr=&usr1;
usr1.rate();usr->rate();//2.通过类的对象、引用和指针来访问

在类的外部定义静态成员,不能重复使用static 
静态成员存在类的对象之外,所以不由构造函数初始化,除了静态常量成员外,不能在类的内部初始化,而是在类的外部定义与初始化静态成员,静态成员定义在任何函数之外

struct A
{
    static int u;
    static int v;
    int a;
    private:
        static int init();
};
int A::u=10;//定义并初始化
int A::v=init();//尽管init()是private,但是我们可以用它来初始化

类内初始化要求我们要将静态对象定义为常量

struct A
{
    static constexpr int rate=10;
    int u;
};
//不需要类外定义了

          静态成员可以用于以下几种非静态成员不能使用的场景:静态成员可以是不完全类型; 静态成员可以是它所属的类类型 ;可以作为默认实参


    不同于其他语言的访问方式,静态成员既可以通过类型访问:myclass::staticname()  也可以通过类对象(对象,指针或者引用)访问:obj.staticname()。  

    一般来说类数据成员在类定义体内不能初始化化,但有个特例 const static 数据成员就可以在类的定义体中进行初始化 。

    类非 static 数据成员在类体内声明,必须要在类体外定义。

class me
{    
    public:
        void show()
        {
            cout << i << j << endl;
        };
 
    private:
        static int i;
        const static int j = 1;
};
 
int me::i = 1; //这一步不能少,否则编译器检查到show()方法中使用i类体外又没有定义会产生编译错误int me::j; //这一步不能少,但只是定义不能再赋值
    需要强调const static 体内初始化但体外定义也不能少,但是如果体外不作定义在定义类对象时会产生编译异常。

    最后补充一点关于类成员函数的重载。函数重载不但可以用参数类型和参数个数不同来重载,还可以通过const修饰变量来实现函数重载,即函数名称、参数个数、参数类别都一样,唯一的区别在于变量是否为const修饰。用 const做重载依据有两种类型:const参数,const函数:

class A
{
  public:
    A() {}
    void func(int *a) //相当于void func(int *a, A *this)
    {
        std::cout << "_func_int_ptr_" << std::endl; 
    }
    void func(const int *a) //相当于void func(const int *a, A *this)
    {
        std::cout << "_func_const_int_ptr_" << std::endl;
    }
    void func(int *a) const //相当于void func(int *a, const A *this)
    {
        std::cout << "_const_func_int_ptr_" << std::endl;
    }
    void func(const int *a) const //相当于void func(const int *a, const A *this)
    {
        std::cout << "_const_func_const_int_ptr_" << std::endl;
    }
};
 
int main(int argc, char* argv[])
{
    A a;
    int nValue = 3;
    const int nValueCnst = 3;
    a.func(&nValue);
    a.func(&nValueCnst);
 
    const A aa;
    aa.func(&nValue);
    aa.func(&nValueCnst);
    return 0;
}
 
其输出为:
_func_int_ptr_
_func_const_int_ptr_
_const_func_int_ptr_
_const_func_const_int_ptr_

从这里可以看出,通过const修饰一个变量可以实现同名称函数的重载。另外,一个类的非const对象可以调用其const函数(如果只定义了const函数版本,非const对象就可以调用const成员函数)。但const 对象无法调用非  const 函数(非const函数可能会修改 this 而 this 是 const对象,有潜在BUG)。

    总结起来,可以初始化的情况有如下几个地方:
    1. 类型为const 且 static 的整型变量可以在定义时直接初始化值(只能用赋值初始化不能用直接初始化) 也可以在体外。

    2. 普通const常量(不包含第一种

情况)必须要在构造函数初始化列表中初始化值。

    3. 只要有static修饰,必须要在类定义体外定义并给值(第一种情况时也需要这么做,不过只能定义不能再给值) static数据不属于任何对象所以不能出现在构造函数初始化列表。

    4. 普通的变量可以在构造函数的内部,通过赋值方式进行。当然这样效率不高。

    5. 数组成员不能在初始化列表里初始化的。只能自动调用数组的无参构造函数(可以在构造函数内操作数组)。  

class obj
{
    public:
       int a;
       const int b;
       static int c;
       static const d = 1; // 体内定义时不能用直接初始化给值
       static const e;
    
       obj():a(0),b(0){};
};
 
int obj::c = 2 ; // 体外定义(不能出现statci关键字)
const int obj::d ; // 体外定义,d已经在体内给只所以只需定义不能给值(const 必须)
const int obj::e = 1 ; // 定义并给值


你可能感兴趣的:(c++)