C++从遗忘到入门(中):https://developer.aliyun.com/article/1480760
▐ 类(class)
C++通过引入类支持了面向对象编程(OOP)。在 C++ 中,类是创建自定义数据类型的核心概念之一。类用于定义与特定类型相关的数据(成员变量)及操作这些数据的函数(成员函数)。通过类,可以实现面向对象编程(OOP)的基本原则,如封装、继承和多态。
关于C++类的知识非常多且复杂,这里介绍常用和重要的部分。
- 定义类
类是通过关键字 class 定义的,后跟类名和类体:
class MyClass { public: // 公共成员,通常的对外提供的方法定义 void setMember(int member); private: // 私有成员,成员变量,仅供内部调用函数 int mMember; // 集团规范推荐,使用m前缀 void innerFunc(); // 函数一律小驼峰 protected: // 受保护成员,成员变量,供子类调用函数 };
下面是类的实际定义:
// person.hpp // C++一般使用 hpp 后缀的头文件,表明包含C++特性的代码(模板、引用、类等) // .h .hpp只是约定的做法,不是语法上的必要性 // 类的定义一般放到头文件中,用来对外声明类 // 类的头文件规范:类名小写 + '_'分割(如果有多个单词的case) // 如果类名:PersonInfo 对应头文件:person_info.hpp #ifndef PERSON_H #define PERSON_H #include <string> #include <iostream> // 声明 'Person' 类 class Person { public: // 构造函数声明 Person(const std::string & name, int age); // 成员函数声明 void printInfo() const; // const成员函数,保证函数不会修改调用对象 // Setters 和 Getters 声明 void setName(const std::string & name); const std::string & getName() const; void setAge(int age); int getAge() const; private: // 成员变量 std::string mName; int mAge; }; #endif // PERSON_H // person.cpp // 实现代码放到同名cpp文件中 #include "person.hpp" // 构造函数定义 Person::Person(const std::string & name, int age) : mName(name), mAge(age) {} // 成员函数定义 void Person::printInfo() const { std::cout << "Name: " << mName << ", Age: " << mAge << std::endl; } // Setters 和 Getters 定义 void Person::setName(const std::string & name) { mName = name; } const std::string & Person::getName() const { return mName; } void Person::setAge(int age) { mAge = age; } int Person::getAge() const { return mAge; }
拓展:
- const成员函数
访问控制
类成员的访问权限可以是 public、private 或 protected:
- Public(公共):公共成员可以在类的外部被访问。
- Private(私有):私有成员只能在类的内部被访问。
- Protected(受保护):受保护成员可以在类的内部以及其派生类中被访问。
构造函数
构造函数是一种特殊的成员函数,它在创建类实例时自动调用。
构造函数可以被重载,以提供不同的初始化方式。成员初始化列表提供了初始化成员变量的一种更高效的方式,对于类中的常量成员、引用成员来说,成员初始化列表是必须的:
class MyClass { public: MyClass(int m1, int m2, int m3) : mM1(m1), mM2(m2), mM3(m3) {} private: int mM1; const int mM2; int & mM3; }; // 类的初始化方式 MyClass a1(1, 2, 3); // 传统构造函数 MyClass a1 = MyClass(1, 2, 3); // 同上 MyClass a2 = {1, 2, 3}; // 列表初始化,会匹配最合适的构造函数 MyClass a3{1, 2, 3}; // 同上
拓展:
- explicit 关键字
析构函数
析构函数是类的一个特殊成员函数,它在类的对象生命周期结束时自动被调用以执行清理工作。主要用途是释放对象占用的资源,并执行一些必要的清理操作,例如释放动态分配的内存、关闭文件和数据库连接等。示例:
class MyClass { public: MyClass() { // 构造函数分配资源或执行初始化 data = new int[10]; // 假设动态分配了内存 } ~MyClass() { // 析构函数释放资源 delete[] data; // 释放动态分配的内存 } private: int* data; // 指向动态分配的内存 };
自动调用析构函数的情况:
1.局部对象:当局部对象的作用域结束时,例如函数结束时,其中的局部对象会被销毁,调用析构函数。
2.动态分配的对象:当使用 delete 操作符删除一个动态分配的对象时,析构函数会被调用。
3.静态和全局对象:当程序结束时,所有的静态和全局对象会被销毁,调用析构函数。
4.临时对象:当临时对象的生命周期结束时,例如临时对象作为函数参数传递,或者在它们创建的表达式结束后,析构函数会被调用。
5.通过 std::unique_ptr 或 std::shared_ptr 管理的对象:当智能指针销毁或被重新赋值,造成引用计数降为零时,析构函数会被调用。
在 C++ 中,通常应用“资源获取即初始化”(RAII)原则来管理资源。RAII 建议在构造函数中获取资源,并在析构函数中释放资源。这样,资源的生命周期就与包含它的对象的生命周期绑定在一起,简化了资源管理并防止了资源泄漏。
当正确使用 RAII 原则时,通常不需要手动调用析构函数,因为 C++ 会确保在对象生命周期结束时自动调用析构函数。然而,如果你使用“裸”指针手动管理资源,就必须非常小心地确保每个分配的资源最终都被释放,否则可能会导致资源泄漏。智能指针(如 std::unique_ptr 和 std::shared_ptr)是现代 C++ 推荐的资源管理方式,它们可以自动管理资源的生命周期,从而避免直接手动管理资源的复杂性和危险。
运算符重载
类可以重载各种运算符,以提供类似于内建类型的行为:
class MyClass { public: MyClass() : data(new int[10]) { } // 构造函数 ~MyClass() { delete[] data; } // 析构函数 // 拷贝赋值运算符 MyClass & operator=(const MyClass& other) { if (this != &other) { // 避免自赋值 std::copy(other.data, other.data + 10, data); } return *this; } private: int* data; }; // 使用 MyClass a; MyClass b = a; // 默认的赋值操作是浅拷贝,这里因为重载了 = 运算符,变成深拷贝 // C++11开始可以删除默认的赋值操作符,从而防止因浅拷贝带来的风险 class MyClass2 { // ... MyClass2 & operator=(const MyClass2 & other) = delete; // 禁用赋值操作符 // ... }; MyClass2 a; MyClass2 b = a; // 非法,MyClass2的 = 运算符被禁用
一些注意事项:
- 运算符重载并不改变运算符的优先级、结合性或操作数个数。这些都是由语言规范定义的。
- 不要滥用运算符重载。重载的运算符应该和它的原始意图保持相关性,否则可能导致代码难以阅读和理解。
- 记得检查自赋值。特别是在重载赋值运算符时(如 operator=),要确保它能正确处理自赋值的情况。
- 为了保持一致性,考虑重载对应的复合赋值运算符。例如,如果你重载了 operator+,那么也应该重载 operator+=。
- 当重载某些运算符,如 ==,通常也需要重载相应的运算符,如 !=,以确保逻辑一致性。
- 某些运算符最好重载为非成员函数。像 << 和 >> 这类运算符,如果要用于输入输出流的话,通常作为非成员函数重载比较合适,因为它们的左操作数通常是流对象。
拓展:
- C++支持重载的运算符
- 转换函数(这个不算运算符重载,例:operator int())
拷贝构造函数和拷贝赋值运算符
对象的赋值操作是常见的操作,应该尽量避免使用浅拷贝,因为这种方式存在潜在风向。为解决这个问题类可以定义专门的拷贝构造函数和拷贝赋值运算符,以控制对象如何被复制:
#include <iostream> class MyClass { public: MyClass() : data(new int[10]) { } // 默认构造函数 ~MyClass() { delete[] data; } // 析构函数 // 拷贝构造函数 MyClass(const MyClass & other) : data(new int[10]) { std::copy(other.data, other.data + 10, data); std::cout << "copy init" << std::endl; } // 拷贝赋值运算符 MyClass & operator=(const MyClass & other) { if (this != &other) { // 避免自赋值 std::copy(other.data, other.data + 10, data); } std::cout << "copy =" << std::endl; return *this; } private: int* data; }; int main() { MyClass a; MyClass b; MyClass c = a; c = b; return 0; } // 程序输出 // copy init // copy =
拓展:
- 浅拷贝 深拷贝
移动构造函数和移动赋值运算符(C++11)
在 C++11 中引入了移动语义,允许从临时对象“移动”资源,而不是复制它们:
#include <iostream> using namespace std; class BigMemoryPool { private: static const int POOL_SIZE = 4096; int* mPool; public: BigMemoryPool() : mPool(new int[POOL_SIZE]{0}) { cout << "call default init" << endl; } // 编译器会优化移动构造函数,正常情况可能不会被执行 // 可以添加编译选项 “-fno-elide-constructors” 关闭优化来观察效果 BigMemoryPool(BigMemoryPool && other) noexcept { mPool = other.mPool; other.mPool = nullptr; cout << "call move init" << endl; } BigMemoryPool & operator=(BigMemoryPool && other) noexcept { if (this != &other) { this->mPool = other.mPool; other.mPool = nullptr; } cout << "call op move" << endl; return *this; } void showPoolAddr() { cout << "pool addr:" << &(mPool[0]) << endl; } ~BigMemoryPool() { cout << "call destructor" << endl; } }; BigMemoryPool makeBigMemoryPool() { BigMemoryPool x; // 调用默认构造函数 x.showPoolAddr(); return x; // 返回临时变量,属于右值 } int main() { BigMemoryPool a(makeBigMemoryPool()); a.showPoolAddr(); a = makeBigMemoryPool(); a.showPoolAddr(); return 0; } // 输出内容 call default init pool addr:0x152009600 instance addr:0x16fdfeda0 pool addr:0x152009600 instance addr:0x16fdfeda0 // 编译器优化,这里a和x其实是同一个实例,因此不会触发移动构造 call default init pool addr:0x15200e600 // 新的临时变量,堆内存重新分配 instance addr:0x16fdfed88 // 临时变量对象地址 call op move // 移动赋值 call destructor pool addr:0x15200e600 // a的Pool指向的内存地址变成新临时对象分配的地址,完成转移 instance addr:0x16fdfeda0 // a对象的地址没有变化 call destructor
拓展:
- 返回值优化(RVO)
- 命名返回值优化(NRVO)
C++11引入移动语义之前,类似的做法需要返回指针或者通过拷贝的方式来保存临时对象,前者会引入资源管理问题后者会有拷贝的性能损耗。
友元函数和友元类
友元函数
友元函数是定义在类外部的普通函数,它被某个类声明为其“友元”。这意味着友元函数可以访问该类的所有成员,包括私有和受保护的成员。友元函数不是类成员函数,也不受类的封装性约束。
友元函数的声明方式是在类的定义内部使用关键字 friend,后跟函数的原型,友元函数实现时不能加类名作用域限定:
#include <iostream> // 声明 Vector2D 类 class Vector2D { private: float x_; float y_; public: Vector2D(float x = 0.0f, float y = 0.0f) : x_(x), y_(y) {} // 友元函数声明,用于重载 + 操作符 friend Vector2D operator+(const Vector2D & a, const Vector2D & b); // 输出 Vector2D 对象的友元函数 friend std::ostream & operator<<(std::ostream & out, const Vector2D & v); }; // 重载 + 操作符的友元函数定义 Vector2D operator+(const Vector2D & a, const Vector2D & b) { return Vector2D(a.x_ + b.x_, a.y_ + b.y_); } // 重载 << 操作符的友元函数定义,用于输出 Vector2D 对象 std::ostream & operator<<(std::ostream & out, const Vector2D & v) { out << "(" << v.x_ << ", " << v.y_ << ")"; return out; } int main() { Vector2D vec1(1.0, 2.0); Vector2D vec2(3.0, 4.0); Vector2D vec3; vec3 = vec1 + vec2; // 使用友元函数重载的 + 操作符 std::cout << "vec1: " << vec1 << std::endl; std::cout << "vec2: " << vec2 << std::endl; std::cout << "vec3: " << vec3 << std::endl; // 输出: vec3: (4, 6) return 0; }
友元类
友元类是一个允许特定类访问另一个类的私有和受保护成员的机制。在 C++ 中,通常情况下,一个类无法访问另一个类的私有(private)和受保护(protected)成员,即使它们需要彼此协作。友元类提供了一种方式,让你可以指定某些类之间有更紧密的关系,并允许它们访问对方的非公共接口。下面是示例:
#include <iostream> class MyClass; // 前向声明 // 声明一个类(FriendClass),该类将访问MyClass的私有和受保护成员 class FriendClass { public: void accessMyClass(MyClass & obj); }; // 声明主类(MyClass) class MyClass { private: int secret; public: MyClass(int val) : secret(val) {} // 声明FriendClass为MyClass的友元类 friend class FriendClass; }; // FriendClass成员函数实现 void FriendClass::accessMyClass(MyClass & obj) { // 可以访问MyClass的私有成员'secret' std::cout << "MyClass secret value is: " << obj.secret << std::endl; } int main() { MyClass obj(42); // 创建MyClass对象 FriendClass friendObj; // 创建FriendClass对象 friendObj.accessMyClass(obj); // 访问MyClass的私有成员 return 0; }
使用友元可能会破坏类的封装性和数据隐藏原则,因为它们允许外部函数或者类直接访问类的私有成员。因此,建议谨慎使用友元,只在确实需要时才使用,并寻找是否有其他设计替代方案。在设计类时,应尽可能通过公共成员函数或成员函数的重载来提供类的行为和操作,而将友元作为特定情况下的解决方案。
继承
类可以从其他类继承,从而获得基类的成员和行为:
class Base { // 基类成员 }; class Derived : public Base { // 派生类成员 };
C++继承方式有三种:
- 公有继承(public)最常见的继承类型。在公有继承中,基类的公有成员和保护成员在派生类中保持其原有的访问级别,而基类的私有成员在派生类中是不可访问的。
- 保护继承(protected)基类的公有成员和保护成员都成为派生类的保护成员。这意味着它们只能被派生类或其进一步的派生类中的成员函数访问。
- 私有继承(private)私有继承会将基类的公有成员和保护成员都变成派生类的私有成员。这意味着这些成员只能被派生类的成员函数访问,而不能被派生类的派生类访问。
C++是支持多重继承的,即可以从多个类派生一个类,但是通常建议谨慎使用,因为多重继承可能会引起一些复杂的问题。
拓展:
虚继承
多态
多态允许派生类重写基类的虚拟函数,使得通过基类引用或指针调用这些函数时可以执行派生类的版本:
#include <iostream> class Base { public: void baseMethod() { std::cout << "Base method" << std::endl; } virtual void polymorphicMethod() { std::cout << "Base polymorphic method" << std::endl; } virtual ~Base() {} // 虚析构函数,用于多态 }; // 公有继承派生类 class Derived : public Base { public: // 重写基类的虚函数 void polymorphicMethod() override { Base::polymorphicMethod(); // 可以通过添加限定域调用基类实现 std::cout << "Derived polymorphic method" << std::endl; } }; int main() { Derived d; d.baseMethod(); // 调用基类的方法 d.polymorphicMethod(); // 调用派生类重写的方法 Base *b = &d; b->polymorphicMethod(); // 通过基类指针调用派生类的方法,体现多态 return 0; }
在类继承的场景中,基类的析构函数一般要声明为虚析构函数,这样才能保证在通过基类指针删除对象时,派生类的资源也能被正确的释放。
拓展:
虚函数表 动态绑定
抽象类和纯虚函数
如果一个类包含至少一个纯虚函数(以 = 0 结尾),则该类被认为是抽象类,不能直接实例化,只包含纯虚函数而没有成员变量的抽象类和Java中的接口(Interface)功能类似。
// Interface in C++ class IShape { public: virtual void draw() const = 0; // 纯虚函数 virtual ~IShape() {} // 虚析构函数以确保派生类的析构函数被调用 }; class Circle : public IShape { public: void draw() const override { // 实现绘制圆形的代码 } }; class Rectangle : public IShape { public: void draw() const override { // 实现绘制矩形的代码 } };
模板类
C++模板类是一种强大的特性,它允许程序员编写泛型且可重用的代码。模板类可以用来定义在编译时可以指定类型参数的类,这意味着可以用相同的基本代码来处理不同的数据类型。可以这么说现代C++的很多功能强大的特性都和模板技术有关系下面是模板类的一般定义语法:
template <typename T> class MyTemplateClass { const T& getValue(); public: T myValue; };
因为模板类的复杂性,这里不做展开。因为模板是一种强大的语言特性,C++中常见的模板类应用如下:
容器类
C++标准库中提供一系列的泛型容器,前面提到过的 vector、list、stack都是模板类实现的。
相关容器的用法可以搜索对应的文档。
智能指针
智能指针,同样是利用模板类技术实现的,它们提供了自动内存管理功能,可以帮助避免内存泄漏。
下面是现代C++提供的智能指针:
- std::unique_ptr:std::unique_ptr 是一个独有所有权的智能指针。它保证同一时间内只有一个智能指针实例可以拥有一个给定的对象。当 std::unique_ptr 被销毁时,它所拥有的对象也会被销毁。std::unique_ptr 通常用于对资源有独占所有权的情况,并且它是不可以被复制的,但可以被移动,以便所有权可以从一个 std::unique_ptr 转移到另一个。
- std::shared_ptr:std::shared_ptr 实现了共享所有权的概念。它通过内部的引用计数机制来跟踪有多少个 std::shared_ptr 实例共享同一个对象。当最后一个这样的指针被销毁时,所拥有的对象将会被删除。std::shared_ptr 适用于多个拥有者需要管理同一个对象的生命周期的情况。
- std::weak_ptr:std::weak_ptr 是一种非拥有(弱)引用的智能指针。它不会增加对象的引用计数,因此不会阻止所指向的对象被销毁。std::weak_ptr 主要用于解决 std::shared_ptr 之间可能出现的循环引用问题。通过 std::weak_ptr,你可以观察一个对象,但不会造成所有权关系。
// 简单示例 // 定义智能指针 // C++11语法 std::unique_ptr<MyClass> my_unique_ptr(new MyClass()); std::shared_ptr<MyClass> my_shared_ptr(new MyClass()); // C++14提供了更安全更现代的方法 auto my_unique_ptr = std::make_unique<MyClass>(); auto my_shared_ptr = std::make_shared<MyClass>(); // 可以按照构造函数的定义传参 // 调用类的方法和普通指针类似 my_unique_ptr->func(); my_shared_ptr->func(); // 在需要传对象指针和引用的场景 // 类指针类型 void testFunc1(MyClass * p); testFunc1(my_unique_ptr.get()); // 通过get获取原始指针 // 引用类型 void testFunc2(MyClass & ref); testFunc2(*my_unique_ptr); // 通过*运算符获取对象的引用
函数对象
上面介绍函数回调时说过的 std::function也是模板类,它是一个泛型函数封装器,其实例可以存储、复制和调用任何可调用对象,如普通函数、Lambda 表达式、函数对象(functors)以及其他函数指针。下面是一些典型用法:
// 封装函数 void printHello() { std::cout << "Hello, World!" << std::endl; } std::function<void()> func = printHello; // 封装Lambda表达式 std::function<int(int, int)> add = [](int a, int b) -> int { return a + b; }; int sum = add(2, 3); // sum 的值为 5 // 封装成员函数 class MyClass { public: void memberFunction() const { std::cout << "Member function called." << std::endl; } }; MyClass obj; std::function<void(const MyClass &)> f = &MyClass::memberFunction; f(obj); // 输出: Member function called. // 封装带有绑定参数的函数 void printSum(int a, int b) { std::cout << "Sum: " << a + b << std::endl; } int main() { using namespace std::placeholders; // 对于 _1, _2, _3... // 绑定第二个参数为 10,并将第一个参数留作后面指定 std::function<void(int)> func = std::bind(printSum, _1, 10); func(5); // 输出: Sum: 15 return 0; }