栈/堆/static/虚表

发布于:2025-03-22 ⋅ 阅读:(16) ⋅ 点赞:(0)

在 C++ 里,栈空间主要用来存放局部变量、函数调用信息等。下面为你介绍栈空间在 C++ 里的运用方式。

1. 局部变量的使用

在函数内部定义的变量会被存于栈空间,当函数执行结束,这些变量会自动被销毁。

#include <iostream>

void exampleFunction() {
    // 定义一个局部变量,存于栈空间
    int localVar = 10;
    std::cout << "Local variable value: " << localVar << std::endl;
}

int main() {
    exampleFunction();
    return 0;
}

2. 函数调用栈

每次调用函数时,系统会在栈上为该函数创建一个栈帧,用来保存函数的局部变量、参数、返回地址等信息。函数返回时,对应的栈帧会被销毁。

#include <iostream>

void func2(int value) {
    std::cout << "Value in func2: " << value << std::endl;
}

void func1() {
    int localVar = 20;
    func2(localVar);
}

int main() {
    func1();
    return 0;
}

3. 递归调用

递归函数是在函数内部调用自身,每次递归调用都会在栈上创建新的栈帧。要注意递归深度,防止栈溢出。

#include <iostream>

// 递归计算阶乘
int factorial(int n) {
    if (n == 0 || n == 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

int main() {
    int num = 5;
    std::cout << "Factorial of " << num << " is: " << factorial(num) << std::endl;
    return 0;
}

栈空间使用的注意事项

  • 栈溢出:若栈空间使用过多,例如递归过深或者局部变量占用空间过大,就会引发栈溢出错误。
  • 生命周期:栈上的变量生命周期局限于定义它的代码块,出了代码块就会被销毁。

在 C++ 中,堆空间用于动态分配内存,可在程序运行时根据需要分配和释放内存。下面详细介绍堆空间的使用方法。

1. 使用 new 和 delete 操作符进行内存分配和释放

  • 分配单个对象:使用 new 操作符为单个对象分配内存,使用 delete 操作符释放内存。
#include <iostream>

int main() {
    // 在堆上分配一个 int 类型的对象
    int* ptr = new int;
    *ptr = 42;
    std::cout << "Value: " << *ptr << std::endl;

    // 释放堆上的内存
    delete ptr;
    return 0;
}
  • 分配数组:使用 new[] 操作符为数组分配内存,使用 delete[] 操作符释放内存。
#include <iostream>

int main() {
    // 在堆上分配一个包含 5 个 int 元素的数组
    int* arr = new int[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = i;
    }

    // 输出数组元素
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    // 释放堆上的数组内存
    delete[] arr;
    return 0;
}

2. 使用智能指针管理堆内存

为了避免手动管理内存带来的内存泄漏问题,C++ 提供了智能指针。常用的智能指针有 std::unique_ptrstd::shared_ptr 和 std::weak_ptr

  • std::unique_ptr:独占所指向的对象,同一时间只能有一个 std::unique_ptr 指向该对象。
#include <iostream>
#include <memory>

int main() {
    // 使用 std::unique_ptr 管理堆上的 int 对象
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    std::cout << "Value: " << *ptr << std::endl;

    // 不需要手动释放内存,std::unique_ptr 会在离开作用域时自动释放
    return 0;
}
  • std::shared_ptr:多个 std::shared_ptr 可以共享同一个对象,使用引用计数来管理对象的生命周期。
#include <iostream>
#include <memory>

int main() {
    // 使用 std::shared_ptr 管理堆上的 int 对象
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    std::shared_ptr<int> ptr2 = ptr1;

    std::cout << "Value: " << *ptr2 << std::endl;

    // 当所有指向该对象的 std::shared_ptr 都被销毁时,对象会自动释放
    return 0;
}

3. 自定义类对象的堆内存管理

在自定义类中,需要注意析构函数的实现,确保在对象销毁时正确释放堆上的内存。

#include <iostream>

class MyClass {
private:
    int* data;
public:
    MyClass(int value) {
        // 在构造函数中分配堆内存
        data = new int(value);
    }

    ~MyClass() {
        // 在析构函数中释放堆内存
        delete data;
    }

    int getValue() const {
        return *data;
    }
};

int main() {
    MyClass obj(42);
    std::cout << "Value: " << obj.getValue() << std::endl;

    return 0;
}

堆空间使用的注意事项

  • 内存泄漏:如果使用 new 分配了内存,但没有使用 delete 或 delete[] 释放,或者智能指针管理不当,会导致内存泄漏。
  • 悬空指针:释放内存后,指针仍然指向原来的内存地址,使用这样的指针会导致未定义行为。

4.C/C++ 中static的作用

  • 静态局部变量:在函数内部用static修饰的局部变量,存储在全局数据区而非栈区。它的生命周期贯穿整个程序运行期间,在程序执行到其声明处时首次初始化,之后的函数调用不再初始化;若未显式初始化,会自动初始化为 0 。其作用域仍在定义它的函数内部。常用于记录函数调用次数或状态
  • 静态全局变量:在全局变量前加static,该变量存储在全局数据区,作用域为声明它的文件,其他文件即使使用extern声明也无法访问。可提高程序的封装性,防止全局变量被意外修改,还能避免多文件项目中不同文件同名全局变量的命名冲突。比如在一个多人协作的大型项目中,每个源文件里的静态全局变量只在本文件内有效,不同文件可使用相同变量名。
  • 静态函数:被static修饰的函数只能在声明它的文件中可见和调用,不能被其他文件使用。有助于提高程序的封装性,减少函数被其他文件错误调用的风险
  • 类的静态数据成员:在类内数据成员声明前加static,该数据成员为类的所有对象共享,在程序中只有一份拷贝,存储在全局数据区。
  • 类的静态成员函数:用static修饰的类成员函数,属于类本身而非类的对象,没有this指针,不能直接访问非静态成员变量和非静态成员函数。可在创建对象前调用,常作为工具函数或用于访问静态数据成员。

--------------------------------------------------------------------------------------------------------------------------------------关于一个函数的地址这里可以提到的是。函数名就是一个函数的地址!但是不能查地址的时候忽略作用域

关于函数指针问题:typedef void (*PluginFunction)();首先PluginFunction它是这个指针的别名,最后的一个括号说明这个指针可以用于没有参数的函数!

#include <iostream>

// 定义一个插件函数
void pluginFunction() {
    std::cout << "This is a plugin function." << std::endl;
}

// 使用函数指针类型表示插件函数类型
typedef void (*PluginFunction)();

// 主程序加载插件并调用插件函数
void loadAndCallPlugin(PluginFunction func) {
    func();
}

int main() {
    loadAndCallPlugin(pluginFunction);
    return 0;
}

从这个图也可以看出虚表也是存在于常量区代码段的位置!

5.多继承的虚表

//多继承的虚函数表
class Base1 {
public:
	virtual void func1() { std::cout << "Base1::func1" << std::endl; }
	virtual void func2() { std::cout << "Base1::func2" << std::endl; }
private:
	int b1;
};

class Base2 {
public:
	virtual void func1() { std::cout << "Base2::func1" << std::endl; }
	virtual void func2() { std::cout << "Base2::func2" << std::endl; }
private:
	int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { std::cout << "Derive::func1" << std::endl; }
	virtual void func3() { std::cout << "Derive::func3" << std::endl; }
private:
	int d1;
};

derive内有两个虚表的原因:分别是Base1内的一个虚表,Base2一个续表,而func3会通过编译器自动放在一个已有的虚表中。

C++ 编译器为每个包含虚函数的类生成虚函数表,目的是为了实现运行时多态。当一个类继承自多个基类,且基类都有虚函数时,该派生类会继承基类的虚表结构。编译器通常会复用已有的虚表,将新的虚函数指针添加到合适的虚表中,而不是为每个新虚函数单独创建一个虚表。这样可以节省内存空间,并保持虚函数调用机制的一致性。