C语言是一门面向过程的编程语言,面向过程更注重于解决问题的步骤和流程。而C++是基于面向对象的,关注的是对象之间的交互,。我们将数据和相关的功能封装在一起,形成对象。每个对象都有自己的属性(数据)和方法(功能)。
以做一道青椒肉丝来说
我们需要厨师、青椒、肉丝、锅、铲、油、盐、老抽等物品,然后再准备烹饪。先放油,油热再放肉丝,肉丝炒的差不多之后再放青椒,然后再放入盐、老抽,最后装盘,这些就属于面向过程;
在面向对象编程中,我们会将青椒肉丝看作一个对象,并定义该对象的属性和方法。
对象:青椒肉丝
- 属性:青椒、肉片、调味料等
- 方法:准备食材、炒菜、调味、出锅等
我们可以创建一个青椒肉丝对象,并调用对象的方法来完成烹饪过程。每个方法内部可以封装相应的逻辑和操作。
例如,调用青椒肉丝对象的"准备食材"方法,内部会包含切青椒、切肉片和腌制肉片的操作。调用"炒菜"方法时,内部会进行炒肉片和炒青椒的步骤。通过调用对象的方法,我们可以按照特定的顺序和逻辑来完成整个烹饪过程。
面向过程适用于简单的、直线式的问题,或者强调步骤和算法的场景。面向对象适用于复杂的、需要模拟真实世界的问题,或者强调对象之间关系和交互的场景。现在市面上主流的编程语言基本上都是面向对象:C++、Java、Python等。
在C语言中,结构体就可以封装多个对象用来描述一些负责的事物。在C++中,将结构体升级成了类,不仅可以定义变量,还可以定义函数。
struct Stack
{
//方法(函数)
void Init(int defaultCapacity = 4)
{
a = (int*)malloc(sizeof(int) * 4);
if (nullptr == a)
{
perror("malloc fail");
exit(-1);
}
capacity = defaultCapacity;
top = 0;
}
void Push(int x)
{
//...
;
}
void Destory()
{
free(a);
a = nullptr;
top = capacity;
}
//...
//成员变量(属性)
int* a;
int top;
int capacity;
};
int main()
{
//兼容C语言
struct Stack s1;
//C++
Stack st2;
st2.Init();
st2.Push(1);
st2.Push(2);
st2.Destory();
return 0;
}
虽然struct
可以定义类,但是为了区分C语言,C++更喜欢用class
来定义类。
class
定义为类的关键字,后面紧跟类名,{}
为类的主体。
class ClassName {
private:
// 成员变量(私有)
DataType memberVariable1;
DataType memberVariable2;
...
public:
// 构造函数
ClassName(); // 默认构造函数
ClassName(parameter1, parameter2, ...); // 带参数的构造函数
// 成员函数(公有)
ReturnType memberFunction1(parameter1, parameter2, ...);
ReturnType memberFunction2(parameter1, parameter2, ...);
...
};
class
关键字来定义类,后面紧跟着类名(通常遵循命名约定,使用驼峰命名法)。private
)进行声明,只能在类的内部访问。public
)进行声明,并可以包含任意的参数和返回类型。成员函数可以访问类的成员变量,执行特定的操作。类里面定义的函数,默认是内联函数,但具体也还是编译器决定是否为内联函数。
当使用class
来定义时,程序编译报错,我们定义的这些方法,无法访问。
这些是因为C++中类进行了封装和访问控制,用三种修饰符来实现:
private
(私有): 成员被声明为 private
时,它们只能在类的内部访问。私有成员对于类的外部是不可见的,外部代码无法直接访问它们。public
(公有): 成员被声明为 public
时,它们可以在类的内部和外部被访问。公有成员对外部代码可见,可以通过类的对象访问和操作。protected
(保护): 成员被声明为 protected
时,它们与私有成员类似,只能在类的内部和派生类中访问。受保护成员对于外部代码是不可见的,但可以在派生类中被继承和访问。访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
class
如果不设置访问限定符,默认为private
;
struct
因为要兼容C语言,所以默认为public
。
面向对象的三大特性:封装、继承、多态。
在面向对象的封装中,类是封装的基本单位,类封装了数据成员和成员函数。封装的主要目的是将相关的数据和操作组织在一起,形成一个独立的实体,对外部隐藏其内部实现细节,只提供公共接口。
比如说,我们国家的博物馆里面的展品,大部分都会设置阻隔,我们观众可以参观,但是无法直接接触到这些展品。这在一定程度上保护了这些展品,同时也规范了观众的行为,满足了观众的基本需求。
在程序中,封装也是如此,通过将数据和操作封装在一起,实现了数据的隐藏和保护。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域
class MyClass {
public:
int publicMember; // 公有成员
void PrintInfo();
private:
int privateMember; // 私有成员
protected:
int protectedMember; // 受保护成员
};
//指定PrintInfor属于MyClass这个类域
void MyClass::PrintInfo()
{
cout << "MyClass" << endl;
}
int main() {
MyClass myObject;
myObject.publicMember = 10; // 可访问公有成员
myObject.PrintInfo();
// 以下代码将导致编译错误,无法访问私有成员和受保护成员
// myObject.privateMember = 20;
// myObject.protectedMember = 30;
return 0;
}
类的实例化是指根据类的定义创建一个具体的对象,使其具备类所定义的属性和行为。
我们定义一个类:
class Person
{
public:
void showInfo()
{
cout << "xxx" << endl;
//...
}
private:
char* _name;
char* _sex;
int _age;
};
这里并没有实例化↑,只是描述出了一个人应该具备的基本属性。
//直接声明对象变量静态实例化对象
Person P1;
Person P2;
实例化对象 ↑ P1、P2。
类就好比一张食谱,食谱上面有许多道菜的做法,但没有实际的菜存在,所以定义出一个类并没有分配实际的内存空间来存储它。
我们将其实例化,就是把这道菜做出来呈现在我们眼前,它实际存在,占据物理空间。
class MyClass
{
public:
void Print()
{
cout << "MyClass" << endl;
}
private:
int a;
};
int main()
{
cout << sizeof(MyClass) << endl;
return 0;
}
类中既有成员变量,又有成员函数,那么这在计算内存的时候,是如何计算的呢?
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
}
执行该程序得出:
4
1
1
我们发现,类的大小,计算的是该类中的成员变量之和,并没有算入函数;而没有成员变量或者是空类,大小为1byte。
这是因为成员函数是共享给所以该类的使用对象使用的代码,并不直接存储在该类中。
打个比方:
假设我们有一个小区,里面有很多房屋,每个房屋都有自己的卧室、客厅、厨房等空间,这些就代表着类对象中的数据成员。
但是小区里面会有公共的篮球场、公园等空间,这些并没有算在自己房子的大小之内,属于公共区域,供小区居民共享使用的。
在这个比喻中,房屋可以类比为类对象的数据成员,而篮球场可以类比为类的成员函数。房屋占用实际的空间,我们说房子多大就是自己的房子有多少个平方,而篮球场作为公共设施,只是为小区居民提供服务,不会计算在房子的大小中。
类成员计算大小时,也遵循内存对齐规则——内存对齐。
我们定义一个人的类Person
class Person
{
public:
void Init(const std::string& n, const std::string& sex, int age)
{
_name = n;
_sex = sex;
_age = age;
}
void Print()
{
cout << _name << " " << _sex << " " << _age << endl;
}
private:
std::string _name;
std::string _sex;
int _age = 0;
};
int main()
{
Person p1, p2;
p1.Init("zhangsan", "man", 21);
p2.Init("cuihua", "woman", 20);
p1.Print();
p2.Print();
}
Person
类中,有Init
和Print
两个成员函数,我们刚才说到,成员函数并没有算入类的空间中,那当p1
调用Init
和Print
的时候,这两个函数是怎么知道是p1
呢?
C++中,给每个非静态成员函数,增加了一个隐藏的指针参数,该指针指向了当前对象,函数体的所以成员变量的操作都是通过指针去访问的。这个对于我们用户来说是透明的,不需要我们来传递,编译器自动完成。
this
指针是成员函数的第一个隐藏指针形参,由编译器自动传递
由于
this
指针是一个形参,所以应该就和普通的参数一样存放在栈区里面,但是有些编译器会对其进行优化。
this
指针是一个常量指针,它指向当前对象并不能被修改,也不能被显式地赋值给其他指针变量。它始终指向当前对象,无法指向其他对象。
有了这些知识储备,我们来看一下,这段程序的运行结果是什么?
class A
{
public:
void PrintA()
{
cout << "PrintA()" << endl;
}
private:
int _a;
};
class B
{
public:
void PrintB()
{
cout << _b << endl;
}
private:
int _b;
};
int main()
{
A* pa = nullptr;
B* pb = nullptr;
pa->PrintA();
pb->PrintB();
return 0;
}
运行发现,控制台上打印出了PrintA()
,随后程序运行崩溃了。
这是因为A和B在调用各自的Print()
成员函数时,没有发生解引用,因为Print()
的地址不在对象中,A和B作为实参传递给了this
指针。但是进入Print()
函数后,由于A和B都是空,所以this
指针也为空,A没有对齐解引用,而B访问了它的成员_b
,本质上就是this->_b
,对空指针解引用,所以会崩溃。
我们需要知道类是面向对象编程的核心概念,类是对象的蓝图,定义了对象的属性和行为,而对象则是根据类的定义创建的实体。
那本期的分享就到这里啦,如果有帮助的话,希望三连支持一下,我们下期再见,如果还有下期的话。