C/C++知识点回顾与总结

  本博客总结C/C++的常见知识点,如有问题欢迎提出,转载请注明出处http://blog.csdn.net/qq_34342154/article/details/78876099

一、C和C++的区别

  • C++在C的基础上增加类
  • C面向过程,C++面向对象
  • C主要考虑通过一个过程将输入量经过各种运算后得到一个输出, C++ 主要考虑是如何构造一个对象模型,让这个模型契合与之对应的问题域, 这样就可以通过获取对象的状态信息得到输出。

二、面向对象的特性

  封装、继承、多态。

  • 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装可以隐藏实现细节,实现代码的模块化。
  • 继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。继承可以实现代码重用,扩展已存在的代码模块,提高开发效率。
  • 补充:子类继承父类大部分的资源,不能继承的有构造函数,析构函数,拷贝构造函数, 赋值运算符,友元函数等等
  • 多态:一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。虽然针对不同对象的具体操作不同,但通过一个公共的类,这些操作可以通过相同的方式被调用。多态实现了接口重用。

三、虚函数

  虚函数的作用是实现多态性,多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。
  
1、什么是虚函数,纯虚函数,抽象基类

  • 虚函数:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。虚函数在基类中是有定义的,即便定义为空。 在子类中可以重写。
  • 纯虚函数:是一种特殊的虚函数,使用virtual关键字,并且在其后面加上=0。纯虚函数在基类中没有定义, 必须在子类中加以实现。
  • 抽象基类:在基类中加入至少一个纯虚函数,使基类成为抽象类。

2、为什么要使用虚函数、纯虚函数

  虚函数
  基于向上类型转换,基类可以通过虚函数对多个子类中相似的功能进行统一管理。简单的说就是使用基类的指针指向子类的对象但是仍然可以正确的调用子类的功能。

  纯虚函数
  在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。

3、虚函数表

  C++中的虚函数的实现一般是通过虚函数表。
  
  类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。
  
  注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。
  
  虚函数(Virtual Function)是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
  
  编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
  
  虚函数表的实现机制参见另一篇博客C++虚函数表剖析。

4、哪些函数不能声明为虚函数

  • 构造函数:虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中;若构造函数声明为虚函数,在调用这个虚构造函数之前,对象还未创建,即还没有分配内存空间,更没有指向虚函数表的指针,所以该虚构造函数无法被调用;
  • 补充:析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数。这是因为,在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,避免内存泄漏。
  • 内联函数:内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;
  • 静态成员函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的。
  • 友元函数:因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

四、数组和链表的区别

从逻辑结构来看:

  • 数组申请的是一块连续的内存空间,编译阶段就确定了空间大小,运行阶段是不允许改变的,不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数,造成数据越界;当数据减少时,造成内存浪费;
  • 链表动态地进行存储分配,现用现申请,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据next指针找到下一个元素。

从内存存储来看:

  • (静态)数组从栈中分配空间, 对于程序员方便快速,但是自由度小,数组可以根据下标直接存储数据。如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。
  • 链表从堆中分配空间, 自由度大但是申请管理比较麻烦 ,链表是物理上非连续的内存空间,对于访问数据,需要从头便利整个链表直到找到要访问的数据,没有数组有效,但是在添加和删除数据方面,只需要知道操作位置的指针,很方便可以实现增删,较数组比较灵活有效率。

从上面的比较可以看出,如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反, 如果需要经常插入和删除元素就需要用链表数据结构了。

五、const和static的使用

1、const

  • const修饰类的成员变量,表示该成员变量不能被修改。
  • const修饰函数,表示本函数不会修改类内的数据成员。不会调用其他非const成员函数。
  • const函数只能调用const函数,非const函数可以调用const函数
    类外定义的const成员函数,在定义和声明出都需要const修饰符。

2、static

对变量:

对局部变量来说:

  • 在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。位于内存中静态存储区; 未初始化的局部变量初始化为0。 作用域仍是局部作用域。注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问。也就是说,一个被声明为静态的变量在这一函数被调用过程中维持上一次的值不变,即只初始化一次),但未改变其作用域。

对全局变量来说:

  • 在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。位于静态存储区,未经初始化的全局静态变量会被程序自动初始化为0,全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。注: static修饰全局变量并未改变其存储位置及生命周期,而是改变了其作用域,使得当前文件外的源文件无法访问该变量。不能被其他文件访问和修改,其他文件中可以使用相同名字的变量,不会产生冲突。

对类:

对成员变量来说:

  • 用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。

注意:

  • 不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
  • 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
  • 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。

对成员函数来说:

  • 用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
  • 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不需要加static修饰符。
  • 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
  • 不可以同时用const和static修饰成员函数。(C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的)。

3、 类的static变量在什么时候初始化,函数的static变量在什么时候初始化

  类的静态成员在类实例化之前就存在了,并分配了内存。函数的static变量在执行此函数时进行实例化。

六、指针和引用

1、指针与引用的区别

  • 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
  • 指针可以有多级,但是引用只能是一级;
  • 指针的值可以为空,也可能指向一个不确定的内存空间,但是引用的值不能为空,并且引用在定义的时候必须初始化为特定对象(因此引用更安全);
  • 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变引用对象了;
  • sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样;

2、引用作为参数的优点

  • 传递引用给函数与传递指针的效果是一样的;
  • 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效);
  • 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用 “*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

注意:

  • 不能返回局部变量的引用。主要原因是局部变量(栈中分配内存)会在函数返回后被销毁,因此被返回的引用就成为了 “无所指”的引用,程序会进入未知状态。
  • 不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题(new在堆中分配内存),可对于这种情况,又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
  • 可以返回类成员的引用,但最好是const。 主要原因是当对象的属性是与某种业务规则相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

3、引用与多态的关系

  引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。例如:

Class A; 
Class B : Class A{…};

B b;
A& ref = b;

七、内存分配

1、一个由C/C++编译的程序占用的内存分为哪几个部分

  • 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。可静态也可动态分配。其操作方式类似于数据结构中的栈。
  • 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。动态分配。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局区(静态区):程序结束后由系统释放,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域;未初始化的全局变量和静态变量在相邻的另一块区域(BSS,Block Started by Symbol),在程序执行之前BSS段会自动清0。
  • 文字常量区:程序结束后由系统释放,常量字符串就是放在这里的。
  • 程序代码区:存放函数体的二进制代码。

2、堆栈溢出的原因

  数组越界, 没有回收内存, 深层次递归调用

3. 内存泄漏

  内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
  
  避免:使用的时候应记得指针的长度; 分配多少内存应记得释放多少, 保证一一对应的关系; 动态分配内存的指针最好不要再次赋值。

八、#define和const的区别

  1. define不会做类型检查(容易出错),const拥有类型,会执行相应的类型检查
  2. define仅仅是宏替换,不占用内存,而const会占用内存
  3. const内存效率更高,编译器可能将const变量保存在符号表中,而不会分配存储空间,这使得它成 为一个编译期间的常量,没有存储和读取的操作

注意

  • 当使用#define定义一个简单的函数时,强烈建议使用内联函数替换
  • 当使用#define时,定义部分的每个形参和整个表达式都必须用括号括起来,以避免不可预料的错误。

九、malloc和new

  1. malloc与free是C/C++语言的标准库函数,new/delete是C++的运算符。但它们都可用于申请动态内存和释放内存。
  2. 对于非内部数据类型的对象而言,用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,和一个能完成清理与释放内存工作的运算符delete。
  3. new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。new delete在实现上其实调用了malloc,free函数。
  4. new 建立的是一个对象;malloc分配的是一块内存。

十、extern的作用

  extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

  也就是说extern有两个作用。第一个作用,当extern修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或者其他模块中使用,记住它是一个声明不是定义!也就是说B模块要是引用模块A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量的定义,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

  第二个作用,与”C”一起连用。extern “C”实现C++与C的混合编程,是用在C和C++之间的桥梁。如:extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时是按着C的规则去翻译相应的函数名而不是C++的。之所以需要这样做是因为C编译器编译函数时不带参数的类型信息,只包含函数符号名字;而C++编译器为了实现函数重载,编译时会带上参数的类型信息,如func(void) 可能变成 _Z4funcv,func(int) 变成_Z4funci, func(int, float)变成 _Z4funcif。

  在C++中引用C语言中的函数和变量,在包含C语言头文件时,需进行下列处理:

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

十一、.h头文件中的ifndef/define/endif 的作用?

  防止该头文件被重复引用。

  详解参见另一篇博客C/C++常用预处理指令总结。

猜你喜欢

转载自blog.csdn.net/qq_34342154/article/details/78876099
今日推荐