EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

C++ 高级程序设计-21-封装

OOP

概念

  • Program = Object1 + Object2 +…… + Objectn
  • Object: Data + Operation
  • Message: function call
  • Class

分类

  • 面向对象(Object-Oriented)
  • 基于对象(Objected-Based):Ada 语言,没有继承

评价标准

  • 开发效率
  • 软件质量。外部质量和内部质量提升。

类包含成员变量和成员函数。

C++ 中还分为头文件和源文件,与 C++ 的编译有关。

头文件中只有函数签名。

头文件 a.h

1
2
3
4
5
6
7
8
class TDate {
public:
void SetDate(int y, int m, int d);
int IsLeapYear();

private:
int year, month, day;
};

源文件 a.cpp

1
2
3
4
5
6
7
8
void TDate::SetDate(int y, int m, int d) {
year = y;
month = m;
day = d;
}
int TDate::IsLeapYear() {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

也可以像 java 一样,函数体和声明写在一起。这样的写法在建议编译器将函数调用编译成内联函数。

1
2
3
4
5
6
7
8
9
10
11
12
class TDate {
public:
void SetDate(int y, int m, int d) {
year = y;
month = m;
day = d;
}
int IsLeapYear() { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }

private:
int year, month, day;
};

绝大多数情况下希望大家分为头文件和源文件写。

一般把成员变量声明为私有,成员函数声明为公有。

构造函数

描述

  • 与类同名、无返回类型
  • 自动调用,不可直接调用
  • 可重载
  • 默认构造函数无参数,当类中未提供默认构造函数时,编译系统提供。构造函数主要工作需要完成对象内存初始化。
  • 可以声明为公有或私有。私有构造函数用于实现单例。

构造函数调用

自动调用

1
2
3
4
5
6
7
8
9
10
11
12
class A {
public:
A();
A(int i);
A(char *p);
};

A a1 = A(1); A a1(a); A a1 = 1; // 三者等价,调用 A(int i)
A a2 = A(); A a2; // 调 A(),注意不能写成: A a2()
A a3 = A("abcd"); // 调用 A(char *p)
A a[4]; // A()
A b[5] = {A(), A(1), A("abcd"), 2, "xyz"};

成员初始化表

构造函数的补充。先于构造函数体执行,按类数据成员声明次序,而不是初始化表次序。该机制有利于减轻编译器负担。

C++98 标准中,只有 static const 常量才能在类内部初始化。因此该标准中,对于 const 成员和引用成员,只能使用初始化表初始化,不能使用赋值语句初始化。

C++11 允许非静态成员在声明处初始化,类似 java。

1
2
3
4
5
6
7
8
class CString {
char* p;
int size;

public:
CString(int x)
: size(x), p(new char[size]) {} // 调用 new 时,size 还未初始化
};
1
2
3
4
5
6
7
8
9
class A {
int x;
const int y;
int& z;

public:
A()
: y(1), z(x), x(0) { x = 100; } // 常量和引用的初始化表
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
int m;

public:
A() { m = 0; }
A(int m1) { m = m1; }
};
class B {
int x;
A a;

public:
B() { x = 0; }
B(int x1) { x = x1; }
B(int x1, int m1)
: a(m1) { x = x1; }
};
void main() {
B b1; //调用 B::B() A::A()
B b2(1); //调用 B::B(int) A::A()
B b3(1, 2); //调用 B::B(int,int) A::A(int)
}

在构造函数中尽量使用成员初始化表取代复制动作。

  • 常量成员、引用成员、对象成员
  • 效率高
  • 数据成员太多时,不采用本条准则,这会降低可维护性。

析构函数

描述

析构函数声明为 ~<className>(),对象消亡时自动调用。 栈上的对象所在作用域结束时消亡,堆上的对象需要手动清除。

Java 的 GC 机制存在效率障碍,不适用于实时性要求很高的程序(金融交易程序)。

C++ 使用 RAII(Resource Acquisition Is Initialization,资源获取即对象初始化)机制。C++ 获取的资源(如文件、连接和数据库)被封装在对象当中,获取资源即创建对象,对象消亡时资源自动被释放。

调用

析构函数声明为私有,强制自主控制对象存储分配,只能分配在堆上而不能分配在栈上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
A();
void destroy() { delete this; }
static void free(A *p) { delete p; }

private:
~A();
};
A a; // 该对象应当自动消亡,但此处无法调用析构函数,编译报错
int main() {
A aa; // 该对象应当自动消亡,但此处无法调用析构函数,编译报错
A* p = new A; // 堆上对象,编译通过
p->destroy();

// Better Solution: 该方式在 p 为空指针时不会报错。
A::free(p);
}

拷贝构造函数

描述

拷贝构造函数(Copy Constructor),在创建对象时,用同一类的对象对其初始化,自动调用。用于把对象的资源拷贝给另一对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
A a;
A b = a; // 赋值语句,调用拷贝构造函数

f(A a) { ... }
A b;
f(b); // 传递实参,调用拷贝构造函数

A f() {
A a;
return a; // 返回对象,调用拷贝构造函数
}

f();

声明为:

1
2
public:
A(const A& a); // 一定要写引用

默认拷贝构造函数,逐个成员初始化(member-wise initialization),对于对象成员,该定义时递归的。

一般深拷贝需要构造函数。

调用

包含成员对象的类:

  • 默认拷贝构造函数:调用成员对象的拷贝构造函数
  • 自定义拷贝构造函数:调用成员对象的默认构造函数

该规则体现了 C++ 的理念:程序员不做的事情,编译器提供默认规则;程序员接管的事情,编译器不再干预。

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
class A {
int x, y;

public:
A() { x = y = 0; }
void inc() {
x++;
y++;
}
};
class B {
int z;
A a;

public:
B() { z = 0; }
B(const B& b) { z = b.z; } // 此处对 this.a 调用默认构造函数,而非默认拷贝构造函数
void inc() {
z++;
a.inc();
}
};

int main() {
B b1; // b1.z=b1.a.x=b1.a.y=0
b1.inc(); // b1.a.x=b1.a.y=b1.z=1
B b2(b1); // b2.z=1, b2.a.x=0, b2.a.y=0
}

移动构造函数

移动构造函数(move constructor),声明为 A(A&&)。用于把对象的资源移动给另一个对象,原对象不再使用。

右值引用

1
2
3
int x = 5;
int &y = x;
const int &z = x; // 常量引用
  1. 右值引用可以绑定到右值上,能改变值,不能绑定到左值上
  2. const 引用只能绑定到左值,不能改变值
  3. const 引用可以绑定到左值和右值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
int val;
void setVal(int v) {
val = v;
}
};

A getA() {
return A();
}

//知道风险,并且想要改变新对象,就使用右值引用&&
int main() {
int a = 1;
int &ra = a; // OK,非 const 引用绑定左值
const A &cra = getA(); // OK,const 引用绑定右值
A &&aa = getA(); // OK,右值引用绑定右值
A &ab = getA(); // ERROR,引用不能绑定右值
}

实例

1
2
3
string::string (string &&s): p(s.p) {
s.p = nullptr;
}

默认移动构造函数

没有自定义拷贝构造函数和析构函数时,编译器生成默认移动构造函数。

五三原则

拷贝构造函数、移动构造函数、析构函数(和拷贝赋值函数、移动赋值函数)三个(五个)方法中,有一个自定义,则其他函数不会产生自定义版本。

动态内存

创建对象

1
2
3
4
5
6
7
8
9
int* a = new int;
A* p = new A; // 调用默认构造函数
A* q = new A(1); // 调用 A(1)



int *p1 = new int[5]; // 默认不进行初始化
int *p2 = new int[5](); // 进行默认初始化
int *p3 = new int[5]{0,1,2,3,4}; // 进行显式对应函数初始化

对象删除

1
2
delete intPtr;
intPtr = NULL;

动态对象数组

1
2
3
A* p;
p = new A[100]; // 调用 100 次构造函数
delete []p; // 调用 100 次析构函数

元素个数在 p 前的一个 4 字节大小的空间内存储。不能显式初始化,相应的类必须有默认构造函数。delete 中的[]不能省。

动态二维数组

略。

const 成员

const 成员变量

略。

const 成员函数

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
void f();
void show() const;
}

int main() {
const A a;
a.f(); // 编辑错误,const 对象调用非 const 成员函数
a.show();
}
1
2
3
4
void show(const A* const this);
// 第一个 const 修饰指针,表示 this 指针不可修改;
// 第二个 const 修饰 this,表示 this 指向的对象不可修改;
// 函数签名当中的 const 相当于参数当中第二个 const

引用成员变量可以被 const 成员函数修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
int a;
int& indirect_int;

public:
A()
: indirect_int(*new int) { ... }
~A() { delete &indirect_int; }
void f() const {
indirect_int++; // 编译通过, const 成员函数可以修改引用成员变量
const_cast<A*> this -> a = 1; // 编译通过,强制通过地址修改对象内容
}
};

mutable 关键字修饰的变量,可以在 const 成员函数中修改。

静态成员

描述

静态成员变量,由类对象共享,有唯一的拷贝,遵循类访问控制。

1
2
3
4
5
6
class A {
int x, y;
static int shared;
}

int A::shared = 0; // 定义一般放在实现文件中

静态成员函数,只能存取静态成员变量,调用静态成员函数。

单例

原则:谁创建,谁归还。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class singleton {
protected:
singleton() {}
singleton(const singleton&);

public:
static singleton* instance() {
return m_instance == NULL ? m_instance = new singleton : m_instance;
}
static void destroy() {
delete m_instance;
m_instance = NULL;
}

private:
static singleton* m_instance;
};

singleton* singleton::m_instance = NULL;

友元

描述

对象外部不能访问该类的私有成员。需要通过私有方法,会降低对私有成员的访问效率,缺乏灵活性。

友元可以访问 privateprotected 的成员。

友元函数、友元类、友元类成员函数。

一个全局函数是一个类的友元,如果在这之前没有声明也是可以进行声明友元函数。

1
2
3
4
5
6
7
8
9
10
void func();
class B;
class C {
void f();
};
class A {
friend void func(); //友元函数
friend class B; //友元类
friend void C::f(); //友元类成员函数
};

友元类函数在完整的类声明出现前不能声明友元函数。因为数据的一致性:避免对应类里面没有这个函数(也就是 C 的完整定义必须有)并且成员函数依赖于类

1
2
3
4
5
6
7
class Vector; // 两个相互适使用的类
class Matrix {
friend void multiply(Matrix& m, Vector& v, Vector& r); // 此处参数必须写 Vector&, 而不能写 Vector,因为之前没有 Vector 类的完整声明,不知道类的大小,无法拷贝空间
};
class Vector {
friend void multiply(Matrix& m, Vector& v, Vector& r);
};

友元不具有传递性。

迪米特法则

迪米特(Demeter)法则:避免将 data member 放在公共接口中,同时努力让接口完满且最小化。

总结

1
2
3
4
5
6
7
8
9
class Empty {
// 编译器为空类提供的函数:
Empty(); // 默认构造函数
Empty(const Empty&); // 默认拷贝构造函数
~Empty(); // 默认析构函数
Empty& operator=(const Empty&); // 默认拷贝赋值函数
Empty *operator&(); // 默认重载取对象地址
const Empty *operator&() const; // 默认重载取常量对象地址
};