参考资料:
每个类都有自己的作用域,在类的作用域之外,普通的数据和函数成员只能由对象、引用或指针使用成员访问运算符访问,类型成员则通过作用域运算符访问。
对于定义在类外部的函数成员而言,一旦遇到类名,定义的剩余部分就是在类的作用域之内了,这就是为什么参数列表和函数体可以直接使用类的其他成员:
void Window_mgr::clear(ScreenIndex i){ // ScreanIndex是类Window_mgr中的类型成员
Screen &s = screens[i];
s.contents = string(s.height*s.width, ' ');
}
需要注意的是,由于返回类型出现在类名之前,所以它位于类的作用域之外,所以如果我们的返回类型是 ScreenIndex
,就必须写成 Window_mgr::ScreenIndex
。
一般的**名字查找(name lookup)**过程比较直接:
这里名字查找只考虑出现名字使用之前出现的声明
由于类的定义分两步处理:首先编译成员的声明,然后编译函数体。所以成员函数体可以使用类中的任何名字,而无需考虑这个名字是否在前面出现过。
需要注意的是,只有成员函数体具有特殊性,成员函数的参数列表和返回类型仍然遵循一般名字查找的规则:
using Money = double;
string bal;
class Account{
public:
Money balance() { return bal; } // 这里bal的类型是Money,Money是double的别名
private:
Money bal;
};
如果类成员使用了某个名字,这个名字代表某种类型,则类不能再重新定义这个名字。
using Money = double;
class Account{
public:
Money balance() { return bal; }
using Money = double; // 错误,Money不能重新定义,即使与原定义一致也不行
};
需要说明是,某些编译器会忽略这个约定(比如 visual studio )。
成员函数体内的名字按照如下方式查找:
// 最好不要隐藏同名成员,这里仅作演示
pos height;
void Screen::dummy_fcn(pos height){
cursor = width * height; // 参数中的height
cursor = width * this->height; // 类中的height
cursor = width * Screen::height; // 类中的height
cursor = width * ::height; // 全局变量height
}
class X{
void func(int);
};
int y;
void X::func(int i){
i = y; // y的声明已经出现过了,所以可以正常使用
}
如果没有在构造函数初始值列表中显式初始化成员,则该成员将在函数体之前执行默认初始化:
Sales_data::Sales_data(const string &s, unsigned cnt, double price){
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
上面这段代码相当于先默认初始化成员,然后再给成员重新赋值。
如果成员是 const
、引用或某种没有定义默认构造函数的类类型,则必须将其初始化。
一旦构造函数体开始执行,初始化就完成了。
在构造函数初始值列表中,每个成员只能出现一次。构造函数初始值列表只说明用于初始化成员的值,而没有限定初始化的顺序。成员的初始化顺与它们在类定义中的出现顺序一致:
class X {
int i;
int j;
public:
// 想用未定义的j初始化i,再用val初始化j
X(int val):j(val), i(j) { }
};
class Sales_data {
public:
Sales_data(string s = "") :bookNo(s) { }
};
如果一个构造函数为所有参数都提供了默认实参,则它实质上也定义了默认构造函数。
C++11 新标准允许我们使用委托构造函数(delegating constructor)。委托构造函数使用本类中其他构造函数执行自己的初始化过程:
class Sales_data {
public:
Sales_data(string s, unsigned cnt, double price) :
bookNo(s), units_sold(cnt), revenue(cnt *price) { }
// 委托构造函数
Sales_data() :Sales_data("", 0, 0) { }
Sales_data(string s) :Sales_data(s, 0, 0) { }
// 先委托默认构造函数,然后默认构造函数在委托三参数构造函数
// 执行完受委托的构造函数后再执行委托函数的函数体
Sales_data(istream &is) :Sales_data() { read(is, *this); }
};
当类类型的对象被默认初始化或值初始化时自动执行默认构造函数。
Sales_data obj1(); // 声明了一个函数
Sales_data obj2; // 使用默认构造函数
如果某个类类型定义了能通过一个实参调用的构造函数,则它实际上定了转换成此类类型的隐式转换机制,这种构造函数称作转换构造函数(converting constructor):
class Sales_data {
public:
// 转换构造函数
Sales_data(string s) :bookNo(s), units_sold(0), revenue(0) { }
};
void func(Sales_data x) { ; }
string null_book = "9-999-99999-9";
func(null_book); // 正确
可以理解为用这个参数隐式构造一个临时量。
func("9-999-99999-9"); // 错误,需要先将const char*转换成string,再由string转换成Sales_data
以上面的代码为例,并不是所有的 string
都是我们需要的 bookNo
。
我们可以将构造函数声明为 explicit
来阻止隐式类型转换:
class Sales_data {
public:
// 阻止隐式类型转换
explicit Sales_data(string s) :bookNo(s), units_sold(0), revenue(0) { }
};
void func(Sales_data x) { ; }
string null_book = "9-999-99999-9";
func(null_book); // 错误
只能在类内声明构造函数时使用 explicit
,不能在类外使用 explicit
。
explicit
构造函数只能用于直接初始化Sales_data item1(null_book); // 正确
Sales_data item2 = null_book; // 错误
func(static_cast<Sales_data>(null_book)); // 正确
当一个类满足如下条件时,它是聚合类(aggregate class):
public
。virtual
函数。聚合类允许用户直接访问其成员,并且具有特殊的初始化形式:
struct Data {
int ival;
string s;
};
Data val1{ 0, "hello" };
如上面的代码所示,我们可以用一个花括号括起来的初始值列表来构造聚合类,初始值的顺序必须与类中定义的顺序相同。如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。
数据成员都是字面值类型的聚合类是字面值常量类;如果一个类不是聚合类,但它符合下述要求,则它也是字面值常量类:
constexpr
构造函数。constexpr
构造函数。constexpr
构造函数前面提到,字面值常量类至少有一个 constexpr
构造函数。构造函数可以声明成 = default
。否则, constexpr
必须既满足构造函数的要求(不需要 return
语句),又满足 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; }
private:
bool hw;
bool io;
bool other;
};
constexpr
构造函数必须初始化所有数据成员(有默认初始值的成员可以不显式初始化),初始值或者使用constexpr
构造函数,或者是一条常量表达式。
有时候,类需要一些成员与类本身直接相关,而不是与类的各个对象保持关联。
我们可以在成员的声明前加上关键字 static
使之与类关联在一起:
class Account {
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
};
类的静态成员存在于任何对象之外,所以每个 Account
对象将包含两个数据成员 owner
、amount
。interestRate
对象只有一个,并被所有对象共享。静态函数成员也不与任何对象绑定在一起,所以不包含 this
指针,也不能声明成 const
。
可以使用作用域运算符直接访问静态成员:
double r;
r = Account::rate();
尽管静态成员不属于类的某个对象,但我们仍然可以通过类的对象、引用或指针来访问静态成员。
成员函数不通过作用域运算符也能直接使用静态成员:
class Account {
public:
void calculate() { amount += amount * interestRate; }
private:
static double interestRate;
}
在类外部定义静态成员时,不能出现 static
关键字,static
只能出现在类内部的声明语句。
由于静态数据成员不属于类的任何一个对象,所以它们不是在创建类的对象时被定义的,也不是由构造函数初始化的。
一般来说,我们通常在任何函数外部定义和初始化静态成员:
double Account::interestRate = initRate();
由于见到 Account
后就知道当前处在类的作用域,所以后面调用 initRate()
不需要作用域运算符;同函数成员在类外部定义一样,interestRate
的定义也可以访问类中的私有成员。
前面提到,类的静态成员不应该在类内初始化。但我们可以为静态成员提供 const
整数初始值(似乎没有这个要求?),但是要求静态成员必须是字面值常量类型 constexpr
且初始值必须是常量表达式。
class Account {
private:
static constexpr int period = 30;
double daily_tbl[period];
};
尽管一个常量静态数据成员已经在类内部初始化了,我们通常还是要在类外部定义一下这个成员,此时不能再指定初始值:
constexpr int Account::period;
静态数据成员可以是不完全类型,比如它本身所属的类类型:
class X{
public:
static X mem1; // 正确
X mem2; // 错误
};
此外,我们可以使用静态成员作为函数成员的默认实参。