程设一些知识点
本文档主要整理了我在复习中遇到比较不熟悉的几个知识点,包括运算符重载、虚函数、模板以及智能指针的使用。
1. 运算符重载
运算符重载允许我们为自定义类型(如类)重新定义或“重载”已有的运算符行为。
1.1. 输入/输出运算符重载 (以 complex 类为例)
在 C++ 中,<< 和 >> 运算符通常分别用于输出和输入。我们可以为自定义类重载这些运算符,以便能像内置类型一样方便地进行输入输出操作。
#include <iostream> // 需要包含 iostream 以使用 cin 和 cout
// 为了代码片段能独立编译,我们添加 using namespace std;// 在大型项目中,更推荐显式使用 std::cin, std::cout 等using namespace std;
class complex {public: double r, i; // 实部和虚部
// 构造函数 (为了示例完整性,添加一个默认构造函数和带参构造函数) complex(double r_val = 0.0, double i_val = 0.0) : r(r_val), i(i_val) {}
// 拷贝构造函数 (用户提供的版本) // 注意:通常如果类中没有动态分配的资源,编译器生成的默认拷贝构造函数就足够了。 // 这里的实现是空的,仅为展示。在实际应用中,如果需要自定义,则应正确拷贝成员。 complex(const complex &c) { r = c.r; i = c.i; // cout << "拷贝构造函数被调用" << endl; // 用于观察 }
// 友元函数重载 >> (输入运算符) // istream& 表示输入流对象的引用,使其可以链式操作 (cin >> a >> b) // complex& com 表示要读取数据的 complex 对象的引用 friend istream &operator>>(istream &in, complex &com) { // 提示用户输入 // cout << "请输入复数的实部和虚部 (以空格分隔): "; // 实际应用中可加提示 in >> com.r >> com.i; // 从输入流中读取实部和虚部 return in; // 返回输入流对象,支持链式输入 }
// 友元函数重载 << (输出运算符) // ostream& 表示输出流对象的引用 // const complex& com 表示要输出的 complex 对象的常量引用 (因为输出操作不应修改对象) friend ostream &operator<<(ostream &out, const complex &com) { out << com.r << " + " << com.i << "i"; // 按 "实部 + 虚部i" 格式输出 // 注意:原代码是 cout << com.r << ' ' << com.i; 为了更像复数,这里做了修改 return out; // 返回输出流对象,支持链式输出 }};
// 示例用法:// int main() {// complex c1;// cout << "请输入一个复数 (实部 虚部): ";// cin >> c1; // 调用 operator>>(cin, c1)// cout << "输入的复数是: " << c1 << endl; // 调用 operator<<(cout, c1)//// complex c2 = c1; // 调用拷贝构造函数// cout << "拷贝后的复数是: " << c2 << endl;// return 0;// }解释:
friend关键字:允许非成员函数访问类的private和protected成员。对于<<和>>重载,通常将它们声明为友元函数,因为它们的左操作数是流对象(istream或ostream),而不是类对象。istream &operator>>(istream &in, complex &com):- 参数
istream &in: 输入流的引用 (例如cin)。 - 参数
complex &com: 要被赋值的complex对象的引用。 - 返回值
istream &: 返回输入流的引用,以支持链式输入 (如cin >> c1 >> c2;)。
- 参数
ostream &operator<<(ostream &out, const complex &com):- 参数
ostream &out: 输出流的引用 (例如cout)。 - 参数
const complex &com: 要输出的complex对象的常量引用(输出操作不应修改对象)。 - 返回值
ostream &: 返回输出流的引用,以支持链式输出 (如cout << c1 << c2;)。
- 参数
1.2. 前置与后置自增/自减运算符重载
自增 (++) 和自减 (--) 运算符有前置和后置两种形式。它们的重载方式略有不同。
1.2.1. 成员函数方式
-
前置运算符 (
++obj或--obj):- 声明:
T& operator++();或T& operator--(); - 返回类型通常是被操作对象的引用 (
T&)。 - 实现时,先修改对象的值,然后返回修改后的对象。
- 声明:
-
后置运算符 (
obj++或obj--):- 声明:
T operator++(int);或T operator--(int); - 参数列表中的
int是一个哑元(dummy argument),仅用于区分前置和后置版本,调用时不需要传递实参。 - 返回类型通常是被操作对象的值 (
T),而不是引用。这是因为后置操作符需要返回对象在自增/自减前的状态。 - 实现时,先保存对象的当前状态(创建一个临时副本),然后修改原始对象的值,最后返回保存的临时副本。
- 声明:
1.2.2. 全局 (友元) 函数方式
-
前置运算符 (
++obj或--obj):- 声明:
T1& operator++(T2& obj);或T1& operator--(T2& obj);(其中T1通常是T2或其引用) - 第一个参数是被操作对象的引用。
- 声明:
-
后置运算符 (
obj++或obj--):- 声明:
T1 operator++(T2& obj, int);或T1 operator--(T2& obj, int);(其中T1通常是T2) - 第一个参数是被操作对象的引用,第二个参数是哑元
int。
- 声明:
1.2.3. Counter 类示例 (全局函数方式)
// 假设 Counter 类的定义class Counter {public: int value;
Counter(int v = 0) : value(v) {}
// 声明友元函数以便它们可以访问 Counter 的私有成员 (如果 value 是 private) // 如果 value 是 public,则友元声明不是严格必需的,但通常是良好实践, // 因为运算符重载通常与类的接口紧密相关。 friend Counter& operator++(Counter& c); // 前置++ friend Counter operator++(Counter& c, int); // 后置++
// 为了方便演示,添加一个输出方法 void print() const { cout << "Counter value: " << value << endl; }};
// 全局函数实现前置 ++Counter& operator++(Counter& c) { ++c.value; // 修改对象的值 return c; // 返回修改后的对象}
// 全局函数实现后置 ++Counter operator++(Counter& c, int) { Counter temp = c; // 保存原始值的副本 ++c.value; // 修改原始对象的值 (也可以调用前置版本: ++c;) return temp; // 返回原始值的副本}
// 示例用法:// int main() {// Counter c1(5);// cout << "初始 "; c1.print();//// Counter c2 = ++c1; // 前置自增// cout << "++c1 后: " << endl;// cout << "c1: "; c1.print();// cout << "c2: "; c2.print();//// Counter c3(10);// cout << "初始 "; c3.print();// Counter c4 = c3++; // 后置自增// cout << "c3++ 后: " << endl;// cout << "c3: "; c3.print();// cout << "c4: "; c4.print();//// return 0;// }要点:
- 后置版本通过一个未命名的
int参数与前置版本区分。 - 前置版本通常返回对象的引用 (
T&),而后置版本通常按值返回 (T),因为它返回的是操作前的对象状态。
2. 虚函数 (Virtual Functions)
虚函数是实现 C++ 运行时多态性的关键机制。当通过基类指针或引用调用派生类中的虚函数时,会根据指针或引用实际指向的对象类型来决定调用哪个版本的函数。
class Base {public: // 虚析构函数 // 如果一个类要作为基类,并且可能会通过基类指针删除派生类对象, // 那么它的析构函数应该声明为 virtual。 // 这样可以确保在删除派生类对象时,先调用派生类的析构函数,然后再调用基类的析构函数。 // = default; 表示使用编译器生成的默认实现。 virtual ~Base() = default;
// 纯虚函数 (Pure Virtual Function) // 纯虚函数在基类中没有实现,它强制派生类必须提供该函数的具体实现。 // const 表示该成员函数不会修改类的成员变量。 // = 0; 表明这是一个纯虚函数。 virtual int function(int x) const = 0;
// 普通虚函数 (示例) virtual void printType() { cout << "This is Base class" << endl; }};
class Derived : public Base {public: // 覆盖 (override) 基类的虚析构函数 // ~Derived() override { cout << "Derived destructor called" << endl; } // C++11 override 关键字 ~Derived() { /* cout << "Derived destructor called" << endl; */ } // 也可以不加 override
// 实现基类中的纯虚函数 // C++11 中推荐使用 override 关键字明确表示这是对基类虚函数的覆盖 int function(int x) const override { return x * x; }
// 覆盖基类的普通虚函数 void printType() override { cout << "This is Derived class" << endl; }};
// 示例用法:// int main() {// // Base b; // 错误!Base 是抽象类,不能实例化//// Derived d;// cout << "d.function(5): " << d.function(5) << endl; // 输出 25//// Base* ptr_b = &d; // 基类指针指向派生类对象// cout << "ptr_b->function(10): " << ptr_b->function(10) << endl; // 输出 100 (调用 Derived::function)// ptr_b->printType(); // 输出 "This is Derived class" (调用 Derived::printType)//// // 如果 Base 的析构函数不是 virtual,通过基类指针 delete 派生类对象可能导致资源泄漏// // Base* ptr_heap = new Derived();// // delete ptr_heap; // 正确调用 Derived 和 Base 的析构函数//// return 0;// }关键点:
virtual ~Base() = default;: 虚析构函数。如果类可能被继承,并且可能通过基类指针删除派生类对象,则析构函数应声明为virtual。这确保了派生类的析构函数会被正确调用,防止资源泄漏。= default表示使用编译器提供的默认实现。virtual int function(int x) const = 0;: 纯虚函数。virtual: 表明它是一个虚函数。const: 表明该函数不会修改类的成员变量(对于const对象或通过const引用/指针调用)。= 0: 将其声明为纯虚函数。纯虚函数在声明它的类中没有定义(实现)。- 包含一个或多个纯虚函数的类称为抽象类。抽象类不能被实例化(不能创建其对象)。
- 派生类必须实现(覆盖)基类中的所有纯虚函数,否则派生类也将成为抽象类。
3. 类模板 (Class Templates)
类模板允许我们定义一个通用的类蓝图,其中的数据类型或某些值可以作为参数在实例化时指定。
#include <stdexcept> // 为了使用 std::out_of_range
template <typename T, int i> // T 是类型参数,i 是非类型参数 (整数常量)class TestClass {public: T buffer[i]; // T 类型的数组,其大小由非类型参数 i 决定
// 构造函数 (示例) TestClass() { // 可以在这里初始化 buffer,例如: // for(int k = 0; k < i; ++k) { // buffer[k] = T(); // 使用 T 类型的默认构造函数初始化 // } }
// 成员函数声明 T getData(int j);
// 另一个成员函数示例 void setData(int j, T value) { if (j >= 0 && j < i) { buffer[j] = value; } else { // 处理越界,例如抛出异常 throw std::out_of_range("Index out of bounds in setData"); } }
int getSize() const { return i; }};
// 类模板的成员函数定义在类外时,需要再次声明模板参数列表template <typename T, int i>T TestClass<T, i>::getData(int j) { if (j >= 0 && j < i) { return *(buffer + j); // 等价于 buffer[j] } else { // 处理越界,例如抛出异常或返回默认值 throw std::out_of_range("Index out of bounds in getData"); // 或者 return T(); // 返回 T 类型的默认构造值 }}
// 示例用法:// int main() {// TestClass<int, 10> intTest; // T 为 int, i 为 10// intTest.setData(0, 100);// intTest.setData(5, 500);//// cout << "intTest.getData(0): " << intTest.getData(0) << endl; // 输出 100// cout << "intTest.getData(5): " << intTest.getData(5) << endl; // 输出 500// cout << "Size of intTest buffer: " << intTest.getSize() << endl; // 输出 10//// TestClass<double, 5> doubleTest; // T 为 double, i 为 5// doubleTest.setData(2, 3.14);// cout << "doubleTest.getData(2): " << doubleTest.getData(2) << endl; // 输出 3.14//// // TestClass<int, 0> zeroSizeTest; // 通常 i 必须大于 0,否则 buffer[0] 是非法的//// try {// cout << intTest.getData(10) << endl; // 会抛出异常// } catch (const std::out_of_range& e) {// cerr << "Exception: " << e.what() << endl;// }//// return 0;// }解释:
template <typename T, int i>: 模板声明。typename T:T是一个类型参数,代表任何数据类型(如int,double,std::string, 自定义类等)。class也可以用来代替typename。int i:i是一个非类型模板参数。它必须是一个编译时常量(如整型、指针、引用,或枚举值)。在这里,它决定了buffer数组的大小。
T buffer[i];: 类成员,一个类型为T,大小为i的数组。T TestClass<T,i>::getData(int j): 当在类模板外部定义成员函数时,必须重复模板参数列表,并使用TestClass<T,i>来限定函数所属的类。
4. 函数模板与智能指针
函数模板允许我们编写一个通用的函数,它可以处理不同类型的数据。智能指针是管理动态分配内存的类,有助于防止内存泄漏。
#include <memory> // 为了使用 std::unique_ptr
// 假设 GtLever 函数的声明 (具体实现未知,仅为示例)// 它接收一个 T 类型的数组,数组长度 n,以及一个 T 类型的 lever 值template <class T>void GtLever(const T* arr, int n, T lever) { cout << "GtLever called with lever: " << lever << " and array: "; for (int i = 0; i < n; ++i) { cout << arr[i] << (i == n - 1 ? "" : ", "); } cout << endl; // 实际的 GtLever 逻辑会在这里}
// 处理输入和调用 GtLever 的函数模板template <class T> // T 是类型参数void processInput(int length, int n_for_gtlever) { // 修改了第二个参数名以示区分 // 使用智能指针 std::unique_ptr 管理动态分配的数组内存 // std::unique_ptr<T[]> arr(new T[length]); // C++14 及以后版本推荐使用 std::make_unique std::unique_ptr<T[]> arr = std::make_unique<T[]>(length);
cout << "Enter " << length << " elements of type " << typeid(T).name() << ":" << endl; for (int i = 0; i < length; i++) { cin >> arr[i]; }
T lever; cout << "Enter the lever value of type " << typeid(T).name() << ":" << endl; cin >> lever;
// 调用 GtLever 函数 // arr.get() 返回指向 unique_ptr所管理数组的原始指针 GtLever(arr.get(), n_for_gtlever, lever); // 假设 GtLever 需要的长度是 n_for_gtlever // 如果 GtLever 处理整个 arr,则应传递 length}
// 示例用法 (构造方法调用):// int main() {// int length, n;//// cout << "Processing char input:" << endl;// cout << "Enter array length: ";// cin >> length;// cout << "Enter n for GtLever: ";// cin >> n;// processInput<char>(length, n); // 实例化函数模板,T 为 char//// cout << "\nProcessing int input:" << endl;// cout << "Enter array length: ";// cin >> length;// cout << "Enter n for GtLever: ";// cin >> n;// processInput<int>(length, n); // 实例化函数模板,T 为 int//// return 0;// }解释:
template <class T>:processInput是一个函数模板,T是其类型参数。std::unique_ptr<T[]> arr(new T[length]);(或std::make_unique<T[]>(length);):std::unique_ptr是一种智能指针,它拥有其指向的对象。当unique_ptr对象本身被销毁时(例如离开作用域),它会自动释放所管理的内存。这有助于防止内存泄漏。T[]表示unique_ptr管理的是一个T类型的数组。new T[length]动态分配一个包含length个T类型元素的数组。std::make_unique<T[]>(length)(C++14+) 是创建unique_ptr的更安全、更简洁的方式。
arr.get():unique_ptr的get()成员函数返回一个指向其所管理内存的原始指针。这在需要将管理的内存传递给不接受unique_ptr的旧式 C API 或函数时很有用。processInput<char>(length, n);: 这是函数模板的显式实例化。编译器会根据指定的类型参数char生成一个特定版本的processInput函数。
5. 构造函数中使用智能指针初始化成员
在类的构造函数中初始化 std::unique_ptr 成员是常见的做法,以确保资源在对象创建时被正确获取,并在对象销毁时自动释放。
// 假设 T 是一个已定义的类型或模板参数// template <typename T> // 如果 C1 本身是模板类class C1 {public: int num; std::unique_ptr<int[]> a; // 使用 int[] 作为示例,原代码是 T[]
// 构造函数 // 使用成员初始化列表来初始化 num 和智能指针 a // std::make_unique<T[]>(n) (C++14+) 是推荐的初始化方式 C1(int n) : num(n), a(std::make_unique<int[]>(n)) { cout << "C1 constructor called. num = " << num << ". Array of size " << n << " allocated." << endl; // 可以在这里初始化数组 a 的内容 for (int i = 0; i < n; ++i) { a[i] = i * 10; // 示例初始化 } }
// 析构函数 (由 unique_ptr 自动管理内存,通常不需要显式 delete) ~C1() { cout << "C1 destructor called. Memory for array will be automatically deallocated by unique_ptr." << endl; }
void printArray() const { if (num > 0 && a) { cout << "Array contents: "; for (int i = 0; i < num; ++i) { cout << a[i] << (i == num - 1 ? "" : ", "); } cout << endl; } else { cout << "Array is empty or not allocated." << endl; } }};
// 示例用法:// int main() {// C1 obj1(5);// obj1.printArray();//// // 当 obj1 离开作用域时,其析构函数被调用,// // unique_ptr a 会自动释放其管理的动态数组内存。//// return 0;// }解释:
std::unique_ptr<T[]> a;: 类C1有一个名为a的成员,它是一个指向T类型数组的unique_ptr。C1(int n) : num(n), a(std::make_unique<T[]>(n)) {}:- 这是构造函数。
:num(n),a(std::make_unique<T[]>(n))是成员初始化列表。这是初始化类成员(尤其是const成员、引用成员和需要构造函数参数的成员对象)的首选方式。a(std::make_unique<T[]>(n))初始化unique_ptr成员a,使其管理一个新分配的大小为n的T类型数组。
- 当
C1类型的对象被销毁时,其成员a(即unique_ptr) 也会被销毁。unique_ptr的析构函数会自动delete[]它所管理的数组,从而防止内存泄漏。
6.作用域解析符在继承中的使用场景
在C++中,作用域解析符(::)在继承关系中主要用于以下几种情况:
-
当派生类中存在与基类同名的成员函数或变量时:
- 如果派生类重新定义了一个与基类同名的函数或变量,派生类的版本会”隐藏”基类的版本
- 这时如果想要访问基类的版本,必须使用作用域解析符
-
在派生类中显式调用基类的虚函数:
- 当想要绕过虚函数机制,明确调用基类版本的虚函数时
-
访问基类的静态成员:
- 虽然可以直接访问,但有时使用作用域解析符可以提高代码可读性
-
从外部直接引用基类或派生类中的静态成员或类型
在你的代码中:
a.Clock::settime(h, m, s);a.Clock::showtime();这里的作用域解析符是不必要的,因为:
AlarmClock类是公有继承自Clock类AlarmClock类中没有定义与Clock类同名的settime()和showtime()函数- 所以这些基类函数在派生类中直接可见,可以直接调用
正确的做法应该是:
a.settime(h, m, s);a.showtime();7.其他知识点补充
1. 内存管理与指针数组
// 动态分配对象数组Shape **s_dynamic_ptr_array; // 指向 Shape 指针的指针 (用于指针数组)s_dynamic_ptr_array = new Shape*[2]; // 分配一个包含两个 Shape* 指针的数组
s_dynamic_ptr_array[0] = new Circle(5.5); // 在堆上创建 Circle 对象,并存储其指针s_dynamic_ptr_array[1] = new Square(9.9); // 在堆上创建 Square 对象,并存储其指针
cout << "total=" << total(s_dynamic_ptr_array, 2) << endl; // total 函数可以正确处理
// 清理内存delete s_dynamic_ptr_array[0]; // 删除单个 Circle 对象delete s_dynamic_ptr_array[1]; // 删除单个 Square 对象delete[] s_dynamic_ptr_array; // 删除指针数组本身2. 对象切片与类型转换
// 错误示例:对象切片A *p;p = new A[2];B b(); C c(); // 注意:这行声明的是函数,而非对象!p[0]=b;p[1]=c;
// 正确做法:使用指针数组A* p_pointers[2]; // 包含两个 A 类型指针的数组B b_obj;C c_obj;
p_pointers[0] = &b_obj; // 存储 B 对象的地址p_pointers[1] = &c_obj; // 存储 C 对象的地址3. 虚基类 (解决菱形继承问题)
class Base {public: virtual void func() {}};class Derived1 : virtual public Base {public: void func() override {}};class Derived2 : virtual public Base {public: void func() override {}};class Derived3 : public Derived1, public Derived2 {public: void func() override {}};4. 静态成员与类型转换
// 静态成员示例class Example {public: static int count; static void printCount() { cout << "Count: " << count << endl; }};
int Example::count = 0; // 静态成员需要在类外定义
// 类型转换示例B *pb = dynamic_cast<B*>(p[0]); // 需要包含 <typeinfo>if (pb) { // 转换成功} else { // 转换失败}5. 构造函数与运算符重载总结
// 运算符重载总结class Complex {public: // 加法运算符重载(成员函数) Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); }
// 前置++(成员函数) Complex& operator++() { ++real; return *this; }
// 后置++(成员函数) Complex operator++(int) { Complex temp = *this; ++(*this); return temp; }
private: double real, imag;};6. 常见问题
Q1: 任何类都要有不必提供参数的构造函数(默认缺省构造函数)。
A1: 错误。 只有在类没有显式定义任何构造函数时,编译器才会自动生成默认构造函数。如果类中定义了带参数的构造函数,则必须显式定义无参构造函数。
Q2: 私有继承中,对于基类中的所有成员,派生类的成员函数都不可直接访问。
A2: 错误。 私有继承会将基类的公有和保护成员变为派生类的私有成员,派生类的成员函数可以访问这些成员,但不能访问基类的私有成员。
Q3: 在C++中,可以声明虚构造函数和虚析构函数。
A3: 错误。 构造函数不能是虚函数(因为对象在构造前虚表指针未初始化),但析构函数通常应声明为虚函数,以确保通过基类指针删除派生类对象时正确调用析构函数链。
Q4: 运算符重载可以改变运算符的优先级吗?
A4: 不可以。 运算符重载不能改变运算符的优先级、结合性或操作数个数,只能改变其操作对象的类型和行为。