C++基础:关于类与对象的学习记录
在C++的编程世界中,类和对象是面向对象编程(OOP)的核心基石。传统的面向过程编程侧重于"怎么做",而面向对象编程则侧重于"谁来做",它将数据和操作数据的方法绑定在一起,使得代码更具模块化、可重用性和易维护性。以下是我在职坐标的学习记录:
1.类和对象的概念:从现实到代码的映射
面向对象编程的核心思想是将现实世界中的事物抽象为代码中的"类"。你可以把"类"理解为一张设计蓝图,它定义了事物应该具备哪些属性(数据)和行为(方法)。而"对象"则是根据这张蓝图在内存中实际建造出来的具体实例。例如,"汽车"是一个类,而"你车库里的那辆红色特斯拉"就是一个对象。
#include <iostream>
using namespace std;
// 定义一个汽车类(蓝图)
class Car {
public:
string brand; // 属性:品牌
void start() { // 行为:启动
cout << brand << " 汽车已启动!" << endl;
}
};
int main() {
Car myCar; // 创建对象(实例)
myCar.brand = "Tesla";
myCar.start(); // 调用对象的方法
return 0;
}
代码讲解:上述代码定义了 Car 类,包含一个字符串属性 brand 和一个无参成员函数 start。在 main 函数中,我们通过 Car myCar; 创建了一个具体的对象,并为其属性赋值、调用其方法,完美展示了类与对象的协作关系。
2.类的定义:封装数据与行为
在C++中,使用 class 关键字来定义类。类内部可以包含成员变量和成员函数。为了保障数据安全,C++提供了访问控制修饰符:public(公开)、private(私有)和 protected(受保护)。
class Student {
public: // 公开区域:外部可以直接访问
string name;
void introduce() {
cout << "我是" << name << ",今年" << age << "岁。" << endl;
}
private: // 私有区域:外部无法直接访问,保证数据安全
int age;
};
代码讲解:Student 类中,name 是公开的,外部可以随意读写;而 age 是私有的,外部无法直接修改,必须通过类内部提供的公开方法(如 introduce)来间接使用。
三大访问修饰符对比
| 访问修饰符 | 类内部访问 | 类外部访问 | 派生类访问 | 核心作用 |
|---|---|---|---|---|
public |
允许 | 允许 | 允许 | 提供对外接口 |
private |
允许 | 禁止 | 禁止 | 隐藏内部实现细节 |
protected |
允许 | 禁止 | 允许 | 为继承体系预留接口 |
3.对象的创建与使用:栈与堆的区别
C++中创建对象有两种主要方式:在栈上直接创建,或在堆上通过指针动态创建。栈上对象生命周期由作用域决定,堆上对象需要手动管理内存。
int main() {
// 1. 栈上创建(推荐,自动释放内存)
Student s1;
s1.name = "张三";
// 2. 堆上创建(需手动delete)
Student* s2 = new Student();
s2->name = "李四"; // 指针使用 -> 访问成员
delete s2; // 必须手动释放内存,防止内存泄漏
s2 = nullptr;
return 0;
}
代码讲解:栈上的 s1 在离开 main 函数时会自动销毁;堆上的 s2 必须使用 -> 运算符访问成员,且用完必须 delete,否则会导致内存泄漏。
4.封装: Getter与Setter的最佳实践
封装是OOP的三大特性之一,其核心是将数据隐藏,并提供安全的访问接口。对于私有成员变量,我们通常提供 getter(获取)和 setter(设置)方法。
class BankAccount {
private:
double balance;
public:
// Setter:带数据校验的设置方法
void setBalance(double amount) {
if (amount >= 0) {
balance = amount;
} else {
cout << "错误:余额不能为负数!" << endl;
}
}
// Getter:安全的获取方法
double getBalance() const { // const表示不修改成员变量
return balance;
}
};
代码讲解:通过 setBalance,我们拦截了非法数据(负数),保证了 balance 始终处于合法状态。这就是封装的魅力所在。
5.构造函数与析构函数:对象的生命周期管理
构造函数在对象创建时自动调用,用于初始化成员;析构函数在对象销毁时自动调用,用于清理资源。
class FileHandler {
private:
string fileName;
public:
// 普通构造函数
FileHandler(string name) {
fileName = name;
cout << fileName << " 文件已打开。" << endl;
}
// 重载构造函数
FileHandler() {
fileName = "default.txt";
cout << "默认文件已打开。" << endl;
}
// 析构函数
~FileHandler() {
cout << fileName << " 文件已安全关闭。" << endl;
}
};
代码讲解:当 FileHandler 对象被创建时,会根据传入的参数调用对应的构造函数;当对象超出作用域或被 delete 时,析构函数 ~FileHandler() 会自动执行,模拟关闭文件的操作。
6.构造函数的初始化列表:高效初始化的秘诀
对于 const 成员或引用成员,必须在初始化列表中赋值。同时,初始化列表比在构造函数体内赋值效率更高,因为它直接初始化,避免了先默认构造再赋值的开销。
class Config {
private:
const int maxRetries;
string path;
public:
// 使用初始化列表
Config(int retries, string p) : maxRetries(retries), path(p) {
// 构造函数体可以为空
}
};
代码讲解:maxRetries 是 const 常量,无法在函数体内被赋值,必须通过 : maxRetries(retries) 这种初始化列表的形式在对象诞生时直接赋予初值。
7.this指针:区分同名变量与链式调用
每个成员函数内部都有一个隐藏的 this 指针,它指向调用该函数的当前对象。常用于解决局部变量与成员变量同名的问题,或实现链式调用。
class Builder {
private:
int width, height;
public:
Builder& setWidth(int width) {
this->width = width; // this->width 是成员变量,width 是局部参数
return *this; // 返回当前对象的引用,支持链式调用
}
Builder& setHeight(int height) {
this->height = height;
return *this;
}
};
// 使用:obj.setWidth(100).setHeight(200);
代码讲解:当参数名与成员变量名相同时,this->width 明确指向成员变量。返回 *this 使得我们可以像搭积木一样连续调用多个设置方法,代码更加优雅。
8.拷贝构造函数与深浅拷贝:内存管理的深水区
当用一个对象去初始化另一个新对象时,会调用拷贝构造函数。如果类中包含指针成员,默认的浅拷贝会导致两个对象指向同一块堆内存,引发"双重释放"崩溃。
class DeepArray {
private:
int* data;
int size;
public:
DeepArray(int s) : size(s) { data = new int[size]; }
// 深拷贝构造函数
DeepArray(const DeepArray& other) {
size = other.size;
data = new int[size]; // 重新分配内存
for(int i = 0; i < size; ++i) data[i] = other.data[i];
}
~DeepArray() { delete[] data; } // 安全释放各自的内存
};
代码讲解:深拷贝在拷贝构造函数中为 data 重新 new 了一块内存,并逐个复制元素。这样两个对象拥有各自独立的内存空间,析构时互不干扰。
浅拷贝与深拷贝对比
| 对比维度 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
|---|---|---|
| 内存分配 | 仅复制指针的值(地址) | 重新分配新的堆内存 |
| 数据独立性 | 两个对象共享同一块内存 | 两个对象拥有独立内存 |
| 析构风险 | 极易引发双重释放(Double Free) | 安全释放,互不影响 |
| 适用场景 | 无指针成员或仅使用智能指针 | 包含裸指针且需手动管理内存 |
9.类与结构体struct的区别:不仅仅是默认权限
在C++中,struct 和 class 在功能上几乎完全相同,都可以包含成员变量、成员函数、构造函数等。它们唯一的硬性区别在于默认的访问权限。
类与结构体对比
| 特性 | class |
struct |
|---|---|---|
| 默认访问权限 | private |
public |
| 默认继承权限 | private |
public |
| 语义习惯 | 强调封装、行为、面向对象 | 强调纯数据聚合、轻量级结构 |
10.类的声明与实现分离:工程化代码组织
在实际项目中,我们通常将类的声明放在 .h 头文件中,将成员函数的实现放在 .cpp 源文件中,以提高编译效率和代码可读性。
// === Calculator.h ===
#ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
private:
int result;
public:
Calculator(); // 声明构造函数
void add(int num); // 声明成员函数
int getResult() const;
};
#endif
// === Calculator.cpp ===
#include "Calculator.h"
#include <iostream>
using namespace std;
// 注意:类外实现必须加上 类名:: 作用域解析符
Calculator::Calculator() : result(0) {}
void Calculator::add(int num) {
result += num;
}
int Calculator::getResult() const {
return result;
}
代码讲解:头文件使用 #ifndef 防止重复包含。在 .cpp 文件中实现函数时,必须使用 Calculator::add 这样的语法告诉编译器这个函数属于哪个类。这是C++大型项目开发的标准规范。
11.总结
类和对象是C++面向对象编程的灵魂。从基础的类定义、访问控制,到内存管理层面的构造/析构函数、深浅拷贝,再到工程化的声明与实现分离,掌握这些知识是进阶C++高手的必经之路。建议初学者多动手敲代码,在调试内存泄漏和编译错误中加深理解。
浙公网安备 33010602011771号