请先阅读:
接上篇
c++类进阶之构造/析构函数
在 C++ 中,构造函数是一种特殊的成员函数,用于初始化对象的状态。构造函数的名称与类名相同,不返回任何类型,甚至不返回 void
。构造函数可以有多种形式,具备不同的特性,以满足不同的初始化需求。
1. 默认构造函数
默认构造函数是在不接受任何参数的情况下调用的构造函数。如果一个类没有定义任何构造函数,编译器会自动生成一个默认构造函数(只要没有其他构造函数)。如果类中定义了其他构造函数,但没有显示定义默认构造函数,且其他构造函数都需要参数,则需要显式定义一个无参数的构造函数,如果你需要无参数的对象创建。
class Example {
public:
Example() {
// 初始化代码
}
};
2. 参数化构造函数
参数化构造函数接受一个或多个参数,用于提供创建对象时更多的灵活性。通过参数化构造函数,可以在创建对象的同时初始化对象的数据成员。
注:自己写了带参构造函数以后,系统就不会给你分配一个默认构造函数了,如果有需要无参构造函数,需要自行添加
class Example {
public:
Example(Example&& other) noexcept {
// 移动初始化代码
}
};
class Example {
public:
int x;
Example(int val) : x(val) {}
};
3. 拷贝构造函数
拷贝构造函数用于初始化一个对象,使其成为另一个同类型对象的副本。如果类中没有显式定义拷贝构造函数,编译器会自动生成一个。
class Example {
public:
Example(const Example& other) {
// 拷贝初始化代码
}
};
4. 移动构造函数(C++11)
移动构造函数用于通过移动而非拷贝的方式初始化一个对象,它接受一个右值引用参数。移动构造函数通常用于优化性能,避免在对象传递过程中创建不必要的临时对象和深拷贝。
class Example {
public:
Example(Example&& other) noexcept {
// 移动初始化代码
}
};
5. 委托构造函数(C++11)
委托构造函数允许一个构造函数在同一个类中调用另一个构造函数,以避免代码重复。
5. 委托构造函数(C++11)
委托构造函数允许一个构造函数在同一个类中调用另一个构造函数,以避免代码重复。
6. 显式和删除的构造函数
- 显式构造函数:通过
explicit
关键字标记的构造函数,防止编译器自动使用该构造函数进行隐式类型转换。 - 删除的构造函数:通过
delete
关键字明确禁止某些构造函数形式,用于控制类的使用方式,如禁止拷贝或移动。
class Example {
public:
explicit Example(int x) {}
Example(const Example&) = delete; // 禁止拷贝构造
};
构造函数的正确使用可以确保对象的正确初始化,同时利用现代 C++ 的特性(如移动语义和委托构造)可以编写更高效、更简洁的代码。
在 C++ 中,析构函数是一个特殊的成员函数,其作用是在对象生命周期结束时执行清理任务,如释放资源、关闭文件、断开网络连接等。析构函数在对象销毁时自动调用,它的名字由波浪符 ~
后跟类名构成,不能带参数,也不返回值。
析构函数的主要特点:
- 自动调用:析构函数在对象的生命周期结束时(如对象离开其作用域或被
delete
删除)被自动调用。 - 无参数无返回:析构函数不接受任何参数,也不返回任何值,甚至不返回
void
。 - 非重载:每个类只能有一个析构函数,因此析构函数不能重载。
析构函数的用途:
- 资源释放:最常见的用途是释放对象在生命周期内申请的资源。这符合 RAII(资源获取即初始化)原则,确保程序的健壁性和异常安全。
- 清理工作:执行任何必要的最终清理工作,如关闭文件句柄或网络连接,解锁互斥锁等。
示例代码:
class Demo {
public:
int* array;
Demo(int size) { // 构造函数
array = new int[size]; // 申请资源
}
~Demo() { // 析构函数
delete[] array; // 释放资源
}
};
void function() {
Demo demo(100); // 在函数作用域中创建对象
} // demo 离开作用域,自动调用析构函数释放资源
特殊情况:
- 虚析构函数:如果一个类预计会被继承,并且可能通过基类指针删除派生类对象,则应将析构函数声明为虚函数。这确保了通过基类指针删除派生类对象时,能够调用正确的析构函数,从而避免资源泄漏。
class Base {
public:
virtual ~Base() {
// 基类的析构函数
}
};
class Derived : public Base {
public:
~Derived() {
// 派生类的析构函数
}
};
删除的析构函数:你可以将析构函数定义为删除的,来阻止对象的删除操作。这在设计某些特殊控制对象生命周期的类时非常有用。
class NonDeletable {
public:
~NonDeletable() = delete; // 阻止删除操作
};
注意事项:
- 确保通过相同的方式分配和释放资源。例如,如果你使用
new[]
分配了内存,应该使用delete[]
来释放它。 - 虚析构函数可以增加一些运行时成本,因为它们需要虚函数机制支持。然而,当设计需要多态行为的类层次结构时,这是必须的。
- 通常,只有当类用于管理资源时,才需要显式定义析构函数。如果类不拥有资源,可以依赖编译器自动生成的析构函数。
理解和正确使用析构函数对于保证 C++ 程序的资源正确管理和程序的稳定性至关重要。
c++类进阶之多态的实现
C++中的多态性是面向对象编程的核心特性之一,它允许使用统一的接口来操作不同的数据类型。多态主要分为两种类型:编译时多态(静态多态)和运行时多态(动态多态)。它们的实现方式和原理有所不同。
1. 编译时多态(静态多态)
编译时多态主要是通过函数重载和运算符重载实现的。这种多态在编译期间解析,所以称为静态多态。
函数重载
函数重载允许在同一作用域内存在多个同名函数,只要它们的参数列表不同(参数类型或参数数量)即可。
class Print {
public:
void show(int i) {
std::cout << "Integer: " << i << std::endl;
}
void show(double f) {
std::cout << "Float: " << f << std::endl;
}
};
运算符重载
运算符重载允许给已有的运算符赋予更多的功能,使它们能在用户定义的数据类型上进行操作。
class Complex {
public:
int real, imag;
Complex(int r = 0, int i = 0) : real(r), imag(i) {}
// 运算符重载
Complex operator + (const Complex& obj) {
return Complex(real + obj.real, imag + obj.imag);
}
};
2. 运行时多态(动态多态)
运行时多态是通过基类指针或引用访问派生类对象实现的,这种多态性依赖虚函数。
虚函数和虚拟继承
运行时多态的关键是虚函数。当一个函数在基类中被声明为虚函数后,派生类可以重写这个函数来提供特定的功能。C++ 使用虚函数表(vtable)来支持运行时多态。
class Animal {
public:
virtual void sound() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Bark" << std::endl;
}
};
void makeSound(Animal& a) {
a.sound(); // 在运行时解析
}
虚函数表 (vtable)
- 虚函数表:如果类中有虚函数,编译器会为这个类创建一个虚函数表。这个表包含指向虚函数地址的指针。
- vptr:每个对象会有一个指针(vptr),指向它的虚函数表。当调用一个虚函数时,实际上是通过 vptr 来间接访问这个函数。
当通过基类的指针或引用调用虚函数时,程序会查看虚函数表以确定应该调用哪个函数,这个查找过程是在运行时进行的,因此称为动态多态。
特点和考虑
- 动态多态的成本:使用动态多态会有性能损失,因为需要间接调用函数。但在需要使用多态性的场景下,这种成本是合理的。
- 设计灵活性:多态性提高了代码的可维护性和扩展性,使得新的类类型可以很容易地整合进现有的系统中。
- 接口一致性:多态允许不同类的对象对同一消息作出不同的响应,从而可以编写出更通用的代码来处理不同类型的对象。
多态性是 C++ 强大功能的体现,正确利用多态可以让我们的代码更加灵活和动态。
c++类进阶之运算符重载
在 C++ 中,运算符重载是一种形式的多态,允许开发者为用户定义的类型指定运算符的操作。这使得用户定义的类型可以像内建类型那样进行操作,增强了代码的可读性和便利性。下面是一些常见的运算符重载类型和它们的注意事项。
常见的运算符重载类型
-
赋值运算符(
=
)- 用于定义对象之间的赋值行为。
class MyClass {
public:
int *data;
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 检查自赋值
delete data;
data = new int(*(other.data));
}
return *this;
}
};
2.算术运算符(+
, -
, *
, /
, %
等)
- 用于定义加减乘除等运算。
class Complex {
public:
double real, imag;
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
};
3.比较运算符(==
, !=
, <
, >
, <=
, >=
)
-
用于定义对象之间的比较。
class MyClass { public: int value; bool operator==(const MyClass& other) const { return value == other.value; } };
-
4.逻辑运算符(
&&
,||
,!
)- 不常重载,因为它们涉及到短路逻辑,不能通过重载直接实现.
-
5.下标运算符(
[]
) - 用于提供类似数组的访问方式。
class IntArray {
public:
int* array;
int& operator[](int index) {
return array[index];
}
};
6.函数调用运算符(()
)
- 使得对象可以像函数那样被调用。
class Adder {
public:
int operator()(int x, int y) const {
return x + y;
}
};
7.输入输出运算符(<<
, >>
)
- 常用于定义对象的输入输出方式,尤其是与流(如
std::ostream
和std::istream
)相关的操作。
std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
os << obj.value;
return os;
}
注意事项
-
自赋值安全:赋值运算符应该处理自赋值的情况,以防止错误和资源泄露。
-
返回引用:对于赋值运算符和流插入/提取运算符,通常返回引用以支持链式调用。
-
操作符的对称性:例如,如果你重载了
+
,通常也会重载+=
。确保操作符的行为在逻辑上是一致的。 -
非成员函数:有些运算符,如
<<
和>>
,通常应该作为非成员函数实现,以允许第一个操作数是非类类型。 -
不要过度重载:只在逻辑上合理的情况下重载运算符,避免造成混淆和误用。不要重载运算符以实现与预期完全不相关的功能。
-
保持简洁:运算符重载应简洁明了,不要在重载的运算符中包含复杂的逻辑。
-
避免重载某些运算符:特别是对于内置类型的运算符,如
&&
、||
和逗号运算符,因为这些运算符涉及到特殊的求值策略(如短路求值),不能通过重载直接模拟。
通过恰当地使用运算符重载,可以让用户自定义的类型更加直观和易于使用,但也需要小心设计以避免引入意外的行为和复杂性。
c++类进阶之纯虚函数
在 C++ 中,纯虚函数是一种特殊的虚函数,其主要用途是在基类中声明一个接口,以强制派生类提供该函数的具体实现。纯虚函数没有实现体(即没有函数定义),而是在函数声明的末尾使用 = 0
来指定。
纯虚函数的作用
-
定义接口:纯虚函数允许基类定义一个接口,该接口必须由任何非抽象派生类实现。这保证了派生类遵循特定的接口规范。
-
抽象类:包含至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化,它主要用于作为其他派生类的基类。
-
多态支持:通过纯虚函数实现的接口可以在运行时多态中使用。这允许通过基类的指针或引用调用派生类的实现,从而实现动态绑定。
用法示例
下面是一个使用纯虚函数的例子,其中定义了一个抽象基类和几个实现了这个基类的派生类:
#include <iostream>
class Shape {
public:
// 纯虚函数
virtual double area() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
};
void printArea(const Shape& shape) {
std::cout << "Area: " << shape.area() << std::endl;
}
int main() {
Circle circle(5);
Rectangle rectangle(10, 5);
printArea(circle);
printArea(rectangle);
return 0;
}
在这个例子中:
Shape
类有一个纯虚函数area()
,使得Shape
成为一个抽象类。Circle
和Rectangle
是Shape
的派生类,它们都提供了area()
函数的具体实现。printArea
函数展示了如何通过基类引用来调用具体派生类的area()
方法,体现了多态的使用。
注意事项
- 抽象类不能实例化:不能创建一个抽象类的对象,但可以创建指向派生类对象的基类指针或引用。
- 析构函数应为虚:如果一个类是多态的基类,它的析构函数应该是虚的。这确保了通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数。
- 实现抽象类的派生类必须覆盖所有纯虚函数:任何从抽象类派生的类,如果想要被实例化,必须为基类中的所有纯虚函数提供实现。
纯虚函数是实现抽象和多态性的强大工具,适当使用可以大大增强程序的可维护性和灵活性。
c++类进阶之接口实现
在 C++ 中,虽然没有直接的关键字或构造来定义接口(如 Java 中的 interface
关键字),但可以通过抽象类来模拟接口的行为。一个纯接口类是由纯虚函数组成的抽象类,这意味着它没有成员变量和已实现的成员函数。这样的类在 C++ 中充当接口的角色。
定义 C++ 接口
一个典型的 C++ 接口类包含以下特点:
- 只有纯虚函数:这确保了派生类必须实现这些函数。
- 没有数据成员:接口只定义行为,不定义状态。
- 可能包含虚析构函数:虽然接口通常不需要实现析构函数,但提供一个虚析构函数是好的做法,以保证派生类的析构函数被正确调用。
示例:定义一个 C++ 接口
以下是一个模拟 C++ 接口的示例:
class IPrintable {
public:
virtual void print() const = 0; // 纯虚函数
virtual ~IPrintable() {} // 虚析构函数,确保派生类的析构器被调用
};
class Document : public IPrintable {
public:
void print() const override {
std::cout << "Document printing..." << std::endl;
}
};
class Image : public IPrintable {
public:
void print() const override {
std::cout << "Image printing..." << std::endl;
}
};
void performPrint(const IPrintable& obj) {
obj.print();
}
int main() {
Document doc;
Image img;
performPrint(doc);
performPrint(img);
return 0;
}
在这个示例中,IPrintable
是一个接口,它定义了一个 print
方法。Document
和 Image
是两个具体的类,它们实现了 IPrintable
接口。performPrint
函数接受一个 IPrintable
类型的引用,展示了如何使用接口来调用具体实现。
使用 C++ 接口的优点
- 提高了代码的模块化:接口作为一个合约,使得不同的编程团队可以独立工作在接口的不同实现上。
- 增加了代码的灵活性:通过接口,可以在不修改现有代码的情况下引入新的行为和实现。
- 便于代码测试和维护:接口使得依赖注入和模拟变得更加容易,有利于单元测试。
注意事项
- 在设计接口时,应仔细考虑所需的功能,避免接口变得过于庞大。一个庞大的接口可能需要被拆分成多个更小的接口,每个接口负责一部分明确的功能。
- 由于 C++ 缺乏内置的接口支持,开发者需要自行确保遵守接口编程的最佳实践。