> 文章列表 > C++类与对象—上

C++类与对象—上

C++类与对象—上

本期我们来学习类与对象

目录

面向过程和面向对象初步认识

类的引入

 访问限定符

类的定义

封装

类的作用域

类的实例化

this指针

C语言和C++实现Stack的对比 


面向过程和面向对象初步认识

C 语言是 面向过程 的, 关注 的是 过程 ,分析出求解问题的步骤,通过函数调用逐步解决问题。
比如我们洗衣服

 这是C语言的的实现,我们要按步骤,一步一步来求解

C++ 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完成。

 再比如我们要设计一个外卖系统

如果是面向过程的话,就是上架,点餐,送餐,结算等等,关注的是步骤

而如果是面向对象的话,就是商家,骑手,客户三者之间的相互关系了,关注的是对象与对象之间关系和交互,就像在虚拟的世界里描述现实世界一样,是将现实世界的类和对象映射到计算机里

大家可以认为面向对象是一种更高级的开发方式,是语言的进化方向,现在新的语言几乎都是面向对象的,所以这也是我们学习的方向

类的引入

我们之前说过,C++里将结构体升级为了类

s1是我们以前创建栈的方式,因为现在的栈是一个类了,所以我们可以直接用类名来创建栈,如上面的s2 

我们栈里面的a,capacity,top都叫做成员变量

 此外,C++还可以在类里面定义函数,这叫做成员函数(方法)

我们之前为了区分,在各种函数前加上了前缀,比如StackInit,QueueInit,现在可以在类里面定义函数,我们就不需要加前缀了

两个函数都叫Init,但是因为域不同,所以不会干扰,C++里{ } 定义的都是域

我们随便写点函数

这里成员变量并不一定要在函数后面,在前面也是可以的,甚至写在中间也没问题,因为类域是一个整体

我们再调用函数就非常方便了,用 . 的方式即可,我们写起来是比C语言舒服的

上面结构体的定义, C++ 中更喜欢用 class 来代替

但我们换成class后又编译不通过了,这就涉及到了权限的问题

 访问限定符

【访问限定符说明】
1. public 修饰的成员在类外可以直接被访问
2. protected private 修饰的成员在类外不能直接被访问 (我们暂时认为 protected private 是类似的 )
3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class 的默认访问权限为 private struct public( 因为 struct 要兼容 C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

 

我们加上public就可以编译通过了,pubilc是公有的,直到遇到下一个访问限定符前都是公有的,但我们一般不希望成员变量也是公有的,所以要加上private

 说明白点就是,我们想给别人用的是公有,不想给的是私有

我们切换成struct也是可以用访问限定符的,因为struct也是类,区别在于struct默认是公有,class默认是私有,所以我们之前用struct可以通过编译,而class不可以,不过我们不推荐用默认,最好显示的指定出来

类的定义

class className
{// 类体:由成员函数和成员变量组成}; // 一定要注意后面的分号
class 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面 分号不能省
类体中内容称为 类的成员: 类中的 变量 称为 类的属性 成员变量 ; 类中的 函数 称为 类的方法 或者 成员函数
类的声明和定义是可以分离的,我们来看一看

 需要注意的是,我们在定义时要加上类域,让编译器知道,这不是一个普通函数,而是一个类的成员函数

另外,如果我们在类里面直接定义函数,就默认是内联,这些函数前加不加inline都无所谓

比如我们的Push和Destroy都是内联,当然最终是否成为内联要取决于编译器

所以我们长的函数定义和声明分离,短的函数就直接定义了

关于类还有一个问题

这里的year是无法区分的,所以我们喜欢在成员变量前加上下划线

后面加下划线也是可以,也有人喜欢加m

其他方式也可以,大家根据喜好选择就行 

封装

面向对象的三大特性: 封装、继承、多态
封装是将数据和方法放在一起,不想给你看的变成私有,想给你看的变成公有,封装的本质是一种更好的管理
比如电脑,我们最后使用的只有鼠标,键盘,显示器等等,但电脑真正工作的是cpu,显卡,内存等等一些硬件元件,我们并不需要关系主板上的线路有些什么,cpu是如何工作的等等
我们举个例子,我们去参观一些景点,博物馆时,有些东西会被保护起来,被围起来,如果不将他们保护起来,我们可能就会看到那些地方刻着xxx到此一游,xxx love xxx等等,有了封装,我们按照规则来游玩,就是一种更好的管理
C语言的数据和方法是分离的,C++是没有分离的,我们在C语言时写的栈,Top函数哪怕只有一行,我们也要写成函数

我们正常用是这样用的

但是如果有人这样写呢?

一会调用函数,一会自己访问

更严重一点的,如果这样搞,就可能会出现随机值了,你怎么知道top是栈顶元素,还是下一个位置呢?

 但是如果我们没有提供STTop函数,让用的人自己去想top是什么,就会出现各种各样的问题

所以C++为了解决这些问题,为了杜绝这种乱写代码的情况产生,就有了封装

我们只能按照规则访问,不遵循规则就报错

类的作用域

类定义了一个新的作用域 ,类的所有成员都在类的作用域中 在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:void PrintPersonInfo();
private:char _name[20];char _gender[3];int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{cout << _name << " "<< _gender << " " << _age << endl;
}
另外,局部域和全局域会影响生命周期,而类域和命名空间不会

类的实例化

我们先看这样一个问题

我们在类里面的成员变量是属于声明还是定义? 

答案是声明,对于变量而言,声明和定义的真正区别在于开不开空间

 而且对于类,是整体定义,而不是一个一个的定义

我们这里创建的s,叫做类的实例化对象,或者对象的定义

类就像房子的设计图一样,而对象就是我们根据设计图设计出的房子

类里面能不能存数据?不能,就像图纸里面不能住人

 所以这种错误就是错误的,因为它没有空间,图纸不能住人

我们再看一个问题

 这里应该输出多少呢?是只算成员呢,还是也要算函数呢?

我们看运行结果

 结论:对象的大小只算成员变量(要结构体对齐),不算成员函数

另外,sizeof对象和sizeof类计算出的结果是一样的,就和sizeof(int)是一样的

我们举个例子

我们先把成员变量设置为公有的,我们来看

s1.top和s2.top,并不是同一块空间,就像两栋房子,都有厨房,都有卧室

但是两个Push,是同一个函数,就像我们在小区里,我们的篮球场,篮球场不需要每家每户都建一个,而是在公共的地方建一个,大家一起来使用,篮球场也可以在每家每户建一个,但是没必要,太浪费了,所以调用函数,就是去公共的区域调用

我们再看这个问题

 为什么这里是1,而不是0呢?

举个例子就是我们在小区里建房子时,有一栋房子我们还没规划好,但是需要给这栋房子留个位置,不然之后就无法建造

 这一个字节就是用来占位的,表示对象存在,不然取地址就没办法了

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 a;
};
int main() {Date d1, d2;d1.Init(2022, 1, 2);d2.Init(2023, 2,3);d1.Print();d2.Print();
}

 我们知道d1,d2是两个不同的对象,但是Print函数是在公共区域的,为什么输出的结果却不同呢?

这里面就有一个隐藏的this指针,编译器会将函数处理为

 同时,函数的调用也会被处理为

 这些都是编译器的暗箱操作

另外,还规定了我们不能在形参和实参上显示的去写,所以上面都是报错

但是可以显示的去用,比如

 我们也不能将this改为空指针

 因为this指针的类型是 类类型* const

补充两种错误的写法

 

 上面的是声明,即我们不能在图纸里住,第二个是 :: 这个符号是域作用限定符

this指针是存在哪里的?对象,栈,堆,静态区还是常量区?

答案是在栈里面,this指针是形参,形参是在栈里的

 vs下对this指针的传递进行优化,对象的地址是放在ecx,ecx存储this指针(ecx是寄存器)

注意,这是vs下面的,不是所有的编译器

我们再看一个问题

 答案为第一段代码选C,第二段代码选B

因为第一段代码p调用Print不会解引用,Print的地址不在对象里,不过p会作为实参传递给this指针

 传递空指针并不会报错

第二段代码同样p调用Print不会解引用,但是this传递过去后,this是空指针,函数内部访问了_a,就是空指针解引用

总结一下

1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。
2. 只能在 成员函数 的内部使用
3. this 指针本质上是 成员函数 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储 this 指针
4. this 指针是 成员函数 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用 户传递

C语言和C++实现Stack的对比 

C语言实现

 我们发现用C语言实现栈(这里我只截了开头和结尾,相信大家都知道C实现的栈是什么样子)

每个函数的第一个参数都是 Stack*
函数中必须要对第一个参数检测,因为该参数可能会为 NULL
函数中都是通过 Stack* 参数操作栈的
调用时必须传递 Stack 结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即 数据和操作数据的方式是分 离开的 ,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
C++实现
typedef int DataType;
class Stack
{
public:void Init(){_array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = 3;_size = 0;}void Push(DataType data){CheckCapacity();_array[_size] = data;_size++;}void Pop(){if (Empty())return;_size--;}DataType Top() { return _array[_size - 1]; }int Empty() { return 0 == _size; }int Size() { return _size; }void Destroy(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:void CheckCapacity(){if (_size == _capacity){int newcapacity = _capacity * 2;DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType));if (temp == NULL){perror("realloc申请空间失败!!!");return;}_array = temp;_capacity = newcapacity;}}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Init();s.Push(1);s.Push(2);s.Push(3);s.Push(4);printf("%d\\n", s.Top());printf("%d\\n", s.Size());s.Pop();s.Pop();printf("%d\\n", s.Top());printf("%d\\n", s.Size());s.Destroy();return 0;
}
C++ 中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被 调用,即封装 ,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传 递Stack* 的参数了,编译器编译之后该参数会自动还原,即 C++ Stack * 参数是编译器维护的, C 语言中需 用用户自己维护
我们可以理解为C和C++的区别就是手动挡和自动挡的区别
C就是手段当,需要自己控制变速箱,C++是自动挡,电脑程序控制变速箱
以上即为本期全部内容,希望大家可以有所收获
如有错误,还请指正