多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C++面向对象编程:从类与对象到三大特性深度解析

C++面向对象编程:从类与对象到三大特性深度解析 1. 从“过程”到“对象”为什么C的类和对象是编程思维的分水岭如果你是从C语言或者更基础的编程语言转过来的第一次接触C的“类”和“对象”时可能会有点懵。C语言里我们处理数据用的是结构体struct处理逻辑用的是函数数据和函数是分开的。这种“面向过程”的编程方式在处理简单任务时很直观但项目规模一大数据和函数之间的关系就变得错综复杂维护起来像在走迷宫。C的“类”Class和“对象”Object就是为了解决这个问题而生的它们构成了“面向对象编程”OOP的基石。简单来说类是一张设计蓝图而对象是根据这张蓝图建造出来的、实实在在的房子。蓝图定义了房子的结构有哪些房间、门窗和功能水电如何布局但你不能住进蓝图里只有根据蓝图建好的房子对象你才能进去生活、使用它的功能。这种思维转变是革命性的。它让你把相关的数据和操作这些数据的函数“打包”在一起形成一个独立的、自包含的“黑盒子”。外界只需要知道这个盒子能做什么通过公共接口而不需要关心它内部是怎么实现的。这极大地提高了代码的复用性、可维护性和可扩展性。无论是开发像Qt这样的图形界面库还是处理像OpenCV中的图像CImage类或其相关类亦或是构建一个游戏里的角色系统类和对象都是你组织代码的核心武器。接下来我们就抛开晦涩的理论直接深入细节看看这张“蓝图”到底怎么画造出来的“房子”又该怎么用。2. 类的深度解剖不只是带函数的struct很多初学者会把C的类简单地理解为“能放函数的struct”。这虽然形象但远远没有触及类的核心。类的设计蕴含了封装、数据隐藏和接口控制的思想。2.1 访问控制类的“门禁系统”这是类与结构体最本质的区别之一。C通过三个关键字来设置访问权限它们决定了类成员的“可见性”public公有这是类对外的“大门”。所有在public区域声明的成员变量和函数在类的外部可以被任意访问。这里通常放置类的“接口函数”即提供给外界使用的功能。private私有这是类的“私人卧室”。private成员只能在类的内部被其成员函数访问外部代码无法直接触碰。这是实现“封装”的关键将数据的实现细节隐藏起来防止被意外修改。protected受保护类似于private但增加了一条特权在类的继承体系中派生类子类可以访问基类的protected成员。这为“继承”特性提供了数据共享的通道。一个典型的类声明结构如下class MyClass { private: int secretData; // 私有数据外部不可见 void helperFunction(); // 私有函数内部辅助用 protected: int familyData; // 受保护数据子类可见 public: // 构造函数 MyClass(); // 析构函数 ~MyClass(); // 公有成员函数对外接口 void setData(int value); int getData() const; };注意在C中struct的默认访问权限是public而class的默认访问权限是private。这是两者语法上最微小的区别但体现了不同的设计意图struct倾向于作为一个纯粹的数据聚合体而class则强调对行为的封装。2.2 构造函数与析构函数对象的“生”与“死”对象从创建到销毁有两个特殊的成员函数全程参与构造函数和析构函数。构造函数在对象创建时自动调用用于初始化对象的状态。它的名字与类名完全相同且没有返回类型。class Rectangle { private: double width; double height; public: // 1. 默认构造函数 Rectangle() : width(1.0), height(1.0) { std::cout 默认矩形被创建 std::endl; } // 2. 带参数的构造函数 Rectangle(double w, double h) : width(w), height(h) { std::cout 矩形( w , h )被创建 std::endl; } // 3. 拷贝构造函数参数为同类对象的常量引用 Rectangle(const Rectangle other) : width(other.width), height(other.height) { std::cout 通过拷贝创建了一个矩形 std::endl; } };构造函数的初始化列表:后面的部分是推荐的初始化方式它先于构造函数体执行直接对成员进行初始化效率高于在函数体内赋值。析构函数在对象生命周期结束如离开作用域、被delete时自动调用用于清理资源如释放动态内存、关闭文件等。它的名字是类名前加~无参数无返回。class FileHandler { private: FILE* filePtr; public: FileHandler(const char* filename) { filePtr fopen(filename, r); if (!filePtr) { /* 错误处理 */ } } ~FileHandler() { if (filePtr) { fclose(filePtr); // 确保文件被关闭 std::cout 文件资源已释放 std::endl; } } };实操心得对于管理资源的类如动态内存、文件句柄、网络连接析构函数是防止资源泄漏的生命线。这就是著名的“RAII”资源获取即初始化理念在构造函数中获取资源在析构函数中释放利用对象的生命周期自动管理资源异常安全。2.3this指针对象自我的指认在类的非静态成员函数内部编译器会自动提供一个名为this的指针它指向调用该成员函数的当前对象。这在以下几种情况非常有用解决命名冲突当成员函数参数名与成员变量名相同时。void setWidth(double width) { this-width width; // 用this明确指代成员变量 }返回对象自身用于支持链式调用。MyClass increase(int val) { this-data val; return *this; // 返回当前对象的引用 } // 可以这样调用obj.increase(5).increase(10);在成员函数中传递当前对象例如将对象自身作为参数传递给其他函数。this是一个常量指针对于MyClass类型的对象其类型是MyClass* const意味着this本身的值即指向的地址不能修改。3. 对象的创建、使用与内存视角理解了蓝图类接下来就是盖房子创建对象和住房子使用对象。3.1 对象的创建方式在C中根据对象的存储期和创建方式主要有以下几种栈上对象自动存储期最常见的方式。对象在作用域内创建离开作用域时自动调用析构函数销毁。{ Rectangle rect(5.0, 3.0); // 在栈上创建调用带参构造函数 rect.getArea(); // 使用对象 } // 离开作用域rect自动销毁调用析构函数这种方式简单、安全、高效是首选。堆上对象动态存储期使用new运算符在堆内存中创建必须使用delete手动释放。Rectangle* pRect new Rectangle(10.0, 20.0); // 在堆上创建 pRect-getArea(); // 使用箭头运算符访问成员 delete pRect; // 必须手动释放否则内存泄漏 pRect nullptr; // 良好习惯释放后置空注意事项务必成对使用new和delete。现代C更推荐使用智能指针如std::unique_ptr,std::shared_ptr来管理堆对象几乎可以完全避免手动delete和内存泄漏问题。静态对象和全局对象在全局或函数内部用static声明生命周期贯穿整个程序运行期。3.2 对象的内存模型初探一个对象在内存中占据一块连续的区域用于存储其所有的非静态成员变量。成员函数包括构造函数、析构函数的代码并不存储在每一个对象中而是存储在代码区所有同类对象共享同一份函数代码。这就是为什么成员函数可以通过this指针知道当前操作的是哪个对象。考虑这个类class Student { private: int id; // 通常4字节 char name[20]; // 20字节 double score; // 8字节 public: void printInfo(); };一个Student对象在内存中的大小粗略地是其所有非静态成员变量的大小之和实际还要考虑内存对齐可能会更大。而printInfo()函数无论创建多少个Student对象在内存中都只有一份。3.3 对象数组与const对象对象数组的创建和普通数组类似但每个元素都会调用构造函数。如果没有提供合适的构造函数创建对象数组可能会出错。Rectangle rectArray[3]; // 调用3次默认构造函数 Rectangle rectArray2[2] {Rectangle(1,2), Rectangle(3,4)}; // 调用带参构造函数const对象是指被声明为常量的对象其数据成员在对象生命周期内不可被修改。const对象只能调用那些被声明为const的成员函数保证不修改对象状态。const Rectangle constRect(5, 5); // constRect.setWidth(10); // 错误const对象不能调用非const成员函数 double area constRect.getArea(); // 正确getArea()必须是const成员函数在成员函数声明末尾加上const关键字就表示这是一个“常量成员函数”它承诺不会修改对象的任何非静态成员变量除非成员被mutable修饰。4. 类的三大特性深入封装、继承与多态面向对象编程有三大核心特性它们是构建复杂系统的支柱。4.1 封装把复杂藏起来封装不仅仅是private。它的核心思想是将数据属性和操作数据的方法函数捆绑在一起并对数据的访问进行控制只暴露必要的接口。好的封装就像一台自动售货机你只需要按按钮调用接口选择商品和投币就能得到商品你不需要知道内部的弹簧、电机和识别电路是如何工作的。实现封装的关键步骤将所有数据成员设置为private这是基本原则防止外部代码随意修改。提供公有的“访问器”和“修改器”即getter和setter函数。在setter中可以进行有效性检查。class BankAccount { private: double balance; public: double getBalance() const { return balance; } // getter bool deposit(double amount) { // 存款带检查 if (amount 0) { balance amount; return true; } return false; } bool withdraw(double amount) { // 取款带检查 if (amount 0 amount balance) { balance - amount; return true; } return false; } };将内部实现的辅助函数设置为private。4.2 继承站在巨人的肩膀上继承允许我们基于一个已有的类基类/父类来定义一个新的类派生类/子类。派生类继承了基类的所有成员除构造函数、析构函数和private成员并可以添加自己的新成员或重写基类的方法。// 基类形状 class Shape { protected: int x, y; // 受保护子类可访问 public: Shape(int x, int y) : x(x), y(y) {} virtual void draw() const { // 虚函数为多态做准备 std::cout 绘制一个形状于( x , y ) std::endl; } }; // 派生类圆形 class Circle : public Shape { // 公有继承 private: int radius; public: Circle(int x, int y, int r) : Shape(x, y), radius(r) {} // 重写基类的虚函数 void draw() const override { std::cout 绘制一个半径为 radius 的圆于( x , y ) std::endl; } double getArea() const { return 3.14159 * radius * radius; } };继承方式public,protected,private决定了基类成员在派生类中的访问权限。最常用的是public继承表示“是一个”的关系Circle是一个Shape。构造函数调用顺序创建派生类对象时先调用基类构造函数再调用派生类构造函数。析构顺序则相反。4.3 多态同一接口多种实现多态是继承的终极应用。它允许我们通过基类的指针或引用来操作派生类对象并根据对象的实际类型来调用相应的方法。这需要两个关键技术虚函数在基类中用virtual声明的成员函数。动态绑定在运行时而非编译时决定调用哪个函数。Shape* shapePtr; // 基类指针 Shape shape1(0, 0); Circle circle1(10, 10, 5); shapePtr shape1; shapePtr-draw(); // 输出绘制一个形状于(0, 0) shapePtr circle1; // 基类指针指向派生类对象 shapePtr-draw(); // 输出绘制一个半径为5的圆于(10, 10) —— 多态发生如果没有virtual关键字shapePtr-draw()将永远调用Shape::draw()这就是“静态绑定”。有了virtual就实现了“动态绑定”或“晚绑定”即多态。纯虚函数与抽象类如果基类中的虚函数没有实际意义可以将其声明为纯虚函数virtual void draw() const 0;。包含纯虚函数的类称为抽象类它不能实例化对象只能作为接口被继承。派生类必须重写所有纯虚函数否则它也是抽象类。5. 进阶特性与性能考量掌握了基础我们再看一些让类设计更强大、更高效的特性。5.1 静态成员属于类本身的成员用static关键字修饰的成员属于类本身而不是任何一个对象。所有对象共享同一份静态成员。静态成员变量必须在类外单独定义和初始化分配内存。class Employee { private: static int count; // 声明记录创建的员工总数 int id; public: Employee() { id count; } static int getCount() { return count; } // 静态成员函数 }; int Employee::count 0; // 在类外定义并初始化静态成员函数只能访问静态成员变量和其他静态成员函数不能访问非静态成员因为没有this指针。调用时使用类名加作用域解析运算符Employee::getCount()。5.2 友元打破封装的特权友元机制允许一个外部函数或另一个类访问当前类的private和protected成员。这打破了封装应谨慎使用。class Box { private: double width; public: Box(double w) : width(w) {} // 声明友元函数 friend void printWidth(const Box box); }; // 友元函数定义可以访问Box的私有成员 void printWidth(const Box box) { std::cout Box width: box.width std::endl; // 直接访问private成员 }友元可以是函数友元函数也可以是类友元类。友元关系是单向的且不能传递。5.3 拷贝控制管理对象的复制行为当对象被拷贝初始化、传参、返回值时编译器会自动生成拷贝构造函数和拷贝赋值运算符。但涉及动态内存管理时浅拷贝会导致问题两个对象指向同一块内存析构时重复释放。这时需要自定义深拷贝。class String { private: char* data; int length; public: // 深拷贝构造函数 String(const String other) { length other.length; data new char[length 1]; strcpy(data, other.data); } // 深拷贝赋值运算符 String operator(const String other) { if (this ! other) { // 防止自赋值 delete[] data; // 释放原有资源 length other.length; data new char[length 1]; strcpy(data, other.data); } return *this; } ~String() { delete[] data; } };移动语义C11为了优化临时对象拷贝带来的性能开销C11引入了右值引用和移动构造函数/移动赋值运算符可以将资源如动态内存“窃取”过来避免不必要的深拷贝。这是现代C性能优化的重要部分。5.4 运算符重载让对象支持自然运算运算符重载允许我们为自定义类型类定义运算符的行为使代码更直观。class Complex { private: double real, imag; public: Complex(double r, double i) : real(r), imag(i) {} // 重载加法运算符成员函数形式 Complex operator(const Complex other) const { return Complex(real other.real, imag other.imag); } // 重载输出运算符通常声明为友元函数 friend std::ostream operator(std::ostream os, const Complex c); }; std::ostream operator(std::ostream os, const Complex c) { os c.real c.imag i; return os; } // 使用 Complex c1(1, 2), c2(3, 4); Complex c3 c1 c2; // 调用重载的 std::cout c3 std::endl; // 调用重载的6. 实战避坑指南与性能调优理论最终要服务于实践。下面是一些从实际项目中总结出来的经验和常见陷阱。6.1 构造函数与析构函数常见问题未初始化的成员变量内置类型int,double, 指针等的成员变量在构造函数中不会自动初始化其值是未定义的。务必在构造函数初始化列表或函数体中初始化所有成员变量。析构函数非虚如果一个类可能被继承作为基类并且会通过基类指针来删除派生类对象那么基类的析构函数必须声明为虚函数virtual ~Base() {}。否则通过基类指针删除派生类对象时只会调用基类的析构函数导致派生类部分的资源泄漏。拷贝构造与赋值运算符的自我赋值在实现拷贝赋值运算符operator时第一件事应该是检查是否是自我赋值if (this ! other)。否则在释放自身资源时可能会同时销毁了要拷贝的数据源。6.2 对象切片问题当派生类对象被赋值给基类对象而不是指针或引用时会发生“对象切片”派生类特有的部分会被“切掉”只保留基类的部分。Circle circle(0,0,10); Shape shape circle; // 对象切片shape只是一个Shape没有radius shape.draw(); // 调用的是Shape::draw()不是Circle::draw()解决方法多态必须通过基类的指针或引用来实现。应使用Shape shapeRef circle;或Shape* shapePtr circle;。6.3 内存管理与智能指针手动管理堆内存new/delete是C bug的主要来源之一内存泄漏、悬空指针、重复释放。在现代C中应优先使用智能指针。std::unique_ptr独占所有权的智能指针。对象只能被一个unique_ptr指向当其销毁时管理的对象也自动销毁。适用于明确的单一所有权场景。#include memory std::unique_ptrRectangle uPtr std::make_uniqueRectangle(5, 3); // 不需要手动deletestd::shared_ptr共享所有权的智能指针。通过引用计数管理内存当最后一个shared_ptr销毁时对象才被释放。适用于多个对象需要共享同一资源时。auto sPtr1 std::make_sharedRectangle(10, 20); auto sPtr2 sPtr1; // 引用计数1std::weak_ptr弱引用指针指向由shared_ptr管理的对象但不增加引用计数。用于打破shared_ptr的循环引用。6.4 内联函数与性能在类定义内部直接实现的成员函数默认是内联的编译器不一定真的内联但有这个建议。内联函数可以消除函数调用的开销适用于短小、频繁调用的函数如getter/setter。class Point { private: int x, y; public: int getX() const { return x; } // 隐式内联 void setX(int val) { x val; } // 隐式内联 };对于复杂的函数不建议内联因为会导致代码膨胀反而可能降低缓存命中率影响性能。6.5 接口设计与const正确性良好的类设计接口应该简洁、明确、不易误用。大量使用const来修饰不修改对象状态的成员函数是一个好习惯。这被称为const正确性。对于不修改成员变量的getter函数务必加上const。对于需要修改成员变量的函数不要加const。参数传递时如果函数不需要修改实参应使用const引用const T传递避免不必要的拷贝。7. 从理论到项目一个综合案例设计让我们设计一个简单的图书馆管理系统中的一个核心类——Book来串联多个概念。#include string #include iostream class Book { private: std::string isbn; // 国际标准书号唯一标识 std::string title; std::string author; int totalCopies; // 总馆藏量 int availableCopies; // 可借阅量 // 静态成员统计所有图书数量 static int bookCount; public: // 构造函数 Book(const std::string i, const std::string t, const std::string a, int copies) : isbn(i), title(t), author(a), totalCopies(copies), availableCopies(copies) { bookCount; std::cout 图书《 title 》已入库当前馆藏总数: bookCount std::endl; } // 拷贝构造函数深拷贝示例虽然这里string会自动处理 Book(const Book other) : isbn(other.isbn), title(other.title), author(other.author), totalCopies(other.totalCopies), availableCopies(other.availableCopies) { bookCount; std::cout 图书《 title 》副本已创建。 std::endl; } // 析构函数 ~Book() { --bookCount; std::cout 图书《 title 》记录已移除剩余馆藏总数: bookCount std::endl; } // 接口函数 bool borrowBook() { if (availableCopies 0) { --availableCopies; std::cout 借阅成功。《 title 》剩余可借: availableCopies std::endl; return true; } else { std::cout 借阅失败。《 title 》已无库存。 std::endl; return false; } } bool returnBook() { if (availableCopies totalCopies) { availableCopies; std::cout 归还成功。《 title 》剩余可借: availableCopies std::endl; return true; } else { std::cout 归还错误数量异常。 std::endl; return false; } } // const成员函数不修改对象状态 void displayInfo() const { std::cout ISBN: isbn \n 书名: 《 title 》\n 作者: author \n 馆藏/可借: totalCopies / availableCopies std::endl; } // 静态成员函数 static int getTotalBookCount() { return bookCount; } // 重载相等运算符判断是否为同一本书基于ISBN bool operator(const Book other) const { return isbn other.isbn; } // 友元函数重载输出运算符 friend std::ostream operator(std::ostream os, const Book book); }; // 静态成员变量定义和初始化 int Book::bookCount 0; // 友元函数定义 std::ostream operator(std::ostream os, const Book book) { os 《 book.title 》 - book.author [ book.availableCopies / book.totalCopies 可借]; return os; } // 使用示例 int main() { Book book1(978-7-121-12345-6, C Primer, Stanley B. Lippman, 5); Book book2(978-7-111-56789-0, Effective C, Scott Meyers, 3); book1.displayInfo(); book1.borrowBook(); book1.borrowBook(); book1.returnBook(); std::cout book1 std::endl; // 使用重载的运算符 if (book1 book2) { // 使用重载的运算符 std::cout 这是同一本书。 std::endl; } else { std::cout 这是不同的书。 std::endl; } std::cout 图书馆当前总藏书量: Book::getTotalBookCount() std::endl; return 0; }这个Book类综合运用了封装数据私有提供公有接口。构造函数/析构函数管理对象生命周期和静态计数。const成员函数displayInfo。静态成员变量和函数bookCount和getTotalBookCount。运算符重载和。友元operator。简单的业务逻辑借书和还书。通过这样一个完整的例子你可以看到类的各个部分是如何协同工作来清晰地建模一个现实世界中的实体并定义其行为的。理解并熟练运用这些概念是写出高质量、易维护的C面向对象代码的关键。
返回列表