MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ static函数与普通函数的特征区别

2023-07-073.1k 阅读

一、作用域与可见性

  1. 普通函数 普通函数在全局作用域中声明时,其作用域是从声明处到文件结束。如果在函数内部声明函数(C++ 标准不允许在函数内部定义函数,但可以声明),其作用域仅限于该函数块。 例如,在下面的代码中:
// 在全局作用域声明普通函数
void normalFunction() {
    std::cout << "This is a normal function." << std::endl;
}

int main() {
    normalFunction(); // 可以在 main 函数中调用全局作用域的普通函数
    return 0;
}

这里的 normalFunction 函数在全局作用域声明,整个源文件都可以访问它。

  1. static 函数 对于 static 函数,如果在文件作用域声明(即不在任何类或函数内部),它的作用域仅限于该文件。这意味着其他文件无法直接访问该 static 函数,即使在其他文件中进行声明也不行。
// 在文件作用域声明 static 函数
static void staticFunction() {
    std::cout << "This is a static function." << std::endl;
}

int main() {
    staticFunction(); // 在本文件内可以调用
    return 0;
}

在另一个文件中,即使声明 void staticFunction(); 并试图调用,链接时也会报错,因为 static 函数的作用域被限制在了声明它的文件中。

二、存储方式

  1. 普通函数 普通函数存储在代码段(text segment)中。在程序运行时,代码段是只读的,其中存放着程序的可执行代码。每个普通函数在代码段中都有对应的机器指令序列。当函数被调用时,系统通过函数指针跳转到相应的机器指令处执行。 例如,对于以下简单的普通函数:
void normalFunction() {
    int a = 10;
    std::cout << "Value of a: " << a << std::endl;
}

当程序编译时,normalFunction 的机器指令会被放置在代码段中。在运行时,当 normalFunction 被调用,控制权会转移到代码段中该函数对应的指令位置执行。

  1. static 函数 static 函数同样存储在代码段中,与普通函数在存储位置上并无本质区别。然而,由于其作用域限制在文件内,编译器在处理 static 函数时可以进行更优化的代码生成。因为编译器知道该函数不会被其他文件调用,它可以对函数内的代码进行更激进的优化,例如内联(inlining)等优化手段可能更容易应用,从而提高程序的执行效率。

三、链接属性

  1. 普通函数 普通函数具有外部链接属性(external linkage)。这意味着在多个源文件组成的项目中,不同文件中的同名普通函数(具有相同的函数签名)会被视为同一个函数。当链接器进行链接操作时,它会将这些同名函数的定义合并为一个。 假设有两个源文件 file1.cppfile2.cppfile1.cpp
void commonFunction() {
    std::cout << "This is from file1." << std::endl;
}

file2.cpp

void commonFunction();
int main() {
    commonFunction(); // 调用 file1.cpp 中的 commonFunction
    return 0;
}

在链接阶段,链接器会将 file1.cppcommonFunction 的定义与 file2.cpp 中对该函数的调用链接起来,程序可以正常运行并输出 This is from file1.

  1. static 函数 static 函数具有内部链接属性(internal linkage)。不同文件中声明的同名 static 函数是相互独立的,它们在各自的文件中拥有独立的定义和实现。链接器不会将不同文件中的同名 static 函数进行合并。 例如,有 file3.cppfile4.cppfile3.cpp
static void sameNameStaticFunction() {
    std::cout << "This is from file3." << std::endl;
}

file4.cpp

static void sameNameStaticFunction() {
    std::cout << "This is from file4." << std::endl;
}

int main() {
    sameNameStaticFunction(); // 调用 file4.cpp 中的 sameNameStaticFunction
    return 0;
}

这里 file3.cppfile4.cpp 中的 sameNameStaticFunction 虽然名字相同,但它们是完全独立的函数,在各自文件中发挥作用,互不干扰。

四、类成员函数中的 static 函数与普通成员函数

  1. 普通成员函数 在类中,普通成员函数与类的特定对象实例相关联。每个对象都有自己的一组成员变量,普通成员函数可以访问和修改这些成员变量。普通成员函数通过 this 指针来访问调用它的对象的成员变量。 例如:
class MyClass {
private:
    int data;
public:
    MyClass(int value) : data(value) {}
    void normalMemberFunction() {
        std::cout << "Data value: " << data << std::endl;
    }
};

int main() {
    MyClass obj(10);
    obj.normalMemberFunction(); // 通过对象调用普通成员函数
    return 0;
}

normalMemberFunction 中,this 指针指向调用该函数的 obj 对象,从而可以访问 objdata 成员变量。

  1. static 成员函数 static 成员函数属于类本身,而不是类的某个特定对象实例。因此,static 成员函数不能直接访问非 static 成员变量,因为非 static 成员变量依赖于对象实例。static 成员函数没有 this 指针,因为它不与任何特定对象相关联。
class MyStaticClass {
private:
    static int staticData;
    int nonStaticData;
public:
    MyStaticClass(int value) : nonStaticData(value) {}
    static void staticMemberFunction() {
        std::cout << "Static data value: " << staticData << std::endl;
        // 以下代码会报错,因为 static 成员函数不能访问非 static 成员变量
        // std::cout << "Non - static data value: " << nonStaticData << std::endl;
    }
};

int MyStaticClass::staticData = 20;

int main() {
    MyStaticClass::staticMemberFunction(); // 直接通过类名调用 static 成员函数
    return 0;
}

在上述代码中,staticMemberFunction 只能访问 staticData 这样的 static 成员变量。如果要访问 nonStaticData,需要通过传递对象实例来间接访问。

五、调用方式

  1. 普通函数 普通函数的调用方式取决于其作用域。全局作用域的普通函数可以直接通过函数名调用。如果是类的普通成员函数,则需要通过类的对象实例来调用。 例如,全局普通函数:
void globalNormalFunction() {
    std::cout << "Global normal function." << std::endl;
}

int main() {
    globalNormalFunction(); // 直接调用全局普通函数
    return 0;
}

对于类的普通成员函数:

class CallClass {
public:
    void normalMemberCall() {
        std::cout << "Normal member function call." << std::endl;
    }
};

int main() {
    CallClass obj;
    obj.normalMemberCall(); // 通过对象调用类的普通成员函数
    return 0;
}
  1. static 函数 文件作用域的 static 函数在本文件内直接通过函数名调用。类的 static 成员函数既可以通过类名直接调用,也可以通过对象实例调用(虽然通过对象实例调用不太符合 static 成员函数的设计初衷,但语法上是允许的)。 例如,文件作用域的 static 函数:
static void fileStaticFunction() {
    std::cout << "File - scope static function." << std::endl;
}

int main() {
    fileStaticFunction(); // 在本文件内调用文件作用域的 static 函数
    return 0;
}

对于类的 static 成员函数:

class StaticCallClass {
public:
    static void staticMemberCall() {
        std::cout << "Static member function call." << std::endl;
    }
};

int main() {
    StaticCallClass::staticMemberCall(); // 通过类名调用 static 成员函数
    StaticCallClass obj;
    obj.staticMemberCall(); // 通过对象实例调用 static 成员函数(不太常用)
    return 0;
}

六、内存管理相关影响

  1. 普通函数 普通函数本身不涉及直接的内存管理问题(除了函数内部局部变量的栈内存管理)。当普通函数被调用时,会在栈上为其局部变量分配内存,函数返回时,这些栈上的内存会被自动释放。 例如:
void normalMemoryFunction() {
    int localVar = 10;
    std::cout << "Local variable value: " << localVar << std::endl;
}

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

normalMemoryFunction 执行时,localVar 在栈上分配内存,函数结束后,localVar 占用的栈内存被释放。

  1. static 函数 static 函数同样不直接涉及复杂的内存管理问题,其内部局部变量的内存管理与普通函数类似。然而,当 static 函数内部存在 static 局部变量时,情况有所不同。static 局部变量在函数第一次调用时初始化,并且其生命周期贯穿整个程序运行期间,而不是在每次函数调用时创建和销毁。
static void staticMemoryFunction() {
    static int staticLocalVar = 0;
    staticLocalVar++;
    std::cout << "Static local variable value: " << staticLocalVar << std::endl;
}

int main() {
    for (int i = 0; i < 5; i++) {
        staticMemoryFunction();
    }
    return 0;
}

在上述代码中,staticLocalVarstaticMemoryFunction 第一次调用时初始化,之后每次调用函数,其值都会保留并递增。

七、多态性与虚函数特性

  1. 普通函数 普通函数不支持多态性。多态性在 C++ 中主要通过虚函数和指针或引用实现。普通函数在编译时就确定了调用哪个函数,不会根据对象的实际类型在运行时动态选择函数版本。 例如:
class Base {
public:
    void normalFunction() {
        std::cout << "Base normal function." << std::endl;
    }
};

class Derived : public Base {
public:
    void normalFunction() {
        std::cout << "Derived normal function." << std::endl;
    }
};

int main() {
    Base baseObj;
    Derived derivedObj;
    Base* ptr = &derivedObj;
    ptr->normalFunction(); // 调用的是 Base 类的 normalFunction,不体现多态
    return 0;
}

这里即使 ptr 指向 Derived 对象,调用的仍然是 Base 类的 normalFunction,因为普通函数在编译时就绑定了调用。

  1. static 函数 static 函数同样不支持多态性。由于 static 函数属于类本身而不是对象实例,没有 this 指针,也就无法根据对象的实际类型在运行时动态选择函数版本。
class StaticBase {
public:
    static void staticFunction() {
        std::cout << "StaticBase static function." << std::endl;
    }
};

class StaticDerived : public StaticBase {
public:
    static void staticFunction() {
        std::cout << "StaticDerived static function." << std::endl;
    }
};

int main() {
    StaticBase* staticPtr = new StaticDerived();
    staticPtr->staticFunction(); // 调用的是 StaticBase 的 staticFunction,不体现多态
    delete staticPtr;
    return 0;
}

在上述代码中,即使 staticPtr 指向 StaticDerived 对象,调用的仍然是 StaticBase 类的 staticFunction,因为 static 函数不支持多态。

八、继承与覆盖特性

  1. 普通函数 在继承体系中,派生类可以定义与基类同名的普通函数,但这不是覆盖(override),而是隐藏(hide)。当通过基类指针或引用调用普通函数时,只会调用基类的函数版本,不会根据对象实际类型调用派生类的函数版本。 例如:
class BaseInherit {
public:
    void normalFunction() {
        std::cout << "Base normal function in inheritance." << std::endl;
    }
};

class DerivedInherit : public BaseInherit {
public:
    void normalFunction() {
        std::cout << "Derived normal function in inheritance." << std::endl;
    }
};

int main() {
    BaseInherit* basePtr = new DerivedInherit();
    basePtr->normalFunction(); // 调用的是 BaseInherit 的 normalFunction
    delete basePtr;
    return 0;
}

这里派生类的 normalFunction 隐藏了基类的 normalFunction,通过基类指针调用时不会调用到派生类的版本。

  1. static 函数 static 函数在继承体系中也不存在覆盖关系。派生类定义与基类同名的 static 函数同样是隐藏基类的 static 函数。通过基类指针或引用调用 static 函数时,调用的是基类的 static 函数版本。
class StaticBaseInherit {
public:
    static void staticFunction() {
        std::cout << "StaticBase static function in inheritance." << std::endl;
    }
};

class StaticDerivedInherit : public StaticBaseInherit {
public:
    static void staticFunction() {
        std::cout << "StaticDerived static function in inheritance." << std::endl;
    }
};

int main() {
    StaticBaseInherit* staticBasePtr = new StaticDerivedInherit();
    staticBasePtr->staticFunction(); // 调用的是 StaticBaseInherit 的 staticFunction
    delete staticBasePtr;
    return 0;
}

在这个例子中,StaticDerivedInheritstaticFunction 隐藏了 StaticBaseInheritstaticFunction,通过基类指针调用时不会调用到派生类的版本。

九、异常处理与错误处理相关特性

  1. 普通函数 普通函数在异常处理方面遵循 C++ 的标准异常处理机制。函数可以抛出异常,调用者可以捕获这些异常进行处理。如果函数内部没有捕获异常,异常会向上层调用栈传递,直到被捕获或者导致程序终止。 例如:
void normalErrorFunction() {
    int num = 10;
    if (num < 20) {
        throw std::runtime_error("Number is less than 20");
    }
}

int main() {
    try {
        normalErrorFunction();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

normalErrorFunction 中抛出 std::runtime_error 异常,在 main 函数中通过 try - catch 块捕获并处理。

  1. static 函数 static 函数在异常处理方面与普通函数并无本质区别。它同样可以抛出异常,调用者也可以捕获异常。例如:
static void staticErrorFunction() {
    int num = 5;
    if (num < 10) {
        throw std::logic_error("Number is less than 10");
    }
}

int main() {
    try {
        staticErrorFunction();
    } catch (const std::logic_error& e) {
        std::cerr << "Caught static function exception: " << e.what() << std::endl;
    }
    return 0;
}

staticErrorFunction 抛出 std::logic_error 异常,在 main 函数中被捕获和处理,与普通函数的异常处理流程一致。

十、应用场景对比

  1. 普通函数

    • 通用功能模块:当需要实现一些通用的功能,并且这些功能可能在多个文件中被调用时,使用普通函数。例如,数学计算库中的函数,如求平方根、三角函数计算等函数,通常设计为普通函数,因为它们需要在不同的项目模块中被广泛调用。
    • 对象特定行为:在类中,普通成员函数用于实现与对象实例紧密相关的行为。比如一个 Person 类的 eat 函数,它会根据每个 Person 对象的不同状态(如饥饿程度等成员变量)进行操作,这种情况下使用普通成员函数。
  2. static 函数

    • 文件私有功能:当某个功能只在一个文件内部使用,不希望被其他文件访问时,使用 static 函数。例如,在一个实现复杂算法的源文件中,可能有一些辅助函数,这些函数只在该文件内对主要算法起辅助作用,将它们声明为 static 函数可以避免命名冲突,同时提高代码的封装性。
    • 类的工具性操作:在类中,static 成员函数适用于实现与类相关但不依赖于具体对象实例的工具性操作。例如,一个 MathUtils 类中的 static 成员函数 calculatePi(),用于计算圆周率,这个操作不依赖于任何 MathUtils 对象实例,适合用 static 成员函数实现。