7.3 类的其他特性
- 如果我们提供一个构造函数,所以编译器将不会自动生成默认构造函数,我们的类需要默认构造函数,必须显式地把它声明出来.用
=default
告诉编译器为我们合成默认的构造函数 - **可变数据成员:**我们希望能修改类的某个数据成员,即使是在一个
const
成员函数内.可以通过在变量的声明加入mutable
关键字做到这一点
class Screen {
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const {
++access_ctr;
}
- 返回*this的成员函数: 一个const成员函数如果以引用的形式返回
*this
,那么它的返回类型将是常量引用
class Screen {
public:
const Screen& display() const {
return *this;
}
Screen& set() {
res = "fun_default";
return *this;
}
public:
string res = "default";
};
定义一对类X 和Y,其中X 包含一个指向 Y 的指针,而Y 包含一个类型为 X 的对象。
class Y;
class X{
Y* y = nullptr;
};
class Y{
X x;
};
- **类之间的友元关系:**如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员;友元关系不存在传递性
7.4 类的作用域
- 名字查找:编译器处理完类中的全部声明后才会处理成员函数的定义(可以简化类代码的组织方式)
- 类型名要特殊处理
typedef double Money;
class Account {
public:
Money balance() { return bal; }
private:
typedef double Money;
Money bal;
};
- 类作用域之后,在外围的作用域中查找
int height;
class Screen {
public:
typedef string::size_type pos;
void dummy_fcn(pos height) {
cursor = width * height; // 在函数作用域表达式中用到的名字,因此height指的是参数声明
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
- 类作用域之后,在外围的作用域中查找
void Screen::dummy_fcn(pos height) {
cursor = width * ::height; // 全局height
}
如果我们把第256页Screen类的pos的typedef放在类的最后一行会发生什么情况?
在 dummy_fcn(pos height) 函数中会出现 未定义的标识符pos。
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
解释下面代码的含义,说明其中的Type和initVal分别使用了哪个定义。如果代码存在错误,尝试修改它。
typedef string Type;
Type initVal();
class Exercise {
public:
typedef double Type;
Type setVal(Type);
Type initVal();
private:
int val;
};
Type Exercise::setVal(Type parm) {
val = parm + initVal();
return val;
}
如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
7.5 构造函数再探
- 构造函数的初始值有时必不可少: 如果成员是
const
或者引用的话,必须將其初始化 - 成员初始化顺序
下面的初始值是错误的,请找出问题所在并尝试修改它。
struct X {
X (int i, int j): base(i), rem(base % j) {}
int rem, base;
};
struct X {
X (int i, int j): base(i), rem(base % j) {}
int base, rem;
};
- 委托构造函数:类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中的另外一个构造函数匹配
class Sales_data
{
public:
Sales_data(const std::string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(n*p)
{
std::cout << "Sales_data(const std::string&, unsigned, double)" << std::endl;
}
// 其余构造函数全部委托给另一个构造函数
Sales_data() : Sales_data("", 0, 0.0f)
{
std::cout << "Sales_data()" << std::endl;
}
Sales_data(const std::string &s) : Sales_data(s, 0, 0.0f)
{
std::cout << "Sales_data(const std::string&)" << std::endl;
}
private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
- 默认构造函数作用:
class NoDefault {
public:
NoDefault(const std::string &);
};
struct A {
NoDefault my_mem;
};
A a; //不能为A合成构造函数
**-使用默认构造函数:**对于C++
的新手来说有一种常犯的错误,它们试图以如下的形式声明一个用默认构造函数初始化的对象
Sales_data obj(); // 错误,声明了一个函数而非对象
Sales_data obj2; // 正确,obj2是一个对象而非函数
(a) 一个类必须至少提供一个构造函数。
(b) 默认构造函数是参数列表为空的构造函数。
© 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
(d) 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
(a) 不正确。如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,并称之为合成的默认构造函数。
(b) 不完全正确。为每个参数都提供了默认值的构造函数也是默认构造函数。
© 不正确。哪怕没有意义的值也需要初始化。
(d) 不正确。只有当一个类没有定义任何构造函数的时候,编译器才会生成一个默认构造函数。
隐式的类类型转换
- 抑制构造函数定义的隐式转换: 我们可以通过将构造函数声明为explicit
加以阻止
- explicit 构造函数只能用于直接初始化
Sales_data item1(null_book); // 正确,直接初始化
Sales_data item2 = null_book;
vector 将其单参数的构造函数定义成 explicit 的,而string则不是,你觉得原因何在?
假如我们有一个这样的函数:
int getSize(const std::vector&);
如果vector没有将单参数构造函数定义成 explicit 的,我们就可以这样调用:
getSize(34);
很明显这样调用会让人困惑,函数实际上会初始化一个拥有34个元素的vector的临时量,然后返回34。但是这样没有任何意义。而 string 则不同,string 的单参数构造函数的参数是 const char * ,因此凡是在需要用到 string 的地方都可以用 const char * 来代替(字面值就是 const char *)。如:
void print(std::string);
print(“hello world”);
- 聚合类: 所有成员都是
public
, 没有定义任何构造函数,没有类内初始值,没有基类,也没virtual
函数
class Data {
public:
int ival;
string s;
};