1. 类与对象:从结构体到抽象数据类型
在 C++ 中,类 是自定义类型的蓝图,它描述了一组数据以及可以对这些数据进行的操作。对象 则是类的实例化实体,拥有独立的内存空间。示例:
Person.h
cpp
#pragma once
#include <string>
class Person {
public:
// 成员函数(行为)
std::string GetName() const;
void SetName(const std::string& name);
void Work();
private:
// 成员变量(属性)
std::string m_name;
int m_age;
};
Person.cpp
cpp
#include "Person.h"
#include <string>
#include <iostream>
std::string Person::GetName() const
{
return m_name;
}
void Person::SetName(const std::string& name)
{
m_name = name;
}
void Person::Work()
{
std::cout << m_name << " is working." << std::endl;
}
调用脚本:
cpp
#include <iostream>
#include "Person.h"
int main() {
Person p;
std::string name = "Alice";
p.SetName(name);
std::cout << p.GetName(); // 输出 Alice
p.Work();
return 0;
}
类与 C 语言的结构体最大的区别是:类可以定义成员函数,并拥有访问控制能力。C++ 的结构体 struct 实际上也是一种类,只是默认访问权限为 public,而 class 默认为 private。
2. 封装:隐藏实现细节,保护数据安全
封装是将数据和操作数据的方法绑定在一起,并对外隐藏内部实现细节的机制。在 C++ 中,封装通过访问限定符实现:
1.private:类内部可访问,派生类和外部代码不可直接访问。
2.protected:类内部和派生类可访问,外部代码不可访问。
3.public:完全公开。
封装的优点:
1.控制数据读写权限,避免非法赋值(如年龄不能为负)。
2.降低模块间耦合,修改内部实现不影响调用者。
3.提供清晰的接口,增强可维护性。
典型设计:将数据成员设为 private,提供公共的 getter/setter 成员函数来控制访问逻辑。
cpp
class BankAccount {
private:
double balance; // 余额不直接暴露
public:
void deposit(double amount) {
if (amount > 0) balance += amount;
}
bool withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const { return balance; }
};
这里外部只能通过 deposit 和 withdraw 安全地操作余额,无法直接修改 balance,保证了数据的完整性。
3. 继承:复用代码,建立类型层级
继承允许我们从已有的类(基类/父类)派生出新的类(派生类/子类),派生类自动获得基类的成员,并可添加新的成员或重写已有功能。
3.1 基本概念与语法
继承允许我们创建一个新类(派生类)来复用、扩展或修改现有类(基类)的行为。它体现了 is-a 关系。
cpp
#pragma once
class Base {
public:
int pub;
protected:
int prot;
private:
int priv; // 派生类不可直接访问
};
cpp
#pragma once
#include "Base.h"
class Derived : public Base { // public 继承
// pub 在 Derived 中仍是 public
// prot 在 Derived 中仍是 protected
// priv 不可访问
};
3.2 继承方式与访问权限变化
派生类继承基类时,可指定继承方式:public、protected、private。它影响基类成员在派生类中的访问级别:
| 基类成员访问 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public成员 | 仍为public | 变为protected | 变为private |
| protected成员 | 仍为protected | 变为protected | 变为private |
| private成员 | 不可直接访问 | 不可直接访问 | 不可直接访问 |
通常使用 public 继承,保持 is-a 关系,这样派生类对象可当作基类对象使用。
cpp
class A { public: int x; };
class B : private A { }; // x 在 B 中为 private
class C : public B {
// 无法访问 x,因为 B 中 x 为 private
};
3.3 构造函数与析构函数调用顺序
创建派生类对象时,先调用基类构造函数(初始化基类部分),再调用派生类构造函数。析构时相反:先析构派生类,再析构基类。
如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类构造函数:
Base.h:
cpp
#pragma once
#include <iostream>
class Base {
public:
Base(int val)
{
std::cout << "Base constructor called\n";
value = val;
}
~Base()
{
std::cout << "Base destructor called\n";
}
int GetBValue() { return value; }
private:
int value;
};
Derived.h:
cpp
#pragma once
#include "Base.h"
class Derived : public Base {
public:
Derived(int a, double b) : Base(a), d_val(b)
{
std::cout << "Derived constructor called\n";
b = d_val;
}
~Derived()
{
std::cout << "Derived destructor called\n";
}
double GetDValue(){return d_val;}
private:
double d_val;
};
调用:
cpp
#include <iostream>
#include "Derived.h"
int main()
{
Derived d(123, 45.67);// 构造函数
printf("Derived object created with Base value: %d and Derived value: %.2f\n", d.GetBValue(), d.GetDValue());
return 0;
}
3.4 同名成员与隐藏
若派生类定义了与基类同名的成员(函数或变量),基类的同名成员会被隐藏。即使函数参数不同,也不会形成重载,而是隐藏。若要调用基类版本,需加 Base::作用域限定。
csharp
#include <iostream>
#include <string>
class Vehicle {
public:
// 基类成员变量
std::string id = "GenericVehicle";
// 基类重载的两个 show 函数
void show(int count) {
std::cout << "Vehicle::show(int): count = " << count << std::endl;
}
void show(double weight) {
std::cout << "Vehicle::show(double): weight = " << weight << std::endl;
}
};
class Car : public Vehicle {
public:
// 派生类定义了同名的 id(类型不同)
int id = 1001; // 隐藏了基类的 std::string id
// 派生类定义了同名的 show 函数(参数类型不同)
void show(const std::string& brand) {
std::cout << "Car::show(string): brand = " << brand << std::endl;
}
// 这一写,基类的所有 show 重载版本都被隐藏
};
int main() {
Car c;
// 1. 成员变量隐藏
std::cout << c.id << std::endl; // 输出 1001(派生类的 int)
std::cout << c.Vehicle::id << std::endl; // 输出 "GenericVehicle"(被隐藏的基类成员)
// 2. 成员函数隐藏
c.show("Toyota"); // 正确,调用 Car::show(string)
// c.show(4); // 错误!Vehicle::show(int) 被隐藏
// c.show(1.5); // 错误!Vehicle::show(double) 被隐藏
// 显式调用被隐藏的基类函数
c.Vehicle::show(4);
c.Vehicle::show(1.5);
return 0;
}
3.5 多重继承与虚基类(进阶)
C++ 支持多重继承,可能带来二义性和菱形继承问题。使用虚基类可让公共基类只保留一份实例。
Intern 同时继承 Student 和 Employee,而两者都继承自 Person。若无虚继承,Intern 对象中会包含两份 Person 的成员,造成冗余和二义性。
1. 非虚继承 ------ 出现二义性和多份基类实例
cpp
#include <iostream>
#include <string>
class Person {
public:
std::string name;
Person(const std::string& n) : name(n) {}
};
class Student : public Person {
public:
int studentId;
Student(const std::string& n, int id) : Person(n), studentId(id) {}
};
class Employee : public Person {
public:
int employeeId;
Employee(const std::string& n, int id) : Person(n), employeeId(id) {}
};
class Intern : public Student, public Employee {
public:
Intern(const std::string& n, int sId, int eId)
: Student(n, sId), Employee(n, eId) {
}
};
int main() {
Intern in("Alice", 1001, 2001);
// 错误:name 不明确,有两份 Person,编译器不知访问哪一个
// std::cout << in.name << std::endl;
// 必须显式指定路径
std::cout << "Student 路径的名字: " << in.Student::name << std::endl;
std::cout << "Employee 路径的名字: " << in.Employee::name << std::endl;
// 两份 name 是独立的,可以不同(逻辑上不合理)
in.Student::name = "Alice_student";
in.Employee::name = "Alice_employee";
std::cout << "修改后 Student name: " << in.Student::name << std::endl;
std::cout << "修改后 Employee name: " << in.Employee::name << std::endl;
return 0;
}
2. 虚继承 ------ 只保留一份公共基类实例
在 Student 和 Employee 继承 Person 时加上 virtual 关键字,Intern 中就只有一份 Person 子对象。
cpp
#include <iostream>
#include <string>
class Person {
public:
std::string name;
Person(const std::string& n = "") : name(n) {}
};
// 虚继承 Person
class Student : virtual public Person {
public:
int studentId;
Student(const std::string& n, int id) : Person(n), studentId(id) {}
};
class Employee : virtual public Person {
public:
int employeeId;
Employee(const std::string& n, int id) : Person(n), employeeId(id) {}
};
class Intern : public Student, public Employee {
public:
// 最底层派生类负责初始化虚基类
Intern(const std::string& n, int sId, int eId)
: Person(n), Student(n, sId), Employee(n, eId) {
}
};
int main() {
Intern in("Alice", 1001, 2001);
// 直接访问 name,不再二义
std::cout << "姓名: " << in.name << std::endl;
// 也可以通过路径访问(都是同一个对象)
std::cout << "通过 Student 路径: " << in.Student::name << std::endl;
std::cout << "通过 Employee 路径: " << in.Employee::name << std::endl;
// 修改后所有路径都反映同一值
in.name = "Alice_new";
std::cout << "修改后: " << in.name << std::endl;
std::cout << "Student 路径: " << in.Student::name << std::endl;
std::cout << "Employee 路径: " << in.Employee::name << std::endl;
return 0;
}
虚基类的构造由最底层派生类负责,需在初始化列表中调用虚基类构造函数。
四、多态
多态意为"多种形态",它允许通过统一的接口操作不同类型的对象,从而提高代码的灵活性和可扩展性。C++ 多态分两种:编译时多态(静态多态) 和 运行时多态(动态多态)。
4.1 编译时多态
在编译阶段就决定调用哪个函数,主要包括:
1.函数重载:同一作用域内函数名相同,参数不同。
2.运算符重载:自定义操作符行为。
3.模板:通过泛型编程实现代码复用,函数模板和类模板。
函数重载示例:
cpp
void print(int i) { std::cout << "int: " << i; }
void print(double d) { std::cout << "double: " << d; }
4.2 运行时多态(动态多态)
运行时多态通过 虚函数 实现,调用哪个函数在运行时根据对象的实际类型决定,核心机制是动态绑定。
4.2.1 虚函数与动态绑定
在基类中将成员函数声明为 virtual,派生类可以重写(override) 该函数。通过基类指针或引用调用虚函数时,会依据所指对象的真实类型调用对应版本的函数。
cpp
#include <iostream>
#include <vector>
#include <memory>
// 基类:图形
class Shape {
public:
// 虚函数:可被派生类重写
virtual void draw() const {
std::cout << "绘制一个通用图形" << std::endl;
}
// 虚析构函数(保证正确析构派生类对象)
virtual ~Shape() = default;
};
// 派生类:圆形
class Circle : public Shape {
public:
void draw() const override { // override 明确表示重写
std::cout << "绘制一个圆形 ○" << std::endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "绘制一个矩形 □" << std::endl;
}
};
// 派生类:三角形
class Triangle : public Shape {
public:
void draw() const override {
std::cout << "绘制一个三角形 △" << std::endl;
}
};
// 接受基类引用,内部调用虚函数(动态绑定)
void renderShape(const Shape& s) {
s.draw(); // 运行时决定调用哪个 draw()
}
int main() {
Circle c;
Rectangle r;
Triangle t;
// 1. 通过基类引用调用虚函数
renderShape(c);
renderShape(r);
renderShape(t);
std::cout << "-----------" << std::endl;
// 2. 通过基类指针调用虚函数,多态容器
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Rectangle>());
shapes.push_back(std::make_unique<Triangle>());
for (const auto& p : shapes) {
p->draw(); // 动态绑定,调用实际对象的 draw()
}
return 0;
}
4.2.2 虚函数表(vtable)与虚指针(vptr)
编译器为每个含有虚函数的类生成一个虚函数表(vtable),表中存放该类所有虚函数的实际地址。每个对象内部有一个不可见的指针(vptr),指向所属类的虚函数表。调用虚函数时,通过 vptr -> vtable 找到函数地址,实现动态分发。这就是动态多态的底层原理。
cpp
#include <iostream>
// 基类:包含一个虚函数
class Base {
public:
virtual void show() { // 虚函数
std::cout << "Base::show()" << std::endl;
}
virtual ~Base() = default; // 虚析构(保证正确释放)
};
// 派生类:重写虚函数
class Derived : public Base {
public:
void show() override { // 重写
std::cout << "Derived::show()" << std::endl;
}
};
// 一个不含虚函数的普通类,用于对比大小
class Empty {};
int main() {
// 1. 观察对象大小(vptr 的证据)
std::cout << "sizeof(Empty) = " << sizeof(Empty) << std::endl; // 通常为 1
std::cout << "sizeof(Base) = " << sizeof(Base) << std::endl; // 含 vptr,通常为 8(64位)
std::cout << "sizeof(Derived) = " << sizeof(Derived) << std::endl; // 同样含 vptr,大小通常与 Base 相同
Base b;
Derived d;
// 2. 通过基类指针调用虚函数(动态绑定,依赖 vtable)
Base* ptr = &b;
ptr->show(); // 输出 Base::show()
ptr = &d;
ptr->show(); // 输出 Derived::show(),多态行为
// 3. 多个对象共享同一个 vtable
Base b2;
std::cout << "Address of b: " << &b << std::endl;
std::cout << "Address of b2: " << &b2 << std::endl;
// 虽然地址不同,但它们内部的 vptr 指向同一个 Base 类的 vtable
return 0;
}
4.2.3 纯虚函数与抽象类
纯虚函数是没有实现的虚函数,声明为 virtual void func() = 0;。包含纯虚函数的类称为抽象类,不能实例化。派生类必须实现所有纯虚函数才能成为具体类。
cpp
#include <iostream>
#include <vector>
#include <memory>
// 抽象类:含有纯虚函数,不能实例化
class Shape {
public:
// 纯虚函数:没有实现,要求派生类必须重写
virtual void draw() const = 0;
// 纯虚函数也可以有多个
virtual double area() const = 0;
// 虚析构(确保派生类资源正确释放)
virtual ~Shape() = default;
// 抽象类中也可以有普通成员函数
void description() const {
std::cout << "这是一个形状" << std::endl;
}
};
// 具体派生类:实现所有纯虚函数
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const override {
std::cout << "绘制一个圆形,半径 = " << radius << std::endl;
}
double area() const override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() const override {
std::cout << "绘制一个矩形,宽 = " << width << ",高 = " << height << std::endl;
}
double area() const override {
return width * height;
}
};
int main() {
// Shape s; // 错误!抽象类不能实例化
// 通过基类指针操作具体对象(多态)
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
for (const auto& sp : shapes) {
sp->draw(); // 动态绑定
std::cout << "面积 = " << sp->area() << std::endl;
sp->description(); // 调用普通成员函数
std::cout << "-----------" << std::endl;
}
return 0;
}
4.2.4 析构函数务必声明为虚函数
当基类指针指向派生类对象并通过 delete 释放时,若析构函数不是虚函数,则只调用基类析构,导致派生类资源无法正确释放(未定义行为)。将基类析构声明为虚函数可保证正确的析构顺序。
错误示例:非虚析构导致资源泄漏
cpp
#include <iostream>
class Base {
public:
Base() { std::cout << "Base 构造" << std::endl; }
~Base() { // 非虚析构!
std::cout << "Base 析构" << std::endl;
}
};
class Derived : public Base {
private:
int* data; // 动态分配的资源
public:
Derived() {
data = new int[100]; // 申请堆内存
std::cout << "Derived 构造,分配资源" << std::endl;
}
~Derived() {
delete[] data; // 释放资源
std::cout << "Derived 析构,释放资源" << std::endl;
}
};
int main() {
Base* ptr = new Derived(); // 基类指针指向派生类对象
delete ptr; // 危险!只调用 Base::~Base()
return 0;
}
可以看到 Derived 的析构函数没有被调用,data 指向的内存永远泄漏,且没有"释放资源"的输出。
正确做法:将基类析构声明为虚函数
cpp
#include <iostream>
class Base {
public:
Base() { std::cout << "Base 构造" << std::endl; }
virtual ~Base() { // 虚析构!
std::cout << "Base 析构" << std::endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() {
data = new int[100];
std::cout << "Derived 构造,分配资源" << std::endl;
}
~Derived() override {
delete[] data;
std::cout << "Derived 析构,释放资源" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 安全!先调 Derived::~Derived(),再调 Base::~Base()
return 0;
}
规则与建议
1.只要一个类被设计为基类(或被继承),它的析构函数就应该是虚函数。
2.如果不打算让类被继承,可以使用 final 关键字阻止派生(C++11)。
3.即使基类的析构函数什么都不做,也请声明为 virtual ~ClassName() = default;,保证多态删除的安全。
4.这条规则同样适用于抽象类:即使无法实例化,虚析构仍然是必需的。
4.2.5 override 和 final 关键字(C++11)
override:显式声明派生类函数重写基类虚函数,编译器会检查函数签名是否正确匹配,避免错误。
final:阻止派生类继续重写某个虚函数,或阻止类被继承。
1. override:显式重写,编译器护航
用 override 修饰派生类的成员函数,明确表示该函数旨在重写基类虚函数。编译器会严格检查函数签名(函数名、参数列表、const 限定、引用限定等)是否与基类虚函数完全匹配。若基类没有对应的虚函数,或签名有误,直接产生编译错误。
cpp
#include <iostream>
class Base {
public:
virtual void draw() const {
std::cout << "Base::draw() const" << std::endl;
}
virtual void resize(int w, int h) {
std::cout << "Base::resize(" << w << ", " << h << ")" << std::endl;
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
// 正确重写,建议添加 override
void draw() const override {
std::cout << "Derived::draw() const" << std::endl;
}
// 错误示范:试图重写 resize 但参数列表不同,且无意中隐藏了基类版本
// void resize(double factor) override; // 编译错误!基类无此虚函数签名
// 正确重写 resize
void resize(int w, int h) override {
std::cout << "Derived::resize(" << w << ", " << h << ")" << std::endl;
}
};
int main() {
Derived d;
Derived* p = &d;
p->draw();
}
2. final:阻止进一步重写或继承
final 可以施加于虚函数或类,实现设计层面的"冻结"。
修饰虚函数:禁止任何派生类再次重写该虚函数。
修饰类:禁止该类被继承。
csharp
#include <iostream>
#include <vector>
#include <memory>
class Animal {
public:
virtual void speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
// 重写 speak 并声明为 final,Cat 不能再重写它
void speak() const override final {
std::cout << "Woof!" << std::endl;
}
};
// 尝试重写 final 虚函数 -> 编译错误
class Husky : public Dog {
public:
// void speak() const override { // 错误!Dog::speak 是 final
// std::cout << "Husky Woof!" << std::endl;
// }
};
// 使用 final 阻止类被继承
class Cat final : public Animal {
public:
void speak() const override {
std::cout << "Meow!" << std::endl;
}
};
// class Persian : public Cat { // 错误!Cat 被声明为 final,不能作为基类
// };
int main() {
Cat d;
Cat* p = &d;
p->speak();
}
综合示例
cpp
#include <iostream>
#include <vector>
#include <memory>
class Shape {
public:
virtual void draw() const = 0;
virtual std::string name() const { return "Shape"; }
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "○" << std::endl;
}
std::string name() const override final { // 禁止后续派生类重写 name
return "Circle";
}
};
class ColoredCircle final : public Circle { // ColoredCircle 不能被继承
public:
void draw() const override {
std::cout << "🌈○" << std::endl;
}
// 不能重写 name,因为它在 Circle 中标记为 final
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<ColoredCircle>());
for (const auto& s : shapes) {
s->draw();
std::cout << "Name: " << s->name() << std::endl;
}
}
4.2.6 协变返回类型
重写虚函数时,若返回类型是类的指针或引用,派生类可以返回更具体的派生类指针/引用,称为协变。
csharp
#include <iostream>
#include <memory>
// 基类:图形
class Shape {
public:
// 虚函数,返回基类引用
virtual const Shape& getSelf() const {
std::cout << "Shape::getSelf()" << std::endl;
return *this;
}
// 另一个虚函数,返回基类指针
virtual Shape* clone() const {
std::cout << "Shape::clone() -> Shape*" << std::endl;
return new Shape(*this);
}
virtual void draw() const { std::cout << "Shape" << std::endl; }
virtual ~Shape() = default;
};
// 派生类:圆形
class Circle : public Shape {
public:
// 协变:返回 Circle& 而非 Shape&
const Circle& getSelf() const override {
std::cout << "Circle::getSelf()" << std::endl;
return *this;
}
// 协变:返回 Circle* 而非 Shape*
Circle* clone() const override {
std::cout << "Circle::clone() -> Circle*" << std::endl;
return new Circle(*this);
}
void draw() const override { std::cout << "Circle" << std::endl; }
};
int main() {
Circle circle;
const Shape& shapeRef = circle;
// 通过基类引用调用 getSelf():动态绑定到 Circle::getSelf()
const Shape& self1 = shapeRef.getSelf(); // 返回类型是 const Shape&,但实际对象是 Circle
self1.draw(); // 输出 Circle
// 通过派生类对象调用:获得精确类型 Circle&
const Circle& self2 = circle.getSelf(); // 返回 Circle&,无需转型
self2.draw(); // 输出 Circle
// clone 测试
Shape* clonedShape = circle.clone(); // Circle::clone() 返回 Circle* 赋值给 Shape*
clonedShape->draw(); // 输出 Circle
delete clonedShape;
// 同样可以使用派生类指针接收,避免转型
Circle* clonedCircle = circle.clone(); // 直接获得 Circle*
clonedCircle->draw();
delete clonedCircle;
return 0;
}
4.3 多态的综合示例
cpp
#include <iostream>
#include <vector>
class Animal {
public:
virtual void speak() const = 0; // 纯虚函数
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
void speak() const override { std::cout << "Meow!\n"; }
};
int main() {
std::vector<Animal*> animals;
animals.push_back(new Dog());
animals.push_back(new Cat());
for (auto animal : animals) {
animal->speak(); // 运行时多态,分别输出 Woof! Meow!
}
// 释放内存
for (auto animal : animals) delete animal;
}
五、封装、继承、多态的协同
封装 让类的内部实现安全隐藏,只暴露接口,降低耦合。
继承 实现代码复用和层次化设计,表达 is-a 关系。
多态 允许通过基类接口调用不同派生类的行为,使得程序易扩展(开闭原则:对扩展开放,对修改封闭)。
设计良好的面向对象系统通常是:封装合理的数据,通过继承建立类的层次,再利用多态编写可以处理不同子类的通用逻辑。这三个特性相互配合,构建出可维护、可扩展的 C++ 程序。
通过上述细节和示例,可以透彻理解 C++ 类与对象的三大支柱特性,并在实际开发中灵活运用。