Reference:The C++ Programming Language 4th edition (Bjarne Stroustrup)
1.explicit构造函数
单参数的构造函数会默认作为参数类型到此类类型隐式类型转换,在许多情况下这种隐式的类型转换很有可能导致错误
幸运的是,我们可以用explicit关键字显示声明构造函数(并不必须是单参数)不用作隐式的类型转换
struct X {
explicit X();
explicit X(int,int);
};
X x1 = {}; // error : implicit
X x2 = {1,2}; // error : implicit
X x3 {}; // OK: explicit
X x4 {1,2}; // OK: explicit
int f(X);
int i1 = f({}); // error : implicit
int i2 = f({1,2}); // error : implicit
int i3 = f(X{}); // OK: explicit
int i4 = f(X{1,2}); // OK: explicit
Good Practice: 默认将单参数的构造函数声明为explicit,除非有强力的需求不这么做。
2. 类内初始化
当存在多个构造函数时, 构造函数的默认参数可能会造成冗余,此时可以对数据成员使用初始化设定项(initializer)
class Date {
int d {today.d};
int m {today.m};
int y {today.y};
public:
Date(int, int, int); // day, month, year
Date(int, int); // day, month, today’s year
Date(int); // day, today’s month and year
Date(); // default Date: today
Date(const char∗); // date in string representation
// ...
3. 类内定义函数
类内定义的函数会被认为是内联函数(inline),通常小型的,使用频率高,几乎不会修改的函数会被定义在类内。
class Date {
public:
void add_month(int n) { m+=n; } // increment the Date’s m
// ...
private:
int d, m, y;
};
4. 可变性(Mutability)
(1) 常成员函数
在函数的声明和定义中都要在参数列表后加const,即const是函数类型的一部分
常对象只能调用常成员函数,非常(non-const)对象可以调用常成员函数和非常成员函数
void Date::add_year(int n);
void f(Date& d, const Date& cd)
{
int i = d.year(); // OK
d.add_year(1); // OK
int j = cd.year(); // OK
cd.add_year(1); // error : cannot change value of a const Date
}
(2) 物理不变性和逻辑不变性(physical and logical constness)
Logical constness: to a user, the function appears not to change the state of its object, but some detail that the user cannot directly observe is updated.
逻辑不变性:对于用户来说,一个函数似乎没有修改其对象的状态,但一些用户不能直接观察的细节得到了更新。
class Date {
public:
// ...
string string_rep() const; // string representation
private:
bool cache_valid;
string cache;
void compute_cache_value(); // fill cache
// ...
};
例如Date内储存的年月日是整形,现在string_rep()要返回字符串格式的日期,重复的将字符串转化为日期可能是一个昂贵的操作,所以一个可行的解决方案是用cache储存字符串格式的日期,当调用string_rep()时返回cahce。从用户的角度,string_rep()并没有改变Date数据,所以它明显应该是const函数,但cache和cache_valid却有时会改变,这个问题可以野蛮的用const_cast解决,但下节将介绍一个更优雅的方法。
(3) mutable
我们可以把类的成员声明为mutable,这意味着const成员函数也能修改他们。
class Date {
public:
// ...
string string_rep() const; // string representation
private:
mutable bool cache_valid;
mutable string cache;
void compute_cache_value() const; // fill (mutable) cache
// ...
};
于是现在string_rep的实现就显而易见了
string Date::string_rep() const
{
if (!cache_valid) {
compute_cache_value();
cache_valid = true;
}
return cache;
}
(4) 间接可变性
声明mutable变量适用于小对象里的小部分数据,当情况很复杂时,更好的办法是把这些数据分离到另外的对象,进行间接存取
上面的例子将变为
struct cache {
bool valid;
string rep;
};
class Date {
public:
// ...
string string_rep() const; // string representation
private:
cache∗ c; // initialize inconstr uctor
void compute_cache_value() const; // fill what cache refers to
// ...
};
string Date::string_rep() const
{
if (!c−>valid) {
compute_cache_value();
c−>valid = true;
}
return c−>rep;
}
const并不会作用于通过指针和引用存取的成员对象,即仅指针的指向不变,指针指向的内容可变。
5. 自引用(Self-Reference)
对于一组相关的更新操作,返回被更新对象的引用往往非常有用,因为这使各个操作可以连接起来。
void f(Date& d)
{
// ...
d.add_day(1).add_month(1).add_year(1);
// ...
}
在所有非static的成员函数中,关键字
this指向调用此成员函数的对象。对于类X,this的类型是X*。不过一般认为this是右值,即它不能被取址或者赋值。在const成员函数中,this的类型是const X*。this的绝大部分使用时隐式的,所有非static成员函数依赖于隐式的使用this来获取与其对应的对象(默认前缀this->)。
一个常见的显示使用this的例子是链表操作:
struct Link {
Link∗ pre;
Link∗ suc;
int data;
Link∗ insert(int x) // inser t x before this
{
return pre = new Link{pre, this, x};
}
void remove() // remove and destroy this
{
if (pre) pre−>suc = suc;
if (suc) suc−>pre = pre;
delete this;
}
// ...
};
6. 静态成员
静态成员变量:A variable that is part of a class, yet is not part of an object of that class, is called a static member.
静态成员函数:A function that needs access to members of a class, yet doesn’t need to be invoked for a particular object, is called a static member function.
上文的类内初始化依赖于全局变量today,可能导致Date在上下文不同时无法使用。
下面的默认初始化不依赖于全局变量:
class Date {
int d, m, y;
static Date default_date;
public:
Date(int dd =0, int mm =0, int yy =0);
// ...
static void set_default(int dd, int mm, int yy); // set default_date to Date(dd,mm,yy)
};
// We can now define the Date constructor to use default_date like this:
Date::Date(int dd, int mm, int yy)
{
d = dd ? dd :default_date.d;
m = mm ? mm : default_date.m;
y = yy ? yy : default_date.y;
// ... check that the Date is valid ...
}
静态成员可以像其他成员一样被引用/调用,而且还可以在不提及对象的情况下被调用:
void f()
{
Date::set_default(4,5,1945); // call Date’s static member set_default()
}
静态成员在使用前必须被定义,包括成员变量:
Date Date::default_date {16,12,1770}; // definition of Date::default_date
void Date::set_default(int d, int m, int y) // definition of Date::set_default
{
default_date = {d,m,y}; // assign new value to default_date
}
注意到此时Date{ }成为了Date::default_date的值的记号,所以我们不需要用另外的函数来读取默认值,当没有歧义是,仅仅用{ }就足够了:
void f1(Date);
void f2(Date);
void f2(int);
void g()
{
f1({}); // OK: equivalent to f1(Date{})
f2({}): // error : ambiguous: f2(int) or f2(Date)?
f2(Date{}); // OK
}
在多线程程序中,static成员数据需要用锁或者某种存取规则来避免竞争条件(Race condition)。
7. 成员类型(Member Types)
类型或者类型别名可以作为类的成员:
template<typename T>
class Tree {
using value_type = T; // member alias
enum Policy { rb, splay, treeps }; // member enum
class Node { // member class
Node∗ right;
Node∗ left;
value_type value;
public:
void f(Tree∗);
};
Node∗ top;
public:
void g(const T&);
// ...
};
成员类可以使用外围类的static成员和类型,但仅当为其提供一个外部类的对象时才能使用外围类的非静态成员,即内部类有权存取外围类的成员,包括私有成员,但它并不了解当前的外围类对象。
template<typename T>
void Tree::Node::f(Tree∗ p)
{
top = right; // error : no object of type Tree specified
p−>top = right; // OK
value_type v = left−>value; // OK: value_type is not associated with an object
}
但外围类对内部类没有特殊的存取权限:
template<typename T>
void Tree::g(Tree::Node∗ p)
{
value_type val = right−>value; // error : no object of type Tree::Node
value_type v = p−>right−>value; // error : Node::r ight is private
p−>f(this); // OK
}
成员类更多是一种概念上的便利而不是一个重要的基础特性
成员别名非常重要,因为它是泛型编程中关联类型的根基
成员枚举常常是枚举类的替代,它避免了枚举名污染外围域