目录
前言
一、类的定义
1.定义格式
2.访问限定符
3.类域
二、实例化
1.概念
2.对象大小
三、this指针
四、C++和C语言实现 Stack 对比
总结
前言
类和对象(上):简单的类实现。
一、类的定义
1.定义格式
class 为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数示例:
#include <iostream>using namespace std;//假设定义一个栈(非完整)class Stack{public://成员函数//初始化void Init(int n = 4){arr = (int*)malloc(n * sizeof(int));if (arr == NULL){perror("malloc fail!");exit(1);}capacity = n;top = 0;}//入栈void Push(int x){//判断空间是否充足if (top == capacity){int newCapacity = 2 * capacity;//申请新空间int* tmp = (int*)realloc(arr, newCapacity * sizeof(int));if (tmp == NULL){perror("realloc fail!");exit(1);}//修改参数arr = tmp;capacity = newCapacity;}//空间充足arr[top++] = x;}//取栈顶元素int Top(){if (top == 0){return -1;}//直接返回栈顶元素return arr[top - 1];}//销毁void DesTroy(){//判断arrif (arr){free(arr);}//销毁后置空arr = nullptr;capacity = top = 0;}private: //成员变量int* arr;int capacity;int top;};int main(){Stack a;a.Init();a.Push(1);a.Push(2);a.Push(3);cout << a.Top() << endl;a.DesTroy();return 0;}
运行结果:
为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者 m 开头,注意C++中这个并不是强制的,只是一些惯例。
如:(简单日期类)
#include <iostream>using namespace std;class Date{public://成员函数void Init(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private://成员变量int _year;int _month;int _day;};int main(){Date a;a.Init();a.Print();return 0;}
运行结果:
C++中 struct 也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct 中可以定义函数,一般情况下我们还是推荐用class定义类。定义在类里面的成员函数默认为inline(内联函数)。
struct Date{public://成员函数void Init(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private://成员变量int _year;int _month;int _day;};
功能上,class 和 struct 是非常相似的,主要的区别在于默认的访问控制级别
默认访问控制:(不加访问限定符)
class:成员变量和成员函数默认为私有(private),继承默认也是私有。struct:成员变量和成员函数默认为公有(public),继承默认也是公有。
2.访问限定符
C++⼀种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。public 修饰的成员在类外可以直接被访问;protected 和 private 修饰的成员在类外不能直接被访问,protected 和 private 是⼀样的,在 c++的继承篇中才能体现出他们的区别。访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到类结束。例:只有 pubilc 限定符
#include <iostream>using namespace std;class Date{public://成员函数void Init(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}//成员变量int _year;int _month;int _day;};int main(){Date a;a.Init(); //可以直接访问成员变量cout << a._year << endl;return 0;}
例:不能访问 private 限定的成员变量或成员函数
补充:
class定义成员没有被访问限定符修饰时默认为 private,struct默认为public。一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。
3.类域
类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。类外定义函数,“类名::” 放在返回类型与函数名之间#include <iostream>using namespace std;class Date{public://成员函数void Init(int year = 1970, int month = 1, int day = 1);void Print();private://成员变量int _year;int _month;int _day;};//声明和定义分离,需要指定类域.(缺省参数不能再写了)void Date::Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Date::Print(){cout << _year << "-" << _month << "-" << _day << endl;}int main(){Date a;a.Init();a.Print();return 0;}
二、实例化
1.概念
用类类型在物理内存中创建对象的过程,称为类实例化出对象。类是对 对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多 少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。#include <iostream>using namespace std;class Date{public:void Init(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private://这⾥只是声明,没有开空间int _year;int _month;int _day;};int main(){//Date类实例化出对象a和b,这里才分配了空间Date a;Date b;return 0;}
2.对象大小
1. 分析⼀下类对象中哪些成员呢?
类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量。
2. 那么成员函数是否包含呢?
首先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。
3. 再分析⼀下,对象中是否有存储指针的必要呢?
我们可以通过反汇编观察成员函数的地址。
Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量 _year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调用函数被编译成汇编指令[call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找(多态在后续篇章)
上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。这个规则与C语言的结构体内存对齐规则基本一致。
内存对齐规则
第一个成员在与结构体偏移量为0的地址处。其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的⼀个对齐数与该成员大小的较小值。VS中默认的对齐数为8结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数中取最小)的整数倍。如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。#include<iostream>using namespace std;// 计算⼀下A / B / C实例化的对象是多⼤?class A{public:void Print(){cout << _ch << endl;}private:char _ch;int _i;};class B{public:void Print(){cout << "Print()" << endl;}};class C{};int main(){A a;B b;C c;cout << sizeof(a) << endl;cout << sizeof(b) << endl;cout << sizeof(c) << endl;return 0;}
运行结果:
画图分析:
a 的内存对齐: b 和 c 都没有成员变量,b 虽然有一个函数,但前面我们已经分析过,成员函数是存储在一个代码段区域,所以 b 和 c 内存大小是一样的,那为什么内存大小是 1 而不是 0 呢其实,b、c即使没有成员变量,但它们作为实例化出的对象,会为它们开辟 1 字节空间大小,目的是占位,因为如果⼀个字节都不给,怎么表示对象存在过呢!所以这⾥给1字节,纯粹是为了占位标识对象存在。可以认为是一种规定。
三、this指针
如上面Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和 Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?(毕竟类成员函数不存储在类中)。那么这里就要看到C++给了一个隐含的this指针解决这里的问题 编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this 指针。比如Date类的 Init 函数的真实原型为: void Init(Date* const this, int year, int month, int day)#include<iostream>using namespace std;class Date{public://真实原型:this指针被const修饰,不能改变指向// void Init(Date* const this, int year, int month, int day)void Init(int year = 1970, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};
类的成员函数中访问成员变量,本质都是通过this指针访问的,如 Init 函数中给_year赋值, this -> _year = year; 我们也可以显示的用this指针去调用成员变量:
#include<iostream>using namespace std;class Date{public://真实原型:this指针被const修饰,不能改变指向// void Init(Date* const this, int year, int month, int day)void Init(int year = 1970, int month = 1, int day = 1){this->_year = year;this->_month = month;this->_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};
C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
接下来,我们做两道选择题巩固下前面的知识
1.下面程序编译运行的结果是()
A、编译报错 B、运行崩溃 C、正常运行
#include <iostream>using namespace std;class A{public:void Print(){cout << "A::Print()" << endl;}private:int _a;};int main(){A* p = nullptr;p->Print();return 0;}
解析:
首先,这题不能选A,这里没有语法错误,你可能觉得解引用的空指针,但根据前面所学,成员函数的地址是在编译时确定的,没有存在对象中,所以即使这里写了 P-> ,它也没有解引用。记住:指针的值是在运行时确定的,即使成员函数的地址在编译时已经确定。我们前面了解了 this 指针,编译器就是通过隐藏的 this 指针去调用不同对象的成员函数,也就是说成员函数的调用本质是传递了 this 指针,也就是 Print 函数中有一个 A* const this 的形参,将 nullptr 赋值给这个形参,但是函数中没有解引用这个空指针,所以程序不会报错。正确答案:C、正常运行
2.下面程序编译运行结果是()
A、编译报错 B、运行崩溃 C、正常运行
#include <iostream>using namespace std;class A{public:void Print(){cout << "A::Print()" << endl;cout << _a << endl;}private:int _a;};int main(){A* p = nullptr;p->Print();return 0;}
解析:
这两题是连着一起的,第一题理解了,就知道这题选什么。这题主要的区别就是 Print 函数中打印了成员变量 a 的值,编译器是通过隐藏的 this 指针访问成员变量的。显示调用如下所以这时候就是解引用空指针了,地址是在运行时确定的,因此此处会在运行时崩溃正确答案:B、运行崩溃。
3. this指针存在内存哪个区域的()
A.栈 B.堆 C.静态区 D.常量区 E.对象里面
解析:
首先这题绝对不能选 E,我们前面计算对象大小都没有计算 this 指针。this 指针是隐藏在成员函数的形参中的,我们都知道调用函数时会创建函数栈帧,函数的形参就存储在函数栈帧中。堆是存放动态开辟的内存地址的。静态区是像全局变量或者 static 修饰的变量等。常量区是像字符串字面量等。正确答案:A.栈。
四、C++和C语言实现 Stack 对比
面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解⼀下封装。
通过下面两份代码对比,我们发现C++实现 Stack 形态上还是发生了挺多的变化,底层和逻辑上没啥变化。
C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这里的封装的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。C++中有⼀些相对方便的语法,比如 Init 给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为 this 指针隐含的传递了,方便了很多,使用类型不再需要 typedef 用类名就很方便在我们这个C++入门阶段实现的 Stack 看起来变了很多,但是实质上变化不大。等着我们后面看STL 中的用适配器实现的Stack,大家再感受C++的魅力。C语言实现 Stack:
#pragma once#include <stdio.h>#include <stdlib.h>#include <stdbool.h>#include <assert.h>typedef int STDataType;//定义栈结构typedef struct Stack{STDataType* arr;int capacity;//栈的空间大小int top;//栈顶}ST;//初始化void STInit(ST* ps){assert(ps);//全初始化为空ps->arr = NULL;ps->capacity = ps->top = 0;}//销毁void STDesTroy(ST* ps){assert(ps);//判断arrif (ps->arr){free(ps->arr);}//销毁后置空ps->arr = NULL;ps->capacity = ps->top = 0;}//入栈void StackPush(ST* ps, STDataType x){assert(ps);//判断空间是否充足if (ps->top == ps->capacity){//使用三目操作符根据容量是否为0来选择如何给定新容量大小int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//1.如果容量为0,赋值4 2.如果容量不为零,进行二倍扩容//申请新空间STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}//修改参数ps->arr = tmp;ps->capacity = newCapacity;}//空间充足ps->arr[ps->top++] = x;}//判空bool StackEmpty(ST* ps){assert(ps);return ps->top == 0;}//出栈void StackPop(ST* ps){assert(ps);//判断栈容量是否为空assert(!StackEmpty(ps));//直接让栈顶下移一位即可--ps->top;}//取栈顶元素STDataType StackTop(ST* ps){assert(ps);assert(!StackEmpty(ps));//判空//直接返回栈顶元素return ps->arr[ps->top - 1];}//获取栈中有效元素个数int STSize(ST* ps){assert(ps);return ps->top;}
C++实现Stack:
#include <iostream>#include <assert.h>using namespace std;typedef int STDateType;class Stack{public://初始化void Init(int n = 4){_a = (STDateType*)malloc(sizeof(STDateType) * n);if (_a == nullptr){perror("malloc fail!");exit(1);}_capacity = n;_top = 0;}//插入void Push(STDateType x){//判断是否扩容if (_top == _capacity){size_t newCapacity = _capacity * 2;STDateType* tmp = (STDateType*)realloc(_a, newCapacity * sizeof(STDateType));if (tmp == nullptr){perror("realloc fail!");exit(1);}_a = tmp;_capacity = newCapacity;}_a[_top++] = x;}//删除void Pop(){assert(_top > 0);--_top;}//判空bool Empty(){return _top == 0;}//取栈顶元素STDateType Top(){assert(_top > 0);return _a[_top - 1];}//销毁void Destroy(){free(_a);_a = nullptr;_capacity = _top = 0;}private://成员变量STDateType* _a;size_t _capacity;size_t _top;};int main(){Stack st;st.Init();st.Push(1);st.Push(2);st.Push(3);st.Push(4);st.Push(5);while (!(st.Empty())){cout << st.Top() << endl;st.Pop();}st.Destroy();return 0;}
运行结果:
总结
以上就是本文的全部内容了,感谢支持。