c/c++面试问题总结

1. 存储区域

1.1 程序存储区域

C语言程序分为映像和运行时两种状态,在编译-连接后形成的映像中,将生成代码段(TEXT),只读数据段(RO Data)和读写数据段(RW Data),都位于程序中的静态区域。在程序运行之前(即程序初始化时),将动态生成未初始化数据段(BSS),在程序的运行时还将动态生成堆(Heap)区域和栈(Stack)区域,BSS、Heap和Stack都属于程序中的动态区域。

区域 存储对象
代码段 在编译链接后存放机器码
只读数据段 1. 只读全局变量(const char g_a[100]=“hello world!”; ); 2. 只读局部变量 (const t_a[100]=“hello world!”;);3. 代码中的常量(printf(“hello world!\n”);)
读写数据段 已初始化的全局变量和已初始化的静态变量(如果只定义未初始化,则不会生成读写数据区)
未初始化数据段 该段中数据没有经过初始化,因此它只会在目标文件中被标识,而不会真正称为目标文件中的一个段,该段将会在运行时产生。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小
局部变量、传参、返回值
malloc calloc realoc new等
int main()
{
   
         char*p =”tiger”;
         p[1]=’I’;
         p++;
         printf(%s\n”,p);
}

此处编译时会提示段错误

1.2 内存泄漏和内存溢出

内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。原因可能如下:

•	 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
•	 代码中存在死循环或循环产生过多重复的对象实体
•	 递归调用太深,导致堆栈溢出等
•	 内存泄漏最终导致内存溢出

内存泄漏是指向系统申请分配内存进行使用(new),但是用完后不归还(delete),导致占用有效内存。常见的几种情况:

(1) 在类的构造函数和析构函数中没有匹配的调用new和delete函数
(2) 在释放对象数组时在delete中没有使用方括号
(3)没有将基类的析构函数定义为虚函数

缓冲区溢出(栈溢出):
程序为了临时存取数据的需要,一般会分配一些内存空间称为缓冲区。如果向缓冲区中写入缓冲区无法容纳的数据,机会造成缓冲区以外的存储单元被改写,称为缓冲区溢出。而栈溢出是缓冲区溢出的一种,原理也是相同的。分为上溢出和下溢出。其中,上溢出是指栈满而又向其增加新的数据,导致数据溢出;下溢出是指空栈而又进行删除操作等,导致空间溢出。

1.3 new和malloc区别

1.new是C++关键字,需要编译器支持;malloc是库函数,需要头文件支持。

2.使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

3.new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void ,需要通过强制类型转换将void指针转换成我们需要的类型。

4.new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。而malloc是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

5.C++允许自定义operator new 和 operator delete 函数控制动态内存的分配。

6.new做两件事,分别是分配内存和调用类的构造函数,而malloc只是分配和释放内存。new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

7.new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

8.内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc不可以。

1.4 堆和栈的区别

   1、管理方式不同;

   2、空间大小不同;

   3、能否产生碎片不同;

   4、生长方向不同;

   5、分配方式不同;

   6、分配效率不同;

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:

打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。

注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的 😃 对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵, 清楚了?

此知识点转载于堆栈区别

1.5 malloc、calloc、realloc、new

在这里插入图片描述

2. 关键字 static

主要功能是隐藏,持久性和初始化为0x0

2.1 全局变量

在所有函数体外定义的static变量(静态全局变量),只能在该文件中有效,不能在其他源文件中使用(文件作用域);对于没有使用static修饰的全局变量,可以在其他的源文件中使用(全局作用域)。这些区别是编译的概念,即如果不按要求使用变量,编译器会报错。

2.2 局部变量

在函数体内定义的static变量只能在该函数体内有效,但无论调用多少次该函数,static变量只初始化一次,该变量的值会在函数运行结束后依旧保持。

2.3 普通函数

和全局变量类似,使函数具有隐藏性。

2.4 类

2.4.1 成员变量

表示该变量不属于类中的某个实例,而是所有的实例共享此变量,该变量需要在类外进行初始化,实际使用中,尽量避免在.cpp文件初始化该变量,容易造成重复定义。除此之外,类的静态变量可以被类的const成员函数合法更改。

2.4.2 成员函数

在内存中只有一份拷贝;只能访问static成员变量,且静态成员变量可以作为静态成员函数的默认参数static void hun(int m = val){…},普通成员函数不允许;该函数的地址可以直接赋值给普通函数指针;

Class A{
   
 static void s_fun(){
    cout<<"++++++++++++++"<<endl;}
 	void f_fun(){
   cout<<"******************"<<endl;}
};
void (*s_ptr)() = &A::s_fun;
  void (A::*f_ptr)() = &A::f_fun;
  s_ptr();    //通过普通的函数指针可以直接调用类中的静态方法
  (a->*f_ptr)(); //通过类成员函数指针调用类中的成员方法

3. 关键字 const

3.1 修饰变量

A],  char  *p;   //p和*p都可以被修改
B], const char  *p;  //*p不可改变,支持p++
C], const char  * const p;  //*p和p都不可修改
D], const (char  *)p;   //p不可改变,*p可以改变
E], char* const p;    //与D相同
F], (char*) const p;   //与D相同
G], char  *p = “constant data”; // {p[0]=’p’;}×   {char s[10];p=s;p[0]=’p’;}√
H], const char  *p = “constant data”;  // 与G相同
I], const char  * const p = “constant data”; // {*p和p都不可修改}
J], const (char  *)p = “constant data”; //  与I相同

3.2 修饰传参

传参分为值传递、指针传递、引用传递
函数参数为传入参数,不管是指针传递和引用传递,为了防止意外更改该参数,加上const修饰,可以起到保护作用。例如:void fun(const int *p) or void fun(const int & x).

函数参数为传出参数,此时的参数为输出参数,不能使用const进行修饰,否则,此刻该参数将失去输出参数的功能。例如:void fun( char* in,char * out ) or void fun(char* in,char* & out).

3.3 修饰返回值

返回值类型 const修饰意义
指针 必须为const指针接收,指向的内容不可变,但const指针指向可变
值传递 返回的使一个临时对象,const修饰无意义
引用 根据实际情况来区分是要获取该对象的一份拷贝,还是该对象的一个别名使用。通常参数返回引用使用在类赋值函数中使用,用于链式表达式的调用,a=b=c;具体可参考string类的赋值函数

3.4 修饰函数

在类中,任何不会修改成员的函数都应该定义成const成员函数,如果在const成员函数中修改了成员,会出现错误。通过此种类型提高程序的健壮性。有关const成员函数的几个特性:

const对象只能访问const成员函数,非const对象两者都可访问;
const对象的成员是不可修改,但是通过指针维护的对象是可以修改的;
const成员函数不可以修改对象成员,不管对象是否具有const属性,在编译是以是否修改成员为依据,进行检查;
如果一个成员被mutable修饰,那么任何方式都可以修改该成员,即便是const成员函数,也可以修改他;

4. 关键字 virtual

4.1 虚函数

虚函数(Virtual Function)是通过一张 虚函数表(Virtual Table) 来实现的。简称为V-Table。在这个表中,主要是存放一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其能真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

那么虚函数表的指针存放在哪里呢?
上述已经描述过了,存放在具体的实例对象中,通过虚函数指针来操控虚函数表,在进行多态的时候,需要用到,根据具体的实例对象就能确定所要调用的是哪一个具体类的具体方法。都是通过虚函数表来完成相应的操作的。在虚函数表中存放的是具体实例类重写的父类的虚函数方法地址。当调用时,根据具体的实例即可访问到具体方法。

综上所述虚函数可以概括为以下几点:

a>虚函数表是全局共享元素,全局就只有一个。
b>虚函数表好比是一个类似数组的东西,在类对象中存储vptr(虚函数指针),并指向虚函数表,因为其不属于方法,也不属于代码,所以不能存放在代码段。
c>虚函数表中存储的是虚函数的方法的地址,其大小在编译节点就已经确定好了,根据的继承关系,就能确定好虚函数表的大小。所以不用动态的分配内存,不在堆中。
d>由于虚函数表是全局共享的,类似于static变量一样,存储在全局代码区域。

构造函数不能为虚函数:
因为虚函数的是通过虚函数指针操控虚函数表来实现的,且虚函数指针存放在实例对象的头部位置,在创建一个实例对象时,需要调用对应的构造函数,此刻对象还未生成,是不能调用虚函数的

析构函数需要写成虚函数:
对于析构函数,是实例对象将要释放资源,需要调用调用析构函数,在继承关系中,为了防止带释放对象的时候调用析构函数,只调用了基类的析构而没有对派生类的构造进行调动,所以,将基类的析构函数记性虚化,从而保证基类和派生类的析构函数都被调用,确保释放其所占用的资源。
当基类指针指向子类对象,但是子类析构函数不是虚函数,调用析构函数的时候,子类的析构函数是不会被调用的。

纯虚函数:
纯虚函数通常用来作为接口。

避免在基类的构造函数和析构函数中调用虚函数

class A{
   
    public:
        A(){
   
            fun();
        }
        virtual void fun(){
   
            cout<<"Afun"<<endl;
        }
};
class B:public A{
   
    public:
        virtual void fun(){
   
            cout<<"Bfun"<<endl;
        }
};
int main(){
   
    B b;    
    return 0;
}

如上所示的调用结果是输出Afun,这是因为在创建对象b的时候,因为B继承A,在进行构造函数的调用的时候,优先调用基类的构造函数,此时的派生类对象尚未完成初始化,此刻虚函数指针还未完成初始化,不能够去检索对应的虚函数表,所以此时进行构造调用的时候为基类的方法。

4.2 虚继承

class 派生类: virtual 基类1virtual 基类2...virtual 基类n
{
   //派生类成员声明};

虚继承的原理过程是通过虚基类指针和虚基类表来实现,一个虚基类指针占用四个字节,虚基类表不占用类存储空间,在虚基类表中存储是虚基类相对于派生类的偏移量,可根据偏移量找到虚基类成员。如果虚继承的类被继承,该派生类同样有一份虚基类指针的拷贝。这样就能保证虚基类中在子类中存在一份拷贝,避免有多分拷贝造成二义性
见实例:

class a{
   
void fun(void);
};
class b : public a{
   };
class bb: public a{
   };
class c : public b{
   };//此种方法会导致同名方法存在多分拷贝,导致调用时会产生二义性
class d : public b,public bb{
   };//同上

c.fun();//二义性,调用出错 需改成  c.b::fun();  c.a::fun();
d.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值