C++ 的面向对象语法和 Java 很相似,因为两者都继承自 Simula。但底层机制有很大不同——尤其是值语义 vs 引用语义、手动内存管理和编译期多态的差异。
类的基本结构
// Person.h(头文件)
#pragma once
#include <string>
class Person {
std::string name; // 默认为 private
int age;
public:
Person(const std::string& name, int age); // 构造函数
void sayHello() const; // const 成员函数
};
// Person.cpp(实现文件)
#include "Person.h"
#include <iostream>
Person::Person(const std::string& name, int age)
: name(name), age(age) {} // 初始化列表(重要!)
void Person::sayHello() const {
std::cout << "Hello, I'm " << name << std::endl;
}
Java 开发者需要注意的几个关键差异:
1. public/private 是分段的
class MyClass {
// 默认为 private
int secret;
public:
int visible;
void foo();
private:
void helper();
};
2. 初始化列表(必须掌握)
初始化列表是 C++ 构造函数中初始化成员变量的正确方式:
class Example {
const int constant; // const 成员
int& reference; // 引用成员
std::string name; // 普通对象成员
public:
// ✅ 初始化列表(推荐)
Example(int c, int& r, const std::string& n)
: constant(c), reference(r), name(n) {
// 函数体内不需要再赋值
}
// ❌ 函数体内赋值(低效)
// Example(int c, int& r, const std::string& n) {
// constant = c; // 编译错误!const 成员不能赋值
// reference = r; // 编译错误!引用不能重新绑定
// name = n; // 先默认构造 name,再赋值(多余操作)
// }
};
必须用初始化列表的情况:
const成员变量- 引用类型成员
- 没有默认构造函数的成员对象
- 基类没有默认构造函数(在派生类构造函数中)
3. const 成员函数
const 成员函数承诺不修改对象:
class Person {
std::string name_;
public:
// const 成员函数——可被 const 和 non-const 对象调用
std::string getName() const {
return name_;
}
// 非 const 成员函数——只能被 non-const 对象调用
void setName(const std::string& n) {
name_ = n;
}
};
void print(const Person& p) {
std::cout << p.getName(); // ✅ const 对象可以调用 const 方法
// p.setName("Bob"); // ❌ 编译错误!const 对象不能调用非 const 方法
}
构造函数与析构函数
特殊成员函数
每个 C++ 类都有六个特殊成员函数(C++11 起):
class Widget {
public:
Widget(); // 默认构造函数
Widget(int id); // 有参构造
Widget(const Widget& other); // 拷贝构造函数
Widget(Widget&& other) noexcept; // 移动构造函数(C++11)
Widget& operator=(const Widget& other); // 拷贝赋值运算符
Widget& operator=(Widget&& other) noexcept; // 移动赋值运算符(C++11)
~Widget(); // 析构函数
};
析构函数(Java 没有!)
析构函数是 RAII 的基石,在对象离开作用域时自动调用:
class ResourceHolder {
int* data;
public:
ResourceHolder(size_t size)
: data(new int[size]) {
std::cout << "分配 " << size << " 个整数" << std::endl;
}
~ResourceHolder() {
delete[] data;
std::cout << "释放资源" << std::endl;
}
// 禁止拷贝(防止双重 delete)
ResourceHolder(const ResourceHolder&) = delete;
ResourceHolder& operator=(const ResourceHolder&) = delete;
};
void use() {
ResourceHolder rh(1000); // 构造时分配
// 使用 rh ...
if (some_condition) {
throw std::runtime_error("出错了");
}
} // rh 离开作用域,析构函数自动调用!即使有异常也会调用!
| 特性 | Java | C++ |
|---|---|---|
| 对象回收 | GC,不确定何时执行 | 离开作用域立即析构 |
| 文件/锁释放 | try-finally 或 try-with-resources | RAII 自动 |
| 内存泄漏风险 | 低 | 需手动管理(智能指针解决) |
| 析构函数 | finalize()(不推荐使用) |
~T()(确定、可靠) |
委托构造(Delegating Constructor)—— C++11
changkun 的《现代 C++ 教程》中介绍:C++11 引入了委托构造的概念,使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的。
#include <iostream>
class Person {
std::string name_;
int age_;
bool has_id_;
public:
// 基础构造函数
Person(const std::string& name, int age, bool has_id)
: name_(name), age_(age), has_id_(has_id) {
std::cout << "完整构造" << std::endl;
}
// 委托构造:调用上面的三参数版本
Person(const std::string& name, int age)
: Person(name, age, false) { // ✅ 委托给三参数构造
std::cout << "委托构造(无 ID)" << std::endl;
}
// 再委托
Person() : Person("Unknown", 0) {
std::cout << "默认构造" << std::endl;
}
};
继承构造(Inheriting Constructor)—— C++11
传统 C++ 中,派生类需要显式传递基类构造函数的参数。C++11 利用关键字 using 引入了继承构造函数的概念:
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托构造
value2 = value;
}
};
class Subclass : public Base {
public:
using Base::Base; // ✅ 继承构造函数!自动继承 Base 的所有构造函数
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl; // 1(Base() 设置)
std::cout << s.value2 << std::endl; // 3(Base(int) 设置)
}
Rule of Three / Five / Zero
// Rule of Three:如果自定义了析构函数、拷贝构造函数、拷贝赋值运算符之一,
// 通常三者都需要自定义(C++98)
class TraditionalResource {
int* data;
public:
TraditionalResource(size_t size) : data(new int[size]) {}
~TraditionalResource() { delete[] data; }
// 拷贝构造
TraditionalResource(const TraditionalResource& other)
: data(new int[*other.data]) {
std::copy(other.data, other.data + *other.data, data);
}
// 拷贝赋值
TraditionalResource& operator=(const TraditionalResource& other) {
if (this != &other) {
delete[] data;
data = new int[*other.data];
std::copy(other.data, other.data + *other.data, data);
}
return *this;
}
};
// Rule of Five:C++11 添加移动构造和移动赋值
// (略,见上一章的移动语义)
// Rule of Zero:最佳实践——让智能指针管理资源,不需要自定义任何特殊成员函数 ✅
class ModernResource {
std::unique_ptr<int[]> data; // 自动管理资源
public:
ModernResource(size_t size)
: data(std::make_unique<int[]>(size)) {}
// 不需要自定义析构、拷贝、移动——unique_ptr 自动处理!
};
💡 良好的编码习惯:始终遵循 Rule of Zero——用智能指针和 RAII 包装器管理资源,让编译器生成的特殊成员函数正确工作。只有在必须自定义资源管理时才使用 Rule of Five。
继承
基本语法——小心默认 private 继承!
class Base {
public:
void publicMethod();
protected:
int protectedField;
private:
int privateField;
};
// 公有继承(对应 Java 的 extends)
class Derived : public Base {
// 可以访问 publicMethod 和 protectedField
// 不能访问 privateField
};
⚠️ 坏味道:忘记
public继承关键字class Derived : Base { // ❌ 默认是 private 继承! // Base 的 public 成员在 Derived 中变成 private };在 C++ 中,
class Derived : Base不等于 Java 的class Derived extends Base!必须写: public Base。
C++ 有三种继承方式:
| 继承方式 | 基类 public → 派生类 | 基类 protected → 派生类 | 基类 private → 派生类 |
|---|---|---|---|
public 继承 |
public | protected | 不可访问 |
protected 继承 |
protected | protected | 不可访问 |
private 继承 |
private | private | 不可访问 |
虚函数与多态
#include <iostream>
#include <memory>
#include <vector>
class Animal {
public:
virtual void speak() const {
std::cout << "..." << std::endl;
}
virtual ~Animal() = default; // ✅ 基类必须虚析构!
};
class Dog : public Animal {
public:
void speak() const override { // ✅ override 关键字(C++11 推荐)
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());
for (const auto& animal : animals) {
animal->speak(); // 多态调用:输出 Woof! 和 Meow!
}
}
// animals 析构时,正确调用 ~Dog()、~Cat()(因为有 virtual ~Animal())
| 特性 | Java | C++ |
|---|---|---|
| 虚函数 | 默认所有方法都是虚的 | 需要 virtual 关键字 |
| 重写 | @Override 注解 |
override 关键字(C++11) |
| 禁止重写 | final 关键字 |
final(C++11) |
| 基类析构 | 不需要 | 必须有 virtual ~Base() |
| 抽象类 | abstract class |
纯虚函数 virtual void f() = 0 |
| 接口 | interface 关键字 |
纯虚基类(全是纯虚函数) |
override 与 final(C++11)
changkun 教程指出:C++11 引入了 override 和 final 这两个关键字来防止意外重载而不是重写的情况。
override:显式告知编译器这是虚函数重写
struct Base {
virtual void foo(int);
virtual void bar() const;
};
struct SubClass : Base {
virtual void foo(int) override; // ✅ 合法,确实重写了 Base::foo(int)
virtual void foo(float) override; // ❌ 非法,基类没有 foo(float)
// virtual void bar() override; // ❌ 非法!忘记写 const,签名不匹配
};
final:防止类被继续继承或虚函数被继续重写
struct Base {
virtual void foo() final; // 禁止子类重写 foo
};
struct SubClass1 final : Base {}; // 合法(但 SubClass1 不能再被继承)
// struct SubClass2 : SubClass1 {}; // ❌ 非法,SubClass1 已 final
struct SubClass3 : Base {
// void foo() override {} // ❌ 非法,foo 已 final
};
💡 良好习惯:永远在意图重写虚函数时使用
override关键字。这可以捕获签名不匹配、拼写错误等 bug,是 C++11 引入的最佳实践之一。
纯虚函数与抽象类
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual double area() const = 0; // 纯虚函数 → 抽象类
virtual ~Shape() = default; // 抽象类也需要虚析构
};
class Circle : public Shape {
double radius_;
public:
Circle(double r) : radius_(r) {}
double area() const override {
return 3.14159 * radius_ * radius_;
}
};
class Rectangle : public Shape {
double width_, height_;
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
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>(3.0, 4.0));
for (const auto& s : shapes) {
std::cout << "面积: " << s->area() << std::endl;
}
}
菱形继承与虚继承
#include <iostream>
class Animal {
public:
int weight = 0;
};
// 虚继承:确保派生类共享同一个基类子对象
class Mammal : virtual public Animal {};
class Bird : virtual public Animal {};
class Bat : public Mammal, public Bird {
public:
void show() {
// 如果没有虚继承,这里会有二义性:
// weight = 10; // error: 'weight' is ambiguous
// 虚继承解决菱形问题:Bat 只有一个 weight
weight = 5; // ✅
std::cout << "Bat weight: " << weight << std::endl;
}
};
⚠️ 坏味道:不必要地使用多继承
多继承虽然强大,但增加了设计复杂度。实践中尽量用组合代替继承,多继承场景大部分可以用组合 + 接口(纯虚类)替代。
强类型枚举 enum class(C++11)
changkun 教程指出:传统 C++ 中枚举类型是不安全的,枚举类型会被视作整数,两种完全不同的枚举类型可以直接比较,甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同。
传统 enum 的问题
// ❌ 坏味道:传统 enum
enum Color { RED, GREEN, BLUE };
enum Feeling { EXCITED, BLUE }; // ❌ 编译错误:BLUE 重复定义!
Color c = RED;
if (c == 0) { // ❌ 隐式转换为 int,逻辑脆弱
// ...
}
if (c == GREEN) { // OK
}
if (c == EXCITED) { // ❌ 不同类型的枚举也能比较!逻辑错误
}
enum class 的解决方案
// ✅ 好习惯:enum class
enum class Color : unsigned int {
RED,
GREEN,
BLUE = 100
};
enum class Feeling {
EXCITED,
CALM
// BLUE 不冲突!因为作用域不同
};
int main() {
Color c = Color::RED; // 必须用作用域限定
// if (c == 0) {} // ❌ 编译错误!不能隐式转换为 int
// if (c == Feeling::EXCITED) {} // ❌ 编译错误!不同类型不能比较
if (c == Color::RED) {} // ✅ 正确
// 需要整数值时,显式转换
int val = static_cast<int>(c);
std::cout << val << std::endl; // 输出 0
// 相同枚举类型的值可以比较
if (Color::BLUE == Color::BLUE) {
std::cout << "相同" << std::endl;
}
}
enum class 的优势:
- 类型安全:不能隐式转换为整数
- 作用域隔离:枚举值在枚举类型的作用域内
- 可指定底层类型:
enum class E : char { ... }
💡 输出 enum class 的值(方便调试)
#include <iostream> #include <type_traits> template<typename T> std::ostream& operator<<( typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, const T& e) { return stream << static_cast<typename std::underlying_type<T>::type>(e); } // 使用 std::cout << Color::BLUE << std::endl; // 输出 100
struct vs class
struct Point {
int x; // 默认 public
int y;
};
class Point {
int x; // 默认 private
int y;
};
最佳实践:
- 纯数据的 POD 类型用
struct - 有封装、行为、不变量保护的用
class
常见陷阱与坏味道
⚠️ 坏味道 1:忘记虚析构函数
class Base {
public:
~Base() { std::cout << "Base 析构" << std::endl; } // ❌ 非虚析构
};
class Derived : public Base {
int* data = new int[100];
public:
~Derived() {
delete[] data;
std::cout << "Derived 析构" << std::endl;
}
};
int main() {
Base* b = new Derived();
delete b; // ❌ 未定义行为!只调用了 Base 析构,Derived 的 data 泄漏!
}
✅ 解决方案:如果类有虚函数,或者作为基类使用,必须提供虚析构函数:
class Base { public: virtual ~Base() = default; // ✅ 虚析构 + default 实现 };
⚠️ 坏味道 2:对象切片(Object Slicing)
class Animal {
public:
virtual void speak() const { std::cout << "..." << std::endl; }
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!" << std::endl; }
};
void process_by_value(Animal a) { // ❌ 按值传递!发生切片
a.speak(); // 输出 "...", 不是 "Woof!"
}
void process_by_ref(const Animal& a) { // ✅ 按引用传递
a.speak(); // 正确调用派生类的版本
}
int main() {
Dog dog;
process_by_value(dog); // 切片:只复制了 Animal 部分
process_by_ref(dog); // 多态:正确调用 Dog::speak()
}
✅ 解决方案:多态对象永远用指针或引用传递。
⚠️ 坏味道 3:在构造/析构中调用虚函数
class Base {
public:
Base() { speak(); } // ❌ 构造函数中调用虚函数!
virtual void speak() { std::cout << "Base构造"; }
};
class Derived : public Base {
public:
Derived() {} // Base() 先执行
void speak() override { std::cout << "Derived构造"; }
};
int main() {
Derived d; // 输出 "Base构造"(不是 "Derived构造"!)
}
原因:在基类构造期间,派生类部分尚未初始化,C++ 运行时将对象的动态类型视为
Base,因此speak()调用的是Base::speak()。析构时同理。
⚠️ 坏味道 4:用 int 枚举而不是 enum class
// ❌ 传统 enum
enum Status { OK, ERROR, PENDING };
void handle(int s) { /* ... */ }
handle(OK); // 隐式转换
handle(42); // ❌ 毫无语义的调用也能编译通过!
// ✅ enum class
enum class Status { OK, ERROR, PENDING };
void handle(Status s) { /* ... */ }
handle(Status::OK); // ✅ 类型安全
// handle(42); // ❌ 编译错误,类型不匹配
⚠️ 坏味道 5:多继承中不用 virtual 继承(菱形继承)
未使用虚继承时,派生类会包含多个基类子对象副本,导致二义性和浪费:
class A { public: int x; };
class B : public A {}; // ❌ 非虚继承
class C : public A {}; // ❌ 非虚继承
class D : public B, public C {};
// D d;
// d.x = 10; // ❌ 二义性:B::x 还是 C::x?
解决方案:使用虚继承(virtual public),或更好——用组合代替继承。
组合优于继承
#include <iostream>
#include <memory>
#include <vector>
// ❌ 继承的误用:为了复用代码而继承
class Animal {
public:
virtual void makeSound() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Woof!" << std::endl;
}
};
// ❌ 坏味道:"是一个"关系不成立,却使用继承
// class CoffeeMachine : public Animal { ... }; // 荒谬!
// ✅ 组合:咖啡机有一个"发声器"组件
class Speaker {
public:
void play(const std::string& sound) const {
std::cout << sound << std::endl;
}
};
class CoffeeMachine {
Speaker speaker_; // ✅ 组合
std::string brand_;
public:
CoffeeMachine(const std::string& brand) : brand_(brand) {}
void brew() {
std::cout << brand_ << " 正在煮咖啡..." << std::endl;
speaker_.play("Beep! 咖啡好了!");
}
};
💡 良好习惯:优先组合,继承只在"is-a"关系明确且需要多态行为时使用。
编码习惯与坏味道总结
✅ 良好编码习惯
| 习惯 | 说明 |
|---|---|
override 关键字 |
重写虚函数时总加 override,防止签名不匹配 |
| 基类虚析构 | 任何作为基类的类都要有 virtual ~Base() = default |
enum class 替代 enum |
类型安全、作用域隔离 |
| 组合优于继承 | 只有明确的"is-a"关系才用继承 |
| Rule of Zero | 用智能指针让特殊成员函数自动正确工作 |
| 初始化列表 | 构造函数用初始化列表,避免默认构造 + 赋值 |
= default / = delete |
显式声明或禁止编译器生成的成员函数 |
final 阻止继承/重写 |
明确设计意图,防止误继承/误重写 |
| 委托构造 | 减少重复代码 |
❌ 需要避免的坏味道
| 坏味道 | 问题 | 解决方案 |
|---|---|---|
| 忘记虚析构 | 派生类资源泄漏 | 基类加 virtual ~T() = default |
| 对象切片 | 丢失派生类信息 | 用指针或引用传递多态对象 |
| 构造/析构中调虚函数 | 不会调用派生类版本,逻辑错误 | 不要在构造/析构中调虚函数 |
| 多继承不用 virtual | 菱形继承的二义性 | 用虚继承,或组合替代继承 |
传统 enum |
类型不安全、命名冲突 | 用 enum class |
| 函数体内赋值初始化 | 性能损失,const/引用成员编译错误 | 初始化列表 |
| 继承而非组合 | 设计脆弱,违反 LSP | 优先组合 |
不写 override |
重写签名错误不被发现 | 总写 override |
完整示例:综合应用
一个综合运用委托构造、继承构造、enum class、override、final、Rule of Zero 的例子:
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// 强类型枚举
enum class LogLevel : unsigned int {
DEBUG,
INFO,
WARNING,
ERROR
};
// 可输出的枚举(通过重载 <<)
std::ostream& operator<<(std::ostream& os, LogLevel level) {
static const char* names[] = {"DEBUG", "INFO", "WARNING", "ERROR"};
return os << names[static_cast<int>(level)];
}
// 基类:日志目标(抽象类)
class LogTarget {
public:
virtual void write(LogLevel level, const std::string& msg) = 0;
virtual ~LogTarget() = default; // ✅ 虚析构
};
// 派生类:控制台日志
class ConsoleTarget final : public LogTarget { // final 阻止进一步继承
public:
void write(LogLevel level, const std::string& msg) override {
std::cout << "[" << level << "] " << msg << std::endl;
}
};
// 派生类:文件日志(组合方式)
class FileTarget : public LogTarget {
std::string filename_;
public:
// 委托构造
FileTarget() : FileTarget("app.log") {}
FileTarget(const std::string& filename)
: filename_(filename) {}
void write(LogLevel level, const std::string& msg) override {
// 实际项目中打开文件写入
std::cout << "[File:" << filename_ << "][" << level << "] "
<< msg << std::endl;
}
};
// 日志器(组合 LogTarget,而非继承)
class Logger {
std::vector<std::unique_ptr<LogTarget>> targets_;
std::string name_;
public:
Logger(const std::string& name) : name_(name) {}
void addTarget(std::unique_ptr<LogTarget> target) {
targets_.push_back(std::move(target));
}
void log(LogLevel level, const std::string& msg) {
// 理想情况下这里用 RAII 加锁
for (const auto& t : targets_) {
t->write(level, name_ + ": " + msg);
}
}
};
int main() {
Logger logger("MyApp");
logger.addTarget(std::make_unique<ConsoleTarget>());
logger.addTarget(std::make_unique<FileTarget>("myapp.log"));
logger.log(LogLevel::INFO, "应用启动");
logger.log(LogLevel::DEBUG, "加载配置...");
logger.log(LogLevel::WARNING, "配置项缺失,使用默认值");
logger.log(LogLevel::ERROR, "数据库连接失败");
// 所有资源通过 RAII 自动释放
}
总结
C++ 的面向对象比 Java 更灵活但也更复杂。核心要点:
- 初始化列表是构造函数的正确写法,不是可选项
- 析构函数是 RAII 的基石,Java 没有等效机制
override和final是 C++11 引入的最实用的面向对象改进- 委托构造和继承构造减少构造函数的重复代码
enum class替代传统enum实现类型安全- 基类必须有虚析构函数,否则派生类资源泄漏
- 避免对象切片:多态对象用指针或引用传递
- 优先组合,继承只在明确的多态场景使用
- 遵循 Rule of Zero,用智能指针自动管理资源
关于继承的对比:
| 场景 | Java | C++ |
|---|---|---|
| 继承默认 | public |
private |
| 多态默认 | 所有方法都可重写 | 只有 virtual 方法可重写 |
| 抽象类 | abstract class |
含纯虚函数的类 |
| 接口 | interface |
纯虚基类 |
| 多继承 | 接口多继承 | 类和接口均可多继承 |
下一篇我们将介绍 C++ 最强大的特性——模板(Template),和 Java 泛型的本质区别。