类(class)是 C++ 中定义用户自定义类型的工具。
类把:

  • 数据(成员变量)
  • 操作数据的函数(成员函数)

封装在一起。

类的核心思想:

  • 数据抽象:只暴露“能做什么”,不强调“怎么做”
  • 封装:把实现细节隐藏起来,只开放必要接口

类的基本概念

一个类通常包含两部分:

  • 接口:提供给用户使用的操作
  • 实现:数据成员、内部逻辑、私有辅助函数

使用类时,应关注:

  • 这个类能做什么
  • 如何正确调用它的接口

而不是它内部如何实现。

类的定义

类定义描述了:

  • 这个类型有哪些成员
  • 这个类型支持哪些操作

基本形式:

1
2
3
4
5
6
class MyClass {
public:
// 公有接口
private:
// 私有实现
};

说明:

  • 类定义以 ; 结束
  • 成员可以是:
  • 数据成员
  • 成员函数
  • 类型别名
  • 常量等

classstruct

二者都能定义类,区别主要在默认访问权限

  • class:默认 private
  • struct:默认 public

例如:

1
2
3
4
5
6
7
class A {
int x; // 默认 private
};

struct B {
int x; // 默认 public
};

对象的定义和使用

类定义好后,就可以用它创建对象。

1
2
3
4
5
6
7
class Sales_data {
public:
std::string bookNo;
unsigned units_sold = 0;
};

Sales_data item;

说明:

  • 类名就是一种类型名
  • 用类定义的变量叫对象

访问成员:

1
2
item.bookNo = "C++";
item.units_sold = 10;

若是指针:

1
2
Sales_data *p = &item;
p->units_sold = 20;

成员函数与 this

成员函数是定义在类中的函数,用于操作类对象。

1
2
3
4
5
6
class Person {
public:
std::string name() const { return name_; }
private:
std::string name_;
};

this 指针

每个非静态成员函数都隐式带有一个 this 指针,指向调用该函数的对象

例如:

1
2
3
4
5
6
7
8
class A {
public:
void set(int x) {
val = x; // 等价于 this->val = x;
}
private:
int val = 0;
};

理解:

  • 在成员函数中直接访问成员,实际上是通过 this 访问
  • this 的类型通常可理解为指向当前对象的指针

const 成员函数

若成员函数不会修改对象状态,应在函数末尾加 const

1
2
3
4
5
6
class Person {
public:
std::string name() const { return name_; }
private:
std::string name_;
};

含义:

  • 该函数承诺不修改对象的数据成员(除 mutable 成员外)
  • const 对象、const 引用、const 指针只能调用 const 成员函数

构造函数

构造函数用于初始化对象

特点:

  • 名字与类名相同
  • 没有返回类型
  • 对象创建时自动调用
  • 可以重载

例如:

1
2
3
4
5
class MyClass {
public:
MyClass() { }
MyClass(int x) { }
};

构造函数初始化列表

推荐使用初始化列表初始化成员:

1
2
3
4
5
6
7
class MyClass {
public:
MyClass(int i, int j) : i_(i), j_(j) { }
private:
int i_;
int j_;
};

作用:

  • 在对象真正创建时直接初始化成员
  • 比“先默认初始化,再在函数体中赋值”更高效、也更规范

为什么初始化列表重要

以下成员必须用初始化列表初始化:

  • const 成员
  • 引用成员
  • 没有默认构造函数的类类型成员

例如:

1
2
3
4
5
6
7
class A {
public:
A(int x) : ref(x), c(10) { }
private:
int &ref;
const int c;
};

成员初始化顺序

成员初始化顺序由“成员声明顺序”决定,不由初始化列表顺序决定。

例如:

1
2
3
4
5
6
7
class A {
public:
A() : j(i), i(10) { } // 实际先初始化 i,再初始化 j
private:
int i;
int j;
};

因此建议:

  • 初始化列表顺序与成员声明顺序保持一致

默认构造函数

默认构造函数指不需要实参就能调用的构造函数。

例如:

1
2
3
4
class A {
public:
A() = default;
};

编译器生成的默认构造函数

只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。

一旦你自己定义了其他构造函数,编译器通常不再自动生成默认构造函数

例如:

1
2
3
4
5
6
class A {
public:
A(int x) { }
};

A a; // 错误,没有默认构造函数

= default

如果想保留编译器默认行为,可以显式要求生成:

1
2
3
4
5
class A {
public:
A() = default;
A(int x) { }
};

委托构造函数

一个构造函数可以调用同类中的另一个构造函数,这叫委托构造函数

1
2
3
4
5
6
7
class A {
public:
A() : A(0) { }
A(int x) : val(x) { }
private:
int val;
};

说明:

  • 委托构造函数的初始化列表中只能写类名本身
  • 实际初始化工作由被委托的构造函数完成

优点:

  • 减少重复代码
  • 统一初始化逻辑

对象的初始化方式

类对象常见初始化方式:

1
2
3
4
5
6
A a;        // 默认初始化
A b(); // 注意:这是函数声明,不是对象
A c(10); // 直接初始化
A d = 10; // 拷贝初始化
A e{10}; // 列表初始化
A f = {10}; // 拷贝列表初始化

注意:

  • A b(); 是经典易错点:它声明了一个函数
  • 列表初始化可避免某些窄化转换

访问控制与封装

C++ 用访问说明符控制成员可见性:

  • public:对外可访问,构成类的接口
  • private:仅类内部可访问,隐藏实现
  • protected:主要用于继承(基础阶段可先了解)

例如:

1
2
3
4
5
6
class A {
public:
void set(int x) { val = x; }
private:
int val = 0;
};

封装的意义:

  • 防止外部随意改内部状态
  • 降低耦合
  • 便于后续修改实现而不影响使用者

友元

类可以授权某些函数或类访问自己的非公有成员,这些被授权者叫友元

友元函数

1
2
3
class MyClass {
friend int func(int, int);
};

特点:

  • 友元不是类成员
  • 但能访问类的 private / protected 成员

友元类

1
2
3
4
5
class B;

class A {
friend class B;
};

表示 B 的成员函数可以访问 A 的非公有成员。

友元的特点

  • 友元声明写在类内
  • 不受所在访问说明符位置影响
  • 友元关系不传递
  • 友元关系不继承
  • 若是一组重载函数,需分别声明为友元

类与 const

const 成员函数

常量对象只能调用常量成员函数:

1
2
3
4
5
6
7
8
9
class A {
public:
int get() const { return val; }
private:
int val = 0;
};

const A a;
a.get(); // 正确

若成员函数未加 const,就不能被 const 对象调用。

返回 *this

成员函数可以返回 *this,常见于链式调用:

1
2
3
4
5
6
7
8
9
class A {
public:
A& set(int x) {
val = x;
return *this;
}
private:
int val = 0;
};

若用于常量对象,通常还会写 const 重载版本。

类的静态成员

静态成员属于类本身,不属于某个具体对象。

分为:

  • 静态数据成员
  • 静态成员函数

静态数据成员

1
2
3
4
class Account {
public:
static double rate;
};

特点:

  • 所有对象共享同一份静态成员
  • 不存储在对象内部
  • 生命周期贯穿整个程序

静态成员函数

1
2
3
4
5
6
class Account {
public:
static double getRate() { return rate; }
private:
static double rate;
};

特点:

  • 不依赖具体对象
  • 没有 this 指针
  • 不能声明为 const
  • 不能直接访问非静态成员

访问静态成员

推荐用类名访问:

1
double r = Account::getRate();

也可以用对象或指针访问,但本质上还是类成员:

1
2
3
4
5
Account a;
Account *p = &a;

a.getRate();
p->getRate();

静态数据成员的定义

静态数据成员通常需要在类外定义一次:

1
double Account::rate = 0.05;

注意:

  • 类内只是声明
  • 类外才是定义
  • 不能再写 static

类内初始化静态成员

对于某些静态常量整型成员,可以类内初始化:

1
2
3
4
class A {
public:
static const int size = 10;
};

现代 C++ 中,也常见:

1
2
3
4
class A {
public:
static constexpr int size = 10;
};

类的声明与前向声明

类可以先声明、后定义:

1
class A;   // 前向声明

此时 A不完全类型

不完全类型能做的事很有限,常见允许:

  • 声明指向它的指针或引用
  • 声明以它为参数/返回值的函数

不能做的事:

  • 定义该类型对象
  • 访问其成员
  • 知道其大小

例如:

1
2
3
class A;
A *p; // 正确
// A obj; // 错误,不完全类型

类作用域

每个类有自己的作用域。
在类外访问成员时通常要借助:

  • . / -> 访问普通成员
  • :: 访问类作用域中的名字

类外定义成员函数时要加类名限定:

1
2
3
4
5
6
7
class A {
public:
void f();
};

void A::f() {
}

类内定义与类外定义

定义在类内的成员函数通常是隐式 inline 的。

1
2
3
4
5
6
class A {
public:
int get() const { return val; } // 隐式 inline
private:
int val = 0;
};

若函数较复杂,通常在类外定义,更清晰:

1
2
3
4
5
6
7
8
9
10
class A {
public:
int get() const;
private:
int val = 0;
};

int A::get() const {
return val;
}

隐式类类型转换

如果类有一个只接受一个实参的构造函数,那么它可能定义了一种从该参数类型到类类型的隐式转换

例如:

1
2
3
4
5
6
7
8
class A {
public:
A(int x) : val(x) { }
private:
int val;
};

A a = 10; // 允许,发生隐式转换

这种构造函数常称为转换构造函数

explicit

为了禁止这种隐式转换,可把构造函数声明为 explicit

1
2
3
4
5
6
class A {
public:
explicit A(int x) : val(x) { }
private:
int val;
};

效果:

1
2
A a(10);    // 正确,直接初始化
// A b = 10; // 错误,不能隐式转换

说明:

  • explicit 主要用于单参数构造函数
  • 只能阻止隐式转换,不影响直接初始化

聚合类

聚合类是一种可直接用花括号初始化成员的简单类。

典型条件(基础复习可先记这版):

  • 所有成员都是 public
  • 没有用户定义的构造函数
  • 没有虚函数、虚基类等复杂特性

例如:

1
2
3
4
5
6
struct Data {
int x;
double y;
};

Data d{1, 3.14};

特点:

  • 初始化顺序与成员声明顺序一致
  • 适合简单数据集合

字面值常量类

若一个类能用于常量表达式相关场景,就可能是字面值常量类

复习时先记核心条件:

  • 数据成员类型本身得适合常量表达式
  • 类通常需要至少一个 constexpr 构造函数
  • 构造过程要足够简单、可在编译期求值

constexpr 构造函数

构造函数也可以是 constexpr

1
2
3
4
5
6
7
8
class Debug {
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
private:
bool hw;
bool io;
bool other;
};

作用:

  • 可用于构造 constexpr 对象
  • 成员初始化必须满足常量表达式要求

定义抽象数据类型

类最常见的用途之一,就是定义抽象数据类型(ADT)

  • 对外只暴露必要操作
  • 内部数据受保护
  • 保证对象始终处于合法状态

设计类时建议:

  1. 先想清楚“对象表示什么”
  2. 再确定“对象能做什么”
  3. 最后决定“哪些数据该隐藏”

一个完整的小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p)
: bookNo(s), units_sold(n), revenue(n * p) { }

std::string isbn() const { return bookNo; }

Sales_data& combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

这个类体现了:

  • 数据成员放在 private
  • 接口放在 public
  • 用构造函数控制初始化
  • const 成员函数保证只读操作
  • 返回 *this 支持链式调用

易错点总结

class 默认是 privatestruct 默认是 public

构造函数没有返回类型

包括 void 也不能写。

成员初始化顺序看“声明顺序”,不看初始化列表顺序

const 成员、引用成员必须在初始化列表中初始化

一旦自定义了构造函数,编译器通常不再自动生成默认构造函数

若需要,显式写:

1
A() = default;

const 对象只能调用 const 成员函数

静态成员函数没有 this

因此:

  • 不能访问非静态成员
  • 不能声明为 const

静态数据成员通常要在类外定义一次

1
double Account::rate = 0.05;

友元不是成员

只是“被授权访问”。

单参数构造函数可能触发隐式转换

若不希望这样,使用 explicit

前向声明只能声明指针/引用,不能定义对象

1
2
3
class A;
A *p; // 对
// A a; // 错