Skip to content

C++ 基础

静态局部变量,全局变量,局部变量的特点,以及使用场景?

简要回答

静态局部变量,全局变量,局部变量的使用场景

  • 静态局部变量:函数定义,用static修饰,生命周期为整个程序,该变量无法在作用域外修改,只能被初始化一次,之后每次调用都保持上以上调用结束时的值,存储在数据段。
  • 全局变量:函数定义,生命周期是整个程序运行区间,程序中可以在任何地方访问,创建在数据区。
  • 局部变量:函数定义,作用域和生命周期只能在声明该变量的函数体内或者类内访问,每次调用重新创建,生命周期随着函数的返回而结束,创建在上。

静态局部变量,全局变量,局部变量的使用场景如下表所示

分类局部变量静态局部变量全局变量
作用域当前函数或者代码块内当前函数内部(外部不能访问)整个程序内
生命周期每次进入函数创建,用完就没程序一运行就存在,一直到程序结束程序启动时创建,程序结束才销毁
初始化行为不赋值的话值不确定(是随机的)初始化一次(默认0),之后值会保留默认初始化为0
存储位置栈区(stack),速度快但不持久数据段或 BSS 段(非栈),可长期保存数据段或 BSS 段,生命周期长
适合场景做临时运算,比如循环里的变量、临时数组等想在函数里“记住之前的值”,如统计次数、递归深度控制等整个程序都需要共享的变量,比如配置信息、缓冲区等

详细回答

全局变量

全局变量定义在所有函数外面,整个程序都能访问,它从程序启动时就存在。

若在其他文件中使用,需要用extern关键字声明,默认也会被初始化为0,和静态局部变量一样存储在静态存储区

若用"static"修饰的全局变量,仅限当前文件使用,避免与其他文件同名变量冲突。

静态局部变量

在函数内部定义,使用static关键字声明,比如"static int count",其生命周期不随函数调用结束而销毁,可保留上次的值

第一次调用函数时初始化后,之后每次调用都不会重新初始化,而是保持上次的值,这种变量被放在一个特殊的内存区域(静态存储区),所以函数执行完也不会被销毁。

默认会被自动初始化为0,适合用来做计数器或者缓存一些需要在多次函数调用之间保持的数据。

局部变量

在函数或代码块内部定义。这种变量的特点是“随用随弃”,随函数调用创建、执行结束后销毁,存储在内存中,存取速度很快,如果不初始化,它的值会是随机的(这点和全局变量、静态变量不同)。

需要注意的是,如果局部变量和全局变量同名,在函数内部会优先使用局部变量,相当于暂时"屏蔽"了同名的全局变量。

局部变量最适合存放一些临时数据,比如循环计数器、函数内部的中间计算结果等,用完就自动释放,不会占用额外内存

怎么用

全局变量是那种“很多地方都要用”的数据,比如配置参数、程序状态什么的,放在最外面,全局共享。但不建议乱用,容易让程序变得混乱、难维护,非必要别用它,用的话要写清楚、注释好

静态局部变量适合在一个函数里需要“记住上一次的值”的情况,比如要在函数里做一个计数器、记录某个初始化状态,只想执行一次,就加个 static,函数每次运行,它都能记得上一次的结果。

局部变量最常用,能用就用它,它只在当前函数里生效,用完就销毁,最干净、安全、不容易出错,处理临时数据、普通运算,直接写局部变量就够了。

静态局部变量,全局变量,局部变量的代码演示

  • 静态局部变量示例
cpp
  #include <iostream>
  using namespace std;
  void counter() {
      static int count = 0;  // 静态局部变量
      count++;
      cout << "Static local count: " << count << endl;
  }
  int main() {
      counter();  // 输出: Static local count: 1
     counter();  // 输出: Static local count: 2
     return 0;
 }
  • 全局变量示例
cpp
  #include <iostream>
  using namespace std;
  int globalVar = 100;  // 全局变量
  void modifyGlobal() {
      globalVar += 50;
      cout << "Modified global: " << globalVar << endl;
  }
  int main() {
      cout << "Initial global: " << globalVar << endl;  // 输出: Initial global: 100
     modifyGlobal();  // 输出: Modified global: 150
     return 0;
 }
  • 局部变量示例
cpp
  void calculate() {
      int localVar = 5;  // 局部变量
      localVar *= 2;
      cout << "Local variable: " << localVar << endl;  // 输出: Local variable: 10
      // localVar 在此处销毁
  }
  int main() {
      calculate();
      // cout << localVar;  // 错误:局部变量不可见
     return 0;
 }

知识拓展

  • 静态局部变量,全局变量,局部变量特点的示意图:

image

  • 内存四区的示意图

    image

指针和引用的区别?

简要回答

  • 指针:是一个变量,存储另一个变量的内存地址,使用时需要引用(*)访问目标值。可重新赋值指向其他对象,支持指针算术(如++),可为空(nullptr),指针可以有const

占用独立的内存(通常是4或8字节),需要手动管理动态内存。

  • 引用:是变量的别名,绑定后不能修改,且不能为空,使用时无需解引用,引用没有const
  • 区别

不存在指向空值的引⽤,但是存在指向空值的指针

详细回答

指针

指针是存储内存地址的变量,通过声明(如int* p = &a),其值为目标对象的地址

底层实现上,指针变量占用独立内存(32位系统4字节,64位系统8字节)。

可以重新赋值(如p = &b),支持指针算术(如p++移动地址)

可为nullptr,表示不指向有效对象,但需注意,未初始化或未判空的指针可能导致野指针,可能引发崩溃。

指针的const的修饰,其取决于const的修饰位置

  1. const int *p:const修饰的是指针指向的对象,不能通过指针修改对象的值,但可以改变指针本身指向的对象。
  2. int *const p:const修饰的是指针本身,不能改变指针指向的对象,但可以改变指针本身。
  3. const int *const p:const修饰的是指针指向的对象和指针本身,不能通过指针修改对象的值,也不能改变指针本身。

指针的灵活性使其在动态内存分配、数组操作、字符串处理及复杂数据结构(如链表、树)中非常有用,但需手动管理内存,否则容易造成内存泄漏

引用

引用是变量的别名(如int& r = a),绑定后与目标共享内存,无独立存储空间

编译时符号表记录引用目标的地址,绑定关系一旦建立,无法更改。

引用必须初始化且不能为空,语法上更简洁,直接操作目标变量(如r = 10)。

引用没有const修饰,但可以绑定到const对象(如const int&)。

由于引用绑定后不可更改,生命周期与绑定对象一致无需手动管理内存,因此在函数参数传递、返回值优化等场景中更安全高效。

指针和引用演示

cpp
#include <iostream>
using namespace std;

int main() {
    // ====== 初始化对比 ======
    int a = 10;
    int* ptr;       // 指针可以延迟初始化
    ptr = &a;       // 现在指向a
    ptr = nullptr;  // 可以设为空指针

   int& ref = a;   // 引用必须初始化且不能为空
   // int& ref2;    // 错误:引用必须初始化
   // int& ref3 = nullptr; // 错误:不能绑定到空

   // ====== 重新绑定对比 ======
   int b = 20;
   ptr = &b;       // 指针可以重新指向b
   // &ref = b;     // 错误:引用不能重新绑定

   // ====== 内存占用对比 ======
   cout << "指针大小: " << sizeof(ptr) << " bytes" << endl; // 4或8字节
   cout << "引用大小: " << sizeof(ref) << " bytes" << endl;  // 显示a的大小

   // ====== 操作方式对比 ======
   *ptr = 30;      // 指针需要解引用
   ref = 40;       // 引用直接操作

    // ====== 安全性对比 ======
   if(ptr != nullptr) {  // 使用指针需要判空(行29)
       cout << *ptr << endl;
   }
   cout << ref << endl;  // 引用无需判空检查(行32)

   // ====== 动态内存管理 ======
   int* dynPtr = new int(50);  // 指针用于动态内存
   cout << *dynPtr << endl;
   delete dynPtr;              // 必须手动释放
   // 引用不能用于动态内存管理

   // ====== 函数参数传递 ======
   auto modifyByPtr = [](int* p) { if(p) *p += 1; };
   auto modifyByRef = [](int& r) { r += 1; };
   
   modifyByPtr(&a);
   modifyByRef(a);
   cout << "a = " << a << endl;  // 输出42

   //====== const的修饰 ======
   // 引用示例    
   const int ci = 10;  
   //int& r = ci;  // 错误:非const引用不能绑定到const对象
   const int& cr = ci;  //正确:const引用可以绑定到const对象
    //指针示例
   int a = 10;
   const int* p1 = &a;  // p1指向的值是const的
   //*p1 = 20;  //错误:不能通过p1修改a的值
   int* const p2 = &a;  // p2本身是const的,不能改变指向
   //p2 = &b;  //错误:不能改变p2指向的对象

   return 0;
}

知识拓展

  • 指针和引用的示意图:

    image

  • 面试官可能追问:在实际编程中,什么情况下应该使用指针,什么情况下应该使用引用?举例子说明。

    • 简答:
    1. 什么时候用指针? 遇到需要处理可能为NULL的情况(如可选参数时)遇到需要改变指向的对象(如遍历链表时)遇到需要动态内存分配(new/delete)遇到需要多级间接访问(如指针的指针)

    2. 什么时候用引用? 函数参数传递,特别是大型对象时,可以避免拷贝开销,遇到必须绑定到有效对象的场景时(如类成员引用)实现链式调用(如返回*this引用)还有运算符的重载

数据类型

整型 short int long 和 long long

C++ 整型数据长度标准

short 至少 16 位

int 至少与 short 一样长

long 至少 32 位,且至少与 int 一样长

long long 至少 64 位,且至少与 long 一样长

在使用8位字节的系统中,1 byte = 8 bit

很多系统都使用最小长度,short 为 16 位即 2 个字节,long 为 32 位即 4 个字节,long long 为 64 位即 8 个字节,int 的长度较为灵活,一般认为 int 的长度为 4 个字节,与 long 等长。

可以通过运算符 sizeof 判断数据类型的长度。例如:

cpp
cout << "int is " << sizeof (int) << " bytes. \n";
cout << "short is " << sizeof (short) << " bytes. \n";

头文件climits定义了符号常量:例如:INT_MAX 表示 int 的最大值,INT_MIN 表示 int 的最小值。

无符号类型

即为不存储负数值的整型,可以增大变量能够存储的最大值,数据长度不变。

int 被设置为自然长度,即为计算机处理起来效率最高的长度,所以选择类型时一般选用 int 类型。

关键字

const 关键字

常量指针 (Const Pointer)

指针本身是常量(顶层const)。声明时必须初始化,且初始化后其值(即存放的地址)不能再改变,不能再指向其他地址。但是,可以通过它修改其所指向对象的值(除非该对象本身也是常量)。

声明形式:int *const ptr = &some_int_var;

指向常量的指针(Pointer to const)

指针指向一个常量(底层const)。不能通过该指针修改其所指对象的值。但指针本身不是常量,可以指向其他地址(只要其他地址的类型兼容即可)。

声明形式:const int *ptr = &some_const_int_var;

网上对常量指针 和 指针常量 解释十分混乱,AI给出的也是时对时错,以上讲解,来自《C++ primer第五版》P57

static关键字的作用

static 是 C++ 中一个非常重要、但也容易被误解的关键字。 它在 不同位置 使用时,含义和作用完全不同。

本文系统讲解以下四种常见用法:

  • 静态函数(全局静态函数)
  • 静态成员变量
  • 静态成员函数
  • 静态局部变量

一、静态函数(全局 static 函数)

定义方式

cpp
static void foo() {
    // ...
}

作用与特点

  • 作用域仅限于当前源文件(.cpp)
  • 其他 .cpp 文件 无法访问
  • 本质:限制链接属性(internal linkage)

为什么要用?

  • 防止命名冲突
  • 实现模块级封装
  • 明确:该函数只在本文件内部使用

示例

cpp
// a.cpp
static int add(int a, int b) {
    return a + b;
}
cpp
// b.cpp
int main() {
    add(1, 2); // ❌ 编译错误:找不到 add
}

现代建议: 在 C++ 中,匿名命名空间 通常优于 static

cpp
namespace {
    int add(int a, int b) {
        return a + b;
    }
}

二、静态成员变量(static 成员变量)

定义特点

  • 属于类本身,而不是某个对象
  • 所有对象 共享同一份数据
cpp
class Counter {
public:
    static int count;
};

必须在类外定义(C++17 之前)

cpp
int Counter::count = 0;

内存与生命周期

  • 存放在 全局/静态区
  • 程序启动时创建
  • 程序结束时销毁

示例

cpp
class Student {
public:
    static int total;
};

int Student::total = 0;

Student a, b;
Student::total = 2;

常见用途

  • 统计对象数量
  • 全局配置
  • 类级别共享状态

三、静态成员函数(static 成员函数)

定义方式

cpp
class Math {
public:
    static int add(int a, int b) {
        return a + b;
    }
};

核心特性

  • 没有 this 指针
  • 只能访问:
    • 静态成员变量
    • 静态成员函数
  • 不能直接访问普通成员变量

调用方式

cpp
int x = Math::add(1, 2);

为什么没有 this?

因为静态成员函数 不依赖具体对象

cpp
Math m;
m.add(1,2);   // 可以,但本质仍是类级调用

使用场景

  • 工具函数(Utility)
  • 工厂方法(Factory)
  • 与类强相关、但不依赖对象状态的逻辑

四、静态局部变量(static 局部变量)

定义方式

cpp
void func() {
    static int x = 0;
    x++;
    std::cout << x << std::endl;
}

核心特性

  • 只初始化一次
  • 生命周期贯穿整个程序
  • 作用域仍然是函数内部

执行效果

cpp
func(); // 输出 1
func(); // 输出 2
func(); // 输出 3

与普通局部变量对比

特性普通局部变量静态局部变量
初始化每次调用只初始化一次
生命周期函数调用期间程序整个生命周期
存储区静态区

经典应用,单例模式(C++11 之后线程安全)

cpp
class Singleton {
public:
    static Singleton& instance() {
        static Singleton obj;
        return obj;
    }
};

五、四种 static 用法对比总结

用法作用范围生命周期核心作用
静态函数当前源文件程序生命周期文件级封装
静态成员变量程序生命周期类共享数据
静态成员函数程序生命周期类级逻辑
静态局部变量函数程序生命周期状态保持

六、总结以下,方面大家记忆

  • static 修饰函数:限制链接范围
  • static 成员变量:类级共享数据
  • static 成员函数:无 this,只能访问静态成员
  • static 局部变量:只初始化一次,状态可保留

const 关键字的作用

const关键字主要用于指定变量、指针、引用、成员函数等的性质

  1. 常量变量:声明常量,使变量的值不能被修改。
  2. 指针和引用:声明指向常量的指针,表示指针所指向的值是常量,不能通过指针修改。声明常量引用,表示引用的值是常量,不能通过引用修改。
cpp
const int* ptr = &constantValue;  // 指向常量的指针
const int& ref = constantValue;  // 常量引用
  1. 成员函数:用于声明常量成员函数,表示该函数不会修改对象的成员变量(对于成员变量是非静态的情况)。

  2. 常量对象:声明对象为常量,使得对象的成员变量不能被修改。

  3. 常引用参数:声明函数参数为常量引用,表示函数不会修改传入的参数。

  4. 常量指针参数:声明函数参数为指向常量的指针,表示函数不会通过指针修改传入的数据。

define 和 typedef 的区别

define
  1. 只是简单的字符串替换,没有类型检查
  2. 是在编译的预处理阶段起作用
  3. 可以用来防止头文件重复引用
  4. 不分配内存,给出的是立即数,有多少次使用就进行多少次替换
typedef
  1. 有对应的数据类型,是要进行判断的
  2. 是在编译、运行的时候起作用
  3. 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝

define 和 inline 的区别

1、define:

定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全。

2、inline:

inline是先将内联函数编译完成生成了函数体直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销;

内联函数是一种特殊的函数,会进行类型检查;

对编译器的一种请求,编译器有可能拒绝这种请求;

C++中inline编译限制:

  1. 不能存在任何形式的循环语句
  2. 不能存在过多的条件判断语句
  3. 函数体不能过于庞大
  4. 内联函数声明必须在调用语句之前

const和define的区别

const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:

  1. const生效于编译的阶段;define生效于预处理阶段。
  2. const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
  3. const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。

new 和 malloc的区别

1、new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

2、使用new操作符申请内存分配时无须指定内存块的大小,而malloc则需要显式地指出所需内存的尺寸。

3、opeartor new /operator delete可以被重载,而malloc/free并不允许重载。

4、new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会

5、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符

6、new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。

表格

constexpr 和 const

const 表示“只读”的语义,constexpr 表示“常量”的语义

constexpr 只能定义编译期常量,而 const 可以定义编译期常量,也可以定义运行期常量。

你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将一个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。

constexpr变量

复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。

必须使用常量初始化:

cpp
constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 1000000007;

如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关。

cpp
constexpr int* p1 = nullptr; // 编译期常量指针,指针值在编译期已知,语义类似 int* const
const int* p2 = nullptr;     // 底层 const,指向常量的指针:指针可改,指向的值不可改
int* const p3 = nullptr;     // 顶层 const,常量指针:指针本身不可改,指向的值可改
constexpr函数:

constexpr函数是指能用于常量表达式的函数。

函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。

cpp
constexpr int new() {return 42;}

为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。
constexpr和内联函数可以在程序中多次定义,一般定义在头文件。

constexpr 构造函数:

构造函数不能说const,但字面值常量类的构造函数可以是constexpr。

constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用 constexpr 修饰

constexpr的好处
  1. 为一些不能修改数据提供保障,写成变量则就有被意外修改的风险。
  2. 有些场景,编译器可以在编译期对constexpr的代码进行优化,提高效率。
  3. 相比宏来说,没有额外的开销,但更安全可靠。

volatile

定义:

[与const绝对对立的,是类型修饰符]影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。

作用:

指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问

使用场合:

在中断服务程序和cpu相关寄存器的定义

举例说明:

空循环:

cpp
for(volatile int i=0; i<100000; i++); // 它会执行,不会被优化掉

extern

定义:声明外部变量【在函数或者文件外部定义的全局变量】

static

作用:实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;默认初始化为0

前置++与后置++

cpp
self &operator++() {
    node = (linktype)((node).next);
    return *this;
}

const self operator++(int) {
    self tmp = *this;
    ++*this;
    return tmp;
}

为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默默给int指定为一个0

1、为什么后置返回对象,而不是引用

因为后置为了返回旧值创建了一个临时对象,在函数结束的时候这个对象就会被销毁,如果返回引用,那么我请问你?你的对象对象都被销毁了,你引用啥呢?

2、为什么后置前面也要加const

其实也可以不加,但是为了防止你使用i++++,连续两次的调用后置++重载符,为什么呢?

原因:

它与内置类型行为不一致;你无法活得你所期望的结果,因为第一次返回的是旧值,而不是原对象,你调用两次后置++,结果只累加了一次,所以我们必须手动禁止其合法化,就要在前面加上const。

3、处理用户的自定义类型

最好使用前置++,因为他不会创建临时对象,进而不会带来构造和析构而造成的格外开销。

std::atomic

问题:a++ 和 int a = b 在C++中是否是线程安全的?

答案:不是

例1:

a++:从C/C++语法的级别来看,这是一条语句,应该是原子的;但从编译器得到的汇编指令来看,其实不是原子的。

其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a代表的内存中

cpp
mov eax, dword ptr [a] # (1)
inc eax     # (2)
mov dword ptr [a], eax # (3)

现在假设i的值是0,有两个线程,每个线程对变量a的值都递增1,预想一下,其结果应该是2,可实际运行结构可能是1!是不是很奇怪?

分析如下:

cpp
int a = 0;
// 线程1(执行过程对应上文汇编指令(1)(2)(3))
void thread_func1() {
 a++;
}
// 线程2(执行过程对应上文汇编指令(4)(5)(6))
void thread_func2() {
 a++;
}

我们预想的结果是线程1和线程2的三条指令各自执行,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1执行完指令(1)和(2)后,eax寄存器中的值变为1,此时操作系统切换到线程2执行,执行指令(3)(4)(5),此时eax的值变为1;接着操作系统切回线程1继续执行,执行指令(6),得到a的最终结果1。

例2:

int a = b; 从C/C++语法的级别来看,这是条语句应该是原子的;但从编译器得到的汇编指令来看,由于现在计算机CPU架构体系的限制,数据不能直接从内存某处搬运到内存另外一处,必须借助寄存器中转,因此这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器(如eax)中,再从该寄存器搬运到变量a的内存地址中:

cpp
mov eax, dword ptr [b]
mov dword prt [a], eax

既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。

解决办法: C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是一个模板类型:

cpp
template<class T>
struct atomic:

我们可以传入具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型

cpp
// 初始化1
std::atomic<int> value;
value = 99;

// 初始化2
// 下面代码在Linux平台上无法编译通过(指在gcc编译器)
std::atomic<int> value = 99;
// 出错的原因是这行代码调用的是std::atomic的拷贝构造函数
// 而根据C++11语言规范,std::atomic的拷贝构造函数使用=delete标记禁止编译器自动生成
// g++在这条规则上遵循了C++11语言规范。

什么是函数指针,如何定义和使用场景

函数指针是指向函数的指针变量。它可以用于存储函数的地址,允许在运行时动态选择要调用的函数。

cpp
// 返回类型 (*指针变量名)(参数列表)
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    // 定义一个函数指针,指向一个接受两个int参数、返回int的函数
    int (*operationPtr)(int, int);

    // 初始化函数指针,使其指向 add 函数
    operationPtr = &add;

    // 通过函数指针调用函数
    int result = operationPtr(10, 5);
    cout << "Result: " << result << endl;

    // 将函数指针切换到 subtract 函数
    operationPtr = &subtract;

    // 再次通过函数指针调用函数
    result = operationPtr(10, 5);
    cout << "Result: " << result << endl;

    return 0;
}

使用场景:

  1. 回调函数: 函数指针常用于实现回调机制,允许将函数的地址传递给其他函数,以便在适当的时候调用。
  2. 函数指针数组: 可以使用函数指针数组实现类似于状态机的逻辑,根据不同的输入调用不同的函数。
  3. 动态加载库: 函数指针可用于在运行时动态加载库中的函数,实现动态链接库的调用。
  4. 多态实现: 在C++中,虚函数和函数指针结合使用,可以实现类似于多态的效果。
  5. 函数指针作为参数: 可以将函数指针作为参数传递给其他函数,实现一种可插拔的函数行为。
  6. 实现函数映射表: 在一些需要根据某些条件调用不同函数的情况下,可以使用函数指针来实现函数映射表。

函数指针和指针函数的区别

函数指针是指向函数的指针变量。可以存储特定函数的地址,并在运行时动态选择要调用的函数。通常用于回调函数、动态加载库时的函数调用等场景。

cpp
int add(int a, int b) {
    return a + b;
}

int (*ptr)(int, int) = &add;  // 函数指针指向 add 函数
int result = (*ptr)(3, 4);    // 通过函数指针调用函数

指针函数是一个返回指针类型的函数,用于返回指向某种类型的数据的指针。

cpp
int* getPointer() {
    int x = 10;
    return &x;  // 返回局部变量地址,不建议这样做
}

struct和Class的区别

相同点:

  • 如果结构体没有定义任何构造函数,编译器会生成默认的无参数构造函数。如果类没有定义任何构造函数,编译器也会生成默认的无参数构造函数。

不同点:

  • 通常,struct用于表示一组相关的数据,而class用于表示一个封装了数据和操作的对象,在实际使用中,可以根据具体的需求选择使用structclass。如果只是用来组织一些数据,而不涉及复杂的封装和继承关系,struct可能更直观;如果需要进行封装、继承等面向对象编程的特性,可以选择使用class
  • struct结构体中的成员默认是公有的(public)。类中的成员默认是私有的(private)。
  • struct 继承时默认使用公有继承。class 继承时默认使用私有继承。
cpp
// 使用 struct 定义
struct MyStruct {
    int x;  // 默认是 public
    void print() {
        cout << "Struct method" << endl;
    }
};

// 使用 class 定义
class MyClass {
public:  // 如果省略,默认是 private
    int y;
    void display() {
        cout << "Class method" << endl;
    }
};

C++强制类型转换

关键字:static_cast、dynamic_cast、reinterpret_cast和 const_cast

1. static_cast

没有运行时类型检查来保证转换的安全性

进行上行转换(把派生类的指针或引用转换成基类表示)是安全的

进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的。

使用:

  1. 用于基本数据类型之间的转换,如把int转换成char。

  2. 把任何类型的表达式转换成void类型。

2. dynamic_cast

在进行下行转换时,dynamic_cast具有类型检查(信息在虚函数中)的功能,比static_cast更安全。

转换后必须是类的指针、引用或者void*,基类要有虚函数,可以交叉转换。

dynamic本身只能用于存在虚函数的父子关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。

3. reinterpret_cast

可以将整型转换为指针,也可以把指针转换为数组;可以在指针和引用里进行肆无忌惮的转换,平台移植性比价差。

4. const_cast

常量指针转换为非常量指针,并且仍然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。去掉类型的const或volatile属性。

请解释C++中const关键字的作用,并给出使用场景。

回答

  1. 修饰普通变量时:指向的变量的值不能修改。
  2. 修饰指针时:分为两种情况:如果是 const int *代表的是常量指针,则该指针指向的值不可以改变;如果是int * const,代表的是指针常量,则该指针的指向不可以改变。
  3. 修饰函数时:const修饰函数是C++的一个扩展,它的目的是为了保证类的封装性。在该类的成员函数后加上const之后,该函数不能修改该类的成员变量
  4. 修饰函数传参时:修饰的参数在函数内不能修改。

注意事项

  1. 因为const修改的值不能修改,所以一开始就需要初始化。
  2. const只在该文件生效,如果要多个文件共享,需加上extern关键字。

解释一下C++中的extern关键字的作用。

【简要回答】

在 C++ 中,extern 关键字主要用于声明全局变量或函数,告知编译器这些变量或函数的定义位于其他文件中,从而实现跨文件共享。其核心作用包括:

  1. 变量声明

    cpp
    extern int x; // 声明(不分配内存),定义需在其他文件中
  2. C++ 与 C 混合编程

    cpp
    extern "C" { /* C函数声明 */ } // 告诉C++编译器按C语言规则链接
  3. 防止重复定义: 在头文件中用 extern 声明变量,源文件中定义,避免多重定义错误。

【详细回答】

1. 声明全局变量(不分配内存)

  • 语法
cpp
// 文件A.cpp
int x = 10; // 定义全局变量x(分配内存)
// 文件B.cpp
extern int x; // 声明x,链接到A.cpp中的定义
void func() {
    x = 20; // 使用A.cpp中定义的x
}
  • 关键点:- 声明 vs 定义:声明仅告知变量类型和名称,定义才分配内存。
    • 多次声明合法:但只能有一个定义(否则链接错误 multiple definition)。

2. 函数的隐式extern

  • 函数声明默认带有 extern,无需显式写出:
cpp
// 文件A.h
extern void func(); // 等价于 void func();
// 文件B.cpp
void func() { /* 实现 */ } // 定义函数
  • 作用: 使函数在整个程序中可见,可被其他文件调用。

【知识拓展】

1. 头文件中的extern(避免重复定义)

  • 正确做法
cpp
// config.h
extern int global_var; // 声明(头文件中)
// config.cpp
int global_var = 0; // 定义(源文件中)
  • 错误示例
cpp
// 错误:头文件中直接定义全局变量
int global_var = 0; // 若被多个源文件包含,会导致重复定义

2. 模板与extern(C++11+)

  • 显式实例化声明
cpp
// 声明:告知编译器某个模板实例已在其他文件中定义
extern template class std::vector<int>;
// 定义(另一个文件中)
template class std::vector<int>;
  • 作用: 减少编译时间(避免重复实例化),常用于大型项目。

3. 常见面试问题

  • Q1:extern int x; 和 int x; 的区别? A
    • extern int x; 是声明,不分配内存,需依赖其他文件的定义。
    • int x; 是定义,分配内存(若多次定义会导致链接错误)。
  • Q2:如何在 C++ 中调用 C 函数? A
cpp
// 方式1:直接声明
extern "C" void c_function(int);
// 方式2:包含C头文件
extern "C" {
    #include <cstdio> // 例如调用printf
}
  • Q3:extern 与 static 的关系? A
    • extern 使变量 / 函数具有外部链接属性(跨文件可见)。
    • static 使变量 / 函数具有内部链接属性(仅当前文件可见)。

总结

extern 关键字的核心价值在于:

  • 分离声明与定义:实现跨文件共享全局资源。
  • 兼容 C 语言:通过 extern "C" 解决函数名修饰问题。
  • 优化编译:结合模板显式实例化减少重复编译。

请描述C++中的static关键字在不同场景下的作用(如局部变量、全局变量、函数等)。

【简要回答】

在 C++ 中,static 关键字的作用根据使用场景不同而有所区别:

  • 局部变量:添加 static 会使其生命周期延长至程序结束,但作用域仍限于函数内部,且只在首次执行时初始化。
  • 全局变量 / 函数:限制其作用域为当前文件,避免被其他文件通过 extern 引用。
  • 类成员变量 / 函数:属于类而非对象实例,所有对象共享同一份静态成员,可通过类名直接访问。

【详细回答】

  1. 静态局部变量

    • 生命周期:存储在静态存储区,程序运行期间持续存在,不会因函数返回而销毁。
    • 初始化:仅在第一次调用函数时初始化,后续调用会保留上次修改的值。
    • 示例:
    cpp
    void counter() {
        static int count = 0;  // 仅首次调用时初始化
        std::cout << count++ << std::endl;
    }
    // 多次调用 counter() 会输出 0、1、2...
  2. 静态全局变量 / 函数

    • 作用域:被限制在定义它们的文件内,其他文件无法通过 extern 声明访问。
    • 用途:避免命名冲突,替代传统头文件中的全局变量定义。
    • 示例:
    cpp
    // file1.cpp
    static int globalData = 10;  // 仅在 file1.cpp 可见
    // file2.cpp
    extern int globalData;  // 无法引用,链接错误
  3. 类的静态成员变量

    • 存储:独立于类的对象,所有对象共享同一份数据。
    • 初始化:必须在类外定义并初始化,且不能在构造函数中初始化。
    • 访问:可通过 ClassName::member 或对象实例访问。
    • 示例:
    cpp
    class Logger {
    public:
        static int logCount;  // 声明静态成员变量
    };
    int Logger::logCount = 0;  // 定义并初始化
  4. 类的静态成员函数

    • 特性:不依赖于对象实例,无 this 指针,只能访问类的静态成员。
    • 调用:直接通过类名调用,无需创建对象。
    • 示例:
    cpp
    class MathUtil {
    public:
        static int add(int a, int b) { return a + b; }  // 静态成员函数
    };
    int result = MathUtil::add(3, 4);  // 直接调用

【知识拓展】

  • 静态与常量的区别
    • static 强调存储和作用域,const 强调不可修改。
    • 静态常量(如 static const int)需在类内声明、类外初始化,但整数类型可在类内直接初始化。
  • 静态对象的析构: 静态对象的析构函数在程序结束时自动调用,顺序与构造相反。
  • 线程安全问题: C++11 起,局部静态变量的初始化是线程安全的(即多线程首次调用函数时,静态变量只会被初始化一次)。
  • 静态类(伪概念): C++ 没有直接支持 “静态类”,但可通过将构造函数设为私有、仅提供静态成员的方式模拟。
  • 静态成员与模板: 类模板的静态成员会为每个实例化的模板类型单独生成一份实例。

常量指针和指针常量之间有什么区别。

简要回答

  • 常量指针:指针本身是常量,指向的地址不能改变,但地址中的值可以改变。
  • 指针常量:指针指向的值是常量,值不能改变,但指针可以指向其他地址。

详细回答

  • 常量指针:声明形式为 int* const ptr,表示指针 ptr 的地址是固定的,不能指向其他地址,但可以通过 ptr 修改该地址中的值。
  • 指针常量:声明形式为 const int* ptr,表示指针 ptr 指向的值是常量,不能通过 ptr 修改该值,但 ptr 可以指向其他地址。

在 《C++ primer第五版》p57 中,有讲解常量指针,但没有提到 指针常量的概念,只提到了 指向常量的指针,其实是一个意思。

知识拓展

  • 双重常量const int* const ptr,表示指针 ptr 的地址和指向的值都是常量,既不能改变地址,也不能改变值。
  • 应用场景:常量指针常用于保护指针地址不被修改,指针常量则用于保护数据不被修改。

C++中如何使用sizeof操作符获取变量或类型的大小?

sizeof 是 C++ 中的编译时一元操作符,用于获取变量或类型所占用的字节数。其核心特点:

  1. 语法
cpp
sizeof(type);      // 获取类型大小(括号必需)
sizeof(expression); // 获取表达式结果类型的大小
sizeof var;        // 获取变量大小(括号可选)
  1. 返回值std::size_t 类型的无符号整数
  2. 编译期计算:不执行表达式,仅分析类型
  3. 常见用途:内存分配、数组遍历、跨平台兼容性

【详细回答】

  1. sizeof 的基本用法

    • 内置类型
    cpp
    sizeof(int);       // 通常为4字节
    sizeof(double);    // 通常为8字节
    sizeof(char);      // 固定为1字节
    • 变量
    cpp
    int x = 10;
    sizeof x;          // 4字节
    sizeof(x);         // 等效写法
    • 数组
    cpp
    int arr[5];
    sizeof(arr);       // 20字节(5×4)
    sizeof(arr)/sizeof(arr[0]); // 计算数组元素个数(5)
    • 指针
    cpp
    int* ptr = nullptr;
    sizeof(ptr);       // 通常为4字节(32位系统)或8字节(64位系统)
  2. sizeof 与表达式

    • 不执行表达式
    cpp
    int func() { return 42; }
    sizeof(func());    // 4字节(int类型大小),但不会调用func()
    • 引用类型
    cpp
    int x = 10;
    int& ref = x;
    sizeof(ref);       // 4字节(引用类型的大小等于被引用类型的大小)
    • 空类 / 结构体
    cpp
    struct Empty {};
    sizeof(Empty);     // 1字节(C++要求每个对象有唯一地址)
  3. sizeof 与类 / 结构体

    • 成员对齐
    cpp
    struct S {
        char c;        // 1字节
        int i;         // 4字节(通常对齐到4字节边界)
        char d;        // 1字节
    };
    sizeof(S);         // 通常为12字节(因对齐填充)
    • 虚函数表
    cpp
    class Base {
        virtual void func() {}
    };
    sizeof(Base);      // 通常为8字节(包含虚函数表指针)

【知识拓展】

  1. sizeof 与模板元编程

    • 类型特征
    cpp
    template<typename T>
    struct TypeSize {
        static constexpr size_t value = sizeof(T);
    };
    • 条件编译
    #if sizeof(void*) == 8
        // 64位系统代码
    #else
        // 32位系统代码
    #endif
  2. C++11/14/17 对 sizeof 的扩展

    • sizeof... 扩展包(C++11):
    cpp
    template<typename... Args>
    size_t total_size(Args... args) {
        return (sizeof(args) + ...); // 折叠表达式
    }
    • decltype 结合 sizeof
    cpp
    int x = 10;
    sizeof(decltype(x)); // 4字节
  3. sizeof 的局限性

    • 动态数组
    cpp
    int* arr = new int[10];
    sizeof(arr);         // 8字节(指针大小),而非40字节
    • 函数参数中的数组
    cpp
        void func(int arr[]) {
    	    sizeof(arr);     // 8字节(数组退化为指针)
    	}
  4. 常见面试陷阱

    • Qsizeof 是函数还是操作符? A:操作符。编译期计算,不产生运行时代码。

    • Q:如何获取动态数组的大小? A:无法通过 sizeof 获取,需自行管理大小(如使用 std::vector)。

    • Q:以下代码输出什么?

      cpp
      int arr[10];
      void func(int arr[]) {
          cout << sizeof(arr) << endl;
      }
      int main() {
          cout << sizeof(arr) << endl;
          func(arr);
          return 0;
      }

      A:输出 40 和 8(假设 64 位系统)。main 中 arr 是数组,func 中 arr 退化为指针。

【总结】

sizeof 是 C++ 中用于获取类型或表达式大小的编译时操作符,其核心价值在于:

  1. 内存管理:精确计算数据结构大小,避免内存泄漏
  2. 跨平台兼容:处理不同系统的类型大小差异
  3. 模板元编程:在编译期进行类型大小的条件判断
  4. 性能优化:通过对齐优化减少内存碎片

理解 sizeof 的工作原理(特别是对齐规则和指针 / 数组的区别)对于编写高效、可移植的 C++ 代码至关重要。

请解释C++中的nullptr与NULL的区别。(考点:nullptr与NULL的区别)【简单】

C++中的nullptr与NULL的区别

1.类型上的区别 nullptr nullptr是C++11引入的关键字,表示一种特殊的空指针类型,具体为std::nullptr_t线程安全类型,这种类型可以隐式转换为任意的指针类型,但不能转换为整数类型。

cpp
int *p=nullptr ;
void *vp=nullptr;
int x=nullptr;//error

NULL: NULL是一个宏定义,通常定义为0(void*)0,它的本质还是一个整数常量,可以隐式的转换为指针类型,但可能引发分歧。

代码示例

cpp
void func(int );
void func(int *);
int main()
{
    func(NULL);    // 调用 func(int),因为 NULL 是整数,但是此时NULL可能存在为二义性
    func(nullptr); // 调用 func(int*),因为 nullptr 是指针类型
    return 0;
}
特性nullptrNULL
定义C++11新增关键字宏,通常为表示整形为0
类型std::nulltpr_t整数常量
类型安全性
转换为整数不可以可以
推荐使用不是

Released under the MIT License.