C/C++面试八股题(四)
目录:
1.请问你用过的指针多不?简单说下都用过哪些指针?
2.指针函数和函数指针区别?
3.常量指针 指针常量 常量引用 区别是什么?
4.使用指针需要注意什么?
5.为什么使用智能指针?
6.指针和引用有什么区别?
7.什么是野指针?如何造成的?怎样避免野指针?
内容:
1.请问你用过的指针多不?简单说下都用过哪些指针?
基本指针类型
- 定义:基本指针类型是指指向基本数据类型(如int、char、float、double等)的指针。这些指针存储的是相应基本数据类型变量的内存地址。
- 用途:基本指针类型常用于访问和修改基本数据类型变量的值,通过指针可以实现对变量的间接访问。例如,可以在函数间传递变量的地址来修改变量的值。
数组指针
- 定义:数组指针是一种指向数组的指针。它可以指向整个数组或者数组中的某个元素。数组指针的类型取决于数组的元素类型和数组的大小。
- 例子:
int arr[5] = {1, 2, 3, 4, 5}; int (*ptr)[5] = &arr; // ptr是一个指向包含5个元素的int数组的指针
- 这里
int (*ptr)[5]
是数组指针类型,&arr
是取数组arr
的地址,将其赋值给ptr
。 - 用途:数组指针在处理多维数组时非常有用。
指针数组
- 定义:指针数组是一个数组,其中的每个元素都是一个指针。这些指针可以指向不同类型的数据,但是在一个指针数组中,通常指针指向的是同一种类型的数据,以便于管理和操作。
- 例子:
int num1 = 1, num2 = 2; int* ptrArray[2]; // 定义一个包含2个元素的指针数组 ptrArray[0] = &num1; ptrArray[1] = &num2;
- 这里
int* ptrArray[2]
是指针数组的定义,它包含两个元素,每个元素都是一个指向int
类型变量的指针。 - 用途:指针数组常用于存储一组同类型的数据地址,比如字符串数组。在 C++ 中,字符串实际上是以
'\0'
结尾的字符数组,所以可以使用指针数组来存储多个字符串。
函数指针
- 定义:函数指针是指向函数的指针。函数指针的类型取决于函数的返回值类型和参数列表。函数指针可以用于在运行时动态地选择要调用的函数。
- 例子:
int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int (*funcPtr)(int, int); // 定义一个函数指针 funcPtr = add; // 将函数指针指向add函数 int result = funcPtr(3, 2); // 调用add函数,result的值为5 funcPtr = subtract; // 将函数指针指向subtract函数 result = funcPtr(3, 2); // 调用subtract函数,result的值为1
- 这里
int (*funcPtr)(int, int)
是函数指针的定义,它可以指向返回值为int
且有两个int
参数的函数。通过将函数指针指向不同的函数,可以实现动态地调用函数。
成员指针
- 定义:成员指针是指向类的成员(成员变量或成员函数)的指针。成员指针分为指向成员变量的指针和指向成员函数的指针。成员指针的类型取决于类的类型、成员的类型以及成员是否为
const
等属性。 - 例子:(指向成员变量的指针):
class MyClass { public: int memberVar; }; int main() { MyClass obj; int MyClass::* ptrToMember = &MyClass::memberVar; obj.*ptrToMember = 10; // 通过成员指针访问和修改成员变量的值 return 0; }
- 这里
int MyClass::* ptrToMember
是指向MyClass
类成员变量的指针。obj.*ptrToMember
是通过对象obj
和成员指针来访问成员变量。 - 例子:(指向成员函数的指针):
class MyClass { public: int add(int a, int b) { return a + b; } }; int main() { MyClass obj; int (MyClass::* funcPtr)(int, int) = &MyClass::add; int result = (obj.*funcPtr)(3, 2); // 通过成员指针调用成员函数 return 0; }
- 这里
int (MyClass::* funcPtr)(int, int)
是指向MyClass
类成员函数的指针,通过(obj.*funcPtr)(3, 2)
来调用成员函数。
2.指针函数和函数指针区别?
指针函数
指针函数是一个返回指针的函数。换句话说,指针函数的返回类型是指针类型。
语法:
return_type* function_name(parameters);
这里,return_type
是指针类型,表示该函数返回一个指针。
例子:
int* getPointer() { int x = 10; return &x; // 返回指向 x 的指针 }
在这个例子中,getPointer
函数返回一个 int*
类型的指针,表示它返回指向整数的指针。
特点:
- 返回指针:指针函数返回一个指向某种数据类型的指针。
- 返回地址:指针函数通常返回局部变量或动态分配内存的地址。
- 返回类型:函数声明中返回类型需要是一个指针类型。
函数指针
函数指针是一个指针变量,它指向一个函数。函数指针可以用来间接调用函数,通常用于回调函数、事件处理、动态链接等场景。
语法:
return_type (*function_pointer_name)(parameter_types);
return_type
是函数返回值的类型。parameter_types
是函数的参数类型。
例子:
#include <iostream> int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int main() { // 定义函数指针 int (*func_ptr)(int, int); // 将函数指针指向 add 函数 func_ptr = add; std::cout << "Add: " << func_ptr(5, 3) << std::endl; // 调用 add 函数 // 改变函数指针指向 subtract 函数 func_ptr = subtract; std::cout << "Subtract: " << func_ptr(5, 3) << std::endl; // 调用 subtract 函数 return 0; }
在这个例子中,func_ptr
是一个指向函数的指针。它可以指向不同的函数(如 add
或 subtract
),然后通过 func_ptr
来调用这些函数。
特点:
- 指向函数:函数指针是用来指向函数的指针变量。
- 调用函数:通过函数指针可以间接调用函数。
- 灵活性:函数指针可以动态选择不同的函数进行调用,常用于回调、事件处理等场景。
区别总结
定义 | 返回指针的函数 | 指向函数的指针变量 |
返回值类型 | 返回类型是一个指针(例如
) | 是一个指向函数的指针,指针类型与函数类型匹配 |
用途 | 用于返回指向某个对象或数据的指针 | 用于间接调用函数,支持回调函数和动态选择函数 |
语法 |
|
|
示例 |
|
|
关键点 | 返回指向变量或对象的指针 | 指向一个具有特定签名的函数,并通过指针调用该函数 |
3.常量指针 指针常量 常量引用 区别是什么?
常量指针
- 常量指针概念:常量指针是一个指针,它指向的内容是常量。也就是说,通过这个指针不能修改它所指向的数据,但指针本身的值(即它指向的地址)可以改变。
- 语法形式:const 数据类型* 指针变量名;
int a = 10; int b = 20; const int* p = &a; // *p = 30; // 这是错误的,不能通过p修改它指向的值 p = &b; // 这是正确的,p可以指向另一个变量
- 解释:在这个例子中,
p
被定义为一个常量指针,指向a
。我们不能通过p
来修改a
的值,因为p
所指向的内容被视为常量。但是,p
本身可以重新指向其他变量,比如让它指向b
。
指针常量
- 指针常量概念:指针常量是一个常量,它一旦被初始化指向一个地址后,就不能再指向其他地址,但可以通过这个指针修改它所指向的数据。
- 语法形式:数据类型* const 指针变量名;
int a = 10; int* const p = &a; *p = 20; // 这是正确的,可以通过p修改它指向的值 // p = &b; // 这是错误的,p不能再指向其他地址
- 解释:这里
p
是一个指针常量,它被初始化为指向a
。之后就不能再让p
指向其他变量了,但是可以通过p
来修改a
的值,如将a
的值修改为20
。
常量引用
- 常量引用概念:常量引用是一种引用,它绑定到一个常量对象,通过这个引用不能修改它所绑定的对象的值。常量引用主要用于函数参数传递等场景,可以避免在函数内部意外修改参数的值。
- 语法形式:const 数据类型& 引用变量名;
int a = 10; const int& r = a; // r = 20; // 这是错误的,不能通过r修改它绑定的值 int b = 30; // r = b; // 这也是错误的,不能通过r修改它绑定的值
- 解释:在这个例子中,
r
是一个常量引用,绑定到a
。不能通过r
来修改a
的值。即使有另一个变量b
,也不能通过r
将a
的值修改为b
的值。常量引用在函数参数中很有用,比如void func(const int& param)
这样的函数定义,在函数内部就不能修改param
的值。
注意:并没有“引用常量”这个概念
4.使用指针需要注意什么?
初始化指针
- 未初始化的指针(野指针)可能会指向不确定的内存位置,从而导致未定义行为。始终确保指针在使用之前被初始化。
避免悬空指针
- 悬空指针指的是指向已释放内存的指针。使用
delete
或free
后,应该将指针设为nullptr
,以避免意外访问已释放的内存。
指针越界
- 指针越界发生在你试图访问指针指向的内存区域之外。特别是在操作数组时,越界访问会导致程序崩溃或不可预测的行为。确保指针不会超出合法内存范围。
避免野指针
- 野指针是指一个指向已经被释放或未初始化的内存地址的指针。它通常会导致程序崩溃或数据损坏。
内存泄漏
- 内存泄漏是指程序分配了内存但没有释放,导致内存无法回收。使用指针时,必须确保每次分配内存后都对应一次
delete
或free
操作。
深拷贝与浅拷贝
- 指针类型的变量在传递时,默认是传递指针本身(浅拷贝)。如果你希望复制的是指针所指向的数据,而不仅仅是指针本身,需要进行深拷贝。这对于避免多个指针指向同一内存区域,并在释放时发生冲突。
指针类型匹配
- 指针在操作时,必须确保指针类型匹配。例如,不能将
int*
类型的指针赋值给char*
类型的指针,除非你明确了解数据的布局并且使用类型转换。
指针运算
- 指针可以进行算术运算(例如递增或递减),但必须小心使用。指针运算会根据指针的类型调整偏移量,例如,对于
int*
指针,每次递增将增加 4 字节(在 32 位机器上)。
5.为什么使用智能指针?
自动内存管理:智能指针自动管理内存,不需要显式调用 delete
,避免了内存泄漏和悬挂指针问题。
异常安全:智能指针能够确保即使发生异常,内存也能被正确释放,避免了因为异常导致的资源泄漏。
简化代码:智能指针让代码更简洁,不需要手动管理内存释放,使得资源管理更直观和安全。
6.指针和引用有什么区别?
基本概念
- 指针(Pointer):指针是一个变量,它存储的是另一个变量的内存地址。通过指针,可以间接访问该内存地址上存储的值。
- 引用(Reference):引用是一个别名,表示对某个变量的引用。引用本身不占用内存空间,它只是一个现有变量的别名,使用引用就像使用原始变量一样。
语法区别
- 指针:声明时使用 *。
- 可以改变指向的对象。
- 指针可以为空(nullptr 或 NULL)。
- 例子:
int x = 10; int y = 20; int* p = &x; // p 是指向 x 的指针 p = &y; // p 现在指向 y
&
。nullptr
。例子:
int x = 10; int y = 20; int& r = x; // r 是 x 的引用 r = y; // r 现在不再是 x 的引用,而是 y 的引用
能否改变目标
- 指针:指针的值(即指向的内存地址)是可以改变的,指针可以指向不同的对象。
- 引用:一旦引用被初始化,它就绑定到某个对象上,不能再改变引用的目标。
空值
- 指针:指针可以为空,即指针可以不指向任何对象。通过
nullptr
可以明确表示指针不指向任何地方。 - 引用:引用必须绑定到一个有效的对象上。C++ 中没有 "空引用" 的概念,如果没有初始化引用,编译器会报错。
解引用
- 指针:可以使用
*
来解引用指针,访问指针所指向的值。
int x = 10; int* p = &x; std::cout << *p << std::endl; // 输出 10
int x = 10; int& r = x; std::cout << r << std::endl; // 输出 10
指向常量
- 指针指向常量:如果指针指向的是常量数据(例如
const
修饰的变量),则需要在指针声明时使用const
。
const int x = 10; const int* p = &x; // p 是指向常量 int 的指针
- 引用指向常量:引用也可以绑定到常量,声明时使用
const
。
const int x = 10; const int& r = x; // r 是指向常量 int 的引用
内存分配
- 指针:指针本身是一个变量,存储着地址值,它通常需要自己分配内存。如果指针是动态分配的,必须显式地释放内存(如使用
delete
)。 - 引用:引用没有自己的内存空间,它只是一个别名。它通常不会直接分配额外的内存,且引用的对象生命周期由原始变量控制。
总结对比
初始化 | 必须初始化,但可以为空 ( | 必须初始化,并且不可为空 |
能否改变目标 | 可以改变指向的对象 | 不能改变目标对象 |
语法 | 使用 | 使用 |
是否需要解引用 | 需要使用 | 不需要解引用,直接使用引用 |
空引用 | 支持空指针( | 不支持空引用 |
内存管理 | 指针需要手动管理内存( | 无需手动管理内存 |
常见用途 | 动态内存分配、多态、链表等 | 传递参数、返回值引用、简化代码 |
7.什么是野指针?如何造成的?怎样避免野指针?
野指针:是指一个指向已经被释放或不存在的内存位置的指针。野指针通常是因为内存被释放后,指针依然保持原来的值,导致指针指向无效的内存区域。访问或解引用这种指针会导致未定义的行为,包括程序崩溃或数据损坏。
产生野指针的原因
- 释放了内存后仍然使用指针:当一个指针指向的内存已经被
delete
或free
释放后,如果指针依然指向这块内存,那么这个指针就变成了野指针。
int* p = new int(10); delete p; // 释放了内存 // 这里 p 是野指针,指向已经释放的内存 std::cout << *p << std::endl; // 未定义行为
- 局部指针超出作用域:如果指针指向了一个局部变量,而这个局部变量在指针使用前已经超出了作用域(即局部变量被销毁),指针也会变成野指针。
int* p; { int x = 10; p = &x; // p 指向了 x } // x 在此作用域结束后销毁 // 这里 p 是野指针,指向已经销毁的变量 std::cout << *p << std::endl; // 未定义行为
- 返回局部变量的指针:如果一个函数返回指向局部变量的指针,当函数退出时,局部变量会被销毁,导致返回的指针变成野指针。
int* func() { int x = 10; return &x; // 返回指向局部变量 x 的指针 } // 返回的指针指向一个已经销毁的局部变量 int* p = func(); // p 是野指针 std::cout << *p << std::endl; // 未定义行为
- 重复释放内存(双重释放):如果
delete
或free
被多次调用在同一块内存上,会导致程序崩溃或其他不可预料的行为。这个问题有时被认为是“野指针”问题的一种表现。
int* p = new int(10); delete p; delete p; // 双重释放,导致未定义行为
如何避免野指针
- 避免使用悬空指针:在释放指针所指向的内存后,立即将指针设置为
nullptr
或NULL
,这可以防止指针变成野指针并避免意外访问。
int* p = new int(10); delete p; p = nullptr; // 设置为 nullptr,避免野指针
- 使用智能指针:C++11 引入的智能指针(如
std::unique_ptr
和std::shared_ptr
)能够自动管理内存。当智能指针超出作用域时,会自动释放内存,从而避免野指针的发生。
std::unique_ptr<int> p = std::make_unique<int>(10); // p 超出作用域时会自动释放内存
- 避免返回指向局部变量的指针:通过返回指向局部变量的指针来避免野指针问题。可以通过返回指向堆上分配的内存或使用引用来避免。
int* func() { int* p = new int(10); // 分配堆内存 return p; // 返回指向堆内存的指针 }
- 使用
nullptr
检查指针:在使用指针之前,始终检查指针是否为nullptr
,这有助于避免解引用野指针。
if (p != nullptr) { std::cout << *p << std::endl; // 解引用前检查 }
- 避免重复释放内存:确保每个
delete
或free
只调用一次。可以在delete
后将指针设为nullptr
来防止重复释放。
int* p = new int(10); delete p; p = nullptr; // 防止再次释放#牛客创作赏金赛##面经##面试##嵌入式##C/C++#
本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中