C++类与对象(1)—初步认识

张开发
2026/5/20 21:48:06 15 分钟阅读
C++类与对象(1)—初步认识
目录一、面向过程和面向对象1. 面向过程编程POP2. 面向对象编程OOP二、类1. C 与 C 结构体语法对比2. 类的定义3. 类的两种定义方式3、访问限定符4、命名规范化5、类的实例化6、计算类对象的大小7、存储方式三、this指针1、定义2、存储位置3、辨析四、封装好处下一篇构造函数析构函数一、面向过程和面向对象C 语言是面向过程的编程范式核心关注解决问题的步骤与流程通过拆解问题逻辑、分步调用函数来完成任务C 是基于面向对象的编程范式核心关注事物本身对象将问题拆解为独立的对象通过对象之间的交互与协作完成功能。面向过程和面向对象是编程领域的两大核心范式二者的本质区别在于代码组织方式和数据处理逻辑的不同。1. 面向过程编程POP面向过程以函数 / 过程为核心数据与操作数据的函数相互分离。程序的编写逻辑是流程化的按照事物处理的先后顺序一步步编写执行步骤数据仅作为函数运行的辅助参数。举例制作一杯咖啡我们会按流程拆分出独立函数烧水函数、研磨咖啡豆函数、冲泡混合函数…… 程序严格按照「烧水→研磨→冲泡」的固定顺序执行聚焦于怎么做。2. 面向对象编程OOP面向对象以对象为核心将数据属性和处理数据的函数方法封装成一个整体。程序由一个个独立的对象构成每个对象拥有自己的特征属性和行为方法具备封装、继承、多态三大核心特性。举例制作一杯咖啡我们会抽象出一个「咖啡机 / 咖啡」对象它的属性水量、咖啡豆、温度它的方法烧水、研磨、冲泡。所有操作都通过调用对象自身的方法完成聚焦于谁来做。二、类C 对 C 语言的结构体进行了全面升级C 语言结构体中只能定义变量而C 结构体既可以定义成员变量也可以定义成员函数。C 的struct本质是一种默认权限为公有、兼容 C 语言的类。1. C 与 C 结构体语法对比C 完全兼容 C 语言的结构体用法同时简化了语法在C中把结构体升级成了类而且结构体的名字就是类型。//C兼容C结构体用法 // C 语言写法 typedef struct ListNode { int val; //C struct ListNode是类型 struct ListNode* next; }LN; // C 写法 struct ListNode { int val; //C ListNode是类型 ListNode* next; };2. 类的定义class className { // 类体由成员函数和成员变量组成 }; // 一定要注意后面的分号class为定义类的关键字C语言中用structclassName为类的名字{ }中为类的主体注意类定义结束时后面分号不能省略。类体中内容称为类的成员成员变量类中的变量描述对象的属性成员函数类中的函数描述对象的行为 / 方法。3. 类的两种定义方式在之前数据结构中学过的栈因为用C语言实现结构体中只能定义变量现在我们可以通过C的方式实现栈这样在结构体中也可以定义函数。第一种声明和定义全部放在类体中写法规则把成员变量 成员函数的实现代码全部写在class大括号内部。注意成员函数和成员变量定义在类中的位置没有要求在调用时会在整个类中查找不会像类之外使用变量或函数时编译器只会向上查找。成员函数如果在类中定义编译器可能会将其当成内联函数处理。这里出现了public和private他们是访问限定符稍后进行讲解。class Stack { public: // 成员函数 void Init(int n 4)//缺省参数 { a (int*)malloc(sizeof(int) * n); if (nullptr a) { perror(malloc fail); return; } capacity n; size 0; } void Push(int x) { //... a[size] x; } private: // 成员变量 int* a; int size; int capacity; }; int main() { Stack st; st.Init(); st.Push(1); st.Push(2); st.Push(3); return 0; }第二种类声明放在.h文件中成员函数定义放在.cpp文件中常用这种核心规则头文件.h只写类的声明成员函数只声明不写函数体成员变量正常定义必须加访问限定符源文件.cpp写成员函数的具体实现必须用类名::域作用限定符标记函数归属第一步头文件Stack.h只声明//struct Stack无需访问限定符 class Stack//必须有访问限定符否则报错 { public: // 成员函数 void Init(int capacity 4); void Push(int x); private: // 成员变量 int* a; int size; int capacity; };class默认权限是private必须写 public开放接口否则外部无法调用函数直接编译报错struct默认权限是public可以不写访问限定符。第二步源文件Stack.cpp写实现函数名前要加 “ 类名: :”(域作用限定符)。#include Stack.h void Stack::Init(int n) { a (int*)malloc(sizeof(int) * n); if (nullptr a) { perror(malloc申请空间失败); return; } capacity n; size 0; } void Stack::Push(int x) { //... a[size] x; }::域作用限定符作用告诉编译器这个函数不是全局函数而是Stack这个类的成员函数。没有Stack::编译器会认为这是一个普通全局函数无法访问类的私有成员。3、访问限定符在头文件中使用 stuct 定义类struct Stack { void Init(int capacity 4); void Push(int x); int* a; int size; int capacity; };成功编译在头文件中使用 class 定义类class Stack { void Init(int capacity 4); void Push(int x); int* a; int size; int capacity; };结果编译后报错如下我们可以发现成员函数无法访问类的成员这是因为未加访问限定符的class中默认访问权限为私有privatestuct 默认为公有public。C实现封装的方式用类将对象的属性与方法结合在一块让对象更加完善通过访问权限选择性的将其接口提供给外部的用户使用。访问权限符说明public修饰的成员在类外可以直接被访问protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符作用域就到 } 即类结束。class的默认访问权限为privatestruct为public(因为struct要兼容C)注意访问限定符只在编译时有用当数据映射到内存后没有任何访问限定符上的区别4、命名规范化我们来看这个代码class Date { public: void Init(int year, int month, int day) { year year; month month; day day; } private: int year; int month; int day; };在这个代码中存在一个问题。在Init函数中你试图将传入的参数赋值给类的成员变量但是由于参数和成员变量的名称相同会导致赋值操作无效。我们可以选择修改参数名或类成员名比较好的是在成员名前加上符号_ 这样小改动保证成员名和函数参数名之间的关系。class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } private: int _year; int _month; int _day; };5、类的实例化用类类型创建对象的过程称为类的实例化类是对对象进行描述的是一个模型一样的东西限定了类有哪些成员定义出一个类并没有分配实际的内存空间来存储它比如入学时填写的学生信息表表格就可以看成是一个类来描述具体学生信息。一个类可以实例化出多个对象实例化出的对象占用实际的物理空间、存储类成员变量。类中成员变量是声明没有分配空间。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 d1;//实例化 d1.Init(2023, 2, 3); return 0; }我们通过下面这段代码看看类实例化之后有没有占用实际的物理空间。class A2 { public: void f2() {} }; int main() { A2 aa1; A2 aa2; //实例化就能打印地址 cout aa1 endl; cout aa2 endl; return 0; }通过打印地址可以发现实例化后变量确实被分配空间用来存储类成员变量了。6、计算类对象的大小类的大小也遵循与结构体一样的计算方式详细请看这篇文章结构体内存对齐7、存储方式我们先来看下面这段代码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 d1; d1.Init(2023, 2, 2); //打印d1的大小 cout sizeof(d1) endl; return 0; }我们由输出结果可知 d1的大小为12字节由此可以推断出类的对象d1中只存储了成员变量。那么为什么成员变量在对象中成员函数不在对象中呢因为每个对象成员变量是不一样的需要独立存储每个对象调用成员函数是一样的它们被放到共享公共区域(代码段 。class A2 { public: void f2() {} }; int main() { A2 a; cout sizeof(a) endl; return 0; }类中只有成员函数这样定义的类对象a大小是1字节这1字节不存储有效数据是用来占位的标识对象被实例化定义出来了。三种类的大小对比// 类中既有成员变量又有成员函数 class A1 { public: void f1(){} private: int a; }; // 类中仅有成员函数 class A2 { public: void f2(){} }; // 类中什么都没有---空类 class A3 {};一个类的大小实际就是该类中”成员变量”之和当然要注意内存对齐注意空类的大小空类比较特殊编译器给了空类一个字节来唯一标识这个类的对象。三、this指针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 d1; Date d2; d1.Init(2022, 2, 2); d2.Init(2023, 2, 2); return 0; }对于上述类有这样的一个问题Date类中有 Init 成员函数函数体中没有关于不同对象的区分那当d1调用 Init 函数时该函数是如何知道应该设置d1对象而不是设置d2对象呢1、定义C中通过引入this指针解决该问题即C编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数让该指针指向当前对象(函数运行时调用该函数的对象)在函数体中所有“成员变量”的操作都是通过该指针去访问。只不过所有的操作对用户是透明的即用户不需要来传递编译器自动完成。如下图所示我们在初始化Init函数中打印this指针看看this指针有没有发挥作用。class Date { public: void Init(int year, int month, int day) { cout this endl; //可以显式使用this不能显式定义this this-_year year; this-_month month; this-_day day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; d1.Init(2022, 2, 2); d2.Init(2023, 2, 2); return 0; }输出了两个地址我们通过调试检查一下这两个地址是不是对象d1和d2的地址。在调试中地址如下上下两幅图对比可以得知每次初始化时this指针会指向参数的地址。在实际中类的成员函数一般不需要加this指针。2、存储位置this存在哪里栈因为他是隐含形参vs下在ecx寄存器中。通过之前计算类的对象大小不包括this所以this在栈上它是一个隐含的形参。3、辨析class Date { public: void Init(int year, int month, int day) { cout this endl; //可以显式使用this不能显式定义this this-_year year; this-_month month; this-_day day; } void func() { cout func() endl; } private: int _year; int _month; int _day; }; int main() { Date* ptr nullptr; ptr-func(); return 0; }程序正常运行如果调用这句呢ptr-Init(2022, 2, 2);结果运行崩溃下面来解释这两种情况通过指针调用成员函数ptr-func();这行代码实际上会被编译器转换成以下形式(*ptr).func();ptr-func()这行代码尝试通过空指针ptr调用成员函数func()。虽然ptr是空指针但是在这种情况下由于func()函数没有使用或修改任何成员变量它可以被静态调用。这意味着它不依赖于具体的对象实例不需要借助this指针因此不会引发崩溃。ptr-Init(2022, 2, 2)这行代码尝试通过空指针ptr调用成员函数Init()。由于Init()函数内部使用了this指针来访问对象的成员变量而空指针没有有效的对象实例对空指针的解引用无效因此在访问this-_year、this-_month和this-_day时会导致程序崩溃。同理这段代码也可以正常运行(*ptr).func();四、封装好处C中数据和方法都封装到类里面控制访问方式愿意给你访问公有不愿意给你访问私有。C语言中数据和方法是分离的数据访问控制是自由的不受限制的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 nullptr; _capacity 0; _size 0; } } private: void CheckCapacity() { if (_size _capacity) { int newcapacity _capacity * 2; DataType* temp (DataType*)realloc(_array, newcapacity * sizeof(DataType)); if (temp nullptr) { perror(realloc申请空间失败!!!); return; } _array temp; _capacity newcapacity; } } private: DataType* _array; int _capacity; int _size; }; ​C中通过类可以将数据 以及 操作数据的方法进行完美结合通过访问权限可以控制那些方法在类外可以被调用即封装在使用时就像使用自己的成员一样更符合人类对一件事物的认知。而且每个方法不需要传递Stack*的参数了编译器编译之后该参数会自动还原即C中 Stack * 参数是编译器维护的C语言中需用用户自己维护。下一篇构造函数析构函数C类与对象(2)—构造函数析构函数

更多文章