# C++ 基础 ## 静态局部变量,全局变量,局部变量的特点,以及使用场景? ### 简要回答 #### 静态局部变量,全局变量,局部变量的使用场景 - **静态局部变量**:函数**内**定义,用static修饰,**生命周期为整个程序**,该变量无法在作用域外修改,**只能被初始化一次**,之后每次调用都保持上以上调用结束时的值,存储在数据段。 - **全局变量**:函数**外**定义,生命周期是**整个程序运行区间**,程序中可以在任何地方访问,创建在**堆**上。 - **局部变量**:函数**内**定义,**作用域和生命周期只能在声明该变量的函数体内或者类内访问**,每次调用重新创建,生命周期随着函数的返回而结束,创建在**栈**上。 #### 静态局部变量,全局变量,局部变量的使用场景如下表所示 | 分类 | 局部变量 | 静态局部变量 | 全局变量 | | -------------- | ---------------------------------------- | ---------------------------------------------------- | ------------------------------------------------ | | **作用域** | 当前函数或者代码块内 | 当前函数内部(外部不能访问) | 整个程序内 | | **生命周期** | 每次进入函数创建,用完就没 | 程序一运行就存在,一直到程序结束 | 程序启动时创建,程序结束才销毁 | | **初始化行为** | 不赋值的话值不确定(是随机的) | 初始化一次(默认0),之后值会保留 | 默认初始化为0 | | **存储位置** | 栈区(stack),速度快但不持久 | 数据段或 BSS 段(非栈),可长期保存 | 数据段或 BSS 段,生命周期长 | | **适合场景** | 做临时运算,比如循环里的变量、临时数组等 | 想在函数里“记住之前的值”,如统计次数、递归深度控制等 | 整个程序都需要共享的变量,比如配置信息、缓冲区等 | ### 详细回答 #### 全局变量 全局变量定义在所有函数外面,**整个程序都能访问**,它从程序启动时就存在。 若在其他文件中使用,需要用**extern关键字**声明,**默认也会被初始化为0**,和静态局部变量一样存储在**静态存储区**。 若用"static"修饰的全局变量,**仅限当前文件**使用,避免与其他文件同名变量冲突。 #### 静态局部变量 在函数内部定义,使用static关键字声明,比如"static int count",其生命周期不随函数调用结束而销毁,可**保留上次的值**。 第一次调用函数时初始化后,之后每次调用都**不会重新初始化,而是保持上次的值**,这种变量被放在一个特殊的内存区域(**静态存储区**),所以函数执行完也不会被销毁。 它**默认会被自动初始化为0**,适合用来做计数器或者缓存一些需要在多次函数调用之间保持的数据。 #### 局部变量 在函数或代码块内部定义。这种变量的特点是“**随用随弃**”,随函数调用创建、执行结束后销毁,存储在**栈**内存中,存取速度很快,如果不初始化,它的值会是随机的(这点和全局变量、静态变量不同)。 需要注意的是,如果局部变量和全局变量同名,在**函数内部**会优先使用**局部变量**,相当于暂时"**屏蔽**"了同名的全局变量。 局部变量最适合存放一些**临时数据**,比如循环计数器、函数内部的中间计算结果等,用完就自动释放,**不会占用额外内存**。 ### 怎么用 全局变量是那种“**很多地方都要用**”的数据,比如配置参数、程序状态什么的,放在最外面,全局共享。但不建议乱用,容易让程序变得混乱、难维护,非必要别用它,**用的话要写清楚、注释好**。 静态局部变量适合在一个函数里需要“**记住上一次的值**”的情况,比如要在函数里做一个计数器、记录某个初始化状态,只想执行一次,就加个 static,函数每次运行,它都能记得上一次的结果。 局部变量**最常用**,能用就用它,它只在当前函数里生效,用完就销毁,**最干净、安全、不容易出错**,处理临时数据、普通运算,直接写局部变量就够了。 ### 静态局部变量,全局变量,局部变量的代码演示 - 静态局部变量示例 ``` #include 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; } ``` - 全局变量示例 ``` #include 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; } ``` - 局部变量示例 ``` void calculate() { int localVar = 5; // 局部变量 localVar *= 2; cout << "Local variable: " << localVar << endl; // 输出: Local variable: 10 // localVar 在此处销毁 } int main() { calculate(); // cout << localVar; // 错误:局部变量不可见 return 0; } ``` ### 知识拓展 - 静态局部变量,全局变量,局部变量特点的示意图: ![image](https://file1.kamacoder.com/i/bagu/202507021.png) - 内存四区的示意图 ![image](https://file1.kamacoder.com/i/bagu/202507022.png) ## **指针和引用**的区别? ### 简要回答 - **指针**:是一个变量,存储另一个变量的内存地址,使用时需要引用(`*`)访问目标值。可重新赋值指向其他对象,支持指针算术(如++),**可为空(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&)。 由于引用绑定后不可更改,**生命周期与绑定对象一致**,**无需手动管理内存**,因此在函数参数传递、返回值优化等场景中更安全高效。 ### 指针和引用演示 ``` #include 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](https://file1.kamacoder.com/i/bagu/202507031.jpg) - 面试官可能追问:**在实际编程中,什么情况下应该使用指针,什么情况下应该使用引用?举例子说明。** - 简答: 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 判断数据类型的长度。例如: ``` cout << "int is " << sizeof (int) << " bytes. \n"; cout << "short is " << sizeof (short) << " bytes. \n"; ``` 头文件climits定义了符号常量:例如:INT_MAX 表示 int 的最大值,INT_MIN 表示 int 的最小值。 #### 无符号类型 即为不存储负数值的整型,可以增大变量能够存储的最大值,数据长度不变。 int 被设置为自然长度,即为计算机处理起来效率最高的长度,所以选择类型时一般选用 int 类型。 ## 关键字 ### const 关键字 - **常量指针(Pointer to Constant)**: 指向常量的指针,即指针所指向的值不可通过该指针修改(`const int* p`),但指针本身可指向其他地址(`p = &other` 合法)。 - **指针常量(Constant Pointer)**: 指针本身是常量,即指针的地址不可修改(`int* const p`),但可通过该指针修改指向的值(`*p = 10` 合法)。 ### static关键字的作用 `static`关键字主要用于控制变量和函数的生命周期、作用域以及访问权限。 1. 静态变量 静态变量在程序的整个生命周期内存在,并且只初始化一次。它们通常用于在函数调用之间保持变量的值。 生命周期:静态变量在程序启动时分配内存,并在程序结束时释放内存。 作用域:全局静态变量的作用域是整个程序,而局部静态变量的作用域是声明它的函数或代码块。 存储位置:静态变量通常存储在全局数据区,而不是栈上。 初始化:静态变量只能初始化一次,且默认初始化为0(对于基本数据类型)。 1. 静态函数 - 在类内部使用`static`关键字修饰的函数是静态函数。 - 静态函数属于类而不是类的实例,可以通过类名直接调用,而无需创建对象。 - 静态函数不能直接访问非静态成员变量或非静态成员函数。 ``` class ExampleClass { public: static void staticFunction() { cout << "Static function" << endl; } }; ``` 1. 静态成员变量 - 在类中使用`static`关键字修饰的成员变量是静态成员变量。 - 所有类的对象共享同一个静态成员变量的副本。 - 静态成员变量必须在类外部单独定义,以便为其分配存储空间。 ``` class ExampleClass { public: static int staticVar; // 静态成员变量声明 }; // 静态成员变量定义 int ExampleClass::staticVar = 0; ``` 1. 静态成员函数 - 在类中使用`static`关键字修饰的成员函数是静态成员函数。 - 静态成员函数不能直接访问非静态成员变量或非静态成员函数。 - 静态成员函数可以通过类名调用,而不需要创建类的实例。 ``` class ExampleClass { public: static void staticMethod() { cout << "Static method" << endl; } }; ``` 1. 静态局部变量 静态局部变量是定义在函数内部的静态变量。尽管它们在函数内部定义,但它们的行为与全局静态变量类似。 生命周期:静态局部变量在程序启动时分配内存,并在程序结束时释放内存,与全局静态变量相同。 作用域:静态局部变量的作用域仅限于定义它们的函数或代码块。 存储位置:与全局静态变量一样,静态局部变量也存储在全局数据区。 初始化:静态局部变量只能初始化一次,且默认初始化为0(对于基本数据类型)。 ``` void exampleFunction() { static int localVar = 0; // 静态局部变量 localVar++; cout << "LocalVar: " << localVar << endl; } ``` ### const 关键字的作用 `const`关键字主要用于指定变量、指针、引用、成员函数等的性质 1. 常量变量:声明常量,使变量的值不能被修改。 2. 指针和引用:声明指向常量的指针,表示指针所指向的值是常量,不能通过指针修改。声明常量引用,表示引用的值是常量,不能通过引用修改。 ``` 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函数从堆上动态分配内存。 ##### 表格 ![img](https://file1.kamacoder.com/i/bagu/_new_malloc_benren.png) ### constexpr 和 const const 表示“只读”的语义,constexpr 表示“常量”的语义 constexpr 只能定义编译期常量,而 const 可以定义编译期常量,也可以定义运行期常量。 你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将一个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。 ##### constexpr变量 复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。 **必须使用常量初始化:** ``` constexpr int n = 20; constexpr int m = n + 1; static constexpr int MOD = 1000000007; ``` 如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关。 ``` constexpr int* p1 = nullptr; // 编译期常量指针,指针值在编译期已知,语义类似 int* const const int* p2 = nullptr; // 底层 const,指向常量的指针:指针可改,指向的值不可改 int* const p3 = nullptr; // 顶层 const,常量指针:指针本身不可改,指向的值可改 ``` ##### constexpr函数: constexpr函数是指能用于常量表达式的函数。 函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。 ``` constexpr int new() {return 42;} ``` 为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。 constexpr和内联函数可以在程序中多次定义,一般定义在头文件。 ##### constexpr 构造函数: 构造函数不能说const,但字面值常量类的构造函数可以是constexpr。 constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用 constexpr 修饰 ##### constexpr的好处 1. 为一些不能修改数据提供保障,写成变量则就有被意外修改的风险。 2. 有些场景,编译器可以在编译期对constexpr的代码进行优化,提高效率。 3. 相比宏来说,没有额外的开销,但更安全可靠。 ### volatile **定义:** [与const绝对对立的,是类型修饰符]影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。 **作用:** 指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问 **使用场合:** 在中断服务程序和cpu相关寄存器的定义 **举例说明:** 空循环: ``` for(volatile int i=0; i<100000; i++); // 它会执行,不会被优化掉 ``` ### extern 定义:声明外部变量【在函数或者文件外部定义的全局变量】 ### static 作用:实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;默认初始化为0 ### 前置++与后置++ ``` 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代表的内存中 ``` mov eax, dword ptr [a] # (1) inc eax # (2) mov dword ptr [a], eax # (3) ``` 现在假设i的值是0,有两个线程,每个线程对变量a的值都递增1,预想一下,其结果应该是2,可实际运行结构可能是1!是不是很奇怪? **分析如下:** ``` 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的内存地址中: ``` mov eax, dword ptr [b] mov dword prt [a], eax ``` 既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。 **解决办法:** C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是一个模板类型: ``` template struct atomic: ``` 我们可以传入具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型 ``` // 初始化1 std::atomic value; value = 99; // 初始化2 // 下面代码在Linux平台上无法编译通过(指在gcc编译器) std::atomic value = 99; // 出错的原因是这行代码调用的是std::atomic的拷贝构造函数 // 而根据C++11语言规范,std::atomic的拷贝构造函数使用=delete标记禁止编译器自动生成 // g++在这条规则上遵循了C++11语言规范。 ``` ## 什么是函数指针,如何定义和使用场景 函数指针是指向函数的指针变量。它可以用于存储函数的地址,允许在运行时动态选择要调用的函数。 ``` // 返回类型 (*指针变量名)(参数列表) 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. **实现函数映射表:** 在一些需要根据某些条件调用不同函数的情况下,可以使用函数指针来实现函数映射表。 ## 函数指针和指针函数的区别 函数指针是指向函数的指针变量。可以存储特定函数的地址,并在运行时动态选择要调用的函数。通常用于回调函数、动态加载库时的函数调用等场景。 ``` int add(int a, int b) { return a + b; } int (*ptr)(int, int) = &add; // 函数指针指向 add 函数 int result = (*ptr)(3, 4); // 通过函数指针调用函数 ``` 指针函数是一个返回指针类型的函数,用于返回指向某种类型的数据的指针。 ``` int* getPointer() { int x = 10; return &x; // 返回局部变量地址,不建议这样做 } ``` ## struct和Class的区别 相同点: - 如果结构体没有定义任何构造函数,编译器会生成默认的无参数构造函数。如果类没有定义任何构造函数,编译器也会生成默认的无参数构造函数。 不同点: - 通常,`struct`用于表示一组相关的数据,而`class`用于表示一个封装了数据和操作的对象,在实际使用中,可以根据具体的需求选择使用`struct`或`class`。如果只是用来组织一些数据,而不涉及复杂的封装和继承关系,`struct`可能更直观;如果需要进行封装、继承等面向对象编程的特性,可以选择使用`class`。 - struct结构体中的成员默认是公有的(public)。类中的成员默认是私有的(private)。 - **struct** 继承时默认使用公有继承。**class** 继承时默认使用私有继承。 ``` // 使用 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. 变量声明 : ``` extern int x; // 声明(不分配内存),定义需在其他文件中 ``` 2. C++ 与 C 混合编程 : ``` extern "C" { /* C函数声明 */ } // 告诉C++编译器按C语言规则链接 ``` 3. **防止重复定义**: 在头文件中用 `extern` 声明变量,源文件中定义,避免多重定义错误。 ### 【详细回答】 #### **1. 声明全局变量(不分配内存)** - 语法 : ``` // 文件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 ``` ,无需显式写出: ``` // 文件A.h extern void func(); // 等价于 void func(); // 文件B.cpp void func() { /* 实现 */ } // 定义函数 ``` - **作用**: 使函数在整个程序中可见,可被其他文件调用。 ### 【知识拓展】 #### **1. 头文件中的`extern`(避免重复定义)** - 正确做法 : ``` // config.h extern int global_var; // 声明(头文件中) // config.cpp int global_var = 0; // 定义(源文件中) ``` - 错误示例 : ``` // 错误:头文件中直接定义全局变量 int global_var = 0; // 若被多个源文件包含,会导致重复定义 ``` #### **2. 模板与`extern`(C++11+)** - 显式实例化声明 : ``` // 声明:告知编译器某个模板实例已在其他文件中定义 extern template class std::vector; // 定义(另一个文件中) template class std::vector; ``` - **作用**: 减少编译时间(避免重复实例化),常用于大型项目。 #### **3. 常见面试问题** - Q1:`extern int x;` 和 `int x;` 的区别? A : - `extern int x;` 是声明,不分配内存,需依赖其他文件的定义。 - `int x;` 是定义,分配内存(若多次定义会导致链接错误)。 - Q2:如何在 C++ 中调用 C 函数? A : ``` // 方式1:直接声明 extern "C" void c_function(int); // 方式2:包含C头文件 extern "C" { #include // 例如调用printf } ``` - Q3:`extern` 与 `static` 的关系? A : - `extern` 使变量 / 函数具有**外部链接属性**(跨文件可见)。 - `static` 使变量 / 函数具有**内部链接属性**(仅当前文件可见)。 ### 总结 `extern` 关键字的核心价值在于: - **分离声明与定义**:实现跨文件共享全局资源。 - **兼容 C 语言**:通过 `extern "C"` 解决函数名修饰问题。 - **优化编译**:结合模板显式实例化减少重复编译。 ## 请描述C++中的static关键字在不同场景下的作用(如局部变量、全局变量、函数等)。 ### 【简要回答】 在 C++ 中,`static` 关键字的作用根据使用场景不同而有所区别: - **局部变量**:添加 `static` 会使其生命周期延长至程序结束,但作用域仍限于函数内部,且只在首次执行时初始化。 - **全局变量 / 函数**:限制其作用域为当前文件,避免被其他文件通过 `extern` 引用。 - **类成员变量 / 函数**:属于类而非对象实例,所有对象共享同一份静态成员,可通过类名直接访问。 ### 【详细回答】 1. 静态局部变量 - 生命周期:存储在静态存储区,程序运行期间持续存在,不会因函数返回而销毁。 - 初始化:仅在第一次调用函数时初始化,后续调用会保留上次修改的值。 - 示例: ``` void counter() { static int count = 0; // 仅首次调用时初始化 std::cout << count++ << std::endl; } // 多次调用 counter() 会输出 0、1、2... ``` 2. 静态全局变量 / 函数 - 作用域:被限制在定义它们的文件内,其他文件无法通过 `extern` 声明访问。 - 用途:避免命名冲突,替代传统头文件中的全局变量定义。 - 示例: ``` // file1.cpp static int globalData = 10; // 仅在 file1.cpp 可见 // file2.cpp extern int globalData; // 无法引用,链接错误 ``` 3. 类的静态成员变量 - 存储:独立于类的对象,所有对象共享同一份数据。 - 初始化:必须在类外定义并初始化,且不能在构造函数中初始化。 - 访问:可通过 `ClassName::member` 或对象实例访问。 - 示例: ``` class Logger { public: static int logCount; // 声明静态成员变量 }; int Logger::logCount = 0; // 定义并初始化 ``` 4. 类的静态成员函数 - 特性:不依赖于对象实例,无 `this` 指针,只能访问类的静态成员。 - 调用:直接通过类名调用,无需创建对象。 - 示例: ``` 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` 可以指向其他地址。 #### 知识拓展 - **双重常量**:`const int* const ptr`,表示指针 `ptr` 的地址和指向的值都是常量,既不能改变地址,也不能改变值。 - **应用场景**:常量指针常用于保护指针地址不被修改,指针常量则用于保护数据不被修改。 ## C++中如何使用sizeof操作符获取变量或类型的大小? **`sizeof`** 是 C++ 中的编译时一元操作符,用于获取变量或类型所占用的字节数。其核心特点: 1. 语法 : ``` sizeof(type); // 获取类型大小(括号必需) sizeof(expression); // 获取表达式结果类型的大小 sizeof var; // 获取变量大小(括号可选) ``` 2. **返回值**:`std::size_t` 类型的无符号整数 3. **编译期计算**:不执行表达式,仅分析类型 4. **常见用途**:内存分配、数组遍历、跨平台兼容性 ### 【详细回答】 1. `sizeof` 的基本用法 - 内置类型 : ``` sizeof(int); // 通常为4字节 sizeof(double); // 通常为8字节 sizeof(char); // 固定为1字节 ``` - 变量 : ``` int x = 10; sizeof x; // 4字节 sizeof(x); // 等效写法 ``` - 数组 : ``` int arr[5]; sizeof(arr); // 20字节(5×4) sizeof(arr)/sizeof(arr[0]); // 计算数组元素个数(5) ``` - 指针 : ``` int* ptr = nullptr; sizeof(ptr); // 通常为4字节(32位系统)或8字节(64位系统) ``` 2. `sizeof` 与表达式 - 不执行表达式 : ``` int func() { return 42; } sizeof(func()); // 4字节(int类型大小),但不会调用func() ``` - 引用类型 : ``` int x = 10; int& ref = x; sizeof(ref); // 4字节(引用类型的大小等于被引用类型的大小) ``` - 空类 / 结构体 : ``` struct Empty {}; sizeof(Empty); // 1字节(C++要求每个对象有唯一地址) ``` 3. `sizeof` 与类 / 结构体 - 成员对齐 : ``` struct S { char c; // 1字节 int i; // 4字节(通常对齐到4字节边界) char d; // 1字节 }; sizeof(S); // 通常为12字节(因对齐填充) ``` - 虚函数表 : ``` class Base { virtual void func() {} }; sizeof(Base); // 通常为8字节(包含虚函数表指针) ``` ### 【知识拓展】 1. `sizeof` 与模板元编程 - 类型特征 : ``` template 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): ``` template size_t total_size(Args... args) { return (sizeof(args) + ...); // 折叠表达式 } ``` - `decltype` 结合 `sizeof` : ``` int x = 10; sizeof(decltype(x)); // 4字节 ``` 3. `sizeof` 的局限性 - 动态数组 : ``` int* arr = new int[10]; sizeof(arr); // 8字节(指针大小),而非40字节 ``` - 函数参数中的数组 : ``` void func(int arr[]) { sizeof(arr); // 8字节(数组退化为指针) } ``` 4. 常见面试陷阱 - **Q**:`sizeof` 是函数还是操作符? **A**:操作符。编译期计算,不产生运行时代码。 - **Q**:如何获取动态数组的大小? **A**:无法通过 `sizeof` 获取,需自行管理大小(如使用 `std::vector`)。 - Q :以下代码输出什么? ``` 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`线程安全类型,这种类型可以隐式转换为任意的指针类型,但不能转换为整数类型。 ``` int *p=nullptr ; void *vp=nullptr; int x=nullptr;//error ``` **NULL**: NULL是一个宏定义,通常定义为`0`或`(void*)0`,它的本质还是一个整数常量,可以隐式的转换为指针类型,但可能引发分歧。 #### 代码示例 ``` void func(int ); void func(int *); int main() { func(NULL); // 调用 func(int),因为 NULL 是整数,但是此时NULL可能存在为二义性 func(nullptr); // 调用 func(int*),因为 nullptr 是指针类型 return 0; } ``` | 特性 | nullptr | NULL | | ---------- | --------------- | --------------------- | | 定义 | C++11新增关键字 | 宏,通常为表示整形为0 | | 类型 | std::nulltpr_t | 整数常量 | | 类型安全性 | 强 | 弱 | | 转换为整数 | 不可以 | 可以 | | 推荐使用 | 是 | 不是 |