面试题--C++基础篇

来源:互联网 发布:云计算专业认识 编辑:程序博客网 时间:2024/05/23 23:03

C++listvector的区别

(1)vector是顺序表,表示的是一块连续的内存,元素被顺序存储;list是双向连接表,在内存中不一定连续。

(2)当数值内存不够时,vector会重新申请一块足够大的连续内存,把原来的数据拷贝到新的内存里面;list因为不用考虑内存的连续,因此新增开销比vector小。

(3)list只能通过指针访问元素,随机访问元素的效率特别低,在需要频繁随机存取元素时,使用vector更加合适。

(4)当向vector插入或者删除一个元素时,需要复制移动待插入元素右边的所有元素;因此在有频繁插入删除操作时,使用list更加合适。

多态的实现原理

C++通过虚函数机制实现多态特性,实现动态绑定。

父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。在每个包含有虚函数的类的对象的最前面(是指这个对象内存布局的最前面)都有一个称之为虚函数指针(vptr)的东西指向虚函数表(vtbl),这个虚函数表(这里仅讨论最简单的单一继承的情况,若果是多重继承,可能存在多个虚函数表)里面存放了这个类里面所有虚函数的指针,当我们要调用里面的函数时通过查找这个虚函数表来找到对应的虚函数,这就是虚函数的实现原理。

注意一点,如果基类已经插入了vptr,则派生类将继承和重用该vptr。vptr(一般在对象内存模型的顶部)必须随着对象类型的变化而不断地改变它的指向,以保证其值和当前对象的实际类型是一致的。

野指针

野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

造成野指针的原因

(1)  指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的默认值是随机的,它会乱指一气。

(2)  指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

(3)  指针操作超越了变量的作用范围。这种情况让人防不胜防。

正确使用指针

1)   声明一个pointer的时候注意初始化为null: int*   pInt   =  NULL;

2)   分配完内存以后注意判断是否为空:

pInt   =  new   int[num];      ASSERT(pInt   !=  NULL);

3)   删除时候注意用对操作符:

对于new  int类型的,用delete

对于new  int[]类型的,用delete []

4)   删除完毕以后记得给他null地址:

delete[]   pInt;    pInt =  NULL;

5)   谁分配的谁回收,不要再一个函数里面分配local  pointer,送到另外一个函数去delete

6)   返回local   address是非常危险的,如必须这样做,请写注释到程序里面,免得忘记

堆和栈的区别

1. 堆栈空间分配区别:

(1) 栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;

(2) 堆(操作系统):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

2. 堆栈缓存方式区别:

(1) 栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;

(2) 堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

内存分配 

一个由C/C++编译的程序占用的内存分为以下几个部分:

1)   栈区(stack:  由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2)   堆区(heap: 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

3)   全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

4)   字符串常量区:常量字符串就是放在这里的。程序结束后由系统释放。

5)   程序代码区:存放函数体的二进制代码。

 使用指针和引用

1)    需要改变实参的时候, 使用指针或引用

2)    如果数据对象是较大的结构,则使用const指针和或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和内存空间。

3) 如果数据对象是类对象,则使用const引用。类对象在数据结构和语义上非常适合使用引用,这也是C++新增引用这项特性的主要原因。

4)    动态分配空间时,必须使用指针

5)    传递数组时,必须使用指针

6)    函数返回指针时, 比如FILE * fopen(const char * path, const char *mode);

7)    另外,有时候需要使用二级指针,即指针的指针,例如:

   MemAllocate(char *a){

    a=(char *)malloc(sizeof(char));

}

当调用此函数进行内存分配时,发现不能分配内存不能成功,因为此时对于a来说,形参改变了,但实参并不会改变,他们对应于不同的内存单元。正确的写法应该是:

    MemAllocate(char **a){

    *a=(char *)malloc(sizeof(char));

}

这样就能够正确地分配内存了。

 C++中重载、重写(覆盖)和隐藏的区别

1)   重载:重载从overload翻译过来,是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

2)   隐藏:隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

3)   重写:重写翻译自override,也翻译成覆盖(更好一点),是指派生类中存在重新定义的函数,针对虚函数(virtual function)而言。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。

 C++函数调用的解析过程

假设调用p->mem()(或者obj.mem()),则依次执行以下4个步骤:

1)   首先确定p(或obj)的静态类型。因为调用成员函数,所以必须是类类型。

2)   在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。若还是未找到,则编译器报错。

3)   一旦找到mem,就进行常规类型检查以确认对于当前找到的mem,本次调用是否合法。 名字一旦找到,编译器就不再继续查找了。

4)   假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

      A.   如果mem是虚函数且我们是通过指针或引用进行调用的,则编译器产生的代码静载运行时确定到底该运行该函数的哪个版                 本,依据对象的动态类型。

      B.   反之,如果mem不是虚函数或者是通过对象进行的调用,则编译器将产生一个常规函数调用。

使用C语言实现多态

/**

  * 使用C语言实现多态的一个简单例子

  * 注意:结构体是根据变量在结构体的偏移量来读取或者修改变量的

  */

#include <stdio.h>struct Animal{void (*move)();};struct Dog{void (*move)();};struct Cat{void (*move)();};void animal_move(){printf("Animal move.\n");}void dog_move(){printf("Dog move.\n");}void cat_move(){printf("Cat move.\n");}void move(Animal *animal){animal->move();}int main(){Dog dog;dog.move = dog_move;Cat cat;cat.move = cat_move;Animal *animal = (Animal*)&dog; move(animal);animal = (Animal*)&cat; move(animal);return 0;}

C++中的单例模式

classSingleton{private:    Singleton() { };    ~Singleton() { };    Singleton(const Singleton&);    Singleton& operator=(constSingleton&);public:    static Singleton& getInstance() {        static Singleton instance;        return instance;    }};

当第一次访问getInstance()方法时才创建实例。C++0X以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的,C++0x之前仍需加锁。

析构函数要设为虚函数原因

在多态中,当使用基类指针或引用操作派生类对象时,如果不把析构函数定义为虚函数,则 delete 销毁对象时,只会调用基类的析构函数,而不会调用实际派生类对象的析构函数,这样可能造成内存泄露。

使用递归需要注意的问题

(1)递归结束条件,为防止递归的无休止调用,在递归函数中要及时返回。

(2)递归是不断调用自身,而间接递归会调用两个或更多的函数,这样对内存的占用是相当巨大的,因才在递归函数中应尽量少用局部变量,并且递归的层数不要太深,否则会引起栈空间溢出

内存泄漏产生原因以及避免方法

内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。更广义的内存泄漏还包括未对系统资源的及时释放,比如句柄、socket等没有使用相应的函数释放掉,导致系统资源的浪费。

解决方法:

(1)养成良好的编码习惯和规范,记得及时释放掉内存或系统资源。

(2)重载new和delete,以链表的形式自动管理分配的内存。

(3)使用智能指针。

C++ 对象的内存布局

影响对象大小的有如下因素

1)成员变量

2)虚函数(产生虚函数表)

3)单一继承(只继承于一个类)

4)多重继承(继承多个类)

5)重复继承(继承的多个父类中其父类有相同的超类)

6)虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

上述的东西通常是C++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里都不讨论,只讨论C++语言上的影响。

本人发现一篇讲解很详细的博客,推荐给大家:

C++ 对象的内存布局(上)

C++ 对象的内存布局(下)





1 0
原创粉丝点击