C++的一些知识点总结

无聊翻去年春招找实习整理的笔记,想起来当时面试前临时抱佛脚的日子,贴出来或许有用。

注意,只是一些知识点,并不全面,也不一定正确。🤣🤣🤣🤣(因为我最后也没找到C++工作,知识点也差不多忘光了)

1.c++的构造函数为什么不能是虚函数,而基类的析构函数必须是虚函数?

构造函数为什么不能是虚函数呢?


    首先需要了解 vptr指针和虚函数表的概念,以及这两者的关联。

    vptr指针指向虚函数表,执行虚函数的时候,会调用vptr指针指向的虚函数的地址。

    当定义一个对象的时候,首先会分配对象内存空间,然后调用构造函数来初始化对象。vptr变量是在构造函数中进行初始化的。又因为执行虚函数需要通过vptr指针来调用。如果可以定义构造函数为虚函数,那么就会陷入先有鸡还是先有蛋的循环讨论中。

基类的析构函数为什么必须是虚函数呢?


    我们都知道,想要回收一个对象申请的资源,那么就需要调用析构函数。虽然我们没有显示地调用析构函数,但是编译器都会默认地为我们执行析构函数。

    那么当我们执行 BaseClass *base = new BaseClass(); 当我们执行 delete base时,会调用析构函数为我们释放资源。而 我们执行BaseClass *sub = new SubClass(); 如果BaseClass基类的析构函数不是虚函数的时候,delete sub 对象的时候,只会释放BaseClass 基类申请的资源,而不是释放SubClass派生类的资源。原因如下:

    基类指针指向了派生类对象,而基类中的析构函数是非virtual的,而虚构函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete的时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏,这是相当危险的:如果系统中有大量的派生类对象被这样创建和销毁,就会有内存不断的泄漏,久而久之,系统就会因为缺少内存而崩溃。

    当然,如果在派生类中没有动态申请有资源的时候,是不会造成内存泄漏的。而当派生类对象的析构函数中有内存需要回收,并且在编程过程中采用了基类指针指向派生类对象,如为了实现多态,并且通过基类指针将对象销毁,这时,就会因为基类的析构函数为非虚函数而不触发动态绑定,从而没有调用派生类的析构函数而导致内存泄漏。    

    因此,为了防止这种情况下的内存泄漏的发生,最后将基类的析构函数写成virtual虚析构函数。



 2.重载和重写的区别  


重载:指同一可访问区域内被声明的几个具有不同参数列的同名函数,使用时根据参数列表确定调用哪个函数,重载不关心函数的返回类型。

class A{
public:
  void test(int i);
  void test(double i);
  void test(int i, double j);
  void test(double i, int j);
  int test(int i);         //错误,非重载
};


重写:即覆盖,指在派生类中存在重新定义的函数。其函数名、参数列表和返回类型同基类中被重写的函数一致,只有函数体不同。重写的基类中被重写的函数必须有virual修饰。

class A
{
public :
       virtual void func1()
      {
           cout << "A::func1()" << endl;
      }
private :
       int _a;
};
class B : public A
{
public :
       virtual void func1()
      {
           cout << "B::func1()" << endl;
      }
private :
       int _b;
};



 3.多态的实现及原理


C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数

为什么?


因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用晚绑定,当编译器使用晚绑定时候,就会在运行时再去确定对象的类型以及正确的调用函数,而要让编译器采用晚绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们就称之为虚函数,一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

 虚表


编译器在编译的时候,发现Father类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即 vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址。


那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数,对于第二段代码程序,由于pFather实际指向的对象类型是Son,因此vptr指向的Son类的vtable,当调用pFather->Son()时,根据虚表中的函数地址找到的就是Son类的Say()函数,且续表在构造函数初始化。

总结


1:每一个类都有虚表

2:虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现,如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现,如果派生类有自己的虚函数,那么虚表中就会添加该项。

3:派生类的虚表中虚地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

这就是c++中的多态性,当c++编译器在编译的时候,发现Father类的Say()函数是虚函数,这个时候c++就会采用晚绑定技术,也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型来确认调用的是哪一个函数,这种能力就叫做c++的多态性,我们没有在Say()函数前加virtual关键字时,c++编译器就确定了哪个函数被调用,这叫做早期绑定。

c++的多态性就是通过晚绑定技术来实现的。

c++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。



 3.new/delete与malloc/free的区别与联系?


 属性


new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。

参数


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

 返回类型


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

分配失败


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

 自定义类型


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

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

重载


C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

 内存区域


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




4.模板声明与定义要放在同一文件中?


“通常情况下,你会在.h文件中声明函数和类,而将它们的定义放置在一个单独的.cpp文件中。但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。这就是为什么所有的STL头文件都包含模板定义的原因。”[1]

"标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。”



 5.四种cast转换


C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast

用于将const变量转为非const

2、static_cast

用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

3、dynamic_cast

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

向上转换:指的是子类向基类的转换

向下转换:指的是基类向子类的转换

它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

4、reinterpret_cast

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



6.const char * 、char const *、 char * const 三者的区别


前两者都是指向char类型常量的指针,第三者是指向char类型的指针常数



 7.C++普通函数,普通成员函数、静态成员函数的区别


普通函数:属于全局函数,不受具体类和对象的限制,可以直接调用。

普通成员函数:C++ 普通成员函数本质上是一个包含指向具体对象this指针的普通函数,即c++类的普通成员函数都隐式包含一个指向当前对象的this指针。

静态成员函数

首先介绍类的静态成员变量: 类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。

静态成员变量的性质:

1)static型变量只被初始化一次,下次执行初始化语句会直接跳过。

2)static 说明一个类的成员为静态成员,经过static修饰的成员变量属于这个类,不再仅仅属于具体的对象。

再介绍静态成员函数: 类体中的成员函数的声明前加上static关键字,该成员函数就成为了该类的静态成员函数。

静态成员函数的性质:

1)不可以调用类的非静态成员。

2)静态成员函数不含this指针。 静态成员函数属于这个类,不再仅仅属于具体的对象。

因此类的静态成员函数和类的普通成员函数的区别是:

静态成员函数不包含指向具体对象的this指针;

普通成员函数包含一个指向具体对象的this指针。

 8.传值传引用传指针


参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。**

(1)传入的实参实际上是a和b的副本而非其本身,所以对副本的改变并不会反应到a和b本身上。

(2)传入的实参实际上是a和b的引用,对引用的改变会直接反应到a和b本身上。

(3)传入的实参实际上是a和b的指针的副本,而且改变的是副本本身(其间接引用),不会(会)影响的指针所指向的值,即a和b本身上。




 9.C++堆栈 内存四区



(1)代码区


程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到代码区,这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。

(2)静态区


静态区存放程序中所有的全局变量和静态变量。

(3)栈区


栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参都存储在栈中,这个动作由编译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超出其作用域时,自动从栈中弹出。

- 每个线程都有自己专属的栈;

- 栈的最大尺寸固定,超出则引起栈溢出;

- 变量离开作用域后栈上的内存会自动释放。


(4)堆区


堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决上面实验三造成的内存溢出困难。一般比较复杂的数据类型都是放在堆中。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。对于一个32位操作系统,最大管理管理4G内存,其中1G是给操作系统自己用的,剩下的3G都是给用户程序,一个用户程序理论上可以使用3G的内存空间。堆上的内存必须手动释放(C/C++),除非语言执行环境支持GC(如C#在.NET上运行就有垃圾回收机制)。那堆内存如何使用?

 10.引用和指针的区别


指针和引用都是地址的概念,指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
程序为指针变量分配内存区域,而不为引用分配内存区域。

1.指针使用时要在前加 * ,引用可以直接使用。

2.引用在定义时就被初始化,之后无法改变;指针可以发生改变。 即引用的对象不能改变,指针的对象可以改变。

3.没有空引用,但有空指针。这使得使用引用的代码效率比使用指针的更高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。

4.对引用使用“sizeof”得到的是变量的大小,对指针使用“sizeof”得到的是变量的地址的大小。

5.理论上指针的级数没有限制,但引用只有一级。即不存在引用的引用,但可以有指针的指针。
int **p //合法
int &&p //非法
6.++引用与++指针的效果不一样。
例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。



11.运算符重载


1. 一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
2. 以下一些双目运算符不能重载为类的友元函数:=、()、[]、->。
3. 类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。 C++提供4个类型转换函数:reinterpret_cast(在编译期间实现转换)、const_cast(在编译期间实现转换)、stactic_cast(在编译期间实现转换)、dynamic_cast(在运行期间实现转换,并可以返回转换成功与否的标志)。
4. 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
5. 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数。
6. 当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部 类型的对象,该运算符函数必须作为一个友元函数来实现。
7. 当需要重载运算符具有可交换性时,选择重载为友元函数。
8. 重载为成员函数时参数将会被调用,例如是二元运算重载,第一个参数成了调用对象,第二的才是真正的参数

#八股文#
全部评论
感谢分享
1 回复 分享
发布于 2022-06-16 08:16
楼主,最后找的什么岗?
点赞 回复 分享
发布于 2022-06-16 09:59
点赞 回复 分享
发布于 2022-06-21 10:59

相关推荐

37 289 评论
分享
牛客网
牛客企业服务