C++笔试题汇总记录
- 一、概述
- 二、概念分类
- 1. 结构体
- 1. C 和 C++ 中 struct 有什么区别?
- 2. C++中的 struct 和 class 有什么区别?
- 2. 类相关
- 1. 类的大小
- 1. 空类的大小
- 2. 一般非空类大小
- 3. 有虚函数类
- 4. 有虚函数类的继承
- 5. 只有虚函数
- 6. 静态数据成员
- 2. C++的三大特性(封装、继承、多态)
- 3. 多态是怎么实现的?
- 4. 什么是虚函数和纯虚函数?以及区别?
- 5. 虚函数是怎么实现的?虚函数在那个阶段生成?虚函数是怎么实现的?虚函数在那个阶段生成?
- 6. 构造函数和析构函数的作用?
- 7. 构造函数和析构函数那个可以被声明为虚函数,为什么?
- 8. 构造函数和析构函数那个可以被重载,为什么?
- 9. this 指针,为什么会存在this指针?
- 10. 类继承中构造函数和析构函数的调用顺序、为什么析构函数要写成虚函数
- 三、内存管理
- 1. 指针
- 1. “引用”与指针的区别是什么?
- 2. malloc/free 和 new/delete 的区别
- 3. 如果在申请动态内存时找不到足够大的内存块,malloc 和 new 将返回 NULL 指针,宣告内存申请失败。你是怎么处理内存耗尽的?
- 4. 一个指针占几个字节
- 5. 什么是指针数组和数组指针
- 6. 什么是函数指针和指针函数以及区别
- 7. 常量指针和指针常量以及区别
- 8. C++程序在内存的分布图
- 9.
- 2. 引用
- 四、
- 五、
- 六、
一、概述
这里记录一下收集的常见的面试题,一些概念题,方便查看,后面会更新
二、概念分类
1. 结构体
1. C 和 C++ 中 struct 有什么区别?
Protection行为 | 能否定义函数 | |
---|---|---|
C | 无 | 否,但可以有函数指针 |
C++ | 有 | 可以,默认是public |
C 本身是面向过程的,但C++是面向对象的,所以,struct基本表现类似class,也是可以有构造的,
struct Person
{
Person() {
m_name = "";
m_age = 0;
}
Person(std::string &name, int &age) {
m_name = name;
m_age = age;
}
std::string m_name;
int m_age;
};
2. C++中的 struct 和 class 有什么区别?
从语法上讲,class和struct做类型定义时只有两点区别:
- 1.默认继承权限。如果不明确指定,来自class的继承按照private继承处理,而struct的继承按照public继承处理;
- 2.成员的默认访问权限。class的成员默认是private权限,struct默认是public权限。 除了这两点,class和struct基本就是一个东西。语法上没有任何其它区别。
就像下面的代码就是手动指定是private
struct Person
{
Person() {
m_name = "";
m_age = 0;
}
Person(std::string &name, int &age) {
m_name = name;
m_age = age;
}
private:
std::string m_name;
int m_age;
};
- pass
- pass
- pass
2. 类相关
1. 类的大小
这里设操作系统位数是32位,则指针大小为4字节,若64位则8字节
1. 空类的大小
为了给实例化的空类提供唯一地址,所以编译器会给空类隐含的添加一个字节,其大小为1
class CBase
{
};
std::cout<<"sizeof(CBase)="<<sizeof(CBase)<<endl; // sizeof(CBase)=1
就算是有函数,也是不会占用空间的
class CBase
{
void FuncA();
};
std::cout<<"sizeof(CBase)="<<sizeof(CBase)<<endl; // sizeof(CBase)=1
2. 一般非空类大小
class CBase
{
int a;
char *p;
}; // sizeof(CBase)=8
这里因为指针的大小取决于操作系统的位数,大小为4
3. 有虚函数类
class CBase
{
public:
CBase(void);
virtual ~CBase(void);
private:
int a;
char *p;
}; //sizeof(CBase)=12
C++ 类中有虚函数的时候有一个指向虚函数的指针(vptr),在32位系统分配指针大小为4字节
4. 有虚函数类的继承
class CChild :
public CBase
{
public:
CChild(void);
~CChild(void);
private:
int b;
}; //sizeof(CChild)=16;
子类的大小是本身成员变量的大小加上父类的大小。
5. 只有虚函数
class A
{
virtual void FuncA();
virtual void FuncB();
}; //sizeof(A)=4;
当C++ 类中有虚函数的时候,会有一个指向虚函数表的指针(vptr),在32位系统分配指针大小为4字节。所以size为4.
6. 静态数据成员
class A
{
int a;
static int b;
virtual void FuncA();
}; //sizeof(A)=8;
静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员.但是它不影响类的大小,不管这个类实际产生了多少实例,还是派生了多少新的类,静态成员数据在类中永远只有一个实体存在。
而类的非静态数据成员只有被实例化的时候,他们才存在.但是类的静态数据成员一旦被声明,无论类是否被实例化,它都已存在.可以这么说,类的静态数据成员是一种特殊的全局变量.
所以该类的size为:int a型4字节加上虚函数表指针4字节,等于8字节。
2. C++的三大特性(封装、继承、多态)
封装:将数据(成员变量)和对数据的操作(成员函数)捆绑在一起,并对外部隐藏数据的具体实现。
继承:通过继承,子类可以继承父类的属性和行为,从而实现代码的重用和扩展。
多态:允许不同的对象以相同的接口进行操作,即同一操作可以作用于不同的对象上,表现出不同的行为。多态有运行时多态(重写),有编译时多态(重载)
3. 多态是怎么实现的?
多态的实现主要依赖于虚函数和动态绑定。多态允许程序在运行时决定调用哪个具体的函数实现。
运行时多态:通过使用虚函数实现,允许基类指针或引用调用派生类中的重写函数。这是通过虚函数表(vtable)和虚指针(vptr)机制实现的。
4. 什么是虚函数和纯虚函数?以及区别?
虚函数 :在基类中声明为 virtual 的成员函数,允许派生类重写(override)该函数。虚函数实现运行时多态。
作用:通过基类指针或引用调用虚函数时,实际调用的是派生类中的重写函数。
纯虚函数:在基类中声明为 virtual 并且等于 0 的虚函数,例如 virtual void func() = 0;。
作用:定义接口,要求所有派生类必须实现纯虚函数。使得基类成为抽象类,不能实例化。
5. 虚函数是怎么实现的?虚函数在那个阶段生成?虚函数是怎么实现的?虚函数在那个阶段生成?
虚函数表:每个类(如果包含虚函数)会有一个虚函数表,它是一个指向虚函数实现的指针数组。每个对象会有一个指向虚函数表的指针(vptr)。
动态绑定:通过 vptr 指针,运行时可以查找虚函数表中的具体函数地址,决定调用哪个函数实现。
编译阶段:编译器生成虚函数表和虚指针。虚函数表在类定义时生成,虚指针在对象实例化时设置。
运行阶段:在程序运行时,当通过基类指针或引用调用虚函数时,程序会根据虚函数表的地址来找到实际应该调用的函数。这种机制实现了多态性,即允许不同的类对象对同一消息做出不同的响应。
6. 构造函数和析构函数的作用?
构造函数:在创建对象时初始化对象的状态。构造函数可以用于分配资源、初始化数据成员等。
特性:构造函数与类名相同,无返回值,可以被重载。
析构函数:在对象生命周期结束时清理对象的资源。析构函数负责释放构造函数分配的资源,如内存、文件句柄等。
特性:析构函数的名字前加 ~,无返回值,不能被重载。
7. 构造函数和析构函数那个可以被声明为虚函数,为什么?
析构函数可以被声明为虚函数:如果基类的析构函数是虚函数,确保派生类的析构函数在基类指针或引用被销毁时正确调用。防止资源泄漏和不完全析构。
构造函数不能被声明为虚函数:构造函数在对象创建时调用,此时对象还未完全构造,无法设置虚函数表,因此构造函数不能是虚函数。
8. 构造函数和析构函数那个可以被重载,为什么?
❌ 析构函数不可以被重载:每个类只能有一个析构函数,负责对象生命周期结束时的清理操作。如果允许重载,会导致析构时不确定性。
✅ 构造函数可以被重载:允许创建对象时通过不同的参数进行初始化。通过重载构造函数,可以提供多种初始化方式。
9. this 指针,为什么会存在this指针?
定义:this 指针是指向当前对象的指针。在类的成员函数内部,this 指针指向调用该函数的对象。this指针是在成员函数的开始前构造,并在成员函数的结束后清除
访问对象成员:允许成员函数访问对象的属性和其他成员函数。
区分成员变量和参数:在成员函数中,this 指针可以用来区分同名的成员变量和函数参数。
10. 类继承中构造函数和析构函数的调用顺序、为什么析构函数要写成虚函数
构造函数的调用顺序:自上而下
- 当建立一个对象时,首先调用基类的构造函数,然后调用下一个派生类的构造函数,依次类推,直至到达最底层目标派生类的构造函数
析构函数的调用顺序:自下而上
- 当删除一个对象时,首先调用该派生类的析构函数,然后调用上一层基类的析构函数,依次类推,直到到达最顶层的基类析构函数
虚析构函数的作用:通过基类指针来删除派生类对象时,基类的析构函数应该是虚函数
在公有继承中,基类对派生类及其对象的操作,只能影响到那些从基类继承下来的成员。如果要用基类对继承成员进行操作,则要把基类的这个成员函数定义为虚函数,析构函数同样需要如此。
如果要用基类指针来删除派生类的对象,而这个基类有一个非虚的析构函数。后果是对象的派生部分不会被销毁。然而基类部分被销毁了,将导致内存泄露。
- 基类析构函数不是虚函数,则析构的时候子类对象没有析构
- 基类析构函数是虚函数,子类对象和父类对象都被析构
三、内存管理
1. 指针
1. “引用”与指针的区别是什么?
指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;
而引用本身就是目标变量的别名,对引用的操作就是对目标变量的直接操作。
1.引用必须被初始化,指针不必。
2.引用初始化以后不能被改变,指针可以改变所指的对象。
3.不存在指向空值的引用,但是存在指向空值的指针
2. malloc/free 和 new/delete 的区别
malloc 与 free 是 C++/C 语言的标准库函数,new/delete 是 C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。因为对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。
因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。注意 new/delete 不是库函数。
3. 如果在申请动态内存时找不到足够大的内存块,malloc 和 new 将返回 NULL 指针,宣告内存申请失败。你是怎么处理内存耗尽的?
1.判断指针是否为 NULL,如果是则马上用 return 语句终止本函数。
2.判断指针是否为 NULL,如果是则马上用 exit(1) 终止整个程序的运行
3.为 new 和 malloc 设置异常处理函数。例如 Visual C++可以用_set_new_hander 函数为 new 设置用户自己定义的异常处理函数,也可以让 malloc 享用与 new 相同的异常处理函数。
4. 一个指针占几个字节
一个指针在32位的计算机上,占4个字节;
一个指针在64位的计算机上,占8个字节。
这个是与计算机的寻找空间能力有关,也就是地址的总线宽度决定
5. 什么是指针数组和数组指针
指针数组是一个数组,其中的每个元素都是指针。这些指针可以指向不同的内存地址,通常用于存储一组相同类型的指针。
就像下面的,ptrArray[] 是一个数组,数组中的类型是 int * 指针类型。
int *ptrArray[5];
数组指针是一个指针,它指向数组的首地址,这个变量保存的值是数组的地址。它本身是一个指针,但指向的内容是一个数组对象,那我们就需要对这个数组指针接引之后才得到值。
下面这种其实是和 int *arrPtr 功能一致。
// arrPtr 是一个指针,指向一个包含 5 个整数的数组
int (*arrPtr)[5];
去获取值的话就是
int a1[3] = {12, 15, 75};
int (*p_a1)[3] = &a1;
int *p_a2 = &a1[0];
std::cout<<*(*p_a1 + 1) <<*(p_a2+1); //输出 15 15
指针数组常用于需要动态管理一组指针的场景,而数组指针则用于处理数组的整体,特别是在函数参数传递和多维数组的处理中比较常见。
6. 什么是函数指针和指针函数以及区别
函数指针是指指向函数的指针变量,而指针函数是一个返回类型为函数指针的函数。
函数指针 是指 指向一个函数的指针变量,用于保存函数地址。它可以指向一个特定类型和签名(参数类型和返回类型)的函数。函数指针的声明形式类似于指向其他类型的指针,但其类型是指向函数的指针类型。
// 声明一个指向返回类型为 void,参数为 int 的函数指针
typedef void (*FuncPtr)(int);
// 定义一个函数
void myFunction(int x) {
// 函数体
}
int main() {
// 声明一个函数指针变量并初始化
FuncPtr ptr = &myFunction;
// 通过函数指针调用函数
ptr(10); // 相当于调用 myFunction(10);
return 0;
}
指针函数 指的是 返回类型 为 指向函数的指针 的函数。换句话说,指针函数是一个返回类型为函数指针的函数。
// 声明一个返回类型为 int*,参数为两个 int指针函数
int (*funcPtr)(int, int);
// 定义一个函数
int add(int a, int b) {
return a + b;
}
// 另一个函数,返回一个函数指针
int (*getAddFunctionPointer())(int, int) {
return &add;
}
int main() {
// 获取 add 函数的函数指针
funcPtr = getAddFunctionPointer();
// 通过函数指针调用函数
int result = funcPtr(3, 4); // 相当于调用 add(3, 4),result 等于 7
return 0;
}
函数指针在声明时需要指定其指向的函数的签名(参数类型和返回类型),而指针函数的返回类型是一个函数指针类型。
函数指针直接指向一个已存在的函数,可以通过该指针调用该函数;而指针函数返回一个函数指针,需要通过该函数指针再调用相应的函数。
7. 常量指针和指针常量以及区别
常量指针: 是指一旦指向了某个对象,就无法再指向其他对象的指针。
int x = 10;
int* const ptr = &x; // ptr 是一个常量指针,指向 int 类型的对象
// 无法再修改 ptr 指向的对象,但可以修改对象本身的值
*ptr = 20; // 合法,修改了 x 的值为 20
// 以下操作不合法,因为 ptr 是常量指针,不能改变指向
// ptr = &y; // 错误,无法改变 ptr 的指向
指针常量:是指指向常量对象的指针,一旦指向了某个对象,不能通过该指针修改所指向对象的值。
int x = 10;
const int* ptr = &x; // ptr 是一个指向常量 int 的指针
// 以下操作合法,可以修改 ptr 所指向对象的值
x = 20; // 修改了 x 的值为 20
// 以下操作不合法,因为 ptr 所指向的对象是常量,不能修改其值
// *ptr = 30; // 错误,不能通过 ptr 修改其指向的对象的值
// 以下操作合法,因为 ptr 本身不是常量,可以改变其指向
int y = 50;
ptr = &y; // 合法,修改了 ptr 的指向为变量 y
常量指针: 强调指针本身是常量,但指向对象的值可以改变;
指针常量: 强调指针所指向的对象是常量,也就是对象值不可变,但指针的值可以变;
简单来说:const 修饰的右边最近的值是常量,不能被修改。