关于C++的知识点总结(有问题欢迎指出)(面试)

 

1.实现一个Memcpy函数

(1).strcpy提供字符串的复制,即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。

而mencpy提供了更一般内存的复制,即mencpy对于需要复制的内容没有限制,因此用途更广。

strcpymemcpy主要有以下3方面的区别。
(1)
复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
(2)
复制的方法不同。strcpy不需要指定长度,它遇到字符串结束符"\0"便结束。memcpy则是根据其第3个参数决定复制的长度。
(3)
用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

Char* strcpy(char* strDest,const char* strSrc)

{

       Assert(strDest!=NULL&&strSrc!=NULL)

       Char* strDestCopy=strDest;

       While((*strDest++=*strSrc++)!=’\0’);

       Return strDestCopy;

}

 

Void *memcpy(void *mento, const void *menfrom,size_t size)

{

assert (mento!=NULL&&menfrom!=NULL);

char* tempFrom=(char*)menFrom;

char* temto=(char*)mento;

while(size--)

{

         *tempto++=*tempfrom++;

}

Return mento;

 

}

2.STL中vector的实现原理 (衍生:Map, Set等实现原理) 

STL(标准模板库):是一个具有工业强度的,高效的C++程序库;该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。

STL的六大组:

(1)容器(Container),是一种数据结构,如list,vector,和deques ,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器;

(2)迭代器(Iterator),提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。迭代器就如同一个指针。事实上,C++的指针也是一种迭代器。但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符地方法的类对象;

(3)算法(Algorithm),是用来操作容器中的数据的模板函数。例如,STL用sort()来对一个vector中的数据进行排序,用find()来搜索一个list中的对象,函数本身与他们操作的数据的结构和类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用;

//4)仿函数(Function object,仿函数(functor)又称之为函数对象(function object),其实就是重载了()操作符的struct,没有什么特别的地方

//(5)迭代适配器(Adaptor

//(6)空间配制器(allocator)其中主要工作包括两部分1.对象的创建与销毁    2.内存的获取与释放

1、(1Vector是顺序容器,是一个动态数组,支持随机存取、插入、删除、查找等操作,在内存中是一块连续的空间。在原有空间不够情况下自动分配空间,增加为原来的两倍。

2vector动态增加大小时,并不是在原空间之后持续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。

3vector优异性能的秘诀之一,就是配置比其所容纳的元素所需更多的内存,一般在使用vector之前,就先预留足够空间(vec.size()/vec.capacity()容量),以避免二次分配,这样可以使vector的性能达到最佳。

4)由于  vector维护的是一个连续线性空间,所以vector支持随机存取 vector随机存取效率高,但是在vector插入元素,需要移动的数目多,效率低下。

 

 

2Map是关联容器,以键值对的形式进行存储,方便进行查找。关键词起到索引的作用,值则表示与索引相关联的数据。以红黑树的结构实现,插入删除等操作都在O(logn)时间内完成。

注意:map的下标操作,其行为与vector很不相同:使用一个不在容器中关键字作为下标,会添加一个具有此关键字的元素到map中。一般使用find函数代替下标操作。

3Set是关联容器,set中每个元素只包含一个关键字。set支持高效的关键字查询操作——检查一个给定的关键字是否在set中。set也是以红黑树的结构实现,支持高效插入、删除等操作。

3. 命名空间std

 C++标准中引入命名空间的概念,是为了解决不同模块或者函数库中相同标识符冲突的问题。最典型的例子就是std命名空间,c++标准库中所有标识符都包含在该命名空间中。

5.请简述智能指针原理,并实现一个简单的智能指针

智能指针是一种资源管理类,通过对原始指针进行封装,在资源管理对象进行析构时对指针指向的内存进行释放;通常使用引用计数方式进行管理

由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。

用智能指针便可以有效缓解这类问题,本文主要讲解常见的智能指针的用法。包括:std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array、boost::weak_ptr、boost:: intrusive_ptr。

6.请实现一个单例模式的类

class Singleton

{

private:

 Singleton();

 static  Singleton*  instance;

public:

 static  Singleton*  GetInstance( )

 {

  If(instance==NULL) 

   Instance=new singlewton();

 Return

   Instance;

 }

};

Singleton* singleton::instance=NULL;

为什么使用静态方法:因为构造函数是私有的,所有不能在类的外部使用new来实例化一个对象,只能通过访问Getinstance()来new一个实例,所以通过静态方法,使用类::GetInstance()来调用方法。

7.如何定义一个只能在堆上(栈上)生成对象的类?

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

只能建立在栈上:

     只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。

  1. class  A  
  2. {  
  3. private :  
  4.     void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
  5.     void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
  6. public :  
  7.     A(){}  
  8.     ~A(){}  
  9. }; 

只能建立在堆上

 类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数

动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数.

  1. class  A  
  2. {  
  3. protected :  
  4.     A(){}  
  5.     ~A(){}  
  6. public :  
  7.     static  A* create()  
  8.     {  
  9.         return   new  A();  
  10.     }  
  11.     void  destory()  
  12.     {  
  13.         delete   this ;  
  14.     }  
  15. };  

8.引用和指针有什么区别?

1.相同点:都是地址的概念,指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名;

2.区别:引用是别名,指针是地址,具体的:

• 指针可以在运行时改变其所指向的值,引用只在定义时被初始化一次,一旦和某个对象绑定就不再改变

引用没有const,指针有const,引用不能为空,指针可以为空

• 从内存上看,指针会分配内存区域,而引用不会,它仅仅是一个别名

• “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小

作用参数传递时,引用不用检查是否为NULL,指针则总是检测,防止为空。

• 引用不能为空,指针可以为空

9.const和define有什么区别?

本质:define只是字符串替换,const参与编译运行,具体的:

• define不会做类型检查,const拥有类型,会执行相应的类型检查

• define仅仅是预处理阶段做内容的替换,因此程序运行时,常量表中并没有define定义的常量,系统不会为它分配内存,⽽const定义的常量,在常量表中,会占用内存。

• define定义表达式时要注意“边缘效应”,加括号。

10.typedef和define的用法和区别?

(1).执行时间不同

关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。

#define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。

(2).功能有差异

typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。

#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

(3)作用域不同

 #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。

而typedef有自己的作用域。

11.define和inline有什么区别?

本质:define只是字符串替换,inline由编译器控制,具体的:

• define只是简单的宏替换,通常会产生二义性;而inline会真正地编译到代码中

• inline函数是否展开由编译器决定,有时候当函数太大时,编译器可能选择不展开相应的函数

12.malloc和new有什么区别?

1,malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

2,对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

3,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

4,C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

5、new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。

13.C++中static关键字作用有哪些?

1、隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏.

2、static的第二个作用是保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,

说到底static还是用来隐藏的。虽然这种用法不常见

3、static的第三个作用是默认初始化为0(static变量)

4、C++中的作用

1)不能将静态成员函数定义为虚函数。  

2)静态数据成员是静态存储的,所以必须对它进行初始化。 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误) 

3)静态数据成员在<定义或说明>时前面加关键字static。

14.C++中const关键字作用有哪些?

 修饰变量

 修饰成员函数,表示该成员函数不会修改成员变量

15.C++中成员函数能够同时用static和const进行修饰?

否,因为static表⽰示该函数为静态成员函数,为类所有;而const是用于修饰成员函数的,两者相矛盾。

16.下面三个变量分别代表什么含义?

const int* ptr;

int const* ptr;

int* const ptr;

前两个代表指向const变量的指针,即指针所指向的对象是const的,不能使用指针修改;最后一个代表const指针,即指针本身是const的,不能指向其他地址。

17.C++中包含哪几种强制类型转换?他们有什么区别和联系?

除了从C语言继承而来的两种转化方式(char a; int b=a(隐士),int *b=(int*)a(显式)),还自己的四种显式的强制类型转换。

• reinterpret_cast: 转换一个指针为其它类型的指针。它也允许从一个指针转换为整数类型,反之亦然. 这个操作符能够在非相关的类型之间转换. 操作结果只是简单的从一个指针到别的指针的值的二进制拷贝. 在类型之间指向的内容不做任何类型的检查和转换?

class A{};

class B{};

A* a = new A;

B* b = reinterpret_cast(a);

• static_cast: 允许执行任意的隐式转换和相反转换动作(即使它是不允许隐式的),例如:应用到类的指针上, 意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换), 同时, 也能够执行相反动作: 转换父类为它的子类

上行转换:子类指针或引用转换成基类表示——安全

下行转换:基类指针或引用转换成子类表示——危险(没有动态类型检查)

class Base {};

class Derive:public Base{};

Base* a = new Base;

Derive *b = static_cast(a);

• dynamic_cast: 只用于对象的指针和引用. 当用于多态类型时,它允许任意的隐式类型转换以及相反过程. 不过,与static_cast不同,在后一种情况里(注:即隐式转换的相反过程),dynamic_cast 会检查操作是否有效. 也就是说, 它会检查转换是否会返回一个被请求的有效的完整对象。检测在运行时进行. 如果被转换的指针不是一个被请求的有效完整的对象指针,返回值为NULL. 对于引用类型,会抛出bad_cast异常

• const_cast: 这个转换类型操纵传递对象的const属性,或者是设置或者是移除,例如:

class C{};

const C* a = new C;

C *b = const_cast(a);

18.下面两段代码的输出分别是什么?(虚函数的基本理解 )

(1)class Base{

    public:

        virtual void Print() const{

            cout << "Print in Base" << endl;

        }

};

class Derive::public base

{

    public:

        void Print() const{

            cout << "Print in Derive" << endl;

        }

};

void Print(const Base* base){

    base->Print();

}

int main(){

    Base b;

    Derive d;

    print(&b);

    print(&d);

    return 0;

}

 

(2)class Base{

    public:

        void Print() const{

            cout << "Print in Base" << endl;

        }

};

class Derive::public base

{

    public:

        void Print() const{

            cout << "Print in Derive" << endl;

        }

};

void Print(const Base* base){

    base->Print();

}

int main(){

    Base b;

    Derive d;

    print(&b);

    print(&d);

    return 0;

}

第一个:Print in Base, Print in Derive

第二个:Print in Base, Print in Base

19.简述C++虚函数作用及底层实现原理

要点是要答出虚函数表和虚函数表指针的作用。C++中虚函数使用虚函数表和虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的虚函数的地址;虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表。

20.一个对象访问普通成员函数和虚函数哪个更快?

访问普通成员函数更快,因为普通成员函数的地址在编译阶段就已确定,因此在访问时直接调 用对应地址的函数,而虚函数在调用时,需要首先在虚函数表中寻找虚函数所在地址,因此相比普 通成员函数速度要慢一些。

21.在什么情况下,析构函数需要是虚函数?

若存在类继承关系并且析构函数中需要析构某些资源时,析构函数需要是虚函数,否则当使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄露等问题

22.内联函数、构造函数、静态成员函数可以是虚函数吗?

都不可以。内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开; 构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类 的,因此不存在动态绑定的概念;静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的;

23.构造函数中可以调用虚函数吗?

可以,但是没有动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数

class Base 

public: 

    Base() 

    { 

        Fuction(); 

    } 

    virtual void Fuction() 

    { 

      cout<<"Base::Fuction" << endl; 

    } 

}; 

class A : public Base 

public: 

    A() 

    { 

        Fuction(); 

    } 

 

    virtual void Fuction() 

    { 

        cout << "A::Fuction" << endl; 

    } 

}; 

 

// 这样定义一个A的对象,会输出什么? 

A a; 

首先回答标题的问题,调用当然是没有问题的,但是获得的是你想要的结果吗?或者说你想要什么样的结果?

有人说会输出:

A::Fuction 

A::Fuction 

如果是这样,首先我们回顾下C++对象模型里面的构造顺序,在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造(虚继承、多重继承这里不讨论),如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样,实际的输出结果是:

Base::Fuction 

A::Fuction 

24.简述C++中虚继承的作用及底层实现原理?

虚继承用于解决多继承条件下的菱形继承问题,底层实现原理与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避免菱形继承中的二义性问题。

25.普通成员函数,虚函数,普通继承,虚继承

普通成员函数是静态调用,编译器隐式传入this指针的值,只会根据指针或引用的“字面值”类对象,调用自己的普通函数;Base *a=new derived; a->f();是调用基类的f()函数;

 

虚函数是动态调用,实现多态,运行时绑定,根据对象的虚指针指向类的虚函数表调用;

基类有自己的虚函数表,子类也有自己的虚函数表,且子类的同名虚函数会在自己的虚函数表中覆盖基类的虚函数。

Base *a=new derived; a->f()调用子类的虚函数f(),Base *a=new Base; a->f()是调用基类的f();

 

普通继承(会有虚函数);

虚继承是为了解决普通菱形继承基类成员对象二义性的问题,通过虚基类指针指向公共对象,是子类对象只保存一份基类对象;

普通继承:(有虚函数表)

 

菱形继承:二义性问题

 

虚继承:虚基类指针解决二义性问题

 

26.this指针

(1)this指针本质上是类成员函数的一个参数,指向对象本身,只是编译器隐藏起来了。但它只能在成员函数中使用,不能在全局,静态函数中使用。

(2)不占用对象的空间,所以不影响sizeof()的值。

(3)我们只有获得一个对象后,才能使用this指针。This指针只有在非静态成员函数中才有定义,所以即使获得一个对象后,也不能通过对象使用this指针。

实例:class Point

{

  int x, y;

public:

  Point(int a, int b) { x=a; y=b;}

  Void MovePoint( int a, int b){ x+=a; y+=b;}

  Void print(){ cout<<"x="<<x<<"y="<<y<<endl;}

};

void main( )

{

   Point point1( 10,10);

   point1.MovePoint(2,2);

   point1.print( );

}

当对象point1调用MovePoint(2,2)函数时,即将point1对象的地址传递给了this指针。
MovePoint函数的原型应该是 void MovePoint( Point *this, int a, int b);第一个参数是指向该类对象的一个指针,我们在定义成员函数时没看见是因为这个参数在类中是隐含的。这样point1的地址传递给了this,所以在MovePoint函数中便显式的写成:
void MovePoint(int a, int b) { this->x +=a; this-> y+= b;}
即可以知道,point1调用该函数后,也就是point1的数据成员被调用并更新了值。
即该函数过程可写成 point1.x+= a; point1. y + = b;

 

27.异常处理(assert/try,catch)

(1)在c++中头文件#include <cassert>,assert(expression)的作用是现计算表达式 expression ,如果其值为假(即为0),那么打印一条出错信息,然后通过调用 abort 来终止程序运行。

(2)Cout遇到错误情况,通过cout输出错误信息

(3)If(something error) do something else do something;

(4)异常处理的机制:C++中的异常处理的机制彻底改变了这种面貌,它使真正的计算处理和错误处理分开来,让程序员不再被这些琐碎的事情所烦扰,能关注于真正的计算处理工作。同时代码的可读性也好了。

其中关键字try表示定义一个受到监控、受到保护的程序代码块;关键字catch与try遥相呼应,定义当try block(受监控的程序块)出现异常时,错误处理的程序模块,并且每个catch block都带一个参数(类似于函数定义时的数那样),这个参数的数据类型用于异常对象的数据类型进行匹配;而throw则是检测到一个异常错误发生后向外抛出一个异常事件,通知对应的catch程序块执行对应的错误处理。

 例如: try{

            int a,b;    char s;

            cin>>a>>s>>b;

            if(s=='/'){

                  if(b==0)  throw "Divided by 0!";

                  cout<<a<<"/"<<b<<"="<<a/b<<endl;

            }

            else

                  if(s=='%') {  

                        if(b==0)      throw a;

                        cout<<a<<"%"<<b<<"="<<a%b<<endl;

                  }

                  else

                        cout<<"Option must be % or /."<<endl;

      }//try

      //捕获int类型的异常并处理

      catch(int i)  {       cout<<"Error occur:"<<i<<"%0"<<endl;  }

      //捕获char* 类型的异常并处理

      catch(char *str) {cout<<"Error occur:"<<str<<endl;  }

      catch(runtime_error err){ cout<<err.what()<<endl;}

      //捕获其它任何异常并处理

      catch(...){cout<<"Unkown Error"<<endl;}

      //没错误不运行catch,有错误至多运行一个catch语句块,且不再返回try语句块中。

      //不管有无异常都要运行到此处

      cout<<"Hello world"<<endl;

28.句柄和指针

句柄:是一个32位的整数,实际上是windows在内存中维护的一个对象(窗口等)内存物理地址列表的整数索引。

29.关键字volatile

提示编译器对象的值可能在编译器未监测到的情况下改变

30.交换两个变量的值,不使用第三个变量

(1)a=a+b; b=a-b; a=a-b;

(2)A=a^b; b=a^b;a=a^b;

31.操作系统的一些知识点

1. 列举几种进程的同步机制,并比较其优缺点。

答:原子操作、信号量机制、自旋锁、管程、会合、分布式系统

 

2 .进程之间通信的途径

答:共享存储系统、消息传递系统、管道:以文件系统为基础

 

3. 进程死锁的原因和4个必要条件

答:资源竞争及进程推进顺序非法;互斥、请求保持、不可剥夺、环路

 

4. 死锁的处理

答:鸵鸟策略、预防策略、避免策略、检测与解除死锁

 

5. 操作系统中进程调度策略有哪几种?

答:FCFS(先来先服务),优先级,时间片轮转,多级反馈

32.纯虚函数如何定义?使用时应注意什么?
答:virtual void f()=0; 是接口,子类必须要实现

33.线程与进程的区别和联系? 线程是否具有相同的堆栈? dll是否有独立的堆栈?

答:进程是死的,只是一些资源的集合,真正的程序执行都是线程来完成的,程序启动的时候操作系统就帮你创建了一个主线程。每个线程有自己的堆栈。DLL中 有没有独立的堆栈,这个问题不好回答,或者说这个问题本身是否有问题。因为DLL中的代码是被某些线程所执行,只有线程拥有堆栈,如果DLL中的代码是 EXE中的线程所调用,那么这个时候是不是说这个DLL没有自己独立的堆栈?如果DLL中的代码是由DLL自己创建的线程所执行,那么是不是说DLL有独 立的堆栈?

以上讲的是堆栈,如果对于堆来说,每个DLL有自己的堆,所以如果是从DLL中动态分配的内存,最好是从DLL中删除,如果你从DLL中分配内存,然后在EXE中,或者另外一个DLL中删除,很有可能导致程序崩溃

34.用两个栈实现一个队列的功能?要求给出算法和思路!

答:设2个栈为S1,S2, 一开始均为空.

入队时,将元素压入s1。

出队时,判断s2是否为空,如不为空,则直接弹出顶元素;如为空,则将s1的元素逐个“倒入”s2,把最后一个元素弹出并出队。

这个思路,避免了反复“倒”栈,仅在需要时才“倒”一次。

这样实现的队列入队和出队的平摊复杂度都还是O(1)

35.#define写一个“标准”宏,这个宏输入两个参数并返回较小的一个

#define min(x,y) ((x)>(y)?(y):(x))

 

posted @ 2016-09-26 09:51  uestc_summer  阅读(273)  评论(0)    收藏  举报