C++面试汇总
面向对象的编程的主要思想
和数据封装其中,以提高程序的重用性,灵活性和可扩展性。类是创建对象的模板,一个类可以创建多个对象。对象是类的实例化。
类是抽象的,不占用存储空间;而对象具体的,占用存储空间。
面向对象有三大特性:封装,继承,多态。
1.C++的三大特性为:继承,多态,封装
(1)继承。一个对象直接使用另一个对象的属性和方法。
优点:1.减少重复的代码。
2.继承是多态的前提。
3.继承增加了类的耦合性。
缺点:1.继承在编译时刻就定义了,无法在运行时刻改变父类继承的实现;
2.父类通常至少定义了子类的部分行为,父类的改变都可能影响子类的行为;
3.如果继承下来的子类不适合解决新问题,父类必须重写或替换,那么这种依赖关系就限制了灵活性,最终限制了复用性。
继承的方式有三种分别为公有继承(public),保护继承(protect),私有继承(private)。
虚继承:
为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
class 派生类名:virtual 继承方式 基类名
virtual是关键字,声明该基类为派生类的虚基类。
在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
(2)多态。
C++中有两种多态,称为动多态(运行期多态)和静多态(编译期多态),
多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:
在程序运行时的多态性通过继承和虚函数来体现;
在程序编译时多态性体现在函数和运算符的重载上;
虚函数就是多态的具体表现,虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数如何实现的?
虚函数是通过一张虚函数表实现的,有多少个虚函数,就有多少个指针;在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题;实际上在编译的时候,编译器会自动加上虚表虚函数的作用实现动态联编,也就是说在程序运行阶段动态的选择合适的成员函数,在定义了虚函数之后,可以在基类的派生类中对虚函数重新定义。虚表的使用方法是如果派生类在自己定义中没有修改基类的虚函数,我们就指向基类的虚函数;如果派生类改写了基类的虚函数,这时续表则将原来指向基类的虚函数的地址替换为指向自身虚函数的指针。必须通过基类类型的引用或指针进行函数调用才会发生多态
重载,是指“同一标识符”在同一作用域的不同场合具有不同的语义,这个标识符可以是函数名或运算符
接口的多种不同实现方式即为多态。
优点:1.大大提高了代码的可复用性;
2.提高了了代码的可维护性,可扩充性;
缺点: 1)易读性比较不好,调试比较困难
2)模板只能定义在.h文件中,当工程大了之后,编译时间十分的变态
(3)封装。隐藏对象的属性和实现细节,仅仅对外提供接口和方法。
优点: 1)隔离变化;2)便于使用; 3)提高重用性; 4)提高安全性
缺点: 1)如果封装太多,影响效率; 2)使用者不能知道代码具体实现。
重载(overload)和覆盖(override):
重载:写一个与已有函数同名但是参数表不同的函数;
覆盖:虚函数总是在派生类中被改写。
派生类
派生类的定义格式如下:
class <派生类名>:[继承方式]<基类名1>
[,[继承方式]<基类名2>,...,[继承方式]<基类名n>]
{
<派生类新增的数据成员和成员函数定义>
};
说明:
(1)定义派生类关键字可以是class或者是struct,两者区别是:用class定义派生类,默认的继承方式是private,用struct定义派生类,默认的继承方式为public。新增加的成员默认属性也是class对应private属性,struct对应public属性。
(2)基类不能被派生类继承的两类函数是构造函数和析构函数。
1.C和C++的区别
1、主要区别:
C语言是面向过程的编程,它最重要的特点是函数,通过main函数来调用各个子函数。程序运行的顺序都是程序员事先决定好的。
C++是面向对象的编程,类是它的主要特点,在程序执行过程中,先由主main函数进入,定义一些类,根据需要执行类的成员函数,过程的概念被淡化了,以类驱动程序运行,类就是对象,所以我们称之为面向对象程序设计。面向对象在分析和解决问题的时候,将涉及到的数据和数据的操作封装在类中,通过类可以创建对象,以事件或消息来驱动对象执行处理。
2、联系:c是c++的子集,所以大部c语言程序都可以不加修改的拿到c++下使用。
2.C++和JAVA区别
Java面向对象,没有指针,编写效率高,执行效率较低。
java内存自动回收(GC垃圾回收机制),多线程编程。
JAVA的应用在高层,C++在中间件和底层
c++用析构函数回收垃圾,java自动回收(GC算法)
Java比C++程序可靠性更高
Java语言不需要程序对内存进行分配和回收
Java语言中没有指针的概念,引入了真正的数组
Java用接口(Interface)技术取代C++程序中的多继承性
3. const 有什么用途
主要有三点:
1:定义只读变量,即常量
2:修饰函数的参数和函数的返回值
const可以修饰函数的返回值,参数及,函数的定义体,被const修饰会受到强制的保护,能防止意外的修改,从而提高函数的健壮性。
3: 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不修改成员变量的值。
3. 指针和引用的区别
1:引用是变量的一个别名,内部实现是只读指针
2:引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
3:引用不能为NULL,指针可以为NULL
4:引用变量内存单元保存的是被引用变量的地址
5:“sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小
6:引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
7:引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址
4. C++中有了malloc / free , 为什么还需要 new / delet e
1,malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2,对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
new/delete、malloc/free底层实现原理:
概述:new/delete的底层实现是调用malloc/free函数实现的,而malloc/free的底层实现也不是直接操作内存而是调用系统API实现的。
5.delete与 delete []区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。
6. 编写类String 的构造函数,析构函数,拷贝构造函数和赋值函数
https://blog.csdn.net/zz460833359/article/details/46651401
7. 单链表的逆置
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (head == NULL || head->next == NULL) return head;
ListNode* rHead = reverseList(head->next); // 反转得到新链表的头节点
head->next->next = head; // 当前节点的下一个节点的next指针反转过来
head->next = NULL; // 设置新链表的尾节点
return rHead;
}
}; 8. 堆和栈的区别
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)― 全局变量和静态变量的存储是放在一块的,
初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区―存放函数体的二进制代码。
栈区与堆区的区别:
1)堆和栈中的存储内容:栈存局部变量、函数参数等。堆存储使用new、malloc申请的变量等;
2)申请方式:栈内存由系统分配,堆内存由自己申请;
3)申请后系统的响应:栈——只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆——首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表 中删除,并将该结点的空间分配给程序;
4)申请大小的限制:Windows下栈的大小一般是2M,堆的容量较大;
5)申请效率的比较:栈由系统自动分配,速度较快。堆使用new、malloc等分配,较慢;
总结:栈区优势在处理效率,堆区优势在于灵活;
内存模型:自由区、静态区、动态区;
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即:自由存储区,动态区、静态区。
自由存储区:局部非静态变量的存储区域,即平常所说的栈;
动态区: 用new ,malloc分配的内存,即平常所说的堆;
静态区:全局变量,静态变量,字符串常量存在的位置;
注:代码虽然占内存,但不属于c/c++内存模型的一部分;
13. 头文件种的ifndef/define/endif 是干什么用的
主要用于防止重复定义宏和重复包含头文件
8、动态规划的本质
答:动归,本质上是一种划分子问题的算法,站在任何一个子问题的处理上看,当前子问题的提出都要依据现有的类似结论,而当前问题的结论是后面问题求解的铺垫。任何动态规划都是基于存储的算法,核心是状态转移方程。
析构函数的调用情况
析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。
11、结构与联合有和区别?
(1). 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
14.有哪几种情况只能用intialization list 而不能用assignment?
答案:当类中含有const、reference 成员变量;基类的构造函数都需要初始化表。
15. C++是不是类型安全的?
答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
16. main 函数执行以前,还会执行什么代码?
答案:全局对象的构造函数会在main 函数之前执行。
17. 描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
18.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
答案:
BOOL : if ( !a ) or if(a)
int : if ( a == 0)
float : const EXPRESSION EXP = 0.000001
if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)
19.请说出const与#define 相比,有何优点?
答案:const作用:定义常量、修饰函数参数、修饰函数返回值三个作用。被Const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
20.简述数组与指针的区别?
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
(2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof(p) << endl; // 4 字节
计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4 字节而不是100 字节
}
第21题: int (*s[10])(int) 表示的是什么?
int (*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。
第23题:将程序跳转到指定内存地址
要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?
*((void (*)( ))0x100000 ) ( ); 首先要将0x100000强制转换成函数指针,即: (void (*)())0x100000 然后再调用它: *((void (*)())0x100000)(); 用typedef可以看得更直观些: typedef void(*)() voidFuncPtr; *((voidFuncPtr)0x100000)();
第24题:int id[sizeof(unsigned long)];这个对吗?为什么?
答案:正确 这个 sizeof是编译时运算符,编译时就确定了 ,可以看成和机器有关的常量。
第26题:const 与 #define 的比较,const有什么优点?
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。
(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
第29题:基类的析构函数不是虚函数,会带来什么问题?
【参考答案】派生类的析构函数用不上,会造成资源的泄漏。
第30题:全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
【参考答案】
生命周期不同:
全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
使用方式不同:通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。
操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。
C++类在内存中的空间分配
注意点1:一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址对应具体该类某一个成员变量的地址。
注意点2:类的成员函数不占用栈空间
类本身是不占有内存的,可是 如果类生成实例那么将会在内存中分配一块内存来存储这个类。
文本文件和二进制文件存取
文本文件和二进制文件的定义:
计算机在物理内存上面存放的都是二进制,所以文本文件和二进制文件的主要区别是在逻辑上的而不是物理上的。而从文件的编码方式来看,文件可以分为文本文件和二进制文件。文本文件是基于字符编码的文件,常见的有ASCII、Unicode等,二进制文件是基于值编码的文件,可以看成是变长编码,你可以根据自己的需要,决定多少个比特代表一个值。
文本文件和二进制文件的存储:
二进制文件就是把内存中的数据按其在内存中存储的形式原样输出到磁盘中存放,即存放的是数据的原形式。
文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,即存放的是数据的终端形式
在实际存储中最好是将数据分成字符数据和非字符数据两类:
如果存储的是字符数据,无论采用文本文件还是二进制文件都是没有任何区别的,所以讨论使用文本文件还是二进制文件是没有意义的。
如果存储的是非字符数据,又要看我们使用的情况来决定:
a:如果是需要频繁的保存和访问数据,那么应该采取二进制文件进行存放,这样可以节省存储空间和转换时间。
B:如果需要频繁的向终端显示数据或从终端读入数据,那么应该采用文本文件进行存放,这样可以节省转换时间。
文本文件的打开方式和二进制文件打开方式的区别:
(1)文本模式中回车被当成一个字符'\n',在文件中如果读到0x1B,文本模式会认为这是文件结束符,会按照一定方式对数据做相应的转换。
(2)二进制模式中'\n'会被认为是两个字符0x0D,0x0A;在读到0x1B时,二进制模式不会对文件进行处理。
C和C++中struct的区别
1. C中struct是用户自定义数据类型(UDT);
C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。
2. C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
C++中,struct的成员默认访问说明符为public(为了与C兼容),class中的默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,
3. C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);
C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例。
求出两个长链表交叉的那个结点
方法一
两个没有环的链表如果是相交于某一结点,如上图所示,这个结点后面都是共有的。所以如果两个链表相交,那么两个链表的尾结点的地址也是一样的。程序实现时分别遍历两个单链表,直到尾结点。判断尾结点地址是否相等即可。时间复杂度为O(L1+L2)。
如何找到第一个相交结点?判断是否相交的时候,记录下两个链表的长度,算出长度差len,接着先让较长的链表遍历len个长度,然后两个链表同时遍历,判断是否相等,如果相等,就是第一个相交的结点。
void Is_2List_Intersect(LinkList L1, LinkList L2) {
if (L1 == NULL || L2 == NULL) {
exit(ERROR);
}
LinkList p = L1;
LinkList q = L2;
int L1_length = 0;
int L2_length = 0;
int len = 0;
while (p->next) {
L1_length ++;
p = p->next;
}
while (q->next) {
L2_length ++;
q = q->next;
}
printf("p: = %d\n", p);
printf("q: = %d\n", q);
printf("L1_length: = %d\n", L1_length);
printf("L2_length: = %d\n", L2_length);
if (p == q) {
printf(" 相交\n");
/*p重新指向短的链表 q指向长链表*/
if (L1_length > L2_length) {
len = L1_length - L2_length;
p = L2->next;
q = L1->next;
}
else {
len = L2_length - L1_length;
p = L1->next;
q = L2->next;
}
while (len) {
q = q->next;
len--;
}
while (p != q) {
p = p->next;
q = q->next;
}
printf("相交的第一个结点是:%d\n", p->data );
}
else {
printf("不相交 \n");
}
}
用宏表示一年的秒数
#define SECONDS_PER_YEAR 60*60*24*365(UL)
c程序的执行过程
1.hello程序的生命周期是从一个高级c语言程序开始的,然后为了在系统上运行hello.c程序,每条c语句都必须被其他程序转化为一系列的低级机器语言指令。
2.预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并将它直接插入到程序文本中。结果就得到另一个C程序,通常以.i作为文件扩展名。
3.编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s。它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言为不同编译器提供了通用的输出语言。
4.汇编阶段。汇编器(as)将hello.s翻译成机器语言指令。并将结果保存在目标文件hello.o中。hello.o是一种二进制文件。它的字节编码是机器语言指令而不是字符。
5.连接阶段。hello程序调用printf函数。它是c编译器都会提供的标准c库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器就是负责这种合并的。
c语言中,常见数据类型的字节数
32位编译器
char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long:
4个字节
long long:
8个字节
unsigned long: 4个字节
64位编译器
char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节
c++常见容器,vector容器capacity和size区别,如何动态增长
在Vector容器中有以下几个关于大小的函数
size() 返回容器的大小
empty() 判断容器是否为空
max_size() 返回容器最大的可以存储的元素
capacity() 返回容器当前能够容纳的元素数量
1. 容器的大小一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效。
2.内存的重新配置会很耗时间。
Vector内存扩展方式
vector内存成长方式可归结以下三步曲:
(1)另觅更大空间;
(2)将原数据复制过去;
(3)释放原空间三部曲。
vector遍历有哪几种方式(尽可能多)
for (size_t i =0; i < vec.size(); i ++) {
}
for_each(ivec.begin(),ivec.end(),fun);
cv:Mat 有几种访问方式
初始化
初始化一个Mat文件出来一般有两种形式:
// 1、imread
Mat src = imread("csdn.png");
//2、create
Mat src;
if(src.empty())
{
src.create(Size size, VC_8UC3);
}
访问方式:
at<type>(i,j)访问
例:int ROWS = 100; // height
int COLS = 200; // width
Mat img1(ROWS , COLS , CV_32FC1);
for (int i=0; i<ROWS ; i++)
{
for (int j=0; j<COLS ; j++)
{
img1.at<float>(i,j) = 3.2f;
}
}
方式2: ptr<type>(i) [j] 方式
例:for (int i=0; i<ROWS ; i++)
{
float* pData1=img5.ptr<float>(i);
for (int j=0; j<COLS ; j++)
{
pData1[j] = 3.2f;
}
}
例:方式3:img.data + step[0]*i + step[1]*j 方式
map容器增删改查,和unorder_map区别,map底层如何实现
增
numCountMap.insert({numName,thisAddTime});
numCountMap.insert(make_pair(numName,thisAddTime));
numCountMap.insert(pair<int,int>(numName,thisAddTime));
numCountMap.insert(map<int,int>::value_type(numName,thisAddTime));
遍历
for(const auto &it : numCountMap)
{}
查
map<int,int>::iterator it=numCountMap.find(alterNum);
删
int eraseReturn=numCountMap.erase(1);
修改和访问
int alterNum=3;
map<int,int>::iterator alterit=numCountMap.find(alterNum);
if(alterit!=numCountMap.end())
{
alterit->second=6;
cout<<"alter num 3 occurs 6 time"<<endl;
}
c++智能指针
C++11中引入了智能指针的概念,方便管理堆内存。
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针还有一个作用是把值语义转换成引用语义。
.智能指针的使用
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr
c++14/17新特性
C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、decltype,和模板的大量改进。
C++14的主要特性可以分为三个领域:Lambda函数、constexpr和类型推导。
C++ 17 旨在使 C++ 成为一个不那么臃肿复杂的编程语言,以简化该语言的日常使用,使开发者可以更简单地编写和维护代码。
C++ 17 是对 C++ 语言的重大更新,引入了许多新的语言特性:
UTF-8 字符文字;
折叠表达式 (fold expressions):用于可变的模板;
内联变量 (inline variables):允许在头文件中定义变量;
在 if 和 switch 语句内可以初始化变量;
结构化绑定 (Structured Binding):for (auto [key,value] : my_map) {…};
虚函数和纯虚函数区别
虚函数(impure virtual)
C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。
子类可以重写父类的虚函数实现子类的特殊化。
纯虚函数(pure virtual)
C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
C++中的纯虚函数也是一种“运行时多态”。
虚函数表
模板
函数模板的写法
函数模板的一般形式如下:
Template <class或者也可以用typename T>
返回类型 函数名(形参表)
{//函数定义体 }
说明: template是一个声明模板的关键字,表示声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型可以包含类类型.
函数模板:模板(Templates)使得我们可以生成通用的函数,这些函数能够接受任意数据类型的参数,可返回任意类型的值,而不需要对所有可能的数据类型进行函数重载。这在一定程度上实现了宏(macro)的作用。
类模板:使得一个类可以有基于通用类型的成员,而不需要在类生成的时候定义具体的数据类型
c++继承多态
c++深拷贝浅拷贝
浅拷贝:编译器仅仅拷贝了结构体的值,而没有创建新的内存空间,而是共享同一块内存空间
深拷贝:编译器会为拷贝的对象分配一定的内存空间
构造函数/委托构造函数/拷贝构造函数(深拷贝/浅拷贝)
构造函数——用于初始化对象
函数名与类名相同,不能有返回值类型,可以有形式参数,也可以没有形式参数,可以是inline函数,可以重载,可以带默认参数值。
在对象创建时自动调用。
默认构造函数(default constructor):调用时可以不需要实参的构造函数。有以下两种:
1. 参数表为空的构造函数
2. 全部参数都有默认值的构造函数。
委托构造函数:在一个类中重载多个构造函数时,这些函数只是形参不同,初始化列表不同,而初始化算法和函数体都是相同的。这个时候,为了避免重复,C++11新标准提供了委托构造函数。更重要的是,可以保持代码的一致性,如果以后要修改构造函数的代码,只需要在一处修改即可。
拷贝构造函数
· 拷贝构造函数是一种特殊的构造函数,其形参为本类对象的引用。
· 作用:用一个已经存在的对象去初始化同类型的新对象。
三种典型情况需要调用拷贝构造函数:
1. 定义一个对象时,用本类的另一个对象作为初始值,发生拷贝构造;
2. 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生拷贝构造;
3. 如果函数的返回值是类的对象时,函数执行完成返回主函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生拷贝构造。
https://blog.csdn.net/weixin_39924163/article/details/80171448
emplace_back和push_back区别
emplace_back和push_back都是向容器内添加数据.
对于在容器中添加类的对象时, 相比于push_back,emplace_back可以避免额外类的复制和移动操作.
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a=b;
printf("constructor is called\n");
}
//拷贝构造函数
CExample(const CExample & c)
{
a=c.a;
printf("copy constructor is called\n");
}
//析构函数
~CExample()
{
cout<<"destructor is called\n";
}
void Show()
{
cout<<a<<endl;
}
};
如何实现一个c++的单例模式
内联函数和宏的区别
内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。\
如何实现一个只在堆或者栈上初始化的类
只能建立在堆上
将析构函数设为私有,类对象就无法建立在栈上了。代码如下:
1. class A
2. {
3. public :
4. A(){}
5. void destory(){ delete this ;}
6. private :
7. ~A(){}
8. };
只能建立在栈上
只要禁用new运算符就可以实现类对象只能建立在栈上
指针函数和函数指针
指针函数
int*fun(intx,inty);
函数指针
int (*fun)(intx,inty);
指针数组和数组指针
数组指针 (*p)[n]
数组指针:是指针——指向数组的指针。
指针数组 *p[n]
指针数组:是数组——装着指针的数组。
· define和inline(编译哪个阶段?)
define:定义预编译时处理的宏,只是简单的字符串替换,无类型检查。
inline:1、inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义,编译阶段完成。 2、内联函数要做类型安全检查,inline是指嵌入代码,在调用函数的地方不是跳转,而是把代码直接写到那里去,对于短小的函数来说,inline函数可以得到一定效率的提升,和c时代的宏函数相比,inline函数更加安全可靠,这个是以增加空间的消耗为代价的。
ps:定义内联函数只是给编译器一个建议,但是最后的决定权还是在于编译器,如果函数的逻辑比较复杂(有循环递归之类的),此时则会内联失败。根据google编程规范,内联函数一般在10行以下,而且逻辑简单。
· 返回局部变量?
函数不能返回指向栈内存的指针
原因:返回值是拷贝值,局部变量的作用域为函数内部,函数执行结束,栈上的局部变量会销毁,内存释放。
可返回的局部变量:
1. 返回局部变量本身
2.常量:
3. 静态局部变量
当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。
4. 堆内存中的局部变量
· 子函数返回结构体有什么问题?返回对象调用了哪些函数?
C 语言中函数返回结构体时如果结构体较大, 则在调用函数中产生该结构的临时变量,并将该变量首地址传递给被调用函数,被调用函数返回时根据该地址修改此临时变量的内容,之后在调用函数中再将该变量复制给用户定义的变量,这也正是 C 语言中所谓值传递的工作方式。
如果结构体较小, 则函数返回时所用的临时变量可保存在寄存器中,返回后将寄存器的值复制给用户定义的变量即可。
· volatile干嘛的?
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile用在如下的三个地方:
中断服务程序中修改的供其它程序检测的变量需要加volatile;
多任务环境下各任务间共享的标志应该加volatile;
存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
· 编译器基本原理?
gcc编译器
第一步配置(configure)
编译器在开始工作之前,需要知道当前的系统环境,比如标准库在哪里、软件的安装位置在哪里、需要安装哪些组件等等。这是因为不同计算机的系统环境不一样,通过指定编译参数,编译器就可以灵活适应环境,编译出各种环境都能运行的机器码。这个确定编译参数的步骤,就叫做”配置”(configure)。
这些配置信息保存在一个配置文件之中,约定俗成是一个叫做configure的脚本文件。通常它是由autoconf工具生成的。编译器通过运行这个脚本,获知编译参数。
源码肯定会用到标准库函数(standard library)和头文件(header)。它们可以存放在系统的任意目录中,编译器实际上没办法自动检测它们的位置,只有通过配置文件才能知道。
编译的第二步,就是从配置文件中知道标准库和头文件的位置。一般来说,配置文件会给出一个清单,列出几个具体的目录。等到编译时,编译器就按顺序到这几个目录中,寻找目标。
对于大型项目来说,源码文件之间往往存在依赖关系,编译器需要确定编译的先后顺序。假定A文件依赖于B文件,编译器应该保证做到下面两点。
(1)只有在B文件编译完成后,才开始编译A文件。
(2)当B文件发生变化时,A文件会被重新编译。
编译顺序保存在一个叫做makefile的文件中,里面列出哪个文件先编译,哪个文件后编译。而makefile文件由configure脚本运行生成,这就是为什么编译时configure必须首先运行的原因。
在确定依赖关系的同时,编译器也确定了,编译时会用到哪些头文件。
第四步头文件的预编译(precompilation)
不同的源码文件,可能引用同一个头文件(比如stdio.h)。编译的时候,头文件也必须一起编译。为了节省时间,编译器会在编译源码之前,先编译头文件。这保证了头文件只需编译一次,不必每次用到的时候,都重新编译了。
不过,并不是头文件的所有内容,都会被预编译。用来声明宏的#define命令,就不会被预编译。
预编译完成后,编译器就开始替换掉源码中bash的头文件和宏。以本文开头的那段源码为例,它包含头文件stdio.h,替换后的样子如下。
1. externintfputs(constchar *, FILE *);
2. extern FILE *stdout;
3. intmain(void)
4. {
5. fputs("Hello, world!\n", stdout);
6. return0;
7. }
为了便于阅读,上面代码只截取了头文件中与源码相关的那部分,即fputs和FILE的声明,省略了stdio.h的其他部分(因为它们非常长)。另外,上面代码的头文件没有经过预编译,而实际上,插入源码的是预编译后的结果。编译器在这一步还会移除注释。
这一步称为”预处理”(Preprocessing),因为完成之后,就要开始真正的处理了。
预处理之后,编译器就开始生成机器码。对于某些编译器来说,还存在一个中间步骤,会先把源码转为汇编码(assembly),然后再把汇编码转为机器码。
下面是本文开头的那段源码转成的汇编码。
种转码后的文件称为对象文件(object file)。
对象文件还不能运行,必须进一步转成可执行文件。如果你仔细看上一步的转码结果,会发现其中引用了stdout函数和fwrite函数。也就是说,程序要正常运行,除了上面的代码以外,还必须有stdout和fwrite这两个函数的代码,它们是由C语言的标准库提供的。
编译器的下一步工作,就是把外部函数的代码(通常是后缀名为.lib和.a的文件),添加到可执行文件中。这就叫做连接(linking)。这种通过拷贝,将外部函数库添加到可执行文件的方式,叫做静态连接(static linking),后文会提到还有动态连接(dynamic linking)。
make命令的作用,就是从第四步头文件预编译开始,一直到做完这一步。
第八步安装(Installation)
上一步的连接是在内存中进行的,即编译器在内存中生成了可执行文件。下一步,必须将可执行文件保存到用户事先指定的安装目录。
表面上,这一步很简单,就是将可执行文件(连带相关的数据文件)拷贝过去就行了。但是实际上,这一步还必须完成创建目录、保存文件、设置权限等步骤。这整个的保存过程就称为”安装”(Installation)。
可执行文件安装后,必须以某种方式通知操作系统,让其知道可以使用这个程序了。比如,我们安装了一个文本阅读程序,往往希望双击txt文件,该程序就会自动运行。
这就要求在操作系统中,登记这个程序的元数据:文件名、文件描述、关联后缀名等等。Linux系统中,这些信息通常保存在/usr/share/applications目录下的.desktop文件中。另外,在Windows操作系统中,还需要在Start启动菜单中,建立一个快捷方式。
这些事情就叫做”操作系统连接”。make install命令,就用来完成”安装”和”操作系统连接”这两步。
写到这里,源码编译的整个过程就基本完成了。但是只有很少一部分用户,愿意耐着性子,从头到尾做一遍这个过程。事实上,如果你只有源码可以交给用户,他们会认定你是一个不友好的家伙。大部分用户要的是一个二进制的可执行程序,立刻就能运行。这就要求开发者,将上一步生成的可执行文件,做成可以分发的安装包。
所以,编译器还必须有生成安装包的功能。通常是将可执行文件(连带相关的数据文件),以某种目录结构,保存成压缩文件包,交给用户。
正常情况下,到这一步,程序已经可以运行了。至于运行期间(runtime)发生的事情,与编译器一概无关。但是,开发者可以在编译阶段选择可执行文件连接外部函数库的方式,到底是静态连接(编译时连接),还是动态连接(运行时连接)。所以,最后还要提一下,什么叫做动态连接。
前面已经说过,静态连接就是把外部函数库,拷贝到可执行文件中。这样做的好处是,适用范围比较广,不用担心用户机器缺少某个库文件;缺点是安装包会比较大,而且多个应用程序之间,无法共享库文件。动态连接的做法正好相反,外部函数库不进入安装包,只在运行时动态引用。好处是安装包会比较小,多个应用程序可以共享库文件;缺点是用户必须事先安装好库文件,而且版本和安装位置都必须符合要求,否则就不能正常运行。
现实中,大部分软件采用动态连接,共享库文件。这种动态共享的库文件,Linux平台是后缀名为.so的文件,Windows平台是.dll文件,Mac平台是.dylib文件。
· 数组和指针(重点难在二维)
int **a;
a = new int*[M];
for (int x = 0; x < M; x++)
a[x] = new int[M];
面向对象的编程的主要思想
和数据封装其中,以提高程序的重用性,灵活性和可扩展性。类是创建对象的模板,一个类可以创建多个对象。对象是类的实例化。
类是抽象的,不占用存储空间;而对象具体的,占用存储空间。
面向对象有三大特性:封装,继承,多态。
1.C++的三大特性为:继承,多态,封装
(1)继承。一个对象直接使用另一个对象的属性和方法。
优点:1.减少重复的代码。
2.继承是多态的前提。
3.继承增加了类的耦合性。
缺点:1.继承在编译时刻就定义了,无法在运行时刻改变父类继承的实现;
2.父类通常至少定义了子类的部分行为,父类的改变都可能影响子类的行为;
3.如果继承下来的子类不适合解决新问题,父类必须重写或替换,那么这种依赖关系就限制了灵活性,最终限制了复用性。
继承的方式有三种分别为公有继承(public),保护继承(protect),私有继承(private)。
虚继承:
为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
class 派生类名:virtual 继承方式 基类名
virtual是关键字,声明该基类为派生类的虚基类。
在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
(2)多态。
C++中有两种多态,称为动多态(运行期多态)和静多态(编译期多态),
多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:
在程序运行时的多态性通过继承和虚函数来体现;
在程序编译时多态性体现在函数和运算符的重载上;
虚函数就是多态的具体表现,虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数如何实现的?
虚函数是通过一张虚函数表实现的,有多少个虚函数,就有多少个指针;在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题;实际上在编译的时候,编译器会自动加上虚表虚函数的作用实现动态联编,也就是说在程序运行阶段动态的选择合适的成员函数,在定义了虚函数之后,可以在基类的派生类中对虚函数重新定义。虚表的使用方法是如果派生类在自己定义中没有修改基类的虚函数,我们就指向基类的虚函数;如果派生类改写了基类的虚函数,这时续表则将原来指向基类的虚函数的地址替换为指向自身虚函数的指针。必须通过基类类型的引用或指针进行函数调用才会发生多态
重载,是指“同一标识符”在同一作用域的不同场合具有不同的语义,这个标识符可以是函数名或运算符
接口的多种不同实现方式即为多态。
优点:1.大大提高了代码的可复用性;
2.提高了了代码的可维护性,可扩充性;
缺点: 1)易读性比较不好,调试比较困难
2)模板只能定义在.h文件中,当工程大了之后,编译时间十分的变态
(3)封装。隐藏对象的属性和实现细节,仅仅对外提供接口和方法。
优点: 1)隔离变化;2)便于使用; 3)提高重用性; 4)提高安全性
缺点: 1)如果封装太多,影响效率; 2)使用者不能知道代码具体实现。
重载(overload)和覆盖(override):
重载:写一个与已有函数同名但是参数表不同的函数;
覆盖:虚函数总是在派生类中被改写。
派生类
派生类的定义格式如下:
class <派生类名>:[继承方式]<基类名1>
[,[继承方式]<基类名2>,...,[继承方式]<基类名n>]
{
<派生类新增的数据成员和成员函数定义>
};
说明:
(1)定义派生类关键字可以是class或者是struct,两者区别是:用class定义派生类,默认的继承方式是private,用struct定义派生类,默认的继承方式为public。新增加的成员默认属性也是class对应private属性,struct对应public属性。
(2)基类不能被派生类继承的两类函数是构造函数和析构函数。
1.C和C++的区别
1、主要区别:
C语言是面向过程的编程,它最重要的特点是函数,通过main函数来调用各个子函数。程序运行的顺序都是程序员事先决定好的。
C++是面向对象的编程,类是它的主要特点,在程序执行过程中,先由主main函数进入,定义一些类,根据需要执行类的成员函数,过程的概念被淡化了,以类驱动程序运行,类就是对象,所以我们称之为面向对象程序设计。面向对象在分析和解决问题的时候,将涉及到的数据和数据的操作封装在类中,通过类可以创建对象,以事件或消息来驱动对象执行处理。
2、联系:c是c++的子集,所以大部c语言程序都可以不加修改的拿到c++下使用。
2.C++和JAVA区别
Java面向对象,没有指针,编写效率高,执行效率较低。
java内存自动回收(GC垃圾回收机制),多线程编程。
JAVA的应用在高层,C++在中间件和底层
c++用析构函数回收垃圾,java自动回收(GC算法)
Java比C++程序可靠性更高
Java语言不需要程序对内存进行分配和回收
Java语言中没有指针的概念,引入了真正的数组
Java用接口(Interface)技术取代C++程序中的多继承性
3. const 有什么用途
主要有三点:
1:定义只读变量,即常量
2:修饰函数的参数和函数的返回值
const可以修饰函数的返回值,参数及,函数的定义体,被const修饰会受到强制的保护,能防止意外的修改,从而提高函数的健壮性。
3: 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不修改成员变量的值。
3. 指针和引用的区别
1:引用是变量的一个别名,内部实现是只读指针
2:引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
3:引用不能为NULL,指针可以为NULL
4:引用变量内存单元保存的是被引用变量的地址
5:“sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小
6:引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
7:引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址
4. C++中有了malloc / free , 为什么还需要 new / delet e
1,malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2,对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
new/delete、malloc/free底层实现原理:
概述:new/delete的底层实现是调用malloc/free函数实现的,而malloc/free的底层实现也不是直接操作内存而是调用系统API实现的。
5.delete与 delete []区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。
6. 编写类String 的构造函数,析构函数,拷贝构造函数和赋值函数
https://blog.csdn.net/zz460833359/article/details/46651401
7. 单链表的逆置
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (head == NULL || head->next == NULL) return head;
ListNode* rHead = reverseList(head->next); // 反转得到新链表的头节点
head->next->next = head; // 当前节点的下一个节点的next指针反转过来
head->next = NULL; // 设置新链表的尾节点
return rHead;
}
}; 8. 堆和栈的区别
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)― 全局变量和静态变量的存储是放在一块的,
初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区―存放函数体的二进制代码。
栈区与堆区的区别:
1)堆和栈中的存储内容:栈存局部变量、函数参数等。堆存储使用new、malloc申请的变量等;
2)申请方式:栈内存由系统分配,堆内存由自己申请;
3)申请后系统的响应:栈——只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆——首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表 中删除,并将该结点的空间分配给程序;
4)申请大小的限制:Windows下栈的大小一般是2M,堆的容量较大;
5)申请效率的比较:栈由系统自动分配,速度较快。堆使用new、malloc等分配,较慢;
总结:栈区优势在处理效率,堆区优势在于灵活;
内存模型:自由区、静态区、动态区;
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即:自由存储区,动态区、静态区。
自由存储区:局部非静态变量的存储区域,即平常所说的栈;
动态区: 用new ,malloc分配的内存,即平常所说的堆;
静态区:全局变量,静态变量,字符串常量存在的位置;
注:代码虽然占内存,但不属于c/c++内存模型的一部分;
13. 头文件种的ifndef/define/endif 是干什么用的
主要用于防止重复定义宏和重复包含头文件
8、动态规划的本质
答:动归,本质上是一种划分子问题的算法,站在任何一个子问题的处理上看,当前子问题的提出都要依据现有的类似结论,而当前问题的结论是后面问题求解的铺垫。任何动态规划都是基于存储的算法,核心是状态转移方程。
析构函数的调用情况
析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。
11、结构与联合有和区别?
(1). 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
14.有哪几种情况只能用intialization list 而不能用assignment?
答案:当类中含有const、reference 成员变量;基类的构造函数都需要初始化表。
15. C++是不是类型安全的?
答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
16. main 函数执行以前,还会执行什么代码?
答案:全局对象的构造函数会在main 函数之前执行。
17. 描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
18.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
答案:
BOOL : if ( !a ) or if(a)
int : if ( a == 0)
float : const EXPRESSION EXP = 0.000001
if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)
19.请说出const与#define 相比,有何优点?
答案:const作用:定义常量、修饰函数参数、修饰函数返回值三个作用。被Const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
20.简述数组与指针的区别?
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
(2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof(p) << endl; // 4 字节
计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4 字节而不是100 字节
}
第21题: int (*s[10])(int) 表示的是什么?
int (*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。
第23题:将程序跳转到指定内存地址
要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?
*((void (*)( ))0x100000 ) ( ); 首先要将0x100000强制转换成函数指针,即: (void (*)())0x100000 然后再调用它: *((void (*)())0x100000)(); 用typedef可以看得更直观些: typedef void(*)() voidFuncPtr; *((voidFuncPtr)0x100000)();
第24题:int id[sizeof(unsigned long)];这个对吗?为什么?
答案:正确 这个 sizeof是编译时运算符,编译时就确定了 ,可以看成和机器有关的常量。
第26题:const 与 #define 的比较 ,const有什么优点?
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。
(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
第29题:基类的析构函数不是虚函数,会带来什么问题?
【参考答案】派生类的析构函数用不上,会造成资源的泄漏。
第30题:全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
【参考答案】
生命周期不同:
全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
使用方式不同:通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。
操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。
C++类在内存中的空间分配
注意点1:一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址对应具体该类某一个成员变量的地址。
注意点2:类的成员函数不占用栈空间
类本身是不占有内存的,可是 如果类生成实例那么将会在内存中分配一块内存来存储这个类。
文本文件和二进制文件存取
文本文件和二进制文件的定义:
计算机在物理内存上面存放的都是二进制,所以文本文件和二进制文件的主要区别是在逻辑上的而不是物理上的。而从文件的编码方式来看,文件可以分为文本文件和二进制文件。文本文件是基于字符编码的文件,常见的有ASCII、Unicode等,二进制文件是基于值编码的文件,可以看成是变长编码,你可以根据自己的需要,决定多少个比特代表一个值。
文本文件和二进制文件的存储:
二进制文件就是把内存中的数据按其在内存中存储的形式原样输出到磁盘中存放,即存放的是数据的原形式。
文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,即存放的是数据的终端形式
在实际存储中最好是将数据分成字符数据和非字符数据两类:
如果存储的是字符数据,无论采用文本文件还是二进制文件都是没有任何区别的,所以讨论使用文本文件还是二进制文件是没有意义的。
如果存储的是非字符数据,又要看我们使用的情况来决定:
a:如果是需要频繁的保存和访问数据,那么应该采取二进制文件进行存放,这样可以节省存储空间和转换时间。
B:如果需要频繁的向终端显示数据或从终端读入数据,那么应该采用文本文件进行存放,这样可以节省转换时间。
文本文件的打开方式和二进制文件打开方式的区别:
(1)文本模式中回车被当成一个字符'\n',在文件中如果读到0x1B,文本模式会认为这是文件结束符,会按照一定方式对数据做相应的转换。
(2)二进制模式中'\n'会被认为是两个字符0x0D,0x0A;在读到0x1B时,二进制模式不会对文件进行处理。
C和C++中struct的区别
1. C中struct是用户自定义数据类型(UDT);
C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。
2. C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
C++中,struct的成员默认访问说明符为public(为了与C兼容),class中的默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,
3. C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);
C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例。
求出两个长链表交叉的那个结点
方法一
两个没有环的链表如果是相交于某一结点,如上图所示,这个结点后面都是共有的。所以如果两个链表相交,那么两个链表的尾结点的地址也是一样的。程序实现时分别遍历两个单链表,直到尾结点。判断尾结点地址是否相等即可。时间复杂度为O(L1+L2)。
如何找到第一个相交结点?判断是否相交的时候,记录下两个链表的长度,算出长度差len,接着先让较长的链表遍历len个长度,然后两个链表同时遍历,判断是否相等,如果相等,就是第一个相交的结点。
void Is_2List_Intersect(LinkList L1, LinkList L2) {
if (L1 == NULL || L2 == NULL) {
exit(ERROR);
}
LinkList p = L1;
LinkList q = L2;
int L1_length = 0;
int L2_length = 0;
int len = 0;
while (p->next) {
L1_length ++;
p = p->next;
}
while (q->next) {
L2_length ++;
q = q->next;
}
printf("p: = %d\n", p);
printf("q: = %d\n", q);
printf("L1_length: = %d\n", L1_length);
printf("L2_length: = %d\n", L2_length);
if (p == q) {
printf(" 相交\n");
/*p重新指向短的链表 q指向长链表*/
if (L1_length > L2_length) {
len = L1_length - L2_length;
p = L2->next;
q = L1->next;
}
else {
len = L2_length - L1_length;
p = L1->next;
q = L2->next;
}
while (len) {
q = q->next;
len--;
}
while (p != q) {
p = p->next;
q = q->next;
}
printf("相交的第一个结点是:%d\n", p->data );
}
else {
printf("不相交 \n");
}
}
用宏表示一年的秒数
#define SECONDS_PER_YEAR 60*60*24*365(UL)
c程序的执行过程
1.hello程序的生命周期是从一个高级c语言程序开始的,然后为了在系统上运行hello.c程序,每条c语句都必须被其他程序转化为一系列的低级机器语言指令。
2.预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并将它直接插入到程序文本中。结果就得到另一个C程序,通常以.i作为文件扩展名。
3.编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s。它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言为不同编译器提供了通用的输出语言。
4.汇编阶段。汇编器(as)将hello.s翻译成机器语言指令。并将结果保存在目标文件hello.o中。hello.o是一种二进制文件。它的字节编码是机器语言指令而不是字符。
5.连接阶段。hello程序调用printf函数。它是c编译器都会提供的标准c库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器就是负责这种合并的。
c语言中,常见数据类型的字节数
32位编译器
char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 4个字节
long long: 8个字节
unsigned long: 4个字节
64位编译器
char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节
c++常见容器,vector容器capacity和size区别,如何动态增长
在Vector容器中有以下几个关于大小的函数
size()返回容器的大小
empty()判断容器是否为空
max_size()返回容器最大的可以存储的元素
capacity()返回容器当前能够容纳的元素数量
1. 容器的大小一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效。
2.内存的重新配置会很耗时间。
Vector内存扩展方式
vector内存成长方式可归结以下三步曲:
(1)另觅更大空间;
(2)将原数据复制过去;
(3)释放原空间三部曲。
vector遍历有哪几种方式(尽可能多)
for (size_t i =0; i < vec.size(); i ++) {
}
for_each(ivec.begin(),ivec.end(),fun);
cv:Mat 有几种访问方式
初始化
初始化一个Mat文件出来一般有两种形式:
// 1、imread
Mat src = imread("csdn.png");
//2、create
Mat src;
if(src.empty())
{
src.create(Size size, VC_8UC3);
}
访问方式:
at<type>(i,j)访问
例:int ROWS = 100; // height
int COLS = 200; // width
Mat img1(ROWS , COLS , CV_32FC1);
for (int i=0; i<ROWS ; i++)
{
for (int j=0; j<COLS ; j++)
{
img1.at<float>(i,j) = 3.2f;
}
}
方式2: ptr<type>(i) [j] 方式
例:for (int i=0; i<ROWS ; i++)
{
float* pData1=img5.ptr<float>(i);
for (int j=0; j<COLS ; j++)
{
pData1[j] = 3.2f;
}
}
例:方式3:img.data + step[0]*i + step[1]*j 方式
map容器增删改查,和unorder_map区别,map底层如何实现
增
numCountMap.insert({numName,thisAddTime});
numCountMap.insert(make_pair(numName,thisAddTime));
numCountMap.insert(pair<int,int>(numName,thisAddTime));
numCountMap.insert(map<int,int>::value_type(numName,thisAddTime));
遍历
for(const auto &it : numCountMap)
{}
查
map<int,int>::iterator it=numCountMap.find(alterNum);
删
int eraseReturn=numCountMap.erase(1);
修改和访问
int alterNum=3;
map<int,int>::iterator alterit=numCountMap.find(alterNum);
if(alterit!=numCountMap.end())
{
alterit->second=6;
cout<<"alter num 3 occurs 6 time"<<endl;
}
c++智能指针
C++11中引入了智能指针的概念,方便管理堆内存。
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针还有一个作用是把值语义转换成引用语义。
.智能指针的使用
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr
c++14/17新特性
C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、decltype,和模板的大量改进。
C++14的主要特性可以分为三个领域:Lambda函数、constexpr和类型推导。
C++ 17 旨在使 C++ 成为一个不那么臃肿复杂的编程语言,以简化该语言的日常使用,使开发者可以更简单地编写和维护代码。
C++ 17 是对 C++ 语言的重大更新,引入了许多新的语言特性:
UTF-8 字符文字;
折叠表达式 (fold expressions):用于可变的模板;
内联变量 (inline variables):允许在头文件中定义变量;
在 if 和 switch 语句内可以初始化变量;
结构化绑定 (Structured Binding):for (auto [key,value] : my_map) {…};
虚函数和纯虚函数区别
虚函数(impure virtual)
C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。
子类可以重写父类的虚函数实现子类的特殊化。
纯虚函数(pure virtual)
C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
C++中的纯虚函数也是一种“运行时多态”。
虚函数表
模板
函数模板的写法
函数模板的一般形式如下:
Template <class或者也可以用typename T>
返回类型 函数名(形参表)
{//函数定义体 }
说明: template是一个声明模板的关键字,表示声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型可以包含类类型.
函数模板:模板(Templates)使得我们可以生成通用的函数,这些函数能够接受任意数据类型的参数,可返回任意类型的值,而不需要对所有可能的数据类型进行函数重载。这在一定程度上实现了宏(macro)的作用。
类模板:使得一个类可以有基于通用类型的成员,而不需要在类生成的时候定义具体的数据类型
c++继承多态
c++深拷贝浅拷贝
浅拷贝:编译器仅仅拷贝了结构体的值,而没有创建新的内存空间,而是共享同一块内存空间
深拷贝:编译器会为拷贝的对象分配一定的内存空间
构造函数/委托构造函数/拷贝构造函数(深拷贝/浅拷贝)
构造函数——用于初始化对象
函数名与类名相同,不能有返回值类型,可以有形式参数,也可以没有形式参数,可以是inline函数,可以重载,可以带默认参数值。
在对象创建时自动调用。
默认构造函数(default constructor):调用时可以不需要实参的构造函数。有以下两种:
1. 参数表为空的构造函数
2. 全部参数都有默认值的构造函数。
委托构造函数:在一个类中重载多个构造函数时,这些函数只是形参不同,初始化列表不同,而初始化算法和函数体都是相同的。这个时候,为了避免重复,C++11新标准提供了委托构造函数。更重要的是,可以保持代码的一致性,如果以后要修改构造函数的代码,只需要在一处修改即可。
拷贝构造函数
· 拷贝构造函数是一种特殊的构造函数,其形参为本类对象的引用。
· 作用:用一个已经存在的对象去初始化同类型的新对象。
三种典型情况需要调用拷贝构造函数:
1. 定义一个对象时,用本类的另一个对象作为初始值,发生拷贝构造;
2. 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生拷贝构造;
3. 如果函数的返回值是类的对象时,函数执行完成返回主函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生拷贝构造。
https://blog.csdn.net/weixin_39924163/article/details/80171448
emplace_back和push_back区别
emplace_back和push_back都是向容器内添加数据.
对于在容器中添加类的对象时, 相比于push_back,emplace_back可以避免额hg外类的复制和移动操作.
class CExample
{
private:
int a;
public:
//构造函数
CExample(int b)
{
a=b;
printf("constructor is called\n");
}
//拷贝构造函数
CExample(const CExample & c)
{
a=c.a;
printf("copy constructor is called\n");
}
//析构函数
~CExample()
{
cout<<"destructor is called\n";
}
void Show()
{
cout<<a<<endl;
}
};
如何实现一个c++的单例模式
内联函数和宏的区别
内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
如何实现一个只在堆或者栈上初始化的类
只能建立在堆上
将析构函数设为私有,类对象就无法建立在栈上了。代码如下:
1. class A
2. {
3. public :
4. A(){}
5. void destory(){ delete this ;}
6. private :
7. ~A(){}
8. };
只能建立在栈上
只要禁用new运算符就可以实现类对象只能建立在栈上
指针函数和函数指针
指针函数
int *fun(int x,int y);
函数指针
int (*fun)(int x,int y);
指针数组和数组指针
数组指针 (*p)[n]
数组指针:是指针——指向数组的指针。
指针数组 *p[n]
指针数组:是数组——装着指针的数组。
· define和inline(编译哪个阶段?)
define:定义预编译时处理的宏,只是简单的字符串替换,无类型检查。
inline:1、inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义,编译阶段完成。 2、内联函数要做类型安全检查,inline是指嵌入代码,在调用函数的地方不是跳转,而是把代码直接写到那里去,对于短小的函数来说,inline函数可以得到一定效率的提升,和c时代的宏函数相比,inline函数更加安全可靠,这个是以增加空间的消耗为代价的。
ps:定义内联函数只是给编译器一个建议,但是最后的决定权还是在于编译器,如果函数的逻辑比较复杂(有循环递归之类的),此时则会内联失败。根据google编程规范,内联函数一般在10行以下,而且逻辑简单。
· 返回局部变量?
函数不能返回指向栈内存的指针
原因:返回值是拷贝值,局部变量的作用域为函数内部,函数执行结束,栈上的局部变量会销毁,内存释放。
可返回的局部变量:
1. 返回局部变量本身
2.常量:
3. 静态局部变量
当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。
4. 堆内存中的局部变量
· 子函数返回结构体有什么问题?返回对象调用了哪些函数?
C 语言中函数返回结构体时如果结构体较大, 则在调用函数中产生该结构的临时变量,并将该变量首地址传递给被调用函数,被调用函数返回时根据该地址修改此临时变量的内容,之后在调用函数中再将该变量复制给用户定义的变量,这也正是 C 语言中所谓值传递的工作方式。
如果结构体较小, 则函数返回时所用的临时变量可保存在寄存器中,返回后将寄存器的值复制给用户定义的变量即可。
· volatile干嘛的?
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile用在如下的三个地方:
中断服务程序中修改的供其它程序检测的变量需要加volatile;
多任务环境下各任务间共享的标志应该加volatile;
存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
· 编译器基本原理?
gcc编译器
第一步 配置(configure)
编译器在开始工作之前,需要知道当前的系统环境,比如标准库在哪里、软件的安装位置在哪里、需要安装哪些组件等等。这是因为不同计算机的系统环境不一样,通过指定编译参数,编译器就可以灵活适应环境,编译出各种环境都能运行的机器码。这个确定编译参数的步骤,就叫做”配置”(configure)。
这些配置信息保存在一个配置文件之中,约定俗成是一个叫做configure的脚本文件。通常它是由autoconf工具生成的。编译器通过运行这个脚本,获知编译参数。
第二步 确定标准库和头文件的位置
源码肯定会用到标准库函数(standard library)和头文件(header)。它们可以存放在系统的任意目录中,编译器实际上没办法自动检测它们的位置,只有通过配置文件才能知道。
编译的第二步,就是从配置文件中知道标准库和头文件的位置。一般来说,配置文件会给出一个清单,列出几个具体的目录。等到编译时,编译器就按顺序到这几个目录中,寻找目标。
第三步 确定依赖关系
对于大型项目来说,源码文件之间往往存在依赖关系,编译器需要确定编译的先后顺序。假定A文件依赖于B文件,编译器应该保证做到下面两点。
(1)只有在B文件编译完成后,才开始编译A文件。
(2)当B文件发生变化时,A文件会被重新编译。
编译顺序保存在一个叫做makefile的文件中,里面列出哪个文件先编译,哪个文件后编译。而makefile文件由configure脚本运行生成,这就是为什么编译时configure必须首先运行的原因。
在确定依赖关系的同时,编译器也确定了,编译时会用到哪些头文件。
第四步 头文件的预编译(precompilation)
不同的源码文件,可能引用同一个头文件(比如stdio.h)。编译的时候,头文件也必须一起编译。为了节省时间,编译器会在编译源码之前,先编译头文件。这保证了头文件只需编译一次,不必每次用到的时候,都重新编译了。
不过,并不是头文件的所有内容,都会被预编译。用来声明宏的#define命令,就不会被预编译。
第五步 预处理(Preprocessing)
预编译完成后,编译器就开始替换掉源码中bash的头文件和宏。以本文开头的那段源码为例,它包含头文件stdio.h,替换后的样子如下。
1. extern int fputs(const char *, FILE *);
2. extern FILE *stdout;
3. int main(void)
4. {
5. fputs("Hello, world!\n", stdout);
6. return 0;
7. }
为了便于阅读,上面代码只截取了头文件中与源码相关的那部分,即fputs和FILE的声明,省略了stdio.h的其他部分(因为它们非常长)。另外,上面代码的头文件没有经过预编译,而实际上,插入源码的是预编译后的结果。编译器在这一步还会移除注释。
这一步称为”预处理”(Preprocessing),因为完成之后,就要开始真正的处理了。
第六步 编译(Compilation)
预处理之后,编译器就开始生成机器码。对于某些编译器来说,还存在一个中间步骤,会先把源码转为汇编码(assembly),然后再把汇编码转为机器码。
下面是本文开头的那段源码转成的汇编码。
种转码后的文件称为对象文件(object file)。
第七步 连接(Linking)
对象文件还不能运行,必须进一步转成可执行文件。如果你仔细看上一步的转码结果,会发现其中引用了stdout函数和fwrite函数。也就是说,程序要正常运行,除了上面的代码以外,还必须有stdout和fwrite这两个函数的代码,它们是由C语言的标准库提供的。
编译器的下一步工作,就是把外部函数的代码(通常是后缀名为.lib和.a的文件),添加到可执行文件中。这就叫做连接(linking)。这种通过拷贝,将外部函数库添加到可执行文件的方式,叫做静态连接(static linking),后文会提到还有动态连接(dynamic linking)。
make命令的作用,就是从第四步头文件预编译开始,一直到做完这一步。
第八步 安装(Installation)
上一步的连接是在内存中进行的,即编译器在内存中生成了可执行文件。下一步,必须将可执行文件保存到用户事先指定的安装目录。
表面上,这一步很简单,就是将可执行文件(连带相关的数据文件)拷贝过去就行了。但是实际上,这一步还必须完成创建目录、保存文件、设置权限等步骤。这整个的保存过程就称为”安装”(Installation)。
第九步 操作系统连接
可执行文件安装后,必须以某种方式通知操作系统,让其知道可以使用这个程序了。比如,我们安装了一个文本阅读程序,往往希望双击txt文件,该程序就会自动运行。
这就要求在操作系统中,登记这个程序的元数据:文件名、文件描述、关联后缀名等等。Linux系统中,这些信息通常保存在/usr/share/applications目录下的.desktop文件中。另外,在Windows操作系统中,还需要在Start启动菜单中,建立一个快捷方式。
这些事情就叫做”操作系统连接”。make install命令,就用来完成”安装”和”操作系统连接”这两步。
第十步 生成安装包
写到这里,源码编译的整个过程就基本完成了。但是只有很少一部分用户,愿意耐着性子,从头到尾做一遍这个过程。事实上,如果你只有源码可以交给用户,他们会认定你是一个不友好的家伙。大部分用户要的是一个二进制的可执行程序,立刻就能运行。这就要求开发者,将上一步生成的可执行文件,做成可以分发的安装包。
所以,编译器还必须有生成安装包的功能。通常是将可执行文件(连带相关的数据文件),以某种目录结构,保存成压缩文件包,交给用户。
第十一步 动态连接(Dynamic linking)
正常情况下,到这一步,程序已经可以运行了。至于运行期间(runtime)发生的事情,与编译器一概无关。但是,开发者可以在编译阶段选择可执行文件连接外部函数库的方式,到底是静态连接(编译时连接),还是动态连接(运行时连接)。所以,最后还要提一下,什么叫做动态连接。
前面已经说过,静态连接就是把外部函数库,拷贝到可执行文件中。这样做的好处是,适用范围比较广,不用担心用户机器缺少某个库文件;缺点是安装包会比较大,而且多个应用程序之间,无法共享库文件。动态连接的做法正好相反,外部函数库不进入安装包,只在运行时动态引用。好处是安装包会比较小,多个应用程序可以共享库文件;缺点是用户必须事先安装好库文件,而且版本和安装位置都必须符合要求,否则就不能正常运行。
现实中,大部分软件采用动态连接,共享库文件。这种动态共享的库文件,Linux平台是后缀名为.so的文件,Windows平台是.dll文件,Mac平台是.dylib文件。
· 数组和指针(重点难在二维)
int **a;
a = new int*[M];
for (int x = 0; x < M; x++)
a[x] = new int[M];