C++类与对象:从基础到实践
立即解锁
发布时间: 2025-08-22 00:47:46 阅读量: 2 订阅数: 16 


C++编程语言精髓与实践
# C++ 类与对象:从基础到实践
## 1. 引言
在编程的世界里,创建新类型是一项至关重要的任务。C++ 为我们提供了强大的工具,用于定义和使用新类型,这些类型在逻辑上与内置类型相似,但在创建方式上有所不同。本文将深入探讨 C++ 中类和对象的相关知识,包括类的定义、成员函数、构造函数、析构函数等,以及对象的创建和使用方式。
## 2. 类的基础
### 2.1 成员函数
成员函数是类的重要组成部分,它可以直接访问类的成员变量。以日期类 `Date` 为例,最初我们可以使用结构体来定义日期的表示,并使用一组函数来操作日期变量:
```cpp
struct Date {
// 表示
int d, m, y;
};
void init_date(Date& d, int, int, int);
// 初始化 d
void add_year(Date& d, int n);
// 给 d 增加 n 年
void add_month(Date& d, int n);
// 给 d 增加 n 个月
void add_day(Date& d, int n);
// 给 d 增加 n 天
```
然而,这种方式没有明确的数据类型和函数之间的联系。我们可以通过将这些函数声明为成员函数来建立这种联系:
```cpp
struct Date {
int d, m, y;
void init(int dd, int mm, int yy);
// 初始化
void add_year(int n);
// 增加 n 年
void add_month(int n);
// 增加 n 个月
void add_day(int n);
// 增加 n 天
};
```
成员函数可以通过标准的结构体成员访问语法来调用,例如:
```cpp
Date my_birthday;
void f()
{
Date today;
today.init(16, 10, 1996);
my_birthday.init(30, 12, 1950);
Date tomorrow = today;
tomorrow.add_day(1);
// ...
}
```
在定义成员函数时,需要指定结构体的名称:
```cpp
void Date::init(int dd, int mm, int yy)
{
d = dd;
m = mm;
y = yy;
}
```
在成员函数中,可以直接使用成员名称,它会自动引用调用该函数的对象的成员。
### 2.2 访问控制
为了限制对类的数据结构的访问,我们可以使用 `class` 关键字,并使用 `public` 和 `private` 标签来分隔类的成员。例如:
```cpp
class Date {
int d, m, y;
public:
void init(int dd, int mm, int yy);
// 初始化
void add_year(int n);
// 增加 n 年
void add_month(int n);
// 增加 n 个月
void add_day(int n);
// 增加 n 天
};
```
`public` 部分构成了类对象的公共接口,非成员函数不能直接访问 `private` 成员。例如:
```cpp
inline void Date::add_year(int n)
{
y += n;
}
void timewrap(Date& d)
{
d.y -= 200;
// 错误: Date::y 是私有的
}
```
限制对数据结构的访问有很多好处,例如可以更容易地定位错误,并且在修改类的表示时,只需要修改成员函数,而不需要修改用户代码。
### 2.3 构造函数
构造函数用于初始化类的对象。使用 `init()` 函数来初始化对象不够优雅且容易出错,更好的方法是使用构造函数。构造函数的名称与类名相同,例如:
```cpp
class Date {
// ...
Date(int, int, int);
// 构造函数
};
```
当类有构造函数时,所有对象都会通过构造函数调用进行初始化。如果构造函数需要参数,则必须提供这些参数:
```cpp
Date today = Date(23, 6, 1983);
Date xmas(25, 12, 1990);
// 缩写形式
Date my_birthday;
// 错误: 缺少初始化器
Date release1_0(10, 12);
// 错误: 缺少第 3 个参数
```
为了提供多种初始化方式,可以定义多个构造函数:
```cpp
class Date {
int d, m, y;
public:
// ...
Date(int, int, int);
// 日, 月, 年
Date(int, int);
// 日, 月, 今天的年份
Date(int);
// 日, 今天的月和年
Date();
// 默认日期: 今天
Date(const char*);
// 字符串表示的日期
};
```
构造函数遵循与其他函数相同的重载规则,编译器会根据参数类型选择正确的构造函数。
### 2.4 静态成员
静态成员是类的一部分,但不属于类的对象。它可以用于解决依赖全局变量的问题。例如,我们可以重新设计日期类,使用静态成员来保存默认日期:
```cpp
class Date {
int d, m, y;
static Date default_date;
public:
Date(int dd = 0, int mm = 0, int yy = 0);
// ...
static void set_default(int dd, int mm, int yy);
// 将默认日期设置为 Date(dd,mm,yy)
};
```
我们可以定义构造函数来使用 `default_date`:
```cpp
Date::Date(int dd, int mm, int yy)
{
d = dd ? dd : default_date.d;
m = mm ? mm : default_date.m;
y = yy ? yy : default_date.y;
// 检查日期是否有效
}
```
静态成员可以像其他成员一样引用,也可以不提及对象,而是使用类名来限定其名称:
```cpp
void f()
{
Date::set_default(4, 5, 1945);
// 调用 Date 的静态成员 set_default()
}
```
静态成员(包括函数和数据成员)必须在某个地方进行定义,定义时不需要重复 `static` 关键字。
### 2.5 复制类对象
默认情况下,对象可以被复制。类对象可以通过复制初始化或赋值来复制。例如:
```cpp
Date d = today;
// 通过复制进行初始化
void f(Date& d)
{
d = today;
}
```
默认的复制语义是成员逐个复制。如果这不是所需的行为,可以定义复制构造函数 `X::X(const X&)` 和赋值运算符来提供更合适的行为。
### 2.6 常量成员函数
为了提供查看日期值的方法,我们可以添加读取日、月和年的函数,并将这些函数声明为常量成员函数:
```cpp
class Date {
int d, m, y;
public:
int day() const { return d; }
int month() const { return m; }
int year() const;
// ...
};
```
常量成员函数不会修改对象的状态,编译器会捕获意外修改对象状态的尝试。常量成员函数可以被常量和非常量对象调用,而非常量成员函数只能被非常量对象调用。
### 2.7 自引用
为了使状态更新函数可以链式调用,我们可以让它们返回对更新后对象的引用。例如:
```cpp
class Date {
// ...
Date& add_year(int n);
// 增加 n 年
Date& add_month(int n);
// 增加 n 个月
Date& add_day(int n);
// 增加 n 天
};
Date& Date::add_year(int n)
{
if (d==29 && m==2 && !leapyear(y+n)) {
// 注意 2 月 29 日
d = 1;
m = 3;
}
y += n;
return *this;
}
```
`*this` 表达式引用调用成员函数的对象,`this` 是指向调用该函数的对象的指针。在非常量成员函数中,`this` 的类型是 `X*`,在常量成员函数中,`this` 的类型是 `const X*`。
### 2.7.1 物理和逻辑常量性
有时候,一个成员函数在逻辑上是常量的,但仍然需要修改成员的值。例如,日期类可能有一个返回字符串表示的函数,为了避免重复计算,可以使用缓存。我们可以使用 `const_cast` 来实现这一点,但这种方法可能会导致未定义行为:
```cpp
class Date {
bool cache_valid;
string cache;
void compute_cache_value();
// 填充缓存
// ...
public:
// ...
string string_rep() const;
// 字符串表示
};
string Date::string_rep() const
{
if (cache_valid == false) {
Date* th = const_cast<Date*>(this);
// 去除常量性
th->compute_cache_value();
th->cache_valid = true;
}
return cache;
}
```
### 2.7.2 Mutable
为了避免显式类型转换和实现依赖的行为,我们可以将缓存管理中涉及的数据声明为 `mutable`:
```cpp
class Date {
mutable bool cache_valid;
mutable string cache;
void compute_cache_value() const;
// 填充(可变)缓存
// ...
public:
// ...
string string_rep() const;
// 字符串表示
};
string Date::string_rep() const
{
if (!cache_valid) {
compute_cache_value();
cache_valid = true;
}
return cache;
}
```
`mutable` 表示即使在常量对象中,该成员也可以被更新。
### 2.8 结构体和类
结构体实际上是成员默认公开的类。例如,下面的 `Date1` 和 `Date2` 声明是等价的:
```cpp
class Date1 {
int d, m, y;
public:
Date1(int dd, int mm, int yy);
void add_year(int n);
// 增加 n 年
};
struct Date2 {
private:
int d, m, y;
public:
Date2(int dd, int mm, int yy);
void add_year(int n);
// 增加 n 年
};
```
使用哪种风格取决于具体情况和个人喜好。通常,对于所有数据都是公开的类,我们可以使用结构体。
### 2.9 类内函数定义
在类定义内定义的成员函数被视为内联成员函数,适用于小而频繁使用的函数。但这种方式可能会导致一些混淆,例如:
```cpp
class Date {
// 可能会引起混淆
public:
int day() const { return d; }
// 返回 Date::d
// ...
private:
int d, m, y;
};
```
为了避免这种混淆,我们可以将数据成员放在前面,或者在类定义后定义内联成员函数:
```cpp
class Date {
public:
int day() const;
// ...
private:
int d, m, y;
};
inline int Date::day() const { return d; }
```
## 3. 高效的用户定义类型
### 3.1 设计高效的日期类
为了构建一个简单而高效的日期类,我们可以这样设计:
```cpp
class Date {
public:
// 公共接口:
enum M { jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };
class Bad_date { };
// 异常类
Date(int dd = 0, M mm = M(0), int yy = 0);
// 0 表示“选择默认值”
// 用于检查日期的函数:
int day() const;
M month() const;
int year() const;
string string_rep() const;
// 字符串表示
void char_rep(char s[]) const;
// C 风格字符串表示
static void set_default(int, M, int);
// 用于更改日期
```
0
0
复制全文
相关推荐










