C++面试题

函数调用的过程:
main()========
1).参数拷贝(压栈),注意顺序是从右到左,即c-b-a;
2).保存d = fun(a, b, c)的下一条指令,即cout<<d<<endl(实际上是这条语句对应的汇编指令的起始位 置);
3).跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;
fun()=====
4).移动ebp、esp形成新的栈帧结构;
5).压栈(push)形成临时变量并执行相关操作;
6).return一个值;
7).出栈(pop);
8).恢复main函数的栈帧结构;
9).返回main函数;
main()========

堆和栈的区别:
1.堆是由程序员动态申请和释放的,栈是由系统来分配的,堆很大,栈很小,栈存放一些局部变量,参数之类的,堆可以存放全局变量,可以存放代码段等等。
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另 一块区域。 - 程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。

左值和右值:

左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),
右值指的则是只能出现在等号右边的变量(或表达式)
举例来说我们定义的变量 a 就是一个左值,而malloc返回的就是一个右值。
或者左值就是在程序中能够寻值的东西,右值就是一个具体的真实的值或者对象,没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,
比如自己定义的class, 可以通过它的成员函数来修改右值。

const 有什么用途:

1).定义只读变量,或者常量(只读变量和常量的区别参考下面一条);
2).修饰函数的参数和函数的返回值;
3).修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const成员函数只能调用const成员函数;
4).只读对象。只读对象只能调用const成员函数。

在C中用const 能定义真正意义上的常量吗?C++中的const呢?

不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真	正的常量(比如通过指针对const变量是可以修改值的),但是c++中是有区别的,c++在编译时会把const常量加入	符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的。
补充:
1). c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,	它是一个只读变量。
2). 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决	定。
3).c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。
4).c语言中只有enum可以实现真正的常量。
5). c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。
6). c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进	行初始化。

宏和内联(inline)函数的比较?

1). 首先宏是C中引入的一种预处理功能;
2). 内联(inline)函数是C++中引用的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;
3). 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;
4). 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;
5). 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译期决定(当然可以通过设置编译器,强	制使用内联);
6). 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也	可能将其作为内联函数。

C++中有了malloc / free , 为什么还需要 new / delete?

malloc/free是标准库函数,new/delete是运算符
new可以触发构造函数,delete可以触发析构函数

C和C++中的强制类型转换?

**
C中是直接在变量或者表达式前面加上(小括号括起来的)目标类型来进行转换,一招走天下,操作简单,但是由于太 过直接,缺少检查,因此容易发生编译检查不到错误,而人工检查又及其难以发现的情况;而C++中引入了下面四种转 换:

1). static_cast
	a. 用于基本类型间的转换
	b. 不能用于基本类型指针间的转换
	c. 用于有继承关系类对象间的转换和类指针间的转换
2). dynamic_cast
	a. 用于有继承关系的类指针间的转换
	b. 用于有交叉关系的类指针间的转换
	c. 具有类型检查的功能
	d. 需要虚函数的支持
3). reinterpret_cast
	a. 用于指针间的类型转换
	b. 用于整数和指针间的类型转换
4). const_cast
	a. 用于去掉变量的const属性
	b. 转换的目标类型必须是指针或者引用
在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。但是在	工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来	抑制其被隐式的调用),而隐式调用经常是bug的来源。实际工程中替代的方式是定义一个普通函数,通过显式的调用	来达到类型转换的目的。

static 有什么用途
1). 静态(局部/全局)变量
2). 静态函数
3). 类的静态数据成员
4). 类的静态成员函数

静态全局变量:
1.该变量在全局数据区分配内存;
2.未经初始化的静态全局变量会被程序自动初始化为0
3.静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

静态局部变量:
1.该变量在全局数据区分配内存;
2.静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
3.它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

静态函数:
1.静态函数不能被其它文件所用;
2.其它文件中可以定义相同名字的函数,不会发生冲突

类的静态成员变量和静态成员函数各有哪些特性?
静态成员变量:
1). 静态成员变量需要在类内声明(加static),在类外初始化(不能加static)
2). 静态成员变量在类外单独分配存储空间,位于全局数据区,因此静态成员变量的生命周期不依赖于类的某个对 象,而是所有类的对象共享静态成员变量;
3). 可以通过对象名直接访问公有静态成员变量;
4). 可以通过类名直接调用公有静态成员变量,即不需要通过对象,这一点是普通成员变量所不具备的。

静态成员函数
1). 静态成员函数是类所共享的;
2). 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;
3). 可以通过对象名直接访问公有静态成员函数;
4). 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。
5).静态成员函数没有this指针

在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?
C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数 原型为:void foo(int x, int y);
该函数被C编译器编译后在库中的名字为 _foo, 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解 决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。

头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?

相同点:
它们的作用是防止头文件被重复包含。
不同点
1). ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器 不支持的情况(主要是比较老的编译器)。
2). 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越 多的编译器开始支持 program once。
3). ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该 语句的文件, 这也是为什么 program once 速度更快的原因。
4). 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的 情况(在编写大型程序时特性需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文 件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包 含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。

指针和引用的区别?

相同点:
1). 都是地址的概念;
2). 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
3). 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。
不同点:
1). 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
2). 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
3). 引用不能为空,指针可以为空;
4). “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
5). 指针和引用的自增(++)运算意义不一样;
6). 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
7). 引用具有更好的可读性和实用性。

引用占用内存空间吗?
可以对引用进行取地址,引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来 完成的。

指针数组和数组指针的区别

数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。

数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[10],p即为指向数组的指针,()优先级 高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行 p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指 针。

指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组 中每一个元素均为指针,其本质为数组。如 int p[n], []优先级高,先与p结合成为一个数组,再由int说明 这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误 的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放 变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。

解释下封装、继承和多态?

1). 封装:
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
2). 继承:
继承主要实现重用代码,节省开发时间。
子类可以继承父类的一些东西。
a. 公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是 私有的,不能被这个派生类的子类所访问。
b. 私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
c. 保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访 问,基类的私有成员仍然是私有的。

什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

1). 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生	成默认拷贝构造函数。
2). 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷	贝。
3). 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用	了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构	造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。

构造函数和析构函数的执行顺序?
构造函数
1). 首先调用父类的构造函数;
2). 调用成员变量的构造函数;
3). 调用类自身的构造函数。
析构函数
对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与 delete的顺序相关。

C++智能指针

参考链接:

智能指针详解

explicit

在c++种explicit关键字只能用来修饰构造函数。使用explicit可以禁止编译器自动调用拷贝初始化,还可以禁止编译器对拷贝函数的参数进行隐式转换。

什么是拷贝初始化?

#include <iostream>
using namespace std;

class A{
	public:
	 A(int x){
		cout<<"我被用了"<<endl;
	}
};

void f(A a)
{
}
int main( ){

	f(1);// 被隐式转换为f(A(1)) ,本来是1却被自动调用了A(1)这就是拷贝初始化
	//输出:"我被调用了"
	return 0;
}

什么是编译器会对构造函数的参数进行隐式转换?

#include <iostream>
using namespace std;
#include<algorithm>
class Str{
	public:
	 Str(int x){
		cout<<"我是想把整数变字符串"<<endl;
	}
	Str(const char* a)
	{
		cout<<"我是想把字符数组变字符串"<<endl;
	}

};


int main( ){

	Str s='c'; 
	// 输出:"我是想把整数变字符串"
	// 它把'c'的ASCII码传进去了,如果这样变成字符串那就得到一个数字,
        // 而我们期待的是把'c'变成字符串。
	return 0;
}

指针函数和函数指针

int* func();//指针函数,就是返回指针的函数
int (*func)();//函数指针
//调用函数指针
func = &function;
func = function;
x = (*func);
x = func;

C++lambda表达式

[capture list] (params list) mutable exception-> return type { function body }

各项具体含义如下

capture list:捕获外部变量列表
params list:形参列表
mutable指示符:用来说用是否可以修改捕获的变量
exception:异常设定
return type:返回类型
function body:函数体

捕获外部变量:
在Lambda表达式中,外部变量的捕获方式也有值捕获、引用捕获、隐式捕获。

1.值捕获

#include <iostream>
using namespace std;

int main()
{
    int a = 123;
    auto f = [a] { cout << a << endl; }; 
    f(); // 输出:123
}

2.引用捕获

int main()
{
    int a = 123;
    auto f = [&a] { cout << a << endl; }; 
    a = 321;
    f(); // 输出:321
}

3.隐式捕获
我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

int main()
{
    int a = 123;
    auto f = [&] { cout << a << endl; };    // 引用捕获
    a = 321;
    f(); // 输出:321
}

4.混合方式
[] 不捕获任何外部变量
[变量名, …] 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符)
[this] 以值的形式捕获this指针
[=] 以值的形式捕获所有外部变量
[&] 以引用形式捕获所有外部变量
[=, &x] 变量x以引用形式捕获,其余变量以传值形式捕获
[&, x] 变量x以值的形式捕获,其余变量以引用形式捕获

线程池

线程池的优势:

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
  4. 提供更强大的功能,延时定时线程池。

JAVA提供了四种:固定大小的采用阻塞队列,不固定大小的,用于延时周期任务的,单个线程的

虚表

  1. 有虚函数的类必存在一个虚表。
  2. 虚表的构建:基类的虚表构建,先填上虚析构函数的入口地址,之后所有虚函数的入口地址按在类中声明顺序填入虚表;派生类的虚表构建,先将基类的虚表内容复制到派生类虚表中,如果派生类覆盖了基类的虚函数,则虚表中对应的虚函数入口地址也会被覆盖,为了后面寻址的一致性。

内存泄露、野指针、内存越界

内存泄露:
概念:用动态内存分配函数动态开辟的空间,在使用完毕后未释放,程序结束后,会导致一直占据该内存单元,直到程序结束,在现代操作系统中,一个应用程序使用的常规内存在程序终止时被释放。这表示一个短暂运行的应用程序中的内存泄漏不会导致严重后果。但是在内存非常有限的系统中都可能导致非常严重的后果,shared_ptr来避免内存泄漏,但是要正确使用。

野指针
“野指针”不是NULL指针,是指指向“垃圾”内存的指针。即指针指向的内容是不确定的。
产生的原因:
1)指针变量没有初始化。因此,创建指针变量时,该变量要被置为NULL或者指向合法的内存单元。
2)指针p被free之后,没有置为NULL,让人误以为p是个合法的指针。
3)指针跨越合法范围操作。不要返回指向栈内存(非静态局部变量)的指针或引用。

内存越界
存在一种情况就是调用栈溢出(stackoverflow),还有一种情况是缓冲区溢出,这两种情况都会导致安全漏洞。

缓冲区溢出
strcpy会一直复制直到碰到\0,很多平台的栈变量是按照地址顺序倒着分配的(高地址向低地址),所以destination溢出后会先修改先前定义的变量,这样黑客就可以把is_administrator改为true,从而造成缓冲区溢出攻击,当然数组越界也可以造成类似的效果,不过现在C++都提供了越界检查的版本

栈溢出攻击
栈溢出攻击:在栈上分配length字节的空间,再往栈顶放上一个data。当Length十分大,会把data挤到栈空间之外,此时如果编译器不做越界检查的话,那么黑客只要用客户端送特定的length和data,就能改写服务器的任意内存(比如黑客可以修改服务器代码的机器码,注入一些JMP指令跳转到黑客想执行的函数)

堆栈缓存的区别
1.栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2.堆是存放在二级缓存中,堆的首地址放在一级缓存缓存中,分配和释放会产生系统调用,由用户态进入内核态,所以速度会慢一些

C++构造函数是否可以抛出异常

构造函数可以抛出异常。但从逻辑上和风险控制上,构造函数中尽量不要抛出异常,既需要分配内存,又需要抛出异常时要特别注意防止内存泄露的情况发生。因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用,就会造成内存泄漏。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员对象,包括其基类的成员,即执行直接基类和成员对象的析构函数

是否在析构函数抛出异常

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

volatile关键字

使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲就是防止编译器优化。
在单任务环境中,如果在两次读取变量之间不改变变量的值,编译器就会发生优化,会将RAM中的值赋值到寄存器中;由于访问寄存器的效率要高于RAM,所以在需要读取变量时,直接寄存器中获取变量的值,而不是从RAM中。
在多任务环境中,虽然在两次读取变量之间不改变变量的值,在一些情况下变量的值还是会发生改变,比如在发生中断程序或者有其他的线程。这时候如果编译器优化,依旧从寄存器中获取变量的值,修改的值就得不到及时的响应(在RAM还未将新的值赋值给寄存器,就已经获取到寄存器的值)。
要想防止编译器优化,就需要在声明变量时加volatile关键字,加关键字后,就在RAM中读取变量的值,而不是直接在寄存器中取值。

构造函数和析构函数可以是虚函数吗

为什么构造函数不能为虚函数?
虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。

为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题?
首先析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
举例说明:
子类B继承自基类A;A *p = new B; delete p;
 1) 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。
2) 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。
补充: B *p = new B; delete p;时也是先调用B的析构函数,再调用A的析构函数。

GDB调试

run和start的区别
都是用于启动程序执行
run如果没有断点会一直执行到程序结束,而start会执行到main函数的起始未知停下来,相当于在main函数第一行语句加了断点。

next和step的区别
n不会进入函数内部,step会进入函数内部

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值