类与对象
绪论
写一个很小的程序时,我们可以把变量和函数都放在一起,程序变大以后,数据与操作数据的函数容易散落各处。类的作用,就是把一个事物的数据和它能完成的操作组织成一个整体。例如,一个学生既有姓名、分数等状态 ,也有修改分数、判断是否及格等行为。把它们放进 Student 类,程序会更接近我们理解现实事物的方式。

类、对象与封装
从蓝图到具体对象
- 类:描述一类事物共同的数据和操作,是一种用户自定义类型
- 对象:按照类创建出的具体实例
- 数据成员:保存对象的状态
- 成员函数:描述对象可以完成的操作

类像一张蓝图,对象才是按蓝图造出的具体产品。定义 Student 类并不会自动产生某个学生,写出 Student a; 后才创建了对象 a。
类的基本语法
c++
class 类名
{
private:
// 私有成员:主要用于保存内部状态
public:
// 公有成员:向类外提供操作接口
protected:
// 保护成员:主要供本类和派生类使用
}; // 这里的分号不能漏
三个访问说明符的基本含义:
| 成员位置 | 本类成员函数 | 派生类成员函数 | 普通类外代码 |
|---|---|---|---|
| public | 可以 | 可以 | 可以 |
| protected | 可以 | 可以 | 不可以 |
| private | 可以 | 不可直接访问 | 不可以 |
注意:这里的派生类不能直接访问基类 private 成员,不等于这些成员消失了。它们仍位于对象的基类部分,只能借助基类公开或保护的接口间接访问。
封装不是简单隐藏
封装有两个层次:
- 把相关的数据与函数组织在同一个类中
- 隐藏内部实现,只允许通过受控接口修改状态
自动售货机不会让顾客直接碰内部电路和库存,而是提供投币口和按钮。
同样,类把内部数据设为 private,再用 public 成员函数检查输入、维护规则。

示例:不会进入非法状态的成绩类
c++
#include <iostream>
using namespace std;
class Score
{
private:
int value;
public:
explicit Score(int v = 0);
bool set(int v);
int getValue() const;
bool passed() const;
};
Score::Score(int v) : value(0){ set(v); }
bool Score::set(int v)
{
if(v < 0 || v > 100)return false;
value = v;
return true;
}
int Score::getValue() const{ return value; }
bool Score::passed() const{ return value >= 60; }
int main()
{
Score score(85);
if(!score.set(101))cout<<"修改失败:成绩必须在 0~100 之间"<<endl;
cout<<"当前成绩:"<<score.getValue()<<endl;
cout<<(score.passed() ? "及格" : "不及格")<<endl;
return 0;
}
对应输出为:
text
修改失败:成绩必须在 0~100 之间
当前成绩:85
及格
逐步理解:
- value 是私有成员,类外不能写 score.value = 101;
- 构造函数先把 value 设为 0,再调用 set(v)
- set 检查参数是否合法,传入 101 时返回 false
- 修改失败后,原来的合法状态 85 没有被破坏
- getValue() 和 passed() 后面的 const 表示它们不会修改当前对象
这才是封装的重要价值,不仅防止别人看见数据,更要让对象始终保持合法状态。
class 与 struct 的区别
在 C++ 中,两者都可以拥有数据成员、成员函数、构造函数、继承和虚函数。最常考的默认规则是:
| 写法 | 成员的默认访问权限 | 继承的默认方式 |
|---|---|---|
| class | private | private |
| struct | public | public |
c++
class A
{
int x; // 默认 private
};
struct B
{
int x; // 默认 public
};
工程中通常用 struct 表达主要保存数据的简单结构,用 class 表达需要维护规则和隐藏实现的对象,但这是一种习惯,不是语法强制要求。
构造函数、初始化列表与析构函数
对象的出生与离开
- 构造函数在对象创建时负责初始化对象
- 析构函数在对象生命周期结束时负责收尾

构造函数的语法与特点
c++
class Point
{
private:
int x;
int y;
public:
Point() : x(0), y(0) {}
Point(int xx, int yy) : x(xx), y(yy) {}
};
构造函数具有以下特点:
- 名称与类名相同
- 没有返回类型,连 void 也不能写
- 可以重载,只要参数列表能够区分
- 创建对象时由语言规则调用,不能像普通成员函数那样对一个已经存在的对象"再构造一次"
Point p; 调用无参构造函数;Point q(3, 4); 调用带两个参数的构造函数。
默认构造函数
能够在不提供实参时被调用的构造函数,称为默认构造函数。下面是两种不同的设计方式:
c++
Point(); // 方式一:无参默认构造函数
Point(int x = 0, int y = 0); // 方式二:所有参数都有默认值
不要在同一个类中同时提供上面两个版本,否则 Point p; 不知道该选谁,会发生调用二义性。
如果类没有声明任何构造函数,编译器通常会隐式声明默认构造函数。一旦用户声明了构造函数,编译器通常不会再额外生成可无参调用的默认构造函数。
c++
class Point
{
public:
Point(int x, int y) {}
};
Point a; // 编译错误:没有 Point()
Point b(1, 2); // 正确
需要默认构造时,可以明确写出:
c++
Point() = default;
但只有当所有需要默认初始化的基类和成员也允许这样做时,= default 才可用。
初始化列表
c++
class Student
{
private:
const int id;
string name;
public:
Student(int number, string studentName) : id(number), name(studentName) {}
};
冒号后的 id(number), name(studentName) 就是成员初始化列表。格式是:成员名(用于初始化它的表达式),初始化列表不是只能写在类内。构造函数在类外定义时同样可以使用:
c++
class Point
{
private:
int x;
int y;
public:
Point(int xx, int yy);
};
Point::Point(int xx, int yy) : x(xx), y(yy) {}
以下情况必须或应优先使用初始化列表:
- 初始化 const 数据成员
- 初始化引用成员
- 调用没有默认构造函数的成员对象
- 初始化基类部分
- 避免先默认构造、再赋值造成的重复工作
成员初始化顺序
c++
class Demo
{
private:
int first;
int second;
public:
Demo() : second(first + 1), first(10) {}
};
尽管列表先写 second,真正的顺序仍是先初始化 first、再初始化 second,因为 first 在类中先声明。为避免误读和警告,初始化列表最好也按照成员声明顺序书写为:Demo() : first(10), second(first + 1) {}。
析构函数
析构函数名称是在类名前加 ~:
c++
class FileGuard
{
public:
~FileGuard()
{
// 对象离开时的收尾工作
}
};
析构函数:
- 没有返回类型
- 没有参数
- 不能重载,一个类至多有一个析构函数
- 普通局部对象离开作用域时自动析构
- 使用 new 创建的对象,要在相应所有权规则下用 delete,或交给智能指针管理
如果没有显式编写析构函数,编译器会生成一个。它不是简单的什么都不做,其中的 string、vector 等类类型成员仍会被正确析构。
观察对象生命周期
先不要运行,试着在纸上预测输出。
c++
#include <iostream>
#include <string>
using namespace std;
class Trace
{
private:
string name;
public:
explicit Trace(string n) : name(n)
{
cout<<"构造:"<<name<<endl;
}
~Trace()
{
cout<<"析构:"<<name<<endl;
}
};
void test()
{
Trace local("普通局部对象");
static Trace once("静态局部对象");
{
Trace inner("内层对象");
}
}
int main()
{
test();
cout<<"----- 第二次调用 -----"<<endl;
test();
cout<<"----- main 即将结束 -----"<<endl;
return 0;
}
对应输出为:
c++
构造:普通局部对象
构造:静态局部对象
构造:内层对象
析构:内层对象
析构:普通局部对象
----- 第二次调用 -----
构造:普通局部对象
构造:内层对象
析构:内层对象
析构:普通局部对象
----- main 即将结束 -----
析构:静态局部对象
关键观察:
- local 每次进入 test() 都会构造,离开函数时析构
- inner 离开内层大括号时立即析构
- once 只在第一次执行到它的定义时构造一次
- 静态局部对象在程序正常结束时析构
this 指针、常成员与静态成员
this 的含义
每个非静态成员函数都针对某个具体对象执行。函数内部可以使用隐含的 this 指针,它指向正在调用这个函数的对象。
c++
class Box
{
private:
int width;
public:
void setWidth(int width)
{
this->width = width;
}
};
在 box.setWidth(10); 中,this 指向 box。当形参与成员同名时,this->width 能清楚表示成员,右侧 width 表示参数。
return *this 与链式调用
this 是指针,*this 就是当前对象。若成员函数返回当前对象的引用,就可以连续调用:
c++
#include <iostream>
using namespace std;
class Counter
{
private:
int value;
public:
explicit Counter(int v = 0) : value(v) {}
Counter &add(int delta)
{
this->value += delta;
return *this;
}
Counter &reset()
{
this->value = 0;
return *this;
}
int getValue() const
{
return this->value;
}
};
int main()
{
Counter counter;
counter.add(3).add(5).add(2);
cout<<counter.getValue()<<endl; // 10
counter.reset().add(7);
cout<<counter.getValue()<<endl; // 7
return 0;
}
对应输出为:
c++
10
7
为什么返回 Counter& 而不是 Counter?返回引用时,下一次调用仍作用于原对象,如果按值返回,可能产生一个副本,语义和开销都会不同。
函数末尾的 const
c++
int getValue() const;
末尾的 const 表示这个非静态成员函数承诺不修改当前对象的普通数据成员。常对象只能调用常成员函数(静态成员函数另当别论):
c++
const Counter counter(10);
cout<<counter.getValue(); // 正确:getValue() 是常成员函数
counter.add(5); // 编译错误:add() 可能修改对象
只读函数应尽量写成常成员函数,它带来两点好处:
- 常对象和指向常对象的引用可以调用
- 编译器帮助检查函数没有意外修改状态
常成员函数中,this 可理解为指向常对象的常指针,所以不能通过它修改普通成员。
避免混淆两种 const 指针
c++
Counter normal(10);
Counter other(20);
Counter *const p1 = &normal; // 常指针:p1 不能改指向,对象可以改
const Counter *p2 = &normal; // 指向常对象的指针:p2 可改指向
p1->add(1); // 正确:能通过 p1 修改 normal
p2 = &other; // 正确:p2 本身可以改指向
// p2->add(1); // 错误:不能经 p2 修改所指对象
判断方法:从变量名向两侧读。
- Counter* const p1:p1 本身是 const
- const Counter* p2:p2 指向的 Counter 是 const
因此,常指针会使其指向的对象不能修改是不准确的,要看 const 修饰的是指针还是所指对象。
static 数据成员
普通数据成员每个对象各有一份,静态数据成员属于类,通常只有一份,由所有对象共享。
- 对象 a 有自己独立的 id、name 和 score
- 对象 b 也有自己独立的 id、name 和 score
- 两个对象共同使用 Player::nextId
c++
#include <iostream>
#include <string>
using namespace std;
class Player
{
private:
static int nextId;
int id;
string name;
int score;
public:
explicit Player(string n) : id(++nextId), name(n), score(0) {}
Player &addScore(int delta)
{
score += delta;
return *this;
}
int getId() const
{
return id;
}
int getScore() const
{
return score;
}
static int issuedCount()
{
return nextId;
}
};
int Player::nextId = 0;
int main()
{
Player a("小明");
Player b("小红");
a.addScore(10).addScore(20);
cout<<"a: "<<a.getId()<<' '<<a.getScore()<<endl;
cout<<"b: "<<b.getId()<<' '<<b.getScore()<<endl;
cout<<"共发出编号:"<<Player::issuedCount()<<endl;
return 0;
}
对应输出为:
c++
a: 1 30
b: 2 0
共发出编号:2
关键规则:
- 普通静态数据成员通常要在类外提供一次定义,如 int Player::nextId = 0;
- 静态成员函数没有 this 指针,因为调用它时不一定存在具体对象
- 静态成员函数可以直接访问静态成员,不能直接访问某个对象的非静态成员
- 推荐用 类名::静态成员函数() 表达它属于整个类
复制构造、复制赋值与资源管理
两句相似的代码
c++
Card b = a; // b 还不存在:创建 b,并用 a 初始化
b = a; // b 已经存在:把 a 的状态赋给 b
它们分别调用:
- 复制构造函数:用已有对象创建新对象
- 复制赋值运算符:两个对象都已存在,用右侧状态替换左侧状态

c++
#include <iostream>
using namespace std;
class Card
{
private:
int value;
public:
explicit Card(int v = 0) : value(v)
{
cout<<"普通构造:"<<value<<endl;
}
Card(const Card &other) : value(other.value)
{
cout<<"复制构造"<<endl;
}
Card &operator=(const Card &other)
{
cout<<"复制赋值"<<endl;
if(this != &other)
{
value = other.value;
}
return *this;
}
int getValue() const
{
return value;
}
};
int main()
{
Card a(10); // 普通构造
Card b = a; // 复制构造
Card c; // 普通构造
c = a; // 复制赋值
Card &same = c;
c = same; // 自赋值测试
cout<<b.getValue()<<' '<<c.getValue()<<endl;
return 0;
}
对应输出为:
c++
普通构造:10
复制构造
普通构造:0
复制赋值
复制赋值
10 10
复制构造函数为什么通常接收 const Card&?
- 使用引用,避免为了传参再次复制
- 使用 const,允许复制常对象,并承诺不改原对象
复制赋值为什么常返回 Card&?为了支持 a = b = c; 这样的连续赋值。if (this != &other) 用于识别 c = c; 自赋值。
编译器生成的复制做了什么?
没有自定义复制操作时,编译器通常进行逐成员复制:
- 新对象的第一个成员复制原对象的第一个成员
- 新对象的第二个成员复制原对象的第二个成员
- 其余成员也依次按照各自类型的复制规则进行复制
如果成员是 int、double、string、vector 等具有正常值语义的类型,这通常正是想要的结果。例如两个 vector 对象复制后,各自拥有可独立修改的内容。
危险主要来自拥有资源的裸指针:逐成员复制只会复制地址,两个对象可能以为自己都拥有同一块内存。
浅复制与深复制

浅复制一个"拥有动态内存"的指针可能导致:
- 一个对象释放后,另一个对象的指针悬空
- 两个对象析构时重复释放同一地址
- 修改一个对象的数据,另一个对象也意外变化
继承与访问控制
是一个还是有一个?
继承表达一种 is-a(是一个) 关系:
- 学生是一个人:class Student : public Person 比较合理
- 狗是一种动物:class Dog : public Animal 比较合理
- 汽车有一个发动机:更适合把 Engine 作为 Car 的成员,而不是让汽车继承发动机

基类保存共同部分,派生类复用它并增加自己的数据或操作。
公有继承的基本写法
c++
#include <iostream>
#include <string>
using namespace std;
class Person
{
private:
string name;
protected:
int age;
public:
Person(string name,int age);
string getName() const;
};
class Student : public Person
{
private:
int score;
public:
Student(string name,int age,int score);
void print() const;
};
Person::Person(string name,int age):name(name),age(age){}
string Person::getName() const
{
return name;
}
Student::Student(string name,int age,int score):Person(name,age),score(score){}
void Student::print() const
{
// 通过基类公有接口访问其私有数据,派生类可直接访问 protected 成员
cout<<getName()<<' '<<age<<' '<<score<<endl;
}
int main()
{
Student student("小明", 12, 95);
student.print();
cout<<student.getName()<<endl;
// cout<<student.age; // 错误:类外不能访问 protected 成员
return 0;
}
对应输出为:
小明 12 95
小明
name 仍然存在于 Student 对象的 Person 基类部分,只是 Student 不能直接写 name,因此通过 getName() 访问。
不同继承方式下的权限变化
| 基类中的成员 | 公有继承后 | 保护继承后 | 私有继承后 |
|---|---|---|---|
| public | 派生类中仍为 public | 变成 protected | 变成 private |
| protected | 仍为 protected | 仍为 protected | 变成 private |
| private | 派生类均不可直接访问 | 派生类均不可直接访问 | 派生类均不可直接访问 |
GESP 常见代码以公有继承为主,它保留派生类是一个基类的公开关系。
派生类如何构造基类部分?
c++
Student::Student(string name,int age,int score):Person(name,age),score(score){}
Person(name, age) 的含义是:调用匹配的 Person 构造函数,初始化当前对象的基类部分。它不是在派生类构造函数体中手动调用普通函数。
如果基类没有无参构造函数,而派生类初始化列表又没有指定基类构造函数,编译器会尝试调用不存在的 Base(),从而报错。
- 构造顺序固定为:基类 -> 按声明顺序构造成员对象 -> 派生类构造函数体
- 析构顺序恰好相反:派生类析构函数体 -> 按相反顺序析构成员对象 -> 基类
基类析构函数会由语言规则自动调用,不需要、也不应该由派生类析构函数手动调用。
可以通过以下代码验证构造与析构顺序:
c++
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout<<"1. 构造 Base"<<endl;
}
~Base()
{
cout<<"6. 析构 Base"<<endl;
}
};
class Member
{
public:
Member()
{
cout<<"2. 构造 Member"<<endl;
}
~Member()
{
cout<<"5. 析构 Member"<<endl;
}
};
class Derived : public Base
{
private:
Member member;
public:
Derived()
{
cout<<"3. 构造 Derived"<<endl;
}
~Derived()
{
cout<<"4. 析构 Derived"<<endl;
}
};
int main()
{
Derived object;
return 0;
}
对应输出为:
c++
1. 构造 Base
2. 构造 Member
3. 构造 Derived
4. 析构 Derived
5. 析构 Member
6. 析构 Base
名字隐藏不是重载
c++
class Base
{
public:
void show(int x) {}
};
class Derived : public Base
{
public:
void show(string text) {}
};
Derived::show 会隐藏基类中同名的 show 集合,它们不因为参数不同就自动成为普通意义上的同一重载集合,可在派生类中写 using Base::show; 把基类版本引入。更重要的是:同名也不等于重写。动态多态中的重写必须满足基类函数为虚函数,派生类函数签名匹配,并建议写 override。
多态
假设函数 introduce 只知道参数是动物,但实际传入的可能是狗,也可能是猫:
- introduce(dog) -> 应输出"汪"
- introduce(cat) -> 应输出"喵"
我们希望调用语句始终是 animal.speak(),真正执行哪个版本则由实际对象决定。这就是运行时多态的直观含义。

c++
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
virtual string speak() const
{
return "未知声音";
}
virtual ~Animal() = default;
};
class Dog : public Animal
{
public:
string speak() const override
{
return "汪";
}
};
class Cat : public Animal
{
public:
string speak() const override
{
return "喵";
}
};
void introduce(const Animal &animal)
{
cout<<animal.speak()<<endl;
}
int main()
{
Dog dog;
Cat cat;
introduce(dog);
introduce(cat);
return 0;
}
对应输出为:
c++
汪
喵
要发生这种动态绑定,通常要满足:
- 基类把函数声明为 virtual
- 派生类提供签名匹配的重写版本,推荐写 override
- 通过基类指针或基类引用调用
- 指针或引用实际指向某个派生类对象
虚函数是真实存在的函数。虚不是函数不存在,而是表示通过基类指针或引用调用时,允许根据对象的动态类型选择最终版本。
静态类型与动态类型
c++
Dog dog;
Animal *p = &dog;
p->speak();
- p 的静态类型:写在程序中的 Animal*
- p 所指对象的动态类型:运行时实际是 Dog
- 因为 speak 是虚函数,最终调用 Dog::speak()
如果基类函数没有 virtual,通过 Animal* 调用时通常按静态类型绑定到 Animal 版本,不会获得这里所说的运行时多态。
override 是编译器检查,不只是注释
c++
class Base
{
public:
virtual void print() const {}
};
class Derived : public Base
{
public:
void print() override {} // 编译错误:少了末尾 const
};
const 也是函数签名匹配的一部分。写上 override 后,编译器会立刻告诉我们没有真正重写。修正为:
c++
void print() const override {}
这也是为什么派生类重写虚函数时应养成写 override 的习惯。
重载、隐藏、重写区别
| 概念 | 发生位置 | 关键条件 | 是否依赖 virtual |
|---|---|---|---|
| 重载 | 通常在同一作用域 | 同名,参数列表不同 | 否 |
| 隐藏 | 派生类与基类之间 | 派生类声明同名成员,基类同名集合被隐藏 | 否 |
| 重写 | 派生类与基类之间 | 基类虚函数,派生类签名匹配 | 是 |
返回类型不同不能单独构成重载:
c++
int f(int x);
double f(int x); // 错误:只有返回类型不同
为什么多态通常用引用或指针
c++
Dog dog;
Animal a = dog; // 把 Dog 按值复制成 Animal,派生部分被切掉
a.speak(); // 这里 a 就是一个独立的 Animal 对象
这个现象叫对象切片。按值放入基类对象时,只保留基类部分。因此,多态接口通常写成:
c++
void f(const Animal &animal); // 基类引用
void g(const Animal *animal); // 基类指针
而不是 void h(Animal animal);。

虚析构函数
问题场景
c++
Base *p = new Derived;
delete p;
如果 Base 的析构函数不是虚函数,通过基类指针删除实际的派生类对象会产生未定义行为。不能简单说成肯定只执行基类析构,标准给出的结论更严重:程序行为没有可靠保证。正确做法:准备作为多态基类的类型,应声明虚析构函数。可运行示例:
c++
#include <iostream>
using namespace std;
class Base
{
public:
virtual ~Base()
{
cout<<"析构 Base"<<endl;
}
};
class Derived : public Base
{
public:
~Derived() override
{
cout<<"析构 Derived"<<endl;
}
};
int main()
{
Base *pointer = new Derived;
delete pointer;
return 0;
}
输出:
text
析构 Derived
析构 Base
先析构最具体的派生部分,再逐层析构基类部分,整个对象才被完整清理。
构造函数为什么不能是虚函数
构造期间对象还在形成,动态类型对应的完整对象尚未构造完毕,所以构造函数不能声明为 virtual。析构函数则可以、而且多态基类通常应该是虚函数。
纯虚函数与抽象类
当基类只负责规定接口
所有图形都可以计算面积,但普通图形没有统一的具体面积公式。我们可以让基类只规定接口:
c++
virtual double area() const = 0;
= 0 表示这是纯虚函数,含有未被实现的纯虚函数的类是抽象类,不能直接创建对象。
c++
#include <iomanip>
#include <iostream>
using namespace std;
class Shape
{
public:
virtual double area() const = 0;
virtual const char *name() const = 0;
virtual ~Shape() = default;
};
class Rectangle : public Shape
{
private:
double width;
double height;
public:
Rectangle(double width,double height):width(width),height(height) {}
double area() const override
{
return width*height;
}
const char *name() const override
{
return "矩形";
}
};
class Circle : public Shape
{
private:
double radius;
public:
explicit Circle(double radius):radius(radius) {}
double area() const override
{
const double pi = 3.141592653589793;
return pi*radius*radius;
}
const char *name() const override
{
return "圆";
}
};
void printShape(const Shape &shape)
{
cout<<shape.name()<<"面积:"<<fixed<<setprecision(2)<<shape.area()<<endl;
}
int main()
{
Rectangle rectangle(3.0, 4.0);
Circle circle(2.0);
printShape(rectangle);
printShape(circle);
return 0;
}
对应输出为:
c++
矩形面积:12.00
圆面积:12.57
抽象类作用
抽象类虽然不能直接实例化,但仍然可以:
- 拥有构造函数和析构函数
- 拥有数据成员
- 拥有已经实现的普通成员函数和虚函数
- 声明指向它的指针或引用
- 作为派生类的统一接口
派生类只有正确重写全部继承而来的纯虚函数,才会成为可实例化的具体类。
常见错误
c++
Shape shape; // 错误:Shape 是抽象类
Shape *p = nullptr; // 正确:可以声明抽象类指针
如果派生类把函数写成了不匹配的签名,它仍可能是抽象类:
c++
class BadCircle : public Shape
{
public:
double area()
{
return 0;
} // 少了 const,没有重写 area() const
const char *name() const override
{
return "错误示例";
}
};
写 override 可以让这种错误马上暴露。
友元与类模板
友元函数
普通非成员函数不能访问类的私有成员,类可以用 friend 把访问权限授予某个函数:
c++
#include <iostream>
using namespace std;
class Point
{
private:
int x;
int y;
public:
Point(int x, int y);
friend bool samePosition(const Point &a, const Point &b);
};
Point::Point(int x, int y) : x(x), y(y){}
bool samePosition(const Point &a, const Point &b)
{
return a.x == b.x && a.y == b.y;
}
int main()
{
Point a(2, 3);
Point b(2, 3);
cout<<(samePosition(a, b) ? "相同" : "不同")<<endl;
return 0;
}
对应输出为:
c++
相同
samePosition 仍然是普通函数,不是 Point 的成员函数,所以调用时写 samePosition(a, b),而不是 a.samePosition(b)。
友元类
c++
class Vault
{
private:
int secret = 42;
friend class Inspector;
};
这表示 Inspector 的成员函数可以访问 Vault 的私有、保护成员。要准确记住:
- 友元关系是单向的:Inspector 是 Vault 的友元,不代表反过来也成立
- 友元关系不传递:A 信任 B、B 信任 C,不代表 A 信任 C
- 友元关系不继承
- Inspector 的成员函数仍属于 Inspector,不会变成 Vault 的成员
友元会扩大访问范围,应在确实需要紧密协作时使用,不能用它绕开合理的封装设计。
类模板
如果盒子类既要保存整数,又要保存字符串,与其复制两份代码,不如写类模板:
c++
#include <iostream>
#include <string>
using namespace std;
template <typename T> class Box
{
private:
T value;
public:
explicit Box(const T &value) : value(value) {}
const T &getValue() const
{
return value;
}
void set(const T &newValue)
{
value = newValue;
}
};
int main()
{
Box<int> number(42);
Box<string> word("GESP");
cout<<number.getValue()<<endl;
cout<<word.getValue()<<endl;
return 0;
}
对应输出为:
42
GESP
Box<int> 与 Box<string> 是由同一个模板产生的不同具体类型。vector<int>、vector<string> 也是同样的思想。
多重继承、菱形继承与组合
多重继承
C++ 允许一个派生类拥有多个直接基类:
c++
class Printer
{
public:
void print() {}
};
class Scanner
{
public:
void scan() {}
};
class AllInOne : public Printer, public Scanner
{
};
AllInOne 同时获得打印和扫描接口。如果多个基类中存在同名成员,调用时可能需要写 object.Printer::work() 指定来源。
菱形继承为什么会产生两份基类

如果 B 和 C 都普通继承 A,D 中会有两条到达 A 的路径,也通常有两个 A 基类子对象。
c++
class A
{
public:
int n = 0;
};
class B : public A
{
};
class C : public A
{
};
class D : public B, public C
{
};
D d;
d.B::n = 1; // 指定经 B 到达的那份 A::n
d.C::n = 2; // 指定经 C 到达的那份 A::n
不能简单写 d.A::n 来区分,因为仍然没有说明走 B 还是 C 这条路径。
虚继承
c++
class B : virtual public A
{
};
class C : virtual public A
{
};
class D : public B, public C
{
};
虚继承可以让 D 中共享一份 A 虚基类子对象,最底层派生类负责初始化虚基类。该机制语法和构造顺序较复杂,六级阶段理解"它用来解决菱形继承中的重复基类"即可。
继承不一定优于组合
继承表示是一个的关系,例如 Dog 是一种 Animal,也就是 is-a。组合表示有一个的关系,例如 Car 拥有一个 Engine,也就是 has-a。当两个类型不是稳定的是一个关系时,优先考虑把一个对象作为另一个对象的成员。组合通常耦合更低,也更容易替换内部实现。
默认继承方式
c++
class D1 : Base
{
}; // class 默认 private 继承
struct D2 : Base
{
}; // struct 默认 public 继承
不能笼统地说不写继承方式就一定是私有继承,必须先看派生类型使用 class 还是 struct。