一、指针数组和数组指针的区别

例如 int(*p)[5]int *p[5] 是两种不同的声明方式,它们在语法和语义上有着显著的区别。

int(*p)[5]

  • 解释: 这是一个指向包含5个整数的数组的指针。

  • 详细解释: p 是一个指针,它指向一个包含5个整数的数组。换句话说,p 可以指向任何具有5个整数的数组。

  • 示例:

int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // p 指向 arr 数组

int *p[5]

  • 解释: 这是一个包含5个指向整数的指针的数组。

  • 详细解释: p 是一个数组,这个数组有5个元素,每个元素都是一个指向整数的指针。换句话说,p 是一个指针数组,其中每个元素都可以指向一个整数。

  • 示例:

int a = 1, b = 2, c = 3, d = 4, e = 5;
int *p[5] = {&a, &b, &c, &d, &e}; // p 是一个指针数组,每个元素指向一个整数

总结

  • int(*p)[5] 是一个指向包含5个整数的数组的指针。

  • int *p[5] 是一个包含5个指向整数的指针的数组。

这两种声明方式在用途和应用场景上有很大的不同,理解它们的区别对于正确使用指针和数组至关重要。

二、纯虚函数的定义和说明

纯虚函数的定义

纯虚函数是在基类中声明但不定义的虚函数,表示该函数没有具体实现,而且强制派生类必须实现这个函数。

纯虚函数的语法是在函数声明后加 = 0,例如:

virtual void func() = 0;

包含纯虚函数的类称为抽象类,抽象类不能直接实例化。

举例:动物类和派生类

假设我们有一个“动物”的基类,基类提供一个虚函数 makeSound(),表示动物发出的声音。但因为每种动物发出的声音不同,所以我们不在基类中定义这个函数,而是让派生类(比如狗、猫)去具体实现。

代码示例:

#include <iostream>
using namespace std;

// 抽象类:Animal(基类)
class Animal {
public:
    // 纯虚函数
    virtual void makeSound() = 0;

    // 普通成员函数
    void eat() {
        cout << "This animal is eating." << endl;
    }
};

// 派生类:Dog
class Dog : public Animal {
public:
    // 实现纯虚函数
    void makeSound() override {
        cout << "Woof! Woof!" << endl;  // 狗的叫声
    }
};

// 派生类:Cat
class Cat : public Animal {
public:
    // 实现纯虚函数
    void makeSound() override {
        cout << "Meow! Meow!" << endl;  // 猫的叫声
    }
};

int main() {
    // Animal a;  // 错误!抽象类不能实例化

    // 创建 Dog 和 Cat 对象
    Dog dog;
    Cat cat;

    // 调用纯虚函数的实现
    dog.makeSound();  // 输出:Woof! Woof!
    cat.makeSound();  // 输出:Meow! Meow!

    // 调用基类的普通函数
    dog.eat();  // 输出:This animal is eating.
    cat.eat();  // 输出:This animal is eating.

    return 0;
}

关键点解析

  • 纯虚函数定义
    在基类 Animal 中定义了一个纯虚函数:

virtual void makeSound() = 0;

它没有实现,表示“每种动物发出的声音由具体的动物类决定”。

  • 抽象类不能实例化
    因为 Animal 类包含纯虚函数,所以它是一个抽象类,无法直接创建对象:

Animal a;  // 错误
  • 派生类必须实现纯虚函数
    在派生类 DogCat 中,必须实现 makeSound() 函数,否则派生类本身也会成为抽象类。

  • 多态性(可扩展性):
    如果将来需要增加其他动物(比如鸟类),只需要继承 Animal 类并实现 makeSound(),不需要修改基类的代码。

纯虚函数的应用场景

纯虚函数主要用于以下情况:

  1. 定义通用接口
    抽象类定义了派生类必须实现的接口,但不提供具体实现,强制派生类实现特定功能。

  2. 多态设计
    通过基类指针或引用调用派生类的实现(即运行时多态)。

  3. 框架设计
    纯虚函数在面向对象的框架设计中非常常见,用于提供可扩展的功能。

三、类模版

类模板允许用户为类定义一种通用模式,使得类中的成员变量成员函数的类型可以是任意类型(通常由用户在实例化时指定)。
通过使用类模板,可以创建更加通用、灵活的类,而无需为不同类型重复编写代码。

类模板的语法:

template <typename T>  // 声明模板,T 是类型参数
class ClassName {
    T data;            // T 是占位符,用于表示类型
public:
    ClassName(T value) : data(value) {}
    T getData() { return data; }
};
  • T 是一个模板类型参数,可以是任意类型(如 int, double, string 等)。

  • 在使用时,需要根据具体的类型来实例化模板类。

示例代码:实现一个类模板

#include <iostream>
using namespace std;

// 定义类模板
template <typename T>
class MyClass {
    T data;  // 数据成员类型为模板类型 T
public:
    MyClass(T value) : data(value) {}  // 构造函数
    T getData() { return data; }       // 返回数据成员的值
};

int main() {
    MyClass<int> intObj(10);          // 实例化模板类为 int 类型
    MyClass<double> doubleObj(3.14);  // 实例化模板类为 double 类型
    MyClass<string> stringObj("Hello"); // 实例化模板类为 string 类型

    cout << "intObj: " << intObj.getData() << endl;  // intObj: 10
    cout << "doubleObj: " << doubleObj.getData() << endl;  // doubleObj: 3.14
    cout << "stringObj: " << stringObj.getData() << endl;  // stringObj: Hello

    return 0;
}

四、析构函数

析构函数的定义

析构函数(Destructor) 是 C++ 类中的一种特殊成员函数,在对象的生命周期结束时自动被调用,用来执行清理工作,比如释放资源(内存、文件句柄等)。

析构函数的特点

  • 1. 函数名称与类名相同,但在前面加上一个 波浪号(~,例如:

~ClassName();
  • 2. 没有返回值(包括 void),也不能有参数,因此不能被重载。

  • 3. 作用

    • 释放对象占用的资源(如动态分配的内存、打开的文件、网络连接等)。

    • 执行清理操作,保证对象销毁时的状态一致性。

  • 4. 自动调用

    • 当对象生命周期结束时,析构函数会被自动调用(如对象超出作用域或被显式删除)。

析构函数的作用

析构函数通常用于:

  1. 释放动态分配的内存: 如果对象内部使用了 new 动态分配内存,析构函数应负责调用 delete

  2. 关闭文件或网络连接: 如果对象打开了文件或网络连接,析构函数可用来关闭它们。

  3. 清理临时资源: 比如释放锁、删除临时文件等。

析构函数的代码示例

示例 1:基本析构函数

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() {
        cout << "Constructor called." << endl;
    }
    ~MyClass() {
        cout << "Destructor called." << endl;
    }
};

int main() {
    MyClass obj;  // 创建对象时调用构造函数
    // 对象超出作用域时(main 函数结束),自动调用析构函数
    return 0;
}

输出结果

Constructor called.
Destructor called.

解析

  • 对象 obj 被创建时,调用构造函数。

  • 程序运行到对象超出作用域时,自动调用析构函数。

示例 2:释放动态内存

#include <iostream>
using namespace std;

class MyClass {
private:
    int* data;  // 动态分配的内存
public:
    // 构造函数:分配内存
    MyClass() {
        data = new int;
        *data = 42;  // 给动态分配的内存赋值
        cout << "Constructor: Memory allocated." << endl;
    }

    // 析构函数:释放内存
    ~MyClass() {
        delete data;  // 释放动态分配的内存
        cout << "Destructor: Memory released." << endl;
    }
};

int main() {
    MyClass obj;  // 创建对象
    return 0;  // 程序结束时自动调用析构函数
}

复制代码

输出结果

Constructor: Memory allocated.
Destructor: Memory released.

解析

  • 构造函数分配了动态内存(new),析构函数释放了内存(delete)。

  • 析构函数保证程序结束时不会造成内存泄漏。


注意事项

  • 不要在析构函数中显式调用析构函数
    析构函数会自动调用,显式调用会导致不可预期的行为(如递归调用)。

  • 虚析构函数

    • 当基类有析构函数且需要通过基类指针删除派生类对象时,析构函数应声明为虚函数,以确保正确调用派生类的析构函数。

virtual ~Base();

  • 避免双重释放: 确保析构函数释放的资源没有被重复释放(如重复调用 delete)。


总结

  • 1. 析构函数是用来清理资源的特殊函数,在对象生命周期结束时自动调用。

  • 2. 它保证了对象销毁时的资源释放,是构造函数的“对称”操作。

  • 3. 如果你的类中使用了动态内存分配或其他需要清理的资源,那么实现一个析构函数是非常重要的!

五、继承的三种方式及访问权限的变化

继承方式

基类的 public

基类的 protected

基类的 private

公有继承 (public)

public

protected

不可访问

保护继承 (protected)

protected

protected

不可访问

私有继承 (private)

private

private

不可访问

  1. public继承

    • 基类的 public 成员在派生类中仍是 public

    • 基类的 protected 成员在派生类中仍是 protected

    • 基类的 private 成员不可被派生类直接访问,但存在于派生类对象中(可以通过基类的 publicprotected 成员间接访问)。

  2. protected继承

    • 基类的 public 成员在派生类中变为 protected

    • 基类的 protected 成员在派生类中仍是 protected

    • 基类的 private 成员不可被派生类直接访问。

  3. private继承

    • 基类的 public 成员在派生类中变为 private

    • 基类的 protected 成员在派生类中变为 private

    • 基类的 private 成员不可被派生类直接访问。