前言

本章开始说明面向对象编程的核心概念,即的概念,但是并不打算详细介绍面向对象的编程思想。

从面向过程编程到面向对象编程是一种思想上的转变,需要经过时间和实践的积累,言语上不必多言,只总结一下其大体特点如下。

抽象

封装 -- 数据隐藏

多态

继承

代码可重用

代码易扩展

其具体指导思想会在设计模式模块进行详细论述。

是一种将__抽象__转换为__用户定义类型__的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
29
30
31
32
// Object.h
#pragma once
#include <string>
using std::string;

struct vec3 {
double x, y, z;
};

class Object {
public:
void set_name(string name) { m_name = name; } // 内联方法
string get_name();

void update(float dt);
void show();

protected:

private:
int m_id; // 编号
string m_name; // 名字
vec3 m_position; // 位置
vec3 m_size; // 大小
vec3 m_color; // 颜色
vec3 m_velocity; // 速度
};

inline string Object::get_name() { // 写在外面的内联方法
return m_name;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Object.cpp
#include <iostream>
#include "Object.h"
using std::cout;
using std::endl;

void Object::update(float dt) {
m_position.x += m_velocity.x;
m_position.y += m_velocity.y;
m_position.z += m_velocity.z;
}

void Object::show() {
cout << "name: " << m_name << endl;
}

访问控制

防止程序直接访问数据被称为数据隐藏。c++类通过访问控制关键字publicprivateprotected来控制外部访问其数据的权利。

public

使用类对象的程序都可直接访问其公有部分。

private

只能通过公有成员函数友元函数(11章)来访问对象的私有成员。

类对象的默认访问控制为private,而结构体struct默认访问控制为public。这大概是类和结构体的唯一区别了。

protected

protecedprivate类似。

其区别与继承有关,将在第13章讨论。

构造函数

标准构造函数

目前来说,还不能像基本类型和结构体那样使用初始化表初始类的成员变量,因为private的成员变量不可访问。

而标准构造函数产生的原因的便是想使类也可以像初始化表那样初始化其成员变量。

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
// 标准构造函数的声明 -- 无返回类型 -- 名称与类名相同
Object(int id, string name, vec3 pos, vec3 s, vec3 color, vec3 vel);

// 标准构造函数的定义 -- 给成员变量赋初值 -- 或进行一些其它的初始化工作
Object::Object(int id, string name, vec3 pos, vec3 s, vec3 color, vec3 vel) {
m_id = id;
m_name = name;
m_position = pos;
m_size = s;
m_color = color;
m_velocity = vel;
}

// 标准构造函数的使用
// 显式调用标准构造 -- 编译器可能会自动创建一个临时变量,再赋值给obj, 也可能不会
Object obj = Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1});
// 隐式调用标准构造
Object obj(1,"ddd", vec3{1, 1, 1}, vec3{1, 1, 1}, vec3{1, 1, 1}, vec3{1, 1, 1});
obj.set_name("dua");
obj.update(1);
obj.show();

// 也可结合new使用
Object* obj = new Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1});
obj->set_name("dua");
obj->update(1);
obj->show();

注意,类对象是无法调用构造函数的。

接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值,如下所示。

1
Object obj = 1;                 // 如果有构造函数 Object(int)的话

但是这样做可能导致问题,后面会使用赋值构造函数替代。

默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。

1
2
3
4
Object obj;                 // 此时调用默认构造函数, 下同
Object obj = Object();
Object* obj = new Object;
Object* obj = new Object();

注意Object* obj();是不对的,这实际是一个函数声明。

如果没有提供任何构造函数,则c++将自动提供默认构造函数,但此函数理论上不做任何工作。

一旦为类提供了构造函数,就必须提供默认构造函数,否则Object obj;此句将报错。

只能有一个默认构造函数,且此默认构造函数不接受任何参数。

我们可能通过给已有的构造函数的所有参数提供默认值,来隐式提供默认构造函数。

1
2
Object(int id=1,const string& name="dd",vec3 pos={1,1,1},
vec3 s={1,1,1},vec3 color={1,1,1},vec3 vel={1,1,1});

也可以通过函数重载定义一个无参数的构造函数作为默认构造函数。

1
2
3
4
5
6
7
8
9
Object();
Object::Object() {
m_id = 0;
m_name = "dd";
m_position = {1,1,1};
m_size = {1,1,1};
m_color = {1,1,1};
m_velocity = {1,1,1};
}

析构函数

析构函数用来完成清理工作。

静态类对象在程序结束时自动调用其析构函数。自动类对象在其所在代码块运行结束时自动调用析构函数。使用new创建的类对象将在使用delete释放内存时自动调用析构函数。

有时程序会创建临时对象,此情况下程序会在结束对此临时对象的使用时自动析构。

通常不会显式调用析构函数(12章有例外)。

如果程序员没有定义析构函数,编译器将隐式地声明一个默认析构函数。

1
2
3
// 析构函数 -- 无返回值 -- 名称为 ~ + 类名 -- 无参数
~Object();
Object::~Object() {}

this指针

this指针指向用来调用成员函数的对象。

this指针被作为隐藏参传递给成员函数,且只能是第一个参数。

在成员函数后面加const的实质是给第一个参数即this指针加`const属性。

1
2
3
4
const Object& Object::min_id(const Object& b) const {
if(m_id > b.m_id) return b;
return *this;
}
1
2
3
void Object::show() const;
// 其C风格定义为
void show(const Object* this);

使用类对象

类对象的声明与内置类型的声明类似。

1
Object obj;

可以像结构体一样操作类成员和方法,但其受访问控制限制。

每个新对象都有自己的存储空间,用于存储内部变量和类成员。

但同一个类的所有对象共享同一组类方法,即每种方法只有一种副本。

类声明和类方法构成了__服务器__,供用户使用。而客户程序员将使用服务器提供的类,进行一些操作,此即__客户端__。

修改类方法的实现时,只要接口没变,便不影响客户端的运行,这也是OOP的核心思想之一。

1
2
3
4
5
6
7
8
9
10
#include "Object.h"

int main() {

Object obj;
obj.set_name("dua");
obj.show();

return 0;
}

对象数组

声明对象数组的方法同声明标准变量数组的方法相同。

1
Object objs[maxn];

首先使用默认构造创建每个元素对象。花括号里的构造函数将创建临时对象。然后将临时对象复制到相应元素中。即要想创建类对象数组,这个类必须有默认构造函数。

1
2
3
4
5
Object objs[4] = {
Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1}),
Object()
};
// 第一个和第二个分别用花括号里的两个构造函数构造,剩下的使用默认构造

类作用域

在类定义中的名称(成员变量和成员函数)的作用域是整个类。这是有别于第九章所介绍的作用域之外的一种新型的作用域。

在类作用域内直接使用const是行不通的,在创建对象之前没有用于存储其值的空间。

1
2
const int maxn = 100;
Object* child[maxn]; // 会报错

可以使用枚举来满足这种需求。

1
2
enum { maxn = 100 };
Object* child[maxn]; // 正确

使用枚举并不会创建类成员,maxn只是一个符号名称。

由于此枚举的作用只是创建符号常量,不用创建变量,故可省略名称。

除此之外,我们也可以使用关键字static来创建静态变量。

1
2
static const int maxn = 100;
Object* child[maxn]; // 正确

此时maxn为静态变量,与其他静态变量存储在一起,而不存储在对象中。

作用域内枚举(c++11)

传统枚举的作用域为整个文件,这可能会导致两个枚举定义中的枚举量发生冲突。

1
2
enum egg {small, medium, large};
enum shirt {small, medium, large}; // 报错,与egg冲突

c++11提供了一种新的枚举量,其作用域为类,使用class(或struct)关键字声明。

1
enum class egg {small, medium, large};

使用该种枚举量的时候,我闪需要使用枚举名来限定枚举量。

1
egg x = egg::small;

类作用域内枚举不会自动转换类型,所以需要手动强制转换。

1
cout << int(x) << endl;

枚举的默认底层实现为int,可使用如下方式改变底层实现,此法也适用于传统枚举。

1
enum class egg:short { small, medium, large};

内联函数

内联函数的编译代码与其他的程序代码内联到一块了,即编译器使用内联函数里的代码来直接替代函数调用,从而不需要像函数调用那样跳来跳去。

所以内联函数是在编译的时候将函数调用使用其实现代码替换的过程。内联函数的运行速度比常规函数快,但需要更多的内存。

在处理函数调用机制所占时间比执行函数代码的时间还长时,使用内联可节约大量的时间,即对代码执行很短,但调用非常频繁的函数,最好使用内联。

使函数变为内联的方法为在函数声明前加关键字inline同时在函数定义前也加inline。但是一般内联函数在声明的同时也直接定义,比较省事。如下代码所示。

1
inline int max(int a, int b) { return a > b; }

除此之外,内联函数与宏定义都是一种代码替换过程,但是有所区别。内联函数按值传递参数,即如果参数为表达式,则函数将传递表达式的值。而宏定义则是通过文本替换来实现的,即使传递的是表达式,也会原封不动地进行文本替换。

阅读全文 »

函数和二维数组

1
2
3
4
5
6
7
8
9
10
int sum(int (*arr)[2], int n) {
int res = 0;
for(int i = 0; i < n; ++i)
for(int j = 0; j < 2; ++j) res += arr[i][j];
return res;
}

int a[3][2] = { {1, 2}, {3, 4}, {5, 6} };
cout << sum(a, 3) << endl;

使用二维数组作为参数,必须指定第二维的维数,用来表示元素的类型。

如上代码所示,表示arr为一个数组名,而数组的每一个元素也是一个数组,由2个int组成,即arr的类型是指向元素类型为2个int的数组的指针。

其中的括号必不可少,因为int *arr[2]表示由2个int型指针组成的数组,但函数参数不能为数组。

除此之外,也可以写为int sum(int arr[][2], int n);,二者含义相同。

只要记住arr为指针而非数组,即函数的参数不能为数组即可。

函数和字符串

字符串作参数

1
2
3
4
5
6
7
8
9
10
void show_str(const char* str) {
while (*str) {
cout << *str;
++str;
}
cout << endl;
}

char str[] = "hello world";
show_str(str);

可以将字符串作为参数,使用char*(指向char类型的指针)作为类型。

1
void show_str(const char* str);

也可使用如下格式,意义相同。

1
void show_str(const char str[]);

字符串作返回值

返回字符串的地址即可。即返回指向字符串首地址的指针。 注意不能返回栈空间的内存,因为函数执行完后便释放了,所以需要使用new关键字申请堆空间的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

char* build_str(char ch, int n) {
char* p_str = new char[1+n]; // 虽然p_str在函数结束时会被释放
p_str[n] = '\0';
while(n-- > 0) p_str[n] = ch;
return p_str; // 但 new 的内存还在
}

int main() {

char* str = build_str('d', 10);
cout << str << endl;
delete [] str; // 所以需要手动delete此内存块

}

函数和结构体

在函数里,我们只要像对待基本数据类型一样对待结构体即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

struct pos {
int x, y;
};

pos sum_pos(pos a, pos b) {
pos res;
res.x = a.x + b.x;
res.y = a.y + b.y;
return res;
}

int main() {
pos m{1, 2};
pos n = {3, 4};

pos r = sum_pos(m, n);
cout << r.x << " " << r.y << endl;

}

我们可以将结构体的地址作为参数或者返回值,以节省时间和空间,但是要注意不能返回栈空间的局部变量的临时内存。

1
2
3
4
5
6
7
8
9
10
11
pos* sum_pos(const pos* a, const pos* b) {
pos* res = new pos;
res->x = a->x + b->x;
res->y = a->y + b->y;
return res;
}

pos* r = sum_pos(&m, &n);
cout << r->x << " " << r->y << endl;
delete r;

也可以将结果作为指针直接放在参数中,而不用返回值。

1
2
3
4
5
6
7
void sum_pos(const pos* a, const pos* b, pos* res) {
res->x = a->x + b->x;
res->y = a->y + b->y;
}
pos r;
sum_pos(&m, &n, &r);
cout << r.x << " " << r.y << endl;

stringarrayvector等类对象,都可看作基本类型,像结构体一样在函数中使用。

递归函数

1
2
3
4
5
// 求n的阶乘
int factorial(int n) {
if(n == 1) return 1; // 循环终止条件
return n * factorial(n-1); // 递归
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 快速排序 -- O(nlogn)
void quick_sort(int a[], int L, int R) { //[L, R]
if(L >= R) return; // 循环终止条件
int x = L, y = R, p = a[L];
while(x < y) { //最后找到的x即为划分地
while(x < y && a[y] >= p) --y; a[x] = a[y]; //y向左到比p小的,移到左边去
while(x < y && a[x] <= p) ++x; a[y] = a[x]; //x向右到比p大的,移到右边去
}// 结束划分
a[x] = p;
quick_sort(a, L, x-1); quick_sort(a, x+1, R); // 递归
}

int a[] { 5, 3, 2 ,1, 4};
quick_sort(a, 0, 4);
for(int i = 0; i < 5; ++i) cout << a[i] << endl;

除了main函数外,函数都可以调用自身,这种在实现中调用自身的函数称为递归函数。

一个递归函数必须有递归调用的终点,否则将会无限调用下去。

每一个递归过程中的函数都是独立的,递归的过程与普通函数调用的过程是一样的。我们只需将函数里对自己本身的调用也看作是对其他函数的调用,只不过功能相同而已,就可以很清楚地理解递归函数了。

函数指针

c++中,函数名即为函数的地址(不带括号)。 如有一个函数bool cmp(int a, int b);,则cmp即为该函数在内存中的地址。

声明

1
2
bool cmp(int a, int b);         // 函数的声明
bool (*p_cmp)(int a, int b); // 函数指针的声明

如上代码所示,由于(*p_cmp)cmp等价,所以(*p_cmp)也是函数,则p_cmp就是指向该函数指针了。

如果不加括号的话,bool *p_cmp(int a, int b);表示一个返回bool*的函数,而非函数指针。所以声明函数指针一定要加括号。

赋值

我们可以将具体的某个函数赋值给函数指针,前提是该函数的参数和返回类型都与函数指针的一致。

1
p_cmp = cmp

调用

我们可以使用函数指针来调用其指向的函数。可以使用(*pf)调用,也可以直接使用指针名作为函数名调用。

下面两句的含义相同。

1
2
cout << (*p_cmp)(1, 2) << endl;
cout << p_cmp(1, 2) << endl;

下面代码为使用函数指针的一个绝佳例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

bool cmp(int a, int b) { return a > b; }
bool cmp_2(int a, int b) { return a <= b; }

//返回两个数的较大值 -- 自定义比较规则
int max_i(int a, int b, bool (*p_cmp)(int a, int b)) {
if(p_cmp(a, b)) return a;
else return b;
}

int main() {
cout << max_i(1, 2, cmp) << endl;
cout << max_i(1, 2, cmp_2) << endl;
}

其输出结果如下。

1
2
2
1

以 函数指针为元素 的数组

1
int (*p_s[3])(int, int);

[]的优先级高于*,故(*p_s[3])表示一个包含三个指针的数组。

指向 函数指针数组 的指针

我们将上面的p_s替换成(*p_ps)

1
int (*(*p_ps)[3])(int, int);        // 有些小变态

加括号表示p_ps是一个指针,它指向一个包含3个元素的数组。

使用typedef简化函数指针

使用typedef可声明函数指针的别名,使代码更容易理解。如下所示。

1
typedef int (*p_fun)(int, int);

这样p_fun就可以看作一个数据类型来使用了,而之前的函数指针数组和指向函数指针数组的指针都可以使用如下方式声明。

1
2
3
p_fun p1 = s1;
p_fun pa[3] = {s1, s2, s3};
p_fun (*pd)[3] = &pa;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int s1(int a, int b) { return a + b + 0; }
int s2(int a, int b) { return a + b + 1; }
int s3(int a, int b) { return a + b + 2; }

int main() {
// 函数指针的数组
int (*p_s[3])(int, int) = { s1, s2, s3};

for(int i = 0; i < 3; ++i) {
cout << p_s[i](1, 2) << " "; // 等价于 cout << (*p_s[i])(1, 2) << endl;
}

// 指向 函数指针数组 的指针
// 也可用 auto p_ps = &p_s; 简化
int (*(*p_ps)[3])(int, int) = &p_s;
for(int i = 0; i < 3; ++i) {
cout << (*p_ps)[i](1, 2) << " "; // 等价于 cout << (*(*p_ps)[i])(1, 2) << endl;
}

}

使用using简化函数指针

c++11增加了using关键字,也可以用来取别名,类似于typedef。如下所示。

1
using p_fun = void(*)(int a, int b);

这样写的好处是可以清楚地看到p_fun的数据类型便是右边的表达式,将名称与类型完全分离开了。

定义及声明

函数是语句执行的空间,通常一个c++程度会由许多函数组成,c语言更是如此。通过调用函数执行函数中的代码,使程序按照一定的流程进行。由此可见一个函数可以被多次使用,简化了重复的过程的代码量,所以说函数就是功能的实现。

函数的一般格式如下。

1
2
3
4
5
// 返回值类型 函数名 参数列表
type_name function_name (parament_list) {
statements;
return value; // value的类型为type_name
}

如下示例代码,分别包含了函数的声明、定义和调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <fstream>
#include <cassert>
using namespace std;

int sum(int a, int b); // 函数声明

int main() {
cout << sum(1, 2) << endl; // 函数调用
return 0;

}

int sum(int a, int b) { // 函数定义
return a + b;
}

函数可在声明处直接定义,但不可以在main函数之后声明,否则识别不出。函数的返回值类型可为整型、浮点型、指针、字符、结构体、对象等。当函数返回值类型为void时不需要有return语句,但可在必要的地方使用return语句来直接跳出函数体。

函数返回值不可为数组,但可以将数组作为结构体或对象的组成部分来返回。

函数原型声明时的参数列表里可不包括变量名,也不必与定义时的变量名一致。

阅读全文 »

if语句

如果括号里的条件判断语句的值为true,则执行该分支的代码块,否则不执行该分支的代码块。

1
if(test) statement;
1
2
if(test) { statement_1; }
else { statement_2; }
1
2
3
4
if(test_1) { statement_1; }
else if(test_2) { statement_2; }
else if(test_3) { statement_3; }
else { statement_4; }
阅读全文 »