C++ 面向对象编程(OOP)
核心:面向对象三大特性------封装、继承、多态,核心思想是"将数据和操作数据的方法绑定在一起",提高代码复用性、可维护性和扩展性。
一、面向对象核心概念
1. 类(Class)与对象(Object)
- 类:抽象的模板,定义了对象的属性(成员变量)和行为(成员函数),是"对象的蓝图"。
- 对象:类的实例,是具体的实体,拥有类定义的属性和行为。
- 关系:一个类可以创建多个对象,每个对象的属性值不同,但行为一致。
示例:类与对象的定义和使用
>using namespace std;
// 定义类(Person类)
class Person {
public: // 访问权限(后续讲解)
// 1. 成员变量(属性):描述对象的特征
string name;
int age;
char gender;
// 2. 成员函数(行为):描述对象的操作
void showInfo() { // 成员函数可以直接访问类内成员< "姓名< "< age< ",性别:"< endl;
}
void setAge(int a) { // 封装:通过成员函数修改私有/公有成员
if (a >=<= 120) { // 增加逻辑校验,保证数据安全
age = a;
} else {
cout << "年龄输入非法!"< endl;
}
}
};
// 主函数:创建并使用对象
int main() {
// 1. 创建对象(实例化类)
Person p1; // 栈上创建对象
Person* p2 = new Person(); // 堆上创建对象(需手动释放)
// 2. 给对象的属性赋值(公有成员可直接访问)
p1.name = "张三";
p1.setAge(20); // 通过成员函数赋值(推荐,可做校验)
p1.gender = '男';
p2->name = "李四"; // 堆对象用 -> 访问成员
p2->setAge(25);
p2->gender = '男';
// 3. 调用对象的成员函数
p1.showInfo(); // 输出:姓名:张三,年龄:20,性别:男
p2->showInfo(); // 输出:姓名:李四,年龄:25,性别:男
// 4. 释放堆对象(避免内存泄漏)
delete p2;
p2 = nullptr; // 避免野指针
return 0;
}
2. 类的访问权限(封装的核心)
C++ 通过访问权限控制类成员的可见性,实现"封装"------隐藏内部实现,只对外提供安全的访问接口。
三种访问权限(优先级:类内都可访问,类外按权限区分):
| 访问权限 | 类内 | 类外 | 子类(继承时) | 说明 |
|---|---|---|---|---|
| public(公有) | ✅ | ✅ | ✅ | 对外暴露的接口(如成员函数、公共属性) |
| protected(保护) | ✅ | ❌ | ✅ | 仅类内和子类可访问(用于继承传递) |
| private(私有) | ✅ | ❌ | ❌ | 仅类内可访问(隐藏核心数据,最常用) |
注意:class 关键字默认访问权限是 private;struct 关键字默认访问权限是 public(struct 更适合单纯存储数据,class 适合面向对象封装)。
二、面向对象三大特性(重点)
1. 封装(Encapsulation)
核心思想
将属性(成员变量)和行为(成员函数)绑定在一起,隐藏对象的内部实现细节,只通过公共接口(public 成员函数)访问对象,保证数据的安全性和完整性。
封装的实现步骤
- 将成员变量设为 private(隐藏数据);
- 提供 public 的成员函数(getter/setter),用于访问和修改成员变量;
- 在成员函数中添加逻辑校验,避免非法数据。
示例:封装的标准实现
cpp
class Student {
private: // 私有成员:隐藏核心数据
string id; // 学号(不可直接修改)
string name; // 姓名
int score; // 成绩(0-100)
public: // 公有接口:对外提供访问方式
// getter 函数:获取私有成员的值
string getId() { return id; }
string getName() { return name; }
int getScore() { return score; }
// setter 函数:修改私有成员的值(带校验)
void setId(string i) { id = i; } // 学号无非法范围,直接赋值
void setName(string n) { name = n; }
void setScore(int s) {
if (s >= 0 &&<= 100) {
score = s;
} else< "成绩非法,默认赋值为< endl;
score = 0;
}
}
// 其他行为:封装的业务逻辑
void showScore()< name< id< ")的成绩:< endl;
}
};
// 使用
int main() {
Student s;
s.setId("2024001");
s.setName("王五");
s.setScore(95); // 合法,赋值95
s.showScore(); // 输出:王五(2024001)的成绩:95
s.setScore(105); // 非法,默认赋值0
s.showScore(); // 输出:王五(2024001)的成绩:0
// s.score = 80; // 错误:private成员,类外不可直接访问
return 0;
}
封装的优势
- 数据安全:避免外部直接修改核心数据,通过校验保证数据合法;
- 代码可维护:修改内部实现时,只要接口不变,外部代码无需修改;
- 代码复用:封装的类可以在多个场景重复使用。
2. 继承(Inheritance)
核心思想
子类(派生类)继承父类(基类)的属性和行为,同时可以扩展自己的新属性和新行为,实现"代码复用",减少冗余。
继承的语法
cpp
// 语法:class 子类名 : 继承方式 父类名
class 子类名 : 继承方式 父类名 {
// 子类自己的成员(属性+函数)
};
三种继承方式(核心区别:父类成员在子类中的访问权限)
| 继承方式 | 父类public成员 | 父类protected成员 | 父类private成员 | 说明 |
|---|---|---|---|---|
| public(公有继承) | 子类public | 子类protected | 子类不可访问 | 最常用,保留父类访问权限 |
| protected(保护继承) | 子类protected | 子类protected | 子类不可访问 | 用于子类再继承的场景 |
| private(私有继承) | 子类private | 子类private | 子类不可访问 | 几乎不用,父类成员被隐藏 |
示例:公有继承(最常用)
cpp
// 父类(基类):Animal
class Animal {
protected: // 保护成员:子类可访问,类外不可访问
string name;
int age;
public:
// 父类的构造函数(后续讲解)
Animal(string n, int a) : name(n), age(a) {}
// 父类的行为
void eat() {
cout << "在吃东西!< endl;
}
void sleep()< name< "在睡觉!"< endl;
}
};
// 子类(派生类):Dog,公有继承Animal
class Dog : public Animal {
private:
string breed; // 子类扩展的新属性:品种
public:
// 子类的构造函数:必须先初始化父类(用初始化列表)
Dog(string n, int a, string b) : Animal(n, a), breed(b) {}
// 子类扩展的新行为
void bark() {
< "(< ")在汪汪叫!" << endl;
}
// 子类重写父类的行为(多态的基础,后续讲解)
void eat() override {
cout << name << "(" << ")在吃< endl;
}
};
// 使用
int main() {
Dog dog("旺财", 3, "中华田园犬");
dog.eat(); // 调用子类重写的eat():旺财(中华田园犬)在吃骨头!
dog.sleep(); // 继承父类的sleep():旺财在睡觉!
dog.bark(); // 子类自己的bark():旺财(中华田园犬)在汪汪叫!
// dog.name; // 错误:父类name是protected,子类类外不可访问
return 0;
}
继承的关键细节
- 子类构造函数:必须先初始化父类(通过初始化列表
: 父类构造函数(参数)),再初始化子类自己的成员; - 父类private成员:无论哪种继承方式,子类都不能直接访问(只能通过父类的public/protected成员函数访问);
- 继承的传递性:子类可以继承父类的父类(如 Dog 继承 Animal,Animal 继承 Object);
- 多重继承(不推荐):一个子类可以继承多个父类,容易出现"菱形继承"(二义性),可通过
virtual虚继承解决。
3. 多态(Polymorphism)
核心思想
同一接口,不同实现------父类的指针/引用指向子类对象时,调用同名成员函数,会执行子类的实现(而非父类),实现"动态绑定"。
多态的实现条件(缺一不可)
- 父类中定义虚函数 (用
virtual关键字修饰成员函数); - 子类重写 虚函数(函数名、参数列表、返回值完全一致,可加
override关键字校验); - 用父类的指针或引用指向子类对象。
示例:多态的标准实现
cpp
#include <string>
using namespace std;
// 父类:Shape(图形)
class Shape {
public:
// 虚函数:父类的接口
virtual void draw() {
< "绘制图形< endl;
}
// 虚析构函数(重点!避免内存泄漏)
virtual ~Shape() {
< "Shape析< endl;
}
};
// 子类1:Circle(圆形)
class Circle : public Shape {
public:
// 重写虚函数(override 可选,用于校验重写是否正确)
void draw() override {
< "绘制圆形(< endl;
}
~Circle() override {
< "Circle析构< endl;
}
};
// 子类2:Rectangle(矩形)
class Rectangle : public Shape {
public:
void draw() override< "绘制矩形(□)"< endl;
}
~Rectangle() override< "Rectangle析构函数" << endl;
}
};
// 测试函数:接收父类引用(满足多态条件3)
void drawShape(Shape& shape) {
shape.draw(); // 调用的是子类的draw(),动态绑定
}
int main() {
Circle circle;
Rectangle rectangle;
// 父类引用指向子类对象,调用draw(),执行子类实现
drawShape(circle); // 输出:绘制圆形(○)
drawShape(rectangle); // 输出:绘制矩形(□)
// 父类指针指向子类对象(另一种方式)
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
shape1->draw(); // 输出:绘制圆形(○)
shape2->draw(); // 输出:绘制矩形(□)
// 释放堆对象:虚析构函数会先调用子类析构,再调用父类析构(避免内存泄漏)
delete shape1;
delete shape2;
return 0;
}
多态的关键细节
-
虚函数:
virtual关键字只需要在父类中声明,子类重写时可省略,但推荐加override校验; -
虚析构函数:如果父类指针指向子类堆对象,父类析构函数必须是虚函数,否则只会调用父类析构,导致子类堆内存泄漏(如上例);
-
纯虚函数与抽象类:
-
纯虚函数:
virtual void draw() = 0;(没有函数体,只有声明); -
抽象类:包含纯虚函数的类,不能实例化对象,只能作为父类被继承(子类必须重写纯虚函数,否则子类也是抽象类);
-
示例:
cppclass Shape { public: virtual void draw() = 0; // 纯虚函数 }; // Shape s; // 错误:抽象类不能实例化 class Circle : public Shape { public: void draw() override { ... } // 必须重写 };
-
-
静态多态(重载)vs 动态多态(虚函数):
- 静态多态:编译期绑定,如函数重载(同一函数名,不同参数);
- 动态多态:运行期绑定,如上述虚函数实现(根据对象实际类型调用对应函数)。
三、类的核心成员(补充)
1. 构造函数(Constructor)
- 作用:创建对象时自动调用,用于初始化对象的成员变量(避免成员变量为随机值);
- 特点:
- 函数名与类名完全一致,无返回值(连 void 都不能写);
- 可重载(多个构造函数,参数列表不同);
- 若未手动定义,编译器会自动生成"默认无参构造函数"(空实现);
- 有参构造函数会覆盖默认无参构造(若需要无参,需手动定义)。
构造函数的三种形式
cpp
class Person {
public:
string name;
int age;
// 1. 无参构造函数(默认构造)
Person() {
name = "未知";
age = 0;
< "无参构造< endl;
}
// 2. 有参构造函数(重载)
Person(string n, int a) {
name = n;
age = a;
cout< "有参构造函数< endl;
}
// 3. 初始化列表构造(推荐,效率更高)
Person(string n, int a, char g) : name(n), age(a), gender(g) {
< "初始化列表< endl;
}
private:
char gender;
};
2. 析构函数(Destructor)
- 作用:对象销毁时自动调用,用于释放对象占用的资源(如堆内存、文件句柄);
- 特点:
- 函数名:
~类名(波浪线+类名),无返回值,无参数(不能重载); - 若未手动定义,编译器会自动生成"默认析构函数"(空实现);
- 若对象占用堆内存,必须手动定义析构函数,释放堆内存(避免内存泄漏);
- 多态场景中,父类析构函数必须是虚函数(
virtual ~类名())。
- 函数名:
3. 拷贝构造函数(Copy Constructor)
- 作用:用一个已存在的对象,创建一个新的对象 (如
Person p2 = p1;); - 语法:
类名(const 类名& 引用对象); - 特点:
- 若未手动定义,编译器会自动生成"默认拷贝构造函数"(浅拷贝);
- 若对象有堆内存成员(如
char* name),默认浅拷贝会导致"双重释放",需手动实现深拷贝。
示例:深拷贝(解决浅拷贝问题)
cpp
class Student {
private:
char* name; // 堆内存成员
int age;
public:
// 有参构造:分配堆内存
Student(const char* n, int a) {
age = a;
name = new char[strlen(n) + 1]; // 分配内存(+1存'\0')
strcpy(name, n); // 复制字符串
}
// 拷贝构造函数(深拷贝)
Student(const Student& s) {
age = s.age;
// 重新分配堆内存,避免与原对象共用一块内存
name = new char[strlen(s.name) + 1];
strcpy(name, s.name);
}
// 析构函数:释放堆内存
~Student() {
delete[] name; // 释放字符串堆内存
name = nullptr;
}
};
4. this 指针
- 定义:隐含在每个非静态成员函数中,指向当前对象(调用成员函数的对象);
- 作用:
- 区分成员变量和函数参数(当参数名与成员变量名相同时);
- 返回当前对象本身(如链式调用)。
示例:this 指针的使用
cpp
class Person {
private:
string name;
int age;
public:
// 参数名与成员变量名相同,用this区分
Person(string name, int age) {
this->name = name; // this->name:成员变量;name:参数
this->age = age;
}
// 返回当前对象,实现链式调用
Person& setAge(int age) {
this->age = age;
return *this; // 返回当前对象的引用
}
Person& setName(string name) {
this->name = name;
return *this;
}
void show()< name< age< endl;
}
};
// 使用:链式调用
int main() {
Person p("张三", 20);
p.setName("李四").setAge(25).show(); // 输出:李四,25岁
return 0;
}
5. 静态成员(static)
-
定义:用
static修饰的类成员(静态成员变量/静态成员函数); -
特点:
- 静态成员变量:属于整个类,所有对象共用同一份内存(不占用单个对象的内存);
- 必须在类外初始化(
类型 类名::静态成员变量 = 初始值;); - 可通过
类名::静态成员变量或对象.静态成员变量访问。
- 静态成员函数:属于整个类,没有 this 指针(不能访问非静态成员,只能访问静态成员);
- 可通过
类名::静态成员函数()或对象.静态成员函数()调用。
示例:静态成员的使用
cpp
class Student {
public:
static int count; // 静态成员变量:统计学生个数
string name;
// 构造函数:每次创建对象,count加1
Student(string n) : name(n) {
count++;
}
// 静态成员函数:访问静态成员变量
static int getCount() {
// return name; // 错误:静态函数不能访问非静态成员
return count;
}
};
// 类外初始化静态成员变量(必须写,否则编译错误)
int Student::count = 0;
int main() {
Student s1("张三");
Student s2("李四< "< Student< endl; // 输出:2
< "学生个数< s1.get< endl; // 输出:2
return 0;
}
四、面向对象常见问题(避坑)
- 构造函数无返回值,不能写
void; - 子类构造函数必须先初始化父类(初始化列表);
- 父类指针指向子类堆对象时,父类析构必须是虚函数(避免内存泄漏);
- 拷贝构造函数的参数必须是"const 引用"(避免无限递归);
- 静态成员函数不能访问非静态成员(无 this 指针);
- 虚函数不能是静态成员函数(静态函数属于类,虚函数属于对象,冲突);
- 私有成员(private)无论哪种继承,子类都不能直接访问。
五、总结
面向对象编程的核心是封装、继承、多态:
- 封装:隐藏细节,保证数据安全,对外提供接口;
- 继承:复用父类代码,扩展子类功能;
- 多态:同一接口,不同实现,实现动态绑定,提高代码灵活性。
C++ 面向对象的核心语法围绕"类和对象"展开,重点掌握构造/析构函数、虚函数、this 指针、静态成员,就能灵活运用面向对象思想开发代码。