我们都知道C语言是面向过程的,关注过程,是通过函数调用逐步解决问题的。C++是面向对象的,讲一件事情拆分成不同的对象,靠对象之间的交互完成。
面向过程强调的是功能行为,以函数为最小单位,考虑怎么做。
面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
以一个经典的例子来说明的话:
人把大象装进冰箱
面向过程
1、把冰箱门打开
2、把大象放进冰箱
3、把冰箱门关闭
面向对象
1、分析动作是由谁来完成的:人、冰箱、大象
2、定义主体的属性和功能
人:人需要有打开关闭冰箱、将大象放入冰箱的功能
冰箱:冰箱需要具有能够开门和关门的属性
大象:大象需要具体能够进入冰箱的功能
二者均可实现代码重用和模块化编程;但面向过程简单直接,容易理解(直男式解决方案),面向对象更为复杂,模块化程度更高。从开发角度来看,面向对象比面向过程复杂,从维护和扩展功能的角度上来看,面向对象更容易操作。
C++结构体的升级
C语言中结构体只能定义变量,在C++中把struct升级成了类,类名就可以代表类型,因此结构体内不仅可以定义变量,也可以定义函数,并且一个类可以实例化出多个对象。
struct Stack
{
//可以在结构体中定义成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc fail");
return;
}
capacity = n;
top = 0;
}
int* array;
size_t capacity;
size_t top;
};
还可以感受一下C与C++的不同
typedef struct ListNodeC
{
int val;
struct LisNodeC* next;
}LTNode;
struct ListNodeCPP
{
int val;
ListNodeCPP* next;
};
这些写法是更偏向于C语言的,在C++中,我们更习惯使用class定义类。
类
class className
{
//类体:有成员函数和成员变量组成
};
C++中的类是C语言结构体的升级版,因此类的定义和结构体很相似。class为定义类的关键字,className为类名,类体部分是成员变量和成员函数
定义方式
声明和定义都放在类体中
class Stack
{
public:
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc fail");
return;
}
capacity = n;
top = 0;
}
void Push(int x)
{
if (top == capacity)
{
size_t newcapacity = capacity == 0 ? 4 : 2 * capacity;
(int*)realloc(array, sizeof(int) * newcapacity);
if (nullptr == array)
{
perror("realloc fail");
return;
}
}
array[top++] = x;
}
private:
int* array;
size_t capacity;
size_t top;
};
像这样,函数的声明和定义都放在类体中了。(private和public会在下面进行讲解)
需要注意的是,如果成员函数在类中定义,编译器可能会将其当做内联函数处理。
声明和定义分离
class Person
{
public:
//显示基本信息的函数
void showInfo();
public:
char* _name;
char* _sex;
int _age;
};
#include"Person.h"
void Person::showInfo()
{
cout << name_ << "-" << _sex << "-" << _age << endl;
}
定义时,成员函数前需要加上类名::来进行指定。
成员变量命名规则的建议
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
//这里不会编译报错,但是实际上并没有初始化成功
//因为局部变量优先,这种写法很难区分函数形参和成员变量
private:
int year;
int month;
int day;
};
在C++中,为了方便区分函数形参和成员变量,我们习惯在变量前加一个 _下划线
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
类的访问限定符和封装
访问限定符
C++实现封装的方式为,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用。
public修饰的成员在类外可以直接访问
protected/private修饰的成员在类外不能直接访问
访问权限作用域从该访问限定符出现的位置开始知道下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到}即类结束
class
的默认访问权限为private,struct为public
封装
面向对象的三大特性:封装、继承、多态。在类和对象阶段,主要是研究类的封装特性。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公共接口来和对象进行交互。封装的本质是一种管理,让用户更方便使用类。
打个比方的话,对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器, USB 插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU 、显卡、内存等一些硬件元件。对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的, CPU 内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来因此对象内部实现细节,控制哪些方法可以在类外部直接使用。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
void PrintPersonInfo();
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
//这里就需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
类的实例化
用类类型创建对象的过程,成为类的实例化
1、类是对对象进行描述的,是一个模型一样的东西,先顶了类有哪些成员,定义出一个类并没有实际分配内存空间来存储它。
2、一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date da1;
da1.Init(2024, 4, 17);
//这里如果da1.Init(_year,_month,_day)就会报错
//因为_day,_year,_month这些成员变量并没有真正开辟空间,只是声明。只有当定义出类变量da1的时候才会对类中的成员变量开辟空间。
}
类实例化出来的对象就像现实中使用建筑设计图建造出房子,类就是设计图,对象就是房子。只设计出需要什么东西,但是没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据。
类的对象大小的计算
一个类的大小,实际就是该类中 ” 成员变量 ” 之和,当然要注意内存对齐, 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
我么可以来实际感受一下
class A1
{
public:
void F1(){}
private:
int _a;
};
class A2
{
public:
void F2(){}
};
class A3
{};
分别计算每个类的大小可以看到
sizeof(A1)=4 sizeof(A2)=1 sizeof(A3)=1
A1中只有一个成员函数和成员变量构成,总大小为4字节
A2中只有一个成员函数,A3中什么都没有,但是都是1个字节,说明类的空间大小没有计算成员函数的大小。
结构体内存对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS 中默认的对齐数为 8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
类成员函数的this指针
this指针的作用
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << "." << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date da1, da2;
da1.Init(2024,4,14);
da2.Init(2024, 4, 15);
da1.Print();
da2.Print();
return 0;
}
对于上述类,有这样的一个问题:
Date类中有Init于Print两个成员函数,函数体中没有关于不同对象的区分,那当da1调用Init函数时,该函数是如何知道应该设置da1对象,而不是设置da2对象呢?
原因在于,C++通过引入this指针解决了这个问题。C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有关于“成员变量”的操作,都是通过该指针去访问,只不过所有的操作对用户都是透明的,编译器自动完成。也就是说上面成员函数实际上是这样的
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << this->_year << " " << this->_month << "." << this->_day << endl;
}
this指针的特性
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。
2. 只能在 “ 成员函数 ” 的内部使用
3. this 指针本质上是 “ 成员函数 ” 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给
this 形参。所以对象中不存储 this 指针 。
4. this 指针是 “ 成员函数 ” 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传
递,不需要用户传递