不要觉的自己很没用,其实你还可以给家人带来温暖,比如爸妈看到你就来火
目录:
一、面向过程和面向对象初步认识
二、类的引入
三、类的定义
四、类的访问限定符及封装
1.访问限定符
2.封装
五、类的作用域
六、类的实例化
七、类的对象大小的计算
1.类对象的存储方式猜测
2.结构体内存对齐规则
3.为什么要进行内存对齐?
八、类成员函数的this指针
1.this指针的作用
2.this指针的特性
3.面试题练习
九、完结撒❀
前言:
对于类和对象的讲解一共划分为3篇,这是第1篇入门篇学习,更适合小白宝宝的体质
–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀-正文开始-❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–
一、面向过程和面向对象初步认识
C 语言是 面向过程 的, 关注 的是 过程 ,分析出求解问题的步骤,通过函数调用逐步解决问题。 比如我们可以拿洗衣服为例:
C++ 是 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完 成。
二、类的引入
C 语言结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。 比如: 之前在数据结构初阶中,用 C 语言方式实现的栈,结构体中只能定义变量 ;现在以 C++ 方式实现, 会发现 struct 中也可以定义函数。 举例代码如下:
typedef int DataType;struct Stack{ void Init(size_t capacity) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败"); 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;}
可以看到在上面代码中,结构体stack里面定义了变量:
DataType * _array ; size_t _capacity ; size_t _size ; 还定义了函数: void Init ( size_t capacity ) void Push ( const DataType & data ) DataType Top () void Destroy () 所以在使用这些函数时,直接在创建的结构体变量中调用即可完成。 上面结 构体的定义, 在 C++ 中更喜欢用 class 来代替,也就是struct换成class。
三、类的定义
类的定义如下:
class className{ // 类体:由成员函数和成员变量组成}; // 一定要注意后面的分号
class 为 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面 分 号不能省略 。 类体中内容称为 类的成员: 类中的 变量 称为 类的属性 或 成员变量 ; 类中的 函数 称为 类的方法 或者 成员函数 类的两种定义方式: 1. 声明和定义全部放在类体中,需注意:成员函数如果 在类中定义 ,编译器可能会将其当成 内 联函数 处理。
2. 类声明放在 .h 文件中,成员函数定义放在 .cpp 文件中,注意: 成员函数名前需要加类名 ::
在后序工作中一般使用第二种定义方式
在上面演示图片中的代码,细心的同学可能已经注意到了,我们定义人的结构体变量名前面都加上了一个_,其实这是为了防止变量名冲突混淆含义而定义的
举例如下:
class Data{public://我们看这个函数会感觉很僵硬void Init(int year, int month, int day){//这里的year、month、day到底是成员变量还是函数形参year = year;month = month;day = day;}private:int year;int month;int day;};
上面代码无法区分Init函数中谁是形参谁是变量
所以我们在实际中一般这样写:
class Data{public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};
其他命名方式都可,这主要看公司要求,一般都是加一个前缀加一个后缀标识区分就行。
上面定义结构体中我们使用了public和private,这是类的访问限定符,下面我们对其进行间接。
四、类的访问限定符及封装
1.访问限定符
C++ 实现封装的方式: 用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用
【访问限定符说明】 1. public 修饰的成员在类外可以直接被访问 2. protected 和 private 修饰的成员在类外不能直接被访问 ( 此处 protected 和 private 是类似的 ) 3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止 4. 如果后面没有访问限定符,作用域就到 } 即类结束。 5. class的默认访问权限为private,struct为public(因为struct要兼容 C) 注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别 我们还拿上面代码举例讲解:class Data{public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};
上面代码,类中共有域为public到private的范围,私有域为private到类结束为止
那么我们在主函数中创建类变量即可访问到函数Init,但是访问不到类的成员变量,因为成员变量范围在私有域内,所以只能在类里面才能被访问
class Data{public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};int main(){Data da1;da1.Init(2024, 4, 7);//直接访问da1.Init(_day);//直接报错return 0;}
之前看到的一道【面试题】:
问题: 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. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量
还拿上面代码举例:
class Data{public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};int main(){Data da1;da1.Init(2024, 4, 7);//直接访问da1.Init(_day);//直接报错return 0;}
类中的成员变量_year,_month,_day都是对其进行的声明,并没有开辟真正的空间,只有当定义出类变量da1时才会真正对类中的成员变量开辟空间,这就是类的实例化。
3. 做个比方。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设 计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象 才能实际存储数据,占用物理空间七、类的对象大小的计算
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算 一个类的大小? 我们举例猜测下面代码的存储方式:class Person{public: void PrintPersonInfo(); void SetPersonInfo();private: char _name[20]; char _gender[3]; int _age;};
1.类对象的存储方式猜测
··· 对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一 个类创建多个对象时, 每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么 如何解决呢?
··· 代码只保存一份,在对象中保存存放代码的地址
··· 只保存成员变量,成员函数存放在公共的代码段
问题:对于上述三种存储方式,那计算机到底是按照那种方式来存储的?
我们再通过对下面的不同对象分别获取大小来分析看下
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只有一个成员函数组成,总大小为1个字节
A3为空类,里面什么都没定义,总大小为1个字节
空类和只有一个成员函数的类大小相等都为1个字节,说明类的空间大小并没有计算成员函数的大小
那么我们就可以推测出
结论:一个类的大小,实际就是该类中 ” 成员变量 ” 之和,当然要注意内存对齐, 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
2.结构体内存对齐规则
下面我们再回顾学习一下结构体的内存对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS 中默认的对齐数为 8 3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。3.为什么要进行内存对齐?
这里查询资料,大部分的参考资料都是这样说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需做两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法
八、类成员函数的this指针
1.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 d1, d2; d1.Init(2022,1,11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0;}
对于上述类,有这样的一个问题: Date 类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当 d1 调用 Init 函 数时,该函数是如何知道应该设置 d1 对象,而不是设置 d2 对象呢? C++ 中通过引入 this 指针解决该问题,即: C++ 编译器给每个 “ 非静态的成员函数 “ 增加了一个隐藏 的指针参数,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有 “ 成员变量 ” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成 。 因为this是编译器自动完成的,不需要用户自己完成,所以 上面代码实际运行实现如下: class Date{public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } //void Print() void Print(Data* const this) { //cout << _year << "-" << _month << "-" << _day << endl; cout << this->_year << "-" << this->_month << "-" << this->_day << endl; }private: int _year; //年 int _month; //月 int _day; //日};int main(){ Date d1, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0;}
上面代码中this指针所在位置就是编译器自己进行处理的操作,因为是隐含的this指针,形参和实参的位置不能显示写,编译器会自行处理,但是this指针是可以在类里面显示着进行使用的。
我们需要知道的是函数体是进行对象区分是通过this指针自动将成员变量的地址传递了过去,当作函数的形参进行了使用。
2.this指针的特性
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。 2. 只能在 “ 成员函数 ” 的内部使用 3. this 指针本质上是 “ 成员函数 ” 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以 对象中不存储 this 指针 。 4. this 指针是 “ 成员函数 ” 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传 递,不需要用户传递
3.面试题练习
下面我们来看两道【面试题】
问:this 指针存在哪里? 答:一般情况下this指针作为形参会存放于栈区当中,有时根据编译器的不同会存放在寄存器ecx当中 根据下面代码选择ABC中一个选项:// 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、正常运行
题解:定义出的类指针变量赋值为空指针,之后访问Print成员函数,这里我们就要清楚程序是如何进行Print函数的访问的——通过this指针,而我们只是向Print函数传了一个空指针作为形参而已,并且我们并没有对空指针进行访问,所以不会出现报错,所以程序最终进入Print函数打印出了Printf()字符串。
根据下面代码选择ABC中一个选项:
// 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
题解:上面代码中PrintA函数里cout<<_a<<endl;在编译运行时真正的运行的代码为:cout<<this->_a<<endl;而这里的this指针为空指针,对空指针访问成员变量最后程序就会运行崩溃。
九、完结撒❀
如果以上内容对你有帮助不妨点赞支持一下,以后还会分享更多编程知识,我们一起进步。
最后我想讲的是,据说点赞的都能找到漂亮女朋友❤