Chapter 4 Object Interaction¶
1 Introduction¶
面向对象程序的基本看法
- 对象之间通过发送/接收消息来协作
- 消息由发送者构造、由接收者解释、最终由成员函数实现
- 消息可能返回结果,也可能改变接收者状态(side effects)
- 封装(Encapsulation):把数据和操作这些数据的方法打包到对象中,并隐藏实现细节
- 抽象(Abstraction):忽略底层细节,只关注更高层的问题描述
- 模块化(Modularization):把整体拆成边界清晰、可独立构造和检查的模块
Clock Display Example
一个 ClockDisplay 可以由 hours 和 minutes 两个 NumberDisplay 组成:
class ClockDisplay {
NumberDisplay hours;
NumberDisplay minutes;
// constructor and methods...
};
class NumberDisplay {
int limit;
int value;
// constructor and methods...
};
2 Constructors and Destructors¶
2. 1 构造函数(constructor)¶
- 只要类定义了构造函数,编译器就在对象创建时自动调用
构造函数自动调用
class X {
int i;
public:
X();
};
void f() {
X a; // 在这里自动调用 a.X()
}
- 构造函数名与类名相同
- 构造函数可以带参数,用于指定对象的初始状态
带参数构造
class Tree {
public:
Tree(int i) { /* ... */ }
};
Tree t(12);
2. 2 析构函数(destructor)¶
- 负责清理(cleanup),和初始化同样重要
- 名字形式为
~ClassName(),没有参数,当对象离开作用域时由编译器自动调用
析构函数声明
class Y {
public:
~Y();
};
存储分配与构造调用
- 编译器会在进入作用域时为该作用域中的对象预留存储
- 但构造函数要等到对象定义语句真正执行到时才调用
2. 3 聚合初始化(Aggregate Initialization)¶
- 数组和简单聚合类型可以直接用花括号初始化
数组与结构体初始化
int a[5] = {1, 2, 3, 4, 5};
int b[6] = {5}; // 其余元素补 0
int c[] = {1, 2, 3, 4}; // 自动推断长度
struct X { int i; float f; char c; };
X x1 = {1, 2.2, 'c'};
X x2[3] = { {1, 1.1, 'a'}, {2, 2.2, 'b'} };
- 能不带参数调用的构造函数就是默认构造函数(default constructor)
带构造函数的聚合初始化
struct Y {
float f;
int i;
Y(int a);
};
// 全初始化,不需要默认构造函数
Y y1[] = { Y(1), Y(2), Y(3) };
// 第二个元素没有显式初始化参数,需要默认构造函数
Y y2[2] = { Y(1) };
// 完全没有提供初始化列表,需要默认构造函数
Y y3[7];
// 没有提供任何初始化参数,需要默认构造函数
Y y4;
自动默认构造函数(auto default constructor)
- 若类完全没有定义任何构造函数,编译器才会自动生成默认构造函数
- 一旦你自己声明了构造函数,编译器通常不再自动补默认构造
3 Fields, Local Variables and this¶
局部变量 vs. 字段
- Field(字段/成员变量):定义在成员函数外,跟随对象整个生命周期存在
- Local variable(局部变量):定义在函数/方法内部,只在调用期间存在
- 字段具有类作用域,可在该类任意成员函数中访问
示例
int TicketMachine::refundBalance() {
int amountToRefund = balance;
balance = 0;
return amountToRefund;
}
amountToRefund的生命周期只持续到函数结束balance属于对象状态,只要对象活着就一直存在- 如果局部变量与字段同名,局部变量会遮蔽字段
this 指针
- 每个非静态成员函数都有一个隐藏参数
this this的类型是指向当前对象的指针- 成员函数调用本质上是把调用者对象地址传进去
this 的等价理解
void Point::print();
// 可理解为
void Point::print(Point* this);
Point a;
a.print();
// 可理解为
Point::print(&a);
4 Initializer List¶
- 初始化列表规则可初始化任意类型的数据成员,比在构造函数体内赋值更直接、更高效
- 真正的初始化顺序由成员声明顺序决定,与初始化列表中的顺序无关
- 析构顺序与初始化顺序相反
成员初始化列表
class Point {
private:
const float x, y;
public:
Point(float xa, float ya)
: y(ya), x(xa) {}
};
初始化 vs. 赋值
Student::Student(string s) : name(s) {} // 初始化
Student::Student(string s) { name = s; } // 赋值
- 初始化发生在进入构造函数体之前
- 赋值发生在构造函数体内部
- 若采用赋值写法,成员对象必须先能默认构造
5 Function Overloading and Default Arguments¶
- 函数重载(Function overloading):同名函数,只要参数列表不同即可共存
重载示例
void print(char* str, int width); // #1
void print(double d, int width); // #2
void print(long l, int width); // #3
void print(int i, int width); // #4
void print(char* str); // #5
print("Pancakes", 15); // #1
print("Syrup"); // #5
print(1999.0, 10); // #2
print(1999, 12); // #4
print(1999L, 15); // #3
重载与自动类型转换
void f(int i);
void f(double d);
f('a'); // 可能歧义
f(2); // 匹配 int
f(2L); // 可能歧义
f(3.2f); // 匹配 double
- 默认参数(Default arguments):写在函数声明中,调用时省略实参,编译器自动补上默认值
默认参数必须从右往左连续提供
int harpo(int n, int m = 4, int j = 5);
int chico(int n, int m = 6, int j); // illegal
int groucho(int k = 1, int m = 2, int n = 3);
beeps = harpo(2); // 等价于 harpo(2, 4, 5)
beeps = harpo(1, 8); // 等价于 harpo(1, 8, 5)
beeps = harpo(8, 7, 6);
6 Const Objects and Const Member Functions¶
const对象只能调用const成员函数,const成员函数不能修改对象状态
示例
int Date::set_day(int d) {
// ...error check d here...
day = d; // ok, non-const so can modify
}
int Date::get_day() const {
day++; // ERROR modifies data member
set_day(12); // ERROR calls non-const member
return day; // ok
}
const成员函数声明和定义中都要重复写const,非修改型成员函数应尽量声明为const
示例
int get_day() const;
int get_day() const { return day };
const / non-const 对象对比
Date when(1, 1, 2001);
int day = when.get_day(); // OK
when.set_day(13); // OK
const Date birthday(12, 25, 1994);
int day = birthday.get_day(); // OK
birthday.set_day(14); // ERROR
- 类中的常量成员必须在初始化列表中初始化
类中的编译期常量
class HasArray {
static const int size = 100;
int array[size]; // ERROR!
};
定义普通数组时,大小必须是编译期常量,const int size 是非静态的 const 成员变量,属于每一个对象,不是编译期常量,编译器无法确定数组大小,故会报错。
方案 1:将该常量设为 static(静态)
// 该成员整个类仅共享一份实例(而非每个对象各有一份)
static const int size = 100;
方案 2:使用匿名枚举的变通写法(经典 Hack)
class HasArray {
// 枚举成员本身就是编译期常量
enum { size = 100 };
int array[size];
};
7 Inline Functions¶
- 普通函数调用有开销:压参数、压返回地址、准备返回值、恢复现场
- 内联函数(inline functions)试图把函数体直接展开到调用点,减少调用开销
示例
inline int f(int i) {
return i * 2;
}
int main() {
int a = 4;
int b = f(a); // 被展开为 int b = a + a;
}
- inline 函数定义通常写在头文件中,可能不会在目标文件(.obj/.o)中生成任何可执行代码
- inline 关键字本身是声明性修饰,而非定义,不必担心因
#include带来传统意义的重复定义 - 内联函数是以空间为代价,换取运行速度的提升
inline比宏安全,有类型检查,没有宏替换副作用
inline vs. macro
#define unsafe(i) ((i) >= 0 ? (i) : -(i))
inline int safe(int i) {
return i >= 0 ? i : -i;
}
- 在类定义内部直接写出的成员函数,默认就是
inline
访问函数(Access functions)
- 小型函数,作用是读取或修改对象的部分状态,也就是对象的一个或多个内部成员变量。
class Cup {
int color;
public:
int getColor() { return color; }
void setColor(int color) {
this->color = color;
}
};
- 小函数、频繁调用函数适合考虑
inline,大函数、递归函数通常不适合 inline关键字不保证函数内联,编译器可能判定函数体积过大或检测到函数存在递归调用(内联函数不支持递归,或递归函数几乎无法被内联),也可能是使用的特定编译器本身就未实现该内联特性。
8 Type of Function Parameters and Return Value¶
8. 1 参数传入方式(way in)¶
- 值传递
void f(Student i);- 函数内创建原对象的副本,修改不影响外部对象,有拷贝开销
- 指针传递
void f(Student *p);- 不修改对象时加 const,即
void f(const Student *p);
- 引用传递
void f(Student& i);- 不修改对象时加 const,即
void f(const Student& i);
8. 2 返回值传出方式(way out)¶
- 返回对象
Student f();- 返回时创建新对象,安全无悬空风险
- 返回指针
Student* f();- 指针必须指向有效内存,禁止指向局部变量
- 返回引用
Student& f();- 引用必须绑定有效对象,禁止引用局部变量
8. 3 内存管理难点¶
示例
char *foo() {
char *p = new char[10];
strcpy(p, "something");
return p;
}
void bar() {
char *p = foo();
printf("%s", p);
delete p;
}
- 内存分配(
new)与释放(delete)分离,易漏释放导致内存泄漏 - 要求调用者必须手动释放,责任不明确
注意
- 需要存储对象 → 用值传递传入
- 仅读取对象值 → 传入 const 指针/引用
- 需要修改对象 → 传入指针/引用
- 函数内新建对象 → 直接返回对象(而非指针)
- 返回指针/引用 → 只能指向传入的对象
- 绝对禁止:在函数内 new 内存并返回指针