C++学习:六个月从基础到就业——面向对象编程:类与对象
本文是我C++学习之旅系列的第八篇技术文章,主要介绍面向对象编程中的类与对象基础概念,包括类的定义、对象的创建、成员函数与成员变量、访问控制等核心知识。查看完整系列目录了解更多内容。
引言
面向对象编程(Object-Oriented Programming,OOP)是一种程序设计范式,它使用"对象"来组织代码和数据,而不是简单地使用函数和数据结构。在C++中,类和对象是实现面向对象编程的基础。类是定义对象特征的模板,而对象是类的具体实例。本文将深入探讨C++中类与对象的概念、语法和使用方法,帮助读者构建坚实的面向对象编程基础。
从结构体到类
我们在前一篇文章中介绍了结构体,在C++中,类可以看作是对结构体的扩展。实际上,在C++中,结构体和类的主要区别只是默认的访问权限不同:结构体的成员默认是公有的(public),而类的成员默认是私有的(private)。
结构体复习
struct Student {
std::string name; // 公有成员
int age; // 公有成员
float gpa; // 公有成员
};
Student s;
s.name = "Alice"; // 直接访问是允许的
转换为类定义
class Student {
std::string name; // 私有成员
int age; // 私有成员
float gpa; // 私有成员
};
Student s;
// s.name = "Alice"; // 编译错误:name是私有的
这种默认访问权限的差别反映了两种不同的设计思想:结构体主要用于组织数据,而类则更强调数据的封装和保护。
类的基本概念
类的定义
在C++中,类的定义使用关键字class
开始,后跟类名和一对花括号,花括号内包含类的成员变量和成员函数(也称为方法):
class ClassName {
// 成员变量和成员函数
};
一个更完整的例子:
class Rectangle {
private: // 私有成员,外部不能直接访问
double width;
double height;
public: // 公有成员,外部可以直接访问
// 构造函数
Rectangle(double w, double h) {
width = w;
height = h;
}
// 计算面积
double area() {
return width * height;
}
// 计算周长
double perimeter() {
return 2 * (width + height);
}
// 获取宽度
double getWidth() {
return width;
}
// 设置宽度
void setWidth(double w) {
if (w > 0) { // 添加验证逻辑
width = w;
}
}
// 获取高度
double getHeight() {
return height;
}
// 设置高度
void setHeight(double h) {
if (h > 0) { // 添加验证逻辑
height = h;
}
}
};
访问修饰符
C++提供了三种访问修饰符来控制类成员的可见性:
- private(私有):只能被类内部的成员函数访问
- protected(保护):能被类内部和派生类访问
- public(公有):可以被任何代码访问
一个包含所有三种访问修饰符的例子:
class Account {
private: // 私有成员
std::string accountNumber;
double balance;
protected: // 保护成员
std::string ownerName;
bool isActive;
public: // 公有成员
Account(const std::string& owner, const std::string& accNum) {
ownerName = owner;
accountNumber = accNum;
balance = 0.0;
isActive = true;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (isActive && amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const {
return balance;
}
};
在这个例子中:
accountNumber
和balance
是私有的,只能通过类的成员函数访问和修改ownerName
和isActive
是保护的,在当前类和派生类中可访问- 构造函数、
deposit
、withdraw
和getBalance
是公有的,任何代码都可以调用这些函数
对象的创建与使用
类定义了对象的模板,要创建对象,我们可以像定义变量一样声明类的实例:
// 创建Rectangle对象
Rectangle rect(5.0, 3.0);
// 调用成员函数
double area = rect.area(); // 返回15.0
double perimeter = rect.perimeter(); // 返回16.0
// 获取与设置属性
double currentWidth = rect.getWidth(); // 返回5.0
rect.setWidth(10.0); // 修改宽度为10.0
还可以使用动态分配创建对象:
// 使用new动态创建对象
Rectangle* pRect = new Rectangle(5.0, 3.0);
// 使用指针调用成员函数
double area = pRect->area();
pRect->setWidth(10.0);
// 使用完对象后释放内存
delete pRect;
类成员的详解
成员变量
类的成员变量(也称为数据成员或属性)是与类的每个实例关联的变量。它们定义了对象的状态。
实例变量与静态变量
类中有两种主要类型的成员变量:
- 实例变量:与每个对象实例关联,每个对象都有自己的副本
- 静态变量:与类关联而非特定实例,所有对象共享同一副本
class Counter {
private:
int instanceCount; // 实例变量,每个对象有独立的副本
static int totalCount; // 静态变量,所有对象共享一个副本
public:
Counter() {
instanceCount = 0;
totalCount++; // 增加静态计数器
}
void increment() {
instanceCount++;
totalCount++;
}
int getInstanceCount() const {
return instanceCount;
}
static int getTotalCount() {
return totalCount;
}
};
// 静态成员变量需要在类外初始化
int Counter::totalCount = 0;
使用示例:
Counter c1, c2;
c1.increment(); // c1.instanceCount = 1, totalCount = 3
c2.increment(); // c2.instanceCount = 1, totalCount = 4
// 通过类名访问静态成员
int total = Counter::getTotalCount(); // total = 4
const成员变量
当一个成员变量被声明为const
时,它必须在创建对象时初始化,且之后不能更改:
class Circle {
private:
const double PI; // const成员变量
double radius;
public:
// const成员必须在初始化列表中初始化
Circle(double r) : PI(3.14159), radius(r) {
// PI = 3.14159; // 错误:不能在构造函数体内为const成员赋值
}
double area() const {
return PI * radius * radius;
}
};
成员函数
成员函数是属于类的函数,它们定义了对象的行为。
定义和声明
成员函数可以在类定义内部声明,并在类内部或外部定义:
// 在类内部定义
class Circle {
private:
double radius;
public:
// 构造函数在类内部定义
Circle(double r) : radius(r) {}
// 成员函数在类内部定义
double area() {
return 3.14159 * radius * radius;
}
};
或者:
// 在类外部定义
class Circle {
private:
double radius;
public:
Circle(double r); // 构造函数声明
double area(); // 成员函数声明
};
// 类外部定义构造函数
Circle::Circle(double r) : radius(r) {
}
// 类外部定义成员函数
double Circle::area() {
return 3.14159 * radius * radius;
}
在大型项目中,通常在头文件(.h)中声明类,并在源文件(.cpp)中定义成员函数,这种分离有助于提高编译效率和代码组织。
const成员函数
const成员函数是承诺不会修改对象状态的函数,它们可以被const对象调用:
class Date {
private:
int day, month, year;
public:
Date(int d, int m, int y) : day(d), month(m), year(y) {}
// const成员函数不能修改对象的状态
int getDay() const {
return day;
}
// 非const成员函数可以修改对象的状态
void setDay(int d) {
day = d;
}
};
const Date birthday(1, 1, 2000); // const对象
int day = birthday.getDay(); // 可以调用const成员函数
// birthday.setDay(2); // 错误:不能在const对象上调用非const成员函数
const成员函数的重要性:
- 表明函数的意图(不修改对象状态)
- 允许const对象调用该函数
- 提高代码安全性
静态成员函数
静态成员函数属于类而非对象实例,它们可以在不创建对象的情况下被调用,但只能访问静态成员变量:
class MathUtils {
public:
// 静态成员函数
static double square(double num) {
return num * num;
}
static double cube(double num) {
return num * num * num;
}
};
// 通过类名调用静态函数,无需创建对象
double squared = MathUtils::square(4.0); // 返回16.0
double cubed = MathUtils::cube(4.0); // 返回64.0
静态成员函数的特点:
- 使用
static
关键字声明 - 通过类名直接调用,不需要对象
- 不能访问非静态成员
- 没有
this
指针
特殊成员函数
C++类有一些特殊的成员函数,如果不显式定义,编译器会自动生成它们。
构造函数
构造函数是创建对象时调用的特殊成员函数,用于初始化对象的状态:
class Person {
private:
std::string name;
int age;
public:
// 默认构造函数(无参数)
Person() : name("Unknown"), age(0) {
std::cout << "Default constructor called" << std::endl;
}
// 带参数的构造函数
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "Parameterized constructor called" << std::endl;
}
// 拷贝构造函数
Person(const Person& other) : name(other.name), age(other.age) {
std::cout << "Copy constructor called" << std::endl;
}
};
构造函数可以通过不同方式创建对象:
Person p1; // 调用默认构造函数
Person p2("Alice", 30); // 调用带参数的构造函数
Person p3 = p2; // 调用拷贝构造函数
Person p4 = Person("Bob", 25); // 临时对象初始化
析构函数
析构函数是对象被销毁时自动调用的特殊函数,用于释放对象占用的资源:
class ResourceManager {
private:
int* data;
public:
// 构造函数分配资源
ResourceManager(int size) {
data = new int[size];
std::cout << "Resource allocated" << std::endl;
}
// 析构函数释放资源
~ResourceManager() {
delete[] data;
std::cout << "Resource freed" << std::endl;
}
};
void useResource() {
ResourceManager rm(100); // 构造函数被调用
// 使用资源...
} // 函数结束时,rm超出作用域,析构函数被调用
析构函数的特点:
- 名称是类名前加波浪号(~)
- 没有返回值,也没有参数
- 每个类只能有一个析构函数
- 当对象超出作用域或被显式删除时,析构函数被调用
this指针
在C++中,每个成员函数都有一个隐含的参数this
,它是一个指向当前对象的指针。this
指针使成员函数能够访问调用它的对象:
class Counter {
private:
int count;
public:
Counter(int c = 0) : count(c) {}
// 使用this指针访问成员变量
void increment() {
this->count++; // 等同于 count++;
}
// 返回对象自身的引用,用于链式调用
Counter& add(int value) {
count += value;
return *this; // 返回当前对象的引用
}
int getCount() const {
return count;
}
};
// 链式调用示例
Counter c;
c.add(5).add(10).increment();
std::cout << c.getCount() << std::endl; // 输出16
this
指针的重要用途:
- 区分同名的成员变量和参数
- 实现链式调用
- 在模板中用于确定类型
class Person {
private:
std::string name;
public:
// 使用this区分成员变量和参数
Person(const std::string& name) {
this->name = name; // 没有this会导致歧义
}
};
类的组合与关联
组合关系
组合是一种"has-a"关系,表示一个类包含另一个类的实例作为其成员:
class Engine {
public:
void start() {
std::cout << "Engine started" << std::endl;
}
void stop() {
std::cout << "Engine stopped" << std::endl;
}
};
class Car {
private:
Engine engine; // Car包含一个Engine
std::string brand;
public:
Car(const std::string& b) : brand(b) {}
void turnOn() {
std::cout << brand << " car turning on..." << std::endl;
engine.start();
}
void turnOff() {
std::cout << brand << " car turning off..." << std::endl;
engine.stop();
}
};
组合的优点:
- 代码重用
- 隐藏实现细节
- 提高模块性
关联关系
关联表示一个类使用另一个类,但不一定拥有它:
class Student; // 前向声明
class Course {
private:
std::string name;
std::vector<Student*> students; // 关联多个Student
public:
Course(const std::string& n) : name(n) {}
void addStudent(Student* s) {
students.push_back(s);
}
std::string getName() const {
return name;
}
};
class Student {
private:
std::string name;
std::vector<Course*> courses; // 关联多个Course
public:
Student(const std::string& n) : name(n) {}
void enrollCourse(Course* c) {
courses.push_back(c);
c->addStudent(this);
}
void listCourses() const {
std::cout << name << " is enrolled in:" << std::endl;
for (const auto& course : courses) {
std::cout << "- " << course->getName() << std::endl;
}
}
};
这种关系通常通过指针或引用实现,能表达多种关联类型:
- 一对一关系
- 一对多关系
- 多对多关系
实例:设计一个银行账户系统
下面是一个简单的银行账户系统设计,展示了类、对象和组合的实际应用:
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
// 交易记录类
class Transaction {
private:
std::string date;
std::string description;
double amount;
bool isDeposit;
public:
Transaction(const std::string& d, const std::string& desc, double amt, bool deposit)
: date(d), description(desc), amount(amt), isDeposit(deposit) {}
void display() const {
std::cout << std::left << std::setw(12) << date
<< std::setw(30) << description
<< std::setw(15) << (isDeposit ? "+" : "-")
<< std::right << std::fixed << std::setprecision(2)
<< amount << std::endl;
}
double getAmount() const {
return isDeposit ? amount : -amount;
}
};
// 客户信息类
class Customer {
private:
std::string name;
std::string address;
std::string phoneNumber;
public:
Customer(const std::string& n, const std::string& addr, const std::string& phone)
: name(n), address(addr), phoneNumber(phone) {}
std::string getName() const {
return name;
}
void updateAddress(const std::string& addr) {
address = addr;
}
void updatePhone(const std::string& phone) {
phoneNumber = phone;
}
void displayInfo() const {
std::cout << "Customer: " << name << std::endl;
std::cout << "Address: " << address << std::endl;
std::cout << "Phone: " << phoneNumber << std::endl;
}
};
// 银行账户类
class BankAccount {
private:
std::string accountNumber;
Customer* owner; // 关联关系
double balance;
std::vector<Transaction> transactions; // 组合关系
// 生成交易记录并添加到历史
void recordTransaction(const std::string& desc, double amount, bool isDeposit) {
// 获取当前日期(简化版)
std::string today = "2023-06-15"; // 实际应用应获取系统日期
Transaction trans(today, desc, amount, isDeposit);
transactions.push_back(trans);
}
public:
BankAccount(const std::string& accNum, Customer* cust, double initialDeposit = 0.0)
: accountNumber(accNum), owner(cust), balance(initialDeposit) {
if (initialDeposit > 0) {
recordTransaction("Initial deposit", initialDeposit, true);
}
}
void deposit(double amount, const std::string& desc = "Deposit") {
if (amount <= 0) {
std::cout << "Error: Deposit amount must be positive." << std::endl;
return;
}
balance += amount;
recordTransaction(desc, amount, true);
std::cout << "Deposit successful. New balance: " << balance << std::endl;
}
bool withdraw(double amount, const std::string& desc = "Withdrawal") {
if (amount <= 0) {
std::cout << "Error: Withdrawal amount must be positive." << std::endl;
return false;
}
if (amount > balance) {
std::cout << "Error: Insufficient funds." << std::endl;
return false;
}
balance -= amount;
recordTransaction(desc, amount, false);
std::cout << "Withdrawal successful. New balance: " << balance << std::endl;
return true;
}
double getBalance() const {
return balance;
}
void printStatement() const {
std::cout << "\n====== Account Statement ======\n";
owner->displayInfo();
std::cout << "Account Number: " << accountNumber << std::endl;
std::cout << "Current Balance: $" << std::fixed << std::setprecision(2) << balance << std::endl;
std::cout << "\nTransaction History:\n";
std::cout << std::left << std::setw(12) << "Date"
<< std::setw(30) << "Description"
<< std::setw(15) << "Type"
<< "Amount" << std::endl;
std::cout << std::string(70, '-') << std::endl;
for (const auto& trans : transactions) {
trans.display();
}
std::cout << std::string(70, '-') << std::endl;
}
};
// 储蓄账户(继承示例,将在下一篇文章详细介绍继承)
class SavingsAccount : public BankAccount {
private:
double interestRate;
public:
SavingsAccount(const std::string& accNum, Customer* cust, double initialDeposit = 0.0, double rate = 0.01)
: BankAccount(accNum, cust, initialDeposit), interestRate(rate) {}
// 计算并添加利息
void addInterest(const std::string& desc = "Interest payment") {
double interest = getBalance() * interestRate;
deposit(interest, desc);
}
};
// 使用示例
int main() {
// 创建客户
Customer alice("Alice Smith", "123 Main St", "555-1234");
// 创建银行账户
BankAccount account("AC001", &alice, 1000.0);
// 执行交易
account.deposit(500.0, "Salary");
account.withdraw(200.0, "Grocery shopping");
account.deposit(100.0, "Gift");
account.withdraw(50.0, "Dinner");
// 打印账户报表
account.printStatement();
// 创建和使用储蓄账户
SavingsAccount savings("SA001", &alice, 2000.0, 0.02);
savings.deposit(1000.0, "Bonus");
savings.addInterest(); // 添加利息
savings.printStatement();
return 0;
}
此示例展示了:
- 如何定义和使用类
- 如何使用成员函数和成员变量
- 组合关系(BankAccount包含Transaction对象)
- 关联关系(BankAccount关联一个Customer对象)
- 继承的简单应用(将在下一篇文章中详细介绍)
类与对象的最佳实践
封装原则
- 隐藏实现细节:将成员变量声明为private,并提供public访问方法
- 提供清晰的接口:设计直观、一致的公共接口
- 最小权限原则:仅暴露必要的功能
// 不好的实践
class BadAccount {
public:
double balance; // 直接暴露数据
void updateBalance(double newBalance) {
balance = newBalance; // 无验证
}
};
// 好的实践
class GoodAccount {
private:
double balance;
public:
double getBalance() const {
return balance;
}
bool deposit(double amount) {
if (amount <= 0) return false;
balance += amount;
return true;
}
bool withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
};
合理使用构造函数和初始化
- 始终初始化所有成员变量
- 使用初始化列表(更高效,对const成员和引用成员必需)
- 提供合理的默认构造函数
- 考虑使用默认参数代替多个构造函数
class Rectangle {
private:
double width;
double height;
const std::string unit;
public:
// 坏习惯:在构造函数体内赋值
Rectangle(double w, double h) {
width = w;
height = h;
// unit = "cm"; // 错误:const成员不能在这里赋值
}
// 好习惯:使用初始化列表
Rectangle(double w, double h, const std::string& u = "cm")
: width(w), height(h), unit(u) {
// 构造函数体可以为空
}
};
使用const修饰符
- 将不修改对象状态的成员函数声明为const
- 将函数参数尽可能声明为const引用
- 成员变量适当使用const修饰
class Vector {
private:
double x, y, z;
public:
Vector(double _x, double _y, double _z) : x(_x), y(_y), z(_z) {}
// 不修改对象状态的函数声明为const
double magnitude() const {
return std::sqrt(x*x + y*y + z*z);
}
// 接受const引用参数
Vector add(const Vector& other) const {
return Vector(x + other.x, y + other.y, z + other.z);
}
// 不能声明为const,因为它修改了对象状态
void scale(double factor) {
x *= factor;
y *= factor;
z *= factor;
}
};
明智地使用友元
友元(friend)提供了一种允许外部函数或类访问私有成员的机制,但应谨慎使用:
class Complex {
private:
double real;
double imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 声明友元函数
friend Complex operator+(const Complex& a, const Complex& b);
// 声明友元类
friend class ComplexCalculator;
};
// 友元函数定义
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real + b.real, a.imag + b.imag);
}
// 友元类
class ComplexCalculator {
public:
static void printDetails(const Complex& c) {
std::cout << "Real: " << c.real << ", Imaginary: " << c.imag << std::endl;
}
static Complex conjugate(const Complex& c) {
return Complex(c.real, -c.imag);
}
};
友元的使用准则:
- 仅在确实需要时使用友元
- 通常用于运算符重载和辅助类/函数
- 避免破坏封装原则
避免构造过大的类
大型类难以理解和维护。将大型类分解为小型、专注于单一职责的类:
// 不好的设计:一个巨大的类
class Monolith {
private:
// 客户数据
std::string customerName;
std::string address;
// 产品数据
std::vector<std::string> products;
// 订单数据
double totalAmount;
bool isPaid;
// 支付处理
void processPayment() { /* ... */ }
// 库存管理
void updateInventory() { /* ... */ }
// 发票生成
void generateInvoice() { /* ... */ }
public:
// 大量接口方法...
};
// 更好的设计:分离关注点
class Customer {
private:
std::string name;
std::string address;
public:
// 客户相关方法...
};
class Product {
private:
std::string name;
double price;
int stock;
public:
// 产品相关方法...
};
class Order {
private:
Customer customer;
std::vector<Product> items;
double totalAmount;
bool isPaid;
public:
// 订单相关方法...
};
class PaymentProcessor {
public:
bool process(Order& order, const std::string& paymentMethod) {
// 处理支付...
}
};
class InvoiceGenerator {
public:
void generate(const Order& order) {
// 生成发票...
}
};
总结
本文介绍了C++中类与对象的基础概念和使用方法,包括类的定义、访问控制、成员变量和成员函数、构造函数和析构函数、this指针以及类之间的关系。类和对象是C++面向对象编程的基础,掌握它们对于编写可维护、可重用和可扩展的代码至关重要。
面向对象编程(OOP)的核心优势包括:
- 封装:隐藏实现细节,只暴露必要的接口
- 代码重用:通过组合和继承实现
- 模块化:将系统分解为相互协作的对象
- 易维护性:对象封装变化,减少修改影响范围
在下一篇文章中,我们将深入探讨类的继承与多态性,这是面向对象编程的另外两个重要支柱。我们还将介绍虚函数、抽象类和接口的概念,进一步展示C++面向对象编程的强大能力。
练习题
-
设计一个
Time
类,表示时、分、秒,提供设置和获取时间、增加秒数以及格式化显示时间的功能。 -
创建一个
Stack
类实现基本的栈操作(push、pop、top、isEmpty、size),使用私有数组存储元素。 -
设计一个简单的
String
类,实现字符串的基本操作,包括构造函数、析构函数、复制、连接和比较等功能。 -
设计一个
ShapeManager
类,能够存储和管理不同类型的形状(如圆形、矩形),并计算它们的总面积。
参考资料
- Bjarne Stroustrup. The C++ Programming Language (4th Edition)
- Scott Meyers. Effective C++
- cppreference.com - Classes
- C++ Core Guidelines - Classes and Class Hierarchies
- Stanley B. Lippman. Inside the C++ Object Model
这是我C++学习之旅系列的第八篇技术文章。查看完整系列目录了解更多内容。