C和C++面试--看的不多只看一篇

C和C++面试

  • 1. 语言基础
    • 1.1 C和C++有什么区别?
    • 1.2 a和&a有什么区别?
    • 1.3 #define和const区别?(编译阶段、安全性、内存占用等)
    • 1.4 inline函数
    • 1.5 对于⼀个频繁使⽤的短⼩函数,应该使⽤什么来实现?有什么优缺点?
    • 1.6 静态链接和动态链接有什么区别?
    • 1.7 ⼀个参数可以既是const⼜是volatile吗?
    • 1.8 全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?
    • 1.9 typedef 和define 有什么区别?
    • 1.10 结构与联合有和区别?
    • 1.11 变量的声明和定义有什么区别?
    • 1.12 简述#ifdef、#else、#endif和#ifndef的作用
    • 1.13 c++中四种cast转换
    • 1.14 strcat、strncat、strcmp、strcpy哪些函数会导致内存溢出?如何改进?
    • 1.15 简述strcpy、sprintf 与memcpy 的区别?
    • 1.16 static的用法(定义和用途)(必考)
    • 1.17 C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
    • 1.18 const的用法(定义和用途)(必考)
    • 1.19 volatile作用和用法
    • 1.20 sizeof 与strlen (字符串,数组)
    • 1.21 形参与实参的区别?
    • 1.22 三种传递方式
    • 1.23 经典的sizeof(struct)和sizeof(union)内存对齐
    • 1.24 用变量a给出下面的定义
    • 1.25 使用32位编译情况下,给出判断所使用机器大小端的方法。
  • 2. 引用相关
    • 2.1引用与指针有什么区别?
    • 2.2 什么是“引用”?申明和使用“引用”要注意哪些问题?
    • 2.3 将“引用”作为函数参数有哪些特点?
    • 2.4 在什么时候需要使用“常引用”?
    • 2.5 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
    • 2.6 左值和右值
    • 2.7 左值引用
    • 2.8 右值引用
    • 2.9 右值引用和左值引用的区别
  • 3. 指针
    • 3.1 new和malloc
    • 3.2 C 语言的 malloc 和 C++ 中的 new 有什么区别
    • 3.3 delete与 delete [] 区别
    • 3.4 简述数组与指针的区别?
    • 3.5 C语言的指针和引用和c++的有什么区别?
    • 3.6 指针常量与常量指针区别?
    • 3.7 C语言的结构体和C++的有什么区别
    • 3.8 如何避免“野指针”
    • 2.9 引用和指针的区别?
  • 4. 野指针和悬空指针
    • 4.1 悬挂指针与野指针有什么区别?
    • 4.2 野指针:
    • 4.3 悬空指针:
  • 5. C++智能指针
    • 5.1 智能指针
    • 5.2 shared_ptr是如何实现的?
    • 5.3 对c++中四个智能指针:shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解
  • 6. 面向对象
    • 6.1 对虚函数和多态的理解
    • 6.2 多态,虚函数,纯虚函数
    • 6.3 动态多态有什么作⽤?有哪些必要条件?
    • 6.4 简述类成员函数的重写、重载和隐藏的区别
    • 6.5 重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?
    • 6.6 构造函数和析构函数可以调用虚函数吗?
    • 6.7 动态绑定是如何实现的?
    • 6.8 虚函数表是针对类的还是针对对象的?同⼀个类的两个对象的虚函数表是怎么维护的?
    • 6.9 为什么基类的构造函数不能定义为虚函数?
    • 6.10 为什么基类的析构函数需要定义为虚函数?
    • 6.11 多继承存在什么问题?如何消除多继承中的⼆义性?
    • 6.12 拷⻉构造函数和赋值运算符重载之间有什么区别?
    • 6.13 请你来说⼀下C++中struct和class的区别
    • 6.14 为什么析构函数⼀般写成虚函数
    • 6.15 拷⻉初始化和直接初始化,初始化和赋值的区别?
  • 7. STL
    • 7.1 什么是STL?
    • 7.2 什么时候需要⽤hash_map,什么时候需要⽤map?
    • 7.3 vector的底层原理
    • 7.4 vector中的reserve和resize的区别
    • 7.5 vector中的size和capacity的区别
    • 7.6 vector中erase方法与algorithn中的remove方法区别
    • 7.7 vector迭代器失效的情况
    • 7.8 正确释放vector的内存(clear(), swap(), shrink_to_fit())
    • 7.9 vector 扩容为什么要以1.5倍或者2倍扩容?
    • 7.10 vector的常⽤函数
    • 7.11 list的底层原理
    • 7.12 list的常⽤函数
    • 7.13 deque的底层原理和常⽤函数
    • 7.14 什么情况下用vector,什么情况下用list,什么情况下用deque
    • 7.15 priority_queue的底层原理
    • 7.16 map 、set、multiset、multimap的底层原理
    • 7.17 map 、set、multiset、multimap的特点
    • 7.18 为何map和set不能像vector⼀样有个reserve函数来预分配数据?
    • 7.19 map 、set、multiset、multimap的特点
    • 7.20 map 、set、multiset、multimap的常⽤函数

1. 语言基础

1.1 C和C++有什么区别?

C++是⾯向对象的语⾔,⽽C是⾯向过程的语⾔;
C++引⼊ new/delete 运算符,取代了C中的 malloc/free 库函数;
C++引⼊引⽤的概念,⽽C中没有;
C++引⼊类的概念,⽽C中没有;
C++引⼊函数重载的特性,⽽C中没有;

1.2 a和&a有什么区别?

假设数组int a[10]; int (p)[10] = &a;其中:
a是数组名,是数组⾸元素地址,+1表示地址值加上⼀个int类型的⼤⼩,如果a的值是0x00000001,加1操作
后变为0x00000005。
(a + 1) = a[1]。
&a是数组的指针,其类型为int (*)[10](就是前⾯提到的数组指针),其加1时,系统会认为是数组⾸地址加
上整个数组的偏移(10个int型变量),值为数组a尾元素后⼀个元素的地址。
若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引⽤时按照int类型⼤⼩来读取。

1.3 #define和const区别?(编译阶段、安全性、内存占用等)

区别一:
编译器处理⽅式不同: #define 宏是在预处理阶段展开,不能对宏定义进⾏调试,⽽ const 常量是在编译阶
段使⽤;
类型和安全检查不同: #define 宏没有类型,不做任何类型检查,仅仅是代码展开,可能产⽣边际效应等错
误,⽽ const 常量有具体类型,在编译阶段会执⾏类型检查;
存储⽅式不同: #define 宏仅仅是代码展开,在多个地⽅进⾏字符串替换,不会分配内存,存储于程序的代
码段中,⽽ const 常量会分配内存,但只维持⼀份拷⻉,存储于程序的数据段中。
定义域不同: #define 宏不受定义域限制,⽽ const 常量只在定义域内有效。

区别二:
1.用#define max 100 ; 定义的常量是没有类型的(不进行类型安全检查,可能会产生意想不到的错误),所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,define所定义的宏变量在预处理阶段的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换;

2.用const int max = 255 ; 定义的常量有类型(编译时会进行类型检查)名字,存放在内存的静态区域中,在编译时确定其值。在程序运行过程中const变量只有一个拷贝,而#define所定义的宏变量却有多个拷贝,所以宏定义在程序运行过程中所消耗的内存要比const变量的大得多;

3.有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

1.4 inline函数

在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。为了解决这个问题,特别的引入了inline修饰符,表示为内联函数。

大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开。

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。

1.5 对于⼀个频繁使⽤的短⼩函数,应该使⽤什么来实现?有什么优缺点?

应该使⽤ inline 内联函数,即编译器将 inline 内联函数内的代码替换到函数被调⽤的地⽅。

优点:
1.在内联函数被调⽤的地⽅进⾏代码展开,省去函数调⽤的时间,从⽽提⾼程序运⾏效率;
2.相⽐于宏函数,内联函数在代码展开时,编译器会进⾏语法安全检查或数据类型转换,使⽤更加安全;

缺点:
1.代码膨胀,产⽣更多的开销;
2.如果内联函数内代码块的执⾏时间⽐调⽤时间⻓得多,那么效率的提升并没有那么⼤;

3.如果修改内联函数,那么所有调⽤该函数的代码⽂件都需要重新编译;
1)内联声明只是建议,是否内联由编译器决定,所以实际并不可控。

1.6 静态链接和动态链接有什么区别?

静态链接是在编译链接时直接将需要的执⾏代码拷⻉到调⽤处;
优点在于程序在发布时不需要依赖库,可以独⽴执⾏;
缺点在于程序的体积会相对较⼤,⽽且如果静态库更新之后,所有可执⾏⽂件需要重新链接;

动态链接是在编译时不直接拷⻉执⾏代码,⽽是通过记录⼀系列符号和参数,在程序运⾏或加载时将这些信息
传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运⾏到指定代码时,在共享执⾏内
存中寻找已经加载的动态库可执⾏代码,实现运⾏时链接;

优点在于多个程序可以共享同⼀个动态库,节省资源;
缺点在于由于运⾏时加载,可能影响程序的前期执⾏性能。

1.7 ⼀个参数可以既是const⼜是volatile吗?

可以,⽤const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使⽤这个变量时,都要⼩⼼地去内存读取这个变量的值,⽽不是去寄存器读取它的备份。

注意:在此⼀定要注意const的意思,const只是不允许程序中的代码改变某⼀变量,其在编译期发挥作⽤,它并没有实际地禁⽌某段内存的读写特性。

1.8 全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?

全局变量是整个程序都可访问的变量,谁都可以访问,⽣存期在整个程序从运⾏到结束(在程序结束时所占内
存释放);
⽽局部变量存在于模块(⼦程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函
数调⽤完毕),局部变量消失,所占据的内存释放。
操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运⾏的
时候被加载.局部变量则分配在堆栈⾥⾯。

1.9 typedef 和define 有什么区别?

用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及
书写复杂使用频繁的宏。
执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其
发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确
的。
对指针的操作不同:typedef 和define 定义的指针时有很大的区别。
注 意 : t y p e d e f 定 义 是 语 句 , 因 为 句 尾 要 加 上 分 号 。 而 d e f i n e 不 是 语 句 , 千 万 不
能 在 句 尾 加 分 号 。

1.10 结构与联合有和区别?

(1). 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。

(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

1.11 变量的声明和定义有什么区别?

变量的定义为变量分配地址和存储空间,变量的声明不分配地址。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。

说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
int main()
{
extern int A;
//这是个声明而不是定义,声明A是一个已经定义了的外部变量
//注意:声明外部变量时可以把变量类型去掉如:extern A;
dosth(); //执行函数
}
int A; //是定义,定义了A为整型的外部变量

1.12 简述#ifdef、#else、#endif和#ifndef的作用

利用#ifdef、#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏

蔽。

#ifdef MATH
#include “math.c”
#endif

在子程序前加上标记,以便于追踪和调试。

#ifdef DEBUG
printf (“Indebugging…!”);
#endif

应对硬件的限制。由于一些具体应用环境的硬件不一样,限于条件,本地缺乏这种设备,只能绕过硬件,直

接写出预期结果。

注 意 : 虽 然 不 用 条 件 编 译 命 令 而 直 接 用 i f 语 句 也 能 达 到 要 求 , 但 那 样 做 目 标 程 序 长
( 因 为 所 有 语 句 都 编 译 ) , 运 行 时 间 长 ( 因 为 在 程 序 运 行 时 间 对 i f 语 句 进 行 测 试 ) 。
而 采 用 条 件 编 译 , 可 以 减 少 被 编 译 的 语 句 , 从 而 减 少 目 标 程 序 的 长 度 , 减 少 运 行 时
间 。

1.13 c++中四种cast转换

C++中四种类型转换是:static_cast,dynamic_cast,const_cast,reinterpret_cast:
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void*转指针等,static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

1.14 strcat、strncat、strcmp、strcpy哪些函数会导致内存溢出?如何改进?

1.strcpy函数会导致内存溢出。
strcpy拷贝函数不安全,他不做任何的检查措施,也不判断拷贝大小,不判断目的地址内存是否够用。
char *strcpy(char *strDest,const char *strSrc)
2.strncpy_s是安全的
strncpy拷贝函数,虽然计算了复制的大小,但是也不安全,没有检查目标的边界。
strncpy(dest, src, sizeof(dest));
3.strcmp(str1,str2),是比较函数,若str1=str2,则返回零;若str1str2,则返回正数。(比较字符串)
4.strcat()函数主要用来将两个char类型连接。例如:
char d[20]=“Golden”;
char s[20]=“View”;
strcat(d,s);
//打印d
printf(“%s”,d);
输出 d 为 GoldenView (中间无空格)

5.strncat()主要功能是在字符串的结尾追加n个字符。
char * strncat(char *dest, const char *src, size_t n);
延伸:
1)memcpy拷贝函数,它与strcpy的区别就是memcpy可以拷贝任意类型的数据,strcpy只能拷贝字符串类型。
2)memcpy 函数用于把资源内存(src所指向的内存区域)拷贝到目标内存(dest所指向的内存区域);有一个size变量控制拷贝的字节数;
void *memcpy(void *dest, void *src, unsigned int count);

1.15 简述strcpy、sprintf 与memcpy 的区别?

操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低。
实现功能不同,strcpy主要实现字符串变量间的拷贝,sprintf主要实现其他数据类型格式到字符串的转化,memcpy主要是内存块间的拷贝。

注意:strcpy、sprintf与memcpy都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

1.16 static的用法(定义和用途)(必考)

1)用static修饰局部变量:使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。

2)用static修饰全局变量:使其只在本文件内部有效,而其他文件不可连接或引用该变量。

3)用static修饰函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的(这一点在大工程中很重要很重要,避免很多麻烦,很常见)。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。

1.17 C 语言的关键字 static 和 C++ 的关键字 static 有什么区别

C中static用来修饰局部静态变量、外部静态变量和函数。
C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。

注意:编程时static的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而C++的静态成员则可以在多个对象实例间进行通信,传递信息。

1.18 const的用法(定义和用途)(必考)

const主要用来修饰变量、函数形参和类成员函数:

1)用const修饰常量:定义时就初始化,以后不能更改;
2)用const修饰形参:func(const int a){};该形参在函数里不能改变;
3)用const修饰类成员函数:该函数对成员变量只能进行只读操作;

注:主要知道const意味着"只读"就可以了。

const int a; // a是一个常整型数
int const a; // a是一个常整型数
const int *a; // a是一个指向常整型数的指针(整型数是不可修改的,但指针可以)
int * const a; // a是一个指向整型数的常指针(指针指向的整型数是可以修改的,但指针是不可修改的)
int const * a const; // a是一个指向常整型数的常指针(指针指向的整型数是不可修改的,同时指针也是不可修改的)

1.19 volatile作用和用法

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量在内存中的值,而不是使用保存在寄存器里的备份(虽然读写寄存器比读写内存快)。

以下几种情况都会用到volatile:
1、并行设备的硬件寄存器(如:状态寄存器);
2、一个中断服务子程序中会访问到的非自动变量;
3、多线程应用中被几个任务共享的变量;

注:
回答不出这个问题的人是不会被雇佣的。这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。

1.20 sizeof 与strlen (字符串,数组)

sizeof与strlen有以下区别:

1.sizeof是运算符,而strlen是C语言标准库函数;
2.strlen 测量的是字符串的实际长度,以’\0’ 结束,返回结果不包括’\0’ ;
3.sizeof 测量的是字符的分配大小,它的参数可以是数组、指针、类型、对象、函数等;
4.编译器在编译时就计算出了sizeof的结果,而strlen必须在运行时才能计算出来;

1.21 形参与实参的区别?

形参属于函数内部的局部变量, 在调用函数时才会分配内存, 在函数调用之后会被释放掉, 因此在函数内部才有效

实参可以使常量, 表达式, 函数等, 无论是何种类型,在函数调用时都必须有一个确定的值,以便把函数的值传递给形参

实参和形参的个数一定要严格匹配(当然可以忽略有默认值形参), 通常情况下函数类型也是应该严格匹配的, 但是允许隐式类型变换,如果类中定义了零参数构造函数,甚至可以使用空初始化列表{}的方式调用零参数构造函数

实参到形参的传递是单向的

形参类型为非指针非引用, 则传递方式为值传递则, 形参为实参的副本, 对形参的任何修改都不会反应在主调函数中

1.22 三种传递方式

值传递是通过拷贝构造函数实现的

指针传递是属于值传递,实参指针向形参传递的是对象的地址

引用传是属于传地址, 相当于对变量起了一个别名, 本质上和指针传递类似传递的都是对象的地址,区别在于对该引用形参的任何操作都会被处理为间接云芝, 也就是会反应到调用函数中

1.23 经典的sizeof(struct)和sizeof(union)内存对齐

内存对齐作用:

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

结构体struct内存对齐的3大规则:

1.对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍;

2.结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍;

3.如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。

#pragma pack(1)
 
struct fun{
  int i;
  double d;
  char c;
};

sizeof(fun) = 13

struct CAT_s
{
    int ld;
    char Color;
    unsigned short Age;
    char *Name;
    void(*Jump)(void);
}Garfield;

1.使用32位编译,int占4, char 占1, unsigned short 占2,char* 占4,函数指针占4个,由于是32位编译是4字节对齐,所以该结构体占16个字节。(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是int是最长的字节,所以按4字节对齐);

2.使用64位编译 ,int占4, char 占1, unsigned short 占2,char* 占8,函数指针占8个,由于是64位编译是8字节对齐(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是函数指针是最长的字节,所以按8字节对齐)所以该结构体占24个字节。

//64位
struct C 
{ 
 double t;   //8   1111 1111
 char b;  //1      1
 int a;   //4      0001111  
 short c;  //2     11000000
};  
 sizeof(C) = 24;  //注意:1 4 2 不能拼在一起

char是1,然后在int之前,地址偏移量得是4的倍数,所以char后面补三个字节,也就是char占了4个字节,然后int四个字节,最后是short,只占两个字节,但是总的偏移量得是double的倍数,也就是8的倍数,所以short后面补六个字节

联合体union内存对齐的2大规则:

1.找到占用字节最多的成员;

2.union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员

//x64
typedef union {
    long i;
    int k[5];
    char c;
}D

要计算union的大小,首先要找到占用字节最多的成员,本例中是long,占用8个字节,int k[5]中都是int类型,仍然是占用4个字节的,然后union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员,为了要容纳k(20个字节),就必须要保证是8的倍数的同时还要大于20个字节,所以是24个字节。

1.24 用变量a给出下面的定义

a) 一个整型数;
b)一个指向整型数的指针;
c)一个指向指针的指针,它指向的指针是指向一个整型数;
d)一个有10个整型的数组;
e)一个有10个指针的数组,该指针是指向一个整型数;
f)一个指向有10个整型数数组的指针;
g)一个指向函数的指针,该函数有一个整型参数并返回一个整型数;
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
答案:

a)int a
b)int *a;
c)int **a;
d)int a[10];
e)int *a [10];
f) int a[10], *p=a;
g)int (*a)(int)
h) int( *a[10])(int)

1.25 使用32位编译情况下,给出判断所使用机器大小端的方法。

C和C++面试--看的不多只看一篇_第1张图片
1.联合体方法判断方法:

利用union结构体的从低地址开始存,且同一时间内只有一个成员占有内存的特性。大端储存符合阅读习惯。联合体占用内存是最大的那个,和结构体不一样。

a和c公用同一片内存区域,所以更改c,必然会影响a的数据

#include 
int main(){
  union w
  {
      int a;
      char b;
  }c;
  c.a = 1;
  if(c.b == 1)
   printf("小端存储\n");
  else
   printf("大端存储\n");
 return 0;
}

2.指针方法

通过将int强制类型转换成char单字节,p指向a的起始字节(低字节)

#include 
int main ()
{
    int a = 1;
    char *p = (char *)&a;
    if(*p == 1)
    {
        printf("小端存储\n");
    }
    else
    {
        printf("大端存储\n");
    }
    return 0;
}

2. 引用相关

2.1引用与指针有什么区别?

  1. 引用必须被初始化,指针不必;
  2. 引用初始化以后不能被改变,指针可以改变所指的对象;
  3. 不存在指向空值的引用,但是存在指向空值的指针;

2.2 什么是“引用”?申明和使用“引用”要注意哪些问题?

答:引用就是某个目标变量的“别名”,对应用的操作与对变量直接操作效果完全相同。

申明一个引用的时候,切记要对其进行初始化。

引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。

声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。

2.3 将“引用”作为函数参数有哪些特点?

(1)传递引用给函数与传递指针的效果是一样的。

这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;

如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

2.4 在什么时候需要使用“常引用”?

如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。

常引用声明方式:const 类型标识符 &引用名=目标变量名;

例1
int a;
const int &ra=a;
ra=1; //错误
a=1; //正确
例2
string foo( );
void bar(string & s);
那么下面的表达式将是非法的:
bar(foo( ));
bar(“hello world”);
原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。
因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const 。

2.5 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?

格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }
好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!
注意事项:

(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部 new 分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)流操作符重载返回值申明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << “hello” << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。

但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。

因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。

赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
#include
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;
cout< cout< }
int &put(int n) {
if (n>=0 && n<=9 ) return vals[n];
else {
cout<<“subscript error”; return error;
}
}

(5)在另外的一些操作符中,却千万不能返回引用:±*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。

主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。

根据前面提到的引用作为返回值的三个规则,2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。

2.6 左值和右值

在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。

左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;

右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。

一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。

2.7 左值引用

左值引用就是我们平常使用的“引用”。引用是为对象起的别名,必须被初始化,与变量绑定到一起,且将一直绑定在一起。

我们通过 & 来获得左值引用,
type &引用名 = 左值表达式;
可以把引用绑定到一个左值上,而不能绑定到要求转换的表达式、字面常量或是返回右值的表达式。举个例子:

int i = 42;
int &r = i; //正确,左值引用
int &r1 = i * 42; //错误, i*42是一个右值
const int &r2 = i * 42; //正确,可以将一个const的引用绑定到一个右值上

2.8 右值引用

右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。

它的主要目的有两个方面:

消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数。

右值引用就是必须绑定到右值的引用,他有着与左值引用完全相反的绑定特性,我们通过 && 来获得右值引用。

右值引用的基本语法type &&引用名 = 右值表达式;

右值有一个重要的性质——只能绑定到一个将要销毁的对象上。举个例子:

int &&rr = i; //错误,i是一个变量,变量都是左值
int &&rr1 = i 42; //正确,i42是一个右值

作用:
右值引⽤的主要⽬的是为了实现转移语义和完美转发,消除两个对象交互时不必要的对象拷⻉,也能够更加简洁明确地定义泛型函数;

2.9 右值引用和左值引用的区别

1.左值可以寻址,而右值不可以。

2.左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。

3.左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

3. 指针

3.1 new和malloc

1)malloc和free是c++/c语言的库函数,需要头文件支持stdlib.h;new和delete是C++的关键字,不需要头文件,需要编译器支持;
2)使用new操作符申请内存分配时,无需指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地支持所需内存的大小。
3)new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型。
4)new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL。
5)delete会调用对象的析构函数,free只会释放内存;
6)对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。
①对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
②由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
③因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。

3.2 C 语言的 malloc 和 C++ 中的 new 有什么区别

1.new 、delete 是操作符,可以重载,只能在C++ 中使用。
2.malloc、free 是函数,可以覆盖,C、C++ 中都可以使用。
3.new 可以调用对象的构造函数,对应的delete 调用相应的析构函数。
4.malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
5.new 、delete 返回的是某种数据类型指针,malloc、free 返回的是void 指针。

注意:malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混用。

3.3 delete与 delete [] 区别

delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。

”delete与new配套,delete []与new []配套,delete[]会调用数组元素的析构函数。

3.4 简述数组与指针的区别?

数组要么在静态存储区被创建(全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
(2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof§,p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容;
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
char a[] = “hello world”;
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof§ << endl; // 4 字节

计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4 字节而不是100 字节
}

3.5 C语言的指针和引用和c++的有什么区别?

1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对
象;
4.可以有const指针,但是没有const引用;
5.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
6.指针可以有多级指针(**p),而引用止于一级;
7.指针和引用使用++运算符的意义不一样;
8.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

3.6 指针常量与常量指针区别?

指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。
常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

3.7 C语言的结构体和C++的有什么区别

C语言的结构体是不能有函数成员的,而C++的类可以有。
C语言的结构体中数据成员是没有private、public和protected访问限定的。而C++的类的成员有这些访问
限定。
C语言的结构体是没有继承关系的,而C++的类却有丰富的继承关系。

注意:虽然C的结构体和C++的类有很大的相似度,但是类是实现面向对象的基础, 而结构体只可以简单地理解为类的前身。

3.8 如何避免“野指针”

1.指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向
NULL。
2.指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向
NULL。
3.指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向
NULL。

2.9 引用和指针的区别?

1.初始化:
1)引用在定义的时候必须进行初始化,并且不能够改变
2)指针在定义的时候不一定要初始化,并且指向的空间可变
2.访问逻辑不同:
1)通过指针访问对象, 用户需要使用间接访问
2)通过引用访问对象, 用户只需使用直接访问, 编译器负责将其处理为间接访问
3.运算结果不同:
1)传指针的实质是传值,传递的值是指针内储存的变量地址;
2)传引用的实质是传地址,传递的是变量的地址。
3)指针通过下标运算结果是指针所指值为基地址加上偏移, 且基地址可变.
4)引用通过下标运算结果是引用的是数组才能有这个操作.
5)自增运算结果不同
6)sizeof运算的结果不同
下标运算:
函数参数:
多级: 有多级指针,但是没有多级引用,只能有一级引用。

4. 野指针和悬空指针

4.1 悬挂指针与野指针有什么区别?

悬挂指针:当指针所指向的对象被释放,但是该指针没有任何改变,以⾄于其仍然指向已经被回收的内存地
址,这种情况下该指针被称为悬挂指针;
野指针:未初始化的指针被称为野指针。

4.2 野指针:

野指针:

野指针指向一个已删除的对象或未申请访问受限内存区域的指针。

与空指针不同,野指针无法通过简单地判断是否为NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

(1)成因

野指针主要是因为这些疏忽而出现的删除或申请访问受限内存区域的指针。

a.指针变量未初始化:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。

所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

b.指针释放之后未置空:有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

c.指针操作超越变量作用域:不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。示例程序如下:

class A {
public:
void Func(void){ cout << “Func of class A” << endl; }
};
class B {
public:
A *p;
void Test(void) {
A a;
p = &a; // 注意 a 的生命期 ,只在这个函数Test中,而不是整个class B
}
void Test1() {
p->Func(); // p 是“野指针”
}
};

函数 Test1 在执行语句 p->Func()时,p 的值还是 a 的地址,对象 a 的内容已经被清除,所以 p 就成了“野指针” 。

(2)规避

a.初始化时置 NULL

指针变量一定要 初始化为NULL,因为任何指针变量(除了static修饰的指针变量)刚被创建时不会自动成为NULL指针,它的 缺省值是随机的。

b。释放时置 NULL

当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。例如:

int *p=newint(6);
delete p;
// 应加入 p=NULL; 以防出错
// …
if(p != NULL)
{
*p=7;
cout << p << endl;
}

对于使用 free 的情况,常常定义一个宏或者函数 xfree 来代替 free 置空指针:

#define xfree(x) free(x); x = NULL;
// 在 C++ 中应使用 nullptr 指代空指针
// 一些平台上的 C/C++ 已经预先添加了 xfree 拓展,如 GNU 的

libiberty
xfree§;
// 用函数实现,例如 GitHub 上的 AOSC-Dev/Anthon-Starter #9:
static inline void *Xfree(void *ptr) {
free(ptr);
#ifdef __cplusplus
return nullptr;
#else
return NULL;
#endif
}
q=Xfree(q);
所以动态分配内存后,如果使用完这个动态分配的内存空间后,必须习惯性地使用delete操作符取释放它。

4.3 悬空指针:

在C/C++等语言中,悬空指针(Dangling Pointer)指的是:一个指针的指向对象已被删除,那么就成了悬空指针。野指针是那些未初始化的指针。
int main(){
chardp = NULL;
for(i=0;i<1;i++) {
char c;
dp =&c;
}
/
注意c的声明周期 /
/
dp 此时为悬空指针 /
}
//这里for循环只是说明c是局部变量
void f(){
char
dp;
/* dp 未初始化,是野指针 */
}

5. C++智能指针

5.1 智能指针

智能指针是⼀个RAII类模型,⽤于动态分配内存,其设计思想是将基本类型指针封装为(模板)类对象指针,并在离开作⽤域时调⽤析构函数,使⽤ delete 删除指针所指向的内存空间。

智能指针的作⽤是,能够处理内存泄漏问题和空悬指针问题。

分为 unique_ptr 、 shared_ptr 和 weak_ptr 三种,各⾃的特点:

1.对于 unique_ptr ,实现独占式拥有的概念,同⼀时间只能有⼀个智能指针可以指向该对象,因为⽆法进⾏
拷⻉构造和拷⻉赋值,但是可以进⾏移动构造和移动赋值;
2.对于 shared_ptr ,实现共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象及相关资源会在
其所指对象不再使⽤之后,⾃动释放与对象相关的资源;
3.对于 weak_ptr ,解决 shared_ptr 相互引⽤时,两个指针的引⽤计数永远不会下降为0,从⽽导致死锁问
题。⽽ weak_ptr 是对对象的⼀种弱引⽤,可以绑定到 shared_ptr ,但不会增加对象的引⽤计数。

5.2 shared_ptr是如何实现的?

  1. 构造函数中计数初始化为1;
  2. 拷⻉构造函数中计数值加1;
  3. 赋值运算符中,左边的对象引⽤计数减1,右边的对象引⽤计数加1;
  4. 析构函数中引⽤计数减1;
  5. 在赋值运算符和析构函数中,如果减1后为0,则调⽤ delete 释放对象。

5.3 对c++中四个智能指针:shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解

C++⾥⾯的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11⽀持,并且第⼀个已经
被11弃⽤。
智能指针的作⽤是管理⼀个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使⽤智能指针可以很⼤程度上的避免这个问题,因为智能指针就是⼀个类,当超出了类的作⽤域是,类会⾃动调⽤析构函数,析构函数会⾃动释放资源。所以智能指针的作⽤原理就是在函数结束时⾃动释放内存空间,不需要⼿动释放内存空间。

unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发⽣异常⽽忘记调⽤delete”)特别有⽤。
采⽤所有权模式。
unique_ptr p3 (new string (“auto”)); //#4
unique_ptr p4; //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3⾮法,避免了p3不再指向有效数据的问题。因此,unique_ptr⽐auto_ptr更安全。
另外unique_ptr还有更聪明的地⽅:当程序试图将⼀个 unique_ptr 赋值给另⼀个时,如果源 unique_ptr 是个临
时右值,编译器允许这么做;如果源 unique_ptr 将存在⼀段时间,编译器将禁⽌这么做,⽐如:

unique_ptr pu1(new string (“hello world”));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string (“You”)); // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。⽽#2不会留下悬挂的unique_ptr,因为它调⽤
unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况⽽已的⾏为
表明,unique_ptr 优于允许两种赋值的auto_ptr 。
「注意」:如果确实想执⾏类似与#1的操作,要安全的᯿⽤这种指针,可给它赋新值。C++有⼀个标准库函数
std::move(),让你能够将⼀个unique_ptr赋给另⼀个。例如:

unique_ptr ps1, ps2;
ps1 = demo(“hello”);
ps2 = move(ps1);
ps1 = demo(“alexia”);
cout << *ps2 << *ps1 << endl;

shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后⼀个引⽤被销
毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被⼏个指针共享。
可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传⼊auto_ptr,
unique_ptr,weak_ptr来构造。当我们调⽤release()时,当前指针会释放资源所有权,计数减⼀。当计数等于0时,
资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使⽤引⽤计数的机制上提供了可
以共享所有权的智能指针。
成员函数:
use_count 返回引⽤计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引⽤计数的减少
get 返回内部对象(指针), 由于已经᯿载了()⽅法, 因此和直接使⽤对象是⼀样的.如 shared_ptrsp(new int(1)); sp 与
sp.get()是等价的

weak_ptr

weak_ptr 是⼀种不控制对象⽣命周期的智能指针, 它指向⼀个 shared_ptr 管理的对象. 进⾏该对象的内存管理的是那个强引⽤的 shared_ptr. weak_ptr只是提供了对管理对象的⼀个访问⼿段。weak_ptr 设计的⽬的是为配合
shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作, 它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象
构造, 它的构造和析构不会引起引⽤记数的增加或减少。weak_ptr是⽤来解决shared_ptr相互引⽤时的死锁问题,如果说两个shared_ptr相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调⽤lock函数来获得shared_ptr。

class B;
class A
{
public:
shared_ptr pb_;
~A()
{
cout<<"A delete
";
}
};
class B
{
public:
shared_ptr pa_;
~B()
{
cout<<"B delete
";
}
};
void fun()
{
shared_ptr pb(new B());
shared_ptr
pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout< cout< }
int main()
{
fun();
return 0;
}

可以看到fun函数中pa ,pb之间互相引⽤,两个资源的引⽤计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调⽤),如果把其中⼀个改为weak_ptr就可以了,我们把类A⾥⾯的shared_ptr pb; 改为weak_ptr pb; 运⾏结果如
下,这样的话,资源B的引⽤开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减⼀,同时pa析构时使A的计数减⼀,那么A的计数为0,A得到释放。

「注意」:不能通过weak_ptr直接访问对象的⽅法,⽐如B对象中有⼀个⽅法print(),我们不能这样访问,pa->pb-

print(); 英⽂pb是⼀个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

6. 面向对象

6.1 对虚函数和多态的理解

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。
举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。

虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。
当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。

注:
使用了虚函数,会增加访问内存开销,降低效率。

6.2 多态,虚函数,纯虚函数

1.多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;

在程序编译时多态性体现在函数和运算符的重载上;

2.虚函数:在基类中冠以关键字 virtual 的成员函数。它提供了一种接口界面。允许在派生类中对基类的虚函数重新定义。

3.纯虚函数的作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。作为接口而存在 纯虚函数不具备函数的功能,一般不能直接被调用。

实现⽅式是在虚函数声明的结尾加上 = 0 即可。

从基类继承来的纯虚函数,在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。

抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类必须用作派生其他类的基类,而不能用于直接创建对象实例。但仍可使用指向抽象类的指针支持运行时多态性。

6.3 动态多态有什么作⽤?有哪些必要条件?

动态多态的作⽤:
隐藏实现细节,使代码模块化,提⾼代码的可复⽤性;
接⼝重⽤,使派⽣类的功能可以被基类的指针/引⽤所调⽤,即向后兼容,提⾼代码的可扩充性和可维护性。
动态多态的必要条件:
需要有继承;
需要有虚函数覆盖;
需要有基类指针/引⽤指向⼦类对象。

6.4 简述类成员函数的重写、重载和隐藏的区别

(1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
virtual的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被virtual
修饰,也可以没有。

(2)隐藏和重写、重载有以下几点不同。
与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。

注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。

6.5 重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?

1.从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
2.从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。

那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。

也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!

重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。

因此,这样的函数地址是在运行期绑定的(晚绑定)。

6.6 构造函数和析构函数可以调用虚函数吗?

1.在C++中,提倡不在构造函数和析构函数中调用虚函数;

2.在构造函数和析构函数调用的所有函数(包括虚函数)都是编译时确定的, 虚函数将运行该类中的版本.

1)因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编;

2)析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

6.7 动态绑定是如何实现的?

当编译器发现类中有虚函数时,会创建⼀张虚函数表,把虚函数的函数⼊⼝地址放到虚函数表中,并且在对象中增加⼀个指针 vptr ,⽤于指向类的虚函数表。当派⽣类覆盖基类的虚函数时,会将虚函数表中对应的指针进⾏替
换,从⽽调⽤派⽣类中覆盖后的虚函数,从⽽实现动态绑定。

6.8 虚函数表是针对类的还是针对对象的?同⼀个类的两个对象的虚函数表是怎么维护的?

虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存⼀个指向该类虚函数表的指针 vptr ,每个对象的 vptr 的存放地址都不同,但都指向同⼀虚函数表。

6.9 为什么基类的构造函数不能定义为虚函数?

虚函数的调⽤依赖于虚函数表,⽽指向虚函数表的指针 vptr 需要在构造函数中进⾏初始化,所以⽆法调⽤定义为
虚函数的构造函数。

6.10 为什么基类的析构函数需要定义为虚函数?

为了实现动态绑定,基类指针指向派⽣类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调⽤基类的析
构函数,只能销毁派⽣类对象中的部分数据,所以必须将析构函数定义为虚函数,从⽽在对象销毁时,调⽤派⽣类
的析构函数,从⽽销毁派⽣类对象中的所有数据。

6.11 多继承存在什么问题?如何消除多继承中的⼆义性?

  1. 增加程序的复杂度,使得程序的编写和维护⽐较困难,容易出错;
  2. 在继承时,基类之间或基类与派⽣类之间发⽣成员同名时,将出现对成员访问的不确定性,即同名⼆义性;
    消除同名⼆义性的⽅法:
    利⽤作⽤域运算符 :: ,⽤于限定派⽣类使⽤的是哪个基类的成员;
    在派⽣类中定义同名成员,覆盖基类中的相关成员;
  3. 当派⽣类从多个基类派⽣,⽽这些基类⼜从同⼀个基类派⽣,则在访问此共同基类的成员时,将产⽣另⼀种不
    确定性,即路径⼆义性;
    消除路径⼆义性的⽅法:
    消除同名⼆义性的两种⽅法都可以;
    使⽤虚继承,使得不同路径继承来的同名成员在内存中只有⼀份拷⻉。

6.12 拷⻉构造函数和赋值运算符重载之间有什么区别?

拷⻉构造函数⽤于构造新的对象;
Student s;
Student s1 = s; // 隐式调⽤拷⻉构造函数
Student s2(s); // 显式调⽤拷⻉构造函数

赋值运算符重载⽤于将源对象的内容拷⻉到⽬标对象中,⽽且若源对象中包含未释放的内存需要先将其释放;
Student s;
Student s1;
s1 = s; // 使⽤赋值运算符

6.13 请你来说⼀下C++中struct和class的区别

在C++中,class和struct做类型定义是只有两点区别:
默认继承权限不同,class继承默认是private继承,⽽struct默认是public继承
class还可⽤于定义模板参数,像typename,但是关键字struct不能同于定义模板参数 C++保留struct关键
字,原因
保证与C语⾔的向下兼容性,C++必须提供⼀个struct
C++中的struct定义必须百分百地保证与C语⾔中的struct的向下兼容性,把C++中的最基本的对象单元规定为
class⽽不是struct,就是为了避免各种兼容性要求的限制
对struct定义的扩展使C语⾔的代码能够更容易的被移植到C++中

6.14 为什么析构函数⼀般写成虚函数

由于类的多态性,基类指针可以指向派⽣类的对象,如果删除该基类的指针,就会调⽤该指针指向的派⽣类析构函数,⽽派⽣类的析构函数⼜⾃动调⽤基类的析构函数,这样整个派⽣类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调⽤基类的析构函数⽽不调⽤派⽣类析构函数,这样就会造成派⽣类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是⼗分必要的。在实现多态时,当⽤基类操作派⽣类,在析构时防⽌只析构基类⽽不析构派⽣类的状况发⽣,要将基类的析构函数声明为虚函数。
举个例⼦:

6.15 拷⻉初始化和直接初始化,初始化和赋值的区别?

1.ClassTest ct1(“ab”); 这条语句属于直接初始化,它不需要调⽤复制构造函数,直接调⽤构造函数
ClassTest(constchar pc),所以当复制构造函数变为私有时,它还是能直接执⾏的。
2.ClassTest ct2 = “ab”; 这条语句为复制初始化,它⾸先调⽤构造函数 ClassTest(const char
pc) 函数创建⼀个
临时对象,然后调⽤复制构造函数,把这个临时对象作为参数,构造对象ct2;所以当复制构造函数变为私有
时,该语句不能编译通过。
3.ClassTest ct3 = ct1;这条语句为复制初始化,因为 ct1 本来已经存在,所以不需要调⽤相关的构造函数,⽽直
接调⽤复制构造函数,把它值复制给对象 ct3;所以当复制构造函数变为私有时,该语句不能编译通过。
4.ClassTest ct4(ct1);这条语句为直接初始化,因为 ct1 本来已经存在,直接调⽤复制构造函数,⽣成对象
ct3 的副本对象 ct4。所以当复制构造函数变为私有时,该语句不能编译通过。
要点就是拷⻉初始化和直接初始化调⽤的构造函数是不⼀样的,但是当类进⾏复制时,类会⾃动⽣成⼀个临时的对象,然后再进⾏拷⻉初始化。

7. STL

7.1 什么是STL?

C++ STL从⼴义来讲包括了三类:算法,容器和迭代器。
算法包括排序,复制等常⽤算法,以及不同容器特定的算法。
容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是
set,map等。
迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

7.2 什么时候需要⽤hash_map,什么时候需要⽤map?

总体来说,hash_map 查找速度会⽐ map 快,⽽且查找速度基本和数据数据量⼤⼩,属于常数级别;⽽ map 的查找速度是 log(n) 级别。

并不⼀定常数就⽐ log(n) ⼩,hash 还有 hash 函数的耗时,明⽩了吧,如果你考虑效率,特别是在元素达到⼀定数量级时,考虑考虑 hash_map。但若你对内存使⽤特别严格,希望程序尽可能少消耗内存,那么⼀定要⼩⼼,

hash_map 可能会让你陷⼊尴尬,特别是当你的 hash_map 对象特别多时,你就更⽆法控制了。⽽且 hash_map
的构造速度较慢。

现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使⽤ 。

7.3 vector的底层原理

vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。

当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间[vector内存增长机制]。

当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。

7.4 vector中的reserve和resize的区别

reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以
提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少
达到参数所指定的大小n。reserve()只有一个参数。
resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有
多个参数。

7.5 vector中的size和capacity的区别

size表示当前vector中有多少个元素(finish - start);
capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);

7.6 vector中erase方法与algorithn中的remove方法区别

vector中erase方法真正删除了元素,迭代器不能访问了
remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操
作,不知道容器的内部结构,所以无法进行真正的删除。

7.7 vector迭代器失效的情况

当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下
一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。

7.8 正确释放vector的内存(clear(), swap(), shrink_to_fit())

vec.clear():清空内容,但是不释放内存。
vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
vec.shrink_to_fit():请求容器降低其capacity和size匹配。
vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

7.9 vector 扩容为什么要以1.5倍或者2倍扩容?

根据查阅的资料显示,考虑可能产⽣的堆空间浪费,成倍增⻓倍数不能太⼤,使⽤较为⼴泛的扩容⽅式有两种,以2倍的⽅式扩容,或者以1.5倍的⽅式扩容。

以2倍的⽅式扩容,导致下⼀次申请的内存必然⼤于之前分配内存的总和,导致之前分配的内存不能再被使⽤,所以最好倍增⻓因⼦设置为(1,2)之间:

C和C++面试--看的不多只看一篇_第2张图片

7.10 vector的常⽤函数

vector vec(10,100); 创建10个元素,每个元素值为100
vec.resize(r,vector(c,0)); ⼆维数组初始化
reverse(vec.begin(),vec.end()) 将元素翻转
sort(vec.begin(),vec.end()); 排序,默认升序排列
vec.push_back(val); 尾部插⼊数字
vec.size(); 向量⼤⼩
find(vec.begin(),vec.end(),1); 查找元素
iterator = vec.erase(iterator) 删除元素

7.11 list的底层原理

list的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,

各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或
删除一个元素,就配置或释放一个元素空间。

list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取

7.12 list的常⽤函数

list.push_back(elem) 在尾部加⼊⼀个数据
list.pop_back() 删除尾部数据
list.push_front(elem) 在头部插⼊⼀个数据
list.pop_front() 删除头部数据
list.size() 返回容器中实际数据的个数
list.sort() 排序,默认由⼩到⼤
list.unique() 移除数值相同的连续元素
list.back() 取尾部迭代器
list.erase(iterator) 删除⼀个元素,参数是迭代器,返回的是删除迭代器的下⼀个位置

7.13 deque的底层原理和常⽤函数

1.deque是⼀个双向开⼝的连续线性空间(双端队列),在头尾两端进⾏元素的插⼊跟删除操作都有理想的时间复杂度。

deque.push_back(elem) 在尾部加⼊⼀个数据。
deque.pop_back() 删除尾部数据。
deque.push_front(elem) 在头部插⼊⼀个数据。
deque.pop_front() 删除头部数据。
deque.size() 返回容器中实际数据的个数。
deque.at(idx) 传回索引idx所指的数据,如果idx越界,抛出out_of_range。

7.14 什么情况下用vector,什么情况下用list,什么情况下用deque

1.vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删
除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用
vector而非deque,因为deque的迭代器比vector迭代器复杂很多。

2.list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。

3.需要从首尾两端进行插入或删除操作的时候需要选择deque。

7.15 priority_queue的底层原理

priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最
高的那一个。

7.16 map 、set、multiset、multimap的底层原理

m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p 的 底 层 实 现 都 是 红 黑 树 , e p o l l 模 型 的 底 层 数
据 结 构 也 是 红 黑 树 , l i n u x 系 统 中 C F S 进 程 调 度 算 法 , 也 用 到 红 黑 树 。
红 黑 树 的 特 性 :
1.每个结点或是红色或是黑色;
2.根结点是黑色;
3.每个叶结点是黑的;
4.如果一个结点是红的,则它的两个儿子均是黑色;
5.每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。

7.17 map 、set、multiset、multimap的特点

set和multiset会根据特定的排序准则⾃动将元素排序,set中元素不允许重复,multiset可以重复。

map和multimap将key和value组成的pair作为元素,根据key的排序准则⾃动将元素排序(因为红⿊树也是⼆叉搜
索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。

map和set的增删改查速度为都是logn,是⽐较⾼效的。

7.18 为何map和set不能像vector⼀样有个reserve函数来预分配数据?

因为在map和set内部存储的已经不是元素本身了,⽽是包含元素的结点。也就是说map内部使⽤的Alloc并不是
map声明的时候从参数中传⼊的Alloc。

7.19 map 、set、multiset、multimap的特点

set和multiset会根据特定的排序准则⾃动将元素排序,set中元素不允许重复,multiset可以重复。

map和multimap将key和value组成的pair作为元素,根据key的排序准则⾃动将元素排序(因为红⿊树也是⼆叉搜
索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。

map和set的增删改查速度为都是logn,是⽐较⾼效的。

7.20 map 、set、multiset、multimap的常⽤函数

it map.begin() 返回指向容器起始位置的迭代器(iterator)
it map.end() 返回指向容器末尾位置的迭代器
bool map.empty() 若容器为空,则返回true,否则false
it map.find(k) 寻找键值为k的元素,并⽤返回其地址
int map.size() 返回map中已存在元素的数量
map.insert({int,string}) 插⼊元素
for (itor = map.begin(); itor != map.end()
{
if (itor->second == “target”)
map.erase(itor++) ; // erase之后,令当前迭代器指向其后继。
else
++itor;
}

你可能感兴趣的:(C++语言学习,c语言,c++,面试)