7.4 类的作用域
在类的作用域之外,使用成员访问运算符来访问普通的数据和函数成员。对类类型成员则使用作用域运算符访问。不论那种情况,跟在运算符之后的名字都必须是对应类的成员。
void Window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
//一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。结果就是,我们可以直接使用类的其他成员就按而无需再次授权了。
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s)
{
screen.push_back(s);
return screen.size() - 1;
}
因为返回类型出现在函数名之前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。所以返回类型必须指明它是那个类的成员。
7.4.1 名字查找与类的作用域
名字查找过程:
1.首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
2.如果没找到,继续查找外层作用域
3.如果最终没有找到匹配的声明,则程序报错。
类的定义分两步:
首先,编译成员的声明。
直到类全部可见后才编译函数体。
用于类成员声明的名字查找
类型名要特殊处理:
一般来说内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过,然而在类中,如果成员使用了外层作用域中的名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; } //使用外层作用域的Money
private:
typedef double Money; //错误,不能重新定义Money
Money bal;
};
尽管重新定义类型名字是一种错误的行为,但是编译器并不为此负责,一些编译器仍将顺利通过这样的代码,而忽略代码有错的事实。(codeblocks有警告)
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
成员定义中的普通块作用域的名字查找:
1.首先,在成员函数内查找该名字的声明,和前面一样,只有在函数使用之前出现的声明才被考虑。
2.如果在成员函数内没有找到,则在类内继续查找,这是类的所有成员都可以别考虑。
3.如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。
一般来说,不建议使用其他成员的名字作为某个成员函数的参数。
int height;
class Screen{
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height)
{
cursor = width * height;
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
建议写法://不要把成员名字作为参数或其他局部变量使用
void dummy_fcn(pos ht)
{
cursor = width * height;
}
显示地通过作用域运算符来进行请求访问外层作用域中的名字。
void dummy_fcn(pos ht)
{
cursor = width * ::height;
}
7.5.构造函数再探
Sales_data(const string &s, unsigned n, double p) : //初始化数据成员
bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data::Sales_data(const string &s, unsigned cnt, double price)//对数据成员进行赋值操作
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
7.5.1构造函数初始值列表
string foo = "hello world"; //定义并初始化
string bar; //莫惹初始化成空string对象
bar = "Hello World!"; //为bar赋一个新值
如果成员是const、引用、或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些函数成员提供初值
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii)
{
i = ii; //正确
ci = ii; //错误,不能给const赋值
ri =i; //错误,ri没被初始化
}
初始值列表是唯一初始化机会:
ConstRef(int ii) : i(ii), ci(ii), ri(ii) { }
建议:使用构造函数初始值
在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。
处理效率问题外更重要的是,一些数据成员必须被初始化。建议养成使用构造函数初始值的习惯,这样能避免某些意想不到的编译错误,特别是遇到有的类含有需要构造函数初始值的成员时。
成员初始化的顺序:
#include <iostream>
#include <string>
using namespace std;
class X {
friend ostream& operator << (ostream& out, const X & item);
int i;
int j;
public:
X(int val) :j(val), i(j) { } //未定义的,i在j之前被初始化,试图使用未定义的j初始化i;
};
ostream& operator << (ostream& out, const X & item)
{
out << item.i << " " << item.j << endl;
return out;
}
int main()
{
X a(2);
cout << a << endl;//输出结果为i= -858993460 j = 2
return 0;
}
程序先用未定义的j初始化i,随后用参数val初始化j,先定义的成员先用其括号内的值初始化,i先用i括号内的值初始化,然后才是j用j括号内的值初始化。
最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。最好用构造函数的参数作为成员的初始值
成员初始化的顺序与他们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,一次类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
建议如下形式:
X(int val, int val) :j(val), i(val) { }
默认实参和构造函数
Sales_data(string s = "") :bookNo(s) { } //提供了一个默认实参,当使用默认实参时,作用域默认构造函数等价
上面这条构造函数和下面两条构造函数等价,且两种方式不能同时出现
Sales_data() = default;
Sales_data(const string &s) :bookNo(s) { }
如果提供cin作为接受istream&参数的构造函数的默认实参,其声明语句为:
Sales_data(std::istream &is = cin)
7.5.3 默认构造函数的作用
Sales_data obj(); //声明了一个函数而非对象
Sales_data obj2; //obj2是一个对象而非函数
一般来说,都应该为类构建一个默认构造函数。
对于编译器合成的默认构造函数来说,类类型的成员执行各自所属类的默认构造函数,内置类型和复合类型的成员只对定义在全局作用域中的对象执行初始化。
7.5.4
隐式的类类型转换
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
string null_book = "9-99-9";
item.combine(null_book); //用参数列表临时生成一个对象
item.combine(cin);
copy(vec.cbegin(), vec.cend(), ostream_iterator<int> (cout, " "));//临时生成了一个ostream_iterator<int>对象
但是编译器只允许一步类类型转换
item.combine("9-99-9"); //错误,需要用户定义的两种转换(1)把"9-99-9"转换成string;
//(2)再把这个(临时的)string转换成(临时的)Sales_datad对象
必须转换成以下形式:
item.combine(string("9-99-9"));
item.combine (Sales_data("9-99-9"));
但是我们可以在声明构造函数时加上关键字explicit,即可阻止这种构造函数定义的隐式转换
explicit Sales_data(const std::string &s) : bookNo(s) { }
explicit Sales_data(std::istream &in) { in >> *this; }关键字explicit只对一个实参的构造函数有效。需要多个参数的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit.只能在类内声明构造函数时使用explicit关键字,在类外部定义不应重复。
explicit Sales_data::Sales_data(std::istream &in)
{ in >> *this; }
用了explicit虽然不能隐式转换,但是可以使用这样的构造函数显示的强制进行转换
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));
接受一个string参数的Sales_data构造函数应该是explicit的。否则,编译器就有可能自动把一个string 对象转换成Sales_data对象,这种做法显得有些随意,某些时候会与程序员的初衷相违背。使用explicit的优点是避免因隐式类类型转换而带来意想不到的错误。缺点是当用户的确需要这样的类类型转换时,不得不使用略显繁琐的方式来实现。
一个const 成员函数如果以引用的形式返回*this,那么他的返回类型就是常量引用。
4.11.3.显示转换
强制类型转换(cast):虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
static静态的 dynamic动态的
void* 指针可以存放任意类型对象的地址,但不能对该指针进行解引用
cast_name<type>(expression) ,type是目标类型,expression是要转化的值,cast_name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种,通常为static_cast.
7.5.5聚合类(aggregate class)
聚合类是的用户可以直接访问其成员,因为其所有成员都是public的。聚合类应满足的条件是:
1.所有成员都是public的。
2.没有定义任何构造函数。
3.没有类内初始值。
4.没有基类,也没有virtual函数(关于这部分知识将在15张详细介绍。
下面的类是一个聚合类
class Sales_data
{
public:
string bookNo;
unsigned units_sold;
double revenue ;
};
或写成
struct Sales_data
{
string bookNo;
unsigned units_sold;
double revenue ;
};
我们可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。
Sales_data item = { "123", 2, 3.5 };//列表内初始值的顺序必须与成员声明的顺序一致
如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量(初始值设定项太多)。
显示的初始化类的对象的成员存在三个明显的缺点:
1.要求类的所有成员都是public的
2.将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错。
3.添加或删除一个成员后,所有的初始化语句都需要更新。
7.5.6 字面值常量类
constexpr函数是指能用于常量表式的函数。constexpr函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句。constexpr函数体内也可以包含其他一些不执行任何操作的语句,比如,空语句,类型别名,using 声明等。
这是一个完整的字面值常量常量类程序:
#include <iostream>
using namespace std;
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_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { other = b; }
private:
bool hw;
bool io;
bool other;
};
int main()
{
constexpr Debug io_sub(false, true, false); //用Debug的类型来声明io_sub,且分别用初始值初始化数据成员
if (io_sub.any())
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); //用Debug的类型来声明prod,且调用第一个constexpr构造函数
if (prod.any())
cerr << "print an erroe message" << endl;
return 0;
}
数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:
1.数据成员必须是字面值类型。
2.类必须至少含有一个constexpr构造函数。
3.如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
4.类必须使用析构函数的默认定义,该成员负责销毁类的对象。
字面值常量类必须至少提供一个constexpr构造函数,且constexpr构造函数体一般是空的。