- 1. 引用:
- 引用作为别名时,声明时就必须初始化
- 常用作函数得形参,使用引用即使用源数据,而不是其副本=\
- 传递类对象参数得标准方式一般是按引用传递
- 指针是变量,可重新赋值,但引用不行
- 使用时必须初始化,且不能再指向其他变量
- 2. 内联函数与宏定义
- 内联函数:用内联取代宏: 1.内联可调试; 2.可进行类型安全检查或自动类型转换; 3.可访问成员变量。 另外,定义在类声明中的成员函数自动转化为内联函数。
- 宏定义会带来的问题:因为宏是简单地替换,1. 缺括号会出现问题,2. 自增运算符加括号也会多次自增,3. 简单的替换有时候也会出现问题。例子:
#define MAX(a,b) ((a)>(b)?(a):(b))
- 宏只是编译前简单替换代码内容,而函数真正产生代码;
- 宏是编译期的,函数是运行期的;
- 宏不是实体,而函数是一个可寻址的实体;
- 宏没有生存期、作用域之类的概念,而函数就有。
- 内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
- 在内联函数中如果有复杂操作将不被内联。如:循环和递归调用。
- 将简单短小的函数定义为内联函数将会提高效率。
-
3. 默认参数的函数
-
函数中某个参数设置了默认值,则它右边的参数也都要有默认值
-
4. 函数重载
-
5.
extern "C
主要用于能够正确实现 C++ 调用其他 C 语言代码,加上 **extern "C"
**之后,会指示编译器这部分代码按照 C 语言的进行编译,而不是 C++ 的。 -
6. 函数模板和类模板
-
7. 构造函数
-
构造函数的名称和类名相同,通过函数重载,可以创建多个同名的构造函数。
-
构造函数没有声明类型。初始化与构造函数的参数列表相匹配。
-
如果不实现构造函数,编译器会生成一个默认构造函数;如果自己实现,则编译器不会生成。
-
8. 析构函数
-
系统默认的析构函数:如果构造函数没有使用 new,只需编译器生成一个什么都不用做的析构函数即可。
-
如果构造函数使用 new 来分配内存,则必须使用 delete 提供的析构函数释放这些内存。
-
static 数据成员和成员函数
-
static 修饰类中成员,表示类的共享数据
-
static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
-
使用 static 成员变量必须在类外进行初始化。
-
用 static 修饰的成员变量在对象中是不占内存的,因为他不是跟对象一起在堆或者栈中生成,用 static 修饰的变量在静态存储区生成的。
-
由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针的(this 指针是指向本对象的指针)。正因为没有 this 指针,所以 static 类成员函数不能访问非 static 的类成员,只能访问 static 修饰的静态类成员。
-
普通成员函数可以通过对象名进行调用,而 static 成员函数必须通过类名进行调用(因为它与类关联)。
-
9. 动态内存分配
-
new/delete是运算符(关键字),malloc/free是函数调用。
-
10. 拷贝构造函数
-
调用拷贝构造函数的时机:
-
1.定义一个新对象并用一个同类型的对象进行初始化时
-
2.对象作为实参或函数返回对象时
-
定义一个拷贝构造函数的固定形式:
Student(const Student& s);
-
11. 浅拷贝和深拷贝
-
如果不定义拷贝构造函数,则编译器会使用默认拷贝构造函数,是浅拷贝
-
浅拷贝遇到含有指针的对象类型时很容易出问题,因为浅拷贝出来的对象和原对象的指针都指向同一个内存空间,当其中一个对象被析构后,另一个对象再使用时就会发生内存溢出。
-
深拷贝遇到指针时,会重新分配一块空间,因此不会有问题。
-
12. 什么时候需要定义拷贝构造函数
-
1.类数据成员有指针
-
2.类数据成员管理资源(如打开一个文件)
-
3.一个类需要析构函数释放资源时,那它也需要一个拷贝构造函数
-
const 关键字
-
初始化 const 成员变量的唯一方法是使用初始化列表(不能在构造函数的函数体中初始化)。
-
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值, 这种措施主要还是为了保护数据而设置的。const 成员函数也称为常成员函数。
-
常成员函数需要在声明和定义的时候在函数头部的结尾加上 const 关键字
-
在 C++ 中,const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)了。
-
13. 运算符重载
-
运算符重载可以选择使用成员函数或非成员函数来实现。
-
非成员函数应是友元函数,这样才可以直接访问类的私有数据。
-
运算符重载的限制
-
1、为防止用户为标准类型重载运算符,重载后的运算符必须至少有一个是用户自 定义类型的数据。
-
2、不能违反运算符原有的运算规则。
-
3、不能重载不存在的运算符,即不能创建新的运算符
-
4、以下运算符不可重载:
sizeof :sizeof运算符 . :成员运算符 .* :成员指针运算符 :: : 域解析运算符 ? : 条件运算符 typid : RTTI运算符 const_cast、dynamic_cast、reinterpret_cast、static_cast :强制类型转换
-
5、只能用作成员函数重载的运算符:
= :赋值运算符 () :函数调用运算符 [] :下标(索引)运算符 -> :通过指针访问类成员的运算
-
C++ 的运算符重载可以作为成员函数重载,也可以作为非成员函数(友元)重载,两者之间最重要的区别在于重载函数的参数列表的不同,作为成员函数重载的时候会隐式的传递 this 指针,而作为非成员函数进行重载的时候需要显式传递。+ C++ 中 ++、-- 运算符的重载式很特别的存在,它们有独特的格式,分为前自增(自减)和后自增(自减)。为了编译器能够进行区分,引入了一个额外的 int 进行区分,但这个 int 不含有实际意义
-
13. 类之间的关系
-
is-a 继承体现
-
has-a 组合体现
-
继承的意义
-
代码重用
-
体现不同抽象层次
-
公有继承:`class Teacher: public Person
-
子类只能访问父类的public和protected成员,不能访问private成员。
-
在构造一个子类时,父类部分由父类的构造函数完成,子类的部分由子类的构造函数完成。
-
构造一个子类时,先构造父类,然后构造子类,析构时相反。`
-
多态:同样的方法调用而执行不同操作、运行不同代码。
- 14. 虚函数与抽象类
- 简书
- 有虚函数的每个类会产生一个虚函数表,用来存储虚成员函数的指针;
- 带有虚函数的每个类都会有一个指向虚函数表的指针;
- 不再是内敛函数,因为内敛函数可以在编译阶段进行替代,而虚函数表示等待,在运行阶段才能确定到达采用哪种函数,所以虚函数不是内敛函数;
- 在父类中声明为加了
virtual
关键字的函数,在子类中重写时候不需要加virtual
也是虚函数。 - 这时如果sizeof一个类D的对象,会发现比类C的对象大4个字节。多出来的这4个字节就是实现虚函数的关键----虚函数表指针vptr。这个指针指向一张名为“虚函数表”(vtbl)的表,而表中的数据则为函数指针,存储了虚函数fun_b()具体实现所对应的位置。注意,普通函数、虚函数、虚函数表都是同一个类的所有对象公有的,只有成员变量和虚函数表指针是每个对象私有的,sizeof的值也只包括vptr和var所占内存的大小(也是个常出现的问题),并且vptr通常会在对象内存的最起始位置,(通常出于效率考虑,会放在对象的开始地址处)。另外,当类有多个虚函数时,仍然只有一个虚函数表指针vptr,而此时的虚函数表vtbl中会有多个函数指针,分别指向对应的虚函数实现区域。在重复一遍虚函数实现的过程:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。
- 构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区,所以构造函数只能作为普通函数存放在类所指定的代码区中。
- 访问普通成员函数更快,因为普通成员函数的地址在编译阶段就已确定,因此在访问时直接调 用对应地址的函数,而虚函数在调用时,需要首先在虚函数表中寻找虚函数所在地址,因此相比普 通成员函数速度要慢一些。
- 若存在类继承关系并且析构函数中需要析构某些资源时,析构函数需要是虚函数,否则当使用父类指针指向子类对象,在
delete
时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄露等问题。 - 内联函数、构造函数、静态成员函数,友元函数都不能是虚函数,内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;
- 静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的,因此是两个不冲突的概念;因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
- 含有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。
- 定义纯虚函数就是为了让基类不可实例化化,因为实例化这样的抽象数据结构本身并没有意义.或者给出实现也没有意义。
- 纯虚函数是一个在基类中说明的虚函数,它在基类中没有定义,要求任何派生类都定义自己的版本。纯虚函数为各派生类提供一个公共界面。
从基类继承来的纯虚函数,在派生类中仍是虚函数。
- 15. 常量指针与指针常量
- int const* p; //const* 常量指针
- const int* p; //const* 常量指针
- int* const p; //*const 指针常量
- 记忆技巧:const是常量,* 是指针;看这两者的前后顺序,如果是
const *
则是常量指针,如果是* const
则是指针常量。 - 指针常量是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。
- 常量指针是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
- 16. 谈谈多态
- 17. c++代码是如何从源代码到可执行文件的
- 预处理,编译,汇编,和链接
- 预处理:
g++ -E helloworld.cpp -o helloworld.i
,将所以#define
删除,并将宏定义展开。将内联函数也替换进来。处理#include
预编译指令,将被包含的文件插入到该预编译指令的位置。过滤掉所有注释”//”
和“/**/”
里面的内容; - 编译就是将预处理的文件进行一系列的词法分析,语法分析,语义分析,以及优化后产生相应的汇编代码文件,这个过程是程序构建的核心部分,也是最复杂的。执行命令如下:
g++ -S helloworld.i -o helloworld.s
;词法分析,语法分析,语义分析,源代码优化,代码生成和目标代码优化。 - 汇编:这个过程比较简单,就是将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件。
- 把每个源代码独立的编译,然后按照它们的要求将它们组装起来,这个组装模块的过程就是链接,链接的过程包括地址和空间的分配,符号决议,和重定位等这些步骤。
- 18. 讲讲红黑树
- 一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点(叶结点即指树尾端NIL
指针或NULL
结点)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。 - 能保证在最坏情况下,基本的动态几何操作的时间均为
O(lgn)
; - 红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
红黑树能够以 O ( l o g 2 n ) O(log_2 n) O(log2n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。 - 相比于
BST
,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)
的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)
。 - 红黑树的算法时间复杂度和
AVL
相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN)
,所以红黑树应用还是高于AVL
树的. 实际上插入AVL
树和红黑树的速度取决于你所插入的数据。 - 如果你的数据分布较好,则比较宜于采用
AVL
树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的。
1. #include<> 和 #include “” 的区别
#Include <>
会先去编译器环境下查找, 找不到再去系统的环境下查找;#include ""
会先在当前文件查找, 找不到再去编译器环境下查找, 找不到再去系统的环境下查找;
2. struct 和 class 到底有什么区别
- 最主要的区别:默认的读取权限不同。struct 默认是 public, 而 class 默认是 private。
- 定义一个复杂的数据类型时,struct 可以替换成 class,但是,class 可用于声明类模板,而struct 就不可以。
3. 预处理是做什么,编译是什么文件,链接中静态和动态区别?
- 预处理:编译器将
C/C++
程序的头文件编译进来,还有宏的替换内联函数的替换生成xxx.i
文件,g++ -E
指令。声明为inline
的函数也会在编译阶段即被展开成代码。 - 编译:编译就是将预处理的文件进行一系列的词法分析,语法分析,语义分析,以及优化后产生相应的汇编代码文件,这个过程是程序构建的核心部分,也是最复杂的。执行命令如下:
g++ -S helloworld.i -o helloworld.s
;词法分析,语法分析,语义分析,源代码优化,代码生成和目标代码优化。 - 将编译阶段生成的多个文件连接为一个整体文件。
- 静态链接和动态链接的区别:静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时链接
4. .dll 是什么?
- Dynamic Link Library,动态链接库文件。
- 在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。一个应用程序可使用多个DLL文件,一个DLL文件也可能被不同的应用程序使用,这样的DLL文件被称为共享DLL文件。
- DLL文件中存放的是各类程序的函数(子过程)实现过程,当程序需要调用函数时需要先载入DLL,然后取得函数的地址,最后进行调用。使用DLL文件的好处是程序不需要在运行之初加载所有代码,只有在程序需要某个函数的时候才从DLL中取出。另外,使用DLL文件还可以减小程序的体积。
5. .fork系列问题,fork后发生什么,还有fork 3次后总共有多少进程?
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t pid[3];
int count = 0;
pid[0] = fork();
pid[1] = fork();
pid[2] = fork();
printf("this is process\n");
return 0;
}
-
- 首先我们要知道一点,使用fork()函数创建子进程是父进程的一个复制品,子进程执行fork后面的程序,前面的程序不会去执行。
-
- 这里主要说明的是,fork()函数会把父进程当前变量的值及fork()后面的代码段复制给子进程。
- ①:执行到这一句的时候,一个进程被创建了,这个进程与父进程一样,拥有一套与父进程相同的变量,相同的一套代码,这里可以粗浅的理解为子进程又复制了一份main函数。这里返回一个子进程的进程号,大于0。(第一次fork)
- ②:子进程怎么执行:子进程从fork()的位置开始执行,也就是说前面的代码不走,但是拥有之前的变量以及变量的值,与父进程的值一样,这次fork(),返回值是0,所以在子进程里面直接执行了pid==0这一个分支,父进程里面并不执行这个分支的语句。这就为我们在写mian函数的时候怎么写子进程的程序提供了一个方法来隔离代码。
6. mutex和信号量区别,信号量怎么销毁?
- 互斥量用于线程的互斥,信号线用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
- 互斥量值只能为 0/1,信号量值可以为非负整数。也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
-
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
- 互斥量(Mutex):Mutex本质上说就是一把锁,提供对资源的独占访问,所以Mutex主要的作用是用于互斥。Mutex对象的值,只有 0 和 1 两个值。这两个值也分别代表了 Mutex 的两种状态。值为 0, 表示锁定状态,当前对象被锁定,用户进程/线程如果试图 Lock 临界资源,则进入排队等待;值为 1,表示空闲状态,当前对象为空闲,用户进程/线程可以 Lock 临界资源,之后 Mutex 值减 1 变为 0。
- Mutex可以被抽象为四个操作:
- 创建 Create
- 加锁 Lock
- 解锁 Unlock
- 销毁 Destroy
- 信号量:(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
- 信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于 0,则访问被允许,计数器减 1;如果为 0,则访问被禁止,所有试图通过它的线程都将处于等待状态。
7. 怎么创建进程?
- 在
window
系统中,创建一个子进程可以使用CreateProcess
方法实现,#include <windows.h>
+ 在linux系统中,fork
函数
8. 深拷贝和浅拷贝
- 在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。
- 浅拷贝:多个对象共用同一块资源,同一块资源释放多次,崩溃或者内存泄漏;
- 深拷贝:拷贝对象的具体内容,二内存地址是自主分配的,拷贝结束之后俩个对象虽然存的值是一样的,但是内存地址不一样,俩个对象页互相不影响,互不干涉,必须显式提供拷贝构造函数和赋值运算符。
- 以下情况都会调用拷贝构造函数:
- 一个对象以值传递的方式传入函数体;
- 一个对象以值传递的方式从函数返回;
- 一个对象需要通过另外一个对象进行初始化。
9. virtual关键字理解?
- c++中的函数调用默认不适用动态绑定。要触发动态绑定,必须满足两个条件:第一,指定为虚函数,通过关键字
virtual
来声明;第二,通过基类类型的引用或指针调用(多态产生的条件)。由此可见,virtual
主要主要是实现动态绑定。 - 友元函数,构造函数,
static
静态函数不能用virtual
关键字修饰; - 普通成员函数 和析构函数 可以用
virtual
关键字修饰; - 当基类的构造函数有动态申请内存时,也即基类的析构函数有手动释放内存的句子,这时候就必须要将基类的析构函数声明为
virtual
类型,如果基类引用或指针向派生类对象,则删除此指针时,我们希望调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。若使用基类指针操作派生类,需要防止在析构时,只析构基类,而不析构派生类。但是,如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。若是将析构函数声明为虚函数,则可以解决此问题。 - 只要基函数定义了
virtual
,继承类的该函数也就具有virtual
属性; - 纯虚函数
virtual void fun() = 0
,含有(或继续)一个或多个纯虚函数的类是抽象基类,抽象基类不能实例化!继承类只有重写这个接口才能被实例化。 - 派生类可以继承多个基类。问题在于:如果这多个基类又是继承自同一个基类时,那么派生类是不是需要多次继承这“同一个基类”中的内容?虚基类可以解决这个问题。简而言之,虚基类可以使得从多个类(它们继承自一个类)中派生出的对象只继承一个对象。
class mytest:virtual public base{...}
。