本文原载于我的博客:https://www.seekingmini.top/archives/自己整理的c面试题
0. 写在前面
大三找实习,也算是面试了不少公司了,凭着记忆整理了一些当时被问到的题目以及自己觉得比较常见的问题。
1. const
的作用
(1) 修饰变量
const int a = 3; // a是常量,值不可改变
(2) 修饰指针
- 指向常量的指针
const int a = 3; // a是整型常量
const int *p = &a; // p是指向整型常量的指针
怎么理解指向常量的指针?我们可以这样认为:
int
是一种数据类型,那么const int
也是一种数据类型,那么就会有指向const int
这种类型的指针,即const int*
。
- 自身是常量的指针
int a = 3;
int* const q = &a; // q是一根常量指针(这意味着q只能指向a)
int b = 4;
*p = 4; // 这是合法的,因为p仍然指向a,而我们只是改变a的值而已
p = &b; // 这是不合法的,因为改变了p指向的对象
时刻记住:如果
const
出现在*
左边,表示被指对象是常量;如果出现在*
右边,表示指针自身是常量;如果出现在*
两边,表示被指对象和指针两者都是常量。
(3) 修饰引用
int a = 3;
void func(const int &a);
函数
func
的参数a
前加入const
既可以避免拷贝,又可以防止对a
的修改。
(4) 修饰成员函数
说明该成员函数内不能修改成员变量。
2. static
的作用
(1) 修饰普通变量
修改变量的存储区域和生命周期,使变量存储在静态区,在主函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
(2) 修饰普通函数
表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以用
static
修饰函数。
(3) 修饰成员变量
使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化。
class Person {
private:
int age;
string name;
public:
static int num; // 表示实例化的对象数
Person(int a, string n) {num++; this->age = a; this->name = n;}
};
int Person::num = 0; // 类外初始化静态变量
void main() {
Person p1(21, "Arron");
cout << p1.num << endl; // 1
Person p2(20, "Seeking");
cout << p2.num << endl; // 2
}
(4) 修饰成员函数
使得不需要生成对象就可以访问该函数,但是在静态函数内不能访问非静态成员,只能访问
static
修饰的类成员。
class Person {
private:
int age;
string name;
public:
static int num; // 表示实例化的对象数
Person(int a, string n) {num++; this->age = a; this->name = n;};
static int getNum() { return num; };
};
int Person::num = 0; // 类外初始化静态变量
void main() {
Person p1(21, "Arron");
cout << p1.getNum() << endl; // 1
}
3. this指针
this
指针是一个隐含于每一个非静态成员函数中的特殊指针,它指向调用该成员函数的那个对象。
当对一个对象调用成员函数时,编译程序先将对象的地址赋给this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。
当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
4. 指针和引用的区别
- 引用必须被初始化,但是不分配存储空间;指针不声明时初始化,在初始化的时候需要分配存储空间。
- 引用初始化后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
5. 面向对象
三大特征——封装、继承、多态。
(1) 封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。
- 优点
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员进行更精确的控制。
- 隐藏信息,实现细节。
(2) 继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
- 注意点
- 子类拥有父类非private的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- 缺陷
- 父类变,子类就必须变。
- 继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的。
- 继承是一种强耦合关系。
(3) 多态
多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。
- C++多态分类及实现
重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
子类型多态(Subtype Polymorphism,运行期):虚函数
参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
- 静态多态的一个应用——函数重载
class A
{
public:
void do(int a);
void do(int a, int b);
};
- 动态多态的一个应用——虚函数
class A // 父类
{
public:
virtual void foo() {
cout<<"A::foo() is called"<<endl;
}
};
class B:public A // 子类
{
public:
void foo() {
cout<<"B::foo() is called"<<endl;
}
};
int main(void) {
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数foo却是B的!
return 0;
}
6. 纯虚函数
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
class A {
public:
virtual int fuc() = 0;
}
(1) 与虚函数的区别
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
- 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
- 虚基类是虚继承中的基类。
7. 内存分配和管理
(1) malloc & free
用于分配、释放内存。
malloc
用于申请指定字节数的内存。申请到的内存中的初始值不确定。
(2) new & delete
new / new[]
:完成两件事,先底层调用malloc
分配了内存,然后调用构造函数(创建对象)。
delete/delete[]
:也完成两件事,先调用析构函数(清理资源),然后底层调用free
释放空间。
new
在申请内存时会自动计算所需字节数,而malloc
则需我们自己输入申请内存空间的字节数。
8. 堆和栈的比较
栈区
由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区
一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收 。其分配方式倒是类似于链表。
(1) 申请方式
- 栈
由系统自动分配。例如对于
int b
,系统自动在栈中为其分配空间。
- 堆
需要自己申请,并指明大小。例如C语言的
malloc
函数和C++的new
运算符。
(2) 申请后系统的响应
- 栈
只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 堆
操作系统有一个记录空闲内存地址的链表。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的
delete
语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
(3) 存储内容
- 栈
在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
- 堆
堆的头部用一个字节存放堆的大小。
9. 红黑树(STL原理)
(1) 应用
关联数组:如STL中的
map
、set
。
(2) 与B树、B+树的区别
- 红黑树的深度比较大,而B树和B+树的深度则相对要小一些。
- B+树则将数据都保存在叶子节点,同时通过链表的形式将他们连接在一起。
(3) B树、B+树
- 应用
大部分文件系统、数据库系统都采用B树、B+树作为索引结构。
- 区别
- B+树中只有叶子节点会带有指向记录的指针(ROWID),而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中。
- B+树中所有叶子节点都是通过指针连接在一起,而B树不会。
- B树的优点
对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。
- B+树的优点
- 非叶子节点不会带上 ROWID,这样,一个块中可以容纳更多的索引项,一是可以降低树的高度。二是一个内部节点可以定位更多的叶子节点。
- 叶子节点之间通过指针来连接,范围扫描将十分简单,而对于B树来说,则需要在叶子节点和内部节点不停的往返移动。
10. 变量的作用域与生命周期
(1) 全局变量
- 作用域
全局作用域。只要在一个文件中定义了就可以作用于其他所有源文件。
注意:如果在两个文件中都定义了相同名字的全局变量,则会发生错误。
- 生命周期
程序运行期一直存在。
- 引用方法
其他文件中必须用
extern
关键字声明要引用的全局变量。
- 内存分布
全局数据区。
代码示例
- main.cpp
extern int a;
int main() {
int b = a;
return 0;
}
- define.cpp
int a = 3;
(2) 全局静态变量
- 作用域
文件作用域(只能作用于当前定义它的文件)。
注意:只要文件不互相包含,在两个不同的文件中是可以定义完全相同的两个静态变量的,它们是两个完全不同的变量。
- 生命周期
程序运行期一直存在。
- 内存分布
全局数据区。
- 定义方法
使用
static
关键字。
代码示例
- main.cpp
static int a = 3;
int main() {
int b = a;
return 0;
}
- define.cpp
static int a = 3;
(3) 局部静态变量
- 作用域
局部作用域(只在局部作用域中可见)。
注意:只被初始化一次,多线程中需加锁保护。
- 生命周期
程序运行期一直存在。
- 内存分布
全局数据区。
- 定义方法
局部作用域用中用
static
定义。
代码示例
- main.cpp
void func() {
static int a = 3;
}
int main() {
func();
return 0;
}
(4) 局部变量
- 作用域
局部作用域(只在局部作用域中可见)。
- 生命周期
程序运行出局部作用域即被销毁。
- 内存分布
栈区。
代码示例
- main.cpp
void func() {
int a = 3;
}
int main() {
func();
return 0;
}
11. 内存分区
C/C++程序主要分为堆、栈、全局/静态存储区和代码区。不同类型的变量存放的区域不同。
(1) 堆区
由程序员手动申请,手动释放,若不手动释放,程序结束后由系统回收。生命周期是整个程序运行期间。使用
malloc
或者new
进行堆的申请,堆的总大小为机器的虚拟内存的大小。
(2) 栈区
由操作系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行后,系统自行释放栈区内存,不需要用户管理。
(3) 全局/静态存储区
全局/静态存储区内的变量在程序编译阶段已经分配好内存空间并初始化。这块内存在程序的整个运行期间都存在,它主要存放静态变量、全局变量和常量。
(4) 代码区
存放程序体的二进制代码。比如我们写的函数,都是在代码区的。
(5) 总结
- 数据区
堆、栈、全局/静态存储区。
- 全局/静态存储区
常量区(静态常量区)、全局区(全局变量区)和静态变量区(静态区)。
- 常量区包括
字符串常量区和常变量区。
- 代码区
存放程序编译后的二进制代码,不可寻址区。
暂时先写这么多~