ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++类和对象(上):从面向对象基础到核心成员函数

C++类和对象(上):从面向对象基础到核心成员函数 在C中类和对象是面向对象编程OOP的基石。本文将从面向对象思想引入、类的定义与封装一直深入到构造函数、析构函数、拷贝构造以及运算符重载等核心机制。一、 面向过程 vs 面向对象C语言面向过程关注的是过程分析出求解问题的步骤通过函数调用逐步解决问题。例如洗衣服 - 拿盆子 - 放水 - 放衣服 - 放洗衣粉 - 揉搓 - 拧干 - 晾晒C面向对象关注的是对象将一件事拆分成不同的对象靠对象之间的交互完成。例如人、衣服、洗衣机、洗衣粉四个对象人只需将衣服放进洗衣机启动按钮即可不需关注洗衣机内部如何运转。二、 类的引入与定义C语言中结构体只能定义变量而在C中结构体struct和类class不仅可以定义变量还可以定义函数。1. 类的两种定义方式方式一声明和定义全部放在类体中这种方式比较直接适合代码量较少的情况。需要注意的是如果成员函数在类体内部定义编译器可能会将其视为内联函数inline来处理。// 全部写在类里面 class Person { public: // 成员函数在类内部定义 void showInfo() { cout _name - _sex - _age endl; } private: char* _name; // 姓名 char* _sex; // 性别 int _age; // 年龄 };方式二声明放在 .h 文件定义放在 .cpp 文件在实际工程开发中更推荐使用这种方式。这能让代码结构更清晰也利于分离编译。注意在.cpp文件中定义成员函数时函数名前必须加上类名::以指明该函数属于哪个类域。---------- person.h (头文件类声明) ----------class Person { public: // 成员函数声明 void showInfo(); private: char* _name; // 姓名 char* _sex; // 性别 int _age; // 年龄 };---------- person.cpp源文件类定义----------#include person.h // 成员函数定义必须加 Person:: void Person::showInfo() { cout _name - _sex - _age endl; }建议日常练习时为了方便可以使用方式一。但在实际工作项目中要使用方式二做到声明与实现分离。2.struct和class的区别在C中struct和class都可以用来定义类区别在于默认的访问权限struct的默认访问权限是public公有。class的默认访问权限是private私有。三、 类的访问限定符及封装1. 访问限定符限定符含义特点public公有类外可以直接访问暴露给用户的接口protected保护类外不能直接访问继承体系中会用到private私有类外不能直接访问隐藏内部实现细节2. 封装封装的核心目的隐藏实现细节只暴露操作接口。将数据和操作数据的方法进行有机结合通过访问权限来隐藏对象的属性和实现细节仅对外公开接口来和对象进行交互。就像电脑一样用户只需要知道如何开机、使用键盘鼠标而不需要关心CPU和显卡的内部工作原理。四、 类的作用域、实例化与对象大小1. 类的作用域类定义了一个新的作用域类的所有成员都在类的作用域中。在类体外定义成员时需要使用::作用域操作符指明成员属于哪个类域。2. 类的实例化类是设计图只定义有哪些东西不占用实际物理内存。对象是房子根据设计图建造出来的实体占用物理内存。注意不能直接访问类中的成员变量必须通过实例化后的对象来访问。3. 对象大小的计算核心规则对象中只存储成员变量不存储成员函数。成员函数存放在代码段属于同类所有对象共享。内存对齐规则同结构体第一个成员在与结构体偏移量为0的地址处。其他成员变量要对齐到某个数字对齐数的整数倍地址处。对齐数 编译器默认的一个对齐数与该成员大小的较小值VS中默认8Linux中默认4。结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍。如果嵌套了结构体嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。特殊情况空类class A {}; // 空类 int main() { cout sizeof(A) endl; // 输出 1 }为什么空类是1因为编译器需要一个独一无二的地址来标识这个类的不同对象所以会分配1个字节作为占位符。五、 类成员函数的 this 指针1. this 指针的本质this指针是隐藏的成员函数形参指向当前调用该函数的对象。在 x86 平台上this通常通过ecx寄存器传递在 x64 平台上则作为第一个参数通过rcx寄存器传递。在类中定义成员函数时编译器会自动加上this指针。例如void Print()实际上是void Print(Date* const this)。2. 经典面试题空指针调用成员函数class A { public: void Print() { cout Print() endl; } void PrintA() { cout _a endl; } private: int _a; }; int main() { A* p nullptr; p-Print(); // 正常运行因为Print()函数没有访问成员变量不需要解引用this p-PrintA(); // 崩溃因为PrintA()访问了_a本质是 this-_a对空指针解引用导致崩溃 return 0; }3. const 成员函数将const修饰的成员函数称之为const成员函数。const修饰类成员函数实际修饰的是该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进行修改。权限问题const对象只能调用const成员函数非const对象既可以调用const成员函数也可以调用非const成员函数。权限放大权限可以缩小非const对象调用const函数但不能放大const对象调用非const函数。六、 类和对象中六大默认成员函数如果一个类中什么成员都没有简称为空类。空类中什么都没有吗并不是的任何一个类在我们不写的情况下都会自动生成下面6个默认成员函数。1. 构造函数 (Constructor)主要任务初始化对象而不是开空间创建对象。特性说明函数名与类名相同返回值无返回值不需要写void调用时机对象实例化时由编译器自动调用重载可以重载默认生成如果类中没有显式定义构造函数编译器会自动生成一个无参的默认构造函数。一旦用户显式定义编译器将不再生成。C11 补丁针对编译器默认生成的构造函数对内置类型不做处理是随机值的问题C11允许在成员声明时给缺省值。class Date { private: int _year 1; // 声明时给缺省值 int _month 1; int _day 1; };2. 析构函数 (Destructor)主要任务完成对象中资源的清理工作而不是销毁对象本身对象销毁由编译器完成。特性说明函数名类名前加~参数与返回值无参数无返回值重载一个类只能有一个析构函数不能重载调用时机对象生命周期结束时C编译系统自动调用默认生成若未显式定义系统会自动生成默认的析构函数什么时候需要自己写析构函数有动态申请资源malloc/new需要显式释放资源。没有动态申请资源不需要写析构。需要释放资源的成员都是自定义类型不需要写析构会自动调用成员对象的析构。3. 拷贝构造函数 (Copy Constructor)主要任务用已存在的对象创建新对象。特性说明函数名与类名相同参数只有一个参数且必须是类类型对象的引用一般用const修饰。使用传值方式会引发无穷递归调用。调用场景使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象。浅拷贝 vs 深拷贝浅拷贝默认生成内置类型成员完成值拷贝自定义类型成员会调用他的拷贝构造。对于指针成员只拷贝指针地址会导致两个对象指向同一块内存二次析构互相影响。深拷贝必须自己实现对于Stack这种有动态资源的类必须自己实现拷贝构造重新分配内存并拷贝数据。// 深拷贝示例 Stack(const Stack st) { _a (int*)malloc(sizeof(int) * st._capacity); if (nullptr _a) return; memcpy(_a, st._a, sizeof(int) * st._top); _top st._top; _capacity st._capacity; }4. 赋值运算符重载 (operator)主要任务两个已经存在的对象之间进行复制拷贝。Date d4 d2;是拷贝构造初始化。d4 d1;是赋值运算符重载已经存在了。格式与要点Date operator(const Date d) { if (this ! d) { // 1. 检测是否自己给自己赋值 _year d._year; _month d._month; _day d._day; } return *this; // 2. 返回*this支持连续赋值 (d5 d4 d1) }参数const T传递引用提高传参效率。返回值T返回引用提高返回效率有返回值目的是为了支持连续赋值。注意赋值运算符只能重载成类的成员函数不能重载成全局函数。默认生成的赋值重载行为跟拷贝构造一样浅拷贝有动态资源的类需要自己实现深拷贝。5. 取地址及 const 取地址操作符重载这两个默认成员函数一般不用重新定义编译器默认会生成。Date* operator() { return this; } const Date* operator() const { return this; }七、 运算符重载进阶1. 运算符重载基本规则不能通过连接其他符号来创建新的操作符如operator。重载操作符必须有一个类类型参数。用于内置类型的运算符其含义不能改变。作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数的第一个参数为隐藏的this。不能重载的运算符5个.*、::、sizeof、?:、.。补充operator、operator()、operator[]、operator-只能重载成类的成员函数不能重载成全局函数。2. 前置 与 后置 的重载为了区分前置和后置后置重载时增加了一个int参数但这个参数并不接收具体的值仅仅是占位跟前置构成重载。// 前置 Date operator() { *this 1; return *this; } // 后置 Date operator(int) { Date tmp *this; *this 1; return tmp; }前置返回之后的对象引用。后置返回之前的对象值。3. 流插入与流提取的重载cout d1;会转换为operator(cout, d1);不能写成成员函数因为成员函数的第一个参数永远是this指针即d1.operator(cout)这不符合我们的使用习惯。必须写成全局函数并在类中声明为友元friend。class Date { // 友元函数声明 friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); }; // 全局定义 ostream operator(ostream out, const Date d) { out d._year 年 d._month 月 d._day 日; return out; // 支持连续输出 }八、 总结学习类和对象核心在于理解封装、内存分布、以及编译器默认生成的六大函数的行为。基础区分面向过程和面向对象掌握类的作用域、实例化与大小计算内存对齐。核心工具熟练使用this指针理解const成员函数的权限缩小原则。生命周期管理深刻理解构造函数初始化、析构函数清理、拷贝构造深/浅拷贝的调用时机和底层行为。运算符重载掌握赋值运算符、前后置、流插入提取的重载规则。
返回列表