xml
hello,这里是AuroraWanderll。
兴趣方向:C++,算法,Linux系统,游戏客户端开发
欢迎关注,我将更新更多相关内容!
这是类和对象系列的第一篇文章:
之前由于第一次发布时篇幅过长,可能导致阅读体验很差,现在我把他按要点进行了适当拆分,希望能帮助读者更好理解,也方便自己复习。
C++面向对象与类和对象(一)----C++重要基础入门知识
简易目录
- 第一个重要问题:面向对象和面向过程的区别
- 第二个重要问题:C++中结构体升级成了类
- 第三个重要问题:类的定义与编码规范
第一个重要问题:面向对象和面向过程的区别
类和对象的知识是我们学习C++编程的基础,它在我们的知识体系中扮演了一个至关重要的角色。
但在开始学习类、对象、继承这些具体概念之前,我们首先要解决一个最根本的问题:到底什么是"面向对象"?它和我们可能更熟悉的"面向过程"编程有什么不同?
一、面向过程编程:关注步骤的"流水线"
核心思想: "怎么做?"------关注的是解决问题需要的一系列步骤或函数。
在面向过程的世界里,程序员像一个流水线工程师,他的任务是精确地设计出每一个操作步骤。对于"把大象装进冰箱"这个问题,他的思路会是这样的:
- 打开冰箱门。
- 把大象塞进去。
- 关上冰箱门。
面向过程的特点:
- 核心是函数: 程序由一个个函数组成,数据(如
door(门的开关状态),content(冰箱中的内容))和操作数据的函数是分离的。 - 线性思维: 代码按照预定的步骤顺序执行。
- 优点: 流程直观,在解决小型、任务明确的问题时非常高效。
缺点: 当系统变得庞大复杂时,数据和函数的关系会变得混乱。如果想增加一个"给冰箱通电"的功能,相关的数据和函数可能会散落在程序的各个角落,难以维护和扩展。
二、面向对象编程:关注对象之间的相互作用
核心思想: "谁来做?"------关注的是解决问题中涉及到的各类对象,以及它们之间的交互。
在面向对象的世界里,程序员像一个社会架构师。他首先会定义出这个问题的解决方案里有哪几种角色(类 ),每种角色有什么属性(数据成员 )和能力(成员函数)。对于同样的问题,他的思路会是:
- 这个问题里涉及几个对象?------ 冰箱 和大象。
- 每个对象各自有什么属性和能力?
- 冰箱:有门(状态)、内部容量、可以执行开门、装东西、关门等动作。
- 大象:有名字、体积等属性。
面向对象的特点:
- 核心是类和对象: 程序是由多个对象组成的,对象是数据(属性)和操作(方法)的封装体。
- 三大特性:
- 封装: 把数据和处理数据的方法捆绑在一起,并可以隐藏内部细节(利用public,protected,private等访问限定符,后面会讲)。比如,
main函数不需要知道Refrigerator(初始化冰箱状态) 内部如何记录门的状态,只需要调用它的openDoor()方法。 - 继承: 允许我们基于已有的类创建新类,实现代码的复用和扩展。(后续章节详解)
- 多态: 允许不同的对象对同一消息做出不同的响应。(后续章节详解)
- 封装: 把数据和处理数据的方法捆绑在一起,并可以隐藏内部细节(利用public,protected,private等访问限定符,后面会讲)。比如,
- 优点: 代码结构清晰,更接近现实世界,易于维护、扩展和复用。当需求变化时,通常只需要修改或扩展某个类,而不会影响整个程序,方便工程管理。
-
举个例子:我们在竞技游戏中,某个角色的技能出现了bug,例如王者荣耀的露娜大招不刷新的bug。我们会说策划怎么还不修复露娜大招bug。而这个说法说的正是对象!我们的露娜是一个对象。如果我们是面向过程,我们则会说哪一步出了问题,而不是谁谁谁出了问题。
可以看出,面向对象似乎比面向过程更贴近我们人的思维方式
第二个重要问题:C++中结构体升级成了类
在C语言中,我们学习的struct已经具有了很强大的功能,它能够在内部定义变量,我们用它来实现了很多很多数据结构。
而现在,在C++中,我们不在拘泥于定义变量,我们的对象,需要有更多的功能与方法,来进行更加复杂却便利的交互。
一、从C的 struct 到C++的 class:一次伟大的升级
在C语言中,struct 只是一个数据集合 ,它允许你将不同的数据类型组合在一起,形成一个新的复合类型。但它不能包含函数(方法)。
我们的struct中现在不仅可以定义变量,我们还可以定义包含方法,而我们所说的这些struct的功能已经无限接近class的功能。
C语言的 struct(数据包):
c
// C语言代码
struct Person_C {
char name[20];
int age;
};
// 只能定义数据,不能定义函数
// 操作这个结构体的函数必须与结构体分离
void printPerson(struct Person_C p) {
printf("Name: %s, Age: %d\n", p.name, p.age);
}
到了C++,class 的概念被引入,它极大地扩展了 struct 的能力。C++中的 class 是一个数据与行为的封装体。
C++的 class(智能对象):
// C++代码
class Person_CPP {
private:
std::string name;
int age;
public:
// 可以在类内部定义函数(方法)
void setName(const std::string& newName) {
name = newName;
}
void setAge(int newAge) {
if(newAge > 0) { // 可以加入逻辑验证
age = newAge;
}
}
void print() const { // 成员函数
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
升级的核心点:
- 数据封装:可以包含成员变量(数据)和成员函数(行为)。
- 访问控制 :引入了
public、private、protected关键字来控制成员的访问权限。 - 构造函数/析构函数:提供了对象自动初始化和清理的能力。
- 继承与多态:成为面向对象继承体系的基础。
可以说我们的C++中的struct已经无限接近于class,但是他们还有一些细微的区别。
二、C++中的 struct 与 class:孪生兄弟的细微差别
现在来到问题的关键:C++中的 struct 和 class 有什么区别?
答案是:除了默认的成员访问权限和默认的继承方式,它们没有任何功能上的区别。 C++对原有的C风格 struct 进行了增强,让它拥有了 class 的所有能力。
核心区别:默认访问权限
class:成员和继承默认是private的。struct:成员和继承默认是public的。
让我们用代码来展示这个区别:
示例1:默认成员访问权限
// 使用 class 关键字
class MyClass {
int data; // 默认为 private,外部无法直接访问
public:
void setData(int d) { data = d; }
};
// 使用 struct 关键字
struct MyStruct {
int data; // 默认为 public,外部可以直接访问
void setData(int d) { data = d; }
};
int main() {
MyClass obj_c;
// obj_c.data = 10; // 错误!data 是 private 成员
obj_c.setData(10); // 必须通过公共接口
MyStruct obj_s;
obj_s.data = 20; // 正确!data 是 public 成员
obj_s.setData(20); // 当然也可以这样做,也就是说通过公共接口或是直接访问都可以
return 0;
}
示例2:默认继承方式
class Base {
public:
int x;
};
// class 默认是 private 继承
class DerivedClass : Base { // 等价于 : private Base
// 在DerivedClass内部,Base的public成员x变成了private
};
// struct 默认是 public 继承
struct DerivedStruct : Base { // 等价于 : public Base
// 在DerivedStruct内部,Base的public成员x仍然是public
};
int main() {
DerivedClass d_c;
// d_c.x = 10; // 错误!由于是private继承,x在派生类中不可见
DerivedStruct d_s;
d_s.x = 10; // 正确!由于是public继承,x在派生类中保持public
return 0;
}
重要提示 :尽管默认行为不同,但我们强烈建议在代码中显式地写出访问控制符和继承方式,以避免混淆。
// 好的实践:显式声明
class MyClass {
private:
int data;
public:
MyClass() = default;
};
struct MyStruct {
public: // 虽然默认就是public,但写上更清晰
int data;
MyStruct() = default;
};
class Derived : public Base { // 显式声明public继承
// ...
};
三、如何选择:struct vs class?
既然功能几乎一样,我们该如何选择?C++社区形成了一些约定俗成的惯例:
- 使用
struct:- 当你主要需要一个纯粹的数据结构时。
- 当所有成员都希望是公共的时(例如:坐标点
Point {x, y},配置参数Config {width, height})。 - 用于与C代码交互的兼容性结构体。
- 在模板元编程中,有时用
struct来作为"编译期函数"或特性标签,因为它默认的public更便捷。
- 使用
class:- 当你需要定义一个具有复杂行为 和严格封装的抽象数据类型(Abstract Data Type)时。
- 当你需要用到私有成员、保护成员,并需要通过公共接口来与对象交互时。
- 当你打算使用继承和多态等面向对象特性时(尽管
struct也可以,但class的语义更贴切)。
简单来说:struct 感觉更像一个"数据包",而 class 感觉更像一个"功能完整的对象"。
总结
- C++的
class是C语言struct的超级增强版,引入了成员函数和访问控制。 - 在C++中,
struct和class是几乎完全相同的概念。 - 它们唯一的核心区别在于默认的访问权限和继承方式。
- 在实际编程中,根据语义和惯例来选择使用哪一个,并始终显式地写出访问控制符,这能让你的代码更清晰、更专业。
理解了这一点,你就可以自信地在C++中使用这两种关键字了。
第三个重要问题:类的定义与编码规范
理解了面向对象的思想和class与struct的关系后,现在让我们正式学习如何在C++中定义类。这就像学习一门新语言的语法规则一样,是打好基础的关键一步。
一、类的基本语法结构
class ClassName
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意这个分号!
class:定义类的关键字ClassName:类的名字(遵循大驼峰命名法,如MyClass){}:类的主体,包含类的所有成员(成员变量啊,成员函数啊等等);:类定义结束的分号绝对不能省略!这是很多初学者容易犯错的地方。
类的成员分为两类:
-
成员变量(属性):描述对象的特征,如人的年龄、姓名
-
成员函数(方法):描述对象的行为,如人会走路、说话
比如我们定义一个学生类,那么他的成员变量就是学号,姓名,年龄
他的成员函数就是写作业,考试,上课
就好像语文中的名词与动词的区别一样。
二、类的两种定义方式
方式1:声明和定义全部放在类体中
class Date {
public:
// 成员函数直接在类内部定义
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
特点:
-
优点:写法简单,适合教学和小型项目
-
注意 :在类内部定义的成员函数,编译器可能会将其当作内联函数处理
这里我们可以简单了解一下内联函数(不是重点,可以跳过):
传统函数调用过程:
- 跳转到函数地址
- 保存现场(压栈)
- 执行函数体
- 恢复现场(出栈)
- 跳转回调用处
内联函数的处理方式: 编译器会将内联函数的代码体直接展开到调用处,避免函数调用的开销(省略了压栈出栈一系列步骤)。
重要提醒 :即使是显式使用 inline 关键字,也只是给编译器的建议,不是命令!编译器最终还是会根据自己的判断来决定是否真正内联。
内联的优缺点
优点:
- 消除函数调用开销,提高性能
- 避免跳转指令,有利于CPU指令流水线
缺点:
-
代码膨胀:每个调用处都展开一份函数体
-
可能增加编译后文件大小
-
调试困难(函数调用栈信息可能丢失)
可能被当成内联函数这一点是需要我们避免的,在这里编译器有完全的自主权,到底有没有被当成内联函数我们不得而知。这是一种不确定性,而且内联可能导致编译完之后代码膨胀(因为展开)
方式2:声明和定义分离(推荐!)
Date.h(头文件):
class Date {
public:
// 只在类中声明函数
void Init(int year, int month, int day);
void Print();
private:
int _year;
int _month;
int _day;
};
Date.cpp(源文件):
#include "Date.h"
#include <iostream>
// 在源文件中定义函数,需要加上类名和作用域解析符 ::
void Date::Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Date::Print() {
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
为什么推荐方式2?
- 分离编译:头文件只放声明,源文件放实现,符合软件工程原则
- 减少依赖:修改函数实现时,只需要重新编译对应的.cpp文件
- 代码清晰:接口和实现分离,更易于阅读和维护
- 避免内联膨胀:不会意外产生大量的内联函数(与上文相呼应)
三、成员变量命名规范(重要!)
这是一个看似简单但极其重要的实践问题。先看一个反面教材:
class Date {
public:
void Init(int year) {
// 灾难!这里的year到底是成员变量还是函数参数?
year = year; // 自己赋值给自己,毫无意义!
}
private:
int year; // 成员变量
};
上面的代码中,year = year 实际上只是把参数赋值给自己,成员变量完全没有被修改!
这样既导致了严重的南辕北辙的逻辑错误,也使阅读代码变成非常困难的一件事。比我们在C语言中的普通的变量命名规范重要的多,后者只是规范而前者是你必须好好命名否则会有严重后果
解决方案:使用命名区分
方案1:使用下划线前缀(常见于Linux/Unix风格)
class Date {
public:
void Init(int year) {
_year = year; // 清晰明了!
}
private:
int _year; // 成员变量加下划线
int _month;
int _day;
};
方案2:使用'm'前缀(常见于Windows/MFC风格)
class Date {
public:
void Init(int year) {
mYear = year; // 同样清晰!
}
private:
int mYear; // 成员变量加m前缀
int mMonth;
int mDay;
};
核心原则:
- 一致性:选择一个风格并在整个项目中坚持使用
- 可读性:让成员变量在代码中一眼就能被认出来
- 避免歧义:确保不会与函数参数或局部变量混淆
在实际工作中,请遵循你所在团队的编码规范。没有绝对的对错,只有一致性最重要!
xml
感谢你能够阅读到这里,如果本篇文章对你有帮助,欢迎点赞收藏支持,关注我,
我将更新更多有关C++,Linux系统·网络部分的知识。