=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.com)
=========================================================================
接上期:
【C++初阶】二、入门知识讲解
(引用、内联函数、auto关键字、基于范围的for循环、指针空值nullptr)-CSDN博客
=========================================================================
一 . 面向过程和面向对象初步认识
C语言 -- 面向过程
- C语言是面向过程的,关注的是过程,分析出求解问题的步骤,
通过函数调用来逐步解决问题
- 例子:
假设要完成洗衣服这个目标,通过C语言完成就需要分出多个过程,如:
拿盆子 => 放水 => 放衣服 => 放洗衣粉 => 手搓 => 换水 => 再放洗衣粉 =>
再手搓 => 拧干衣服 => 晾衣服
C++ -- 面向对象
- C++是面向对象的,关注的是对象,将一件事情拆分成不同的对象,
依靠对象之间的交互解决问题
- 例子:
同样是完成洗衣服这个目标,通过C++完成就需要找出需要的相应的对象,
洗衣服需要的对象有:人、衣服、洗衣粉、洗衣机,
然后就可以依靠对象的交互来完成整个洗衣服的过程:
人将衣服放进洗衣机 => 倒入洗衣粉 => 启动洗衣机 ,清洗衣服并甩干
可以看到整个过程是通过我们定义的四个对象之间的交互完成的,
人不需要关心洗衣机具体是如何操作的
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
二 . 类的引入 -- struct类
C语言struct结构体中只能定义变量,
在C++中,strcut结构体内不仅可以定义变量,还可以定义函数。例如:
之前在数据结构初阶中,用C语言方式实现的栈:
【数据结构初阶】五、线性表中的栈
(C语言 -- 顺序表实现栈)_高高的胖子的博客-CSDN博客这里实现的栈Stack结构体中只能定义变量;
而如果以C++方式实现栈的话,会发现Stack结构体中还可以定义函数
图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
三 . 类的定义 -- class类
class类的介绍:
- C++中,虽然可以使用struct来定义类,但还是更喜欢通过class来定义类
图示:
class className { //类体:由成员变量和成员函数组成 }; //和struct一样最后需要加上分号
- class为定义类的关键字,className为类的名字,
{ } 中为类的主体,注:类定义结束时最后的分号不能省略
- 类体中的内容称为类的成员:
类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数
- 定义类的成员变量时一般会在成员变量名前加上 “_” ,表示该变量为类内部的成员变量
图示:
class类的两种定义方式:
第一种:成员函数的声明和定义都在类体中
注:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
图示:
---------------------------------------------------------------------------------------------
第二种:类声明放在头文件中,成员函数实现放在 .cpp文件 中
- 注:
在 .cpp文件 中,需要通过类名和作用域限定符(::)指定实现类中的某个函数
- 类的作用域:
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
四 . 类的访问限定符和封装
访问限定符:
- C++实现封装的方式:
用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限(通过访问限定符管理权限)选择性的将其接口提供给外部的用户使用
访问限定符有三个:
- public(公有)、protected(保护)、private(私有)
- public(公有):被public修饰的成员在类外和类内都可以直接被访问
protected(保护)和 private(私有):
被修饰的成员在类外不能直接被访问,只能在类内被访问注:
在类内外访问这方面,这两个访问限定符是类似的,
在当前阶段,可以认为protected和private是没有区别的,
之后了解了继承才能够知道两者的区别
---------------------------------------------------------------------------------------------
访问权限作用域范围:
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止,
如果后面没有再出现过访问限定符,则作用域就到 “}” 为止,即类结束
- class的默认访问权限为private,struct的为public(因为struct要兼容C语言)
注:
访问限定符只在编译时有用,当数据映射到内存后,就没有任何访问限定符上的区别了
图示:
---------------------------------------------------------------------------------------------
C++中struct和class的区别:
C++需要兼容C语言,所以C++中struct除了可以当成类使用,还可以当成结构体使用。
struct定义的类和class定义的类的区别:struct定义的类默认的访问权限是public,class定义的类默认的访问权限是private
注:
在继承和模板参数列表位置,struct和class也有区别,后面再进行了解
封装:
- 面向对象有三大特性:封装、继承、多态。
在类和对象阶段,主要研究的是类的封装特性
- 封装:
将数据和操作数据的函数(方法)进行有机结合,隐藏对象的属性和实现细节,
仅对外公开接口来和对象进行交互
- 封装本质是一种管理,让用户能够更方便使用类,例如:
对于电脑这样一个复杂的设备,提供给用户的就只有一个开关机键,
通过键盘输入、显示器、USB插孔等,让用户和计算机进行交互,完成日常事务。
但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
- 对于计算机使用者而言,不用关心内部核心部件,
比如主板上线路是如何布局的、CPU内部是如何设计的等等。
用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。
因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,
仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可
- 要在C++语言中实现封装,
可以通过类将数据以及操作数据的函数(方法)进行有机结合,
通过访问权限来隐藏对象内部实现的细节,控制哪些函数可以在类外部直接被调用
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
五 . 类的实例化
- 用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,
仅定义出一个类时并没有分配实际的内存空间来存储它。
例如:
建造房子时需要的设计图,设计图就可以看成是一个类,
通过设计图可以设计建造出多个类似的房子,
这里多个类似的房子就可以看成是设计图类的多个对象,
设计图只设计出需要什么东西,但是并没有实体的建筑存在,
同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
- 一个类可以示例化出多个对象,实例化出的对象,占用实际的物理空间,
存储类成员变量,但没有存储类成员函数,
因为同一个类的多个对象调用的是相同的成员函数,所以成员函数会存放在公共代码区图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
六 . 类对象模型
计算类对象的大小:
类中既可以有成员变量,又可以有成员函数,
那么一个类的对象中包含了什么?又要如何计算一个类的大小呢?
类对象的存储方式:
- 一个类中可能有成员变量,或者成员函数,
但实际上类对象中只有类的成员变量,而没有成员函数,
因为成员函数可以被多个类对象共用,所以成员函数是存放在公共代码区中的,
而每个类对象的成员变量则是独立的
- 所以一个类对象的大小,实际就是该类中“成员变量”之和,
计算其大小时,需要用到和计算结构体大小同样的方法:内存对齐
- 一个类中如果没有成员变量,则称该类为空类,计算空类大小会比较特殊,
编译器给了空类对象一个字节来唯一标识这个空类的对象图示:
结构体内存对齐规则:
计算类(对象)大小需要了解关于内存对齐的知识,
关于内存对齐之前有博客详细介绍过:学C的第三十天【自定义类型:结构体、枚举、联合】_高高的胖子的博客-CSDN博客
内存对齐简单回顾:
- 第一个成员在与结构体偏移量为0的地址处
- 其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处,
对齐数 = 编译器默认的一个对齐数 与 该成员类型大小 两者中的较小值
(VS中默认的对齐数为8)
- 结构体总大小为:最大对齐数的整数倍
最大对齐数 = 所有变量类型中最大者 与 默认对齐参数 两者中的较小值
- 如果是嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,
结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
七 . this指针
this指针的引入:
先定义一个日期类Date:
//日期类Date: 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; Date d2; d1.Init(2023, 10, 7); d2.Init(2022, 10, 7); d1.Print(); d2.Print(); return 0; }
- 对于上述类,有这样一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,
那当 d1对象 调用 Init成员函数 时,
该函数是如何知道应该设置 d1对象,而不是设置 d2对象 呢?
C++中通过引入 this指针 来解决该问题,
即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的this指针参数,
让该指针指向当前对象(函数运行时调用该函数的对象),
在函数体中所有“成员变量”的操作,都是通过该指针去访问。
只不过所有的操作对用户是透明的,即用户不需要自己传递,编译器自动完成
this指针的特性:
- this指针的类型:类类型* const ,即成员函数中,不能给this指针赋值
- 只能在“成员函数”的内部使用
- this指针本质是“成员函数”的形参,当对象调用成员函数时,
将对象地址作为实参传递给this形参。所以对象中不存储this指针
- this指针是“成员函数”第一个隐藏的指针形参,
一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
- this指针是一个形参,是个局部变量,是存放在栈帧上面的
(VS编译器中,this指针被存放在ecx寄存器中)图示:
C语言和C++实现类的对比:
C语言实现类(型):
C语言实现一个类型(结构体)时,该类型相关操作函数有以下共性:
- 每个相关操作函数的第一个参数都是接收该类型(结构体)变量的指针,
来获取该类型的“底层结构”
- 函数中必须要对第一个参数(“底层结构”)进行检测,因为该参数可能会为NULL
- 函数中都是通过第一个参数(“底层结构”)来操作该类型(结构体)的
- 调用函数时必须传递该类型(结构体)变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,
即数据和操作数据的方式是分离开的,而且实现上相对会复杂一些,
涉及到大量指针操作,稍不注意可能就会出错
---------------------------------------------------------------------------------------------
C++实现类:
C++中通过类可以将 数据 以及 操作数据的函数(方法)进行完美结合,
通过访问权限还可以控制那些方法在类外是否可以被调用,即封装,
在使用时就像使用自己的成员一样,更符合人对一件事务的认知。
而且每个成员函数(成员方法)不需要传递像C语言中的第一个参数(“底层结构”),
编译器编译之后该参数会自动还原(通过隐藏的this指针),
即C++中的“底层结构”参数是由编译器维护的,而C语言中则需要用户自己维护
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本篇博客相关代码:
Stack.h -- 头文件:
#pragma once //类头文件: class Stack { private: //这是访问修饰符下一标题处会了解到 //成员变量: int* a; int top; int capacity; public: //同样是访问修饰符 //成员函数: void Init(); //栈初始化函数(方法)-- 声明 void Push(int x); //出栈函数(方法)-- 声明 /* * 成员函数可以分文件实现, * 也可以直接就在头文件中实现, * 但这样的直接在类中实现函数的话, * 该函数会被默认为是内联函数(inline), * (虽然是内联函数,但编译器确定是否展开) * * 所以正确的用法是: * 长函数的声明和定义要分离(分文件实现), * 短函数可以直接在类中就进行声明 */ bool Empty() { return top = 0; } };
Stack.cpp -- C++文件
#define _CRT_SECURE_NO_WARNINGS 1 //包含类头文件: #include "Stack.h" //类函数(方法)实现文件: /* * 通过命名空间(类名)和作用域限定符, * 来指定实现类中对应的函数(方法) * (类定义的也是一个域) */ //栈初始化函数(方法)-- 实现 void Stack::Init() //指定实现Stack类中的Init函数(方法) { a = 0; top = 0; capacity = 0; //a、top、capacity都是栈的成员变量 } //出栈函数(方法)-- 实现 void Stack::Push(int x) //指定实现Stack类中的Push函数(方法) { //…… }
Test.cpp -- C++文件
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <iostream> using namespace std; //使用C++实现一个栈: //C语言中实现栈: //struct Stack //{ // int* a; // int top; // int capacity; //}; // C语言中栈相应的函数: //void StackInit(struct Stack* ps); //void StackPush(struct Stack* ps, int x); //…… /* * 由此可见:C语言中栈的数据和方法是分离的 * 数据 -- struct Stack * 方法 -- StackInit、StackPush等等 * (C语言 -- 面向过程) * * C++中:兼容C语言struct的所有用法, * 不仅兼容,而且还将struct升级成了“类” */ //C++中实现栈: //struct Stack //{ // int* a; // int top; // int capacity; // // /* // * 2、类中可以定义与该类相关的函数(方法) // * // * (1)不需要像C语言中将数据和函数(方法)分离。 // * // * (2)因为函数(方法)被包含在类中, // * 所以不再需要像C语言中必须将函数定义在全局中, // * 所以函数名不需要定义得像 StackInit(堆的初始化函数)一样, // * 来特指是谁的初始化函数,直接在类中定义为 Init 即可, // * 该 Init函数 在Stack(堆)类的域中, // * 就是只属于Stack的Init函数 // */ // // void Init() // { // a = 0; // top = 0; // capacity = 0; // } // // void Push(int x) // { // //…… // } // //}; //int main() //{ // //C语言中调用struct“类型”: // struct Stack s1; // // //C++中调用struct“类”: // Stack s2; // /* // * 1、类名就是类型,Stack就是类型,不需要加struct // * struct后的名称就是类名,要调用类,直接调用类名即可, // * 不用加struct // */ // // //C++中: // //调用类中的变量或者函数的方法 // //和调用结构体成员的方法一样: // s2.Init(); //调用栈类中的Init函数 (C++) // // //调用栈类中的出栈Push函数(C++): // s2.Push(1); // s2.Push(2); // s2.Push(3); // s2.Push(4); // // //C语言中: // struct Stack s1; // //声明时还需加struct(不加typedef)的话 // // //调用的是全局中的函数: // StackInit(&s1); // StackPush(&s1, 1); // StackPush(&s1, 2); // StackPush(&s1, 3); // // return 0; //} //假设定义一个链表结点类: struct ListNode { ListNode* next; /* * 这里可以直接使用类名类型来定义变量了, * 而不是C语言中的:struct ListNode* next; */ int val; }; //class Date //{ //public: // void Init(int year, int month, int day) // { // _year = year; // _month = month; // _day = day; // } //private: // /* // * C++中一般会在成员变量前加一个 “_” , // * 表示该变量为类内部的成员变量, // * 防止在成员函数调用时和形参命名冲突 // */ // // int _year; // int _month; // int _day; // //}; //int main() //{ // Date d; // // d.Init(2023, 10, 17); // // return 0; //} /* * C++中,虽然可以使用struct定义类, * 但还是更喜欢通过class来定义类, * * class中由两部分构成: * 变量(成员变量)和函数(成员函数), * 两者统称类的成员 * * 类会通过访问限定符来实现封装, * 访问限定符分为: * public(共有)、protected(保护)、private(私有) * * public(共有):类中和类外都可以进行访问 * protected(保护):类中可以访问,类外不能访问 * private(私有):类中可以访问,类外不能访问 * * 在当前阶段,可以认为protected和private是没有区别的, * 等后面了解了继承才能够知道两者的区别 * * struct 和 class 的区别: * 1、class的默认访问权限为private,但实践中建议还是明确写上限定符 * struct默认访问权限为public(为了兼容C语言) * 2、(其它就没有什么大的区别) * * C++中设置访问限定符的目的: * C语言中没有访问限定符的概念,数据和方法是分离的, * 有时实现一个目标可以通过数据完成,也可以通过方法完成, * 程序员素养比较高的话应该是使用方法(函数)完成,这是比较规范的 * * 所以C++中类的成员默认是private私有的,无法在外部调用数据, * 在解决一个问题时就只能通过在类中定义方法(函数)来完成, * 提高代码的规范性 */ //class Stack //{ //private: // //私有:让以下三个成员变量的权限为私有 // int* a; // int top; // int capacity; // //public: // //共有:让以下的两个成员函数的权限为共有 // void Init() // { // a = 0; // top = 0; // capacity = 0; // } // // void Push(int x) // { // //…… // } // // bool Empty() // { // return top == 0; // } // ///* //* 一个访问限定符的作用范围为: //* 如果后面还有限定符 -- 当前限定符到下个限定符 //* 如果后面没有限定符 -- 当前限定符到 “}” //*/ //}; //int main() //{ // Stack s1; // // //权限为共有的成员可以在类外部进行调用: // s1.Init(); //Init函数为共有 // s1.Push(1); //Push函数为共有 // s1.Push(2); // s1.Push(3); // s1.Push(4); // // //权限为私有或保护的成员不可以在类外部进行调用: // s1.a = 0; //成员变量a为私有 //} //类的实例化: //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; // //这里这些成员变量只是声明,还没有开辟空间 // /* // * 变量是否定义(实现)要看是否有开辟空间 // * (在内存中开辟空间) // */ //}; /* * 类 和 对象 --> 一对多的关系 * 一个类可以有多个对象,可以想象类是设计图, * 对象是通过设计图建出来的房子,一个设计图 * 可以设计建出多个类似的房子 */ //class A //{ //private: // char _ch; // int _a; //}; //内存对齐问题 class B {}; //没有成员变量的类(空类)的大小:1 /* * 没有成员变量的类,说明该类不需要存储数据, * 虽然该类没有成员变量,但还是可以创建该类的对象, * 为了要表示一个空类的对象,证明空类B的对象存在, * 就需要为这个空类对象开一个字节大小的空间, * 这个字节不存储有效数据,仅标识定义的对象存在过 */ class C { public: void f() {}; }; //有成员函数没有成员变量的类的大小:1 /* * 该类还是没有成员变量,所以本质还是一个空类, * 虽然有成员函数,但成员函数并不存放在该类中, * 而是存储在公共代码区中,所以该类大小为1个字节 */ //int main() //{ // Date d1; //定义(实例化)一个对象 // Date d2; //再定义(实例化)一个对象 // Date d3; //再再定义(实例化)一个对象 // // /* // * 定义一个对象后,对象中的成员变量 // * 作为对象一部分,一起开辟了空间, // * 这时成员变量才被定义(实现)了 // */ // // d1.Init(2023, 10, 7); // d2.Init(2022, 10, 7); // /* // * (同个类)不同对象的成员函数是一样的: // * 这里d1和d2调用的是同一个函数 // *(汇编指令call调用的是同一函数地址) // */ // // //假设将成员变量的权限设置为public: // d1._year++; // d2._year++; // /* // * (同个类)不同对象的的成员变量是不一样的: // * 这里 d1的_year 和 d2的_year 不是同一个 // */ // // return 0; //} //int main() //{ // Date d1; //定义(实例化)一个对象 // Date d2; //再定义(实例化)一个对象 // Date d3; //再再定义(实例化)一个对象 // // //使用sizeof计算类的大小: // cout << sizeof(d1) << endl; //8个字节 // /* // * sizeof计算类对象的大小和 // * sizeof计算结构体大小是一样的, // * 所以要考虑内存对齐 // * // * sizeof计算类对象的大小时, // * 只会计算成员变量的大小,再考虑内存对齐, // * 不会考虑成员函数的大小 // * // * 成员函数不在对象里面,因为同一个类的不同对象 // * 调用的是同一个成员函数(同名函数的情况下), // *(汇编指令call调用的是同一函数地址) // * 但同一个类的不同对象各自的成员变量是独立的, // * 类对象d1中的成员变量 和 类对象d2中的成员变量 // * 是不同的,所以sizeof计算类对象时计算的是其成员变量 // * // * 成员变量存在对象中,而成员函数不存在对象中的原因: // * 成员变量各个对象不同(独立), // * 但成员函数调用的都是同一个, // * 所以没必要在所有对象中都存成员函数的地址, // * 将其放在一个公共的区域(公共代码区)是更适合的, // * 公共代码区在编译完成后是一堆指令, // * 编译链接时就可以确定函数的地址 // */ // cout << sizeof(A) << endl; //A类大小:8个字节 // cout << sizeof(B) << endl; //B类大小:1个字节 // cout << sizeof(C) << endl; //C类大小:1个字节 // // return 0; //} class Date { public: //Date类初始化函数: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } //Data类打印日期函数: void Print() { cout << _year << "_" << _month << "_" << _day << endl; } /* * d1 和 d2 都调用该成员函数,但结果却不同的原因: * * 我们写的 void Print()函数,编译器编译时实际上是这样的: * void Print(Date* const this),有一个隐藏的参数--this指针 * (所有我们定义的成员函数都默认有一个this指针参数) * * d1调用函数时,访问的是: &d1->_year、&d1->month、&d1->_day * 此时 this指针 就是 d1的指针 ,&d1是实参,Date* this是形参 * 访问的是d1对象的_year、_month、_day * * d2调用函数时,访问的是: &d2->_year、&d2->month、&d2->_day * 此时 this指针 就是 d2的指针 ,&d2是实参,Date* this是形参 * 访问的是d2对象的_year、_month、_day * * this指针是形参,是个局部变量,是存放在栈帧上面的 * (VS编译器中,this指针被存放在ecx寄存器中) */ //编译器编译时的Print函数: void Print(Date* const this) { cout << this->_year << "_" << this->_month << "_" << this->_day << endl; /* * 注:不能显式写出this相关实参和形参,但可以在成员函数中显式写上this指针 *(之后有地方会需要显式地在成员函数中写出this指针) */ } private: int _year; int _month; int _day; }; class A { private: char _ch; int _a; }; int main() { Date d1; //定义(实例化)一个对象 Date d2; //再定义(实例化)一个对象 d1.Init(2023, 10, 7); d2.Init(2022, 10, 7); d1.Print(); d2.Print(); //编译器编译时的426和427代码: d1.Print(&d1); //传送“this”指针 d2.Print(&d2); //传送“this”指针 return 0; } //对象用 “.” 访问成员;指针用 “->” 访问成员 /* * 1、 * C语言数据和函数(方法)是分离的, * 而C++通过类将数据(成员变量)和成员函数(成员方法)绑定在一起 * * 2、 * C语言调用函数时,需要将类型变量传给函数, * 而C++因为有this指针的存在,不需要传类对象给成员函数 */