C++_Primer07.class

定义抽象数据类型

使用结构体设计一个抽象数据类型,用于存放书籍的信息,包括ISBN编号、销量、总销售收入,并提供一些操作这些信息的方法,使其能够实现以下功能:

Sales_data total;
if (read(cin, total)) {
    Sales_data trans;
    while (read(cin, trans)) {
        if (total.isbn() == trans.isbn()) {
            total.combine(trans);
        } else {
            print(cout, total) << endl;
            total = trans;
        }
    }
    print(cout, total) << endl;
} else {
    cerr << "No data?!" << endl;
}

利用结构体定义 Sales_data 类型:

struct Sales_data {
    // 成员函数
    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    // 成员变量
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
// 在结构体外面声明其他接口,非成员函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream& print(std::ostream, const Sales_data&);
std::istream& read(std::istream, Sales_dcata&);

定义在类或结构体内部的函数是隐式的 inline 函数

编译器在编译时首先编译成员的声明,然后才是成员函数体,所以成员函数体可以随意使用类的其他成员而无需考虑定义的次序

add, read, print 等辅助函数非成员函数,并不属于类本身

所有的成员都必须在类的内部声明,但成员函数体可以定义在类外

通常将函数的声明和定义分开,但一般写在同一个头文件中

this

当调用 total.isbn() 函数时,实际上是在替某个对象调用它,即 this,它就是当前正在使用的对象;编译器负责把 total 的地址传递给 isbn 的隐式形参 this,可以等价地认为编译器将该调用重写为类似以下的形式:

Sales_data::isbn(&total)

const 成员函数

std::string isbn() const {return bookNo;};

以上函数定义中,在参数列表之后有个 const 关键字,它是作用在 this 上的,作用是限制 this,不允许函数体内修改 this 的内容。

而 this 本身是一个常量指针,不允许其指向其他对象,则以上函数定义可以做如下理解:

std::string Sales_data::isbn(const Sales_data* const this) {return this->bookNo;};

像这样使用 const 的成员函数被称为 常量成员函数 (const member function);其中的 this 只能读取成员变量,而不能修改。

类外定义成员函数

double Sales_data::avg_price() const {
    if (units_sold) {
        return revenue / units_sold;
    } else {
        return 0;
    }
}

:: 为作用域运算符,表明说明 avg_price 函数是属于 Sales_data 作用域内的。即使定义在类外部,也仍然能使用成员变量 revenueunits_sold

返回 this 对象的函数

// 这个函数设计的初衷是模仿 += 运算符
// 一般来说当我们定义的函数类似于某个内置运算符时,应令该函数的行为尽量模仿这个运算符,
// 内置的赋值运算符把它的左侧运算对象当成左值返回,所以这里返回了 this 指向的对象
// rhs: right hand Sales_data
Sales_data& Sales_data::combine(const Sales_data& rhs) {
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return * this;
}

read 和 print 函数

// 输入交易信息: ISBN号,出售总数,售出价格
istream& read(istream& is, Sales_data& item) {
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}
ostream& print(ostream& os, Sales_data& item) {
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.ave_price();
    return os;
}

IO 类属于不能被拷贝的类型,所以我们只能通过引用来传递他们
而且读写操作会改变流的内容,所以这两个函数都是普通引用,而非对常量的引用

add 函数

Sales_data add(const Sales_data& lhs, const Sales_data& rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

构造函数

每个类初始化时可以定义多个构造函数,如果没有定义构造函数,则编译器会合成一个默认构造函数(synthesized default constructor),用来初始化成员:

  • 如果存在类内的初始值,用它来初始化成员
  • 否则,默认初始化该成员

如果类内包含有类型或复合类型成员的类应该在类内初始化这些成员,或定义一个自己的默认构造函数。否则在创建类的对象时可能得到未定义的值
如果类中包含一个其他类类型的成员,且这个成员的类型没有默认构造函数,则编译器将无法默认初始化该成员,创建类的对象时就可能得到未定义的值

构造函数重载例:

struct Sales_data {
    Sales_data() = default;         // 要求编译器生成默认构造函数,也可自定义
    Sales_data(const std::string& s): bookNo(s) {}
    Sales_data(const std::string& s, unsigned n, double p):
        bookNo(s), units_sold(n), revenue(p*n) {}
    Sales_data(std::istream&);      // 在类外定义
    // 其他成员
    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

Sales_data::Sales_data(std::istream& is){
    read(is, *this);
}

构造函数中用来初始化成员的部分叫做初始值列表:bookNo(s), units_sold(n), revenue(p*n)

拷贝、赋值和析构

  • 拷贝
    初始化变量、以值的方式传递或返回一个对象时
  • 赋值
    赋值运算符
  • 析构
    一个局部对象会在创建它的块结束时被销毁

当类需要分配类对象之外的资源时,编译器合成的版本常常会失效;管理动态内存的类通常不能依赖于合成版本。
不过很多需要动态内存的类能使用 vector 对象或 string 对象管理必要的存储空间。使用 vector 或 string 类能避免分配和释放内存带来的复杂性。

如果类包含 vector 或 string 成员,则其拷贝、赋值和销毁的合成版本能够正常工作

// 赋值语句
total = trans;
// 等价于:
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;

访问控制和封装

class Sales_data{
public:
    Sales_data() = default;
    Sales_data(const std::string& s): bookNo(s) {}
    Sales_data(const std::string& s, unsigned n, revenue(p*n)):
            bookNo(s), units_sold(n), revenue(p*n) {}
    Sales_data(std::istream&);
    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
private:
    double avg_price() const
        { return units_sold ? revenue/units_sold : 0; }
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

class 和 struct 关键字都可以用来定义类,唯一的区别是访问权限不同。
使用 struct 时,定义在第一个访问说明符之前的成员是 public 的;而 class 则相反,这些成员都是 private 的。
处于统一编程风格的考虑,当我们希望定义的类的所有成员是 public 的时,使用 struct;反之,使用 class 。

友元

当非成员方法需要访问类的私有成员时,可以令这些函数成为类的友元(friend),需要在类的定义中增加一条以 friend 开始的函数声明即可:

class Sales_data{
    friend Sales_data add(const Sales_data&, const Sales_data&);
    friend std::istream& read(std::istream&, Sales_data&);
    friend std::ostream& print(std::ostream&, const Saels_data&);
public:
    Sales_data() = default;
    Sales_data(const std::string& s): bookNo(s) {}
    Sales_data(const std::string& s, unsigned n, revenue(p*n)):
            bookNo(s), units_sold(n), revenue(p*n) {}
    Sales_data(std::istream&);
    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
private:
    double avg_price() const
        { return units_sold ? revenue/units_sold : 0; }
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
// 非成员函数的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream& print(std::ostream, const Sales_data&);
std::istream& read(std::istream, Sales_dcata&);

友元声明只能出现在类定义的内部,位置不限。友元不是类的成员,不受它所在区域访问控制级别的约束。但通常在类的开始或结束前的位置集中声明友元。

友元的声明仅仅制定了访问权限,并非函数声明

类还可以把其他的类定义成友元,也可以把其他类的成员函数定义成友元。另外,友元函数可以定义在类的内部,这样的函数是隐式内联的。

类的其他特性

类型成员

// 定义窗口类,用字符串表示窗口,包含一个用于保存 Screen 内容的String成员
// 和三个 string::size_type 类型的成员,表示光标位置和屏幕的高和宽
class Screen {
public:
    typedef std::string::size_type pos;
    Screen() = default;
    Screen(pos ht, pos wd, char c): height(ht), width(wd), contents(ht*wd, c) {}
    char get() const { return contents[cursor]; }
    inline char get(pos ht, pos wd) const;
    Screen& move(pos r, pos c);
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

类中的类型成员必须先定义后使用,所以类型成员通常出现在类开始的地方。

内联特性

我们可以在类的内部把 inline 作为声明的一部分显式地声名成员函数,同样,也可以在类外用 inline 关键字修饰函数的定义:

inline
Screen& Screen::move(pos r, pos c){
    pos row = r * width;
    cursor = row + c;
    return * this;
}
char Screen::get(pos r, pos c) const {
    pos row = r * width;
    return contents[row + c];
}

可变数据成员

mutable data member

有时我们希望能修改类的某个数据成员,即使是一个const 成员函数内。可以通过在变量的声明中加入 mutable 关键字:

// 给 Screen 增加一个可变成员
class Screen {
public:
    void some_member() const;
private:
    mutable size_t access_ctr;
};
void Screen::some_member() const {
    ++ access_ctr;
    ...
}

返回 *this 的成员函数

class Screen {
public:
    Screen& set(char);
    Screen& set(pos, pos, char);
};
inline Screen& Screen::set(char c) {
    contents[cursor] = c;
    return *this;
}
inline Screen& Screen::set(pos row, pos col, char ch) {
    contents[row*width + col] = ch;
    return *this;
}

// 使用链式操作:
myScreen.move(4, 0).set('#');

如果上面程序中返回的不是 Screen& 而是 Screen,则其行为大不相同:

Screen temp = myScreen.move(4, 0);  // 拷贝赋值操作
temp.set('#');                      // 不会改变 myScreen 中的值

上述代码中的 move 函数返回的是对象的副本,若对副本进行链式操作,则对原对象不会有任何作用

如果某个函数是 const 成员函数并且返回了 *this,则它不能嵌入到一组动作的序列中去:

// display 是 Screen 类的常量成员函数
Screen myScreen;
mysCreen.display(cout).set('*');        // 调用set将引发错误

若想在一组动作序列中使用常量成员函数,可以私有化常量成员函数,然后用一个公有成员函数调用它:

// 根据对象是否是 const 重载了 display 函数
class Screen {
public:
    Screen& display(std::ostream& os) {
        do_display(os);
        return *this;
    }
    const Screen& display(std::ostream& os) const {
        do_display(os);
        return *this;
    }
private:
    void do_display(std::ostream& os) const {
        os << contents;
    }
};

do_display() 是在类内部定义的,默认具有内联特性,所以没有任何额外开销
而且这样做避免了重复代码

类类型

初始化类的对象

Sales_data item1;
class Sales_data item2;     // 与上一条语句等价

上述两种使用方法等价,其中第二种方式是从 C 语言继承来的

类的声明

class Screen;

这种声明有时称为前向声明(forward declaration),它向程序引入了名字 Screen 并指明 Screen 是一种类类型。类在它声明之后定义之前是一个不完全类型(incomplete type),只能在非常有限的情况下使用不完全类型:可以定义指向这种类型的指针或引用,也可以声明以不完全类型作为参数或返回类型的函数

只有当类全部完成后类才算被定义,所以类的成员类型不能是类自己,但可以是指向自身类型的引用或指针:

class Link_screen {
    Screen window;
    Link_screen* next;
    Link_screen* prev;
};

把类作为友元

类还可以把其他的类定义成友元,也可以把其他类的成员函数定义成友元。另外,友元函数可以定义在类的内部,这样的函数是隐式内联的:

// Window_mgr 是用来管理窗口的管理器
// clear 是用来重置窗口的函数
class Screen {
    // 将 Window_mgr 指定为 Screen 的友元后,Screen 的所有成员对于 Window_mgr  就都变成可见的了
    friend class Window_mgr;
    ...
};
class Window_mgr {
public:
    using ScreenIndex = std::vector<Screen>::size_type;
    void clear(ScreenIndex);
private:
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i) {
    screen& s = screen[i];
    s.contents = string(s.height * s.width, ' ');
}

友元不具有传递性。即,如果 Window_mgr 有它自己的友元,则这些友元并不能理所当然的具有访问 Screen 的特权。

成员函数作为友元

// 只为 clear 提供访问权限
class Screen {
    friend void Window_mgr::clear(ScreenIndex);
};

要想让某个成员函数作为友元,必须按以下方式设计程序:

  • 首先定义 Window_mgr 类,声明 clear 函数,但不能定义它。在 clear 使用 Screen 成员之前必须先声明 Screen
  • 然后定义 Screen,其中包括对 clear 的友元声明
  • 最后定义 clear,此时它才可以使用 Screen 的成员

例:

struct X {
    friend void f() {}          // 友元函数可以定义在类的内部
    X() { f(); }                // 错误:f 还没有被声明
    void g();
    void h();
};
void X::g() { return f(); }     // 错误:f还没有被声明
void f();                       // 声明 f()
void X::h() { return f(); }     // 正确:现在 f 的声明在作用域中存在了

类的作用域

不同成员的访问方法:

Screen::pos ht = 24, wd = 80;   // 类类型成员的访问方式
Screen scr(ht, wd, ' ');
Screen* p = &scr;
char c = scr.get();
c = p->get();

名字查找

  • 首先在名字所在的块中查找声明语句,只考虑在名字的使用之前出现的声明
  • 如果没找到,继续查找外层作用域
  • 如果最终没找到匹配的声明,则程序报错

对于类来说,有一点小的区别:

  • 首先编译成员的声明
  • 知道类全部可见后才编译函数体

这种两阶段的处理方式只适用于成员函数中使用的名字。而类类型(成员函数的返回类型和参数类型)则必须确保使用前可见。

类型名要特殊处理

一般来说,内层作用域可以重新定义外层作用域中的名字,即使改名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而改名字代表一种类型,则类不能在之后重新定义该名字:

typedef double Money;
class Account {
public:
    Money balence() { return bal; }     // 使用外层作用域的 Money
private:
    typedef double Money;               // 错误:不能重新定义 Money
    Money bal;
}

有些编译器仍然能顺利编译这样的代码
正确的做法是在类的开始处定义类型

隐藏名字的查找

如果块作用域中有和外层作用域重名的名字,则外层作用域被隐藏。如果想调用外层作用域的名字,可以用以下方法:

// 类中的成员被隐藏时
void Screen::dummy_fcn(pos height) {
    cursor = width * this->height;      // 使用成员变量 height,而不是局部参数 height
}

// 全局变量被隐藏时(不建议的写法)
void Screen::dummy_fcn(pos height) {
    cursor = width * ::height;          // 调用全局变量 height
}

练习35:

#include <string>

using namespace std;

typedef string Type;
Type initVal();

class Exercise {
    public:
        typedef double Type;
        Type setVal(Type);
        Type initVal();
    private:
        int val;
};
// 定义在外部的成员在使用类类型时要写明作用域
Exercise::Type Exercise::setVal(Exercise::Type param) {
    val = param + initVal();
    return val;
}
Exercise::Type Exercise::initVal() {
    val = 2;
    return val;
}

int main(int argc, char** argv){
    Exercise ex;

    return 0;
}

构造函数再探

构造函数初始值列表

sales_data {
public:
    Sales_data() = default;
    Sales_data(const string& s, unsigned cnt, double price);
private:
    string bookNo;
    unsigned units_sold;
    double revenue;
}

对于第二个构造函数,有两种写法:

// 初始化
Sales_data::Sales_data(const string& s, unsigned cnt, double price):
    bookNo(s), units_sold(cnt), revenue(cnt*price) {}
// 赋值操作
Sales_data::Sales_data(const string& s, unsigned cnt, double price) {
    bookNo = s;
    units_sold = cnt;
    revenue = cnt * price;
}

第一种写法是初始化,第二种是赋值操作,虽然在这个例子中得到的结果相同,但是他们有很大的区别。比如成员中有 const 类型或引用类型,则必须用初始化的方式,因为 const 成员和引用类型的成员是不可赋值的:

class ConstRef {
public:
    ConstRef(int ii);
private:
    int i;
    const int ci;
    int& ri;
}
// 错误:ci 和 ri 必须被初始化
ConstRef::ConstRef(int ii) {
    i = ii;                     // 正确
    ci = ii;                    // 错误:不能给 const 赋值
    ri = i;                     // 错误:ri 没有被初始化
}
// 正确的写法:
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(ii) {}

关于初始化顺序,构造函数对初始化的顺序没什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了:

class X {
    int i;
    int j;
public:
    // 错误:未定义的:i 在 j 之前被初始化
    X(int val): j(val), i(j) {}
};

上例中,编译器会先初始化i,试图使用未定义的值j初始化i

最好使构造函数初始值的顺序与成员声明的顺序保持一致。并避免使用某些成员初始化其他成员

委托构造函数

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

class Sales_data{
public:
    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); }
}

默认构造函数的作用

当对象被默认初始化或值初始化时自动执行默认构造函数。所以类必须包含一个默认构造函数以便在以下情况下使用。

默认初始化情形:

  • 在块作用域内不使用任何初始值定义一个非静态变量或数组时
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时
  • 当类类型的成员没有在构造函数初始值列表中显式地初始化时

值初始化的情形:

  • 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时
  • 不使用初始值定义一个局部静态变量时
  • 使用形如T( )的表达式显式地请求值初始化时

练习 7.43:
假定有一个类名为NoDefault的类,它有一个接受int的构造函数,但是没有默认构造函数。定义类c,c有一个NoDefault类型的成员,定义c的默认构造函数。

class NoDefault {
public:
    NoDefault(int val): value(val) {}
private:
    int value;
}

class C {
public:
    C(): nd(5) {}
}
private:
    NoDefault nd;

隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们称这种构造函数为转换构造函数(converting constructor)

只允许一步类类型转换

Sales_data 类中的combine函数接受一个 Sales_data 对象的引用,而 Sales_data 类其中一个构造函数接受一个 string 对象

// 接收一个 string 参数的构造函数
Sales_data(const std::string& s): bookNo(s) {}
// 接受一个 Sales_data 对象引用作为参数的函数
Sales_data& combine(const Sales_data&);

调用 combine 函数时发生类型转换:

// 错误:需要用户定义的两种转换:
// “9-999-99999-9” 转换成 string(临时)
// 把 string 转换成 Sales_data
item.combine("9-999-99999-9")

// 正确
item.combine(string("9-999-99999-9"))
item.combine(Sales_data("9-999-99999-9"))

类类型转换并不总是有效

class Sales_data {
public:
    Sales_data() = default;
    Sales_data(const std::string &s): bookNo(s) {}
    Sales_data(std::istream&);
}

item.combine(cin);

Sales_data 的其中一个构造函数可以把 cin 隐式地转换成 Sales_data,当调用combine函数时,会创建了一个临时 Sales_data 对象,然后将其传递给 combine 了。然而实际上,在 combine 完成后就不能再访问它了,所以这个转换是没有意义的。

抑制构造函数定义的隐式转换

用 explicit 阻止隐式转换的发生:

class Sales_data {
public:
    Sales_data() = default;
    explicit Sales_data(const std::string &s): bookNo(s) {}
    explicit Sales_data(std::istream&);
}

此时,以下调用将无法通过编译:

item.combine(null_book);
item.combine(cin);

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

explicit 构造函数只能用于直接初始化

Sales_data item1(null_book);    // 正确
Sales_data item2 = null_book;   // 错误,不能将 explicit 构造函数用于拷贝形式的初始化

也可以显式地进行转换:

item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));

练习7.51
vector将其单参数的构造函数定义成explicit的,而string则不是,你觉得原因何在?

Such as a function like that:

int getSize(const std::vector<int>&);

if vector has not defined its single-argument constructor as explicit. we can use the function like:

getSize(34);

What is this mean? It's very confused.

But the std::string is different. In ordinary, we use std::string to replace const char * (the C language). so when we call a function like that:

void setYourName(std::string); // declaration.
setYourName("pezy"); // just fine.

it is very natural.

聚合类

aggregate class

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

  • 所有成员都是 public 的
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有 virtual 函数
struct Data {
    int ival;
    string s;
};

Data val1 = {0, "Anna"};

显式地初始化类的对象成员缺点:

  • 要求类的所有成员都是 public 的
  • 将正确初始化每个对象的每个成员的重任交给了用户,使初始化过程冗长乏味且易出错
  • 添加、删除一个成员或更改成员顺序后,所有初始化语句都需要更新

字面值常量类

数据成员都是字面值类型(constexpr 修饰)的聚合类是字面值常量类。
符合以下要求的非聚合类,也是字面值常量类:

  • 数据成员都必须是字面值类型
  • 类必须至少含有一个 constexpr 构造函数
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象

constexpr 构造函数

构造函数不能是 const 的,但字面值常量类的构造函数可以是 constexpr 函数。
一个字面值常量类必须至少提供一个 constexpr 构造函数。

constexpr 构造函数可以声明成 =default 的形式,或是删除函数的形式。否则 constexpr 构造函数就必须既符合构造函数的要求(不能包含返回语句),又要符合 constexpr 函数的要求(意味着它能拥有的唯一可执行语句就是返回语句),所以 constexpr 构造函数一般来说应该是空的:

class Debug {
public:
    constexpr Debug(bool b = true): 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; }
    void set_hw(bool b) { hw = b; }
    void set_io(bool b) { io = b; }
    void set_other(bool b) { other = b; }
private:
    bool hw;        // 硬件错误
    bool io;        // IO 错误
    bool other;     // 其他错误
}

constexpr 构造函数必须初始化所有数据成员,初始值或者使用 constexpr 构造函数,或者是一条常量表达式。

constexpr 构造函数用于生成 constexpr 对象以及 constexpr 函数的参数或返回类型:

constexpr Debug io_sub(false, true, true);
if (io_sub.any())
    cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false);
if (prod.any())
    cerr << "print an error message" << endl;

类的静态成员

静态成员的类内初始化

通常情况,类的静态成员不应该在类的内部初始化。然而可以为静态成员提供 const 整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的 constexpr. 初始值必须是常量表达式:

class Account {
public:
    static double rate() { return interestRate; }
    static void rate(double);
private:
    static constexpr int period = 30;
    double daily_tbl[period];
}

静态成员适用的场景

  • 静态成员的类型可以是不完全类型
    • 静态成员的类型可以就是它所属的类类型
  • 可以使用静态成员作为普通成员的默认实参
class Bar {
public:
    //
private:
    static Bar mem;     // 正确,静态成员可以是不完全类型
    Bar* mem2;          // 正确,指针成员可以是不完全类型
    Bar mem3;           // 错误,数据成员必须是完全类型
}
class Screen {
public:
    Screen& clear(char = background);
private:
    static const char bkground;
}

小结

类有两项基本功能:

  • 数据抽象,即定义数据成员和函数成员的能力
  • 封装,即保护类的成员不被随意访问的能力

类可以将其他类或函数设为友元,这样他们就能访问类的非公有成员了

类可以定义一种特殊成员函数:构造函数,其作用是控制初始化对象的方式。构造函数可以重载,构造函数应该使用构造函数初始值列表来初始化所有数据成员。

类还能定义静态成员。一个可变成员永远不会是 const,即使在 const 成员函数内也能修改它的值;一个静态成员可以是函数也可以是数据,静态成员存在于所有对象之外。

posted @ 2022-02-28 22:31  keep-minding  阅读(59)  评论(0)    收藏  举报