1.程序运行知识
1.1 内存布局和分配方式
C程序的内存布局如下:
- 静态存储区:存储全局变量和static变量,通常在程序编译期间已经分配好了。
- BSS段:存放未初始化的static变量和全局变量
- Data段:存放初始化过的static变量和全局变量
- Text段:存储程序的二进制代码,程序代码区。
- 堆:程序运行时通过malloc申请的内存区存放在堆中,需要使用free来释放该内存空间,生存期在malloc和free之间。
- 栈:执行函数时,函数的局部变量存储在栈中,执行结束后自动释放该内存区域,栈内存分配运算内置与处理器指令集中。
C++程序的内存布局与C程序布局类似,区别是C++不再区分全局变量和静态变量是否已经初始化,全部存储在静态存储区;另外堆中存放new/delete申请释放的资源,而malloc和free申请的资源存放在自由存储区。
1.2 内存溢出原因
- 栈溢出:越界访问造成,例如局部变量数组越界访问或者函数内局部变量使用过多,超出了操作系统为该进程分配的栈的大小,还有递归函数层次过多超过了栈大小。
- 堆溢出:程序申请了资源但忘记释放该资源,造成内存泄露,累积泄露内存过多会造成内存溢出。
1.3 内存泄露和检测
-
C++内存泄漏检测内存泄露是指程序中动态分配了内存,但是在程序结束时没有释放这部分内存,从而造成那一部分内存不可用的情况。
-
动态内存泄露检测:检查new/delete的资源是否正确释放,检查程序运行期间内存是否一直在增长,使用内存检测工具来检测泄露情况。
1.4 程序生成过程
- 预处理阶段:根据文件中的预处理指令来修改源文件的内容。如#include指令,作用是把头文件的内容添加到.cpp文件中。
- 编译阶段:将其翻译成等价的中间代码或汇编代码。
- 汇编阶段:把汇编语言翻译成目标机器指令。
- 链接阶段:例如,某个源文件中的函数可能引用了另一个源文件中定义的某个函数;在程序中可能调用了某个库文件中的函数。
1.5 预编译
- 定义:预编译又称为预处理 , 是做些代码文本的替换工作。处理 # 开头的指令 , 比如拷贝 #include 包含的文件代码, #define 宏定义的替换 , 条件编译等。
- 功能:宏定义,文件包含,条件编译。
1.6 头文件的作用
- 保存程序的声明。
- 通过头文件可以来调用库函数。因为有些代码不能向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,编译器会从库中提取相应的代码。
- 如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
2. 基本数据类型和用法知识
2.1 struct与class的区别
- 默认情况下,struct的成员变量是public的,而class是private的。
- struct保证成员按照声明顺序在内存中存储,而class不能保证。
- 默认情况下,struct是public继承,而class是private继承。
2.2 struct与union的区别
- struct中各个成员变量是独立的,union中的成员变量共享同一片内存区域,内存区域长度由成员变量中长度最大的一个决定。
- struct不能保证分配的是连续内存,但union分配的是连续内存。
2.3 const和define的用途以及区别
- const用途:用来定义常量、修饰函数参数、修饰函数返回值,可以避免被修改,提高程序的健壮性。
- define用途:是宏定义,在编译的时候会进行替换,这样做的话可以避免没有意义的数字或字符串,便于程序的阅读。
- 区别:const定义的数据有数据类型,而宏常量没有数据类型。编译器可以对const常量进行类型检查。而对宏定义只进行字符替换,没有类型安全检查,所以字符替换时可能出错。
2.4 枚举和define的区别
- #define 是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
- 一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
- 枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。
2.5 内联函数和宏的区别
- 内联函数在编译时展开,宏在预编译时展开。
- 在编译的时候内联函数可以直接被嵌入到目标代码中,而宏只是一个简单的文本替换,内联函数可以完成诸如类型检测、语句是否正确等编译功能,宏就不具备这样的功能。inline函数是函数,宏不是函数。
2.6 new/delete和malloc/free的区别
- new/delete用调用构造函数来实例化对象和调用析构函数释放对象申请的资源。
- malloc/free用来申请内存和释放内存,但是申请和释放的对象只能是内部数据类型。
- malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。
2.7 delete和delete[]的区别
- delete只会调用一次析构函数,delete[]会调用每一个成员的析构函数。
- delete与new配套,delete []与new []配套,用new分配的内存用delete删除用new[]分配的内存用delete[]删除。
2.8 指针和引用的概念和区别
-
指针指向一块内存,指针保存的是内存的地址;引用是变量的别名,本质是引用该变量的地址。解引用是取指针指向的地址的内容,取地址是获得变量在内存中的地址。
- 引用在创建的同时必须初始化,保证引用的对象是有效的,所以不存在NULL引用。
- 指针在定义的时候不必初始化,所以,指针则可以是NULL,可以在定义后面的任何地方重新赋值。
- 引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用。
- 指针在任何时候都可以改变为指向另一个对象。
- 引用的创建和销毁并不会调用类的拷贝构造函数。
- 因为不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,所以比指针安全。由于const 指针仍然存在空指针,并且有可能产生野指针,所以还是不安全。
- 程序会给指针变量分配内存区域,而引用不需要分配内存区域。
- 返回引用时,在内存中不产生被返回值的副本。
2.9 memset,memcpy和strcpy的区别
- memset用来对一段内存空间全部设置为某个字符。
- memcpy是内存拷贝函数,可以拷贝任何数据类型的对象。
- strcpy只能拷贝字符串,遇到’\0′结束拷贝。
2.10 指针在16位机,32位机,64位机中分别占多大内存
- 16位机:2字节。
- 32位机:4字节。
- 64位机:8字节。
2.11 字符指针,浮点数指针和函数指针哪个占用内存更大
- 一样大,指针的占用内存大小只和机器相关。
2.12 如何引用一个全局变量
- 在同一文件中:直接引用。
- 咋不同文件中:直接引用头文件;使用extern声明变量。
2.13 变量声明和定义的区别
- 变量声明:告诉编译器有某个类型的变量,但不会为其分配内存。
- 变量定义:位该类型的变量分配内存。
2.14 野指针,未初始化指针和空指针的区别
- 野指针:指向一个已删除的对象或无意义地址的指针。
- 原因:指针变量没有被初始化,或者指针p被free或者delete之后,没有置为NULL。
- 空指针:空指针表示“未分配” 或者“尚未指向任何地方” 的指针。
- 区别:空指针可以确保不指向任何对象或函数; 而未野指针或初始化指针则可能指向任何地方。
2.15 常量指针和指针常量的区别
- 常量指针:是一个指向常量的指针。可以防止对指针误操作而修改该常量。
- 指针常量:是一个常量,且是一个指针。指针常量不能修改指针所指向的地址,一旦初始化,地址就固定了,不能对它进行移动操作。但是指针常量的内容是可以改变。
2.16 指针函数和函数指针的区别
- 指针函数:返回值是指针的函数。
- 函数指针:一个指向函数的指针。函数名被括号括起来,并且加有指针符号。
2.17 const char*, char const*, char* const的区别
- char * const cp;//cp是常指针,指向char类型的数据
- const char * cp;//cp是char类型的指针,指向const char
- char const * p;//C++里面没有const*的运算符,所以const属于前面的类型。
2.18 static全局变量与普通的全局变量的区别
- 全局变量在整个工程文件内都有效。
- 静态全局变量只在定义它的文件内有效。
- 全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。
2.19 static局部变量和普通局部变量的区别
- 静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失,直到程序运行结束后才释放。
- 普通局部变量在定义它的函数内有效,这个函数返回会后失效。
- static局部变量会自动初始化,而局部变量不会。
2.20 sizeof用在不同对象上的区别
- sizeof是C语言的一种单目操作符,并不是函数。sizeof以字节的形式返回操作数的大小。
- 若操作数具有类型char、unsigned char或signed char,其结果等于1。
- 当操作数是指针时,sizeof依赖于系统的位数。
- 当操作数具有数组类型时,其结果是数组的总字节数。
- 联合类型操作数的sizeof是其最大字节成员的字节数。
- 结构类型操作数的sizeof是这种类型对象的总字节数。
- 如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。
2.21 sizeof与strlen的区别
- sizeof是运算符,计算数据所占的内存空间;strlen()是一个函数,计算字符数组的字符数。
- sizeof可以用类型作参数;strlen()只能用char*作参数,必须是以‘/0’结束。
- 数组做sizeof的参数不退化,传递给strlen就退化为指针了。
- sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。该类型保证能容纳实现建立的最大对象的字节大小。
2.22 空指针指向了内存的什么地方
-
标准并没有对空指针指向内存中的什么地方这一个问题作出规定,一般取决于系统的实现。我们常见的空指针一般指向 0 地址,即空指针的内部用全 0 来表示。空指针的“逻辑地址”一定是0,对于空指针的地址,操作系统是特殊处理的。并非空指针指向一个0地址的物理地址。在实际编程中不需要了解在我们的系统上空指针到底是一个 0指针还是非0地址,我们只需要了解一个指针是否是空指针就可以了——编译器会自动实现其中的转换,为我们屏蔽其中的实现细节。
2.23 有一个char * 型指针刚好指向一些int 型变量, 我想跳过它们。 为什么((int *)p)++; 不行?
- 类型转换的实质“把这些二进制位看作另一种类型, 并作相应的对待”。
- ((int *)p)++是一个转换操作符, 根据定义它只能生成一个右值(rvalue)。
- 而右值既不能赋值, 也不能用++ 自增。
- 正确的做法:p = (char *)((int *)p + 1);。
3. 面向对象知识
3.1 面向对象三个基本特点
- 封装:将客观事物抽象成类,每个类对自身的数据和方法。封装可以使得代码模块化,目的是为了代码重用。
- 继承:子类继承父类的方法和属性,继承可以扩展已存在的代码,目的是为了代码重用。
- 多态:通过继承同一个基类,产生了相关的不同的派生类,与基类中同名的成员函数在不同的派生类中会有不同的实现,也就是说:一个接口、多种方法。
3.2 多态的作用
- 可以隐藏实现的细节,使得代码模块化;方便扩展代码;
- 可以实现接口重用。
3.3 空类默认的成员函数
- 默认构造函数
- 析构函数
- 复制构造函数
- 赋值运算符
3.4 类的成员函数重载、覆盖和隐藏的概念和区别
- 重载是指再同一个作用域内,有几个同名的函数,但是参数列表的个数和类型不同。
- 函数覆盖是指派生类函数覆盖基类函数,函数名、参数类型、返回值类型一模一样。派生类的对象会调用子类中的覆盖版本,覆盖父类中的函数版本。
- 隐藏”是指派生类的函数屏蔽了与其同名的基类函数。
- 覆盖和隐藏的区别:
- 派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。
- 派生类的函数与基类的函数同名,参数也相同。基类函数有virtual,是覆盖,没有virtual就是隐藏。
3.5 基类和子类的构造、析构顺序
- 定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数
- 先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了
3.6 深拷贝与浅拷贝的区别
-
深拷贝意味着拷贝了资源和指针
-
浅拷贝只是拷贝了指针,没有拷贝资源
3.7 构造函数的特点
-
构造函数只在建立对象的时候自动被调用一次
-
构造函数必须是公共的,否则无法生成对象
-
构造函数只负责为自己的类构造对象
3.8 析构函数的特点
- 函数名称固定:~类名( )
- 没有返回类型,没有参数
- 不可以重载,一般由系统自动的调用
3.8 公有继承、私有继承、受保护的继承
- 公有继承时,派生类对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有和受保护成员;公有继承时基类受保护的成员,可以通过派生类对象访问但不能修改。
- 私有继承时,基类的成员只能被直接派生类的成员访问,无法再往下继承。
- 受保护继承时,基类的成员也只被直接派生类的成员访问,无法再往下继承。
3.9 类成员中只能使用构造函数的初始化列表而不能赋值的有哪些
- const成员
- 引用成员
3.10 函数模板与类模板的区别
- 函数模板是模板的一种,可以生成各种类型的函数实例,函数模板的实例化是由编译程序在处理函数调用时自动完成的
- 类模板的实例化必须由程序员在程序中显式地指定。
3.11 引用与多态的关系
- 引用就是对象的别名。
- 引用主要用作函数的形参。
- 引用必须用与该引用同类型的对象初始化: 引用是除指针外另一个可以产生多态效果的手段。
- 一个基类的引用可以指向它的派生类实例。
3.12 static成员变量和static成员函数
- static数据成员独立于该类的任意对象而存在。
- tatic数据成员(const static数据成员除外)在类定义体内声明,必须在类外进行初始化。
- static数据成员定义放在cpp文件中,不能放在初始化列表中。
- static成员函数在类的外部定义。
- Static成员函数没有this形参。
- 可以直接访问所属类的static成员,不能直接使用非static成员。
- 因为static成员不是任何对象的组成部分,所以static成员函数不能被声明为const。
- static成员函数也不能被声明为虚函数。
3.13 static总结
- 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。
- 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问。
- 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内。
- 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
- 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
3.13 const总结
- 欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了。
- 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const。
- 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值。
- 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量。
- 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
4. STL标准库
4.1 STL
- 容器:主要的七种容器 vector,list,deque,map,multimap,set,multiset。
- 算法
- 迭代器
参考文献
[1] c++面试知识,知乎。