1. 面向对象与面向过程的比较
想象一下,你要盖一栋房子。
面向过程 (Procedure-Oriented):就像你自己亲力亲为地去盖。你的思维是线性的:我先去搬砖 -> 然后和水泥 -> 接着砌墙 -> 最后封顶。你关注的是步骤,是一步一步的动词(动作)。写代码就像写菜谱,第一步做什么,第二步做什么。
// 面向过程的C语言风格代码示例:做一顿饭
void washVegetables() { /* 洗菜 */ }
void cutVegetables() { /* 切菜 */ }
void cook() { /* 炒菜 */ }
int main() {
washVegetables(); // 第一步:洗
cutVegetables(); // 第二步:切
cook(); // 第三步:炒
return 0;
}
面向对象 (Object-Oriented):就像你是一个建筑师或包工头。你的思维是先设计蓝图,再组织分工。你会先想:“我需要一栋房子,这栋房子由门、窗、墙、屋顶组成”。这里的“房子”、“门”、“窗”就是对象。你关注的是事物,是名词(东西),以及这些东西各自有什么功能(方法)。
// 面向对象的C++风格代码示例:组织一顿饭
class Chef { // 1. 先设计一个“厨师”蓝图
public:
void washVegetables() { /* 厨师能洗菜 */ }
void cutVegetables() { /* 厨师能切菜 */ }
void cook() { /* 厨师能炒菜 */ }
};
int main() {
Chef chefWang; // 2. 根据蓝图,请来一个叫chefWang的厨师(创建一个对象)
// 3. 指挥厨师去做事(调用对象的方法)
chefWang.washVegetables();
chefWang.cutVegetables();
chefWang.cook();
return 0;
}
核心区别:
- 面向过程:关心步骤,自己干。
- 面向对象:关心事物,指挥别人(对象)干。代码更模块化,更易维护和复用,就像乐高积木,每个零件(对象)各司其职。
为了更直观地理解两者的工作流程差异,请看下面的流程图:
2. 初步理解C++的类
“类”(Class)是面向对象的核心概念。刚才的“厨师蓝图”就是一个类。我们再打个比方:
- 类 (Class):就像是汽车的设计图纸。图纸上规定了这辆车应该有:
- 属性 (Attributes):比如颜色、品牌、最高时速。这些是名词,在类里叫成员变量。
- 行为 (Methods):比如加速、刹车、鸣笛。这些是动词,在类里叫成员函数。
- 对象 (Object):就是根据那张设计图纸制造出来的真实汽车。根据一张图纸,可以造出千千万万辆真实的汽车。
让我们用代码来画一张“小狗”的设计图:
#include <iostream>
#include <string>
using namespace std; // 为了代码简洁,先告诉编译器我们使用标准命名空间
// 1. 画一张“小狗”的设计图(定义一个类)
class Dog {
// 通常会把属性设为private,像锁在笼子里,保护起来,防止被随意修改。后面会细讲。
private:
string name; // 小狗的名字(成员变量)
int age; // 小狗的年龄(成员变量)
// 通常把行为设为public,像公共接口,谁都可以调用。
public:
// 2. 小狗的行为(成员函数)
void setName(string dogName) { // 给小狗起名的方法
name = dogName;
}
void setAge(int dogAge) { // 设置小狗年龄的方法
age = dogAge;
}
void bark() { // 小狗叫的方法
cout << name << " says: Wang Wang! I'm " << age << " years old." << endl;
// cout 可以理解为“打印到屏幕上”, endl 是换行。
// << 可以想象成“把内容输送过去”。
}
};
int main() {
// 3. 根据图纸制造一只真实的小狗(创建一个对象)
Dog myDog;
// 4. 指挥我的小狗做事(调用对象的方法)
myDog.setName("Wang Cai"); // 给我的小狗起名叫“旺财”
myDog.setAge(2); // 设置它的年龄为2岁
myDog.bark(); // 让我的小狗叫
return 0;
}
输出结果:
Wang Cai says: Wang Wang! I'm 2 years old.
看,我们不需要关心bark()函数内部是怎么实现的,我们只需要知道“让小狗叫”这个行为就行了。这就是封装:把复杂的实现细节隐藏起来,只暴露简单的使用接口。
下面我们用一张类图来更形象地展示Dog类的设计:
图示:Dog类将数据(name, age)私有(-)封装,只对外提供公共(+)的方法来交互。
3. this指针的分析
现在有个问题:在类的成员函数内部,如果参数名字和成员变量名字一样怎么办?比如:
void setName(string name) {
name = name; // 哪个name是参数?哪个name是成员变量?分不清了!
}
这就好比你在一个房间里,房间里有一个大喇叭(成员变量),你手里拿着一个小喇叭(参数)。你说“喇叭开喊”,别人怎么知道是哪个喇叭?
为了解决这个问题,C++提供了一个叫做 this指针 的秘密武器。
- this指针:可以理解为对象自己的“身份证”或者“自指针”。在任何一个成员函数内部,this指针都默默地指向调用这个函数的那个对象自己。
我们用this来修改上面的代码:
class Dog {
private:
string name;
int age;
public:
void setName(string name) { // 参数名和成员变量名可以相同了
this->name = name; // this->name 表示“我自己的name”,
// 等号右边的name是传进来的参数。
// 这就好比说:“我自己的名字(this->name) = 你传过来的新名字(name)”
}
void setAge(int age) {
this->age = age; // “我自己的年龄 = 你传过来的新年龄”
}
void bark() {
// 即使这里不用,this指针也是存在的,可以写成:
// cout << this->name << " says: Wang Wang!" << endl;
cout << name << " says: Wang Wang!" << endl; // 通常name前隐含了this->
}
};
一句话记住this:this就像每个对象心里的“我”字,它总是指向自己。当出现名字冲突时,用this->来明确指明“我自己的那个变量”。
4. 析构函数与构造函数
现在我们来学习两个特殊的函数:对象的“出生证明”和“遗嘱”。
构造函数 (Constructor)
- 它是什么:一个特殊的、在创建对象时自动调用的成员函数。
- 它的作用:初始化对象,为对象的成员变量赋初值。就像孩子一出生,医生就会给他登记出生日期、体重、身高。
- 它的特点:
- 函数名与类名完全相同。
- 没有返回值类型(连void都没有)。
析构函数 (Destructor)
- 它是什么:一个特殊的、在对象被销毁时自动调用的成员函数。
- 它的作用:清理善后,比如释放对象在生命周期内申请的内存、关闭打开的文件等。就像一个人临终前要立下遗嘱,处理自己的财产。
- 它的特点:
- 函数名是在类名前加一个**波浪号~**。
- 没有返回值类型,也没有参数。
#include <iostream>
using namespace std;
class Dog {
private:
string name;
int* ptr; // 一个指针,用来演示析构函数的作用
public:
// 1. 构造函数(可以重载,有多个版本)
Dog() { // 默认构造函数(无参)
name = "Unnamed";
ptr = new int(0); // 在对象创建时,动态申请一块内存
cout << "A dog named \"" << name << "\" is born! (Default Constructor)" << endl;
}
Dog(string name, int age) { // 带参数的构造函数
this->name = name;
ptr = new int(age); // 申请内存,并用age的值初始化
cout << "A dog named \"" << name << "\" is born! (Parameterized Constructor)" << endl;
}
// 2. 析构函数(只有一个,没有参数)
~Dog() {
delete ptr; // 在对象销毁时,释放之前申请的内存,防止内存泄漏
cout << "The dog named \"" << name << "\" says goodbye... (Destructor)" << endl;
}
void bark() {
cout << name << " says: Wang Wang! I'm " << *ptr << " years old." << endl;
}
};
int main() {
cout << "Start of main" << endl;
{ // 一个新的作用域开始
Dog dog1; // 调用默认构造函数,对象dog1诞生!
Dog dog2("Buddy", 3); // 调用带参构造函数,对象dog2诞生!
dog1.bark();
dog2.bark();
} // 作用域结束,dog1和dog2的生命周期到了,它们的析构函数会被自动调用!
cout << "End of main" << endl;
return 0;
}
输出结果:
Start of main
A dog named "Unnamed" is born! (Default Constructor)
A dog named "Buddy" is born! (Parameterized Constructor)
Unnamed says: Wang Wang! I'm 0 years old.
Buddy says: Wang Wang! I'm 3 years old.
The dog named "Buddy" says goodbye... (Destructor)
The dog named "Unnamed" says goodbye... (Destructor)
End of main
注意看析构函数调用的顺序:后创建的对象(dog2)先被销毁,先调用析构函数,就像拆房子要从上往下拆一样。
5. const成员
const是“常量”(constant)的缩写,意思是“不变的”。它有两种主要用法:
1. const对象(常量对象)
就像你买了一个“防修改保护套”给对象套上,声明这个对象自身是常量,不可修改。
const Dog myDog("Lucky", 5); // 创建一个常量小狗Lucky
// myDog.setName("Not Lucky"); // 错误!常量对象不能调用可能修改其内容的函数!
myDog.bark(); // 如果bark()函数保证不会修改对象,那么它就可以被调用
这就引出了下一个问题:如何让bark()函数向编译器保证它不会修改对象呢?
2. const成员函数(常量成员函数)
在成员函数的参数列表后面加上const关键字,这个函数就变成了常量成员函数。它向编译器和使用者承诺:“我绝不会修改这个对象里的任何成员变量”。
class Dog {
// ... 其他成员同上 ...
// 常量成员函数:承诺不会修改对象状态
void bark() const { // 注意函数声明后的const
// this->age = 10; // 错误!在const成员函数内不允许修改成员变量!
cout << name << " says: Wang Wang! (I promise I won't change anything!)" << endl;
}
// 非常量成员函数:允许修改对象
void haveBirthday() {
(*ptr)++; // 年龄加1
}
};
int main() {
const Dog lucky("Lucky", 5); // 常量对象
lucky.bark(); // 正确:bark()是const函数,可以被常量对象调用
// lucky.haveBirthday(); // 错误!haveBirthday()不是const函数,常量对象不能调用
Dog regularDog("Regular", 2); // 非常量对象
regularDog.bark(); // 正确:非常量对象可以调用const函数
regularDog.haveBirthday(); // 正确:非常量对象也可以调用非const函数
return 0;
}
生活化比喻:
- const对象就像一个被玻璃罩罩住的博物馆展品,只能看,不能摸。
- const成员函数就像是展品旁边一个“只读”的讲解员,他只会给你讲解(读数据),绝不会动手改变展品(写数据)。
6. static成员
static意思是“静态的”。类的static成员(变量或函数)有一个非常特别的属性:它不属于任何一个对象,而是属于整个类。
- 静态成员变量:所有对象共享的同一个变量。就像班级里的公共班费,班费不属于任何一个学生,但每个学生都可以知道和使用它。
- 静态成员函数:只能操作静态成员变量的函数。就像班级的生活委员,他管理的只是班费(静态变量),而不是某个学生的私人零花钱(普通成员变量)。
#include <iostream>
using namespace std;
class Dog {
private:
string name;
// 静态成员变量:记录创建了多少只小狗(不属于任何一只狗,属于Dog类本身)
static int count; // 仅仅是在类内声明,需要在类外单独定义和初始化
public:
Dog(string n) : name(n) {
count++; // 每创建一只小狗,计数器就+1
cout << name << " joined! Total dogs: " << count << endl;
}
~Dog() {
count--; // 每消失一只小狗,计数器就-1
cout << name << " left! Total dogs: " << count << endl;
}
// 静态成员函数:它没有this指针,因为它不属于某个特定对象
static int getCount() {
// cout << name; // 错误!静态函数不能访问普通的非静态成员变量(name)
return count; // 正确!静态函数可以访问静态成员变量
}
};
// 在类外定义和初始化静态成员变量(很重要!)
int Dog::count = 0; // “::”是作用域解析运算符,意思是“Dog类里的count”
int main() {
cout << "Initial dog count: " << Dog::getCount() << endl; // 通过类名直接访问静态函数
Dog* dog1 = new Dog("Wang Cai");
cout << "Now, total dogs: " << Dog::getCount() << endl; // 输出:1
{
Dog dog2("Xiao Bai");
cout << "Now, total dogs: " << Dog::getCount() << endl; // 输出:2
// 也可以通过对象访问静态函数,但它属于类,不属于对象
cout << "Same count, accessed by dog2: " << dog2.getCount() << endl; // 输出:2
} // dog2析构,count减1
cout << "After Xiao Bai left: " << Dog::getCount() << endl; // 输出:1
delete dog1;
cout << "Final dog count: " << Dog::getCount() << endl; // 输出:0
return 0;
}
**记住static**:它是类的“公共财产”和“公共服务”,与具体的对象无关。
7. 友元
在C++中,类的private和protected成员是受保护的,外部函数和其他类的函数不能访问。这就像你的私家卧室,外人不能进。
但有时候,你可能会希望你的最好的朋友可以破例进入你的卧室。friend(友元)就是这个“破例”。
- 友元函数:一个非成员函数(普通函数)被授予了访问某个类的私有成员的权力。
- 友元类:一个类的所有成员函数都被授予了访问另一个类私有成员的权力。
注意:友元关系是单向的,也不具有传递性(你朋友的朋友不是你的朋友)。要谨慎使用友元,因为它破坏了封装性。
#include <iostream>
using namespace std;
// 提前声明Dog类,这样Friend函数才知道它的存在
class Dog;
// 一个全局函数(非成员函数)
void friendFunction(const Dog& d);
class Dog {
private:
string secret; // 小狗的秘密,私有成员
public:
Dog() : secret("I hid my bone under the tree!") {} // 初始化秘密
// 声明友元函数!注意,这是在类的内部声明的。
// 这句声明就是在说:“friendFunction函数是我的朋友,让它进我的私人房间!”
friend void friendFunction(const Dog& d);
};
// 实现友元函数
void friendFunction(const Dog& d) {
// 因为这个函数是Dog类的友元,所以它可以访问Dog的私有成员secret
cout << "The dog tells me a secret: \"" << d.secret << "\"" << endl;
// 如果不是友元,这行代码会报错:无法访问private成员
}
int main() {
Dog myDog;
// myDog.secret; // 错误!main函数不是友元,不能访问私有成员。
friendFunction(myDog); // 正确!友元函数可以访问。
return 0;
}
输出结果:
The dog tells me a secret: "I hid my bone under the tree!"
友元比喻:就像公司里,所有员工的薪资都是保密的(private),但HR经理的职能需要查看所有人的薪资,那么就可以把HR经理设为所有员工的friend。但普通员工之间不能互相查看薪资。
总结
好啦,今天我们用了大量的比喻和简单的代码,学习了C++面向对象最最基础的概念。我们来回顾一下:
概念 中文 核心比喻 Class & Object 类与对象 设计图纸 vs 真实产品 this pointer this指针 对象的身份证 / “我” Constructor/Destructor 构造函数/析构函数 出生证明 / 遗嘱 const Member const成员 玻璃罩展品 / 只读讲解员 static Member static成员 公共班费 / 生活委员 friend 友元 最好的朋友(特权)
面向对象编程的思想就是:将数据和操作数据的方法捆绑在一起,形成一个“对象”,然后通过对象之间的交互来完成复杂的程序。它更符合我们人类对现实世界的认知方式。