一.变量
1. 全局变量与 static
变量(作用域、生存周期)
全局变量
- 作用域:全局变量的作用域从它被定义的地方开始,一直到程序结束。在定义它的文件内部以及通过
extern
关键字在其他文件中都可以访问。 - 生存周期:全局变量的生存周期贯穿整个程序执行期间,从程序开始执行到程序结束。
static
变量(在函数外部定义的)
- 作用域:
static
变量具有文件作用域,即它只在定义它的文件内部可见,其他文件即使通过extern
也不能访问。 - 生存周期:与全局变量相同,
static
变量的生存周期也是贯穿整个程序执行期间,从程序开始执行到程序结束。但是,由于它的作用域限制,它只能在定义它的文件内部被访问和修改。
2. static
函数与普通函数的区别
- 作用域:
static
函数具有文件作用域,即它只能在定义它的文件内部被调用。而普通函数(非static
)则可以在整个程序中通过函数原型声明后,在任何文件中被调用(只要包含了相应的头文件或声明)。 - 链接性:
static
函数具有内部链接性(internal linkage),这意味着编译器只为该函数生成一份代码,且该代码仅在该文件内部可见。而普通函数具有外部链接性(external linkage),编译器会为每个调用该函数的文件生成一份指向该函数代码的指针(或类似的机制),以便在链接时解析函数调用。 - 使用场景:
static
函数常用于隐藏函数实现细节,减少命名冲突,或当函数只在定义它的文件内部使用时。普通函数则用于实现需要在多个文件中共享的功能。
3. 两个文件中声明两个同名变量?(使用了与未使用 extern
?)
在C++中,如果两个文件中声明了同名的全局变量,并且没有使用extern
关键字来明确指定它们之间的链接关系,那么这两个变量实际上是两个独立的变量,它们分别属于各自的文件作用域。
-
未使用
extern
:每个文件中的同名全局变量都是独立的,它们之间没有任何关系。每个变量只在定义它的文件内部可见和可访问。 -
使用
extern
:如果在一个文件中定义了全局变量(例如,在file1.cpp
中定义了int x;
),然后在另一个文件(file2.cpp
)中想要访问这个变量,就需要在file2.cpp
中使用extern
关键字来声明这个变量(extern int x;
)。这样,file2.cpp
中的x
就指向了file1.cpp
中定义的那个全局变量,它们实际上是同一个变量。但是,需要注意的是,extern
声明不能出现在函数内部,它必须位于所有函数之外的全局作用域中。
总结来说,extern
关键字用于在多个文件中共享同一个全局变量的声明,而如果不使用extern
,则每个文件中的同名全局变量都是独立的。
4. 全局数组和局部数组的初始化
全局数组
- 定义位置:全局数组是在函数外部定义的,即它们对整个程序可见。
- 初始化:
- 如果没有显式初始化,全局数组(包括静态数组)的元素会自动初始化为0(对于基本数据类型,如int, float等)。这是因为全局变量和静态变量的存储区域在程序的数据段(data segment),未初始化的全局变量会被编译器自动初始化为0。
- 如果显式初始化,则可以指定数组元素的具体值。
- 生命周期:全局数组的生命周期贯穿整个程序执行期间,从程序开始执行到程序结束。
局部数组
- 定义位置:局部数组是在函数内部定义的,其作用域限定在定义它的函数内部。
- 初始化:
- 如果不显式初始化,局部数组不会自动初始化为0,它们的初始值是未定义的(即它们包含垃圾值)。
- 如果显式初始化,可以指定数组元素的具体值,或者通过初始化列表进行部分或全部初始化。
- 生命周期:局部数组的生命周期从它被定义时开始,到包含它的函数执行结束时结束。
5. 指针和引用的区别
代表意义
- 指针:是一个变量,存储的是另一个变量的地址。指针可以指向任何类型的数据,包括数组、结构体、函数等。
- 引用:是变量的别名,必须在声明时被初始化,且一旦与某个变量绑定后,就不能再改变为另一个变量的引用。
内存占用
- 指针:占用内存空间,用于存储被指向变量的地址。指针的大小取决于系统架构(如32位系统下通常是4字节,64位系统下通常是8字节)。
- 引用:不占用独立的内存空间,它仅仅是一个别名,对内存的影响仅限于对引用的变量本身进行操作。
初始化
- 指针:必须显式初始化,否则它可能指向一个随机的内存地址,导致不可预测的行为。
- 引用:必须在声明时通过另一个变量来初始化,且初始化后不能改变其指向。
指向是否可改
- 指针:指向可以改变,可以指向另一个变量的地址。
- 引用:一旦与某个变量绑定,其指向不能改变。
能否为空
- 指针:可以指向
nullptr
(或NULL
,在C++11之前),表示它不指向任何有效的内存地址。 - 引用:不能为空,必须在声明时被初始化指向一个有效的对象。
6. C/C++中的强制转换
在C和C++中,强制类型转换用于显式地将一种数据类型的表达式转换成另一种类型。C++引入了更安全的类型转换操作符,而C语言中的类型转换主要通过类型转换运算符(如(type)expression
)实现。
C风格类型转换
(type)expression
:这是C语言风格的类型转换,它在C++中仍然有效,但不够安全,因为它不会检查类型之间是否存在合理的转换路径。
C++风格类型转换
C++提供了四种类型转换操作符,它们提供了更明确和安全的类型转换方式:
- static_cast(expression):用于基本数据类型之间的转换,以及有明确定义转换路径的类之间的转换。
- dynamic_cast(expression):主要用于类的层次结构中的安全向下转换(即派生类到基类指针或引用的转换)。它只适用于含有虚函数的类。
- const_cast(expression):用于去除或添加变量的const或volatile限定符。
- reinterpret_cast(expression):允许进行几乎任何类型的指针或引用之间的转换,包括不相关的类型之间的转换。这种转换是不安全的,因为它仅仅是对位模式进行重新解释。
每种转换都有其特定的用途和限制,使用时应根据具体需求和上下文选择适当的转换方式。
7. 如何修改 const
变量、const
与 volatile
如何修改 const
变量
在C++中,const
关键字用于声明一个变量为常量,这意味着一旦变量被初始化后,其值就不应该被修改。因此,从常规编程的角度来看,直接修改一个 const
变量的值是不被允许的,也是不符合C++设计初衷的。然而,在某些特定情况下(如调试或学习目的),我们可以通过一些“hack”的方式来实现,但这些方法并不推荐在生产代码中使用。
- 通过指针的强制类型转换:虽然不推荐,但你可以通过将
const
变量的地址赋给一个指向非const
类型的指针,然后通过这个指针修改值。这违反了const
的设计意图,可能导致未定义行为。
const int x = 10;
int* px = const_cast<int*>(&x); // 强制类型转换
*px = 20; // 现在x的值被修改了,但这是未定义行为
const
与 volatile
-
const
:表明变量的值不应该被修改。编译器会检查对const
变量的修改,确保它们不被意外修改。const
主要用于优化和提供代码的语义清晰性。 -
volatile
:表明变量的值可能会意外地被改变,即该变量的值可能在程序的控制流之外被修改(例如,由硬件或并发运行的另一个线程)。使用volatile
告诉编译器不要对这样的变量进行优化(比如缓存其值),而是每次直接从内存中读取它的值。
volatile
和 const
可以一起使用,例如 const volatile int* ptr;
,这表示指针指向的值是常量,但这个值可能会被外部因素改变,因此每次访问都需要直接从内存读取。
8. 静态类型获取与动态类型获取(typeid
、dynamic_cast
:转换目标类型必须是引用类型)
静态类型获取
typeid
:用于在运行时获取对象的类型信息。typeid
可以用于任何类型的表达式,包括基本数据类型、指针、用户定义类型等。对于多态类型,typeid
会在运行时确定对象的实际类型(通过RTTI,运行时类型信息)。对于非多态类型,typeid
在编译时就已经确定了类型。
动态类型获取
dynamic_cast
:主要用于安全地将基类指针或引用转换为派生类指针或引用。它要求基类必须含有虚函数(即多态类型),因为dynamic_cast
在运行时检查转换的安全性。如果转换不安全(即对象不是目标类型的实例),则指针类型的dynamic_cast
会返回nullptr
,而引用类型的dynamic_cast
会抛出std::bad_cast
异常。注意,dynamic_cast
的目标类型必须是引用类型或指针类型。
9. 如何比较浮点数大小?
直接使用 ==
来比较浮点数的大小是不安全的,因为浮点数的表示可能受到精度限制和舍入误差的影响。这意呀着,即使两个数学上不相等的浮点数,也可能在内存中表示为相同的值。
正确的做法是使用一个小的容差(epsilon)来比较两个浮点数是否“足够接近”:
#include <cmath>
#include <iostream>bool isEqual(double a, double b, double epsilon = 1e-9) {return std::fabs(a - b) < epsilon;
}int main() {double a = 0.1 + 0.2;double b = 0.3;if (isEqual(a, b)) {std::cout << "a and b are equal within epsilon." << std::endl;} else {std::cout << "a and b are not equal." << std::endl;}return 0;
}
在这个例子中,isEqual
函数通过比较两个数的差的绝对值是否小于一个小的容差(epsilon)来判断这两个数是否足够接近,从而可以认为它们是相等的。选择适当的 epsilon 值取决于你的具体应用场景和所需的精度。