面试题 1 :什么是空指针?
在 C++ 中,空指针是一个特殊的指针值,它不指向任何有效的内存地址。空指针通常用于表示指针不指向任何对象或函数。在C++11及以后的版本中, nullptr 是表示空指针的推荐方式。
nullptr 是一个指针类型的字面量,它不能转换为整数类型,也不能被解引用。这使得 nullptr 在类型安全性和语义上比传统的 NULL 或 0 更有优势。
以下是一些关于空指针的要点:
(1)定义:在 C++11 及以后的版本中, nullptr 是一个关键字,用于表示空指针。
(2)类型安全: nullptr 是一个指针类型的字面量,因此它不能隐式地转换为整数。这有助于防止一些由于类型不匹配导致的错误。
(3)比较: nullptr 可以与任何指针类型进行比较,包括 void* 指针。比较的结果将是 true ,如果指针是空指针;否则是 false 。
(4)解引用:尝试解引用 nullptr (即尝试访问 nullptr 所指向的内存地址)将导致程序崩溃或未定义行为。
(5)初始化:可以将指针变量初始化为 nullptr ,以表示它不指向任何对象。
(6)与 NULL 的区别:在 C++11 及以后的版本中, NULL 通常被定义为 0 或 (void*)0 ,它是一个整数值,可以隐式地转换为任何指针类型。然而,由于 NULL 可以隐式地转换为整数,这可能导致一些类型相关的错误。因此,在现代C++编程中,推荐使用 nullptr 代替 NULL 。
下面是一个使用 nullptr 的例子:
int* ptr = nullptr; // 指针ptr1被初始化为空指针
if (nullptr == ptr)
{// ptr1是空指针,执行这里的代码
}// 下面的代码将导致未定义行为,因为不能解引用空指针
// int value = *ptr;
在实际编程中,当函数返回一个指针,而该指针可能不指向任何有效对象时,通常使用 nullptr 来表示这种情况。同样,当函数的参数是一个指针,并且该参数可能不被使用时,可以将其传递为 nullptr 。
面试题 2 :什么是悬垂指针?
悬垂指针是指向已经被释放或收回的内存地址的指针。当所指向的对象被释放或收回,但对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称为悬垂指针。悬垂指针往往导致程序错误,而且难以检测。若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的悬垂指针,则将产生无法预料的后果,因为这些内存现在包含的可能已经是完全不同的数据。悬垂指针通常是由于不正确的内存管理造成的,比如当使用 new 分配内存后,如果忘记使用 delete 释放内存,或者释放了内存后仍然保留原来的指针,就可能导致悬垂指针的产生。
为了避免悬垂指针,可以采取以下一些措施:
(1)使用智能指针( std::unique_ptr 或 std::shared_ptr ),这些智能指针在超出作用域或重新赋值时会自动释放内存。
(2)在释放内存后将指针设为 nullptr ,这样可以明确地表示指针不再指向任何有效的内存地址。
(3)避免返回局部变量的地址或引用,因为局部变量在函数返回后会被销毁,其地址可能不再有效。
(4)在编写涉及指针操作的代码时,要特别小心,确保所有的内存分配和释放都是正确和匹配的。
注意:悬垂指针和野指针有所不同。野指针是指从未被初始化或从未指向有效对象的指针,而悬垂指针则是曾经有效但现在失效的指针。
面试题 3 :如何理解指针的加减运算?
指针的加减运算在 C++ 中是一种重要的操作,它允许程序员直接操作内存地址。这种运算实际上是对指针所指向的内存地址进行算术运算,从而改变指针的指向。
具体来说,指针的加法运算通常是将指针向后(或向前)移动一定的内存单元数。例如,如果一个指针 p 指向数组的第 n 个元素,那么 p+1 就会指向第 n+1 个元素。这里的 1 并不表示具体的元素数量,而是表示指针 p 的类型所占用的字节数。例如,如果 p 是一个整型指针(在大多数系统上,整型占用 4 个字节),那么 p+1 就会跳过 4 个字节,指向下一个整型元素。
指针的减法运算也是类似的,它会根据指针的类型计算出两个指针之间的内存单元数量。例如,如果 p 和 q 是两个指针,那么 p-q 就会计算出 p 和 q 之间相隔的内存单元数,也就是 p 所指向的内存地址与 q 所指向的内存地址之间的差值,除以 p 的类型所占用的字节数。
需要注意的是,指针的加减运算只能在具有相同类型的指针之间进行,否则会导致编译错误。此外,对指针进行加减运算时,必须确保结果指针仍然指向有效的内存地址,否则可能会导致程序崩溃或未定义行为。
如下为样例代码:
#include <iostream> int main()
{int vals[] = { 1, 2, 3, 4, 5 }; // 一个包含5个整数的数组 int *ptr = vals; // 定义一个指向数组首元素的指针p // 使用指针加减运算遍历数组 for (int i = 0; i < 5; i++){std::cout << "vals[" << i << "] = " << *ptr << std::endl; // 输出当前指针指向的元素值 ptr++; // 指针向后移动一个整型元素的位置(即移动 4 个字节,假设整型大小为 4 字节) }// 现在用减法运算来将指针移回数组的开始位置 ptr = vals; // 重置指针到数组开始位置 // 使用指针减法运算计算数组末尾元素的地址与数组首元素地址的差值 int offset = (vals + 5) - ptr; // 计算从数组首元素到末尾元素的指针差值 std::cout << "offset between end and start of array: " << offset << std::endl;return 0;
}
上面代码的输出为:
vals[0] = 1
vals[1] = 2
vals[2] = 3
vals[3] = 4
vals[4] = 5
offset between end and start of array: 5
在上面代码中,首先定义了一个指向整型数组 vals 首元素的指针 ptr 。然后通过一个循环,每次将指针 ptr 向后移动一个整型元素的位置(通过 ptr++ ),并输出当前指针所指向的元素值。这就是指针加法运算的应用。
在循环结束后,重置指针 ptr 到数组的开始位置,然后使用指针减法运算计算从数组首元素到末尾元素的指针差值。注意这里的差值并不是数组元素的数量,而是内存地址的差值,这个差值会被解释成以指针类型大小(在这里是整型的大小)为单位的数量。
面试题 4 :什么是指针的数组?它与数组的指针有什么区别?
指针的数组和数组的指针是 C++ 语言中的两种不同概念,它们在定义和使用上有显著的区别。
指针的数组
(1)指针的数组是一个数组,其中每个元素都是一个指针。换句话说,它存储了多个指针的地址,这些指针可以指向不同类型的数据或对象。
(2)声明方式为 type *array[],其中 type 为指针指向的数据类型。例如,int *ptrArray[5] 表示一个包含5个指向整数类型数据的指针的数组。
(3)在指针数组中,每个元素(即每个指针)都可以单独指向不同的内存空间,因此它可以用于存储不同类型或不同位置的数据,提供了更大的灵活性。
数组的指针:
(1)数组的指针是一个指针,它指向一个数组的首地址。也就是说,它存放的是一个数组的首地址。
(2)声明方式为 type (*ptr)[size],其中 type 为数组中元素的数据类型,size 为数组的大小。例如,int (*ptr)[5] 表示一个指向包含5个整数类型元素的数组的指针。
(3)数组指针解析出来的是整个数组,因此可以通过该指针遍历并访问数组的所有元素。数组指针通常用于处理多维数组。
区别
(1)本质:指针的数组本质上是一个数组,而数组的指针本质上是一个指针。
(2)用途:指针的数组通常用于存储指向不同类型或不同位置的数据的指针,而数组的指针用于访问和操作整个数组。
(3)声明方式:指针的数组声明时,* 紧跟在类型后面;而数组的指针声明时,* 紧跟在变量名前面,并用括号 () 将 * 和变量名括起来。
(4)灵活性:指针的数组更加灵活,因为它允许存储指向不同类型数据的指针;而数组的指针则更专注于对整个数组的操作。
面试题 5 :什么是函数指针?如何使用它?
函数指针是一个指针变量,它存储了函数的内存地址,因此可以通过该指针变量来调用函数。函数指针的声明方法一般为:返回值类型 (*指针变量名)([形参列表])。
使用函数指针的基本步骤如下:
(1)定义函数指针类型:首先,需要定义一个函数指针类型,这通常是通过定义一个指向特定类型函数的指针变量来实现的。例如,如果有一个返回 int 类型并接受两个 int 类型参数的函数,可以这样定义函数指针类型: int (*func)(int, int) ;
(2)初始化函数指针:然后,可以将一个函数的地址赋值给这个指针。这通常是通过将函数名(不带括号)赋值给指针变量来实现的。例如,如果有一个名为 myFunc 的函数,可以这样初始化函数指针:func = myFunc;
(3)通过函数指针调用函数:最后,可以通过函数指针来调用函数。这通常是通过在函数指针后加上括号,并在括号内提供函数所需的参数来实现的。例如,可以这样通过函数指针调用函数:int res = func(a, b);
需要注意的是,函数指针的使用需要谨慎,因为错误的使用可能会导致程序崩溃或其他未定义行为。在使用函数指针时,应确保函数指针指向的函数具有正确的返回类型和参数类型,并且在调用函数时提供了正确的参数。
此外,函数指针也有一些特定的用途,例如可以作为函数的参数,或者用于实现回调函数等高级功能。在这些情况下,函数指针的使用可能会更加复杂,需要更深入的理解和实践。
如下为样例代码:
#include<stdio.h> // 定义一个函数,它接受一个整数参数并返回一个整数
int add(int a, int b)
{return a + b;
}// 定义一个函数指针类型
typedef int(*func)(int, int);// 主函数
int main()
{// 定义一个函数指针变量并初始化 func ptr = add;// 使用函数指针调用函数 int res = ptr(1, 2);printf("the result is: %d\n", res); // 输出:The result is: 3 return 0;
}
上面代码的输出为:
the result is: 3
在上面代码中,首先定义了一个函数 add ,它接受两个整数参数并返回它们的和。然后,我们定义了一个函数指针类型 func ,它指向一个接受两个整数参数并返回一个整数的函数。
在 main 函数中,定义了一个 func 类型的变量 ptr ,并将 add 函数的地址赋值给它。然后,通过 ptr 调用 add 函数,并将结果存储在 res 变量中。最后打印出这个结果。