目录
三、类和对象
(一) 面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++ 是 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完成。再举个例子来说,在点外卖:
面向过程:点餐下单,接单,分派骑手,骑手送餐
面向对象:用户,商家,骑手
注:C++并不是纯面向对象的语言,而是基于面向对象
(二)类的引入
C 语言结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。 比如:之前在数据结构初阶中,用C 语言方式实现的栈,结构体中只能定义变量 ;现在以 C++ 方式实现, 会发现 struct中也可以定义函数 。//C语言结构体 typedef int DataType; struct Stack { void Init(size_t capacity) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc fail"); return; } _capacity = capacity; _size = 0; } void Push(const DataType& data) { // 扩容 _array[_size] = data; ++_size; } DataType Top() { return _array[_size - 1]; } void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } DataType* _array; size_t _capacity; size_t _size; }; int main() { Stack s; s.Init(10); s.Push(1); s.Push(2); s.Push(3); cout << s.Top() << endl; s.Destroy(); return 0; }
上面结构体的定义,在C++中更喜欢用class来代替。
没错,结构体升级成了类!
在声明结构体/类变量时语法也稍有差异
struct Stack s1; //C语言的结构体/C++的结构体 Stack s; //C++的类
(三)类的定义
C++的类既可以用struct也可以用class,不过一般用class
class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为 类的成员: 类中的 变量 称为 类的属性 或 成员变量 ; 类中的 函数 称为 类的方法 或者 成员函数 。1、类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果 在类中定义 ,编译器可能会将其当成 内 联函数 处理。2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加 类名::
一般情况下,更期望采用第二种方式。
2、成员变量命名方式建议
这样的代码看起来是不是很难受
class Date { void Init(int year, int month, int day) { year = year; //哪个是成员变量,哪个是函数形参??? month = month; day = day; } int year; int month; int day; };
所以为了避免这问题,我们一般在成员变量加个前缀,比如:
class Date { void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; };
(四)类的访问限定符及封装
1、访问限定符
C++ 实现封装的方式: 用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用。【访问限定符说明】1. public 修饰的成员在类外可以直接被访问2. protected 和 private 修饰的成员在类外不能 直接 被访问3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. 如果后面没有访问限定符,作用域就到 } 即类结束。5. class 的默认访问权限为 private , struct 为 public注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
访问限定符的产生是保护代码的安全性,在多人合作时,如果每个人都能访问类中的成员变量,容易出bug,就比如栈的取栈顶元素,并不知道top是从0开始还是-1开始,直接用成员变量就容易出错,所以我们把成员变量私有起来,用公有的函数去提取栈顶元素,而类中的函数是由写栈的人完成,他自己知道代码是如何写的,就会减少bug或者争议。
【面试题】
问题: C++ 中 struct 和 class 的区别是什么?解答:C++需要兼容 C 语言,所以 C++ 中 struct 可以当成结构体使用。另外 C++ 中 struct 还可以用来定义类。和class 定义类是一样的,区别是 struct 定义的类默认访问权限是 public , class 定义的类默认访问权限是private 。注意:在继承和模板参数列表位置, struct 和 class 也有区别,后序给大家介绍。2、封装
【面试题】
面向对象的三大特性: 封装、继承、多态 。在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。封装本质上是一种管理,让用户更方便使用类 。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB 插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU 、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的, CPU 内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可 。在 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; }
(六)类的实例化
用类类型创建对象的过程,称为类的实例化1. 类是对对象进行描述的 ,是一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没 有分配实际的内存空间 来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。谜语: " 年纪不大,胡子一把,主人来了,就喊妈妈 " 谜底:山羊2. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量
int main() { Person._age = 100; // 编译失败:error C2059: 语法错误:“.” return 0; }
Person 类是没有空间的,只有 Person 类实例化出的对象才有具体的年龄。3. 做个比方。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间int main() { person p; p._age = 100; //依然不行,因为_age是私有的 return 0; }
(七)类对象模型
1、如何计算类对象的大小
值得说明的地方是类的内存对齐是和结构体一样的
成员变量是存到对象的,但是成员函数不是放在对象的,因为每个对象的实例化都是调用那一个成员函数,但是成员变量都是各自的
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 d; cout << sizeof(d) << endl; return 0; }
2、类对象的存储方式猜测
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时, 每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?代码只保存一份,在对象中保存存放代码的地址只保存成员变量,成员函数存放在公共的代码段
我们用代码来推出是哪种方式↓
class Date { public: void Init(int year, int month, int day) { int i = 0; cout << year << endl; } }; int main() { Date d; cout << sizeof(d) << endl; return 0; }
再综合1、如何计算类对象的大小 中的结果
结论:一个类的大小,实际就是该类中 ” 成员变量 ” 之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。3、结构体内存对齐规则
1. 第一个成员在与结构体偏移量为 0 的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS 中默认的对齐数为 83. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。【面试题】
1. 结构体怎么对齐? 为什么要进行内存对齐?2. 如何让结构体按照指定的对齐参数进行对齐?能否按照 3 、 4 、 5 即任意字节对齐?3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
(八)this指针
1、引出
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
class Date { public: void Init(int year, int month, int day) //表面是3个参数,实际是4个 多个this { _year = year; _month = month; _day = day; } void Print() //表面是0个参数,实际是1个 有this { cout <<_year<< "-" <<_month << "-"<< _day <<endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(2023,7,20); d2.Init(2023,7,21); d1.Print(); d2.Print(); return 0; }
处理之后的代码↓
class Date { public: void Init(Date* this,int year, int month, int day) //表面是3个参数,实际是4个 多个this { _year = year; _month = month; _day = day; } void Print(Date* this) //表面是0个参数,实际是1个 有this { cout <<_year<< "-" <<_month << "-"<< _day <<endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(&d1,2023,7,20); d2.Init(&d2,2023,7,21); d1.Print(&d1); d2.Print(&d2); return 0; }
2、特性
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。2. 只能在 “ 成员函数 ” 的内部使用3. this 指针本质上是 “成员函数”的形参(在栈帧) ,当对象调用成员函数时,将对象地址作为实参传递给this 形参。所以 对象中不存储this指针 。4. this 指针是 “ 成员函数 ” 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递【面试题】
1. this指针存在哪里?寄存器
2. this指针可以为空吗?可以
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
选C,虽然p是一个空指针,但是成员函数不存在对象,而是公共区域,并没有解引用。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout<<_a<<endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
选B函数中的_a,会变为this->_a,而this就是p,会产生空指针问题,运行崩溃
3、C语言和C++实现Stack的对比
1)C语言
typedef int DataType; typedef struct Stack { DataType* array; int capacity; int size; }Stack; void StackInit(Stack* ps) { assert(ps); ps->array = (DataType*)malloc(sizeof(DataType) * 3); if (NULL == ps->array) { assert(0); return; } ps->capacity = 3; ps->size = 0; } void StackDestroy(Stack* ps) { assert(ps); if (ps->array) { free(ps->array); ps->array = NULL; ps->capacity = 0; ps->size = 0; } } void CheckCapacity(Stack* ps) { if (ps->size == ps->capacity) { int newcapacity = ps->capacity * 2; DataType* temp = (DataType*)realloc(ps->array, newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc申请空间失败!!!"); return; } ps->array = temp; ps->capacity = newcapacity; } } void StackPush(Stack* ps, DataType data) { assert(ps); CheckCapacity(ps); ps->array[ps->size] = data; ps->size++; } int StackEmpty(Stack* ps) { assert(ps); return 0 == ps->size; } void StackPop(Stack* ps) { if (StackEmpty(ps)) return; ps->size--; } DataType StackTop(Stack* ps) { assert(!StackEmpty(ps)); return ps->array[ps->size - 1]; } int StackSize(Stack* ps) { assert(ps); return ps->size; } int main() { Stack s; StackInit(&s); StackPush(&s, 1); StackPush(&s, 2); StackPush(&s, 3); StackPush(&s, 4); printf("%d\n", StackTop(&s)); printf("%d\n", StackSize(&s)); StackPop(&s); StackPop(&s); printf("%d\n", StackTop(&s)); printf("%d\n", StackSize(&s)); StackDestroy(&s); return 0; }
2)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 语言中需用用户自己维护 。