• 备战秋招,面试知识点总结:语言类(一)


    static关键字的作用:

    1. 全局静态变量

    作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾(只在本cpp内可见)。

    2.  局部静态变量

    作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

    3. 静态函数

    在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。

    函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;

    warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;

    4. 类的静态函数

    静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。

    在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
     
    对于函数定义和代码块之外的变量声明,static修改标识符的链接属性,由默认的external变为internal,作用域和存储类型不改变,这些符号只能在声明它们的源文件中访问。

    对于代码块内部的变量声明,static修改标识符的存储类型,由自动变量改为静态变量,作用域和链接属性不变。这种变量在程序执行之前就创建,在程序执行的整个周期都存在。

    对于被static修饰的普通函数,其只能在定义它的源文件中使用,不能在其他源文件中被引用

    对于被static修饰的类成员变量和成员函数,它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。

    c++中四种cast转换:

    1、const_cast:用于将const变量转为非const

      对于const变量,我们不能修改它的值,这是这个限定符最直接的表现。但是我们就是想违背它的限定希望修改其内容怎么办呢?

    下边的代码显然是达不到目的的: 

    const int constant = 10;
    int modifier = constant;

      因为对modifier的修改并不会影响到constant,这暗示了一点:const_cast转换符也不该用在对象数据上,因为这样的转换得到的两个变量/对象并没有相关性

    只有用指针或者引用,让变量指向同一个地址才是解决方案,可惜下边的代码在C++中也是编译不过的: 

    const int constant = 21;
    int* modifier = &constant 

      把constant交给非const的引用也是不行的。于是const_cast就出来消灭const,以求引起程序世界的混乱。

      常量对象(或基本类型)不可以被转换成非常量对象(或基本类型)。

      使用const_cast去掉const属性,其实并不是真的改变原类类型(或基本类型)的const属性,它只是又提供了一个接口(指针或引用),使你可以通过这个接口来改变类型的值。const int* 这种形式为底层const,也就是说无法通过这个指针去修改它指向的区域,但可通过变量本身去修改它子集。这里又提供了一个接口就是指相当于另外一个指针也指向了这个区域,但它不是底层const型的,因此可以通过这个接口来修改这块区域。

    2、static_cast

      static_cast相当于传统的C语言里的强制转换.

      ①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。

      进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;

      进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

      ②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。

      ③把空指针转换成目标类型的空指针。

      ④把任何类型的表达式转换成void类型。 注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性

      注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性

    3、dynamic_cast

      用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。

      在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能

      1)e的类型是目标type的公有派生类:派生类向基类转换一定会成功。向上转换

      2)e的类型是目标type的基类,当e是指针指向派生类对象,或者基类引用引用派生类对象时,类型转换才会成功,当e指向基类对象,试图转换为派生类对象时,转换失败。基类指针指向派生类,此时向下转换成功。比如有时需要调用派生类的非虚函数,此时用dynamic_cast进行转换,更安全。

      3)e的类型就是type的类型时,一定会转换成功。

      4、reinterpret_cast

      几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

    5、为什么不使用C的强制转换?

      C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

    请说一下C/C++ 中指针和引用的区别?

    1.指针有自己的一块空间,而引用只是一个别名;

    2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

    3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;

    4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

    5.可以有const指针,但是没有const引用;

    6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

    7.指针可以有多级指针(**p),而引用至于一级;

    8.指针和引用使用++运算符的意义不一样;

    9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

    请你说一下你理解的c++中的smart pointer四个智能指针: shared_ptr,unique_ptr,weak_ptr,auto_ptr

    为什么要使用智能指针:

    智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间

    1. auto_ptr(c++98的方案,cpp11已经抛弃)

    弃用原因:当两个智能指针都指向同一个堆空间时,每个智能指针都会delete一下这个堆空间,这会导致未定义行为。

    采用所有权模式。

    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.

    此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

    针对这个问题有3种策略:
    1、进行深度复制,有几个指针就复制几个对象;
    2、制定指针专有权的概念。即,只有一个智能指针能真正指向一个特定的对象,也只有该指针能析构这个对象所占用的空间,直到把这个指针赋给另一个指针,后一个指针才能真正指向这个对象,而前一个指针就不再起作用了,从而避免了两次delete而导致的未定义行为。这个概念比较适合auto_ptr和unique_ptr,但后者要求更严格(unique_ptr);
    3、记录性智能指针。即,有一个智能指针指向某对象,就把这个对象上的智能指针数加1,有一个指针不再指向该对象,就把这个对象上的智能指针数减1。只有当最后一个智能指针生命期结束了,才真正释放对象空间。(shared_ptr)
     

    2. unique_ptr(替换auto_ptr)

    unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

    采用所有权模式,还是上面那个例子

    1
    2
    3
    unique_ptr<string> p3 (new string ("auto"));   //#4
    unique_ptr<string> p4;                       //#5
    p4 = p3;//此时会报错!!

    编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

    另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

    unique_ptr<string> pu1(new string ("hello world"));
    unique_ptr<string> pu2;
    pu2 = pu1;                                      // #1 not allowed
    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

    其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

    注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

    unique_ptr<string> ps1, ps2;
    ps1 = demo("hello");
    ps2 = move(ps1);
    ps1 = demo("alexia");
    cout << *ps2 << *ps1 << endl;

    3. shared_ptr

    shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

    shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

    成员函数:

    use_count 返回引用计数的个数

    unique 返回是否是独占所有权( use_count 为 1)

    swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

    reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

    get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的

    shared_ptr不要使用这样的方式初始化:

    int *p=new int(1);
    shared_ptr<int> p1(p);
    cout<<p1.use_count()<<endl;
    shared_ptr<int> p2(p);
    cout<<p1.use_count()<<endl;
    cout<<p2.use_count()<<endl;
    

     这样p1和p2的引用计数均为1,析构时将会对p区域析构两次.包括get()方法也是,shared_ptr.get返回原始指针,如果使用这个原始指针初始化shared_ptr也会造成这样的问题。

    正确的初始化方式应该使用make_shared或者另一个shared_ptr来初始化:

    int *p=new int(1);
    shared_ptr<int> p1(p);
    cout<<p1.use_count()<<endl;
    shared_ptr<int> p2(p1);
    cout<<p1.use_count()<<endl;
    cout<<p2.use_count()<<endl;
    

     这样p1与p2的引用计数均为2,不会出现内存崩溃的问题。


    4. weak_ptr

    weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

    weak_ptr无法管理对象,必须通过expired判断shared_ptr指向的对象是否可用,如果返回false则可用,可以通过lock()->func()来升为shared_ptr并调用func().

    注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

    回答一下数组和指针的区别:

    指针

    数组

    保存数据的地址

    保存数据

    间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据

    直接访问数据,

    通常用于动态的数据结构

    通常用于固定数目且数据类型相同的元素

    通过Malloc分配内存,free释放内存

    隐式的分配和删除

    通常指向匿名数据,操作匿名函数

    自身即为数据名

      

    为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数

    将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

    C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

    假设A为基类,B为继承类,尽管A的析构函数并不是虚函数,如果B* b=new B();delete b时仍会先调用B的析构,在调用A的析构。而如果A* b=new B();delete b时则只会调用A的析构函数。

    说一下函数指针

    函数指针是指向函数的指针变量。

    函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。

    C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

      

    C++中析构函数的作用

    如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

    如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

    类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

    静态函数和虚函数的区别

    静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

    1、对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
    2、对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。

    3、静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
    4、动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期

    重载和覆盖

    重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
    重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

    为什么不能通过函数返回值对函数进行重载:比如int func()与void func()。实际上在使用时是可以忽略函数的返回值的,比如int func(),可以不用将返回值赋值而直接使用: func()。这与调用void func()时没有区别,因此不能作为对函数重载的依据。

    strcpy和strlen

    strcpy是字符串拷贝函数,原型:

    char *strcpy(char* dest, const char *src);

    从src逐字节拷贝到dest,直到遇到''结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数。
    strlen函数是计算字符串长度的函数,返回从开始到''之间的字符个数。

    strcpy (目标串地址,源串的开始地址): 从源串的开始到结尾('')完全拷贝到目标串地址
    strncpy(目标串地址,源串的开始地址,n): 从源串的开始拷贝n个字符到目标串地址,n大于源串长度时,遇到''结束; n小于源串长度时,到第n个字符结束,但不会在目标串尾补''

    虚函数和多态(静态多态与动态多态)

    多态的实现主要分为静态多态动态多态静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
    虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

    智能指针shared_ptr的实现

    template<typename T>
    class smart
    {
    private:
    	T* _ptr;
    	int* _count; //reference couting
    
    public:
    	//构造函数
    	smart(T* ptr = nullptr) :_ptr(ptr)
    	{
    		if (_ptr)
    		{
    			_count = new int(1);
    		}
    		else
    		{
    			_count = new int(0);
    		}
    	}
    
    	//拷贝构造
    	smart(const smart& ptr)
    	{
    		if (this != &ptr)
    		{
    			this->_ptr = ptr._ptr;
    			this->_count = ptr._count;
    
    			(*this->_count)++;//count位于堆中,因此在某个指向该区域的shared_ptr中对count+1等价于所有shared_ptr均+1.
    		}
    	}
    
    	//重载operator=
    	smart operator=(const smart & ptr)
    	{
    		if (this->_ptr == ptr._ptr)
    		{
    			return *this;
    		}
    		if (this->_ptr)
    		{
    			(*this->_count)--;//这里是改变指向,因此原指向的count要-1并判断是否析构。而shared_ptr<int> p=a;则是调用的拷贝构造。
    			if (this->_count == 0)
    			{
    				delete this->_ptr;
    				delete this->_count;
    			}
    		}
    		this->_ptr = ptr._ptr;
    		this->_count = ptr._count;
    		(*this->_count)++;
    		return *this;
    	}
    
    	//operator*重载
    	T& operator*()
    	{
    		if (this->_ptr)
    		{
    			return *(this->_ptr);
    		}
    	}
    
    	//operator->重载
    	T& operator->()
    	{
    		if (this->_ptr)
    		{
    			return this->_ptr;
    		}
    	}
    
    	//析构函数
    	~smart()
    	{
    		(*this->_count)--;
    		if (*this->_count == 0)
    		{
    			delete this->_ptr;
    			delete this->_count;
    		}
    	}
    	//return reference couting
    	int use_count()
    	{
    		return *this->_count;
    	}
    };
    

      

    常量存放在内存的哪个位置

    对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。

    //main.cpp  
    int a = 0; 全局初始化区  
    char *p1; 全局未初始化区  
    main()  
    {  
        int b; 栈  //因为是在main()开始之后,所以在栈区,这与int a=0不同。
        char s[] = "abc"; 栈  
        char *p2; 栈  
        char *p3 = "123456"; 123456在常量区,p3在栈上。  
        static int c =0; 全局(静态)初始化区  
        p1 = (char *)malloc(10);  
        p2 = (char *)malloc(20);  
        分配得来得10和20字节的区域就在堆区。  
        strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"
    优化成一个地方。  
    }  
    

     

    如果同时定义了两个函数,一个带const,一个不带,会有问题吗?

    当这两个函数作为普通的函数时,编译会报错,无法仅按返回类型区分两个函数
    当这两个函数作为类的成员函数时,是没有问题的
    在类外,const修饰函数只能修饰返回值,如:
    const int func();
    而重载无法根据返回值来确定。
    但在类内:
    int func() const
    这样是可行的。

    C++函数栈空间的最大值

    默认是1M,不过可以调整

    ulimit -s 102400,即修改为100MB。

    说一说extern“C”

    假设某个函数的原型为:

    void foo( int x, int y );
    

    该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。
    _foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。

    假设在C++中,模块A的头文件如下:

    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    int foo( int x, int y );
    #endif
    

    在模块B中引用该函数:

    // 模块B实现文件 moduleB.cpp
    #include "moduleA.h"
    foo(2,3);
    

    实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!
      加extern "C"声明后的编译和连接方式
      加extern "C"声明后,模块A的头文件变为:

    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    extern "C" int foo( int x, int y );
    #endif
    

    在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
    (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
    (2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

    总而言之就是:这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

    因此,在使用C的库时,要在声明C的库的头文件时加上extern C(extern C{#include<?>}),这样cpp文件才不会将函数名按照C++的方式进行命名,从而导致按照新名的方式去C的库的模块中去寻找而找不到。

    extern "C"的惯用法

    在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

    extern "C"
    {
    #include "cExample.h"
    }

    new/delete与malloc/free的区别是什么

    new/delete是C++的关键字,而malloc/free是C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数

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

    new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

    malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

    C++允许重载new/delete操作符,而malloc不允许重载。

    说说你了解的RTTI

    运行时类型检查,在C++层面主要体现在dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针。对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info

    虚函数表具体是怎样实现运行时多态的

    子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。

    C语言是怎么进行函数调用的(有时间仔细看看)

    每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。

    C++中拷贝构造函数的形参能否进行值传递?

    不能。如果使用值传递,则传递参数时对参数又会调用一次拷贝构造,在新的拷贝构造中也又会进行一次拷贝构造,最后循环造成栈溢出。

    赋值函数为什么要有返回值?赋值函数为什么要返回引用?

    像a=b=c这种方式,需要有返回值才可用。

    返回引用效率更高,如果值返回,则会用*this构造一个新的类,然后返回这个类的引用。

    说一说select

    select在使用前,先将需要监控的描述符对应的bit位置1,然后将其传给select,当有任何一个事件发生时,select将会返回所有的描述符,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大

    select,poll和epoll:

    (1)select==>时间复杂度O(n)

    它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

    (2)poll==>时间复杂度O(n)

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

    (3)epoll==>时间复杂度O(1)

    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

    select系统调用的目的是:在一段指定时间内。监听用户感兴趣的文件描写叙述符上的可读、可写和异常事件。poll和select应该被归类为这种系统调用,它们能够堵塞地同一时候探測一组支持非堵塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。 

    静态函数和虚函数的区别:

    静态函数在编译的时候就已经确定运行时机虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

    C++中类成员的访问权限

    C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

    C++中三种继承方式

    (1) 公有继承(public)

    公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

    (2)私有继承(private)

    私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

    (3)保护继承(protected)

    保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。

    private能够对外部子类保密,即除了成员所在的类本身可以访问之外,别的都不能直接访问protected能够对外部保密,但允许子类直接访问这些成员

    C++中struct和class的区别

    在C++中,可以用struct和class定义类,都可以继承。区别在于:structural的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。

    另外,class还可以定义模板类形参,比如template <class T, int i>。

  • 相关阅读:
    1.JMeter===添加响应断言
    1.Linux下Git入门学习
    14.Selenium+Python使用火狐浏览器问题解决
    13.Selenium不再支持PhantomJS
    12.Selenium+Python案例 -- 今日头条(获取科技栏目的所有新闻标题)
    11.Selenium+Python案例--百度
    10.Selenium+Python+任务计划程序实现定时发送邮件
    The connection to adb is down and a sever error has occured的解决
    Eclipse与github整合完整版
    GIT命令整理
  • 原文地址:https://www.cnblogs.com/lxy-xf/p/11328080.html
Copyright © 2020-2023  润新知