“Within C++, there is a much smaller and cleaner language struggling to get out.”
Notes - C++ Learning | C++学习笔记
2023-05-08
Yibo Li
yli12@stevens.edu; yiboli@link.cuhk.edu.hk
http://eborlee.github.io
Table Of Contents
- Notes - C++ Learning | C++学习笔记
- Book: Numerical Methods in Finance with C++
- C++ Primer Plus
- My Notes
- Effective C++
- 04 Make sure that objects are initialized before they’re used
- Chapter 2: Construtors, Destructors, and Assignment Operators
- 05: Know what functions C++ silently writes and calls
- 06: Explicitly disallow the use of compiler-generated functions you do not want
- 07: Declare destructors virtual in polymorphic base classes
- 08: Prevent exceptions from leaving destructors
- 09: Never call virtual functions during construction or destruction
- 10: Have assignment operators returns a reference to *this
- 4 Designs and Declarations 设计与声明
Book: Numerical Methods in Finance with C++
1 Basic Usages
- It is a best practice to almost always set your destructors to be virtual.
- const double getK() and double getK() const: 后者为常量成员函数,无法修改对象的任何成员变量,除非该变量被声明为mutable。
1 Binomial Pricer
Ways of passing parameters:
-
Passed By Value: double S(double S0, double U, …)
When a function is called, a copy of that variable is made in a separate location in computer memory. The func could see and change the copy, but has no access to the original varible. And the calling program could also see the copy and only has access to the original variable.
-
Passed By Reference: int GetInputData(double& S0, …)
C++ can return only a single value in a function and this way enables to pass all the inputs back to the program.
In this case, a single copy of the variable in computer memory is shared by the function and the calling program.
Separate Compilation
After defining some function protoptypes in .h file: changes to one of the .cpp files do not require the other file to be recompiled as long as the function prototypes remain unchanged. In large projects this can mean considerable savings in compilation time.
2 Binomial Pricer Revisited
3 American Options
3.1 Multiple Inheritance
菱形继承问题的解决方案:
- 1.Virtual Inheritance
- 2.作用域解析运算符
- 3.重新定义函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
using namespace std;
class A
{
public:
void func(){
cout<<"This is A"<<endl;
};
};
class B: virtual public A{
public:
void func() {
cout<<"This is B"<<endl;
};
};
class C: virtual public A{
public:
void func() {
cout<<"This is C"<<endl;
};
};
class D: public B, public C{
public:
void func() {
cout<<"This is D"<<endl;
}
};
class E: public B, public C{
// E不给实现,就会导致二义性,编译器不知道该找哪个方法
};
class F: public B, public C{
public:
void func(){
C::func();
}
};
int main(){
D d;
E e;
d.func();
// e.func(); // 二义性,无法编译 对比python,python是运行时报错,基于MRO。
// 方法2 作用域解析运算符
e.B::func();
// 方法3 重新定义函数
F f;
f.func();
}
题外话,对比python,python对于菱形继承,D继承自BC,而D没有给方法实现,第一python的多重继承和方法解析是在运行时进行,而不是在编译时。第二,python的MRO(Method Resolution Order)算法会根据MRO顺序来决定使用哪个方法,除非明确调用不存在的方法和属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A:
def func(self):
print("A")
class B(A):
def func(self):
print("B")
class C(A):
def func(self):
print("C")
class D(B,C):
pass
d = D()
d.func() # 输出B
print(D.mro())
3.2 Virtual Inheritance
3.3 Class Templates
4 Non-linear Solvers
Basic Knowledge:
BSM Formula: \(C(S_0, K, T,\sigma,r)=S_0N(d_+)-Ke^{-rT}N(d_-)\\ d_+=\frac{ln(\frac{S_0}{K})+(r+\frac{\sigma^2}{2})T}{\sigma\sqrt{T}}\\ d_-=d_+-\sigma\sqrt{T}\\ N(x)=\int_{-\infty}^x \frac{1}{\sqrt{2\pi}}e^{-\frac{y^2}{2}}dy\) If the values of S0, K, T, σ,r are all known, then we can calculate the price of option. Among these items, only σ is not known and needed to be estimated using the market option price.
There are two common methods for this purpose: Bisection method and Newton-Raphson method.
Bisection Method:
We want to compute a solution x to an equation: f(x) = c, where f is a given function from an interval [a,b] to R. It is assumed that f is continuous on [a,b] and f(a)-c, f(b)-c have opposite signs. Then there must be an x∈[a,b] such that f(x)=c.
The bisection method works by constructing sequences l, r of left and right approximations by induction:
(l+r)/2 -> mid, f(mid)-c
If f(l) * f(mid) > 0, root lies in [mid, r], mid -> l;
If f(l) * f(mid) <0, root lies in [l, mid], mid ->r;
If f(mid) == 0, mid is the root, Stop.
| And if | mid - l | <= Accuracy requirement, stop. Input the mid and the corresponding f(mid)-c at that moment. |
Newton-Raphson Method:
f is assumed to be differentiable on [a,b] and construct a sequence xn as follows: \(Take ~x0∈(a,b).\\ For~ n=0,1,2,..., let\\ x_{n+1}=x_n-\frac{f(x_n)-c}{f'(x_n)}\) If the equation f(x)=c has a solution x∈(a,b) such that f’(x)≠0 and x0 is chosen close enough to x, then xn will converge to x.
4.1 使用Function Pointer的基本实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef Solver01_h
#define Solver01_h
double SolveByBisect(double (*Fct)(double x), double Tgt,
double LEnd, double REnd, double acc)
{
double left = LEnd, right = REnd, mid = (left + right) / 2;
double y_left = Fct(left) - Tgt, y_mid = Fct(mid) - Tgt;
while (mid - left > acc)
{
if ((y_left > 0 && y_mid > 0) || (y_left < 0 && y_mid < 0))
{
left = mid;
y_left = y_mid;
}
else
right = mid;
mid = (left + right) / 2;
y_mid = Fct(mid) - Tgt;
}
return mid;
}
// Newton-Raphson Method
double SolveByNR(double (*Fct)(double x), double (*DFct)(double x), double Tgt,
double guess, double acc)
{
double x_prev = guess;
double x_next = x_prev - (Fct(x_prev) - Tgt) / DFct(x_prev);
while (x_next - x_prev > acc || x_prev - x_next > acc)
{
x_prev = x_next;
x_next = x_prev - (Fct(x_prev) - Tgt) / DFct(x_prev);
}
return x_next;
}
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "Solver01.h"
#include <iostream>
using namespace std;
double F1(double x){return x*x-2;}
double DF1(double x){return 2*x;}
int main(){
double Acc = 0.001;
double LEnd=0.0, REnd=2.0;
double Tgt = 0.0;
cout << "Root of F1 by bisect:"
<< SolveByBisect(F1, Tgt, LEnd, REnd, Acc)
<<endl;
double Guess = 1.0;
cout << "Root of F1 by newton-raphson:"
<< SolveByNR(F1, DF1, Tgt, Guess, Acc)
<<endl;
return 0;
}
这种方式简单却不易于expansion。如果传入的Function形如f(x)没问题,但是f(x,a) = x^2-a不适用。
4.2 使用Virtual Functions: Enable dynamic binding
Dynamic Binding :动态联编是指在运行时根据实际对象的类型来确定要调用的函数实现,而不仅仅根据指针或引用的静态类型。这意味着,当通过基类的指针或引用调用虚函数时,实际调用的是对象的派生类中所重写的虚函数。这种动态绑定机制使得在继承体系中可以实现多态性,允许通过基类的接口来操作具体的派生类对象,而不需要显式指定对象的具体类型。
而如果在签名中声明为类对象本身而不是其指针,就会导致: 虽然可以传入其子类,但是调用方法还是调用的同一个父类方法,而不会调用子类重写的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
class Animal {
public:
virtual void MakeSound() {
std::cout << "Animal is making a sound" << std::endl;
}
};
class Dog : public Animal {
public:
void MakeSound() override {
std::cout << "Dog is barking" << std::endl;
}
};
class Cat : public Animal {
public:
void MakeSound() override {
std::cout << "Cat is meowing" << std::endl;
}
};
void PerformSound(Animal* animal) {
animal->MakeSound();
}
void PerformSound2(Animal animal) {
animal.MakeSound();
}
int main() {
Animal animal;
Dog dog;
Cat cat;
// 当使用基类指针作为参数时,可以实现运行时的多态性,使代码能够根据实际传递的派生类对象来选择相应的函数实现。
// dynamic binding
PerformSound(&animal); // Output: "Animal is making a sound"
PerformSound(&dog); // Output: "Dog is barking"
PerformSound(&cat); // Output: "Cat is meowing"
// 如果直接传递基类对象本身,编译器在编译时就确定了要调用的函数实现,无法在运行时动态选择不同的函数实现。
PerformSound2(animal); // Output: "Animal is making a sound"
PerformSound2(dog); // Output: "Animal is making a sound"
PerformSound2(cat); // Output: "Animal is making a sound"
return 0;
}
继续Solver的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef Solver02_h
#define Solver02_h
class Function
{
public:
virtual double Value(double x) = 0;
virtual double Deriv(double x) = 0;
};
double SolveByBisect(Function *Fct, double Tgt, double LEnd, double REnd, double Acc)
{
double left = LEnd, right = REnd, mid = (left + right) / 2;
double y_left = Fct->Value(left) - Tgt, y_mid = Fct->Value(mid) - Tgt;
while (mid - left > Acc)
{
if ((y_left > 0 && y_mid > 0) || (y_left < 0 && y_mid < 0))
{
left = mid;
y_left = y_mid;
}
else
right = mid;
mid = (left + right) / 2;
y_mid = Fct->Value(mid) - Tgt;
}
return mid;
}
double SolveByNR(Function *Fct, double Tgt, double guess, double acc)
{
double x_prev = guess;
double x_next = x_prev - (Fct->Value(x_prev) - Tgt) / Fct->Deriv(x_prev);
while (x_next - x_prev > acc || x_prev - x_next > acc)
{
x_prev = x_next;
x_next = x_prev - (Fct->Value(x_prev) - Tgt) / Fct->Deriv(x_prev);
}
return x_next;
}
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "Solver02.h"
#include <iostream>
using namespace std;
class F1 : public Function
{
public:
double Value(double x) { return x * x - 2; }
double Deriv(double x) { return 2 * x; }
} MyF1;
class F2 : public Function
{
private:
double a;
public:
F2(double a_)
{
a = a_;
}
double Value(double x) { return x * x - a; }
double Deriv(double x) { return 2 * x; }
} MyF2(3.0);
int main()
{
double Acc = 0.001;
double LEnd = 0.0, REnd = 2.0;
double Tgt = 0.0;
cout << "Root of F1 by bisect:"
<< SolveByBisect(&MyF1, Tgt, LEnd, REnd, Acc)
<< endl;
double Guess = 1.0;
cout << "Root of F2 by newton-raphson:"
<< SolveByNR(&MyF2, Tgt, Guess, Acc)
<< endl;
return 0;
}
使用虚函数在运行时进行动态绑定是有一些开销的,这是因为动态绑定需要在运行时查找正确的函数实现。这些开销包括:
- 虚函数表(vtable):为了实现动态绑定,编译器会为每个包含虚函数的类生成一个虚函数表。虚函数表是一个指针数组,用于存储每个虚函数的地址。对于每个类对象,都会包含一个指向其虚函数表的指针。这样,当调用虚函数时,需要通过指针查找正确的函数地址。
- 虚函数调用开销:调用虚函数时,需要通过指针间接调用函数。这涉及额外的间接跳转操作,相对于直接调用非虚函数,会增加一定的开销。
总体来说可以忽略不计,但是以下情景需要注意:
- 循环中使用虚函数会造成大量开销
- 使用虚函数的地方可以进行性能优化,例如使用内联(inline)修饰符来尝试避免间接跳转的开销。
- 考虑使用其他的技术,如模板和策略模式,来避免使用虚函数带来的开销。
4.3 使用Templates模板
极大的保留了虚函数的优点,但是将type checking从runtime转移到了compile time.
代码与虚函数大体相似,但简化了:不需要定义抽象基类和其虚函数,具体的公式类也不需要再继承共同的父类。
缺点:如果有大量的函数将会带来较长的编译时间和较大的.exe文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#ifndef Solver03_h
#define Solver03_h
template <typename Function>
double SolveByBisect(Function *Fct, double Tgt, double LEnd, double REnd, double Acc)
{
double left = LEnd, right = REnd, mid = (left + right) / 2;
double y_left = Fct->Value(left) - Tgt, y_mid = Fct->Value(mid) - Tgt;
while (mid - left > Acc)
{
if ((y_left > 0 && y_mid > 0) || (y_left < 0 && y_mid < 0))
{
left = mid;
y_left = y_mid;
}
else
right = mid;
mid = (left + right) / 2;
y_mid = Fct->Value(mid) - Tgt;
}
return mid;
}
template <typename Function>
double SolveByNR(Function *Fct, double Tgt, double guess, double acc)
{
double x_prev = guess;
double x_next = x_prev - (Fct->Value(x_prev) - Tgt) / Fct->Deriv(x_prev);
while (x_next - x_prev > acc || x_prev - x_next > acc)
{
x_prev = x_next;
x_next = x_prev - (Fct->Value(x_prev) - Tgt) / Fct->Deriv(x_prev);
}
return x_next;
}
#endif
此时这两个solver函数都是模板函数,即可以接收任意类型的Fct,编译器会在将来这个函数被调用时,根据调用的那句话中声明的typename的类型,来生成不同版本的函数,这样就避免了运行时开销。比如template
编译器针对已知的类型,更好的进行优化;编译器更容易进行内联优化;没有虚函数的运行时开销;生成更好的分支预测代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "Solver03.h"
#include <iostream>
using namespace std;
class F1
{
public:
double Value(double x) { return x * x - 2; }
double Deriv(double x) { return 2 * x; }
} MyF1;
class F2
{
private:
double a;
public:
F2(double a_)
{
a = a_;
}
double Value(double x) { return x * x - a; }
double Deriv(double x) { return 2 * x; }
} MyF2(3.0);
int main()
{
double Acc = 0.001;
double LEnd = 0.0, REnd = 2.0;
double Tgt = 0.0;
cout << "Root of F1 by bisect:"
<< SolveByBisect(&MyF1, Tgt, LEnd, REnd, Acc)
<< endl;
double Guess = 1.0;
cout << "Root of F2 by newton-raphson:"
<< SolveByNR(&MyF2, Tgt, Guess, Acc)
<< endl;
return 0;
}
同时,由于不再使用虚函数,SolveByBisect和SolveByNR的声明也不需要再使用Fct指针,使用Fct就可以了。
4.4 Computing Implied Volatility
Define EurCall.h for computing the call option price using BSM using solver03.h, i.e. Templates version.
To compute it using Newton-Raphson method, we also need an expression for the derivative of the European call option price with respect to volatility σ calculated from BSM formula, Vega, is given by: \(v=\frac{1}{\sqrt{2\pi}}S_0e^{\frac{-d_+^2}{2}}\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef EurCall_h
#define EurCall_h
class EurCall
{
public:
double T, K;
EurCall(double T_, double K_) { T = T_, K = K_; }
double d_plus(double S0, double sigma, double r);
double d_minus(double S0, double sigma, double r);
double PriceByBSFormula(double S0, double sigma, double r);
double VegaByBSFormula(double S0, double sigma, double r);
};
#endif
Implement the methods in EurCall.cpp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "EurCall.h"
#include <cmath>
#include <iostream>
using namespace std;
double N(double x)
{
double result;
if (x > 0)
{
result = 0.5 * (1.0 + erf(x / std::sqrt(2.0)));
}
else if (x == 0)
{
result = 0.5;
}
else
{
result = 0.5 * (1.0 - erf(-x / std::sqrt(2.0)));
}
return result;
}
double EurCall::d_plus(double S0, double sigma, double r)
{
return (log(S0 / K) + (r + 0.5 * pow(sigma, 2.0)) * T) / (sigma * sqrt(T));
}
double EurCall::d_minus(double S0, double sigma, double r)
{
return d_plus(S0, sigma, r) - sigma * sqrt(T);
}
double EurCall::PriceByBSFormula(double S0, double sigma, double r)
{
return S0 * N(d_plus(S0, sigma, r)) - K * exp(-r * T) * N(d_minus(S0, sigma, r));
}
double EurCall::VegaByBSFormula(double S0, double sigma, double r)
{
double pi = 4.0 * atan(1.0);
return S0 * exp(-d_minus(S0, sigma, r) * d_plus(S0, sigma, r) / 2) * sqrt(T) / sqrt(2.0 * pi);
}
Run the functions in main_for_cal_imp.cpp. Here create a class as a intermediary class to translate the functions from EurCall to Solvers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include "Solver03.h"
#include "EurCall.h"
#include "EurCall.cpp"
#include <iostream>
using namespace std;
// 需要一个中间类进行初始化和翻译函数名
class Intermediary : public EurCall
{
private:
double S0, r;
public:
Intermediary(double S0_, double r_, double T_, double K_) : EurCall(T_, K_)
{
S0 = S0_;
r = r_;
}
double Value(double sigma)
{
return PriceByBSFormula(S0, sigma, r);
}
double Deriv(double sigma)
{
return VegaByBSFormula(S0, sigma, r);
}
};
int main()
{
double S0 = 100.0;
double r = 0.1;
double T = 1.0;
double K = 100.0;
Intermediary Call(S0, r, T, K);
double Acc = 0.001;
double LEnd = 0.01, REnd = 1.0;
double Tgt = 12.56;
cout << "Root of F1 by bisect:"
<< SolveByBisect(&Call, Tgt, LEnd, REnd, Acc)
<< endl;
double Guess = 0.23;
cout << "Root of F2 by newton-raphson:"
<< SolveByNR(&Call, Tgt, Guess, Acc)
<< endl;
return 0;
}
5 Monte Carlo methods
LU Decomposition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <Eigen/Dense>
#include <Eigen/LU>
using namespace std;
using namespace Eigen;
int main(){
typedef Matrix<double, 4,4> Matrix4x4;
Matrix4x4 p;
p<< 7,3,-1,2,
3,8,1,-4,
-1,1,4,-1,
2,-4,-1,6;
cout<< "Mat_P:\n"<<p<<endl<<endl;
PartialPivLU<Matrix4x4> lu(p); // 此处在栈区创建了一个PPL对象,其构造函数自动对传入矩阵进行LU分解
cout<< "Lu_Mat:\n"<<lu.matrixLU()<< endl<<endl;
// 由于lu分解的下三角矩阵的主对角线都是1,所以需要先初始化一个单位矩阵
// 使用到了静态函数
Matrix4x4 l = MatrixXd::Identity(4,4);
// 先获取一个从0,0开始的4*4的子矩阵再限定其视图为下三角,接受赋值
l.block<4,4>(0,0).triangularView<StrictlyLower>() = lu.matrixLU();
cout<<"L_mat:\n"<<l<<endl<<endl;
// 由于上三角矩阵的主对角线是0,所以直接接受赋值
Matrix4x4 u = lu.matrixLU().triangularView<Upper>();
cout<<"u_mat:\n"<<u<<endl<<endl;
C++ Primer Plus
Chapter 4. 复合类型
4.8 指针、数组和指针算术
- 指针变量+1后,增加量等于其指向类型的字节数。
- C++将数组名解释为第一个元素的地址, 被解释为这个元素的指针。但本身是一个标识符,不是真正的指针
- *(stacks + 1) <==> stacks[1]
- 对数组使用sizeof得到整个数组的长度,而对指针应用sizeof得到的是指针的字节长度,即使指针指向一个数组。
- 数组名:首个元素的地址, &数组名:整个数组的地址。数字上这两个地址相同,但是数组名是一个元素字节长度内存块的地址,而&数组名是一个数组整个长度字节内存块的地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// C++将数组名解释为数组第一个元素的地址
// wages = &wages[0] = address of first element of array
double wages[3] = {10.0, 20.0, 30.0};
short stacks[3] = {3, 2, 1};
double * pw = wages;
short * ps = &stacks[0];
cout << "pw=" << pw << ", *pw=" << *pw << endl;
pw = pw + 1;
cout << "pw=" << pw << ", *pw=" << *pw << endl;
cout << "ps=" << ps << ", *ps=" << *ps << endl;
cout << "stacks[0]=" << stacks[0] << ", stacks[1]=" << stacks[1] << endl;
cout << "*stacks=" << *stacks << ", *(stacks+1)=" << *(stacks+1) << endl;
cout<<sizeof(wages)<<endl;
cout<<sizeof(pw)<<endl;
// 数组名:首个元素的地址, &数组名:整个数组的地址
cout<<stacks<< ", "<<&stacks<<endl;
1
2
3
4
5
6
7
8
9
// output
pw=0x7ff7b61391f0, *pw=10
pw=0x7ff7b61391f8, *pw=20
ps=0x7ff7b61391e2, *ps=3
stacks[0]=3, stacks[1]=2
*stacks=3, *(stacks+1)=2
24
8
0x7ff7bb3e61e2, 0x7ff7bb3e61e2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
short tell[10];
short (*pas)[10] = &tell;
//(*pas)[i] 的意思是“解引用指针 pas 得到一个数组,然后访问该数组的第 i 个元素”
cout<<pas<<endl; // 指针指向的地址
cout<<pas[0]<<endl; // 也是首元素的地址
cout<<pas[1]<<endl; // 跳过1个数组,但指向了未定义的内存地址
cout<<*pas<<endl; // 解引用,数组的首元素的地址
cout<<(*pas)[0]<<endl; // 解引用之后获取第一个元素的值
cout<<*(*pas)<<endl; // 第一次解引用获取一个数组的指针,对数组指针再次解引用
//output
>>>
0x7ff7b0bb61d0
0x7ff7b0bb61d0
0x7ff7b0bb61e4
0x7ff7b0bb61d0
-28656
-28656
在这个特定的情境下,pas 是一个指向长度为10的 short 数组的指针。对于 pas[1] 的解释在于,编译器认为你想跳过第一个长度为10的 short 数组,然后到达第二个这样的数组。当然,在这个特定的例子中,第二个这样的数组并不存在,所以 pas[1] 会指向一个未定义的内存区域。
通常,pointer_to_array[n] 的意义是“跳过 n 个数组,到达第 n+1 个数组的起始地址”,这里的数组是 pointer_to_array 指针所指向的类型。所以 pas[1] 就是 tell 数组后面 10(数组长度)个 short 类型大小的位置的地址。这通常是一个不安全的操作,因为这个地址可能没有被程序合法地分配。访问这个地址通常会导致未定义的行为。
此外,sizeof(pointer)与sizeof(*sizeof)也是不同的。前者是查询储存该指针本身所需的字节长度,比如4字节或8字节。而后者是查询指针指向的数据类型的大小。
keyword static
在类定义中直接声明的变量通常是非静态成员变量。每个类的对象都有其自己的一份非静态成员变量的拷贝。相对的,静态成员变量是属于类本身的,不属于对象,所有该类的对象共享同一份静态成员变量。
1
2
3
4
5
class Example {
public:
int a; // 非静态成员变量
static int b; // 静态成员变量
};
非常量引用不可绑定或传入常量:
当一个引用没有被const修饰,不能用这个引用去绑定常量值,因为这意味着你可能会通过这个引用来修改引用的值,但是这与常量不可变的特性是冲突的,如:
1
2
3
4
5
6
7
8
9
10
const int x = 10;
// invalid
int &ref = x;
void func(int &a){};
const int constVar = 10;
int nonConstVar = 10;
func(constVar); // invalid
func(nonConstVar); // valid
My Notes
keyword const
1)创建变量时修饰: const Example obj;
目的:
- 对象的所以非static成员变量都不可被修改,除非是被mutable修饰的。
- 限制为const的对象只能调用const函数,确保成员函数不会修改对象的状态;
- 当对象作为函数的参数被传入时,确保函数不会修改该对象;
- 有时会对函数返回的对象使用const;
- 明确语义;
2)函数形参const:void func(const &int a)
3)函数声明末尾const:void func(int a) const
Member Initialization List 成员初始化列表
不可用于static变量
智能指针
性能优化的一些建议
引用传递: 除非有特殊需求,大型对象(例如std::vector, std::deque等)应尽量通过引用进行传递,以避免不必要的拷贝。
const修饰: 如果某个参数或者成员函数不会对对象的状态进行修改,使用const关键字。这样能让编译器进行一些优化,并且使代码更安全。
预分配内存: 如果知道容器将要存储的元素数量,考虑预先分配容器大小(如std::vector::reserve)。
函数内联: 对于那些小型且频繁调用的函数,考虑使用内联(inline)以减少函数调用的开销。
优化数据结构: 考虑数据访问模式,使用最适合的数据结构。例如,在大量查找操作中使用哈希表。
避免全局变量: 全局变量会影响代码的模块性和可预测性,应尽量避免。
多线程: 如果有一些独立的计算密集型任务,可以考虑使用多线程进行优化。
算法复杂性: 选择时间复杂性更优的算法可以大幅提升程序性能。
延迟初始化: 仅在必要时进行资源的初始化和分配。
局部性原理: 尽量优化数据的局部性访问,特别是在循环等操作中。
RAII: 资源获取即初始化(RAII)是C++中一种常用的资源管理和异常安全的设计模式,可考虑其用于资源的自动管理。
使用现代C++特性: 比如auto关键字、范围基础的for循环等,它们不仅可以简化代码,还有助于编译器进行优化。
keyword assert
用于开发阶段的调试。
keyword noexcept
加上noexcept的函数将承诺不会抛出异常。如果真的存在异常,将会导致整个程序停止。主要是为了告诉编译器,使编译器进行优化,让编译器知道这里绝不会抛异常,不必多生成保护栈上对象安全析构的代码。
=default
using与typedef的区别
using支持模板别名,而typedef不可
template <typename T>
using Vec = std::vector<T>;
可读性更好,顺序更自然
1
2
using IntPtr = int*;
typedef int* IntPtr;
嵌套类型时using更直观
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef void (*Func)(int, int);
using Func = void (*)(int, int);
// 函数表功能
typedef int (*OperationFuncPtr)(int, int);
int multiply(int a, int b) {
return a * b;
}
OperationFuncPtr operations[] = {add, multiply};
int result1 = operations[0](3, 4); // 3 + 4 = 7
int result2 = operations[1](3, 4); // 3 * 4 = 12
// 回调
void execute(AddFuncPtr func, int a, int b) {
int result = func(a, b);
std::cout << "Result: " << result << std::endl;
}
// 成员函数指针
class MyClass {
public:
void myMethod(int a) {
std::cout << "Value: " << a << std::endl;
}
};
using MethodPtr = void (MyClass::*)(int);
MyClass obj;
MethodPtr ptr = &MyClass::myMethod;
(obj.*ptr)(5); // 输出 "Value: 5"
BTW: 关于函数名的理解:函数名本质上是一个指向函数地址的指针。正因为如此可以直接将函数名赋值给一个函数指针,如:void (*funcPtr)() = myFunction; funcPtr(); 当然,也可以通过对&myFunction明确的取地址。
Using可以用来引入基类的名称,从而实现函数重载,否则同名不同参的函数在子类中,基类函数会被隐藏,强行调用(如传入基类函数声明的参数类型,会报错)
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
void someMethod(int a) {
}
};
class Derived : public Base {
public:
using Base::someMethod;
void someMethod(double d){}
};
⭐Memory Pool
https://github.com/cacay/MemoryPool/tree/master
结构:
使用模板类,接受两个模板参数:元素类型T和块大小BlockSize
.h file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H
#include <climits>
#include <cstddef>
template <typename T, size_t BlockSize = 4096>
class MemoryPool
{
public:
/* Member types */
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef const T* const_pointer;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef std::false_type propagate_on_container_copy_assignment;
typedef std::true_type propagate_on_container_move_assignment;
typedef std::true_type propagate_on_container_swap;
template <typename U> struct rebind {
typedef MemoryPool<U> other;
};
/* Member functions */
MemoryPool() noexcept;
MemoryPool(const MemoryPool& memoryPool) noexcept;
MemoryPool(MemoryPool&& memoryPool) noexcept;
template <class U> MemoryPool(const MemoryPool<U>& memoryPool) noexcept;
~MemoryPool() noexcept;
MemoryPool& operator=(const MemoryPool& memoryPool) = delete;
MemoryPool& operator=(MemoryPool&& memoryPool) noexcept;
pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;
// Can only allocate one object at a time. n and hint are ignored
pointer allocate(size_type n = 1, const_pointer hint = 0);
void deallocate(pointer p, size_type n = 1);
size_type max_size() const noexcept;
template <class U, class... Args> void construct(U* p, Args&&... args);
template <class U> void destroy(U* p);
template <class... Args> pointer newElement(Args&&... args);
void deleteElement(pointer p);
private:
union Slot_ {
value_type element;
Slot_* next;
};
typedef char* data_pointer_;
typedef Slot_ slot_type_;
typedef Slot_* slot_pointer_;
slot_pointer_ currentBlock_;
slot_pointer_ currentSlot_;
slot_pointer_ lastSlot_;
slot_pointer_ freeSlots_;
size_type padPointer(data_pointer_ p, size_type align) const noexcept;
void allocateBlock();
static_assert(BlockSize >= 2 * sizeof(slot_type_), "BlockSize too small.");
};
#include "MemoryPool.tcc"
#endif // MEMORY_POOL_H
子类对父类构造函数的调用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Method1: 构造函数初始化列表
class Base {
public:
Base(int a) {}
};
class Derived : public Base {
public:
Derived(int a) : Base(a) {} // 调用父类的构造函数
};
// Method2:默认构造,即隐式地调用
class Base {
public:
Base() {} // 默认构造函数
};
class Derived : public Base {
public:
Derived(int a) {} // 隐式调用父类的默认构造函数
};
// Method3: using
// C++11引入了一个新特性,允许子类通过using声明继承父类的构造函数。这样,子类可以直接使用父类的构造函数,而不需要在子类中重新定义它们。
class Base {
public:
Base(int a) {}
Base(int a, int b) {}
};
class Derived : public Base {
public:
using Base::Base; // 继承父类的所有构造函数
};
隐藏构造函数而提供static create的用途
使用静态方法
create来创建类对象,而不直接让用户调用构造函数的设计,通常有以下几个目的:
控制对象的创建: 通过
create方法,你可以更好地控制对象的创建过程。例如,你可以在创建对象前后添加额外的逻辑,如参数检查、日志记录或资源分配等。返回子类实例: 如果你的类是一个基类,并且你想在运行时根据某些条件返回不同的子类实例,
create方法可以实现这一目的。用户不需要知道具体的子类类型,只需知道基类类型。单例模式: 如果你想要在程序中创建一个特定类的单一实例,
create方法可以帮助实现单例模式。它可以保持对该实例的引用,并在需要时返回现有实例,而不是创建新实例。对象池:
create方法可以用于实现对象池,以重用已经创建的对象而不是每次都创建新对象,从而提高性能。隐藏构造函数:
create方法可以隐藏构造函数的具体实现,使得构造函数可以被保护或私有化,这样用户就不能直接创建对象,必须通过create方法来创建。错误处理: 通过
create方法,你可以更容易地处理错误。例如,如果对象创建失败,create方法可以返回null或抛出异常,而构造函数则不能返回null。返回智能指针:
create方法可以返回一个智能指针,如std::shared_ptr或std::unique_ptr,从而确保对象的生命周期得到正确管理。这种设计模式可以使类的设计更为灵活,有助于实现代码的解耦和复用。同时,它还可以让类的用户更容易理解和使用该类,降低错误的可能性。
Dynamic Type Checking
dynamic_cast是C++中的一个类型转换运算符,它在运行时检查对象的类型。dynamic_cast主要用于处理类之间的继承关系。
表达式dynamic_cast<Derived*>(basePtr)尝试将basePtr(一个指向基类对象的指针)转换为指向派生类对象的指针。结果可以是两种情况之一:
- 成功转换:
- 如果
basePtr实际上指向的是一个Derived类型的对象,dynamic_cast将成功,并返回一个有效的指向Derived类型对象的指针。
- 如果
- 转换失败:
- 如果
basePtr不是指向一个Derived类型的对象,dynamic_cast将失败,并返回nullptr。
- 如果
1
2
3
4
5
6
7
Base* basePtr = &d;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 动态类型检查
if (derivedPtr) {
// basePtr 实际上是指向 Derived 类型的对象
} else {
// basePtr 不是指向 Derived 类型的对象
}
Constructors
Conversion Constructor
只是是一个参数的构造函数就是转换构造函数,就支持这种隐式转换
1
2
3
4
5
6
7
8
class Example {
public:
int value;
Example(int val) : value(val) {} // 转换构造函数
};
Example ex = 42; // 隐式转换 int 到 Example
Example ex = Example(42); // 相当于显示调用构造函数
如果想避免这种隐式转换带来的隐患,则需要explicit关键字
1
2
3
4
5
6
7
8
class Example {
public:
int value;
explicit Example(int val) : value(val) {} // 现在,这个构造函数不允许隐式转换
};
Example ex = Example(42); // 正确
// Example ex = 42; // 错误,因为不允许隐式转换
Move Constructor
重点是构造器形参中的右值引用。如下是一种移动语义的写法。另一种是std::move
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Example {
public:
int* value;
Example(Example&& other) noexcept : value(other.value) {
other.value = nullptr;
}
};
class DBConn {
public:
DBConn(DBConnection&& dbConnection) noexcept : db(std::move(dbConnection)), closed(false) {}
// ... 其他成员函数和数据成员
private:
DBConnection db;
bool closed;
};
Copy Constructor
重点是同类对象的引用作为形参。这与conversion operator有区别。那个是不同类的隐式转换。
1
2
3
4
5
6
class Example {
public:
int value;
Example(const Example& other) : value(other.value) {}
};
Return by value + 赋值的运行逻辑
在C++中,当函数通过值返回对象时,通常会涉及到对象的拷贝或移动。在你提供的代码
myClass mc = func();中,func函数返回一个myClass类型的对象,然后该对象被赋值给外部变量mc。下面是这个过程的详细解释:
- 函数内部对象的生命周期:
- 在
func函数内部,会创建一个myClass类型的临时对象。这个对象的生命周期仅在func函数的执行期间存在。一旦func函数返回,这个临时对象就会超出其生命周期。- 返回过程:
- 在
func函数返回时,原始的临时对象会被复制或移动到一个新的临时对象中。通常,编译器会尝试使用移动构造函数(如果可用)而不是拷贝构造函数,以避免不必要的对象复制。这个优化通常被称为返回值优化(RVO)或命名返回值优化(NRVO)。- 赋值过程:
- 新的临时对象然后被赋值给
mc变量。如果mc变量已经存在,会调用拷贝赋值运算符或移动赋值运算符(取决于临时对象是否可以被移动)。如果mc变量是新创建的,那么会调用拷贝构造函数或移动构造函数。- 外部变量的生命周期:
- 一旦临时对象被赋值给
mc变量,临时对象的生命周期就结束了,它会被销毁。mc变量的生命周期从它被创建的时刻开始,直到它超出作用域或被销毁时结束。总的来说,
func内部的原始对象和外部的mc变量是两个完全独立的对象,它们有各自的生命周期。在func函数返回并赋值给mc变量的过程中,可能会发生对象的拷贝或移动操作。
Effective C++
04 Make sure that objects are initialized before they’re used
Chapter 2: Construtors, Destructors, and Assignment Operators
05: Know what functions C++ silently writes and calls
如果开发者自己没有声明,编译器会自动声明:copy构造函数,default构造函数(当没有任何构造函数时),copy assignment操作符,析构函数。其中析构函数是non-virtual的,除非该类包含其他的virtual函数。所有这些函数都是public且inline的。
其中copy构造函数和copy assign操作符的默认功能是,将来源对象的每一个non-static成员变量拷贝到目标对象。
如果一个类的成员里包含reference成员,那么C++将拒绝为该类生成copy assignment操作符。第一是本身C++具有引用不可变性,引用的指向不可变,因此copy赋值过程中,该reference成员首先不会改指向实参对象中的那个reference成员。第二,那就只能是修改指向的值了,但这会进一步影响持有该成员的引用的其他对象,所以这个copy assign行为必须由开发者自己定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value);
...
private:
std::string& nameValue;
const T objectValue;
};
std::string newDog("A");
std::string oldDog("B");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p=s; // what would happen?
面对包含const成员的类,编译器也是一样,由于更改const成员是不合法的,因此编译器不知道如何处理赋值函数。
此外,如果基类将copy assignment操作符声明为private,则编译器将不会给子类生成copy assign操作符。因为原本子类的copy assignment操作符应该能够处理base class的成分。
06: Explicitly disallow the use of compiler-generated functions you do not want
明确拒绝我们不想使用的编译器自动生成的函数
不声明copy构造函数或copy assignment操作符,编译器可能会自动产出一份,于是class支持了copying。但是如果我们就是不希望class支持copying呢?
关键在于:编译器自动产出的函数都是public的,因此一个解决方案是令这些函数为private,从而阻止外部的调用。
这个方法不是绝对的安全,因为member函数和friend函数都可以调用private函数。
可以声明一个基类来专门负责:
1
2
3
4
5
6
7
8
9
10
11
12
class Uncopyable {
protected:
Uncopyale() {}; // 允许子类对象构造和析构
~Uncopyable() {}; // 注意这里不一定得是virtual的,见条款07
private:
Uncopyable(const Uncopyable&); // 阻止copy
Uncopyable& operator=(const Uncopyable&);
}
class HomeForSale: private Uncopyable { // private使得基类的public和protected成员在子类中变为私有成员
... // class不需要再声明copy构造函数和copy赋值运算符为private
}
你认为uncopyable这个基类的析构函数不需要是virtual的原因有哪些?
ChatGPT
在此情况下,
Uncopyable基类的析构函数不需要是virtual的,主要有以下几个原因:
- 非多态基类:
Uncopyable的目的是为了禁止复制和赋值,而不是为了提供多态行为。通常,只有当基类打算被用作多态基类时,析构函数才需要是virtual的。- 无需基类指针:
Uncopyable基类没有任何其他的成员函数,也不包含任何可重写的虚函数。因此,很少有情况需要通过指向Uncopyable的指针或引用来操作HomeForSale对象。无需通过基类指针删除派生类对象,所以不需要virtual析构函数。- 无资源管理:
Uncopyable基类不包含任何资源(如动态分配的内存),也没有定义任何实际的析构逻辑。因此,即使析构函数不是virtual的,也不会出现资源泄露的问题。- 减少虚表开销: 为基类定义
virtual析构函数会为该类添加一个虚表(vtable)。虚表是一个存储类的虚函数地址的表,它会增加每个对象的大小,并可能稍微降低性能。在这种情况下,由于Uncopyable的目的是为了提供禁止复制和赋值的能力,而不是为了提供多态行为,所以没有必要为它增加虚表的开销。综上所述,
Uncopyable基类的析构函数不需要是virtual的,因为它不是为了提供多态行为,而是为了禁止复制和赋值。
BTW,about private继承所带来的:子类继承了基类的实现,但并不继承基类的接口,如何理解?子类内部可访问基类成员,但子类对象不能调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
void baseFunction() {
std::cout << "Base function called" << std::endl;
}
};
class Derived : private Base {
public:
void derivedFunction() {
baseFunction(); // 正确:在Derived内部可以访问Base的成员
}
};
int main() {
Derived d;
d.derivedFunction(); // 输出:Base function called
// d.baseFunction(); // 错误:baseFunction在Derived中是私有的
return 0;
}
如果一定想调用呢?通过创建一个新方法,或使用using声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
void baseFunction() {
std::cout << "Base function called" << std::endl;
}
};
class Derived : private Base {
public:
using Base::baseFunction; // 使用 using 声明使 baseFunction 在 Derived 类中可访问
void derivedFunction() {
baseFunction(); // 现在这是正确的
}
};
int main() {
Derived d;
d.derivedFunction(); // 输出:Base function called
d.baseFunction(); // 现在这也是正确的
return 0;
}
07: Declare destructors virtual in polymorphic base classes
polymorphic base classes should declare a virtual destructor. 并且如果class中带有任何virtual函数,那么它就应该拥有一个virtual析构函数;
classes的设计目的如果不是作为base classes使用,或不是为了多态,就不该声明virtual函数
比如有一个记录时间的基类TimeKeeper,可能会有多种子类,AtomicClock,WaterClock,WristClock。并设计了一个工厂函数提供一个计时对象,工厂函数会返回一个base class指针,指向derived class对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TimeKeeper() {
public:
TimeKeeper();
~TimeKeeper(); // non-virtual
};
class AtomicClock: public TimeKeeper {...};
class WaterClock: public TimeKeeper {...};
class WristClock: public TimeKeeper {...};
TimeKeeper* getTimeKeeper();
// 为了遵守factory函数的规矩,被该函数返回的对象必须是在堆区。为了避免内存泄漏,需要delete
...
delete ptk;
条款13认为依赖于客户执行delete动作,本身就具有某种错误倾向;
此处更严重的问题是,工厂函数返回的指针指向一个derived class对象比如AtomicClock对象,但是对象却经由一个base class指针比如TimeKeeper* 来删除,而当前基类的析构函数是non-virtual的。会导致:对象的derived成分没被销毁,子类的析构函数也未能被运行,导致诡异的局部销毁,从而造成资源泄露。因此修改:
1
2
3
4
5
class TimeKeeper() {
public:
TimeKeeper();
virtual ~TimeKeeper(); //virtual
};
此外,像TimeKeeper这样的基类一般还会有除了析构函数以外的其他virtual函数,比如virtual getCurrentTime,在不同的子类中应该有不同的实现。再次强调:任何class只要带有virtual函数几乎确定也应该有一个virtual destructor。如果不打算将其作为base class,一般不需要使用virtual。
即使class完全没有virtual函数,non-virtual析构函数有时候也会带来问题:比如继承标准库的一些类如string,其析构函数是non-virtual的。
1
class SpecialString: puhblic std::string { // Really awful! }
假如程序无意中将一个pointer-to-SpecialString转换为pointer-to-string,并将转换所得的指针delete掉,就会导致行为不明确。
1
2
3
4
5
6
SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss; // 向上转型
...
delete ps; // 未定义!SpecialString的析构函数没有被调用
C++并没有像java一样的final关键字来禁止派生
关于pure virtual析构函数,必须为其提供定义。这个会在一些场景下提供一些便利,但是pure virtual会导致类变成抽象基类,无法实例化。析构函数的运作顺序:从most derived的类开始被调用,然后依次到最顶上的base class的析构函数。因此,编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,因此必须提供一个定义。否则导致链接错误。
1
2
3
4
5
6
class AWOV {
public:
virtual ~AWOV() = 0; // 纯虚析构函数
};
AWOV::~AWOV() {} // 纯虚析构函数的定义
08: Prevent exceptions from leaving destructors
C++不禁止析构函数吐出异常,但不建议这样做。比如一个容器装了多个对象,容器销毁时试图销毁对象们,但是对象们的析构吐出异常,多个异常导致不明确的行为。
假设有一数据库连接类,其中有一个方法void close(); 以便断开连接。为了避免客户忘记调用close方法,常用的做法是再写一个DBconn类来管理DBconnection对象。在DBconn的析构中调用DBconnection对象db的close方法。
1
DBconn dbc(DBconnection::create());
但是假如析构中报错,就会造成问题。有两个简单的应对方案:
1)如果close异常,就abort结束程序:
1
2
3
4
5
6
7
DBConn::~DBconn(){
try{
db.close();
}catch(..){
std::abort();
}
}
2)吞下异常
1
2
3
4
5
6
7
DBConn::~DBConn(){
try{
db.close();
}catch(...){
// 不处理,只是比如记录错误日志
}
}
两者都无法对close导致异常作出反应,更好的策略是重新设计DBConn接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DBConn{
public:
void close(){ // 供client使用的新函数
db.close();
closed=True;
}
~DBConn(){
if(!closed){
try{
db.close();
}catch(...){
// 记录日志,abort之类的
}
}
}
private:
DBConnection db; // 另外,我觉得这里,如果没有给移动构造函数的话,将会导致db在内存中有两个,造成资源浪费
bool closed;
};
如:
1
2
3
4
5
6
7
8
9
10
11
class DBConn {
public:
// 移动构造函数
DBConn(DBConnection&& dbConnection) noexcept : db(std::move(dbConnection)), closed(false) {}
// ... 其他成员函数和数据成员
private:
DBConnection db;
bool closed;
};
总结:
析构函数不应抛出异常。如果client需要对某个操作函数运行期间抛出的异常做出反应,class应该提供一个普通函数而非在析构函数中执行该操作。
09: Never call virtual functions during construction or destruction
绝不在构造和析构过程中调用virtual函数。
假如有如下类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Transaction{
public:
Transaction();
virtual void log() const =0;
...
};
Transaction::Transaction(){
...
log(); // 基类的构造函数中的最后调用记录日志
}
class BuyTransaction: public Transaction{
public:
virtual void log() const;
}
int main(){
BuyTransaction b;
}
此时,这段代码会导致致命的不明确行为的问题:
由于C++构造函数的调用顺序是,先调用基类的构造函数,再是子类。此时,基类构造函数所调用的log,实际上不是子类的版本,而是基类自己的。可以这么说:基类构造期间,virtual函数不是virtual的。
这个现象的原因是:基类构造过程中,此时子类的成员变量尚未初始化。如果此时调用的virtual函数下降至子类的版本,而子类版本的函数往往用的都是local成员变量,而这些成员变量还没有初始化。这就导致了未定义的行为。所以C++不允许这么搞。
更根本的原因是,子类对象的基类构造期间,对象的类型是基类!virtual函数会被编译器解析至基类,同时运行期类型信息也会把对象视为基类类型。因此在子类构造函数开始执行之前,它不会成为一个子类对象。
此外,检查是否构造和析构期间调用了virtual函数也可能遇到麻烦,当有多个构造函数的时候。因此,避免代码重复的做法就是把重复的逻辑代码放到init函数中,这会造成更隐蔽的问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Transaction{
public:
Transaction(){
init();
}
virtual void log() const =0;
...
private:
void init(){
...
log();
}
}
本质上还是在构造函数中调用virtual函数,但是编译器和连接器是检查不出来的。当然这里的虚函数log是纯虚的,那么执行的时候执行到这里想调用基类的log的时候,就会终止程序;但是如果不是纯虚的,只是个虚函数,即基类有实现的时候,一切看起来都在正常运行。对于程序员来说就难以发现为什么错误地调用了基类的函数版本。
一个解决方案是:
把log改为non-virtual,并要求子类构造函数传递必要信息给基类的构造函数,然后基类的构造函数就可以安全的执行non virtual log。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
// 基类
class Base {
public:
Base(const std::string& logInfo) {
log(logInfo); // 在构造函数中执行非虚拟 log 函数
}
void log(const std::string& info) const { // 非虚拟 log 函数
std::cout << info << std::endl;
}
};
// 子类
class Derived : public Base {
public:
Derived(const std::string& logInfo) : Base(logInfo) { // 传递参数给基类的构造函数
// ... 其他初始化代码
}
};
int main() {
Derived d("Logging from Derived constructor"); // 输出: Logging from Derived constructor
return 0;
}
关于上面这个写法:
首先想记录一下关于virtual和多态:之所以要用virtual是因为涉及到多态的需求,而多态是涉及到指针和引用操作,即使用基类的指针调用函数的时候,如果不是virtual的,就会导致调用到了基类的版本。但假如类型是明确的,Dog* d = new Dog();而不是 Animal* d = new Dog(); 基类子类的函数版本调用并不存在问题,也就不需要virtual。所以这里不能理解错乱。因此上述这段代码,使用non-virtual当然就不支持多态场景了。
使用了成员初始化列表。当然,构造函数的执行顺序还是先基类再子类,但是使用成员初始化列表可以让子类给基类的构造函数传递参数,而这种初始化行为是先于基类的构造函数执行的,也就是说基类构造执行前就已经拿到了这个初始化参数。这也正是成员初始化列表的意义:不像在构造函数内部进行赋值的做法,它先于构造函数体执行前就进行初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>
class Base {
public:
Base(const std::string& name) : name_(name) {
std::cout << "Base constructor called with name: " << name_ << std::endl;
}
private:
std::string name_;
};
class Derived : public Base {
public:
Derived(const std::string& name, int value) : Base(name), value_(value) {
std::cout << "Derived constructor called with value: " << value_ << std::endl;
}
private:
int value_;
};
int main() {
Derived d("example", 42); // 输出:
// Base constructor called with name: example
// Derived constructor called with value: 42
return 0;
}
此外,对于其他的子类成员变量初始化,基类的构造器执行总是先于子类成员初始化。
BTW,const成员和引用成员是不可以通过赋值来初始化的。
1
2
3
4
5
6
7
class Example {
public:
Example(int a, const std::string& str) : value_(a), name_(str) {}
private:
const int value_; // const 数据成员
std::string& name_; // 引用数据成员
};
10: Have assignment operators returns a reference to *this
令operator=返回一个给*this的引用
4 Designs and Declarations 设计与声明
18: Make interfaces easy to use correctly and hard to use incorrectly
开发一个接口,首先需要考虑client可能做出什么样的错误。
诸如:
1
2
3
4
5
6
7
class Date{
public:
Date(int month, int day, int year);
};
Date(3,30,1995); // True
Date(30,3,1995); // False
针对这种不值得拥有的代码的问题上,我们需要Type System来协助。导入简单的wrapper types来区分天数、月份和年份:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <iomanip>
struct Day
{
explicit Day(int d) : val(d) {}
int val;
};
struct Month
{
explicit Month(int m) : val(m) {}
int val;
};
struct Year
{
explicit Year(int y) : val(y) {}
int val;
};
class Date
{
private:
Month month;
Day day;
Year year;
public:
Date(const Month &m, const Day &d, const Year &y) : month(m), day(d), year(y){};
// 一般重载运算符作为类的非成员变量通过friend关键字来获取访问类的私有成员的权限
friend std::ostream &operator<<(std::ostream& os, const Date &date);
};
std::ostream &operator<<(std::ostream& os, const Date &date)
{
// 如果date.month这里不加.month是不行的,除非struct Month内部声明了Month是如何<<的
os << std::setw(2) << std::setfill('0') << date.month << '/'
<< std::setw(2) << std::setfill('0') << date.day.val << '/'
<< date.year.val;
return os;
}
int main(){
Date d(30, 3, 1995); // Wrong
Date d(Day(30), Month(3), Year(1995)); // Wrong
Date d(Month(3), Day(30), Year(1995)); // Right
}
此时,类型安全了,但是还需要限制其值,比如月份不能有13月:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Month {
public:
static Month Jan(){
return Month(1);
};
...
friend std::ostream& operator<<(std::ostream& os, const Month &month);
private:
// 也就是说不允许手动构造Month对象,而要通过类的静态方法来构造。
explicit Month (int m):month(m){};
int month;
};
std::ostream& operator<<(std::ostream& os, const Month &month){
os<< month.month;
return os;
}
int main(){
Date d(Month::Jan(), Day(5), Year(1995));
}
任何接口如果要求客户必须记得做哪些事情,就有着不正确使用的倾向,因为客户可能会忘记。比如Rule 13的factory函数返回一个指向Investment继承体系内的动态分配对象:
1
Investment* createInvestment();
为避免资源泄露,该指针最终必须被删除,这一逻辑导致了潜在的两个问题:
1)client忘记delete 2)delete了两次
因此,rule 13的做法是将返回值存储于一个智能指针内,auto_ptr or tr1::shared_ptr。
又有了新问题:如果client忘记将指针包装成智能指针?优秀的做法是:先发制人,令该工厂函数直接返回一个智能指针:
1
std::tr1::shared_ptr(Investment) createInvestment();
shared_ptr允许智能指针创建时,我们指定一个自定义的资源释放函数,deleter删除器。auto_ptr是不行的。如:
1
2
3
4
5
6
7
8
std::tr1::shared_ptr<Investment> createInvestment(){
std::tr1::shared_ptr<Investment> retVal(
// 想创建一个null shared_ptr,而0是一个int,虽然其可以被转换为指针 但为shared_ptr的构造器要求第一参数是一个真正的指针
static_cast<Investment* >(0), getRidOfInvestment);
retVal = ...;
return retVal;
}
注:如果业务逻辑上原始指针能够先于智能指针创建,直接将原始指针交给智能指针要比先把智能指针初始化再赋值要好,原因见rule 26。
还有一个优势是 解决了cross-dll problem 不懂。可被用来解除互斥锁 见rule 14
但是智能指针往往开销要高于原始指针,如:
1)更大的内存开销:引用计数和自定义删除器
2)运行时开销:拷贝和赋值发生时,引用计数更新
19:Treat class design as type design
设计class犹如设计type,需要考虑的问题:
-
新type的对象如何被创建和销毁?构造器、析构函数、内存分配函数、释放函数(operator new, operator new[], operator delete, operator delete[])
-
对象的初始化和赋值应该有什么差别?初始化和赋值的区别?Rule 4
-
如果对象以值传递意味着什么?copy构造函数用来定义pass by value如何实现
-
什么是新type的合法值?约束是什么,错误检查,异常抛出
-
需要配合某个继承图系吗inheritance graph。如是,则将受到既有class设计的束缚,特别是virtual和non-virtua的问题(Rule34&36)
-
新type需要什么样的转换?是否允许T1类型被隐式转换为T2,则T1内部需要operator T2。如果只允许explicit构造函数存在,则得写出专门负责执行的转换函数,且不得提供type conversion operators non-explicit-one-argument构造函数。(见rule 15的隐式和显示转换示例) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// type conversion operators class T1 { public: operator T2() const { // ...转换逻辑... return T2_converted_value; } }; class T2 { // ... }; int main() { T1 obj1; T2 obj2 = obj1; // 隐式转换 } // non-explicit-one-argument constructor // 注意,单参构造函数与copy构造函数很像,但有区别。单参重在从另一类型对象创建本类的新对象,进行类型转换操作。而copy构造函数是从同类的另一对象创建新对象。 class T2 { public: T2(const T1& obj) { // ...转换逻辑... } // ... }; int main() { T1 obj1; T2 obj2 = obj1; // 隐式转换 } // explicit显式转换,不允许隐式转换 class T2 { public: explicit T2(const T1& obj) { // ...转换逻辑... } // ... }; int main() { T1 obj1; T2 obj2 = obj1; // 错误:不允许隐式转换 T2 obj3(obj1); // 正确:显式转换 }
-
什么样的操作符和函数是合理的?(Rule 23,24,46)
-
什么样的标准函数应该驳回?private ,Rule 6
-
谁该取用新type的成员?成员public、protected or private?哪个类和函数应该是friend?
-
什么是新type的undeclared interface?对效率、异常安全(Rule29)和资源运用如多任务锁定和动态内存有何保证?
-
该type有多一般化?究竟是一个独特的class,还是一个class家族,是否需要使用class template
- 是否真的需要一个新type?是否能通过继承添加新功能就能满足需求?
20 Prefer pass-by-reference-to-const to pass-by value
以const引用传递替代值传递
缺省情况下C++以by value方式传递对象至函数。即:函数参数是以实参的副本为初值,而调用端获得的也是函数返回值的一个副本。
假如Person类具有两个String成员,Student类继承自Person类。student对象以值传递方式给函数validteStudent。总共的操作:copy构造student,copy构造student内部的两个string,copy构造person对象,copy构造person内部的两个string。再同样地进行析构。6次构造6次析构。
同时,引用传递还可以避免slicing对象切割的问题。
对象切割:
在C++中,当你将一个对象以值传递给一个函数时,会发生对象的切割(object slicing)。对象切割是指只有基类部分的数据被复制和传递给函数,而派生类特有的数据则被切割掉。这是因为目标类型(在本例中是基类)只有足够的空间来存储基类的数据,没有额外的空间来存储派生类特有的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
class Base {
public:
virtual void A() const {
std::cout << "Base::A" << std::endl;
}
};
class Derived : public Base {
public:
void A() const override {
std::cout << "Derived::A" << std::endl;
}
};
void callA(Base obj) {
obj.A();
}
int main() {
Derived d;
callA(d); // Output: Base::A
return 0;
}
在这个例子中,函数
callA接受一个Base类型的对象作为参数。当我们尝试将一个Derived对象传递给callA时,Derived对象被切割为Base对象,只有基类部分的数据被传递给callA函数。因此,当在callA函数中调用A方法时,Base类版本的A方法被调用,而不是Derived类版本的A方法。为了避免对象切割并保留多态行为,你应该使用指针或引用传递对象,而不是值传递。
如果callA的形参是引用传递,但是基类A函数没加virtual,会发生什么
如果
callA的形参是通过引用传递,但基类的A函数没有被声明为virtual,那么将发生静态绑定(static binding),而不是动态绑定(dynamic binding)。这意味着在编译时会确定要调用的函数版本,而不是在运行时。在这种情况下,即使你传递一个派生类对象给
callA函数,也会调用基类版本的A函数,而不是派生类版本的A函数。下面是一个例子来说明这种情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
class Base {
public:
void A() const { // 注意: 这里没有使用 virtual 关键字
std::cout << "Base::A" << std::endl;
}
};
class Derived : public Base {
public:
void A() const {
std::cout << "Derived::A" << std::endl;
}
};
void callA(Base& obj) { // 使用引用传递
obj.A();
}
int main() {
Derived d;
callA(d); // Output: Base::A
return 0;
}
但对于,内置类型、STL迭代器和函数对象,pass-by-value可以接受。
24: Declare non-member functions when type conversions should apply to all parameters.
若函数的所有参数都需要类型转换,为此采用non-member函数。
考虑如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
namespace myns{
class Rational{
public:
int enumerator() const{
return _enumerator;
}
int denominator() const{
return _denominator;
}
Rational(int enumerator=0, int denominator=1)
: _enumerator(enumerator), _denominator(denominator){}
const Rational operator* (const Rational& rhs const;)
private:
int _enumerator;
int _denominator;
};
Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.enumerator()/lhs.denominator()*rhs.enumerator()/rhs.denominator());
}
}
int main(){
// 这个文件说明了几件事:
// 1) 编译器在类内找不到对应的运算符,就会去同一命名空间下找nonmember函数成功运行
// 2) 当类构造器参数有初值,多个参数都有,那么是支持隐式类型转换的。如果没给初值多参数那不行。
// 3) 理应将成员变量private
myns::Rational r1, r2;
r1 = 3;
r2 = 2;
myns::Rational r3 = r1*r2;
myns::Rational r4 = 7*r3; // 编译器在operator*的传参过程中发生了隐式类型转换
std::cout<<r3.enumerator()<<std::endl;
std::cout<<r4.enumerator()<<std::endl;
std::cout<<r3.denominator()<<std::endl;
const Rational operator* (const Rational& rhs const;)这个member函数对于两个Rational对象相乘OK,对于Rational对象与int相乘,也ok,因为编译器会在形参传参这里调用构造函数进行隐式类型转换(因为编译器发现构造器中需要一个int就可以构造Rational对象)。但是int*Rational是失败的。因为int本身没有class,也就没有相应的operator*成员函数。
编译器也会在命名空间或者global作用域内找 non-member的operator*
当然,如果构造函数是explicit的,那么不管是Rational*2还是2*Rationa都不行。