2.7 快速初始化成员变量
- C++98:
- 使用 ‘=’ 初始化类中成员变量,成员变量必须满足:
① static ② const ③ 整型或枚举型
- 使用 ‘=’ 初始化类中成员变量,成员变量必须满足:
class Init {
public:
Init() : a(0) {}
Init(int d) : a(d) {}
private:
int a;
const static int b = 0; // ok
int c = 1; // error
static int d = 0; // error
static const double e = 1.3; // error,不是整型或枚举型
static const char *const f = "e"; // error, 不是整型或枚举型
}
- C++11
- 允许非静态成员变量的初始化,且有多种形式。
struct {
int a = 1; // 使用 '=' 初始化
double e {2.3}; // 使用 '{}' 初始化
};
#include
using namespace std;
struct C {
C(int i) :
c(i) {}
int c;
};
struct Init {
int a = 1;
string b("Hello"); // error
C c(1); // error
}
圆括号表达式初始化非静态成员 b 和 c 都会出错。
- C++11 支持就地初始化非静态成员的同时,又支持初始化列表。如果两者同时使用,是否会冲突?
#include
using namespace std;
struct Mem {
Mem() { cout << "Mem defulat, num = " << num << endl; }
Mem(int i)
: num(i) {
cout << "Mem defulat, num = " << num << endl;
}
int num = 2; // 使用 = 初始化非静态成员
}
class Group {
public:
Group() { cout << "Group default. val: " << val << endl; }
Group(int i)
: val('G'),
a(i) {
cout <<"Group. val: " << val << endl;
}
void NumofA() { cout << "number of A: " << a.num << endl; }
void NumofB() { cout << "number of B: " << b.num << endl; }
private:
char val{'g'}; // 使用 {} 初始化非静态成员
Mem a;
Mem b{19}; // 使用 {} 初始化非静态成员
}
int main() {
Mem member; // Mem defulat, num = 2
Group group; // Mem default, num = 2
// Mem default, num = 19
// Group default. val: g
group.NumOfA(); // number of A: 2
group.NumOfB(); // number of B: 19
Group group2(7); // Mem defulat, num = 7
// Mem defulat, num = 19
// Group. val: G
group2.NumOfA(); // number of A: 7
group2.numOfB(); // number of B: 19
}
2.8 非静态成员的 sizeof
- C++98
- 无法对非静态成员变量使用sizeof
#include
using namespace std;
struct People {
public:
int hand;
static People *all;
}
int main()
{
Pople p;
cout << sizeof(p.hand) << endl; // C++98 ok, C++11 ok
cout << sizeof(Pople::all) << endl; // C++98 ok, C++11 ok
cout << sizeof(People::hand) << endl; // C++98 err, C++11 ok
}
2.9 扩展的 friend 语法
friend 关键字用于声明类的 友元, 友元可以无视类中的成员属性。无论是public、protected或private,友元类或友元函数都可以访问,这完全破坏了面向对象中封装性的概念。通常,转件建议使用 Get/Set 方法访问类成员,但是,friend会使程序员少些很多代码。
class Poly;
typedef Poly P;
class LiLei {
friend class Poly; // C++98 ok, C++11 ok
};
class Jim {
friedn Poly; // C++98 error, C++11 ok
};
class HanMeiMei {
friend P; // C++98 error, C++11 ok
}
程序员可以为类模板声明友元
class P;
template
class People {
friend T;
};
People pp; // 类型 P 在这里是 People 类型的友元
People Pi; // 对于 int 类型模板参数,友元声明被忽略
// 为了方便测试,进行了危险的定义
#ifdef UNIT_TEST
#define private public
#endif
class Defender {
public:
void Defence(int x, int y) {}
void Trackle(int x, int y) {}
private:
int pos_x = 15;
int pos_y = 0;
int speed = 2;
int stamina = 120;
};
class Attacker {
public:
void Move(int x, int y) {}
void SpeedUp(float ration) {}
private:
int pos_x = 0;
int pos_y = -30;
int speed = 3;
int stamina = 100;
};
#ifdef UNIT_TEST
class Validator {
public:
void Validate(int x, int y, Defender & d) { }
void Validate(int x, int y, Attacker & a) { }
};
int main() {
Defender d;
Attacker a;
a.Move(15, 30);
d.Defence(15, 30);
a.SpeedUp(1.5f);
d.Defence(15, 30);
Validator v;
v.Validate(7, 0, d);
v.Validate(1, -10, a);
return 0;
}
#endif
将 private 关键字统一替换成了 public 关键字。
template
class DefenderT {
public:
friend T;
void Defence(int x, int y) {}
void Trackle(int x, int y) {}
private:
int pos_x = 15;
int pos_y = 0;
int speed = 2;
int stamina = 120;
};
template
class AttackerT {
public:
friend T;
void Move(int x, int y) {}
void SpeedUp(float ration) {}
private:
int pos_x = 0;
int pos_y = -30;
int speed = 3;
int stamina = 100;
};
using Defender = DefenderT;
using Attacker = AttackerT;
class Validator {
public:
void Validate(int x, int y, Defender & d) { }
void Validate(int x, int y, Attacker & a) { }
};
using DefenderTest = DefenderT;
using AttackerTest = AttackerT;
int main() {
Defender d;
Attacker a;
a.Move(15, 30);
d.Defence(15, 30);
a.SpeedUp(1.5f);
d.Defence(15, 30);
Validator v;
v.Validate(7, 0, d);
v.Validate(1, -10, a);
return 0;
}
2.10 final/override 控制
- 重载(overload):是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
- 重写(override):派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
#include
using namespace std;
class MathObject {
public:
virtual double Arith() = 0;
virtual void Prin() = 0;
};
class Printable : public MathObject {
public:
double Arith() = 0;
void Print() { // C++98中我们无法阻止该接口被重写
cout << "Output is : " << Arith() << endl;
}
};
class Add2 : public Printable {
public:
Add2(double a, double b)
: x(a),
y(b) { }
double Arith() {
return x + y;
}
private:
double x, y;
};
class Mul3 : public Printable {
public:
Mul3(double a, double b, double c)
: x (a),
y (b),
z (c) {}
double Arith() {
return x * y * z;
}
private:
double x, y, z;
};
和 Java 类似,通过 final 关键字阻止 函数继续重写。
struct Object {
virtual void fun() = 0;
};
struct Base : public Object {
void fun() final; // 声明为 final
};
struct Derived : public Base {
void fun(); // 无法通过编译
};
final 同样可以终止虚函数被重写,但这没有意义。
为了便于阅读,发现类中的重写方法,引入 override 关键字。
如果派生类在虚函数声明时使用了 override 描述符,那么该函数必须重载其基类中的同名函数,否则无法编译。
struct Base {
virtual void Turing() = 0;
virtual void Dijkstra() = 0;
virtual void VNeumann(int g) = 0;
virtual void DKnuth() const;
void Print();
};
struct DerivedMid: public Base {
// void VNeumann(double g);
// 接口被隔离了,曾想多一个版本的 VNeumann 函数
};
struct DerivedTop: public DerivedMid {
void Turing() override;
void Dikjstra() override; // error,拼写错误
void VNeumann(char g) override; // error,参数不一致
void DKnuth() override; // error, const 属性不一致
void Print() override; // error,非虚函数
};
2.11 模板函数的默认模板参数
#include
using namespace std;
// 定义一个函数模板
template
void TemFun(T a) {
cout << a << endl;
}
int main() {
TempFun(1); // TempFun(1)
TempFun("1"); // TempFun("1")
}
- 默认模板参数
template
class DefClass1;
template
class DefClass2; // error
template
class DefClass3;
template
class DefClass4; // error
template
void DefFunc1(T1 a, T2 b);
template
void DefFunc2(T a);
- 推导规则:如果能从函数实参中推导出类型的话,默认模板参数就不会使用
template
void f(T t = 0, U u = 0);
void g()
{
f(1, 'c'); // f(1, 'c')
f(1); // f(1, 0)
f(); // error, T 无法推导
f(); // f(0, 0)
f(); // f(0, 0)
}
2.12 外部模板
2.12.1 为什么需要外部模板?
C 中 extern 的目的:
extern int i;
一个文件定义 i, 多个文件声明 i, 但是 i 只有一份数据。
对函数模板来说,存在一模一样的问题。不同的是,发生问题的不是变量,而是函数。
// test.cpp
template
void fun(T) {
}
// test1.cpp
#include "test.h"
void test1() { fun(3); }
// test2.cpp
#include "test.h"
void test2() { fun(4); }
/*
* 问题:
* 由于两个源代码使用的模板函数的参数类型一致,所以再编译 test1.cpp 时编译器会实例化 fun(int).
* 在编译 test2.cpp 时,编译器会再一次实例化函数 fun(int)。
* 那么结果就是 test1.o 和 test2.o 会有两份一模一样的函数 fun(int) 代码。
*/
代码重复,为了节省空间,保留其中之一就可以了。事实上,大部分链接器也是这么做的。链接器通过一些编译器辅助的手段将重复的模板函数代码 fun
问题是:对于源码中的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要删除重复的实例化代码。很明显,这太麻烦。
2.12.2 显式的实例化与外部模板的声明
// 显式实例化
template
void fun(T) {
}
template (int);
编译器编译时会强制实例化 fun
// 外部模板
extern template void fun(int);
// test1.cpp
template void fun(int); // 显式实例化
void test1() {
fun(3);
}
// test2.cpp
extern template void fun(int); // 外部模板声明
void test() {
fun(3);
}
注意问题:如果外部模板声明出现在某个编译单元中,那么与之对应的显式实例化必须出现于另一个编译单元中或同一个编译单元的后续代码中。
外部模板声明不能用于一个静态函数(文件域函数),但是可以用于类静态成员函数(因为静态函数没有外部链接属性,不能再本编译单元外出现)。
2.13 局部和匿名类型作模板实参
- C++98:
- 局部类型和匿名类型在C++98中不能做模板的实参
template
class X {};
template
void TempFun(T t) {};
struct A{} a;
struct {int i;} b; // b 是匿名类型变量
typedef struct {
int i;
} B; // B 是匿名类型
void Fun()
{
struct C{} c; // C 是局部类型
X x1; // C++98 error, C++11 ok
X x2; // C++98 error, C++11 ok
X x3; // C++98 error, C++11 ok
TempFun(a); // C++98 error, C++11 ok
TempFun(b); // C++98 error, C++11 ok
TempFun(c); // C++98 error, C++11 ok
}
template
struct MyTemplate {};
int main()
{
MyTemplate t; // error
return 0;
}