【指针】03.指针和引用、万能指针、野指针、智能指针
【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM
【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym
【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM
【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0
其他
指针和引用
32.指针和引用的异同是什么?
相同
- 都可以用于访问内存中的变量或对象。
- 都可以作为函数参数,传递变量或对象的引用或地址。
- 都可以用于动态分配内存,并在程序中进行内存管理。
- 都可以用于实现数据结构,如链表、树等。
- 都可以作为成员变量出现在类中,并用于实现类的数据成员和成员函数。
区别(区别很多,记这几个够了)
- 内存模型不同: 指针是一个变量,它存储着一个内存地址,而引用是一个别名,它是已经存在的变量或对象的别名。因此,指针本身占据内存空间,而引用不占用内存空间。
- 指针和引用的自增(++)运算符意义不同,指针是对内存地址自增,而引用是对值的自增。
- 指针需要解引用,引用使用时无需解引用(*)。
- 指针可变,引用只能在定义时被初始化一次,之后不可变。
- 指针可以为空,引用不能为空。
- “sizeof 指针”得到的是指针本身的大小,在32 位系统指针变量占用4字节内存,“sizeof 引用”得到的是所指向的变量(对象)的大小。
记忆:内增,可变空解S
#include "stdio.h"
int main(){
int x = 5;
int *p = &x;
int &q = x;
printf("%d %d\n",*p,sizeof(p));
printf("%d %d\n",q,sizeof(q));
}
//结果
5 8
5 4
由结果可知,引用使用时无需解引用(*),指针需要解引用;我用的是64位操作系统,“sizeof 指针”得到 的是指针本身的大小,及8个字节。而“sizeof 引用”得到的是的对象本身的大小及int的大小,4个字节。
33.为什么有了指针还需要引用
指针是一个变量,存储着内存地址,可以通过解引用操作符 *
来访问所指向的内存。指针可以为空(nullptr
),可以被重新赋值指向其他对象,甚至可以指向无效的内存地址。指针的优势在于它的灵活性和动态性,可以动态分配和释放内存,以及实现数据结构和动态数据结构的设计。指针也可以作为函数参数进行传递,从而实现在函数内部修改实参的值。
引用是一个别名,它为现有的对象提供了一个新的名称。引用必须在声明时初始化,并且不能被重新赋值引用其他对象。引用在语法上与被引用的对象相同,可以像使用对象本身一样使用引用。引用的优势在于它的简洁性和安全性,它提供了一种直接访问对象的方式,不需要解引用操作,同时不会涉及指针的复杂性和潜在的错误。
尽管指针和引用都可以用于在函数之间传递参数和访问对象,但它们有一些区别和适用场景:
-
空值(null value):指针可以为空,即指向空地址(
nullptr
),而引用必须始终引用一个有效的对象。当对象可能不存在或需要表示空值时,可以使用指针。例如,当函数需要返回一个可能为空的结果时,可以使用指针作为返回值。 -
重新赋值:指针可以被重新赋值指向其他对象,而引用一旦初始化后就不能被重新赋值。如果需要在函数内部修改实参的值,可以使用指针作为函数参数;如果只需要访问对象而不修改它,可以使用引用。
-
安全性和简洁语义:引用在语义上表示对现有对象的别名,不会产生空指针或无效引用的问题,因此引用相对更安全。同时,引用语义更直观和简洁,可以使代码更易读和易懂。
因此,指针和引用在不同的情况下具有不同的用途。指针提供了更大的灵活性和动态性,适用于需要动态分配内存、重新赋值、或表示可能为空的对象的情况。引用提供了更简洁和直接的访问方式,适用于只需访问对象而不需要重新赋值的情况。根据具体的需求和语义,可以选择使用指针或引用来满足编程的要求。
34.指针和引用如何相互转换?
指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。
int a = 5;
int *p = &a;
void fun(int &x){}//此时调用fun可使用 : fun(*p);
//p是指针,加个*号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,
//p指针指向的对象会转换成引用X。
引用转指针:把引用类型的对象用&取地址就获得指针了。
指针和引用之间如何转换?:question:
指针和引用之间的转换方式与对象之间的转换方式相似。可以使用以下方法进行转换:
- 将指向派生类对象的指针赋值给指向基类对象的指针是安全的,因为基类指针只能访问派生类对象的基类部分。将指向派生类对象的引用赋值给指向基类对象的引用也是安全的。
- 将指向基类对象的指针或引用转换为指向派生类对象的指针或引用,需要使用强制类型转换操作符(static_cast/dynamic_cast)。这种转换需要进行向下转型的安全检查。如果基类对象实际上是派生类对象,则向下转型成功。如果基类对象不是派生类对象,则向下转型失败,将导致未定义的行为。
35.函数调用时传入参数为引用、指针、传值的区别
- 传值
在传值方式中,函数会将参数的值复制一份,并在函数内部使用这份复制品。这意味着函数内部对参数的任何修改都不会影响函数外部的原始参数。
- 传指针
在传指针方式中,函数会接收参数的地址,也就是指向参数内存位置的指针。这意味着函数内部可以直接访问原始参数,并进行修改。
- 传引用
在传引用方式中,函数会接收参数的引用,也就是指向参数的别名。这意味着函数内部可以直接访问原始参数,并进行修改,就像传指针一样。但与传指针不同的是,我们在函数调用时不需要使用取地址符&来获取参数的地址,而是直接使用参数本身。
36.C++中的指针传参和引用传参底层原理你知道吗?
指针传参
本质上是值传递,它所传递的是一个地址值。
值传递过程中,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。如果改变被调函数中的指针地址本身,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
如果修改指针指向的地址的值,那就和引用传参一样。
引用传参
本质上是值传递,它所传递的是一个地址值,是由主调函数放进来的实参变量的地址。(引用底层是指针常量,指针本身指向不可变,指向的值可变)被调函数的形式参数也作为局部变量在栈中开辟了内存空间。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。
指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
37.在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?
什么情况用指针当参数,什么时候用引用,为什么?
- 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
- 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
- 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
38.在进行函数参数以及返回值传递时,其中使用引用传参的好处有哪些?
-
在函数内部可以对参数进行修改
-
提高函数调用和运行的效率。因为没有了传值和生成副本的时间和空间消耗,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
-
编程和阅读更清晰。使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;
但是有以下的限制:
- 不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁
- 不能返回函数内部分配的内存的引用。
- 可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。
万能指针void*
39.void * 使用
- void*可以指向任何类型的地址,但是带类型的指针不能指向void * 的地址
正常来说如果两个指针类型不一样的话,两个指针变量是不可以直接相等的。例如int* a, float* b,假如令a = b,会直接发生编译错误,而void * 指针可以等于任何类型的指针。但是反过来不可以,也就是说一个有类型的指针不能指向一个void类型的变量(哪怕此时void变量已经指向了一个有类型的地址)。
float f = 5.5;
float* pf = &f;
void* pv = pf;
float* pf2 = pv; //编译错误,有类型的指针变量不能指向void*变量
- void*指针只有强制类型转换以后才可以正常取值
int main(int argc, const char * argv[]) {
float f = 5.5;
float* pf = &f;
void* pv;
pv = pf; //这句是可以的
cout<<*pv<<endl; //编译错误,这样直接对pv取值是错误的
cout<<*(float*)pv<<endl; //强制类型转换后可以取值
return 0;
}
在令pv = pf后,此时pv和pf指向的是同一个地址,值相同,但是两者的类型是不一样的。pf作为浮点型指针,是可以直接取到浮点数的,但是pv必须要强制类型转换以后才可以取值,也就是说一个void*的指针必须要经过强制类型转换以后才有意义。
int main(int argc, const char * argv[]) {
float f = 5.5;
float* pf = &f;
void* pv;
pv = pf;
cout<<*(float*)pv<<endl; //强制类型转换后可以取值,值为5.5
cout<<*(int*)pv<<endl; //强制类型转换,值为1085276160
cout<<(int)(*(float*)pv)<<endl;//取值后再次类型转换,值为5
return 0;
}
如果把一个指向float * 的值的void * 指针,强制转换成int*也是不对的。也就是说地址保存了什么样的变量,就要转化成哪种类型的指针,否则就会出错。
- void*指针变量和普通指针一样可以通过等于0或者NULL来初始化,表示一个空指针
void* pv = 0;
void* pv2 = NULL;
cout<<pv <<endl; //值为0x0
cout<<pv2<<endl; //值为0x0
- 当void * 指针作为函数的输入和输出时,表示可以接受任意类型的输入指针和输出任意类型的指针
如果函数的输入类型为void*,在调用时由于是值传递,所以函数实际接收到的应该就是一个地址值。这个值可以是任意类型。
void* test(void* a)
{
return a;
}
int main() {
static int a = 5;
int* pi = &a;
cout<<pi<<endl; //值为0x100001060
cout<<test(pi)<<endl; //值为0x100001060
cout<<test((void*)pi)<<endl; //值为0x100001060
}
如果函数的输入类型为void*,在调用时由于是值传递,所以函数实际接收到的应该就是一个地址值。这个值可以是任意类型。
int a = 5;
int* pi = &a;
void* test()
{
return pi;
}
int main() {
cout<<test()<<endl; //值为0x100001060
}
输出时同样也是值传递,因此可以输出任意类型指针指向的地址。
- 和void的区别
//返回了一个空指针
void* say_hello(void* args)
{
cout << "Hello World!" << endl;
return 0;
}
//没有返回值
void say_hello(void* args)
{
cout << "Hello World!" << endl;
return;
}
其实两个函数实现的内容是一样的。但是void*返回类型的函数返回了一个空指针,而void型没有返回值。
40.void * 作用
- 函数传参时不确定类型,或者要支持多类型的传参;
void function(int dataType, void* data) {
// 根据dataType的不同值,进行不同的转换
switch (dataType) {
case 0:
int* a = (int*)data;
case 1:
char* a = (char*)data;
...
}
}
- 当函数的返回值不考虑类型指关心大小的时候
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
memcpy和memset对外接收任何类型的指针,这样是合理并且必要的,因为这是内存操作函数,是对bit进行操作的,考虑数据类型是没有任何意义的。
int *a=NULL;
a=(int *)malloc(sizeof(int));//返回的是void*,所以赋值给其他指针类型要强转一下
同样的,malloc函数只关注你要多大的内存,你需要把它怎么划分是你的事情,但是你需要显式的表明你是怎么划分的。这里语法要求是必须的,void *类型转为其他类型必须强制类型转换。
野指针
41.野指针是什么?
野指针是一种指针变量,它指向的内存地址已经不再被分配给该程序使用。
野指针通常会出现在以下情况下:
- 对未初始化的指针进行间接引用;
当指针被创建时,指针不可能自动指向NULL,这时,默认值是随机的,此时的指针成为野指针。
- 对已经释放的指针进行间接引用;
当指针被free或delete释放掉时,如果没有把指针设置为NULL,则会产生野指针(或叫悬空指针),因为释放掉的仅仅是指针指向的内存,并没有把指针本身释放掉。
- 对指向栈内存的指针在函数返回后进行间接引用;
下面有道题
42.如何避免野指针?
- 对指针进行初始化。
//将指针初始化为NULL。
char *p = NULL;
//用malloc分配内存
char * p = (char * )malloc(sizeof(char));
//用已有合法的可访问的内存地址对指针初始化
char num[30] = {0};
char *p = num;
- malloc函数分配完内存后需注意:
检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL。可以通过if语句来判断)
清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)
//s是需要置零的空间的起始地址; n是要置零的数据字节个数。
void bzero(void *s, int n);
//如果要清空空间的首地址为p,value为值,size为字节数。
void memset(void *start, int value, int size);
- 指针用完后释放内存,将指针赋NULL
delete(p);
p = NULL;
43.野指针和悬空指针
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
- 野指针 野指针,指针未初始化或已释放
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr
,这样在使用时编译器就不会直接报错,产生非法内存访问。
- 悬空指针 悬空指针,指针已释放但未置为 NULL 或者超出了其指向的内存区域
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr
。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
44.请问运行下面的Test()函数会有什么样的后果?
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf("%s\n", str);
}
打印野指针内容,可能是乱码。
GetMemory()返回的是指向栈内存的指针,但该栈内存已被释放,该指针的地址不是 NULL,成为野指针,新内容不可知。
45.请问运行下面的Test()函数会有什么样的后果?
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str,"hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf("%s\n", str);
}
}
篡改堆区野指针指向的内容,后果难以预料,非常危险。
free(str);之后,str成为野指针,没有置为NULL,if(str != NULL)语句不能阻止篡改操作。
智能指针
46.C++中的智能指针是什么?
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,使用
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)